File size: 1,723 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
"""
Detector interface.

Every detector is a plug-in that takes a PIL image and returns a
DetectorResult. The ensemble layer aggregates results from all enabled
detectors into a single Probabilities vector.

Adding a new detector = subclassing Detector + registering it in ensemble.py.
The /v1 API response shape never changes.
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass

from PIL import Image


@dataclass
class DetectorResult:
    """Output of a single detector run.

    Attributes
    ----------
    name :
        Stable identifier for this detector (used in API `signals`).
    score :
        The detector's own "fakeness" estimate in [0, 1].
        1.0 means "definitely synthetic"; 0.0 means "definitely authentic".
    contributions :
        Optional per-class hints in [0, 1]. Keys must be a subset of the
        4-class taxonomy: "authentic", "ai_generated", "deepfake", "edited".
        Detectors that only know "real vs. fake" leave this empty and let
        the ensemble splat their score across the relevant classes.
    notes :
        Free-form human-readable note (surfaced in API for debugging).
    """

    name: str
    score: float
    contributions: dict[str, float]
    notes: str | None = None


class Detector(ABC):
    """Base class for all forensic detectors.

    Detectors must be safe to instantiate once at process start and reused
    across requests — load model weights in __init__, not in run().
    """

    name: str = "abstract"

    @abstractmethod
    def run(self, image: Image.Image) -> DetectorResult:
        """Score a single PIL image. Must not mutate the image."""
        raise NotImplementedError