face-intel / api /routes /faces.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
6.16 kB
"""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()