# src/config.py from dataclasses import dataclass @dataclass class Config: resolution: int = 518 patch_grid: int = 37 n_patch: int = 1369 embed_dim: int = 768 backbone: str = "stub" # "stub" (local CI) | "dinov2_vitb14_reg" (GPU host) # REJECTED (kept for reference): RINE-style last-N fusion never beat single-layer readout. # n_intermediate_layers: int = 1 extract_layer: int = 8 # SHIP DEFAULT (train.py trains 8 and 10 for the ensemble) — >0 = read patch tokens after DINOv2 block L (1-indexed) and TRUNCATE the stack to L blocks (real FLOP saving); tests whether localized signal survives earlier. 0 = full depth. Mutually exclusive with n_intermediate_layers>1. # REJECTED: MIL top-k image logits (k=16/64 and DC-centered) gave no transfer gain — the # head's self-attention broadcasts the verdict into every patch, so top-k of a broadcast # is the broadcast. Left commented for the record. # mil_top_k: int = 0 # mil_centered: bool = False label_smooth: float = 0.05 # BCE target smoothing eps: targets eps/1−eps instead of 0/1 (bounds optimal logits, calibrates probs). 0 = hard labels. # REJECTED: warp minimal-pair augmentation taught the warp artifact, not anatomy # (unseen-GAN AUC −0.027). Implementation retained in data/aug/warp.py for reference. # warp_strength: float = 1.0 # p_global_warp: float = 0.0 corruption_scale: float = 1.0 # train-only: >1 stretches corruption severity ranges toward their harsh end (clamped). Ship default 1.0 — scaling alone measured negative; keep at 1.0 unless re-validated together with lambda_feat. lambda_feat: float = 1.5 # feature-consistency weight: cosine drift of the pooled trainable representation across the clean/corrupted pair (label-blind, both classes) — forces the decision path onto corruption-stable semantic/geometric directions. fusion_project: bool = True # fusion projection: True=dense DxD (~590K); False=scalar layer-mix (K params, preferred at low data) train_impls: dict | None = None # {family:[impl,...]} operator subset for TRAINING — set BEFORE training so held-out impls stay genuinely unseen (the impl-holdout gap is only valid when training is restricted to this subset) data_parallel: bool = False # DataParallel across all visible GPUs (e.g. dual T4): splits the heavy frozen-backbone forward for ~2x + halves per-GPU batch memory. No idle GPU regardless of arm count. head_hidden: int = 0 # 0 = linear-probe head; >0 = MLP head (Linear-GELU-Linear) head_pool: str = "attn" # "attn" (baseline MIL) | "dist" (+mean/std/top-k patch-logit stats) | "nbr" (dist + zero-init 3x3 depthwise conv over the patch grid = local adjacency; density-gate redirect, orthogonal to global-realism signal) | "enc" (dist + zero-init self-attention encoder w/ 2D pos-embed = configural "sequence-not-bag" test) # NULLED (never shipped): opt-in spectral branch — JPEG/blur corruption destroys the # frequency signal it reads; only viable as a fusion member, which we did not ship. # use_freq_branch: bool = False enc_layers: int = 2 # EncoderPoolHead: transformer-encoder depth (head_pool="enc") enc_heads: int = 8 # EncoderPoolHead: self-attention heads (head_pool="enc") use_pos_embed: bool = True # EncoderPoolHead: add fixed 2D sin/cos positional embedding before the encoder # SUPERSEDED by deshortcut=True (the full pipeline); kept for ablation only. # defeat_resolution: bool = False deshortcut: bool = False # train: thumbnail-degrade+contrast+sharpness+codec aug (stronger shortcut defeat) p_codec: float = 0.5 # deshortcut: P(random JPEG/PNG re-encode) — kills the codec-history shortcut random_resolution: bool = False # train: per-batch random resolution (varied token grid, no res anchor) res_lo: int = 392 # random_resolution range lo (mult of 14) res_hi: int = 644 # random_resolution range hi (mult of 14) fair_eval: bool = False # eval: normalize real+fake to common size+format (no res/format tell) feature_mode: str = "jit" # Phase-2: "jit"|"cache" — cache path not yet implemented (no consumer in Phase 1) compile: bool = False # torch.compile toggle (GPU-host only) amp: bool = False # fp16 autocast on the FROZEN backbone only (T4 tensor cores ~2x); head stays fp32 gpu_preprocess: bool = False # workers emit uint8 (resize only); float/normalize/res-resize happen ON GPU (halves worker CPU, 4x smaller host transfers). Bilinear resize commutes with per-channel normalize, so numerics match the CPU path. lambda_aug: float = 0.8 lambda_cons_max: float = 0.3 # NULLED (never shipped): teacher distillation from a pretrained forensics model was cut # early on licensing + complexity grounds; the frozen-backbone head needs no teacher. # lambda_kd: float = 0.0 pos_weight: float = 0.0 # 0 = unweighted BCE; >0 scales the fake(1) term (calibration) entropy_gate: float = 0.5 stack_prob: float = 0.1 seed: int = 0 epochs: int = 10 epoch_frac: float = 1.0 # train on this fraction of the loader per epoch (0.5 = half an epoch); shuffle=True makes the half-pass a random subsample batch_size: int = 32 eval_batch_size: int = 32 # eval forward-pass chunk (predict_fn); bounds VRAM on large val sets eval_pool_max: int = 64 # max decoded val images resident at once (bounds HOST RAM in streaming eval) eval_workers: int = 0 # >0 = thread-pool the eval corruption prep (PIL releases the GIL); 0 = serial (legacy) holdout_eval_cap: int = 0 # 0=eval whole holdout; >0=stratified subsample cap (speed) val_eval_cap: int = 0 # 0=eval whole val EVERY epoch; >0=stratified per-epoch subsample (a 7.5k val x 19 conds = hours of CPU-bound corruption PER EPOCH on the 47k corpus) lr: float = 1e-3 lr_schedule: str = "none" # "none" | "onecycle" (per-step mid-epoch warmup+cosine) num_workers: int = 2 pin_memory: bool = True max_images: int = 0 # 0 = all data_root: str = "data" cache_dir: str = "cache" # Weights & Biases logging (OFF by default; training hosts flip use_wandb=True) use_wandb: bool = False wandb_project: str = "track5-aigc" wandb_run_name: str = "" # empty -> wandb autogenerates log_every: int = 10 # log train metrics every N steps @classmethod def smoke(cls) -> "Config": return cls(max_images=200, epochs=1, compile=False, batch_size=8)