Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GEMINI_API_KEY=your_gemini_api_key_here
FIREBASE_CREDENTIALS_PATH=path/to/your/firebase-service-account.json
# Optional: set to a specific project ID if needed
# FIREBASE_PROJECT_ID=your-project-id
Binary file added backend/__pycache__/main.cpython-311.pyc
Binary file not shown.
68 changes: 68 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Antigravity — OneEye Backend
FastAPI application entry point.
"""

import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from routers.api import router as api_router

# ---------- Logging ---------- #

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(name)-24s | %(levelname)-7s | %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("antigravity")


# ---------- Lifespan ---------- #

@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("🚀 Antigravity backend starting up...")
yield
logger.info("🛑 Antigravity backend shutting down.")


# ---------- App ---------- #

app = FastAPI(
title="OneEye — Antigravity Backend",
description="Real-time road hazard detection & agentic safety monitor powered by Gemini 2.0 Flash.",
version="0.1.0",
lifespan=lifespan,
)

# CORS — allow the Next.js frontend (adjust origins as needed)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Tighten in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Register routes
app.include_router(api_router)


# ---------- Health ---------- #

@app.get("/")
async def root():
return {
"service": "OneEye Antigravity",
"status": "operational",
"version": "0.1.0",
}


@app.get("/health")
async def health():
return {"status": "ok"}
Empty file added backend/models/__init__.py
Empty file.
Binary file added backend/models/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file not shown.
63 changes: 63 additions & 0 deletions backend/models/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Pydantic models for API requests, responses, and data schemas."""

from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
import uuid


# ---------- Gemini Vision Analysis ---------- #

class AccidentAnalysis(BaseModel):
"""Structured output from Gemini 2.0 Flash frame analysis."""
accident: bool
severity: Optional[str] = Field(None, description="Critical | High | Medium | Low")
type: Optional[str] = Field(None, description="Multi-vehicle | Pedestrian | Hazard")
description: Optional[str] = Field(None, description="Short summary of what happened")
emergency_priority: Optional[int] = Field(None, ge=1, le=10)


class Incident(BaseModel):
"""A confirmed incident stored in Firestore."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
camera_id: str
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
accident: bool = True
severity: str
type: str
description: str
emergency_priority: int
frame_base64: Optional[str] = Field(None, description="Base64 encoded impact frame")


# ---------- Chat API ---------- #

class ChatRequest(BaseModel):
message: str


class ToolCall(BaseModel):
tool_name: str
parameters: dict


class ChatResponse(BaseModel):
action: str = "tool_call"
tool_name: Optional[str] = None
parameters: Optional[dict] = None
text: Optional[str] = None # For plain-text responses


# ---------- Vision Control ---------- #

class VisionStartRequest(BaseModel):
camera_id: str = "cam_01"
video_filename: str = "sample.mp4"
interval_seconds: float = 2.0


class VisionStatus(BaseModel):
camera_id: str
status: str # "running" | "stopped"
loop_id: int = 0
frames_analyzed: int = 0
8 changes: 8 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fastapi==0.115.0
uvicorn[standard]==0.30.0
opencv-python-headless==4.10.0.84
google-genai==1.14.0
firebase-admin==6.6.0
python-dotenv==1.0.1
pydantic==2.9.0
python-multipart==0.0.9
Empty file added backend/routers/__init__.py
Empty file.
Binary file added backend/routers/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added backend/routers/__pycache__/api.cpython-311.pyc
Binary file not shown.
133 changes: 133 additions & 0 deletions backend/routers/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
API Router — All HTTP endpoints for the Antigravity backend.

Endpoints:
POST /api/chat — Agentic chat with Gemini Function Calling
POST /api/start_vision — Start a camera vision loop
POST /api/stop_vision — Stop a camera vision loop
GET /api/status/{cam} — Get status of a camera loop
GET /api/incidents — List incidents (with optional filters)
GET /api/incidents/{id}— Get a single incident by ID
"""

import asyncio
import logging
import os
from typing import Optional

from fastapi import APIRouter, HTTPException

from models.schemas import (
ChatRequest,
ChatResponse,
VisionStartRequest,
VisionStatus,
Incident,
)
from services import gemini_service, vision_service, db_service

logger = logging.getLogger("antigravity.api")
router = APIRouter(prefix="/api")


# ---------- Chat ---------- #

@router.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
"""Send a message to the System Commander agent."""
result = await gemini_service.chat_agent(req.message)
return ChatResponse(**result)


# ---------- Vision Control ---------- #

async def _on_frame_analyzed(camera_id: str, frame_b64: str, loop_id: int) -> bool:
"""
Callback invoked by vision_service for each extracted frame.
Sends the frame to Gemini for analysis. Returns True if accident was detected.
"""
analysis = await gemini_service.analyze_frame(frame_b64)

if analysis.get("accident"):
incident = Incident(
camera_id=camera_id,
severity=analysis.get("severity", "Unknown"),
type=analysis.get("type", "Unknown"),
description=analysis.get("description", "Accident detected."),
emergency_priority=analysis.get("emergency_priority", 5),
frame_base64=frame_b64,
)
await db_service.save_incident(incident.model_dump())
logger.info(f"🚨 Incident saved for {camera_id} — {analysis.get('severity')}")
return True

return False


@router.post("/start_vision", response_model=VisionStatus)
async def start_vision(req: VisionStartRequest):
"""Start the vision processing loop for a camera."""
video_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "videos")
video_path = os.path.join(video_dir, req.video_filename)

if not os.path.isfile(video_path):
raise HTTPException(
status_code=404,
detail=f"Video file '{req.video_filename}' not found in backend/videos/",
)

# Start background task
task = asyncio.create_task(
vision_service.start_camera_loop(
camera_id=req.camera_id,
video_path=video_path,
interval=req.interval_seconds,
on_frame_callback=_on_frame_analyzed,
)
)
vision_service.register_task(req.camera_id, task)

return VisionStatus(
camera_id=req.camera_id,
status="running",
)


@router.post("/stop_vision")
async def stop_vision(camera_id: str = "cam_01"):
"""Stop the vision processing loop for a camera."""
vision_service.stop_camera(camera_id)
return {"camera_id": camera_id, "status": "stopped"}


@router.get("/status/{camera_id}", response_model=VisionStatus)
async def get_status(camera_id: str):
"""Get the current status of a camera feed."""
state = vision_service.get_camera_status(camera_id)
return VisionStatus(camera_id=camera_id, **state)


# ---------- Incidents ---------- #

@router.get("/incidents")
async def list_incidents(
severity: Optional[str] = None,
type: Optional[str] = None,
limit: int = 10,
):
"""List incidents with optional filters."""
incidents = await db_service.get_incidents(
severity=severity,
incident_type=type,
limit=limit,
)
return {"incidents": incidents, "count": len(incidents)}


@router.get("/incidents/{incident_id}")
async def get_incident(incident_id: str):
"""Get full details of a specific incident."""
incident = await db_service.get_incident_by_id(incident_id)
if not incident:
raise HTTPException(status_code=404, detail="Incident not found")
return incident
Empty file added backend/services/__init__.py
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading