""" 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