File size: 5,139 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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""
Central configuration for DeepFakeScanner.

All runtime knobs are read from environment variables with sensible defaults
so the service can run unchanged from local dev → HF Spaces → Cloud Run.

Read this once at import; do NOT scatter os.getenv() calls throughout the code.
"""
from __future__ import annotations

import os
from dataclasses import dataclass


def _env_bool(name: str, default: bool) -> bool:
    raw = os.getenv(name)
    if raw is None:
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}


def _env_int(name: str, default: int) -> int:
    raw = os.getenv(name)
    if raw is None:
        return default
    try:
        return int(raw)
    except ValueError:
        return default


@dataclass(frozen=True)
class Settings:
    # ------------------------------------------------------------------ API
    api_title: str = "DeepFakeScanner"
    api_version: str = "0.2.0"
    max_upload_bytes: int = _env_int("MAX_UPLOAD_BYTES", 10 * 1024 * 1024)  # 10 MB

    # --------------------------------------------------------- Image policy
    # Privacy-by-default: visitor uploads are NEVER persisted.
    # Logged-in paid users will be able to opt in later via a separate flag.
    store_uploads: bool = _env_bool("STORE_UPLOADS", False)

    # ---------------------------------------------------------- Detectors
    # Stage 1: only the CLIP-based classifier is wired up.
    # Stage 2 will flip these on as the detectors are trained / verified.
    enable_clip_detector: bool = _env_bool("ENABLE_CLIP_DETECTOR", True)
    enable_frequency_detector: bool = _env_bool("ENABLE_FREQUENCY_DETECTOR", False)
    enable_face_swap_detector: bool = _env_bool("ENABLE_FACE_SWAP_DETECTOR", False)

    # The CLIP backbone we use for feature extraction.
    # OpenAI CLIP ViT-B/32 weights are MIT-licensed (commercially safe).
    clip_model_name: str = os.getenv("CLIP_MODEL_NAME", "openai/clip-vit-base-patch32")

    # Path to the trained classifier-head checkpoint. ClipClassifier auto-
    # loads this in __init__ if the file exists. The same env var works
    # locally (dev machine) and inside the inference Docker image.
    # Stage 3A: head_v3a.pt (multi-generator: Flux+SDXL+SD3.5+AuraFlow).
    head_checkpoint_path: str = os.getenv(
        "HEAD_CHECKPOINT_PATH", "data/checkpoints/head_v3a.pt"
    )

    # Fallback for production: if the local checkpoint file isn't present,
    # ClipClassifier downloads the weights from this HF Hub model repo at
    # startup. The repo is PRIVATE — the container authenticates with the
    # HF_TOKEN secret set in the HF Space settings (huggingface_hub reads
    # HF_TOKEN from the environment automatically). Setting this to an
    # empty string disables the fallback (used in tests).
    # Stage 3A: head_v3a.pt. The Stage 2 head_v1.pt remains in the same
    # private repo for rollback — set HEAD_CHECKPOINT_HF_FILENAME=head_v1.pt
    # (and MODEL_VERSION=v0.3.0-stage2) to revert without a redeploy.
    head_checkpoint_hf_repo: str = os.getenv(
        "HEAD_CHECKPOINT_HF_REPO", "Veridicate/scanner-head-v1"
    )
    head_checkpoint_hf_filename: str = os.getenv(
        "HEAD_CHECKPOINT_HF_FILENAME", "head_v3a.pt"
    )

    # ---------------------------------------------------------- Provenance
    enable_c2pa_check: bool = _env_bool("ENABLE_C2PA_CHECK", True)

    # ------------------------------------------------------------ Storage
    # Postgres URL for scan records (None → in-memory fallback for local dev).
    database_url: str | None = os.getenv("DATABASE_URL")

    # S3-compatible blob storage (e.g. Cloudflare R2). Only used when
    # store_uploads is True AND a paid user opts in.
    blob_endpoint: str | None = os.getenv("BLOB_ENDPOINT")
    blob_bucket: str | None = os.getenv("BLOB_BUCKET")

    # ---------------------------------------------------- Model identifier
    # Surfaced in API responses so clients can pin behaviour to a version.
    # v0.4.0-stage3a = CLIP head trained on a multi-generator dataset
    # (50k Flux + 20k SDXL + 20k SD 3.5 + 10k AuraFlow + matched authentic).
    # In-distribution test accuracy 98.43%; +5.55 pp vs the Stage 2 head on
    # the SDXL hold-out (a generator never seen in training). See
    # docs/stage3a-implementation.md.
    # Previous: v0.3.0-stage2 (Flux-only, head_v1.pt) — still available in the
    # HF Hub repo for rollback (see head_checkpoint_hf_filename above).
    model_version: str = os.getenv("MODEL_VERSION", "v0.4.0-stage3a")

    # --------------------------------------------------------------- CORS
    # Browser preflight allowlist for the static-HTML frontend. The default
    # covers the production domain, any Cloudflare Pages preview, and
    # localhost (any port) for local dev. Override via env var if a new
    # origin needs access.
    cors_allow_origin_regex: str = os.getenv(
        "CORS_ALLOW_ORIGIN_REGEX",
        r"^(https://(www\.)?veridicate\.com|https://[a-z0-9-]+\.pages\.dev|https://[a-z0-9-]+\.dulipcf\.workers\.dev|http://localhost(:\d+)?|http://127\.0\.0\.1(:\d+)?)$",
    )


settings = Settings()