face-intel / providers /reverse /face_index_provider.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
3.97 kB
"""
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)
# The FaceIndex instance is injected at runtime by the orchestrator
# via the pipeline_output's metadata, OR looked up lazily.
# For now, we mark availability based on the enable flag.
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
# Use the largest face
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
# The actual index search is performed by the FaceIndexService
# (called from the API layer). This provider just signals that
# the query embedding is ready.
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