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
125 lines
3.1 KiB
Python
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
|