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

225 lines
5.9 KiB
Python

"""Shared fixtures. All media is synthetic (ffmpeg testsrc/sine, drawn shapes)."""
from __future__ import annotations
import subprocess
from collections.abc import Callable
from pathlib import Path
import cv2
import numpy as np
import pytest
import soundfile
from fastapi.testclient import TestClient
from app.config import Settings
from app.main import create_app
from app.models import Backend, Registry, default_backends
from .fakes import fake_backends
TOKEN = "test-token-" + "x" * 32
AUTH = {"Authorization": f"Bearer {TOKEN}"}
def ffmpeg(*args: str) -> None:
subprocess.run(["ffmpeg", "-v", "error", "-y", *args], check=True)
def write_video(path: Path, frames: list[np.ndarray], fps: int = 25) -> None:
height, width = frames[0].shape[:2]
proc = subprocess.Popen(
[
"ffmpeg",
"-v",
"error",
"-y",
"-f",
"rawvideo",
"-pix_fmt",
"bgr24",
"-s",
f"{width}x{height}",
"-r",
str(fps),
"-i",
"pipe:0",
"-c:v",
"libx264",
"-crf",
"10",
"-pix_fmt",
"yuv420p",
str(path),
],
stdin=subprocess.PIPE,
)
assert proc.stdin is not None
for frame in frames:
proc.stdin.write(frame.tobytes())
proc.stdin.close()
assert proc.wait() == 0
def decode(path: Path) -> np.ndarray:
probe = (
subprocess.run(
[
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=p=0",
str(path),
],
check=True,
capture_output=True,
text=True,
)
.stdout.strip()
.split(",")
)
width, height = int(probe[0]), int(probe[1])
raw = subprocess.run(
[
"ffmpeg",
"-v",
"error",
"-i",
str(path),
"-map",
"0:v:0",
"-fps_mode",
"passthrough",
"-f",
"rawvideo",
"-pix_fmt",
"bgr24",
"pipe:1",
],
check=True,
capture_output=True,
).stdout
return np.frombuffer(raw, np.uint8).reshape(-1, height, width, 3)
@pytest.fixture(scope="session")
def media_root(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("media")
ffmpeg(
"-f",
"lavfi",
"-i",
"testsrc=size=320x240:rate=25:duration=2",
"-f",
"lavfi",
"-i",
"sine=frequency=440:duration=2",
"-c:v",
"libx264",
"-crf",
"10",
"-pix_fmt",
"yuv420p",
"-c:a",
"aac",
"-shortest",
str(root / "testsrc.mp4"),
)
ffmpeg(
"-f",
"lavfi",
"-i",
"testsrc=size=320x240:rate=25:duration=2",
"-f",
"lavfi",
"-i",
"mandelbrot=size=320x240:rate=25",
"-filter_complex",
"[1:v]trim=duration=2,setpts=PTS-STARTPTS[b];[0:v][b]concat=n=2:v=1:a=0",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
str(root / "cut.mp4"),
)
# Seeded 4-px block noise with a tone: lots of local detail for blur/pixelate checks.
rng = np.random.default_rng(7)
noise = [
np.kron(rng.integers(0, 256, (60, 80, 3), dtype=np.uint8), np.ones((4, 4, 1), np.uint8))
for _ in range(25)
]
write_video(root / "noise-silent.mp4", noise)
ffmpeg(
"-i",
str(root / "noise-silent.mp4"),
"-f",
"lavfi",
"-i",
"sine=frequency=330:duration=1",
"-c:v",
"copy",
"-c:a",
"aac",
"-shortest",
str(root / "noise.mp4"),
)
# A white disc moving left to right on black: the synthetic "face"/"mark".
frames = []
for i in range(40):
frame = np.zeros((240, 320, 3), np.uint8)
cv2.circle(frame, (60 + i * 5, 120), 25, (255, 255, 255), -1)
frames.append(frame)
write_video(root / "disc.mp4", frames)
# A bright square in the right half for saliency.
frame = np.full((240, 320, 3), 40, np.uint8)
cv2.rectangle(frame, (240, 100), (280, 140), (255, 255, 255), -1)
write_video(root / "spot.mp4", [frame] * 10)
# 120 BPM click track (8 s) followed by 1.5 s of silence.
sr = 22050
clicks = np.zeros(int(sr * 9.5), np.float32)
for beat in range(16):
start = int(beat * 0.5 * sr)
t = np.arange(int(0.03 * sr)) / sr
clicks[start : start + t.size] += 0.8 * np.sin(2 * np.pi * 1000 * t) * np.exp(-t * 80)
soundfile.write(str(root / "clicks.wav"), clicks, sr)
return root
def make_settings(media_root: Path, tmp_path: Path, **overrides: object) -> Settings:
values: dict[str, object] = {
"token": TOKEN,
"media_root": str(media_root),
"models_dir": str(tmp_path / "models"),
"allow_model_download": False,
}
values.update(overrides)
return Settings(**values) # type: ignore[arg-type]
ClientFactory = Callable[..., TestClient]
@pytest.fixture
def make_client(media_root: Path, tmp_path: Path) -> ClientFactory:
def make(
backends: dict[str, Backend] | None = None, fakes: bool = True, **overrides: object
) -> TestClient:
settings = make_settings(media_root, tmp_path, **overrides)
merged = default_backends()
if fakes:
merged.update(fake_backends())
merged.update(backends or {})
return TestClient(create_app(settings, Registry(settings, backends=merged)))
return make
@pytest.fixture
def client(make_client: ClientFactory) -> TestClient:
return make_client()