Built the Sentinel Tab
This commit is contained in:
142
backend/routers/cctv.py
Normal file
142
backend/routers/cctv.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
backend/routers/cctv.py - CCTV ingestion and auto-mode session linkage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, require_role
|
||||
from backend.db.pool import get_pool
|
||||
from backend.services.auto_mode_matcher import auto_mode_match_session
|
||||
from backend.services.nemoclaw_client import profile_cctv_visitor
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CCTVEventRequest(BaseModel):
|
||||
zone: str
|
||||
session_id: str | None = None
|
||||
license_plate: str | None = None
|
||||
face_description: str | None = None
|
||||
vehicle_description: str | None = None
|
||||
raw_payload: dict[str, Any] = Field(default_factory=dict)
|
||||
captured_at: datetime | None = None
|
||||
|
||||
|
||||
class FinalizeAutoModeRequest(BaseModel):
|
||||
session_id: str
|
||||
|
||||
|
||||
async def _ensure_session(
|
||||
conn: asyncpg.Connection,
|
||||
*,
|
||||
session_id: str | None,
|
||||
) -> None:
|
||||
if not session_id:
|
||||
return
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO perception_sessions (id, session_mode, video_asset_id, auto_mode_evidence)
|
||||
VALUES ($1::uuid, 'auto', 'unknown', '{}'::jsonb)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""",
|
||||
session_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/event", summary="Receive a CCTV frame event from the ONVIF/RTSP bridge")
|
||||
async def ingest_cctv_event(
|
||||
body: CCTVEventRequest,
|
||||
pool: asyncpg.Pool = Depends(get_pool),
|
||||
user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
|
||||
) -> dict[str, Any]:
|
||||
del user
|
||||
profile = await profile_cctv_visitor(
|
||||
license_plate=body.license_plate,
|
||||
zone=body.zone,
|
||||
face_description=body.face_description,
|
||||
vehicle_description=body.vehicle_description,
|
||||
)
|
||||
captured_at = body.captured_at or datetime.now(timezone.utc)
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
await _ensure_session(conn, session_id=body.session_id)
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO cctv_events
|
||||
(zone, license_plate, vehicle_class, wealth_indicator, nemoclaw_tags,
|
||||
nemoclaw_notes, linked_session_id, captured_at, raw_payload)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5::text[], $6, $7::uuid, $8, $9::jsonb)
|
||||
RETURNING id::text
|
||||
""",
|
||||
body.zone,
|
||||
body.license_plate,
|
||||
profile.vehicle_class,
|
||||
profile.wealth_indicator,
|
||||
profile.tags_to_add,
|
||||
profile.notes,
|
||||
body.session_id,
|
||||
captured_at,
|
||||
body.raw_payload,
|
||||
)
|
||||
|
||||
if body.session_id:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE perception_sessions
|
||||
SET auto_mode_evidence = auto_mode_evidence || $1::jsonb
|
||||
WHERE id = $2::uuid
|
||||
""",
|
||||
{
|
||||
"license_plate": body.license_plate,
|
||||
"vehicle_description": body.vehicle_description,
|
||||
"face_description": body.face_description,
|
||||
"vehicle_class": profile.vehicle_class,
|
||||
"wealth_indicator": profile.wealth_indicator,
|
||||
"nemoclaw_tags": profile.tags_to_add,
|
||||
"latest_cctv_event_id": row["id"],
|
||||
},
|
||||
body.session_id,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ingested",
|
||||
"event_id": row["id"],
|
||||
"session_id": body.session_id,
|
||||
"wealth_indicator": profile.wealth_indicator,
|
||||
"vehicle_class": profile.vehicle_class,
|
||||
"tags_to_add": profile.tags_to_add,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/finalize-auto-mode", summary="Match or create a lead after an auto mode session")
|
||||
async def finalize_auto_mode(
|
||||
body: FinalizeAutoModeRequest,
|
||||
pool: asyncpg.Pool = Depends(get_pool),
|
||||
user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
|
||||
) -> dict[str, Any]:
|
||||
del user
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
try:
|
||||
result = await auto_mode_match_session(conn, session_id=body.session_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
return {
|
||||
"status": "matched",
|
||||
"session_id": body.session_id,
|
||||
"lead_id": result.lead_id,
|
||||
"action": result.action,
|
||||
"confidence": result.confidence,
|
||||
"rationale": result.rationale,
|
||||
"tags_applied": result.tags_applied,
|
||||
}
|
||||
Reference in New Issue
Block a user