| """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) |
|
|
|
|
| |
| |
| |
|
|
| 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() |
|
|