File size: 2,814 Bytes
2e175db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
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