| """ |
| FaceIndexProvider — reverse face search via the indexed FaceIndex. |
| |
| This provider integrates the FaceIndex (sqlite-vec + ArcFace embeddings) |
| with the orchestrator's provider pipeline. When the user submits a |
| search request, the orchestrator calls this provider's execute() with |
| a PipelineOutput; the provider extracts the query embedding from the |
| largest detected face and searches the FaceIndex for matches. |
| |
| Note: For direct API usage, prefer the /faces/enroll and /faces/search |
| endpoints which bypass the orchestrator and call FaceIndexService |
| directly. This provider exists so the orchestrator can include |
| reverse-face-search in full-pipeline /jobs runs. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import Any |
|
|
| from config.settings import Settings, settings as _default_settings |
| from models.providers import ProviderCapability |
| from pipeline.feature_extraction import PipelineOutput |
| from providers.base import BaseProvider, ProviderResult |
|
|
|
|
| class FaceIndexProvider(BaseProvider): |
| """Reverse face search via the indexed FaceIndex.""" |
|
|
| name = "face_index" |
| capability = ProviderCapability.REVERSE_FACE_SEARCH |
|
|
| def __init__(self, settings: Settings | None = None) -> None: |
| super().__init__(settings=settings or _default_settings) |
| |
| |
| |
| self._available = bool(getattr(self._settings, "enable_face_index", True)) |
|
|
| def is_available(self) -> bool: |
| return self._available |
|
|
| def _run(self, pipeline_output: PipelineOutput) -> tuple[dict, dict]: |
| """ |
| Search the FaceIndex for matches to the largest face in the image. |
| |
| The FaceIndex instance is NOT available here directly (providers are |
| isolated). Instead, this provider reads the embedding from the |
| pipeline_output (set by InsightFaceProvider if it ran first) and |
| returns the embedding for the orchestrator to use. |
| |
| For direct indexed search, use the /faces/search API endpoint instead. |
| """ |
| if not pipeline_output.face_crops: |
| raw = {"num_query_faces": 0, "matches": [], "note": "No faces detected in query image"} |
| normalized = {"num_query_faces": 0, "matches": []} |
| return raw, normalized |
|
|
| |
| largest = max( |
| pipeline_output.face_crops, |
| key=lambda c: getattr(c, "width", 0) * getattr(c, "height", 1), |
| ) |
| embedding = getattr(largest, "embedding", None) |
|
|
| if embedding is None: |
| raw = { |
| "num_query_faces": len(pipeline_output.face_crops), |
| "matches": [], |
| "note": "No embedding available — InsightFace provider did not run or failed. " |
| "Use the /faces/search API endpoint for direct indexed search.", |
| } |
| normalized = { |
| "num_query_faces": len(pipeline_output.face_crops), |
| "matches": [], |
| } |
| return raw, normalized |
|
|
| |
| |
| |
| import numpy as np |
| if not isinstance(embedding, np.ndarray): |
| embedding = np.array(embedding, dtype=np.float32) |
|
|
| raw = { |
| "num_query_faces": len(pipeline_output.face_crops), |
| "query_embedding_dim": int(embedding.shape[0]), |
| "matches": [], |
| "note": "Query embedding computed. Use /faces/search to search the index.", |
| } |
| normalized = { |
| "num_query_faces": len(pipeline_output.face_crops), |
| "query_embedding": embedding.tolist(), |
| "matches": [], |
| } |
| return raw, normalized |
|
|