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

125 lines
3.1 KiB
Python

"""Request bodies. Field names mirror the TypeScript request objects (camelCase).
Every model forbids unknown fields; that is what keeps a target-face image
out of synthesizeIdentity.
"""
from __future__ import annotations
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, StrictBool, model_validator
Ms = Annotated[int, Field(ge=0, le=7 * 24 * 3600 * 1000)]
Unit = Annotated[float, Field(ge=0.0, le=1.0)]
Path = Annotated[str, Field(min_length=1, max_length=4096)]
SampleFps = Annotated[float, Field(gt=0.0, le=60.0)]
TimesMs = Annotated[list[Ms], Field(max_length=100_000)]
class Strict(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
class NormalizedBox(Strict):
x: Unit
y: Unit
w: Unit
h: Unit
class NormalizedPoint(Strict):
x: Unit
y: Unit
class BoxKeyframe(Strict):
tMs: Ms
box: NormalizedBox
class PathRequest(Strict):
path: Path
class TranscribeRequest(PathRequest):
language: Annotated[str, Field(min_length=2, max_length=16, pattern=r"^[A-Za-z-]+$")] | None = None
class DetectFacesRequest(PathRequest):
sampleFps: SampleFps
timesMs: TimesMs | None = None
class TrackFacesRequest(PathRequest):
sampleFps: SampleFps
class TimesRequest(PathRequest):
timesMs: TimesMs
class ClassifyContentRequest(PathRequest):
sampleFps: SampleFps
extraTimesMs: TimesMs | None = None
class MarkPrompt(Strict):
tMs: Ms
box: NormalizedBox | None = None
point: NormalizedPoint | None = None
@model_validator(mode="after")
def needs_box_or_point(self) -> MarkPrompt:
if self.box is None and self.point is None:
raise ValueError("prompt needs a box or a point")
return self
class SegmentPromptedRequest(PathRequest):
prompt: MarkPrompt
propagate: StrictBool
outDir: Path
class InpaintVideoRequest(PathRequest):
maskDir: Path
outPath: Path
RedactionMode = Literal[
"blur", "pixelate", "solid", "sticker", "partial_eyes", "partial_upper", "partial_lower", "cover"
]
class LandmarkFrame(Strict):
tMs: Ms
points: Annotated[list[NormalizedPoint], Field(max_length=512)]
class RedactionInstruction(Strict):
keyframes: Annotated[list[BoxKeyframe], Field(max_length=200_000)]
mode: RedactionMode
paddingPct: Annotated[float, Field(ge=0.0, le=100.0)]
featherPct: Annotated[float, Field(ge=0.0, le=50.0)]
color: Annotated[str, Field(pattern=r"^#[0-9a-fA-F]{6}$")] | None = None
landmarks: Annotated[list[LandmarkFrame], Field(max_length=200_000)] | None = None
maskDir: Path | None = None
@model_validator(mode="after")
def needs_region(self) -> RedactionInstruction:
if not self.keyframes and not self.maskDir:
raise ValueError("redaction needs keyframes or a maskDir")
return self
class RedactRequest(PathRequest):
redactions: Annotated[list[RedactionInstruction], Field(min_length=1, max_length=1000)]
outPath: Path
class SynthesizeIdentityRequest(PathRequest):
track: Annotated[list[BoxKeyframe], Field(min_length=1, max_length=200_000)]
seed: Annotated[int, Field(ge=-(2**53 - 1), le=2**53 - 1)]
outPath: Path