""" Person API — Face identities and sighting trails. See everywhere a face was detected, which cameras, and when. """ from fastapi import APIRouter, Depends, Query, UploadFile, File from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from pydantic import BaseModel from typing import Optional from uuid import UUID from datetime import datetime from app.core.database import get_db from app.models.person import PersonIdentity, FaceEvent router = APIRouter(prefix="/persons", tags=["persons"]) class PersonIdentityResponse(BaseModel): id: UUID name: Optional[str] label: Optional[str] is_registered: int is_watchlisted: int thumbnail_path: Optional[str] first_seen_at: Optional[datetime] last_seen_at: Optional[datetime] total_sightings: int class Config: from_attributes = True @router.get("/identities", response_model=list[PersonIdentityResponse]) async def list_identities( name: Optional[str] = Query(None), limit: int = Query(50, le=200), db: AsyncSession = Depends(get_db), ): q = select(PersonIdentity).order_by(desc(PersonIdentity.last_seen_at)).limit(limit) if name: q = q.where(PersonIdentity.name.ilike(f"%{name}%")) result = await db.execute(q) return result.scalars().all() @router.get("/identities/{identity_id}/trail") async def person_trail(identity_id: UUID, db: AsyncSession = Depends(get_db)): """ Full sighting trail — every camera this face appeared on and when. Groups by camera to show movement pattern. """ result = await db.execute( select(FaceEvent) .where(FaceEvent.identity_id == identity_id) .order_by(desc(FaceEvent.captured_at)) .limit(500) ) events = result.scalars().all() return { "identity_id": str(identity_id), "total_sightings": len(events), "trail": [ { "camera_id": str(e.camera_id), "captured_at": e.captured_at, "similarity_score": e.similarity_score, "face_snapshot_path": e.face_snapshot_path, "snapshot_path": e.snapshot_path, "vehicle_event_id": str(e.vehicle_event_id) if e.vehicle_event_id else None, } for e in events ] } @router.patch("/identities/{identity_id}") async def register_identity( identity_id: UUID, name: Optional[str] = None, label: Optional[str] = None, is_watchlisted: Optional[int] = None, notes: Optional[str] = None, db: AsyncSession = Depends(get_db), ): """Give a name/label to an auto-detected unknown identity.""" result = await db.execute(select(PersonIdentity).where(PersonIdentity.id == identity_id)) identity = result.scalar_one_or_none() if not identity: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Person identity not found") if name is not None: identity.name = name identity.is_registered = 1 if label is not None: identity.label = label if is_watchlisted is not None: identity.is_watchlisted = is_watchlisted await db.commit() return {"message": "Identity registered"} @router.get("/events") async def list_face_events( camera_id: Optional[UUID] = Query(None), identity_id: Optional[UUID] = Query(None), limit: int = Query(100, le=500), db: AsyncSession = Depends(get_db), ): q = select(FaceEvent).order_by(desc(FaceEvent.captured_at)).limit(limit) if camera_id: q = q.where(FaceEvent.camera_id == camera_id) if identity_id: q = q.where(FaceEvent.identity_id == identity_id) result = await db.execute(q) events = result.scalars().all() return [ { "id": str(e.id), "camera_id": str(e.camera_id), "identity_id": str(e.identity_id) if e.identity_id else None, "detection_confidence": e.detection_confidence, "similarity_score": e.similarity_score, "face_snapshot_path": e.face_snapshot_path, "captured_at": e.captured_at, } for e in events ]