"""Face routes — detect / recognize / intelligence / index endpoints.""" from __future__ import annotations from typing import List, Optional from fastapi import APIRouter, Depends, File, Form, UploadFile from pydantic import BaseModel from api.deps import ( get_detection_service, get_recognition_service, get_face_intelligence_service, get_face_index_service, ) from models.jobs import JobKind, JobRequest from services.detection_service import DetectionService from services.recognition_service import RecognitionService from services.face_intelligence_service import FaceIntelligenceService from services.face_index_service import FaceIndexService router = APIRouter() class FaceRequest(BaseModel): image_url: Optional[str] = None image_base64: Optional[str] = None providers: List[str] = [] @router.post("/detect") async def detect_faces(req: FaceRequest, svc: DetectionService = Depends(get_detection_service)): job_req = JobRequest( kind=JobKind.DETECTION, image_url=req.image_url, image_base64=req.image_base64, providers=req.providers, ) return await svc.detect(job_req) @router.post("/recognize") async def recognize_faces(req: FaceRequest, svc: RecognitionService = Depends(get_recognition_service)): job_req = JobRequest( kind=JobKind.RECOGNITION, image_url=req.image_url, image_base64=req.image_base64, providers=req.providers, ) return await svc.recognize(job_req) @router.post("/intelligence") async def face_intelligence( req: FaceRequest, svc: FaceIntelligenceService = Depends(get_face_intelligence_service), ): """Run face intelligence: quality, blur, pose, best-face, clustering.""" job_req = JobRequest( kind=JobKind.DETECTION, image_url=req.image_url, image_base64=req.image_base64, providers=req.providers, ) return await svc.analyze(job_req) @router.get("/gallery") async def list_gallery(svc: RecognitionService = Depends(get_recognition_service)): return {"persons": svc.list_known_persons()} @router.delete("/gallery/{name}") async def remove_from_gallery(name: str, svc: RecognitionService = Depends(get_recognition_service)): return svc.remove_known_person(name) # --------------------------------------------------------------------------- # # Reverse Face Search (PimEyes-style indexed search) # --------------------------------------------------------------------------- # class EnrollURLRequest(BaseModel): image_url: str name: Optional[str] = None source_url: Optional[str] = None metadata: Optional[dict] = None class SearchURLRequest(BaseModel): image_url: str top_k: Optional[int] = None threshold: Optional[float] = None @router.post("/enroll") async def enroll_face( image: UploadFile = File(...), name: Optional[str] = Form(None), source_url: Optional[str] = Form(None), metadata: Optional[str] = Form(None), svc: FaceIndexService = Depends(get_face_index_service), ): """ Enroll a face into the searchable reverse-search index. Upload an image (multipart/form-data) with optional name, source_url, and metadata (JSON string). The largest face in the image will be detected, embedded (512-d ArcFace), and added to the index. """ import json image_bytes = await image.read() md = json.loads(metadata) if metadata else None return await svc.enroll( image_bytes=image_bytes, name=name, source_url=source_url, metadata=md, ) @router.post("/enroll/url") async def enroll_face_url( req: EnrollURLRequest, svc: FaceIndexService = Depends(get_face_index_service), ): """Enroll a face from a URL (alternative to multipart upload).""" return await svc.enroll( image_url=req.image_url, name=req.name, source_url=req.source_url, metadata=req.metadata, ) @router.post("/search") async def search_faces( image: UploadFile = File(...), top_k: Optional[int] = Form(None), threshold: Optional[float] = Form(None), svc: FaceIndexService = Depends(get_face_index_service), ): """ Search the index for faces matching the uploaded image. Returns up to `top_k` matches with similarity >= `threshold`. Each match includes the face_id, name, source_url, metadata, and similarity score (0-1, higher is better). """ image_bytes = await image.read() return await svc.search( image_bytes=image_bytes, top_k=top_k, threshold=threshold, ) @router.post("/search/url") async def search_faces_url( req: SearchURLRequest, svc: FaceIndexService = Depends(get_face_index_service), ): """Search by image URL (alternative to multipart upload).""" return await svc.search( image_url=req.image_url, top_k=req.top_k, threshold=req.threshold, ) @router.get("/list") async def list_enrolled_faces( limit: int = 50, offset: int = 0, name: Optional[str] = None, svc: FaceIndexService = Depends(get_face_index_service), ): """Paginated list of enrolled faces in the index.""" return {"faces": svc.list(limit=limit, offset=offset, name=name)} @router.get("/{face_id}") async def get_face( face_id: str, svc: FaceIndexService = Depends(get_face_index_service), ): """Get details of a specific enrolled face.""" face = svc.get(face_id) if face is None: return {"success": False, "error": "Face not found", "face_id": face_id} return {"success": True, "face": face} @router.delete("/{face_id}") async def delete_face( face_id: str, svc: FaceIndexService = Depends(get_face_index_service), ): """Remove a face from the index.""" deleted = svc.delete(face_id) return {"success": deleted, "face_id": face_id, "message": "Deleted" if deleted else "Not found"} @router.get("/stats") async def face_index_stats( svc: FaceIndexService = Depends(get_face_index_service), ): """Index statistics: total count, recent enrollments, etc.""" return svc.stats()