feat: replace face pipeline with InsightFace, add content classifier

Face detection/recognition:
- Replace YuNet + SFace with InsightFace buffalo_l (RetinaFace + ArcFace)
- 512-d ArcFace embeddings (was 128-d SFace), migration 0006 resizes column
- Remove YOLO person-bbox workaround — RetinaFace is accurate enough
- Detection threshold 0.65 cleanly separates real faces (0.72+) from
  false positives on dogs/paintings (0.56-0.61)

Content-type classification:
- CLIP zero-shot classifier using native PyTorch text encoder + ONNX
  image encoder for high-quality text-image similarity
- Categories: photograph, screenshot, document, receipt, meme, artwork
- Writes Tag(kind=content_type) per photo via photo_tags
- Margin-based confidence: top-1 vs top-2 score difference
- New ClassifierSettings in config (enabled, min_confidence)
- Wired into vision_fanout pipeline

Tested: 6 real faces from 4 photos (zero false positives), 11/13 photos
classified (8 photograph, 2 artwork, 1 meme).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 13:49:02 +02:00
parent f48e099bd2
commit fa9b21856f
14 changed files with 331 additions and 73 deletions

View File

@@ -15,7 +15,7 @@ import logging
from functools import lru_cache
from app.config import settings
from app.services.vision.base import Embedder, OCREngine, ObjectDetector, FaceProcessor
from app.services.vision.base import Embedder, OCREngine, ObjectDetector, FaceProcessor, ContentClassifier
logger = logging.getLogger(__name__)
@@ -46,6 +46,11 @@ class ModelRegistry:
logger.info("Loading face processor (backend=%s)", self._vision.backend)
return self._load_backend().create_face_processor()
@lru_cache(maxsize=1)
def get_classifier(self) -> ContentClassifier:
logger.info("Loading content classifier (backend=%s)", self._vision.backend)
return self._load_backend().create_classifier()
@lru_cache(maxsize=1)
def _load_backend(self):
"""Import and instantiate the configured backend."""
@@ -70,6 +75,8 @@ class ModelRegistry:
self.get_detector()
if self._vision.faces.enabled:
self.get_face_processor()
if self._vision.classifier.enabled:
self.get_classifier()
logger.info("Vision model warmup complete")