face-intel / providers /base.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
5.65 kB
"""
Provider Framework — the contract every provider must satisfy.
After the refactor, this module contains ONLY the Protocol, BaseProvider
helper, and ProviderResult dataclass. The registry itself has moved to
`providers/registry.py` so it can be instantiated and injected.
Key design decisions:
1. **Provider Protocol, not ABC** — structural typing, no inheritance
ceremony. A provider is any object with the right methods.
2. **Completely isolated** — a provider only imports its own deps,
never imports from siblings. A failing optional dep (e.g.
`insightface` missing) must not affect other providers.
3. **ProviderResult** carries the raw response (preserved verbatim),
the normalized fields, the latency, and any error. Nothing is
discarded.
4. **Capability flags** declare what a provider can do so the
orchestrator can route queries correctly.
"""
from __future__ import annotations
import time
from dataclasses import dataclass, field
from typing import Any, Optional, Protocol, runtime_checkable
from models.providers import ProviderCapability, ProviderStatus
# --------------------------------------------------------------------------- #
# Result object
# --------------------------------------------------------------------------- #
@dataclass
class ProviderResult:
"""
The contract returned by every provider.
`raw` is preserved verbatim for evidence/audit.
`normalized` is provider-specific cleaned data (boxes, encodings,
image URLs, etc.).
"""
provider: str
capability: ProviderCapability
success: bool
elapsed_ms: float
raw: Any = None
normalized: dict = field(default_factory=dict)
error: Optional[str] = None
error_type: Optional[str] = None
metadata: dict = field(default_factory=dict)
retry_count: int = 0
def to_dict(self) -> dict:
return {
"provider": self.provider,
"capability": self.capability.value,
"success": self.success,
"elapsed_ms": self.elapsed_ms,
"raw": self.raw,
"normalized": self.normalized,
"error": self.error,
"error_type": self.error_type,
"metadata": self.metadata,
"retry_count": self.retry_count,
}
# --------------------------------------------------------------------------- #
# Provider Protocol
# --------------------------------------------------------------------------- #
@runtime_checkable
class Provider(Protocol):
"""
Structural contract for every provider.
A provider class must define:
- name: str
- capability: ProviderCapability
- is_available() -> bool
- execute(input_data: Any) -> ProviderResult
"""
name: str
capability: ProviderCapability
def is_available(self) -> bool: ...
def execute(self, input_data: Any) -> ProviderResult: ...
# --------------------------------------------------------------------------- #
# BaseProvider — optional convenience class
# --------------------------------------------------------------------------- #
class BaseProvider:
"""Convenience base with timing + error handling boilerplate.
Subclasses implement `_run(pipeline_output) -> (raw, normalized)`.
The orchestrator ALWAYS passes a `pipeline.feature_extraction.PipelineOutput`
object. Providers extract what they need:
- detection providers → pipeline_output.image
- recognition providers → pipeline_output.image + pipeline_output.face_crops + pipeline_output.gallery
- scraper providers → pipeline_output.scrape_url (or .image for reverse search)
- reverse-search provs → pipeline_output.image
"""
name: str = "base"
capability: ProviderCapability = ProviderCapability.DETECTION
def __init__(self, settings=None) -> None:
# settings is optional; providers that need tuning knobs store it.
self._settings = settings
def is_available(self) -> bool:
return True
def execute(self, input_data: Any) -> ProviderResult:
return self._safe_execute(input_data)
# ------------------------------------------------------------------ #
# Subclass hook
# ------------------------------------------------------------------ #
def _run(self, input_data: Any) -> tuple[Any, dict]:
raise NotImplementedError
def _safe_execute(self, input_data: Any) -> ProviderResult:
t0 = time.perf_counter()
try:
if not self.is_available():
return ProviderResult(
provider=self.name,
capability=self.capability,
success=False,
elapsed_ms=0.0,
error="Provider not available (disabled or missing dependencies)",
error_type="NotConfigured",
)
raw, normalized = self._run(input_data)
elapsed = (time.perf_counter() - t0) * 1000.0
return ProviderResult(
provider=self.name,
capability=self.capability,
success=True,
elapsed_ms=round(elapsed, 3),
raw=raw,
normalized=normalized,
)
except Exception as e:
elapsed = (time.perf_counter() - t0) * 1000.0
return ProviderResult(
provider=self.name,
capability=self.capability,
success=False,
elapsed_ms=round(elapsed, 3),
error=str(e),
error_type=type(e).__name__,
)