SMSD75 commited on
Commit
dddb186
·
verified ·
1 Parent(s): b56a7b8

Update SynthScope Space from the GitHub repository

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LEADERBOARD.md +2 -0
  2. benchmark/README.md +1 -1
  3. benchmark/bench_eval.py +4 -7
  4. benchmark/submissions/restrav.json +50 -9
  5. benchmark/submissions/stall.json +63 -0
  6. conf/dataset/image/arforensics.yaml +2 -1
  7. conf/model/image/adapted_dino.yaml +15 -0
  8. conf/model/image/aide.yaml +11 -0
  9. conf/model/image/bfree.yaml +5 -0
  10. conf/model/image/clip.yaml +15 -0
  11. conf/model/image/cnn.yaml +12 -0
  12. conf/model/image/co_spy.yaml +5 -0
  13. conf/model/image/cure.yaml +3 -0
  14. conf/model/image/d3qe.yaml +7 -0
  15. conf/model/image/dda.yaml +4 -0
  16. conf/model/image/dinov2.yaml +15 -0
  17. conf/model/image/dinov3.yaml +15 -0
  18. conf/model/image/effort.yaml +3 -0
  19. conf/model/image/fat_former.yaml +9 -0
  20. conf/model/image/moejepa.yaml +3 -0
  21. conf/model/image/npr.yaml +7 -0
  22. conf/model/image/omniaid.yaml +3 -0
  23. conf/model/image/omniaid_genimage.yaml +3 -0
  24. conf/model/image/omniaid_v1.yaml +3 -0
  25. conf/model/image/pgc.yaml +5 -0
  26. conf/model/image/safe.yaml +5 -0
  27. conf/model/image/spai.yaml +11 -1
  28. conf/model/image/vib_net.yaml +7 -0
  29. conf/model/image/vib_net_official.yaml +7 -0
  30. dataset.py +104 -53
  31. docs/ADD_DETECTOR.md +17 -2
  32. docs/BENCHMARKS.md +2 -1
  33. docs/CAPABILITIES.md +6 -4
  34. experiments/image/adapted_dinov2_vitl14.sh +1 -3
  35. experiments/image/aide.sh +4 -3
  36. experiments/image/clip_vitl14.sh +1 -3
  37. experiments/image/co_spy.sh +0 -2
  38. experiments/image/d3qe.sh +1 -3
  39. experiments/image/dinov2_vitl14.sh +1 -3
  40. experiments/image/dinov3_vitl14.sh +1 -3
  41. experiments/image/fat_former.sh +0 -6
  42. experiments/image/npr.sh +0 -2
  43. experiments/image/pgc.sh +0 -2
  44. experiments/image/safe.sh +1 -3
  45. experiments/image/spai.sh +0 -3
  46. experiments/image/vib_net.sh +0 -2
  47. experiments/video/d3.sh +1 -1
  48. experiments/video/restrav.sh +1 -1
  49. experiments/video/stall.sh +3 -3
  50. interface/backend/engine.py +9 -9
LEADERBOARD.md CHANGED
@@ -34,3 +34,5 @@ Metrics are percentages. **mAP** = mean AP over generators. Each track is ranked
34
  | --- | --- | --- | --- | --- | --- | --- | --- | --- |
35
  | 1 | SPLIT | training-free | 99.41 | 99.79 | — | 99.97 | ✓ | Training-free X-CLIP patch roughness x motion incoherence; Acc at the real-only FPR-1% calibration |
36
  | 2 | D3 | training-free | 95.53 | 68.84 | — | 98.45 | ✓ | Training-free X-CLIP 2nd-order feature volatility (official preprocessing) |
 
 
 
34
  | --- | --- | --- | --- | --- | --- | --- | --- | --- |
35
  | 1 | SPLIT | training-free | 99.41 | 99.79 | — | 99.97 | ✓ | Training-free X-CLIP patch roughness x motion incoherence; Acc at the real-only FPR-1% calibration |
36
  | 2 | D3 | training-free | 95.53 | 68.84 | — | 98.45 | ✓ | Training-free X-CLIP 2nd-order feature volatility (official preprocessing) |
37
+ | 3 | STALL | training-free | 56.89 | 32.05 | — | 82.59 | ✓ | Training-free DINOv3 spatio-temporal likelihood under VATEX real-video statistics |
38
+ | 4 | ReStraV | frozen-probe | 30.01 | 33.60 | — | 43.06 | ✓ | Frozen DINOv2 trajectory geometry + MLP trained on GenVidBench; does not transfer to GenVideo-Val (AUROC below 50) |
benchmark/README.md CHANGED
@@ -19,7 +19,7 @@ from the submission files in [`submissions/`](submissions/) — see [LEADERBOARD
19
  | `genimage-sdv1.4` | image | GenImage SD-v1.4 real/fake | in-distribution diffusion | **AUROC** |
20
  | `genvideo-val` | video | GenVideo-Val: all 10,000 MSR-VTT reals against every fake, per generator | mixed | **AUROC** (AP with fake as positive is unstable here: as few as 56 fakes against 10,000 reals) |
21
 
22
- **These three tracks are only the start.** The repo supports 13 image and 8 video benchmark datasets (see the table in the
23
  [README](../README.md#-benchmarks)), and the web page's Leaderboard tab already compares detectors on 30+ benchmark groups.
24
  A dataset becomes a standardized track once its protocol (test set, grouping, metric) is fixed, so results stay comparable;
25
  adding one means a schema entry, an `evaluate.py` track and verified submissions, described in
 
19
  | `genimage-sdv1.4` | image | GenImage SD-v1.4 real/fake | in-distribution diffusion | **AUROC** |
20
  | `genvideo-val` | video | GenVideo-Val: all 10,000 MSR-VTT reals against every fake, per generator | mixed | **AUROC** (AP with fake as positive is unstable here: as few as 56 fakes against 10,000 reals) |
21
 
22
+ **These three tracks are only the start.** The repo supports 20 image and 8 video benchmark datasets (see the table in the
23
  [README](../README.md#-benchmarks)), and the web page's Leaderboard tab already compares detectors on 30+ benchmark groups.
24
  A dataset becomes a standardized track once its protocol (test set, grouping, metric) is fixed, so results stay comparable;
25
  adding one means a schema entry, an `evaluate.py` track and verified submissions, described in
benchmark/bench_eval.py CHANGED
@@ -144,8 +144,9 @@ def discover(benchmark: str, root: Path, shared_reals: Path | None, select: set[
144
 
145
 
146
  def native_res_transform(tf):
147
- """Drop the up-scaling Resize/CenterCrop from a model's eval transform, keeping
148
- ToTensor + Normalize, so images are scored at NATIVE resolution.
 
149
 
150
  Some methods (e.g. NPR on GANGen-Detection/freqnet) detect up-sampling artifacts and
151
  MUST NOT be fed a bilinearly up-scaled image — resizing a 128x128 GAN crop to 256x256
@@ -153,11 +154,7 @@ def native_res_transform(tf):
153
  GANGen recipe runs with ``no_resize=True``; this reproduces it. Safe only when every
154
  image in a per-generator batch shares one resolution (true for freqnet's per-gen sets),
155
  since bench_eval stacks a batch into a single tensor."""
156
- import torchvision.transforms as T
157
- if not isinstance(tf, T.Compose):
158
- return tf
159
- kept = [t for t in tf.transforms if not isinstance(t, (T.Resize, T.CenterCrop))]
160
- return T.Compose(kept)
161
 
162
 
163
  @torch.no_grad()
 
144
 
145
 
146
  def native_res_transform(tf):
147
+ """Drop the up-scaling resize/center-crop steps from a model's eval transform, keeping
148
+ to_dtype + normalize, so images are scored at NATIVE resolution (ValueError for eval lists
149
+ whose own steps resize, see ``AIGITransformations.without_resize``).
150
 
151
  Some methods (e.g. NPR on GANGen-Detection/freqnet) detect up-sampling artifacts and
152
  MUST NOT be fed a bilinearly up-scaled image — resizing a 128x128 GAN crop to 256x256
 
154
  GANGen recipe runs with ``no_resize=True``; this reproduces it. Safe only when every
155
  image in a per-generator batch shares one resolution (true for freqnet's per-gen sets),
156
  since bench_eval stacks a batch into a single tensor."""
157
+ return tf.without_resize()
 
 
 
 
158
 
159
 
160
  @torch.no_grad()
benchmark/submissions/restrav.json CHANGED
@@ -3,20 +3,61 @@
3
  "track": "genvideo-val",
4
  "modality": "video",
5
  "model_type": "restrav",
6
- "paper": "2025",
7
  "url": "https://github.com/SMSD75/SynthScope",
8
  "evaluation": "frozen-probe",
9
  "metrics": {
10
- "acc": null,
11
- "map": null,
12
- "ap": 96.67,
13
- "auroc": 97.06
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  },
15
- "per_generator": null,
16
  "submitted_by": "SynthScope",
17
- "date": "2026-06-02",
18
  "verified": true,
19
- "source": "SynthScope reproduction (logged Test Metrics); see repro/FINAL_RESULTS.md",
20
- "protocol_notes": "Frozen DINOv2 trajectory geometry + MLP",
21
  "synthscope_version": "0.1.0"
22
  }
 
3
  "track": "genvideo-val",
4
  "modality": "video",
5
  "model_type": "restrav",
6
+ "paper": "Interno et al., 2025",
7
  "url": "https://github.com/SMSD75/SynthScope",
8
  "evaluation": "frozen-probe",
9
  "metrics": {
10
+ "acc": 30.01,
11
+ "map": 33.6,
12
+ "ap": null,
13
+ "auroc": 43.06
14
+ },
15
+ "per_generator": {
16
+ "Crafter": {
17
+ "auroc": 89.15,
18
+ "ap": 68.75
19
+ },
20
+ "Gen2": {
21
+ "auroc": 2.76,
22
+ "ap": 6.37
23
+ },
24
+ "HotShot": {
25
+ "auroc": 0.1,
26
+ "ap": 3.35
27
+ },
28
+ "Lavie": {
29
+ "auroc": 34.51,
30
+ "ap": 25.86
31
+ },
32
+ "ModelScope": {
33
+ "auroc": 78.96,
34
+ "ap": 59.16
35
+ },
36
+ "MoonValley": {
37
+ "auroc": 2.07,
38
+ "ap": 3.02
39
+ },
40
+ "MorphStudio": {
41
+ "auroc": 92.58,
42
+ "ap": 74.66
43
+ },
44
+ "Show_1": {
45
+ "auroc": 91.56,
46
+ "ap": 66.49
47
+ },
48
+ "Sora": {
49
+ "auroc": 0.26,
50
+ "ap": 0.29
51
+ },
52
+ "WildScrape": {
53
+ "auroc": 38.7,
54
+ "ap": 28.08
55
+ }
56
  },
 
57
  "submitted_by": "SynthScope",
58
+ "date": "2026-11-02",
59
  "verified": true,
60
+ "source": "repro/bench_results/restrav_genvideo_full.json (benchmark/bench_eval_video.py --dataset genvideo, no --limit)",
61
+ "protocol_notes": "Frozen DINOv2 trajectory geometry + MLP trained on GenVidBench; does not transfer to GenVideo-Val (AUROC below 50)",
62
  "synthscope_version": "0.1.0"
63
  }
benchmark/submissions/stall.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "method": "STALL",
3
+ "track": "genvideo-val",
4
+ "modality": "video",
5
+ "model_type": "stall",
6
+ "paper": "Ben Hayun et al., CVPR'26",
7
+ "url": "https://github.com/SMSD75/SynthScope",
8
+ "evaluation": "training-free",
9
+ "metrics": {
10
+ "acc": 56.89,
11
+ "map": 32.05,
12
+ "ap": null,
13
+ "auroc": 82.59
14
+ },
15
+ "per_generator": {
16
+ "Crafter": {
17
+ "auroc": 87.91,
18
+ "ap": 53.66
19
+ },
20
+ "Gen2": {
21
+ "auroc": 90.0,
22
+ "ap": 58.91
23
+ },
24
+ "HotShot": {
25
+ "auroc": 76.97,
26
+ "ap": 21.25
27
+ },
28
+ "Lavie": {
29
+ "auroc": 86.42,
30
+ "ap": 53.53
31
+ },
32
+ "ModelScope": {
33
+ "auroc": 78.21,
34
+ "ap": 20.92
35
+ },
36
+ "MoonValley": {
37
+ "auroc": 82.67,
38
+ "ap": 22.83
39
+ },
40
+ "MorphStudio": {
41
+ "auroc": 83.22,
42
+ "ap": 28.34
43
+ },
44
+ "Show_1": {
45
+ "auroc": 82.29,
46
+ "ap": 30.09
47
+ },
48
+ "Sora": {
49
+ "auroc": 80.73,
50
+ "ap": 2.8
51
+ },
52
+ "WildScrape": {
53
+ "auroc": 77.48,
54
+ "ap": 28.22
55
+ }
56
+ },
57
+ "submitted_by": "SynthScope",
58
+ "date": "2026-11-02",
59
+ "verified": true,
60
+ "source": "repro/bench_results/stall_genvideo_full.json (benchmark/bench_eval_video.py --dataset genvideo, no --limit)",
61
+ "protocol_notes": "Training-free DINOv3 spatio-temporal likelihood under VATEX real-video statistics",
62
+ "synthscope_version": "0.1.0"
63
+ }
conf/dataset/image/arforensics.yaml CHANGED
@@ -3,7 +3,8 @@ name: arforensics
3
  # Open-MAGVIT2), 6000 fakes + 6000 ImageNet reals each. (HF Yanran21/ARForensics, corrected test release, 21 GB, MIT)
4
  root_dir: /hddstore/ssalehi/SynthScope/ARForensics
5
  hf_repo_id: Yanran21/ARForensics
6
- train_batch_size: 32 # D3QE's training data (train/val splits) is fetched only when a train_transform_list is set
 
7
  val_batch_size: 64
8
  test_batch_size: 512
9
  num_workers: 8
 
3
  # Open-MAGVIT2), 6000 fakes + 6000 ImageNet reals each. (HF Yanran21/ARForensics, corrected test release, 21 GB, MIT)
4
  root_dir: /hddstore/ssalehi/SynthScope/ARForensics
5
  hf_repo_id: Yanran21/ARForensics
6
+ with_train_splits: false # true: also build (and fetch) the train/val splits, D3QE's training data (39 GB)
7
+ train_batch_size: 32
8
  val_batch_size: 64
9
  test_batch_size: 512
10
  num_workers: 8
conf/model/image/adapted_dino.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # Adapter-tuned DINOv2: frozen DINOv2 ViT-L/14 with a trainable bottleneck adapter per block + a linear head.
6
+ model_type: adapted_dino
7
+ backbone_name: dinov2_vitl14
8
+ use_adapter: true
9
+ bottleneck_dim: 64
10
+ freeze_backbone: true
11
+
12
+ # Test-time preprocessing: inference, benchmarks and the end-of-training test.
13
+ eval_transform_list: [resize_short_256, center_crop_224, to_dtype, normalize_imagenet]
14
+ # Training augmentation.
15
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_224, random_horizontal_flip, to_dtype, normalize_imagenet]
conf/model/image/aide.yaml CHANGED
@@ -19,3 +19,14 @@ dct:
19
  stride: 16
20
  output_size: 256
21
  grade_N: 6
 
 
 
 
 
 
 
 
 
 
 
 
19
  stride: 16
20
  output_size: 256
21
  grade_N: 6
22
+
23
+ # Test-time preprocessing (official): inference and benchmarks.
24
+ eval_transform_list: [aide_five_view]
25
+ # Training augmentation: the five native-resolution views, long side capped at 1024 (the recipe of the SynthScope_train
26
+ # checkpoint, repro/retrain_aide_native_v2.sbatch).
27
+ train_transform_list: [random_horizontal_flip, aide_five_view_train]
28
+ # Validation during training (differs from the test-time preprocessing above).
29
+ val_transform_list: [aide_five_view_train]
30
+ # Caps the long side at 1024 to bound dataloader-worker memory during the end-of-training test; inference and
31
+ # benchmarks use the uncapped eval list.
32
+ test_transform_list: [aide_five_view_train]
conf/model/image/bfree.yaml CHANGED
@@ -9,3 +9,8 @@ model_type: bfree
9
  timm_arch: vit_base_patch14_reg4_dinov2.lvd142m
10
  crop_size: 504
11
  pretrained_backbone: false
 
 
 
 
 
 
9
  timm_arch: vit_base_patch14_reg4_dinov2.lvd142m
10
  crop_size: 504
11
  pretrained_backbone: false
12
+
13
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
14
+ eval_transform_list: [to_dtype, normalize_imagenet]
15
+ # Native resolution (variable size): an end-of-training test through the training loaders needs
16
+ # DatasetConfig.test_batch_size=1 (no training recipe exists).
conf/model/image/clip.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # UnivFD (Ojha et al., CVPR'23): frozen CLIP ViT-L/14 image features + a linear real/fake head.
6
+ model_type: clip
7
+ backbone_name: CLIP:ViT-L/14
8
+ use_adapter: false
9
+ bottleneck_dim: 64
10
+ freeze_backbone: true
11
+
12
+ # Test-time preprocessing: inference, benchmarks and the end-of-training test.
13
+ eval_transform_list: [resize_short_256, center_crop_224, to_dtype, normalize_clip]
14
+ # Training augmentation.
15
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_224, random_horizontal_flip, to_dtype, normalize_clip]
conf/model/image/cnn.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # CNNDetection (Wang et al., CVPR'20): ImageNet ResNet-50 fine-tuned end to end as a real/fake classifier.
6
+ model_type: cnn
7
+ freeze_backbone: false
8
+
9
+ # Test-time preprocessing: inference, benchmarks and the end-of-training test.
10
+ eval_transform_list: [resize_short_256, center_crop_224, to_dtype, normalize_imagenet]
11
+ # Training augmentation.
12
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_224, random_horizontal_flip, to_dtype, normalize_imagenet]
conf/model/image/co_spy.yaml CHANGED
@@ -19,3 +19,8 @@ feat_interp_alpha: 0.2
19
 
20
  num_classes: 2
21
  freeze_backbone: true
 
 
 
 
 
 
19
 
20
  num_classes: 2
21
  freeze_backbone: true
22
+
23
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
24
+ eval_transform_list: [resize_short_384, center_crop_384, to_dtype]
25
+ # Training augmentation.
26
+ train_transform_list: [resize_short_384, gaussian_blur, jpeg_compression, random_crop_384, random_horizontal_flip, to_dtype]
conf/model/image/cure.yaml CHANGED
@@ -13,3 +13,6 @@ base_filename: PE-Core-L14-336.pt
13
  base_checkpoint: null # local PE-Core-L14-336.pt override (or env SYNTHSCOPE_PE_CORE_CKPT)
14
  load_base_backbone: true # false = build the architecture only (smoke tests / key checks)
15
  label_smoothing: 0.0
 
 
 
 
13
  base_checkpoint: null # local PE-Core-L14-336.pt override (or env SYNTHSCOPE_PE_CORE_CKPT)
14
  load_base_backbone: true # false = build the architecture only (smoke tests / key checks)
15
  label_smoothing: 0.0
16
+
17
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
18
+ eval_transform_list: [cure_eval]
conf/model/image/d3qe.yaml CHANGED
@@ -8,3 +8,10 @@ defaults:
8
  # vq_ds16_c2i.pt) are fetched from their own releases, as in the official code.
9
  model_type: d3qe
10
  vq_checkpoint: null # local vq_ds16_c2i.pt; null -> $SYNTHSCOPE_LLAMAGEN_VQ, else download from the Hugging Face Hub
 
 
 
 
 
 
 
 
8
  # vq_ds16_c2i.pt) are fetched from their own releases, as in the official code.
9
  model_type: d3qe
10
  vq_checkpoint: null # local vq_ds16_c2i.pt; null -> $SYNTHSCOPE_LLAMAGEN_VQ, else download from the Hugging Face Hub
11
+
12
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
13
+ eval_transform_list: [resize_short_256, center_crop_256, to_dtype]
14
+ # Training augmentation.
15
+ train_transform_list: [resize_short_256, blur_jpeg_cnndet, random_crop_256, random_horizontal_flip, to_dtype]
16
+ # Validation during training (differs from the test-time preprocessing above).
17
+ val_transform_list: [resize_short_256, blur_jpeg_cnndet_val, center_crop_256, to_dtype]
conf/model/image/dda.yaml CHANGED
@@ -12,3 +12,7 @@ lora_rank: 8
12
  lora_alpha: 1.0
13
  lora_targets: [attn.qkv, attn.proj, mlp.fc1, mlp.fc2]
14
  freeze_backbone: true
 
 
 
 
 
12
  lora_alpha: 1.0
13
  lora_targets: [attn.qkv, attn.proj, mlp.fc1, mlp.fc2]
14
  freeze_backbone: true
15
+
16
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
17
+ eval_transform_list: [center_crop_336, to_dtype, normalize_clip]
18
+ # No training lists: DDA trains on dataset image/dda_train, which builds its own official paired transforms.
conf/model/image/dinov2.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # DINOv2 probe: frozen DINOv2 ViT-L/14 + a linear real/fake head.
6
+ model_type: dinov2
7
+ backbone_name: dinov2_vitl14
8
+ use_adapter: false
9
+ bottleneck_dim: 64
10
+ freeze_backbone: true
11
+
12
+ # Test-time preprocessing: inference, benchmarks and the end-of-training test.
13
+ eval_transform_list: [resize_short_256, center_crop_224, to_dtype, normalize_imagenet]
14
+ # Training augmentation.
15
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_224, random_horizontal_flip, to_dtype, normalize_imagenet]
conf/model/image/dinov3.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # DINOv3 probe: frozen DINOv3 ViT-L/16 (gated weights) + a linear real/fake head.
6
+ model_type: dinov3
7
+ backbone_name: dinov3_vitl16
8
+ use_adapter: false
9
+ bottleneck_dim: 64
10
+ freeze_backbone: true
11
+
12
+ # Test-time preprocessing: inference, benchmarks and the end-of-training test.
13
+ eval_transform_list: [resize_short_256, center_crop_256, to_dtype, normalize_dinov3]
14
+ # Training augmentation.
15
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_256, random_horizontal_flip, to_dtype, normalize_dinov3]
conf/model/image/effort.yaml CHANGED
@@ -7,3 +7,6 @@ defaults:
7
  model_type: effort
8
  clip_name: openai/clip-vit-large-patch14
9
  svd_rank: 1023
 
 
 
 
7
  model_type: effort
8
  clip_name: openai/clip-vit-large-patch14
9
  svd_rank: 1023
10
+
11
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
12
+ eval_transform_list: [effort_cv2_resize_224, to_dtype, normalize_clip]
conf/model/image/fat_former.yaml CHANGED
@@ -11,3 +11,12 @@ num_classes: 2
11
  classnames: [real, fake]
12
  freeze_backbone: true
13
  force_fp32: true
 
 
 
 
 
 
 
 
 
 
11
  classnames: [real, fake]
12
  freeze_backbone: true
13
  force_fp32: true
14
+
15
+ # Official FatFormer (utils/dataset.py) normalizes with ImageNet mean/std, not CLIP's, despite the CLIP backbone (the
16
+ # official-weights eval reproduces 98.39 acc / 99.70 mAP with it).
17
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
18
+ eval_transform_list: [resize_256, center_crop_224, to_dtype, normalize_imagenet]
19
+ # Training augmentation.
20
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_224, random_horizontal_flip, to_dtype, normalize_imagenet]
21
+ # Validation during training (differs from the test-time preprocessing above).
22
+ val_transform_list: [resize_short_256, center_crop_224, to_dtype, normalize_imagenet]
conf/model/image/moejepa.yaml CHANGED
@@ -13,3 +13,6 @@ num_experts: 6
13
  top_k: 2
14
  router_temperature: 0.5
15
  out_classes: 3
 
 
 
 
13
  top_k: 2
14
  router_temperature: 0.5
15
  out_classes: 3
16
+
17
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
18
+ eval_transform_list: [vjepa2_processor]
conf/model/image/npr.yaml CHANGED
@@ -5,3 +5,10 @@ defaults:
5
  model_type: npr
6
  num_classes: 2
7
  freeze_backbone: false
 
 
 
 
 
 
 
 
5
  model_type: npr
6
  num_classes: 2
7
  freeze_backbone: false
8
+
9
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
10
+ eval_transform_list: [resize_256, to_dtype, normalize_imagenet]
11
+ # Training augmentation.
12
+ train_transform_list: [resize_256, random_crop_224, random_horizontal_flip, to_dtype, normalize_imagenet]
13
+ # Validation during training (differs from the test-time preprocessing above).
14
+ val_transform_list: [resize_256, center_crop_224, to_dtype, normalize_imagenet]
conf/model/image/omniaid.yaml CHANGED
@@ -16,3 +16,6 @@ moe_lambda_gating_cls: 0.1
16
  dropout: 0.0
17
  gradient_checkpointing_enable: false
18
  is_hybrid: true
 
 
 
 
16
  dropout: 0.0
17
  gradient_checkpointing_enable: false
18
  is_hybrid: true
19
+
20
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
21
+ eval_transform_list: [resize_336, to_dtype, normalize_clip]
conf/model/image/omniaid_genimage.yaml CHANGED
@@ -16,3 +16,6 @@ moe_lambda_gating_cls: 0.1
16
  dropout: 0.0
17
  gradient_checkpointing_enable: false
18
  is_hybrid: true
 
 
 
 
16
  dropout: 0.0
17
  gradient_checkpointing_enable: false
18
  is_hybrid: true
19
+
20
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
21
+ eval_transform_list: [resize_512, resize_336, to_dtype, normalize_clip]
conf/model/image/omniaid_v1.yaml CHANGED
@@ -16,3 +16,6 @@ moe_lambda_gating_cls: 0.1
16
  dropout: 0.0
17
  gradient_checkpointing_enable: false
18
  is_hybrid: true
 
 
 
 
16
  dropout: 0.0
17
  gradient_checkpointing_enable: false
18
  is_hybrid: true
19
+
20
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
21
+ eval_transform_list: [resize_512, resize_336, to_dtype, normalize_clip]
conf/model/image/pgc.yaml CHANGED
@@ -19,3 +19,8 @@ label_smoothing: 0.1
19
  tau_rgb: 0.5
20
  tau_res: 0.5
21
  num_classes: 1
 
 
 
 
 
 
19
  tau_rgb: 0.5
20
  tau_res: 0.5
21
  num_classes: 1
22
+
23
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
24
+ eval_transform_list: [center_crop_224, to_dtype, normalize_imagenet]
25
+ # Training augmentation.
26
+ train_transform_list: [random_horizontal_flip, color_jitter_pgc, pad_random_crop_224, to_dtype, normalize_imagenet]
conf/model/image/safe.yaml CHANGED
@@ -6,3 +6,8 @@ defaults:
6
  # it does NOT use backbone_name. model_type=safe is dispatched before the CLIP fallback.
7
  model_type: safe
8
  freeze_backbone: false
 
 
 
 
 
 
6
  # it does NOT use backbone_name. model_type=safe is dispatched before the CLIP fallback.
7
  model_type: safe
8
  freeze_backbone: false
9
+
10
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
11
+ eval_transform_list: [center_crop_256, to_dtype]
12
+ # Training augmentation.
13
+ train_transform_list: [random_crop_256_pad, random_horizontal_flip, random_rotation_180, color_jitter_safe, to_dtype, random_mask_safe]
conf/model/image/spai.yaml CHANGED
@@ -15,4 +15,14 @@ use_sca: true
15
  patch_size: 224
16
  patch_stride: 224
17
  num_heads: 12
18
- attn_embed_dim: 1536
 
 
 
 
 
 
 
 
 
 
 
15
  patch_size: 224
16
  patch_stride: 224
17
  num_heads: 12
18
+ attn_embed_dim: 1536
19
+
20
+ # Test-time preprocessing (official): inference and benchmarks.
21
+ eval_transform_list: [cap_long_side_1024, to_dtype]
22
+ # Training augmentation. SPAI takes [0, 1] images; ImageNet normalization is applied inside the model.
23
+ train_transform_list: [resize_short_256, gaussian_blur, jpeg_compression, random_crop_224, random_horizontal_flip, to_dtype]
24
+ # Validation during training (differs from the test-time preprocessing above).
25
+ val_transform_list: [resize_short_256, center_crop_224, to_dtype]
26
+ # Batchable stand-in for the end-of-training test (the native-resolution eval list varies in size); inference and
27
+ # benchmarks use the eval list.
28
+ test_transform_list: [resize_short_256, center_crop_224, to_dtype]
conf/model/image/vib_net.yaml CHANGED
@@ -13,3 +13,10 @@ vib_latent_dim: 256
13
  dropout: 0.5
14
  std_shift: 5.0
15
  freeze_backbone: true
 
 
 
 
 
 
 
 
13
  dropout: 0.5
14
  std_shift: 5.0
15
  freeze_backbone: true
16
+
17
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
18
+ eval_transform_list: [center_crop_224, to_dtype, normalize_clip]
19
+ # Training augmentation.
20
+ train_transform_list: [blur_jpeg_cnndet, pad_random_crop_224, random_horizontal_flip, to_dtype, normalize_clip]
21
+ # Validation during training (differs from the test-time preprocessing above).
22
+ val_transform_list: [blur_jpeg_cnndet_val, center_crop_224, to_dtype, normalize_clip]
conf/model/image/vib_net_official.yaml CHANGED
@@ -17,3 +17,10 @@ std_floor: 0.0
17
  clip_fp32: true
18
  # The paper's trade-off parameter is not reported; 1e-3 is the standard VIB value (Alemi et al.).
19
  vib_beta: 0.001
 
 
 
 
 
 
 
 
17
  clip_fp32: true
18
  # The paper's trade-off parameter is not reported; 1e-3 is the standard VIB value (Alemi et al.).
19
  vib_beta: 0.001
20
+
21
+ # Test-time preprocessing (official): inference, benchmarks and the end-of-training test.
22
+ eval_transform_list: [center_crop_224, to_dtype, normalize_clip]
23
+ # Training augmentation.
24
+ train_transform_list: [blur_jpeg_cnndet, pad_random_crop_224, random_horizontal_flip, to_dtype, normalize_clip]
25
+ # Validation during training (differs from the test-time preprocessing above).
26
+ val_transform_list: [blur_jpeg_cnndet_val, center_crop_224, to_dtype, normalize_clip]
dataset.py CHANGED
@@ -1,6 +1,7 @@
1
  from torch.utils.data import Dataset
2
  from typing import Literal
3
  from pathlib import Path
 
4
  import hashlib
5
  import json
6
  import os
@@ -22,6 +23,7 @@ import numpy as np
22
  import torch
23
  import torch.nn.functional as F
24
  from decord import VideoReader, cpu
 
25
  from torch.utils.data import DataLoader
26
  from transformations import AIGITransformations
27
  from torchvision.io import decode_image, ImageReadMode
@@ -71,6 +73,75 @@ def _has_videos(directory: Path) -> bool:
71
  return directory.is_dir() and any(_iter_videos(directory))
72
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  class _PILFrames:
75
  """The part of decord's ``VideoReader`` API that ``sample_video_frames`` uses, for animated images decord cannot open
76
  (GIFs: decord fails with "broken metadata" on them). Every frame is composited to RGB at the canvas size, and the
@@ -1128,16 +1199,13 @@ class CNNDetectionDataset(Dataset):
1128
  class CNNDetectionDatasetModule:
1129
  def __init__(self, config):
1130
  self.config = config
1131
- train_list = list(config.DatasetConfig.train_transform_list)
1132
- eval_list = list(config.DatasetConfig.eval_transform_list)
1133
- self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None
1134
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1135
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1136
  classes = config.DatasetConfig.get("train_classes", None) # ProGAN classes for train/val (None: all 20)
1137
  classes = list(classes) if classes else None
1138
  self.train_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch, classes=classes)
1139
- self.val_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "val", self.eval_transform, prefetch=prefetch, classes=classes)
1140
- self.test_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch)
1141
 
1142
  def train_dataloader(self):
1143
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
@@ -1149,9 +1217,9 @@ class CNNDetectionDatasetModule:
1149
  return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
1150
 
1151
 
1152
- def _split_train_subset(dataset, per_class, val_per_class, eval_transform, seed=0):
1153
  """Keep a seeded ``per_class`` images per label of ``dataset`` for training and return a disjoint ``val_per_class``
1154
- per label as a validation copy with ``eval_transform``: for recipes trained on part of a train split, validated on
1155
  held-out training images (GenImage's ``val`` alias is its test split, which must not select the model)."""
1156
  rng = random.Random(seed)
1157
  train_idx, val_idx = [], []
@@ -1167,30 +1235,27 @@ def _split_train_subset(dataset, per_class, val_per_class, eval_transform, seed=
1167
  ds.images = [dataset.images[i] for i in keep]
1168
  ds.labels = [dataset.labels[i] for i in keep]
1169
  ds.folder_names = [dataset.folder_names[i] for i in keep]
1170
- val.transform = eval_transform
1171
  return val
1172
 
1173
 
1174
  class GenImageDatasetModule:
1175
  def __init__(self, config):
1176
  self.config = config
1177
- train_list = list(config.DatasetConfig.train_transform_list)
1178
- eval_list = list(config.DatasetConfig.eval_transform_list)
1179
- self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None
1180
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1181
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1182
  source_url = config.DatasetConfig.get("source_url", GenImageDataset.google_drive_folder_url)
1183
  GenImageDataset.google_drive_folder_url = source_url
1184
  GenImageDataset.hf_repo_id = config.DatasetConfig.get("hf_repo_id", None)
1185
  GenImageDataset.hf_subdir = config.DatasetConfig.get("hf_subdir", None)
1186
  self.train_dataset = GenImageDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch)
1187
- self.test_dataset = GenImageDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch)
1188
  per_class = config.DatasetConfig.get("train_per_class", None) # e.g. VIB-Net: 16k real + 16k SD v1.4
1189
  if per_class:
1190
  self.val_dataset = _split_train_subset(self.train_dataset, int(per_class),
1191
- int(config.DatasetConfig.get("val_per_class", 1000)), self.eval_transform)
1192
  else:
1193
- self.val_dataset = GenImageDataset(config.DatasetConfig.root_dir, "val", self.eval_transform, prefetch=prefetch)
1194
 
1195
  def train_dataloader(self):
1196
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
@@ -1757,14 +1822,11 @@ class UniversialFakeDetectionDataset(Dataset):
1757
  class AIGIBenchDatasetModule:
1758
  def __init__(self, config):
1759
  self.config = config
1760
- train_list = list(config.DatasetConfig.train_transform_list)
1761
- eval_list = list(config.DatasetConfig.eval_transform_list)
1762
- self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None
1763
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1764
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1765
  self.train_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "train", config.DatasetConfig.setting, self.train_transform, prefetch=prefetch)
1766
- self.val_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "val", config.DatasetConfig.setting, self.eval_transform, prefetch=prefetch)
1767
- self.test_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "test", config.DatasetConfig.setting, self.eval_transform, prefetch=prefetch)
1768
 
1769
  def train_dataloader(self):
1770
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
@@ -1779,11 +1841,10 @@ class AIGIBenchDatasetModule:
1779
  class DiTFakeDatasetModule:
1780
  def __init__(self, config):
1781
  self.config = config
1782
- eval_list = list(config.DatasetConfig.eval_transform_list)
1783
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1784
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1785
  repo_id = config.DatasetConfig.get("repo_id", DiTFakeDataset.repo_id)
1786
- self.test_dataset = DiTFakeDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch, repo_id=repo_id)
1787
 
1788
  def test_dataloader(self):
1789
  return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
@@ -1792,14 +1853,13 @@ class DiTFakeDatasetModule:
1792
  class FreqNetDetectionDatasetModule:
1793
  def __init__(self, config):
1794
  self.config = config
1795
- eval_list = list(config.DatasetConfig.eval_transform_list)
1796
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1797
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1798
  source_url = config.DatasetConfig.get("source_url", FreqNetDetectionDataset.google_drive_folder_url)
1799
  self.test_dataset = FreqNetDetectionDataset(
1800
  config.DatasetConfig.root_dir,
1801
  "test",
1802
- self.eval_transform,
1803
  prefetch=prefetch,
1804
  source_url=source_url,
1805
  )
@@ -1817,14 +1877,13 @@ class FreqNetDetectionDatasetModule:
1817
  class UniversialFakeDetectionDatasetModule:
1818
  def __init__(self, config):
1819
  self.config = config
1820
- eval_list = list(config.DatasetConfig.eval_transform_list)
1821
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1822
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1823
  source_url = config.DatasetConfig.get("source_url", UniversialFakeDetectionDataset.source_url)
1824
  self.test_dataset = UniversialFakeDetectionDataset(
1825
  config.DatasetConfig.root_dir,
1826
  "test",
1827
- self.eval_transform,
1828
  prefetch=prefetch,
1829
  source_url=source_url,
1830
  )
@@ -1842,8 +1901,7 @@ class UniversialFakeDetectionDatasetModule:
1842
  class CoSpyDatasetModule:
1843
  def __init__(self, config):
1844
  self.config = config
1845
- eval_list = list(config.DatasetConfig.eval_transform_list)
1846
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1847
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1848
  source_datasets = config.DatasetConfig.get("source_datasets", None)
1849
  generator_models = config.DatasetConfig.get("generator_models", None)
@@ -1854,7 +1912,7 @@ class CoSpyDatasetModule:
1854
  self.test_dataset = CoSpyDataset(
1855
  config.DatasetConfig.root_dir,
1856
  "test",
1857
- self.eval_transform,
1858
  prefetch=prefetch,
1859
  source_datasets=list(source_datasets) if source_datasets else None,
1860
  generator_models=list(generator_models) if generator_models else None,
@@ -2015,10 +2073,7 @@ class VideoFolderDatasetModule:
2015
  def __init__(self, config):
2016
  self.config = config
2017
  cfg = config.DatasetConfig
2018
- train_list = list(cfg.get("train_transform_list", []))
2019
- eval_list = list(cfg.get("eval_transform_list", []))
2020
- self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None
2021
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
2022
  prefetch = bool(cfg.get("prefetch", False))
2023
  common = dict(
2024
  num_frames=cfg.get("num_frames", 16),
@@ -2030,8 +2085,8 @@ class VideoFolderDatasetModule:
2030
  resize_interp=cfg.get("resize_interp", "bilinear"),
2031
  )
2032
  self.train_dataset = VideoFolderDataset(cfg.root_dir, "train", self.train_transform, prefetch=prefetch, **common)
2033
- self.val_dataset = VideoFolderDataset(cfg.root_dir, "val", self.eval_transform, prefetch=prefetch, **common)
2034
- self.test_dataset = VideoFolderDataset(cfg.root_dir, "test", self.eval_transform, prefetch=prefetch, **common)
2035
 
2036
  def train_dataloader(self):
2037
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
@@ -2148,10 +2203,9 @@ class _TestOnlyVideoModule:
2148
  def __init__(self, config):
2149
  self.config = config
2150
  cfg = config.DatasetConfig
2151
- eval_list = list(cfg.get("eval_transform_list", []))
2152
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
2153
  self.test_dataset = self.DATASET_CLASS(
2154
- cfg.root_dir, "test", self.eval_transform, prefetch=bool(cfg.get("prefetch", False)),
2155
  num_frames=cfg.get("num_frames", 16), fps=cfg.get("fps", None),
2156
  window_seconds=cfg.get("window_seconds", 2.0), frame_size=cfg.get("frame_size", 224),
2157
  hf_repo_id=cfg.get("hf_repo_id", self.DATASET_CLASS.hf_repo_id),
@@ -2922,10 +2976,9 @@ class _TestOnlyImageModule:
2922
  def __init__(self, config):
2923
  self.config = config
2924
  cfg = config.DatasetConfig
2925
- eval_list = list(cfg.get("eval_transform_list", []))
2926
- self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
2927
  self.test_dataset = self.DATASET_CLASS(
2928
- cfg.root_dir, "test", self.eval_transform, prefetch=bool(cfg.get("prefetch", False)))
2929
 
2930
  def train_dataloader(self):
2931
  return None
@@ -2993,20 +3046,18 @@ class SIDSetDatasetModule(_TestOnlyImageModule):
2993
 
2994
 
2995
  class ARForensicsDatasetModule(_TestOnlyImageModule):
2996
- """The test split for benchmarking; with a ``train_transform_list`` in the config also the train and val splits
2997
- (D3QE's training data), validated with ``val_transform_list`` (default: ``eval_transform_list``)."""
2998
  DATASET_CLASS = ARForensicsDataset
2999
 
3000
  def __init__(self, config):
3001
  super().__init__(config)
3002
  cfg = config.DatasetConfig
3003
- train_list = list(cfg.get("train_transform_list", []))
3004
  self.train_dataset = self.val_dataset = None
3005
- if train_list:
3006
  prefetch = bool(cfg.get("prefetch", False))
3007
- val_list = list(cfg.get("val_transform_list", cfg.get("eval_transform_list", [])))
3008
- self.train_dataset = ARForensicsDataset(cfg.root_dir, "train", AIGITransformations(train_list), prefetch=prefetch)
3009
- self.val_dataset = ARForensicsDataset(cfg.root_dir, "val", AIGITransformations(val_list), prefetch=prefetch)
3010
 
3011
  def _loader(self, dataset, batch_size, shuffle):
3012
  cfg = self.config.DatasetConfig
 
1
  from torch.utils.data import Dataset
2
  from typing import Literal
3
  from pathlib import Path
4
+ import functools
5
  import hashlib
6
  import json
7
  import os
 
23
  import torch
24
  import torch.nn.functional as F
25
  from decord import VideoReader, cpu
26
+ from omegaconf import OmegaConf
27
  from torch.utils.data import DataLoader
28
  from transformations import AIGITransformations
29
  from torchvision.io import decode_image, ImageReadMode
 
73
  return directory.is_dir() and any(_iter_videos(directory))
74
 
75
 
76
+ def _config_list(section, name):
77
+ """``section.<name>`` as a list, or None when the section or the key is absent or null."""
78
+ value = None if section is None else section.get(name, None)
79
+ return None if value is None else list(value)
80
+
81
+
82
+ DATASET_CONF_DIR = Path(__file__).resolve().parent / "conf" / "dataset"
83
+
84
+
85
+ @functools.lru_cache(maxsize=None)
86
+ def _dataset_yaml_lists(name: str) -> tuple[dict[str, list | None], ...]:
87
+ """The ``train`` / ``val`` / ``eval`` transform lists shipped in the dataset yaml files of dataset ``name`` (matched
88
+ by file stem or ``name:``; several files may share a name), i.e. what DatasetConfig holds unless overridden."""
89
+ found = []
90
+ for path in sorted(DATASET_CONF_DIR.rglob("*.yaml")):
91
+ cfg = OmegaConf.load(path)
92
+ if name in (path.stem, cfg.get("name", None)):
93
+ found.append({key: _config_list(cfg, f"{key}_transform_list") for key in ("train", "val", "eval")})
94
+ return tuple(found)
95
+
96
+
97
+ def _overridden(data, key: str, value: list) -> bool:
98
+ """Whether DatasetConfig's ``<key>_transform_list`` was overridden (differs from its dataset yaml). False when the
99
+ yaml file or the key cannot be found."""
100
+ shipped = [lists[key] for lists in _dataset_yaml_lists(str(data.get("name", ""))) if lists[key] is not None]
101
+ return bool(shipped) and value not in shipped
102
+
103
+
104
+ def _first_list(*candidates):
105
+ """The first candidate list that is set (not None), else None. An explicit ``[]`` counts as set: an empty list in
106
+ the model config intentionally stops the fallback to the dataset config (that split gets no transform)."""
107
+ return next((c for c in candidates if c is not None), None)
108
+
109
+
110
+ def resolve_transforms(config) -> tuple[AIGITransformations | None, AIGITransformations | None, AIGITransformations | None]:
111
+ """The ``(train, val, test)`` transforms of a run. Each comes from ``ModelConfig`` (the detector's own preprocessing)
112
+ and falls back to ``DatasetConfig`` for models whose config sets none. Precedence (first list set wins):
113
+
114
+ * train: model ``train_transform_list``, dataset ``train_transform_list``
115
+ * val: model ``val_transform_list``, model ``eval_transform_list``, dataset ``val_transform_list``, dataset
116
+ ``eval_transform_list``
117
+ * test: model ``test_transform_list`` (a batchable stand-in for native-resolution eval lists), model
118
+ ``eval_transform_list``, dataset ``eval_transform_list``
119
+
120
+ A DatasetConfig list that was overridden (differs from its dataset yaml, e.g. an old
121
+ ``DatasetConfig.eval_transform_list=`` override) but loses to a model list is reported.
122
+ An empty list gives None."""
123
+ model = getattr(config, "ModelConfig", None)
124
+ data = getattr(config, "DatasetConfig", None)
125
+ model_train = _config_list(model, "train_transform_list")
126
+ model_val = _first_list(_config_list(model, "val_transform_list"), _config_list(model, "eval_transform_list"))
127
+ model_test = _first_list(_config_list(model, "test_transform_list"), _config_list(model, "eval_transform_list"))
128
+ data_lists = {key: _config_list(data, f"{key}_transform_list") for key in ("train", "val", "eval")}
129
+ for key, model_list in (("train", model_train), ("val", model_val), ("eval", model_test)):
130
+ if (data_lists[key] is not None and model_list is not None and data_lists[key] != model_list
131
+ and _overridden(data, key, data_lists[key])):
132
+ print(f"[transforms] DatasetConfig.{key}_transform_list is ignored: the model config's list is used "
133
+ f"({model_list})", flush=True)
134
+ train = _first_list(model_train, data_lists["train"])
135
+ val = _first_list(model_val, data_lists["val"], data_lists["eval"])
136
+ test = _first_list(model_test, data_lists["eval"])
137
+ # Val and test convert the decoded uint8 tensors to PIL first, so they run the exact inference code (benchmarks
138
+ # and the web UI feed PIL; tensor resizes differ by 1-2/255, and some eval steps take PIL only). Train stays on
139
+ # tensors for speed.
140
+ return (AIGITransformations(train) if train else None,
141
+ AIGITransformations(val, pil_input=True) if val else None,
142
+ AIGITransformations(test, pil_input=True) if test else None)
143
+
144
+
145
  class _PILFrames:
146
  """The part of decord's ``VideoReader`` API that ``sample_video_frames`` uses, for animated images decord cannot open
147
  (GIFs: decord fails with "broken metadata" on them). Every frame is composited to RGB at the canvas size, and the
 
1199
  class CNNDetectionDatasetModule:
1200
  def __init__(self, config):
1201
  self.config = config
1202
+ self.train_transform, self.val_transform, self.test_transform = resolve_transforms(config)
 
 
 
1203
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1204
  classes = config.DatasetConfig.get("train_classes", None) # ProGAN classes for train/val (None: all 20)
1205
  classes = list(classes) if classes else None
1206
  self.train_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch, classes=classes)
1207
+ self.val_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "val", self.val_transform, prefetch=prefetch, classes=classes)
1208
+ self.test_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "test", self.test_transform, prefetch=prefetch)
1209
 
1210
  def train_dataloader(self):
1211
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
 
1217
  return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
1218
 
1219
 
1220
+ def _split_train_subset(dataset, per_class, val_per_class, val_transform, seed=0):
1221
  """Keep a seeded ``per_class`` images per label of ``dataset`` for training and return a disjoint ``val_per_class``
1222
+ per label as a validation copy with ``val_transform``: for recipes trained on part of a train split, validated on
1223
  held-out training images (GenImage's ``val`` alias is its test split, which must not select the model)."""
1224
  rng = random.Random(seed)
1225
  train_idx, val_idx = [], []
 
1235
  ds.images = [dataset.images[i] for i in keep]
1236
  ds.labels = [dataset.labels[i] for i in keep]
1237
  ds.folder_names = [dataset.folder_names[i] for i in keep]
1238
+ val.transform = val_transform
1239
  return val
1240
 
1241
 
1242
  class GenImageDatasetModule:
1243
  def __init__(self, config):
1244
  self.config = config
1245
+ self.train_transform, self.val_transform, self.test_transform = resolve_transforms(config)
 
 
 
1246
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1247
  source_url = config.DatasetConfig.get("source_url", GenImageDataset.google_drive_folder_url)
1248
  GenImageDataset.google_drive_folder_url = source_url
1249
  GenImageDataset.hf_repo_id = config.DatasetConfig.get("hf_repo_id", None)
1250
  GenImageDataset.hf_subdir = config.DatasetConfig.get("hf_subdir", None)
1251
  self.train_dataset = GenImageDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch)
1252
+ self.test_dataset = GenImageDataset(config.DatasetConfig.root_dir, "test", self.test_transform, prefetch=prefetch)
1253
  per_class = config.DatasetConfig.get("train_per_class", None) # e.g. VIB-Net: 16k real + 16k SD v1.4
1254
  if per_class:
1255
  self.val_dataset = _split_train_subset(self.train_dataset, int(per_class),
1256
+ int(config.DatasetConfig.get("val_per_class", 1000)), self.val_transform)
1257
  else:
1258
+ self.val_dataset = GenImageDataset(config.DatasetConfig.root_dir, "val", self.val_transform, prefetch=prefetch)
1259
 
1260
  def train_dataloader(self):
1261
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
 
1822
  class AIGIBenchDatasetModule:
1823
  def __init__(self, config):
1824
  self.config = config
1825
+ self.train_transform, self.val_transform, self.test_transform = resolve_transforms(config)
 
 
 
1826
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1827
  self.train_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "train", config.DatasetConfig.setting, self.train_transform, prefetch=prefetch)
1828
+ self.val_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "val", config.DatasetConfig.setting, self.val_transform, prefetch=prefetch)
1829
+ self.test_dataset = AIGIBenchDataset(config.DatasetConfig.root_dir, "test", config.DatasetConfig.setting, self.test_transform, prefetch=prefetch)
1830
 
1831
  def train_dataloader(self):
1832
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
 
1841
  class DiTFakeDatasetModule:
1842
  def __init__(self, config):
1843
  self.config = config
1844
+ self.test_transform = resolve_transforms(config)[2]
 
1845
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1846
  repo_id = config.DatasetConfig.get("repo_id", DiTFakeDataset.repo_id)
1847
+ self.test_dataset = DiTFakeDataset(config.DatasetConfig.root_dir, "test", self.test_transform, prefetch=prefetch, repo_id=repo_id)
1848
 
1849
  def test_dataloader(self):
1850
  return DataLoader(self.test_dataset, batch_size=self.config.DatasetConfig.test_batch_size, shuffle=False, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
 
1853
  class FreqNetDetectionDatasetModule:
1854
  def __init__(self, config):
1855
  self.config = config
1856
+ self.test_transform = resolve_transforms(config)[2]
 
1857
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1858
  source_url = config.DatasetConfig.get("source_url", FreqNetDetectionDataset.google_drive_folder_url)
1859
  self.test_dataset = FreqNetDetectionDataset(
1860
  config.DatasetConfig.root_dir,
1861
  "test",
1862
+ self.test_transform,
1863
  prefetch=prefetch,
1864
  source_url=source_url,
1865
  )
 
1877
  class UniversialFakeDetectionDatasetModule:
1878
  def __init__(self, config):
1879
  self.config = config
1880
+ self.test_transform = resolve_transforms(config)[2]
 
1881
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1882
  source_url = config.DatasetConfig.get("source_url", UniversialFakeDetectionDataset.source_url)
1883
  self.test_dataset = UniversialFakeDetectionDataset(
1884
  config.DatasetConfig.root_dir,
1885
  "test",
1886
+ self.test_transform,
1887
  prefetch=prefetch,
1888
  source_url=source_url,
1889
  )
 
1901
  class CoSpyDatasetModule:
1902
  def __init__(self, config):
1903
  self.config = config
1904
+ self.test_transform = resolve_transforms(config)[2]
 
1905
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1906
  source_datasets = config.DatasetConfig.get("source_datasets", None)
1907
  generator_models = config.DatasetConfig.get("generator_models", None)
 
1912
  self.test_dataset = CoSpyDataset(
1913
  config.DatasetConfig.root_dir,
1914
  "test",
1915
+ self.test_transform,
1916
  prefetch=prefetch,
1917
  source_datasets=list(source_datasets) if source_datasets else None,
1918
  generator_models=list(generator_models) if generator_models else None,
 
2073
  def __init__(self, config):
2074
  self.config = config
2075
  cfg = config.DatasetConfig
2076
+ self.train_transform, self.val_transform, self.test_transform = resolve_transforms(config)
 
 
 
2077
  prefetch = bool(cfg.get("prefetch", False))
2078
  common = dict(
2079
  num_frames=cfg.get("num_frames", 16),
 
2085
  resize_interp=cfg.get("resize_interp", "bilinear"),
2086
  )
2087
  self.train_dataset = VideoFolderDataset(cfg.root_dir, "train", self.train_transform, prefetch=prefetch, **common)
2088
+ self.val_dataset = VideoFolderDataset(cfg.root_dir, "val", self.val_transform, prefetch=prefetch, **common)
2089
+ self.test_dataset = VideoFolderDataset(cfg.root_dir, "test", self.test_transform, prefetch=prefetch, **common)
2090
 
2091
  def train_dataloader(self):
2092
  return DataLoader(self.train_dataset, batch_size=self.config.DatasetConfig.train_batch_size, shuffle=True, num_workers=self.config.DatasetConfig.num_workers, persistent_workers=self.config.DatasetConfig.persistent_workers, pin_memory=self.config.DatasetConfig.pin_memory)
 
2203
  def __init__(self, config):
2204
  self.config = config
2205
  cfg = config.DatasetConfig
2206
+ self.test_transform = resolve_transforms(config)[2]
 
2207
  self.test_dataset = self.DATASET_CLASS(
2208
+ cfg.root_dir, "test", self.test_transform, prefetch=bool(cfg.get("prefetch", False)),
2209
  num_frames=cfg.get("num_frames", 16), fps=cfg.get("fps", None),
2210
  window_seconds=cfg.get("window_seconds", 2.0), frame_size=cfg.get("frame_size", 224),
2211
  hf_repo_id=cfg.get("hf_repo_id", self.DATASET_CLASS.hf_repo_id),
 
2976
  def __init__(self, config):
2977
  self.config = config
2978
  cfg = config.DatasetConfig
2979
+ self.train_transform, self.val_transform, self.test_transform = resolve_transforms(config)
 
2980
  self.test_dataset = self.DATASET_CLASS(
2981
+ cfg.root_dir, "test", self.test_transform, prefetch=bool(cfg.get("prefetch", False)))
2982
 
2983
  def train_dataloader(self):
2984
  return None
 
3046
 
3047
 
3048
  class ARForensicsDatasetModule(_TestOnlyImageModule):
3049
+ """The test split for benchmarking; with ``DatasetConfig.with_train_splits`` (default false) also the train and val
3050
+ splits (D3QE's training data, 39 GB), with the run's train and val transforms (see ``resolve_transforms``)."""
3051
  DATASET_CLASS = ARForensicsDataset
3052
 
3053
  def __init__(self, config):
3054
  super().__init__(config)
3055
  cfg = config.DatasetConfig
 
3056
  self.train_dataset = self.val_dataset = None
3057
+ if bool(cfg.get("with_train_splits", False)):
3058
  prefetch = bool(cfg.get("prefetch", False))
3059
+ self.train_dataset = ARForensicsDataset(cfg.root_dir, "train", self.train_transform, prefetch=prefetch)
3060
+ self.val_dataset = ARForensicsDataset(cfg.root_dir, "val", self.val_transform, prefetch=prefetch)
 
3061
 
3062
  def _loader(self, dataset, batch_size, shuffle):
3063
  cfg = self.config.DatasetConfig
docs/ADD_DETECTOR.md CHANGED
@@ -19,13 +19,28 @@ for the maintainers and stop.
19
  - Imports + helper classes at module top-level (repo rule: no nested defs/imports).
20
  - Add `models/<modality>/<k>/__init__.py` and a `README.md` (see structure below).
21
  2. **`conf/model/image/<k>.yaml`** (or `video/`): `defaults: [/model/default, _self_]`, then `model_type: <k>`
22
- plus any model knobs. Add `conf/dataset/...` / `conf/trainer/...` only if a new recipe is needed.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  3. **Dispatch** — add a branch in `trainer.py` `_build_model` (the `if model_type == "<k>": return SynthScope<Name>Model(config)` chain, ~line 69+). The interface lazy-imports by `model_type` in `interface/backend/engine.py` — mirror it if needed.
24
  4. **Interface registry** — `interface/backend/registry.py` `_add(ModelSpec(...))`:
25
  - `key`, `name`, `paper`, `modality` (`image`|`video`), `overrides=["model@ModelConfig=image/<k>"]`,
26
  - `predict_kind`: `ce` (logits→`softmax[:,1]`), `bce` (`sigmoid`), `bce_npr`, `vib`, or `video`,
27
  - `checkpoint="<k>.pth"`, `ckpt_kind`: `synthscope` (our trained weights, strict load) **or** `official_<k>` + a `remap=remap_<k>` function (rename official keys → our module names) for the authors' released weights,
28
- - `transform`: the eval preprocessing — match the **official** eval exactly (resize vs crop, norm vs none). Add a `tf_*` helper if needed. (Getting this wrong silently tanks results — see SAFE: a resize destroyed the high-freq signal.)
29
  - `weights`: `"official"` | `"synthscope_trained"` | `"training_free"` (shown in the web UI badge).
30
  5. **Per-model `README.md`** — mirror `models/image/pgc/README.md`: short method explanation + official-repo/paper links, the pipeline, "SynthScope adaptations", and a results table in the **Paper / SynthScope_official / SynthScope_train** schema.
31
 
 
19
  - Imports + helper classes at module top-level (repo rule: no nested defs/imports).
20
  - Add `models/<modality>/<k>/__init__.py` and a `README.md` (see structure below).
21
  2. **`conf/model/image/<k>.yaml`** (or `video/`): `defaults: [/model/default, _self_]`, then `model_type: <k>`
22
+ plus any model knobs, and the model's preprocessing as named transform lists:
23
+ - `eval_transform_list` (required for image models): the **official** test-time preprocessing, used for
24
+ inference, benchmarks and the end-of-training test;
25
+ - `train_transform_list`: the training augmentation;
26
+ - `val_transform_list` (optional): validation during training, when it differs from the eval list;
27
+ - `test_transform_list` (optional): replaces the eval list for the end-of-training test only, e.g. a batchable
28
+ stand-in for SPAI's native-resolution eval, or AIDE's five views with the long side capped at 1024 to bound
29
+ dataloader-worker memory; benchmarks and inference still use `eval_transform_list`.
30
+
31
+ Each name is a step built by `AIGITransformations` in `transformations.py`; add new step names there.
32
+ The model's lists take precedence over the dataset's: the `train/eval/val_transform_list` of a
33
+ `conf/dataset/...` config are fallbacks, used only for models whose config sets none, and a warning is printed
34
+ when a dataset list is shadowed. Validation and test pipelines run on PIL images like inference, so an
35
+ end-of-training test with `eval_transform_list` preprocesses exactly as the benchmarks do.
36
+ Video models are out of scope for now: their preprocessing is the registry's video settings plus the dataset
37
+ lists. Add `conf/dataset/...` / `conf/trainer/...` only if a new recipe is needed.
38
  3. **Dispatch** — add a branch in `trainer.py` `_build_model` (the `if model_type == "<k>": return SynthScope<Name>Model(config)` chain, ~line 69+). The interface lazy-imports by `model_type` in `interface/backend/engine.py` — mirror it if needed.
39
  4. **Interface registry** — `interface/backend/registry.py` `_add(ModelSpec(...))`:
40
  - `key`, `name`, `paper`, `modality` (`image`|`video`), `overrides=["model@ModelConfig=image/<k>"]`,
41
  - `predict_kind`: `ce` (logits→`softmax[:,1]`), `bce` (`sigmoid`), `bce_npr`, `vib`, or `video`,
42
  - `checkpoint="<k>.pth"`, `ckpt_kind`: `synthscope` (our trained weights, strict load) **or** `official_<k>` + a `remap=remap_<k>` function (rename official keys → our module names) for the authors' released weights,
43
+ - `transform=model_transform("image/<k>")`: builds the eval preprocessing from the model config's `eval_transform_list` — match the **official** eval exactly there (resize vs crop, norm vs none). (Getting this wrong silently tanks results — see SAFE: a resize destroyed the high-freq signal.)
44
  - `weights`: `"official"` | `"synthscope_trained"` | `"training_free"` (shown in the web UI badge).
45
  5. **Per-model `README.md`** — mirror `models/image/pgc/README.md`: short method explanation + official-repo/paper links, the pipeline, "SynthScope adaptations", and a results table in the **Paper / SynthScope_official / SynthScope_train** schema.
46
 
docs/BENCHMARKS.md CHANGED
@@ -222,7 +222,8 @@ the GPU). Models are downloaded once into the Hugging Face cache (tens of GB eac
222
 
223
  - **A detector**: follow [`docs/ADD_DETECTOR.md`](ADD_DETECTOR.md); then add it to the Leaderboard as in section 3.
224
  - **A dataset**: add `conf/dataset/image/<name>.yaml` with the source (`hf_repo_id`, `source_url`, ...), make it follow the
225
- layout in section 2, and add a profile to `bench_eval.py` if its grouping is new.
 
226
  - **A generator**: add a function `call(image_png | None, prompt, size=None) -> png bytes` and one entry in `GENERATORS` in
227
  `interface/backend/generators.py`. `image_png=None` means "create from the prompt alone". Then run section 4 with
228
  `--generator <your key>`.
 
222
 
223
  - **A detector**: follow [`docs/ADD_DETECTOR.md`](ADD_DETECTOR.md); then add it to the Leaderboard as in section 3.
224
  - **A dataset**: add `conf/dataset/image/<name>.yaml` with the source (`hf_repo_id`, `source_url`, ...), make it follow the
225
+ layout in section 2, and add a profile to `bench_eval.py` if its grouping is new. Its transform lists are fallbacks,
226
+ used only for models whose config (`conf/model/...`) sets no lists of its own.
227
  - **A generator**: add a function `call(image_png | None, prompt, size=None) -> png bytes` and one entry in `GENERATORS` in
228
  `interface/backend/generators.py`. `image_png=None` means "create from the prompt alone". Then run section 4 with
229
  `--generator <your key>`.
docs/CAPABILITIES.md CHANGED
@@ -72,10 +72,12 @@ To wire it in: `conf/model/<modality>/<key>.yaml` (`model_type: <key>`), a branc
72
  with groups `model/`, `dataset/`, `trainer/`. Multi-GPU via 🤗 `accelerate` (`accelerate_config.yaml`).
73
  - **`dataset.py`** — image/video dataset loaders; `_decode_rgb` feeds uint8 `[3,H,W]` tensors;
74
  `sample_video_frames(..., edge_crop=)` for video.
75
- - **`transformations.py`** — composable named transforms / augmentations: `resize_224`,
76
- `resize_short_{256,384,448}`, `center_crop_{224,256,384,448}`, `random_crop_*`, `pad_random_crop_224`
77
  (PGC), `gaussian_blur`, `jpeg_compression`, `random_horizontal/vertical_flip`, `random_rotation_180`,
78
- `color_jitter_safe`, `to_tensor`/`to_dtype`. **Eval-transform fidelity is critical** — artifact/
 
 
79
  low-level detectors (SAFE/AIDE/SPAI/PGC/NPR) are wrecked by a wrong resize; match the official eval
80
  exactly (see `repro/investigate/pgc_gap_investigation.md` and the SAFE crop-not-resize lesson).
81
  - **`metrics.py`** — torchmetrics binary `Accuracy` (real/fake), `AveragePrecision` (AP),
@@ -170,6 +172,6 @@ New detectors are integrated **on a `detector/<key>` branch and pushed for revie
170
 
171
  1. Confirm it passes both rules (public repo + strong results). Read `ADD_DETECTOR.md`.
172
  2. `git checkout -b detector/<key>`; add the 5 files (model, conf, trainer dispatch, registry ModelSpec, README) + smoke test.
173
- 3. **Match the official eval transform exactly** in the `ModelSpec` (resize vs crop, norm) — this is the #1 silent-failure source.
174
  4. Reproduce on the paper's benchmark with official weights via `bench_eval.py --ckpt`; fix `remap_<k>` for key mismatches; record per-gen AP/AUROC in the Paper/SynthScope_official/SynthScope_train schema.
175
  5. (Optional) add an interpretability map. Push the branch; do **not** merge to `main`.
 
72
  with groups `model/`, `dataset/`, `trainer/`. Multi-GPU via 🤗 `accelerate` (`accelerate_config.yaml`).
73
  - **`dataset.py`** — image/video dataset loaders; `_decode_rgb` feeds uint8 `[3,H,W]` tensors;
74
  `sample_video_frames(..., edge_crop=)` for video.
75
+ - **`transformations.py`** — composable named transforms / augmentations: `resize_{224,256,336,512}`,
76
+ `resize_short_{256,384,448}`, `center_crop_{224,256,336,384,448}`, `random_crop_*`, `pad_random_crop_224`
77
  (PGC), `gaussian_blur`, `jpeg_compression`, `random_horizontal/vertical_flip`, `random_rotation_180`,
78
+ `color_jitter_safe`, `to_tensor`/`to_dtype`. Each model config names its pipelines (`eval_transform_list`,
79
+ `train_transform_list`, optional `val_transform_list` / `test_transform_list`); the web registry builds
80
+ `ModelSpec.transform` from `eval_transform_list`. **Eval-transform fidelity is critical** — artifact/
81
  low-level detectors (SAFE/AIDE/SPAI/PGC/NPR) are wrecked by a wrong resize; match the official eval
82
  exactly (see `repro/investigate/pgc_gap_investigation.md` and the SAFE crop-not-resize lesson).
83
  - **`metrics.py`** — torchmetrics binary `Accuracy` (real/fake), `AveragePrecision` (AP),
 
172
 
173
  1. Confirm it passes both rules (public repo + strong results). Read `ADD_DETECTOR.md`.
174
  2. `git checkout -b detector/<key>`; add the 5 files (model, conf, trainer dispatch, registry ModelSpec, README) + smoke test.
175
+ 3. **Match the official eval transform exactly** in the model config's `eval_transform_list` (resize vs crop, norm) — this is the #1 silent-failure source.
176
  4. Reproduce on the paper's benchmark with official weights via `bench_eval.py --ckpt`; fix `remap_<k>` for key mismatches; record per-gen AP/AUROC in the Paper/SynthScope_official/SynthScope_train schema.
177
  5. (Optional) add an interpretability map. Push the branch; do **not** merge to `main`.
experiments/image/adapted_dinov2_vitl14.sh CHANGED
@@ -6,9 +6,7 @@ export CUDA_VISIBLE_DEVICES=1,2,3,4
6
  export OMP_NUM_THREADS=1
7
 
8
  accelerate launch --config_file accelerate_config.yaml --num_processes 4 main.py \
 
9
  ModelConfig.bottleneck_dim=64 \
10
- ModelConfig.model_type=dino \
11
  ModelConfig.use_adapter=true \
12
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_224,random_horizontal_flip,to_dtype,normalize_dinov2] \
13
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_224,to_dtype,normalize_dinov2] \
14
  ModelConfig.backbone_name=dinov2_vitl14
 
6
  export OMP_NUM_THREADS=1
7
 
8
  accelerate launch --config_file accelerate_config.yaml --num_processes 4 main.py \
9
+ model@ModelConfig=image/adapted_dino \
10
  ModelConfig.bottleneck_dim=64 \
 
11
  ModelConfig.use_adapter=true \
 
 
12
  ModelConfig.backbone_name=dinov2_vitl14
experiments/image/aide.sh CHANGED
@@ -5,15 +5,16 @@ export TORCH_HOME=/nvmestore/ssalehi/.cache/torch
5
  export CUDA_VISIBLE_DEVICES=1,2
6
  export OMP_NUM_THREADS=1
7
 
 
 
8
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
9
  trainer@TrainerConfig=image/aide \
10
  model@ModelConfig=image/aide \
11
  dataset@DatasetConfig=image/genimage \
12
- DatasetConfig.train_batch_size=16 \
 
13
  DatasetConfig.val_batch_size=16 \
14
  DatasetConfig.test_batch_size=16 \
15
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_256,random_horizontal_flip,to_dtype] \
16
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_256,to_dtype] \
17
  logger.name=aide \
18
  logger.mode=online \
19
  logger.tags=[SynthScope,AIDE,genimage]
 
5
  export CUDA_VISIBLE_DEVICES=1,2
6
  export OMP_NUM_THREADS=1
7
 
8
+ # Five native-resolution views per image (aide.yaml lists): batch 8 and 5 workers bound memory, as in the run behind
9
+ # the README's SynthScope_train checkpoint (repro/retrain_aide_native_v2.sbatch).
10
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
11
  trainer@TrainerConfig=image/aide \
12
  model@ModelConfig=image/aide \
13
  dataset@DatasetConfig=image/genimage \
14
+ DatasetConfig.train_batch_size=8 \
15
+ DatasetConfig.num_workers=5 \
16
  DatasetConfig.val_batch_size=16 \
17
  DatasetConfig.test_batch_size=16 \
 
 
18
  logger.name=aide \
19
  logger.mode=online \
20
  logger.tags=[SynthScope,AIDE,genimage]
experiments/image/clip_vitl14.sh CHANGED
@@ -9,9 +9,7 @@ export OMP_NUM_THREADS=1
9
  # accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py
10
 
11
  accelerate launch --config_file accelerate_config.yaml --num_processes 4 main.py \
 
12
  ModelConfig.bottleneck_dim=64 \
13
- ModelConfig.model_type=clip \
14
  ModelConfig.use_adapter=false \
15
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_224,random_horizontal_flip,to_dtype,normalize_clip] \
16
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_224,to_dtype,normalize_clip] \
17
  ModelConfig.backbone_name=CLIP:ViT-L/14
 
9
  # accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py
10
 
11
  accelerate launch --config_file accelerate_config.yaml --num_processes 4 main.py \
12
+ model@ModelConfig=image/clip \
13
  ModelConfig.bottleneck_dim=64 \
 
14
  ModelConfig.use_adapter=false \
 
 
15
  ModelConfig.backbone_name=CLIP:ViT-L/14
experiments/image/co_spy.sh CHANGED
@@ -11,8 +11,6 @@ export OMP_NUM_THREADS=1
11
  DatasetConfig.train_batch_size=16 \
12
  DatasetConfig.val_batch_size=16 \
13
  DatasetConfig.test_batch_size=16 \
14
- DatasetConfig.train_transform_list=[resize_short_384,gaussian_blur,jpeg_compression,random_crop_384,random_horizontal_flip,to_dtype] \
15
- DatasetConfig.eval_transform_list=[resize_short_384,center_crop_384,to_dtype] \
16
  logger.name=co_spy \
17
  logger.mode=online \
18
  logger.tags=[SynthScope,CoSpy,genimage]
 
11
  DatasetConfig.train_batch_size=16 \
12
  DatasetConfig.val_batch_size=16 \
13
  DatasetConfig.test_batch_size=16 \
 
 
14
  logger.name=co_spy \
15
  logger.mode=online \
16
  logger.tags=[SynthScope,CoSpy,genimage]
experiments/image/d3qe.sh CHANGED
@@ -22,11 +22,9 @@ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/d3qe}
22
  model@ModelConfig=image/d3qe \
23
  dataset@DatasetConfig=image/arforensics \
24
  DatasetConfig.root_dir=$DATA \
 
25
  DatasetConfig.train_batch_size=32 \
26
  DatasetConfig.val_batch_size=64 \
27
- "+DatasetConfig.train_transform_list=[resize_short_256,blur_jpeg_cnndet,random_crop_256,random_horizontal_flip,to_dtype]" \
28
- "+DatasetConfig.val_transform_list=[resize_short_256,blur_jpeg_cnndet_val,center_crop_256,to_dtype]" \
29
- "DatasetConfig.eval_transform_list=[resize_short_256,center_crop_256,to_dtype]" \
30
  TrainerConfig.save_path=$SAVE \
31
  "metrics=[real_accuracy,fake_accuracy]" \
32
  logger.name=d3qe \
 
22
  model@ModelConfig=image/d3qe \
23
  dataset@DatasetConfig=image/arforensics \
24
  DatasetConfig.root_dir=$DATA \
25
+ DatasetConfig.with_train_splits=true \
26
  DatasetConfig.train_batch_size=32 \
27
  DatasetConfig.val_batch_size=64 \
 
 
 
28
  TrainerConfig.save_path=$SAVE \
29
  "metrics=[real_accuracy,fake_accuracy]" \
30
  logger.name=d3qe \
experiments/image/dinov2_vitl14.sh CHANGED
@@ -9,9 +9,7 @@ export OMP_NUM_THREADS=1
9
  # accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py
10
 
11
  accelerate launch --config_file accelerate_config.yaml --num_processes 4 main.py \
 
12
  ModelConfig.bottleneck_dim=64 \
13
- ModelConfig.model_type=dinov2 \
14
  ModelConfig.use_adapter=false \
15
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_224,random_horizontal_flip,to_dtype,normalize_dinov2] \
16
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_224,to_dtype,normalize_dinov2] \
17
  ModelConfig.backbone_name=dinov2_vitl14
 
9
  # accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py
10
 
11
  accelerate launch --config_file accelerate_config.yaml --num_processes 4 main.py \
12
+ model@ModelConfig=image/dinov2 \
13
  ModelConfig.bottleneck_dim=64 \
 
14
  ModelConfig.use_adapter=false \
 
 
15
  ModelConfig.backbone_name=dinov2_vitl14
experiments/image/dinov3_vitl14.sh CHANGED
@@ -9,9 +9,7 @@ export OMP_NUM_THREADS=1
9
  # accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py
10
 
11
  accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
 
12
  ModelConfig.bottleneck_dim=64 \
13
- ModelConfig.model_type=dinov3 \
14
  ModelConfig.use_adapter=false \
15
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_256,random_horizontal_flip,to_dtype,normalize_dinov3] \
16
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_256,to_dtype,normalize_dinov3] \
17
  ModelConfig.backbone_name=dinov3_vitl16
 
9
  # accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py
10
 
11
  accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
12
+ model@ModelConfig=image/dinov3 \
13
  ModelConfig.bottleneck_dim=64 \
 
14
  ModelConfig.use_adapter=false \
 
 
15
  ModelConfig.backbone_name=dinov3_vitl16
experiments/image/fat_former.sh CHANGED
@@ -5,10 +5,6 @@ export TORCH_HOME=/nvmestore/ssalehi/.cache/torch
5
  export CUDA_VISIBLE_DEVICES=1,2
6
  export OMP_NUM_THREADS=1
7
 
8
- # Official FatFormer (Michel-liu/FatFormer, utils/dataset.py) normalizes with **ImageNet**
9
- # mean/std -- NOT CLIP stats -- despite the CLIP ViT-L/14 backbone. `normalize_dinov2` is the
10
- # ImageNet Normalize in this repo, so we use it here to match the official preprocessing (and the
11
- # deployed official-weights eval, which reproduces 98.39 acc / 99.70 mAP with ImageNet norm).
12
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
13
  trainer@TrainerConfig=image/fat_former \
14
  model@ModelConfig=image/fat_former \
@@ -16,8 +12,6 @@ export OMP_NUM_THREADS=1
16
  DatasetConfig.train_batch_size=32 \
17
  DatasetConfig.val_batch_size=32 \
18
  DatasetConfig.test_batch_size=32 \
19
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_224,random_horizontal_flip,to_dtype,normalize_dinov2] \
20
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_224,to_dtype,normalize_dinov2] \
21
  logger.name=fat_former \
22
  logger.mode=online \
23
  logger.tags=[SynthScope,FatFormer,aigibench]
 
5
  export CUDA_VISIBLE_DEVICES=1,2
6
  export OMP_NUM_THREADS=1
7
 
 
 
 
 
8
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
9
  trainer@TrainerConfig=image/fat_former \
10
  model@ModelConfig=image/fat_former \
 
12
  DatasetConfig.train_batch_size=32 \
13
  DatasetConfig.val_batch_size=32 \
14
  DatasetConfig.test_batch_size=32 \
 
 
15
  logger.name=fat_former \
16
  logger.mode=online \
17
  logger.tags=[SynthScope,FatFormer,aigibench]
experiments/image/npr.sh CHANGED
@@ -25,8 +25,6 @@ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/npr}
25
  "+DatasetConfig.train_classes=[car,cat,chair,horse]" \
26
  DatasetConfig.train_batch_size=32 \
27
  DatasetConfig.val_batch_size=64 \
28
- "DatasetConfig.train_transform_list=[resize_256,random_crop_224,random_horizontal_flip,to_dtype,normalize_dinov2]" \
29
- "DatasetConfig.eval_transform_list=[resize_256,center_crop_224,to_dtype,normalize_dinov2]" \
30
  TrainerConfig.save_path=$SAVE \
31
  "metrics=[real_accuracy,fake_accuracy]" \
32
  logger.name=npr \
 
25
  "+DatasetConfig.train_classes=[car,cat,chair,horse]" \
26
  DatasetConfig.train_batch_size=32 \
27
  DatasetConfig.val_batch_size=64 \
 
 
28
  TrainerConfig.save_path=$SAVE \
29
  "metrics=[real_accuracy,fake_accuracy]" \
30
  logger.name=npr \
experiments/image/pgc.sh CHANGED
@@ -25,8 +25,6 @@ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/pgc}
25
  DatasetConfig.root_dir=$DATA \
26
  DatasetConfig.train_batch_size=32 \
27
  DatasetConfig.val_batch_size=64 \
28
- "DatasetConfig.train_transform_list=[random_horizontal_flip,color_jitter_pgc,pad_random_crop_224,to_dtype,normalize_dinov2]" \
29
- "DatasetConfig.eval_transform_list=[center_crop_224,to_dtype,normalize_dinov2]" \
30
  TrainerConfig.save_path=$SAVE \
31
  "metrics=[real_accuracy,fake_accuracy]" \
32
  logger.name=pgc \
 
25
  DatasetConfig.root_dir=$DATA \
26
  DatasetConfig.train_batch_size=32 \
27
  DatasetConfig.val_batch_size=64 \
 
 
28
  TrainerConfig.save_path=$SAVE \
29
  "metrics=[real_accuracy,fake_accuracy]" \
30
  logger.name=pgc \
experiments/image/safe.sh CHANGED
@@ -7,14 +7,12 @@ export OMP_NUM_THREADS=1
7
 
8
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
9
  trainer@TrainerConfig=image/safe \
 
10
  dataset@DatasetConfig=image/aigibench \
11
- ModelConfig.model_type=safe \
12
  ModelConfig.freeze_backbone=false \
13
  DatasetConfig.train_batch_size=64 \
14
  DatasetConfig.val_batch_size=64 \
15
  DatasetConfig.test_batch_size=64 \
16
- DatasetConfig.train_transform_list=[random_crop_256_pad,random_horizontal_flip,random_rotation_180,color_jitter_safe,to_dtype,random_mask_safe] \
17
- DatasetConfig.eval_transform_list=[center_crop_256,to_dtype] \
18
  TrainerConfig.epochs=100 \
19
  TrainerConfig.lr=0.0005 \
20
  TrainerConfig.weight_decay=0.05 \
 
7
 
8
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
9
  trainer@TrainerConfig=image/safe \
10
+ model@ModelConfig=image/safe \
11
  dataset@DatasetConfig=image/aigibench \
 
12
  ModelConfig.freeze_backbone=false \
13
  DatasetConfig.train_batch_size=64 \
14
  DatasetConfig.val_batch_size=64 \
15
  DatasetConfig.test_batch_size=64 \
 
 
16
  TrainerConfig.epochs=100 \
17
  TrainerConfig.lr=0.0005 \
18
  TrainerConfig.weight_decay=0.05 \
experiments/image/spai.sh CHANGED
@@ -5,15 +5,12 @@ export TORCH_HOME=/nvmestore/ssalehi/.cache/torch
5
  export CUDA_VISIBLE_DEVICES=1,2
6
  export OMP_NUM_THREADS=1
7
 
8
- # SPAI expects [0,1] images (to_dtype only); ImageNet normalization is applied inside the model.
9
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
10
  trainer@TrainerConfig=image/spai \
11
  model@ModelConfig=image/spai \
12
  DatasetConfig.train_batch_size=32 \
13
  DatasetConfig.val_batch_size=32 \
14
  DatasetConfig.test_batch_size=32 \
15
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_224,random_horizontal_flip,to_dtype] \
16
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_224,to_dtype] \
17
  logger.name=spai \
18
  logger.mode=online \
19
  logger.tags=[SynthScope,SPAI,genimage]
 
5
  export CUDA_VISIBLE_DEVICES=1,2
6
  export OMP_NUM_THREADS=1
7
 
 
8
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
9
  trainer@TrainerConfig=image/spai \
10
  model@ModelConfig=image/spai \
11
  DatasetConfig.train_batch_size=32 \
12
  DatasetConfig.val_batch_size=32 \
13
  DatasetConfig.test_batch_size=32 \
 
 
14
  logger.name=spai \
15
  logger.mode=online \
16
  logger.tags=[SynthScope,SPAI,genimage]
experiments/image/vib_net.sh CHANGED
@@ -31,8 +31,6 @@ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/vib_net}
31
  DatasetConfig.prefetch=false \
32
  DatasetConfig.train_batch_size=256 \
33
  DatasetConfig.val_batch_size=256 \
34
- "DatasetConfig.train_transform_list=[blur_jpeg_cnndet,pad_random_crop_224,random_horizontal_flip,to_dtype,normalize_clip]" \
35
- "DatasetConfig.eval_transform_list=[blur_jpeg_cnndet_val,center_crop_224,to_dtype,normalize_clip]" \
36
  TrainerConfig.save_path=$SAVE \
37
  "metrics=[real_accuracy,fake_accuracy]" \
38
  logger.name=vib_net \
 
31
  DatasetConfig.prefetch=false \
32
  DatasetConfig.train_batch_size=256 \
33
  DatasetConfig.val_batch_size=256 \
 
 
34
  TrainerConfig.save_path=$SAVE \
35
  "metrics=[real_accuracy,fake_accuracy]" \
36
  logger.name=vib_net \
experiments/video/d3.sh CHANGED
@@ -8,7 +8,7 @@ export OMP_NUM_THREADS=1
8
  # D3 is training-free: a frozen CLIP ViT-B/16 extracts per-frame features, and the per-video
9
  # second-order std becomes the detection score. Frames are decoded to 224x224 in [0,1] and
10
  # normalized by the dataset transform pipeline (normalize_clip here, matched to the CLIP
11
- # backbone -- switch to normalize_dinov2 if you set a DINOv2/ResNet backbone). AP/AUROC are the
12
  # meaningful metrics. The video benchmark must be present under DatasetConfig.root_dir;
13
  # this is a template (data download is out of scope).
14
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 1 main.py \
 
8
  # D3 is training-free: a frozen CLIP ViT-B/16 extracts per-frame features, and the per-video
9
  # second-order std becomes the detection score. Frames are decoded to 224x224 in [0,1] and
10
  # normalized by the dataset transform pipeline (normalize_clip here, matched to the CLIP
11
+ # backbone -- switch to normalize_imagenet if you set a DINOv2/ResNet backbone). AP/AUROC are the
12
  # meaningful metrics. The video benchmark must be present under DatasetConfig.root_dir;
13
  # this is a template (data download is out of scope).
14
  /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 1 main.py \
experiments/video/restrav.sh CHANGED
@@ -10,7 +10,7 @@ export OMP_NUM_THREADS=1
10
  # 224x224 over a CENTER 2 s window (24 frames, BICUBIC) and fed to DINOv2 as RAW [0,1] with
11
  # *no* ImageNet normalization -- this matches the official ReStraV repo verbatim
12
  # (dinov2_features.preprocess = frames/255 + BICUBIC resize, no Normalize) and the deployed
13
- # serving spec (registry video_norm = identity). Do NOT add normalize_dinov2 here: the
14
  # detector is served raw [0,1], so normalizing only at train time creates a train/serve
15
  # mismatch that collapses cross-generator transfer. Official optimizer is plain Adam (no
16
  # weight decay), 10 epochs -- matched below. The video benchmark must be present under
 
10
  # 224x224 over a CENTER 2 s window (24 frames, BICUBIC) and fed to DINOv2 as RAW [0,1] with
11
  # *no* ImageNet normalization -- this matches the official ReStraV repo verbatim
12
  # (dinov2_features.preprocess = frames/255 + BICUBIC resize, no Normalize) and the deployed
13
+ # serving spec (registry video_norm = identity). Do NOT add normalize_imagenet here: the
14
  # detector is served raw [0,1], so normalizing only at train time creates a train/serve
15
  # mismatch that collapses cross-generator transfer. Official optimizer is plain Adam (no
16
  # weight decay), 10 epochs -- matched below. The video benchmark must be present under
experiments/video/stall.sh CHANGED
@@ -8,7 +8,7 @@ export OMP_NUM_THREADS=1
8
  # STALL (Ben Hayun et al., CVPR'26) is training-free: a frozen DINOv3 ViT-L/16 extracts
9
  # per-frame features, which are scored against a VATEX real-video calibration via two whitened
10
  # isotropic-Gaussian likelihood branches (spatial + temporal). Frames are decoded square at 224
11
- # in [0,1] and normalized with ImageNet stats -- normalize_dinov2 here, because the official
12
  # DINOv3 transform uses ImageNet mean/std (NOT normalize_dinov3). AP/AUROC are the meaningful
13
  # metrics. The video benchmark must be present under DatasetConfig.root_dir.
14
  #
@@ -24,8 +24,8 @@ export DINO_V3_WEIGHTS=${DINO_V3_WEIGHTS:-/hddstore/ssalehi/SynthScope/dinov3_ch
24
  DatasetConfig.test_batch_size=8 \
25
  DatasetConfig.num_frames=16 \
26
  DatasetConfig.frame_size=224 \
27
- DatasetConfig.train_transform_list=[normalize_dinov2] \
28
- DatasetConfig.eval_transform_list=[normalize_dinov2] \
29
  metrics=[real_accuracy,fake_accuracy,AP,AUC] \
30
  logger.name=stall \
31
  logger.mode=online \
 
8
  # STALL (Ben Hayun et al., CVPR'26) is training-free: a frozen DINOv3 ViT-L/16 extracts
9
  # per-frame features, which are scored against a VATEX real-video calibration via two whitened
10
  # isotropic-Gaussian likelihood branches (spatial + temporal). Frames are decoded square at 224
11
+ # in [0,1] and normalized with ImageNet stats -- normalize_imagenet here, because the official
12
  # DINOv3 transform uses ImageNet mean/std (NOT normalize_dinov3). AP/AUROC are the meaningful
13
  # metrics. The video benchmark must be present under DatasetConfig.root_dir.
14
  #
 
24
  DatasetConfig.test_batch_size=8 \
25
  DatasetConfig.num_frames=16 \
26
  DatasetConfig.frame_size=224 \
27
+ DatasetConfig.train_transform_list=[normalize_imagenet] \
28
+ DatasetConfig.eval_transform_list=[normalize_imagenet] \
29
  metrics=[real_accuracy,fake_accuracy,AP,AUC] \
30
  logger.name=stall \
31
  logger.mode=online \
interface/backend/engine.py CHANGED
@@ -13,7 +13,6 @@ from collections import OrderedDict
13
 
14
  import numpy as np
15
  import torch
16
- import torchvision.transforms as T
17
  from PIL import Image
18
  from safetensors.torch import load_file as load_safetensors
19
 
@@ -61,14 +60,12 @@ def _overlay_to_png_b64(sal: torch.Tensor, base: Image.Image, out_size: int = 28
61
 
62
 
63
  def _geometric_base(spec, pil: Image.Image) -> Image.Image:
64
- """The model's input region in RGB (apply only the Resize/CenterCrop steps of the eval
65
- transform, skipping ToTensor/Normalize) so the overlay aligns with the saliency. Custom
66
  transforms (AIDE/SPAI native-res) fall back to the native image."""
67
- import torchvision.transforms as T
68
  base = pil.convert("RGB")
69
- for t in getattr(spec.transform, "transforms", []) or []:
70
- if isinstance(t, (T.Resize, T.CenterCrop)):
71
- base = t(base)
72
  return base
73
 
74
 
@@ -329,7 +326,10 @@ class Engine:
329
  elif kind == "aide_dct": # AIDE: native-res DCT per-window energy (patch selection)
330
  if pil is None:
331
  return None
332
- sal = spec.transform.dct_energy_map(pil) # [nH,Wn]
 
 
 
333
  elif kind == "spai_patch": # SPAI: per-patch spectral fake-evidence over the 224-grid
334
  sal = model.patch_fake_map(x) # [nH,nW] or None (five-crop/large grid)
335
  if sal is None:
@@ -351,7 +351,7 @@ class Engine:
351
  t = x[0].detach().float().cpu()
352
  if t.dim() == 4:
353
  t = torch.cat(list(t), dim=2)
354
- norm = next((s for s in getattr(spec.transform, "transforms", []) if isinstance(s, T.Normalize)), None)
355
  if norm is not None:
356
  t = t * torch.tensor(norm.std)[:, None, None] + torch.tensor(norm.mean)[:, None, None]
357
  elif spec.key == "cure": # CuRe maps pixels to (x - 127.5) / 127.5
 
13
 
14
  import numpy as np
15
  import torch
 
16
  from PIL import Image
17
  from safetensors.torch import load_file as load_safetensors
18
 
 
60
 
61
 
62
  def _geometric_base(spec, pil: Image.Image) -> Image.Image:
63
+ """The model's input region in RGB (apply only the resize/center-crop steps of the eval
64
+ transform, skipping to_dtype/normalize) so the overlay aligns with the saliency. Custom
65
  transforms (AIDE/SPAI native-res) fall back to the native image."""
 
66
  base = pil.convert("RGB")
67
+ for t in spec.transform.geometric_steps():
68
+ base = t(base)
 
69
  return base
70
 
71
 
 
326
  elif kind == "aide_dct": # AIDE: native-res DCT per-window energy (patch selection)
327
  if pil is None:
328
  return None
329
+ five_view = next((t for t in spec.transform.transforms if hasattr(t, "dct_energy_map")), None)
330
+ if five_view is None:
331
+ return None
332
+ sal = five_view.dct_energy_map(pil) # [nH,Wn]
333
  elif kind == "spai_patch": # SPAI: per-patch spectral fake-evidence over the 224-grid
334
  sal = model.patch_fake_map(x) # [nH,nW] or None (five-crop/large grid)
335
  if sal is None:
 
351
  t = x[0].detach().float().cpu()
352
  if t.dim() == 4:
353
  t = torch.cat(list(t), dim=2)
354
+ norm = spec.transform.normalize_step()
355
  if norm is not None:
356
  t = t * torch.tensor(norm.std)[:, None, None] + torch.tensor(norm.mean)[:, None, None]
357
  elif spec.key == "cure": # CuRe maps pixels to (x - 127.5) / 127.5