""" API schemas (v1). These are the public contract for the /v1/scan/image endpoint. Treat any change to a field name, type, or required-ness as a breaking change. Forward-compatibility notes --------------------------- • `probabilities` is a fixed 4-class vector. Stage 1 only meaningfully populates `authentic` and `ai_generated`; `deepfake` and `edited` start at 0.0 until the corresponding detectors come online in Stage 2. Clients should treat 0.0 as "not assessed" for an unsigned-classifier slot — but the field is always present so the schema itself never changes. • `signals` is a list, currently with a single entry for the CLIP detector. More detectors (frequency, face-swap) append to this list in later stages. • `provenance` is always present. When C2PA is disabled or unavailable, `c2pa_present` is False and `c2pa_valid` is None. """ from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field # The 4-class taxonomy. New classes must NOT be added without bumping API to v2. Verdict = Literal["authentic", "ai_generated", "deepfake", "edited", "uncertain"] class Probabilities(BaseModel): """Probability mass over the 4 mutually-exclusive classes. Sums to 1.0 (allowing for floating-point rounding within ±1e-3). """ authentic: float = Field(ge=0.0, le=1.0) ai_generated: float = Field(ge=0.0, le=1.0) deepfake: float = Field(ge=0.0, le=1.0) edited: float = Field(ge=0.0, le=1.0) class DetectorSignal(BaseModel): """Per-detector contribution, surfaced for transparency / debugging. `score` is the detector's own internal "fakeness" estimate in [0, 1]; its meaning depends on the detector. Aggregation into `probabilities` happens in the ensemble layer. """ name: str score: float = Field(ge=0.0, le=1.0) notes: str | None = None class Provenance(BaseModel): """C2PA / Content Credentials check. A trustworthy C2PA manifest from a known camera or generator can short- circuit the model — those signals are currently advisory only. """ c2pa_present: bool c2pa_valid: bool | None = None # None → not checked / unverifiable issuer: str | None = None # e.g. "Sony", "OpenAI", "Adobe" claim_generator: str | None = None # raw claim_generator string from the manifest class ScanResponse(BaseModel): """The /v1/scan/image response. This is the public contract.""" verdict: Verdict confidence: float = Field(ge=0.0, le=1.0) probabilities: Probabilities signals: list[DetectorSignal] provenance: Provenance model_version: str scan_id: str latency_ms: float class ErrorResponse(BaseModel): """Uniform error shape for non-2xx responses.""" error: str detail: str | None = None