Claude 5e2094f2ad
feat(media): local inference sidecar for Media Studio
FastAPI service in infra/media-worker implementing every
MediaInferenceProvider operation behind HTTP API v1, plus capabilities.
Bearer auth, MEDIA_ROOT realpath enforcement, strict request schemas,
cancellation on disconnect, GPU/CPU gates with 503 overflow, input
size/duration limits, lazy SHA256-pinned model downloads, fail-closed
redaction with atomic output, offline pytest suite with fake backends,
CPU/CUDA/ROCm Dockerfile and a license review of every model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
2026-10-03 18:19:47 +00:00

507 lines
18 KiB
Python

"""Model registry: which backend serves each operation, its license, and its files.
Weights are never part of the image or the repo. They download lazily on first
use into MEDIA_MODELS_DIR/<backend id>/ and are verified against the pinned
SHA256 and size before anything loads them. A mismatch deletes the file and
the operation reports `unsupported` with the reason.
Default backends are commercially usable. Anything else must be listed in
MEDIA_OPTIONAL_BACKENDS by the operator before it runs.
"""
from __future__ import annotations
import hashlib
import importlib.util
import logging
import os
import threading
import urllib.request
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from .errors import Unsupported
log = logging.getLogger("media-worker.models")
OPERATIONS = (
"detectScenes",
"transcribe",
"detectFaces",
"trackFaces",
"faceLandmarks",
"embedFrames",
"classifyContent",
"segmentPrompted",
"inpaintVideo",
"redact",
"synthesizeIdentity",
"audioFeatures",
"saliency",
)
HF = "https://huggingface.co"
YUNET_REV = "3cc26e7f1014a5ee5d74a42acee58bafc9d0a310"
SFACE_REV = "3d7082438a6e4551e840c9b2bb60b71e8da4b524"
WHISPER_REV = "536b0662742c02347bc0e980a01041f333bce120"
SIGLIP_REV = "4649052661e53c7000355844105f8a1792088239"
LAMA_REV = "c3c0c9e468934d62e79c329e35d82dd09ff8c444"
SAM2_REV = "de431c4043854a71d8101e17995dfe596bf101a5"
@dataclass(frozen=True)
class ModelFile:
name: str
url: str
sha256: str
sizeBytes: int
@dataclass(frozen=True)
class BackendSpec:
"""One way to serve an operation. `id` doubles as the model cache directory."""
id: str
model: str | None
license: str | None
commercialUse: bool | None
experimental: bool
source: str | None
files: tuple[ModelFile, ...] = ()
# Python modules that must be importable.
requires: tuple[str, ...] = ()
# Set when the backend can never run (e.g. no licensed model exists).
unavailable_reason: str | None = None
@dataclass
class Backend:
spec: BackendSpec
# Builds the runtime object from {file name: verified local path} and the settings.
load: Callable[[dict[str, str], Any], Any] | None = None
def _yunet_file() -> ModelFile:
return ModelFile(
"face_detection_yunet_2023mar.onnx",
f"{HF}/opencv/face_detection_yunet/resolve/{YUNET_REV}/face_detection_yunet_2023mar.onnx",
"8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4",
232589,
)
def _siglip(name: str, sha256: str, size: int) -> ModelFile:
path = name if name == "tokenizer.json" else f"onnx/{name}"
return ModelFile(name, f"{HF}/Xenova/siglip-base-patch16-224/resolve/{SIGLIP_REV}/{path}", sha256, size)
def _whisper(name: str, sha256: str, size: int) -> ModelFile:
return ModelFile(name, f"{HF}/Systran/faster-whisper-small/resolve/{WHISPER_REV}/{name}", sha256, size)
def _lazy(module: str, attr: str) -> Callable[[dict[str, str], Any], Any]:
def load(files: dict[str, str], settings: Any) -> Any:
mod = importlib.import_module(f"app.ops.{module}")
return getattr(mod, attr)(files, settings)
return load
YUNET = BackendSpec(
id="yunet-2023mar",
model="opencv-yunet-2023mar",
license="MIT",
commercialUse=True,
experimental=False,
source="https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet",
files=(_yunet_file(),),
requires=("cv2",),
)
SIGLIP_SOURCE = "https://huggingface.co/google/siglip-base-patch16-224"
SIGLIP_VISION = _siglip(
"vision_model_quantized.onnx",
"ef14a954f3d57e1806666432bd9785004c1dc27100aa260eee0cb0f10a5de058",
99499129,
)
SIGLIP_FULL = _siglip(
"model_quantized.onnx",
"9171eb00c38b9ec82f924877356d008b79e3285dbac7cd10965827bee30c9a99",
210977441,
)
SIGLIP_TOKENIZER = _siglip(
"tokenizer.json",
"4a17c975210be5ab4c36b47d8dae4eefb866dbfb1e676e394aad85dc30a3ae08",
2398744,
)
def default_backends() -> dict[str, Backend]:
return {
"detectScenes": Backend(
BackendSpec(
id="pyscenedetect-adaptive",
model="pyscenedetect-adaptive",
license="BSD-3-Clause",
commercialUse=True,
experimental=False,
source="https://github.com/Breakthrough/PySceneDetect",
requires=("scenedetect", "cv2"),
),
_lazy("scenes", "load_scene_detector"),
),
"transcribe": Backend(
BackendSpec(
id="faster-whisper-small",
model="faster-whisper-small",
license="MIT",
commercialUse=True,
experimental=False,
source="https://huggingface.co/Systran/faster-whisper-small",
files=(
_whisper(
"config.json",
"b55496ac7940a7ae47d2c01eab40edfd8701feec1229d9cce3b40014383fb828",
2370,
),
_whisper(
"model.bin",
"3e305921506d8872816023e4c273e75d2419fb89b24da97b4fe7bce14170d671",
483546902,
),
_whisper(
"tokenizer.json",
"fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab",
2203239,
),
_whisper(
"vocabulary.txt",
"34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913",
459861,
),
),
requires=("faster_whisper",),
),
_lazy("transcribe", "load_transcriber"),
),
"detectFaces": Backend(YUNET, _lazy("faces", "load_face_detector")),
"trackFaces": Backend(
BackendSpec(
id=YUNET.id,
model="opencv-yunet-2023mar+iou-tracker",
license="MIT",
commercialUse=True,
experimental=False,
source=YUNET.source,
files=YUNET.files,
requires=YUNET.requires,
),
_lazy("faces", "load_face_detector"),
),
"faceLandmarks": Backend(
BackendSpec(
id=YUNET.id,
model="opencv-yunet-2023mar-5pt",
license="MIT",
commercialUse=True,
experimental=False,
source=YUNET.source,
files=YUNET.files,
requires=YUNET.requires,
),
_lazy("faces", "load_face_detector"),
),
"embedFrames": Backend(
BackendSpec(
id="siglip-base-patch16-224",
model="siglip-base-patch16-224",
license="Apache-2.0",
commercialUse=True,
experimental=False,
source=SIGLIP_SOURCE,
files=(SIGLIP_VISION,),
requires=("onnxruntime",),
),
_lazy("embeddings", "load_frame_embedder"),
),
"classifyContent": Backend(
BackendSpec(
id="siglip-base-patch16-224",
model="siglip-base-patch16-224-zero-shot",
license="Apache-2.0",
commercialUse=True,
# Zero-shot prompts, not a trained moderation head.
experimental=True,
source=SIGLIP_SOURCE,
files=(SIGLIP_FULL, SIGLIP_TOKENIZER),
requires=("onnxruntime", "tokenizers"),
),
_lazy("embeddings", "load_content_classifier"),
),
"segmentPrompted": Backend(
BackendSpec(
id="sam2.1-hiera-tiny",
model="sam2.1-hiera-tiny",
license="Apache-2.0",
commercialUse=True,
experimental=True,
source="https://github.com/facebookresearch/sam2",
files=(
ModelFile(
"sam2.1_hiera_tiny.pt",
f"{HF}/facebook/sam2.1-hiera-tiny/resolve/{SAM2_REV}/sam2.1_hiera_tiny.pt",
"7402e0d864fa82708a20fbd15bc84245c2f26dff0eb43a4b5b93452deb34be69",
156008466,
),
),
requires=("torch", "sam2"),
),
_lazy("segment", "load_segmenter"),
),
"inpaintVideo": Backend(
BackendSpec(
id="lama-big-onnx",
model="big-lama+dis-flow",
license="Apache-2.0",
commercialUse=True,
experimental=True,
source="https://huggingface.co/Carve/LaMa-ONNX",
files=(
ModelFile(
"lama_fp32.onnx",
f"{HF}/Carve/LaMa-ONNX/resolve/{LAMA_REV}/lama_fp32.onnx",
"1faef5301d78db7dda502fe59966957ec4b79dd64e16f03ed96913c7a4eb68d6",
208044816,
),
),
requires=("onnxruntime", "cv2"),
),
_lazy("inpaint", "load_inpainter"),
),
"redact": Backend(
BackendSpec(
id="opencv-redact",
model="opencv-redact",
license="Apache-2.0",
commercialUse=True,
experimental=False,
source="https://github.com/opencv/opencv",
requires=("cv2",),
),
),
"synthesizeIdentity": Backend(
BackendSpec(
id="synthetic-identity",
model=None,
license=None,
commercialUse=None,
experimental=True,
source=None,
unavailable_reason="no commercially licensed model",
),
),
"audioFeatures": Backend(
BackendSpec(
id="ffmpeg-ebur128-librosa",
model="ffmpeg-ebur128+librosa-beat",
license="ISC",
commercialUse=True,
experimental=False,
source="https://github.com/librosa/librosa",
requires=("librosa",),
),
),
"saliency": Backend(
BackendSpec(
id="spectral-residual",
model="spectral-residual",
license="Apache-2.0",
commercialUse=True,
experimental=False,
source="https://doi.org/10.1109/CVPR.2007.383267",
requires=("cv2",),
),
),
}
# Opt-in backends that are not clean enough to be defaults.
SFACE = Backend(
BackendSpec(
id="sface-2021dec",
model="opencv-sface-2021dec",
license="Apache-2.0 (weights); training data provenance unverified",
commercialUse=None,
experimental=True,
source="https://github.com/opencv/opencv_zoo/tree/main/models/face_recognition_sface",
files=(
ModelFile(
"face_recognition_sface_2021dec.onnx",
f"{HF}/opencv/face_recognition_sface/resolve/{SFACE_REV}/face_recognition_sface_2021dec.onnx",
"0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79",
38696353,
),
),
requires=("cv2",),
),
_lazy("faces", "load_identity_embedder"),
)
OPTIONAL_BACKENDS = {"faceIdentity": SFACE}
def _sha256(path: str) -> str:
digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
def _module_available(name: str) -> bool:
try:
return importlib.util.find_spec(name) is not None
except (ImportError, ValueError):
return False
@dataclass
class Registry:
settings: Any
backends: dict[str, Backend] = field(default_factory=default_backends)
optional: dict[str, Backend] = field(default_factory=lambda: dict(OPTIONAL_BACKENDS))
_runtimes: dict[str, Any] = field(default_factory=dict)
_verified: set[str] = field(default_factory=set)
_failed: dict[str, str] = field(default_factory=dict)
_lock: threading.Lock = field(default_factory=threading.Lock)
_file_locks: dict[str, threading.Lock] = field(default_factory=dict)
# -- availability -------------------------------------------------------
def _license_block(self, spec: BackendSpec, key: str) -> str | None:
if spec.commercialUse is True:
return None
if key in self.settings.optional_backends or spec.id in self.settings.optional_backends:
return None
return "backend is not licensed for commercial use and not enabled in MEDIA_OPTIONAL_BACKENDS"
def unavailable_reason(self, backend: Backend, key: str) -> str | None:
spec = backend.spec
if spec.unavailable_reason:
return spec.unavailable_reason
blocked = self._license_block(spec, key)
if blocked:
return blocked
missing = [name for name in spec.requires if not _module_available(name)]
if missing:
return f"python module not installed: {', '.join(missing)}"
if spec.id in self._failed:
return self._failed[spec.id]
if not self.settings.allow_model_download:
absent = [f.name for f in spec.files if not os.path.isfile(self._file_path(spec, f))]
if absent:
return "model files not installed and MEDIA_MODEL_DOWNLOAD=0"
return None
def backend(self, operation: str) -> Backend:
return self.backends[operation]
def optional_enabled(self, key: str) -> bool:
backend = self.optional.get(key)
return backend is not None and self.unavailable_reason(backend, key) is None
# -- runtime -------------------------------------------------------------
def runtime(self, operation: str) -> Any:
"""Return the loaded runtime for an operation, downloading weights on first use."""
return self._runtime(self.backends[operation], operation)
def optional_runtime(self, key: str) -> Any:
return self._runtime(self.optional[key], key)
def _runtime(self, backend: Backend, key: str) -> Any:
reason = self.unavailable_reason(backend, key)
if reason:
raise Unsupported(reason)
cache_key = f"{backend.spec.id}:{key}"
with self._lock:
if cache_key in self._runtimes:
return self._runtimes[cache_key]
files = {f.name: self.ensure_file(backend.spec, f) for f in backend.spec.files}
runtime = backend.load(files, self.settings) if backend.load else None
with self._lock:
self._runtimes.setdefault(cache_key, runtime)
return self._runtimes[cache_key]
# -- files ---------------------------------------------------------------
def _file_path(self, spec: BackendSpec, model_file: ModelFile) -> str:
return os.path.join(self.settings.models_dir, spec.id, model_file.name)
def _file_lock(self, path: str) -> threading.Lock:
with self._lock:
return self._file_locks.setdefault(path, threading.Lock())
def ensure_file(self, spec: BackendSpec, model_file: ModelFile) -> str:
path = self._file_path(spec, model_file)
with self._file_lock(path):
if path in self._verified:
return path
if not os.path.isfile(path):
if not self.settings.allow_model_download:
raise Unsupported(f"model file {model_file.name} is not installed")
self._download(model_file, path)
ok = os.path.getsize(path) == model_file.sizeBytes and _sha256(path) == model_file.sha256
if not ok:
os.unlink(path)
reason = f"model file {model_file.name} failed SHA256 verification"
self._failed[spec.id] = reason
raise Unsupported(reason)
self._verified.add(path)
return path
def _download(self, model_file: ModelFile, path: str) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
partial = f"{path}.part"
log.info("downloading %s", model_file.url)
try:
with (
urllib.request.urlopen(model_file.url, timeout=60) as response,
open(partial, "wb") as handle,
):
written = 0
for chunk in iter(lambda: response.read(1 << 20), b""):
written += len(chunk)
if written > model_file.sizeBytes:
raise ValueError("download larger than pinned size")
handle.write(chunk)
os.replace(partial, path)
except Exception as error:
if os.path.exists(partial):
os.unlink(partial)
raise Unsupported(f"model file {model_file.name} could not be downloaded: {error}") from error
# -- capabilities ----------------------------------------------------------
def operation_capabilities(self) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
for operation in OPERATIONS:
backend = self.backends[operation]
spec = backend.spec
result[operation] = {
"available": self.unavailable_reason(backend, operation) is None,
"experimental": spec.experimental,
"model": spec.model,
"license": spec.license,
"commercialUse": spec.commercialUse,
}
return result
def prefetch(self) -> None:
"""Download and verify every default backend's files (operator warm-up)."""
for operation in OPERATIONS:
backend = self.backends[operation]
if self.unavailable_reason(backend, operation) is None:
for model_file in backend.spec.files:
self.ensure_file(backend.spec, model_file)