""" FaceService — InsightFace-based face detection, embedding, and identity matching. Uses pgvector cosine similarity for fast 1:N identity search. """ import cv2 import numpy as np import logging from typing import Optional from dataclasses import dataclass, field from uuid import UUID from app.core.config import settings logger = logging.getLogger(__name__) @dataclass class FaceDetection: bbox: dict = field(default_factory=dict) # {"x","y","w","h"} embedding: Optional[np.ndarray] = None # 512-dim vector detection_confidence: float = 0.0 face_crop: Optional[np.ndarray] = None matched_identity_id: Optional[UUID] = None similarity_score: float = 0.0 class FaceService: def __init__(self): self._app = None self._ready = False def load(self): """Load InsightFace model — call once at startup.""" try: from insightface.app import FaceAnalysis self._app = FaceAnalysis( name=settings.FACE_MODEL, allowed_modules=["detection", "recognition"], ) self._app.prepare( ctx_id=settings.GPU_DEVICE, det_size=(640, 640), ) self._ready = True logger.info(f"InsightFace loaded (model={settings.FACE_MODEL})") except Exception as e: logger.error(f"Failed to load InsightFace: {e}") def detect(self, frame: np.ndarray) -> list[FaceDetection]: """ Detect all faces in a frame and extract embeddings. Returns list of FaceDetection objects. """ if not self._ready or self._app is None: return [] try: faces = self._app.get(frame) except Exception as e: logger.debug(f"Face detection error: {e}") return [] detections = [] for face in faces: bbox = face.bbox.astype(int) x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3] det = FaceDetection( bbox={"x": x1, "y": y1, "w": x2 - x1, "h": y2 - y1}, embedding=face.normed_embedding, # Already L2-normalized detection_confidence=float(face.det_score), face_crop=frame[max(0, y1):y2, max(0, x1):x2], ) detections.append(det) return detections def cosine_similarity(self, emb1: np.ndarray, emb2: np.ndarray) -> float: """Cosine similarity between two normalized embeddings.""" return float(np.dot(emb1, emb2)) def embeddings_to_list(self, embedding: np.ndarray) -> list[float]: """Convert numpy embedding to list for pgvector storage.""" return embedding.tolist() # Singleton face_service = FaceService()