refactor: strip AI pipeline to binary photo/other classifier

Drops face recognition, OCR, object detection, and semantic embeddings.
The sole remaining vision task is a CLIP-based binary classifier
(photography vs other); photos in "other" get needs_review=true so
screenshots, documents, memes and scans can be triaged from a new
filter pill in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 22:27:17 +02:00
parent 5c531f11da
commit 574d71371f
50 changed files with 700 additions and 3068 deletions

View File

@@ -1,84 +1,29 @@
"""
ModelRegistry — singleton that lazy-loads vision models per worker process.
Usage from Celery tasks:
from app.services.vision.registry import registry
embedder = registry.get_embedder()
vec = embedder.embed_image(img)
Models are created on first access and cached for the worker's lifetime.
The registry reads settings.vision to decide which backend to use and
where model weights live.
ModelRegistry — lazy-loads the single content classifier per worker.
"""
import logging
from functools import lru_cache
from app.config import settings
from app.services.vision.base import Embedder, OCREngine, ObjectDetector, FaceProcessor, ContentClassifier
from app.services.vision.base import ContentClassifier
logger = logging.getLogger(__name__)
class ModelRegistry:
"""Central access point for all vision models."""
def __init__(self):
self._vision = settings.vision
@lru_cache(maxsize=1)
def get_embedder(self) -> Embedder:
logger.info("Loading embedder: %s (backend=%s)", self._vision.embedder.name, self._vision.backend)
return self._load_backend().create_embedder()
@lru_cache(maxsize=1)
def get_ocr(self) -> OCREngine:
logger.info("Loading OCR engine (backend=%s)", self._vision.backend)
return self._load_backend().create_ocr()
@lru_cache(maxsize=1)
def get_detector(self) -> ObjectDetector:
logger.info("Loading object detector (backend=%s)", self._vision.backend)
return self._load_backend().create_detector()
@lru_cache(maxsize=1)
def get_face_processor(self) -> FaceProcessor:
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."""
backend_name = self._vision.backend
if backend_name == "onnx":
from app.services.vision.onnx_backend import ONNXBackend
return ONNXBackend(self._vision)
elif backend_name == "rocm":
from app.services.vision.rocm_backend import ROCmBackend
return ROCmBackend(self._vision)
else:
raise ValueError(f"Unknown vision backend: {backend_name}")
from app.services.vision.classify import CLIPContentClassifier
return CLIPContentClassifier(self._vision)
def warmup(self):
"""Pre-load all enabled models. Called from Celery worker_process_init
on the vision queue to avoid cold-start latency on the first task."""
logger.info("Warming up vision models...")
self.get_embedder()
if self._vision.ocr.enabled:
self.get_ocr()
if self._vision.detector.enabled:
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")
logger.info("Warming up vision classifier...")
self.get_classifier()
logger.info("Vision warmup complete")
# Module-level singleton. Import this from tasks.
registry = ModelRegistry()