SMSD75 commited on
Commit
1e6e499
·
verified ·
1 Parent(s): 09234a9

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. LICENSES.md +2 -0
  2. conf/dataset/image/arforensics.yaml +2 -0
  3. conf/dataset/image/mirage_test.yaml +16 -0
  4. conf/model/image/omniaid.yaml +18 -0
  5. conf/model/image/omniaid_genimage.yaml +18 -0
  6. conf/model/image/omniaid_v1.yaml +18 -0
  7. conf/trainer/image/d3qe.yaml +24 -0
  8. conf/trainer/image/npr.yaml +13 -5
  9. conf/trainer/image/pgc.yaml +17 -6
  10. dataset.py +102 -11
  11. deploy/hf-space/fetch_weights.py +2 -2
  12. docs/BENCHMARKS.md +1 -0
  13. docs/CAPABILITIES.md +1 -0
  14. docs/WILD_BENCHMARKS.md +1 -0
  15. experiments/image/d3qe.sh +34 -0
  16. experiments/image/npr.sh +28 -14
  17. experiments/image/pgc.sh +25 -18
  18. interface/backend/build_repro_full.py +24 -7
  19. interface/backend/datasets.py +3 -0
  20. interface/backend/engine.py +3 -0
  21. interface/backend/registry.py +23 -0
  22. interface/backend/repro_full.json +160 -18
  23. models/image/d3qe/README.md +21 -4
  24. models/image/d3qe/d3qe.py +5 -3
  25. models/image/dda/dda.py +4 -3
  26. models/image/npr/README.md +28 -3
  27. models/image/npr/npr.py +7 -0
  28. models/image/omniaid/README.md +88 -0
  29. models/image/omniaid/__init__.py +0 -0
  30. models/image/omniaid/official_arch.py +681 -0
  31. models/image/omniaid/omniaid.py +46 -0
  32. models/image/pgc/README.md +38 -7
  33. pyproject.toml +1 -0
  34. repro/MASTER_RESULTS.jsonl +39 -0
  35. repro/RESULTS_SUMMARY.md +23 -7
  36. repro/bench_results/d3qe_trained_arforensics.json +67 -0
  37. repro/bench_results/d3qe_trained_cnndetection.json +67 -0
  38. repro/bench_results/d3qe_trained_ep3_arforensics.json +67 -0
  39. repro/bench_results/d3qe_trained_genimage_full.json +60 -0
  40. repro/bench_results/npr_official3090_forensynths8.json +74 -0
  41. repro/bench_results/npr_official3090_ufd.json +74 -0
  42. repro/bench_results/npr_official_nprpth_forensynths8.json +74 -0
  43. repro/bench_results/npr_official_nprpth_ufd.json +74 -0
  44. repro/bench_results/npr_officialcode_rerun_forensynths8.json +74 -0
  45. repro/bench_results/npr_officialcode_rerun_ufd.json +74 -0
  46. repro/bench_results/npr_trained_best_forensynths8.json +74 -0
  47. repro/bench_results/npr_trained_best_ufd.json +74 -0
  48. repro/bench_results/npr_trained_last_forensynths8.json +74 -0
  49. repro/bench_results/npr_trained_last_ufd.json +74 -0
  50. repro/bench_results/omniaid_bfree_online.json +25 -0
LICENSES.md CHANGED
@@ -33,6 +33,7 @@ third-party components, and every **pretrained checkpoint / backbone** SynthScop
33
  | `cure` | CuRe (arXiv 2609.14316) | manic-cui/CuRe | Apache-2.0 (repo `LICENSE`) | **official** 25 MB adapter (`cure_adapter.pt`, loads as `cure.pth`) | Apache-2.0 (shipped in the repo; verify before redistributing) |
34
  | `d3qe` | D3QE (ICCV'25) | Zhangyr2022/D3QE | MIT (repo `LICENSE`); `official/` vendors its network code — see the file headers | **official** checkpoint (`model_epoch_best.pth` from HF `Yanran21/D3QE`, loads as `d3qe.pth`) | MIT (HF model card license tag) |
35
  | `dda` | DDA (NeurIPS'25) | roy-ch/Dual-Data-Alignment | Apache-2.0 (repo `LICENSE`); `official_arch.py` vendors its `lora.py` — see its header | **official** checkpoint (`DDA_ckpt.pth` from HF `Junwei-Xi/Dual-Data-Alignment`, loads as `dda.pth`) | Apache-2.0 (HF model card license tag) |
 
36
  | `dinov2` | DINOv2 probe | facebookresearch/dinov2 | Apache-2.0 (verify) | DINOv2 backbone | Apache-2.0 (verify) |
37
  | `dinov3` | DINOv3 probe | facebookresearch/dinov3 | DINOv3 License (gated) | DINOv3 backbone | **gated** — DINOv3 License |
38
  | `adapted_dino` | Adapter-tuned DINOv2 (SynthScope) | this repo | Apache-2.0 | — (SynthScope-trained) | n/a |
@@ -45,6 +46,7 @@ third-party components, and every **pretrained checkpoint / backbone** SynthScop
45
  | Backbone | Used by | License |
46
  | --- | --- | --- |
47
  | OpenAI CLIP (ViT-L/14) | `clip`, `fat_former`, `d3`, `d3qe` | MIT (verify) |
 
48
  | OpenCLIP (ConvNeXt-XXL, LAION) | `aide` | code MIT; weights per LAION / model card (verify) |
49
  | DINOv2 (ViT-L/14, ViT-S/14) | `dinov2`, `adapted_dino`, `restrav`, `dda` | Apache-2.0 (verify) |
50
  | DINOv3 (ViT-L/16) | `dinov3`, `stall` | **gated** — DINOv3 License (non-commercial; accept upstream) |
 
33
  | `cure` | CuRe (arXiv 2609.14316) | manic-cui/CuRe | Apache-2.0 (repo `LICENSE`) | **official** 25 MB adapter (`cure_adapter.pt`, loads as `cure.pth`) | Apache-2.0 (shipped in the repo; verify before redistributing) |
34
  | `d3qe` | D3QE (ICCV'25) | Zhangyr2022/D3QE | MIT (repo `LICENSE`); `official/` vendors its network code — see the file headers | **official** checkpoint (`model_epoch_best.pth` from HF `Yanran21/D3QE`, loads as `d3qe.pth`) | MIT (HF model card license tag) |
35
  | `dda` | DDA (NeurIPS'25) | roy-ch/Dual-Data-Alignment | Apache-2.0 (repo `LICENSE`); `official_arch.py` vendors its `lora.py` — see its header | **official** checkpoint (`DDA_ckpt.pth` from HF `Junwei-Xi/Dual-Data-Alignment`, loads as `dda.pth`) | Apache-2.0 (HF model card license tag) |
36
+ | `omniaid`, `omniaid_v1`, `omniaid_genimage` | OmniAID (arXiv 2511.08423) | yunncheng/OmniAID | MIT (README badge; no LICENSE file in the repo); `official_arch.py` vendors `models/OmniAID.py` — see its header | **official** checkpoints (`ckpt/checkpoint_omniaid_{v2,v1,genimage_sd14}.pth` from HF `Yunncheng/OmniAID`, loaded as `<key>.pth`) | MIT (HF model card license tag) |
37
  | `dinov2` | DINOv2 probe | facebookresearch/dinov2 | Apache-2.0 (verify) | DINOv2 backbone | Apache-2.0 (verify) |
38
  | `dinov3` | DINOv3 probe | facebookresearch/dinov3 | DINOv3 License (gated) | DINOv3 backbone | **gated** — DINOv3 License |
39
  | `adapted_dino` | Adapter-tuned DINOv2 (SynthScope) | this repo | Apache-2.0 | — (SynthScope-trained) | n/a |
 
46
  | Backbone | Used by | License |
47
  | --- | --- | --- |
48
  | OpenAI CLIP (ViT-L/14) | `clip`, `fat_former`, `d3`, `d3qe` | MIT (verify) |
49
+ | OpenAI CLIP ViT-L/14@336 (HF `openai/clip-vit-large-patch14-336`) | `omniaid`, `omniaid_v1`, `omniaid_genimage` | MIT |
50
  | OpenCLIP (ConvNeXt-XXL, LAION) | `aide` | code MIT; weights per LAION / model card (verify) |
51
  | DINOv2 (ViT-L/14, ViT-S/14) | `dinov2`, `adapted_dino`, `restrav`, `dda` | Apache-2.0 (verify) |
52
  | DINOv3 (ViT-L/16) | `dinov3`, `stall` | **gated** — DINOv3 License (non-commercial; accept upstream) |
conf/dataset/image/arforensics.yaml CHANGED
@@ -3,6 +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
  test_batch_size: 512
7
  num_workers: 8
8
  persistent_workers: true
 
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
10
  persistent_workers: true
conf/dataset/image/mirage_test.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: mirage_test
2
+ # Mirage-Test (OmniAID, arXiv 2511.08423) — real/fake images from recent realism-tuned generators in five categories
3
+ # (Human, Animal, Object, Scene, Anime; Anime is fakes only and is paired with the other categories' reals).
4
+ # (HF Yunncheng/Mirage-Test, MIT)
5
+ root_dir: /hddstore/ssalehi/SynthScope/MirageTest
6
+ hf_repo_id: Yunncheng/Mirage-Test
7
+ test_batch_size: 512
8
+ num_workers: 8
9
+ persistent_workers: true
10
+ pin_memory: true
11
+ prefetch: true
12
+ eval_transform_list:
13
+ - resize_short_256
14
+ - center_crop_224
15
+ - to_dtype
16
+ - normalize_clip
conf/model/image/omniaid.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # OmniAID v2 (recommended; Mirage-Train, more robust augmentation). Expert configuration of the released checkpoint_omniaid_v2.pth (clean_test.py settings);
6
+ # read from the checkpoint's shapes (experts, rank, router width; top-k from the official config).
7
+ model_type: omniaid
8
+ CLIP_path: openai/clip-vit-large-patch14-336
9
+ num_experts: 6
10
+ rank_per_expert: 1
11
+ moe_router_hidden_dim: 256
12
+ moe_top_k: 2
13
+ moe_lambda_orth: 0.001
14
+ moe_lambda_balance: 0.001
15
+ moe_lambda_gating_cls: 0.1
16
+ dropout: 0.0
17
+ gradient_checkpointing_enable: false
18
+ is_hybrid: true
conf/model/image/omniaid_genimage.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # OmniAID GenImage-SD v1.4 (the paper's GenImage/Mirage/AIGCDetect tables). Expert configuration of the released checkpoint_omniaid_genimage_sd14.pth (config_omniaid_genimage_paper.json);
6
+ # read from the checkpoint's shapes (experts, rank, router width; top-k from the official config).
7
+ model_type: omniaid
8
+ CLIP_path: openai/clip-vit-large-patch14-336
9
+ num_experts: 3
10
+ rank_per_expert: 4
11
+ moe_router_hidden_dim: 64
12
+ moe_top_k: 1
13
+ moe_lambda_orth: 0.01
14
+ moe_lambda_balance: 0.1
15
+ moe_lambda_gating_cls: 0.1
16
+ dropout: 0.0
17
+ gradient_checkpointing_enable: false
18
+ is_hybrid: true
conf/model/image/omniaid_v1.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/default
3
+ - _self_
4
+
5
+ # OmniAID v1 (Mirage-Train, the paper's OmniAID-Mirage). Expert configuration of the released checkpoint_omniaid_v1.pth (config_omniaid_mirage_paper.json);
6
+ # read from the checkpoint's shapes (experts, rank, router width; top-k from the official config).
7
+ model_type: omniaid
8
+ CLIP_path: openai/clip-vit-large-patch14-336
9
+ num_experts: 6
10
+ rank_per_expert: 8
11
+ moe_router_hidden_dim: 256
12
+ moe_top_k: 2
13
+ moe_lambda_orth: 0.001
14
+ moe_lambda_balance: 0.001
15
+ moe_lambda_gating_cls: 0.1
16
+ dropout: 0.0
17
+ gradient_checkpointing_enable: false
18
+ is_hybrid: true
conf/trainer/image/d3qe.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # D3QE's official training recipe (train.py + options.TrainOptions defaults; train.sh only sets the augmentation):
2
+ # AdamW lr 8e-5, betas (0.9, 0.999), weight decay 0.01, batch 32 on one GPU, 10 epochs, no warmup. Validation after
3
+ # every epoch keeps the best-accuracy checkpoint; EarlyStopping (patience 5, delta 0.001, then 0.002 after a drop)
4
+ # divides the learning rate by 10, stopping below 1e-6. The released checkpoint has total_steps 62500 = 10 epochs.
5
+ epochs: 10
6
+ lr: 0.00008
7
+ weight_decay: 0.01
8
+ val_interval: 1
9
+ test_interval: 1
10
+ save_interval: 1
11
+ save: true
12
+ save_policy: max
13
+ save_path: /ssdstore/ssalehi/SynthScope/models/d3qe
14
+ seed: 42
15
+ warmup:
16
+ mode: "off" # quoted: a bare off is YAML boolean False, which the trainer reads as "on"
17
+ warmup_ratio: 0.0
18
+ lr_scheduler: linear
19
+ plateau:
20
+ patience: 5
21
+ delta: 0.001
22
+ delta_after_drop: 0.002
23
+ factor: 10
24
+ min_lr: 0.000001
conf/trainer/image/npr.yaml CHANGED
@@ -2,17 +2,25 @@ defaults:
2
  - /trainer/default
3
  - _self_
4
 
 
 
 
5
  trainer_type: npr
6
- epochs: 10
7
  lr: 2.0e-4
8
- weight_decay: 0.0
9
  label_smoothing: 0.0
10
  val_interval: 1
11
  test_interval: 1
12
  save_interval: 1
13
- save: false
14
  save_policy: max
 
 
15
  warmup:
16
- mode: off
17
- warmup_ratio: 0.05
18
  lr_scheduler: linear
 
 
 
 
2
  - /trainer/default
3
  - _self_
4
 
5
+ # NPR's official training recipe (README: train.py --classes car,cat,chair,horse --batch_size 32 --delr_freq 10
6
+ # --lr 0.0002 --niter 50; networks/trainer.py: Adam, betas (0.9, 0.999), no weight decay): 4-class ProGAN, 50 epochs,
7
+ # learning rate x0.9 after epochs 10, 20, 30, 40; the released checkpoint is the last epoch's model.
8
  trainer_type: npr
9
+ epochs: 50
10
  lr: 2.0e-4
11
+ weight_decay: 0.0 # AdamW with weight decay 0 == the official Adam
12
  label_smoothing: 0.0
13
  val_interval: 1
14
  test_interval: 1
15
  save_interval: 1
16
+ save: true
17
  save_policy: max
18
+ save_path: /ssdstore/ssalehi/SynthScope/models/npr
19
+ seed: 100 # the official seed_torch(100)
20
  warmup:
21
+ mode: "off" # quoted: a bare off is YAML boolean False, which the trainer reads as "on"
22
+ warmup_ratio: 0.0
23
  lr_scheduler: linear
24
+ lr_step:
25
+ every: 10
26
+ factor: 0.9
conf/trainer/image/pgc.yaml CHANGED
@@ -2,11 +2,15 @@ defaults:
2
  - /trainer/default
3
  - _self_
4
 
5
- # PGC trains LoRA adapters + the residual stream + PGCM + classifier (DINO backbone frozen).
6
- # Official recipe (xiaoyu6868/PGC): AdamW, lr 5e-5, weight_decay 0.05, 100 epochs (--niter 100),
7
- # CosineAnnealingLR over all steps, label smoothing 0.1, effective batch 128.
 
 
 
 
8
  trainer_type: pgc
9
- epochs: 100
10
  lr: 5.0e-5
11
  weight_decay: 0.05
12
  label_smoothing: 0.1
@@ -15,7 +19,14 @@ test_interval: 1
15
  save_interval: 1
16
  save: true
17
  save_policy: max
 
 
18
  warmup:
19
- mode: on
20
- warmup_ratio: 0.05
21
  lr_scheduler: cosine
 
 
 
 
 
 
2
  - /trainer/default
3
  - _self_
4
 
5
+ # PGC's official training recipe (xiaoyu6868/PGC train.py + engine/trainer.py, README 3.2): AdamW lr 5e-5, weight
6
+ # decay 0.05, betas (0.9, 0.999), label smoothing 0.1 (in the model's compute_loss), micro-batch 32 x 4 accumulation
7
+ # = 128, gradient-norm clipping 1.0, seed 42. The released ProGAN checkpoint is only ~400 steps in (the authors:
8
+ # DINOv2's prior overfits ProGAN within the first epoch), so the run is a fixed budget of micro-batches with a
9
+ # checkpoint every 100, and the early curve is evaluated. Official cosine schedule: T_max = 100 epochs of
10
+ # micro-batches but stepped once per optimizer step, so over the first 2000 micro-batches the LR stays at 5e-5
11
+ # (>99.99%); it is run here at constant 5e-5.
12
  trainer_type: pgc
13
+ epochs: 1
14
  lr: 5.0e-5
15
  weight_decay: 0.05
16
  label_smoothing: 0.1
 
19
  save_interval: 1
20
  save: true
21
  save_policy: max
22
+ save_path: /ssdstore/ssalehi/SynthScope/models/pgc
23
+ seed: 42
24
  warmup:
25
+ mode: "off" # quoted: a bare off is YAML boolean False
26
+ warmup_ratio: 0.0
27
  lr_scheduler: cosine
28
+ step_budget:
29
+ max_steps: 2000 # micro-batches
30
+ save_every: 100
31
+ grad_accum_steps: 4
32
+ max_grad_norm: 1.0
dataset.py CHANGED
@@ -903,7 +903,8 @@ class CNNDetectionDataset(Dataset):
903
  "test": ("cnn_synth_testset", "test"),
904
  }
905
 
906
- def __init__(self, root_dir: str | Path, split: Literal["train", "val", "test"], transform: AIGITransformations | None = None, prefetch: bool = False):
 
907
  super().__init__()
908
  self.root_dir = Path(root_dir)
909
  self.split = split
@@ -914,6 +915,12 @@ class CNNDetectionDataset(Dataset):
914
  print(f"Downloading {self.split} split from Google Drive...")
915
  self._download_data()
916
  self.images, self.labels, self.folder_names = self._load_data()
 
 
 
 
 
 
917
  self.unique_folder_names = sorted(set(self.folder_names)) or [self.split]
918
 
919
  @classmethod
@@ -1081,8 +1088,10 @@ class CNNDetectionDatasetModule:
1081
  self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None
1082
  self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1083
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1084
- self.train_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch)
1085
- self.val_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "val", self.eval_transform, prefetch=prefetch)
 
 
1086
  self.test_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch)
1087
 
1088
  def train_dataloader(self):
@@ -2626,9 +2635,12 @@ class ARForensicsDataset(_TestOnlyImageDataset):
2626
  Janus-Pro, RAR, Switti, Open-MAGVIT2), 6000 fakes and 6000 ImageNet reals each, laid out as
2627
  ``ARForensics/test/<generator>/{0_real,1_fake}``. Sourced from the HF dataset ``Yanran21/ARForensics`` (MIT): the
2628
  corrected test release (MAR replaced by Switti) in four independent zips, ARForensics_updated_test_part00{1..4}.zip
2629
- (21 GB); a generator can span two zips, so all four are extracted into one folder."""
 
 
2630
  hf_repo_id = "Yanran21/ARForensics"
2631
- hf_pattern = "ARForensics_updated_test_part*.zip"
 
2632
 
2633
  @classmethod
2634
  def prepare_split(cls, root_dir, split="test", force=False):
@@ -2636,20 +2648,70 @@ class ARForensicsDataset(_TestOnlyImageDataset):
2636
  if not force and _has_images(dest):
2637
  return
2638
  from huggingface_hub import snapshot_download
2639
- dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True)
2640
- snapshot_download(repo_id=cls.hf_repo_id, repo_type="dataset", local_dir=str(dl), allow_patterns=[cls.hf_pattern])
2641
- staging = root_dir / "_staging"
2642
- for archive in sorted(dl.glob(cls.hf_pattern)):
 
 
 
 
 
 
 
 
 
 
2643
  _safe_extract_zip(archive, staging)
2644
- inner = staging / "ARForensics" / "test"
2645
  if not inner.is_dir():
2646
- raise RuntimeError(f"ARForensics: expected ARForensics/test/ inside the archives, found {list(staging.iterdir())}")
2647
  if dest.exists():
2648
  shutil.rmtree(dest)
2649
  shutil.move(str(inner), str(dest))
2650
  shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(staging, ignore_errors=True)
2651
 
2652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2653
  class _TestOnlyImageModule:
2654
  """Shared loader for the eval-only image benchmarks (test split only)."""
2655
  DATASET_CLASS = _TestOnlyImageDataset
@@ -2724,8 +2786,36 @@ class RRDatasetModule(_TestOnlyImageModule):
2724
 
2725
 
2726
  class ARForensicsDatasetModule(_TestOnlyImageModule):
 
 
2727
  DATASET_CLASS = ARForensicsDataset
2728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2729
 
2730
  class VideoFeedbackDataset(VideoFolderDataset):
2731
  """VideoFeedback (TIGER-Lab) — used by STALL. 11 generators vs DiDeMo+Panda70M real, laid out
@@ -2954,6 +3044,7 @@ SynthScopeModuleDict = {
2954
  "bfree_online": BFreeOnlineDatasetModule,
2955
  "rrdataset": RRDatasetModule,
2956
  "arforensics": ARForensicsDatasetModule,
 
2957
  # Video benchmarks. EvalCrafter/GenVidBench/VideoFeedback/ComGenVid have dedicated classes that
2958
  # download + extract their HF archives into the real/fake layout; the rest use the generic
2959
  # VideoFolderDatasetModule.
 
903
  "test": ("cnn_synth_testset", "test"),
904
  }
905
 
906
+ def __init__(self, root_dir: str | Path, split: Literal["train", "val", "test"], transform: AIGITransformations | None = None, prefetch: bool = False,
907
+ classes: list[str] | None = None):
908
  super().__init__()
909
  self.root_dir = Path(root_dir)
910
  self.split = split
 
915
  print(f"Downloading {self.split} split from Google Drive...")
916
  self._download_data()
917
  self.images, self.labels, self.folder_names = self._load_data()
918
+ if classes: # e.g. the 4-class ProGAN setting (car, cat, chair, horse) of CNNDetection/NPR/FatFormer
919
+ keep = [i for i, f in enumerate(self.folder_names) if f in set(classes)]
920
+ if not keep:
921
+ raise ValueError(f"CNNDetection {split}: none of the classes {list(classes)} found")
922
+ self.images = [self.images[i] for i in keep]; self.labels = [self.labels[i] for i in keep]
923
+ self.folder_names = [self.folder_names[i] for i in keep]
924
  self.unique_folder_names = sorted(set(self.folder_names)) or [self.split]
925
 
926
  @classmethod
 
1088
  self.train_transform = AIGITransformations(train_list) if len(train_list) > 0 else None
1089
  self.eval_transform = AIGITransformations(eval_list) if len(eval_list) > 0 else None
1090
  prefetch = getattr(config.DatasetConfig, "prefetch", False)
1091
+ classes = config.DatasetConfig.get("train_classes", None) # ProGAN classes for train/val (None: all 20)
1092
+ classes = list(classes) if classes else None
1093
+ self.train_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "train", self.train_transform, prefetch=prefetch, classes=classes)
1094
+ self.val_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "val", self.eval_transform, prefetch=prefetch, classes=classes)
1095
  self.test_dataset = CNNDetectionDataset(config.DatasetConfig.root_dir, "test", self.eval_transform, prefetch=prefetch)
1096
 
1097
  def train_dataloader(self):
 
2635
  Janus-Pro, RAR, Switti, Open-MAGVIT2), 6000 fakes and 6000 ImageNet reals each, laid out as
2636
  ``ARForensics/test/<generator>/{0_real,1_fake}``. Sourced from the HF dataset ``Yanran21/ARForensics`` (MIT): the
2637
  corrected test release (MAR replaced by Switti) in four independent zips, ARForensics_updated_test_part00{1..4}.zip
2638
+ (21 GB); a generator can span two zips, so all four are extracted into one folder. D3QE's training data is in the
2639
+ same repo: ``train`` (100k LlamaGen fakes + 100k ImageNet reals; one zip split into 7 parts, 35 GB) and ``val``
2640
+ (10k + 10k, 3.7 GB), each laid out as ``ARForensics/<split>/{0_real,1_fake}``."""
2641
  hf_repo_id = "Yanran21/ARForensics"
2642
+ hf_patterns = {"test": "ARForensics_updated_test_part*.zip", "val": "val/ARForensics_val.zip",
2643
+ "train": "train/ARForensics_train_split.zip.a?"}
2644
 
2645
  @classmethod
2646
  def prepare_split(cls, root_dir, split="test", force=False):
 
2648
  if not force and _has_images(dest):
2649
  return
2650
  from huggingface_hub import snapshot_download
2651
+ pattern = cls.hf_patterns[split]
2652
+ dl = root_dir / f"_downloads_{split}"; dl.mkdir(parents=True, exist_ok=True)
2653
+ snapshot_download(repo_id=cls.hf_repo_id, repo_type="dataset", local_dir=str(dl), allow_patterns=[pattern])
2654
+ archives = sorted(dl.glob(pattern))
2655
+ if split == "train": # a single zip split into parts: join them first
2656
+ joined = dl / "train_joined.zip"
2657
+ with open(joined, "wb") as out:
2658
+ for part in archives:
2659
+ with open(part, "rb") as f:
2660
+ shutil.copyfileobj(f, out, 1 << 24)
2661
+ part.unlink()
2662
+ archives = [joined]
2663
+ staging = root_dir / f"_staging_{split}"
2664
+ for archive in archives:
2665
  _safe_extract_zip(archive, staging)
2666
+ inner = staging / "ARForensics" / split
2667
  if not inner.is_dir():
2668
+ raise RuntimeError(f"ARForensics: expected ARForensics/{split}/ inside the archives, found {list(staging.iterdir())}")
2669
  if dest.exists():
2670
  shutil.rmtree(dest)
2671
  shutil.move(str(inner), str(dest))
2672
  shutil.rmtree(dl, ignore_errors=True); shutil.rmtree(staging, ignore_errors=True)
2673
 
2674
 
2675
+ class MirageTestDataset(_TestOnlyImageDataset):
2676
+ """Mirage-Test (OmniAID, arXiv 2511.08423) — real and fake images from recent generators tuned for realism, in five
2677
+ content categories laid out as ``<category>/{0_real,1_fake}`` (Human, Animal, Object, Scene, Anime). Sourced from the
2678
+ HF dataset ``Yunncheng/Mirage-Test`` (MIT). Anime ships fakes only ("due to copyright considerations") and the paper
2679
+ scores it by its fake accuracy, so the loader pairs Anime with the reals of the other four categories: its
2680
+ ``fake_acc`` is the paper's Anime number, and the other categories' balanced accuracy is the paper's "Overall"."""
2681
+ hf_repo_id = "Yunncheng/Mirage-Test"
2682
+ categories = ("Human", "Animal", "Object", "Scene", "Anime")
2683
+
2684
+ @classmethod
2685
+ def prepare_split(cls, root_dir, split="test", force=False):
2686
+ root_dir = Path(root_dir); dest = root_dir / split
2687
+ if not force and _has_images(dest):
2688
+ return
2689
+ from huggingface_hub import snapshot_download
2690
+ dl = root_dir / "_downloads"; dl.mkdir(parents=True, exist_ok=True)
2691
+ snapshot_download(repo_id=cls.hf_repo_id, repo_type="dataset", local_dir=str(dl),
2692
+ allow_patterns=[f"{c}/*" for c in cls.categories])
2693
+ if dest.exists():
2694
+ shutil.rmtree(dest)
2695
+ dest.mkdir(parents=True)
2696
+ for c in cls.categories:
2697
+ shutil.move(str(dl / c), str(dest / c))
2698
+ reals = sorted(p for c in cls.categories if c != "Anime"
2699
+ for p in (dest / c / "0_real").rglob("*") if p.suffix.lower() in IMAGE_EXTENSIONS)
2700
+ _link_images(reals, dest / "Anime" / "0_real")
2701
+ shutil.rmtree(dl, ignore_errors=True)
2702
+
2703
+ def _load_data(self):
2704
+ # Fakes are nested one level deeper, by generator (``Human/1_fake/<generator>/*.jpg``): the label comes from the
2705
+ # 0_real/1_fake ancestor and the group from the category folder.
2706
+ images, labels, folder_names = [], [], []
2707
+ for filepath in _iter_images_follow(self.destination_dir):
2708
+ parts = filepath.relative_to(self.destination_dir).parts
2709
+ images.append(filepath)
2710
+ labels.append(1 if "1_fake" in parts else 0)
2711
+ folder_names.append(parts[0])
2712
+ return images, labels, folder_names
2713
+
2714
+
2715
  class _TestOnlyImageModule:
2716
  """Shared loader for the eval-only image benchmarks (test split only)."""
2717
  DATASET_CLASS = _TestOnlyImageDataset
 
2786
 
2787
 
2788
  class ARForensicsDatasetModule(_TestOnlyImageModule):
2789
+ """The test split for benchmarking; with a ``train_transform_list`` in the config also the train and val splits
2790
+ (D3QE's training data), validated with ``val_transform_list`` (default: ``eval_transform_list``)."""
2791
  DATASET_CLASS = ARForensicsDataset
2792
 
2793
+ def __init__(self, config):
2794
+ super().__init__(config)
2795
+ cfg = config.DatasetConfig
2796
+ train_list = list(cfg.get("train_transform_list", []))
2797
+ self.train_dataset = self.val_dataset = None
2798
+ if train_list:
2799
+ prefetch = bool(cfg.get("prefetch", False))
2800
+ val_list = list(cfg.get("val_transform_list", cfg.get("eval_transform_list", [])))
2801
+ self.train_dataset = ARForensicsDataset(cfg.root_dir, "train", AIGITransformations(train_list), prefetch=prefetch)
2802
+ self.val_dataset = ARForensicsDataset(cfg.root_dir, "val", AIGITransformations(val_list), prefetch=prefetch)
2803
+
2804
+ def _loader(self, dataset, batch_size, shuffle):
2805
+ cfg = self.config.DatasetConfig
2806
+ return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, num_workers=cfg.num_workers,
2807
+ persistent_workers=cfg.persistent_workers, pin_memory=cfg.pin_memory)
2808
+
2809
+ def train_dataloader(self):
2810
+ return None if self.train_dataset is None else self._loader(self.train_dataset, self.config.DatasetConfig.train_batch_size, True)
2811
+
2812
+ def val_dataloader(self):
2813
+ return None if self.val_dataset is None else self._loader(self.val_dataset, self.config.DatasetConfig.val_batch_size, False)
2814
+
2815
+
2816
+ class MirageTestDatasetModule(_TestOnlyImageModule):
2817
+ DATASET_CLASS = MirageTestDataset
2818
+
2819
 
2820
  class VideoFeedbackDataset(VideoFolderDataset):
2821
  """VideoFeedback (TIGER-Lab) — used by STALL. 11 generators vs DiDeMo+Panda70M real, laid out
 
3044
  "bfree_online": BFreeOnlineDatasetModule,
3045
  "rrdataset": RRDatasetModule,
3046
  "arforensics": ARForensicsDatasetModule,
3047
+ "mirage_test": MirageTestDatasetModule,
3048
  # Video benchmarks. EvalCrafter/GenVidBench/VideoFeedback/ComGenVid have dedicated classes that
3049
  # download + extract their HF archives into the real/fake layout; the rest use the generic
3050
  # VideoFolderDatasetModule.
deploy/hf-space/fetch_weights.py CHANGED
@@ -15,8 +15,8 @@ sys.path.insert(0, str(ROOT))
15
 
16
  # Detectors whose upstream licenses allow a free public demo. NPR's repository has no license; its first author gave
17
  # written permission (2026-10-11) to use NPR.pth in this free, non-commercial demo with credit (see LICENSES.md).
18
- DEFAULT = ["clip", "cnn", "npr", "safe", "aide", "co_spy", "fat_former", "pgc", "cure", "dda", "d3qe", "vib_net",
19
- "dinov2", "adapted_dino", "restrav"]
20
 
21
 
22
  def main() -> None:
 
15
 
16
  # Detectors whose upstream licenses allow a free public demo. NPR's repository has no license; its first author gave
17
  # written permission (2026-10-11) to use NPR.pth in this free, non-commercial demo with credit (see LICENSES.md).
18
+ DEFAULT = ["clip", "cnn", "npr", "safe", "aide", "co_spy", "fat_former", "pgc", "cure", "dda", "d3qe", "omniaid",
19
+ "vib_net", "dinov2", "adapted_dino", "restrav"]
20
 
21
 
22
  def main() -> None:
docs/BENCHMARKS.md CHANGED
@@ -86,6 +86,7 @@ have no automatic source: get them from their official sites and place them in t
86
  | `bfree_online` | BFree-Online (B-Free's viral images; used by DDA and CuRe): real and fake images that went viral, several web-scraped copies of each (about 1400 in the paper). Only URLs are released; the loader keeps an image only if its md5 matches, so dead links and placeholders are skipped (`<root>/missing.txt`; in October 2026, 807 of the 1111 listed were still online: 269 real, 538 fake). One group, `test/viral` | `viral_images_dataset/BFree_viral_images.csv` in `grip-unina/B-Free` (nonprofit use only) |
87
  | `rrdataset` | RRDataset (ICCV 2025; used by CuRe): three scenarios, `original`, `transfer` (shared over online platforms) and `redigital` (re-digitized), about 8500 real and 8500 fake each. Only the test archive is used | Zenodo record `14963880` (`RRDataset_test.tar.gz`, 20 GB, CC BY 4.0) |
88
  | `arforensics` | ARForensics (ICCV 2025; used by D3QE): images from 7 visual autoregressive generators, LlamaGen, VAR, Infinity, Janus-Pro, RAR, Switti and Open-MAGVIT2, 6000 fakes and 6000 ImageNet reals each. The corrected test release (MAR replaced by Switti) | HF `Yanran21/ARForensics` (`ARForensics_updated_test_part00{1..4}.zip`, 21 GB, MIT) |
 
89
  | `diffusionforensics`, `diffusion1kstep`, `freqnetdetection` | DiffusionForensics, Diffusion-1kStep, FreqNet sets | see the yamls |
90
  | `genvideo` | GenVideo-Val (DeMamba; D3 Table 1, STALL): 10 generators vs 10,000 MSR-VTT reals, shared reals hard-linked into every generator | ModelScope `cccnju/Gen-Video` (`GenVideo-Val.zip`, 14 GB) |
91
  | `videophy` | VideoPhy test set (D3 Table 3): 3360 clips from 10 generators, released as URLs; dead links are skipped (both CogVideoX sets were offline in October 2026). Fake-only, so paired with GenVideo-Val's MSR-VTT reals, as D3 does | `videophysics/videophy_test_public` (MIT) + ModelScope for the reals |
 
86
  | `bfree_online` | BFree-Online (B-Free's viral images; used by DDA and CuRe): real and fake images that went viral, several web-scraped copies of each (about 1400 in the paper). Only URLs are released; the loader keeps an image only if its md5 matches, so dead links and placeholders are skipped (`<root>/missing.txt`; in October 2026, 807 of the 1111 listed were still online: 269 real, 538 fake). One group, `test/viral` | `viral_images_dataset/BFree_viral_images.csv` in `grip-unina/B-Free` (nonprofit use only) |
87
  | `rrdataset` | RRDataset (ICCV 2025; used by CuRe): three scenarios, `original`, `transfer` (shared over online platforms) and `redigital` (re-digitized), about 8500 real and 8500 fake each. Only the test archive is used | Zenodo record `14963880` (`RRDataset_test.tar.gz`, 20 GB, CC BY 4.0) |
88
  | `arforensics` | ARForensics (ICCV 2025; used by D3QE): images from 7 visual autoregressive generators, LlamaGen, VAR, Infinity, Janus-Pro, RAR, Switti and Open-MAGVIT2, 6000 fakes and 6000 ImageNet reals each. The corrected test release (MAR replaced by Switti) | HF `Yanran21/ARForensics` (`ARForensics_updated_test_part00{1..4}.zip`, 21 GB, MIT) |
89
+ | `mirage_test` | Mirage-Test (OmniAID, arXiv 2511.08423): real and fake images from recent generators tuned for realism, in five categories: Human, Animal, Object, Scene and Anime. Anime is fakes only, so the loader pairs it with the other categories' reals; its fake accuracy is the paper's Anime number | HF `Yunncheng/Mirage-Test` (49k files, MIT) |
90
  | `diffusionforensics`, `diffusion1kstep`, `freqnetdetection` | DiffusionForensics, Diffusion-1kStep, FreqNet sets | see the yamls |
91
  | `genvideo` | GenVideo-Val (DeMamba; D3 Table 1, STALL): 10 generators vs 10,000 MSR-VTT reals, shared reals hard-linked into every generator | ModelScope `cccnju/Gen-Video` (`GenVideo-Val.zip`, 14 GB) |
92
  | `videophy` | VideoPhy test set (D3 Table 3): 3360 clips from 10 generators, released as URLs; dead links are skipped (both CogVideoX sets were offline in October 2026). Fake-only, so paired with GenVideo-Val's MSR-VTT reals, as D3 does | `videophysics/videophy_test_public` (MIT) + ModelScope for the reals |
docs/CAPABILITIES.md CHANGED
@@ -30,6 +30,7 @@ matching `conf/model/<modality>/<key>.yaml`, a `trainer.py._build_model` branch,
30
  | `pgc` | image | bce | official | pgc_peak | LoRA-DINOv2 + YCbCr residual, peak-guided |
31
  | `cure` | image | ce | official | – | Frozen PE-Core L/14 + Q/V LoRA + source-response subspace (mixing-ratio regression) |
32
  | `d3qe` | image | bce | official | – | LlamaGen VQ quantization error + codebook-aware transformer + CLIP ViT-L/14 (autoregressive images) |
 
33
  | `dda` | image | bce | official | – | DINOv2 ViT-L/14 + LoRA, pixel- and frequency-aligned training data (center-crop 336) |
34
  | `vib_net` | image | vib | synthscope | – | Variational information bottleneck (FLAVA) |
35
  | `d3` | video | video | training-free | – | Disentangled detail dynamics (X-CLIP) |
 
30
  | `pgc` | image | bce | official | pgc_peak | LoRA-DINOv2 + YCbCr residual, peak-guided |
31
  | `cure` | image | ce | official | – | Frozen PE-Core L/14 + Q/V LoRA + source-response subspace (mixing-ratio regression) |
32
  | `d3qe` | image | bce | official | – | LlamaGen VQ quantization error + codebook-aware transformer + CLIP ViT-L/14 (autoregressive images) |
33
+ | `omniaid` (+ `omniaid_v1`, `omniaid_genimage`) | image | ce | official | – | Hybrid MoE on CLIP ViT-L/14@336: routed semantic experts + an always-on artifact expert |
34
  | `dda` | image | bce | official | – | DINOv2 ViT-L/14 + LoRA, pixel- and frequency-aligned training data (center-crop 336) |
35
  | `vib_net` | image | vib | synthscope | – | Variational information bottleneck (FLAVA) |
36
  | `d3` | video | video | training-free | – | Disentangled detail dynamics (X-CLIP) |
docs/WILD_BENCHMARKS.md CHANGED
@@ -14,6 +14,7 @@ Each number comes from `repro/bench_results/<model>_<dataset>.json`; regenerate
14
  | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
15
  | CuRe | **79.7 / 85.5** | **98.1 / 99.7** | **92.7 / 96.5** | **95.9 / 99.1** | **91.6 / 98.1** | **82.0 / 88.9** | 90.0 |
16
  | DDA | 67.2 / 75.0 | **97.9 / 99.8** | **90.8 / 96.4** | **90.2 / 96.1** | **95.0 / 98.9** | **76.0 / 81.7** | 86.2 |
 
17
  | SPAI | 61.6 / 69.0 | **93.8 / 98.7** | **76.2 / 84.6** | **80.6 / 88.4** | 59.8 / 60.2 | 64.8 / 72.0 | 72.8 |
18
  | Co-Spy | 71.0 / 76.5 | **96.7 / 99.5** | 64.3 / 69.5 | 61.6 / 71.2 | 56.3 / 73.3 | 66.8 / 73.9 | 69.4 |
19
  | VIB-Net | 65.8 / 74.3 | **94.8 / 99.7** | 58.5 / 73.6 | 56.1 / 71.3 | 51.9 / 53.6 | 60.0 / 66.4 | 64.5 |
 
14
  | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
15
  | CuRe | **79.7 / 85.5** | **98.1 / 99.7** | **92.7 / 96.5** | **95.9 / 99.1** | **91.6 / 98.1** | **82.0 / 88.9** | 90.0 |
16
  | DDA | 67.2 / 75.0 | **97.9 / 99.8** | **90.8 / 96.4** | **90.2 / 96.1** | **95.0 / 98.9** | **76.0 / 81.7** | 86.2 |
17
+ | OmniAID (v2) | 61.7 / 83.6 | 74.1 / 93.1 | **86.0 / 95.3** | **88.4 / 96.9** | 70.3 / 87.2 | 71.8 / 82.3 | 75.4 |
18
  | SPAI | 61.6 / 69.0 | **93.8 / 98.7** | **76.2 / 84.6** | **80.6 / 88.4** | 59.8 / 60.2 | 64.8 / 72.0 | 72.8 |
19
  | Co-Spy | 71.0 / 76.5 | **96.7 / 99.5** | 64.3 / 69.5 | 61.6 / 71.2 | 56.3 / 73.3 | 66.8 / 73.9 | 69.4 |
20
  | VIB-Net | 65.8 / 74.3 | **94.8 / 99.7** | 58.5 / 73.6 | 56.1 / 71.3 | 51.9 / 53.6 | 60.0 / 66.4 | 64.5 |
experiments/image/d3qe.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # D3QE (ICCV 2025): the official training recipe, reproduced in SynthScope.
3
+ # data ARForensics train (100k LlamaGen vs 100k ImageNet) and val (10k + 10k); downloaded on first use
4
+ # model frozen LlamaGen VQ-16 + frozen CLIP ViT-L/14; only the D3AT transformer and the heads are trained
5
+ # train short side 256 -> CNNDetection blur/JPEG (p 0.1 each) -> RandomCrop 256 -> flip -> [0, 1], no normalization
6
+ # val short side 256 -> the same blur/JPEG at fixed settings (sigma 1.5, PIL q65) -> CenterCrop 256
7
+ # optimizer AdamW lr 8e-5, weight decay 0.01, batch 32 on ONE GPU (BatchNorm in the heads), 10 epochs,
8
+ # best-validation-accuracy checkpoint, learning rate /10 after 5 epochs without improvement
9
+ # Evaluate the result with: python benchmark/bench_eval.py --model d3qe --ckpt <save_path>/model_best.pth --dataset arforensics
10
+ export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/nvmestore/ssalehi/.triton}
11
+ export HF_HOME=${HF_HOME:-/nvmestore/ssalehi/.cache/huggingface}
12
+ export TORCH_HOME=${TORCH_HOME:-/nvmestore/ssalehi/.cache/torch}
13
+ export CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}
14
+ export OMP_NUM_THREADS=1
15
+ DATA=${DATA:-/nvmestore/ssalehi/SynthScope/ARForensics}
16
+ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/d3qe}
17
+
18
+ # Plain single-process run (no `accelerate launch`): the repo's accelerate_config.yaml uses DeepSpeed with gradient
19
+ # clipping 1.0, which the official recipe does not have.
20
+ /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope python main.py \
21
+ trainer@TrainerConfig=image/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 \
33
+ logger.mode=offline \
34
+ "logger.tags=[SynthScope,D3QE,arforensics]"
experiments/image/npr.sh CHANGED
@@ -1,20 +1,34 @@
1
  #!/bin/bash
2
- export TRITON_CACHE_DIR=/nvmestore/ssalehi/.triton
3
- export HF_HOME=/nvmestore/ssalehi/.cache/huggingface
4
- export TORCH_HOME=/nvmestore/ssalehi/.cache/torch
5
- export CUDA_VISIBLE_DEVICES=1,2
 
 
 
 
 
 
 
 
6
  export OMP_NUM_THREADS=1
 
 
7
 
8
- # NPR trains a small ResNet from scratch on neighboring-pixel residuals; feed [0,1] images (to_dtype only).
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/npr \
11
  model@ModelConfig=image/npr \
12
- dataset@DatasetConfig=image/genimage \
13
- DatasetConfig.train_batch_size=128 \
14
- DatasetConfig.val_batch_size=512 \
15
- DatasetConfig.test_batch_size=512 \
16
- DatasetConfig.train_transform_list=[resize_short_256,gaussian_blur,jpeg_compression,random_crop_224,random_horizontal_flip,to_dtype] \
17
- DatasetConfig.eval_transform_list=[resize_short_256,center_crop_224,to_dtype] \
 
 
 
18
  logger.name=npr \
19
- logger.mode=online \
20
- logger.tags=[SynthScope,NPR,genimage]
 
1
  #!/bin/bash
2
+ # NPR (CVPR 2024): the official training recipe, reproduced in SynthScope.
3
+ # data ForenSynths / CNNDetection ProGAN train + val, 4-class setting (car, cat, chair, horse)
4
+ # train Resize((256, 256)) -> RandomCrop 224 -> flip -> ImageNet normalization (no blur/JPEG: the official
5
+ # data_augment call is commented out); NPR's neighboring-pixel residual is computed inside the model
6
+ # val Resize((256, 256)) -> CenterCrop 224 -> ImageNet normalization
7
+ # optimizer Adam lr 2e-4 (AdamW, weight decay 0), batch 32 on ONE GPU, 50 epochs, lr x0.9 every 10 epochs
8
+ # checkpoint model_last.pth (the official code releases the last epoch); model_best.pth is kept for reference
9
+ # Evaluate with: python benchmark/bench_eval.py --model npr --ckpt <save_path>/model_last.pth --benchmark forensynths --data <CNNDetection/test>
10
+ export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/nvmestore/ssalehi/.triton}
11
+ export HF_HOME=${HF_HOME:-/nvmestore/ssalehi/.cache/huggingface}
12
+ export TORCH_HOME=${TORCH_HOME:-/nvmestore/ssalehi/.cache/torch}
13
+ export CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}
14
  export OMP_NUM_THREADS=1
15
+ DATA=${DATA:-/nvmestore/ssalehi/SynthScope/CNNDetection}
16
+ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/npr}
17
 
18
+ # Plain single-process run (no `accelerate launch`): the repo's accelerate_config.yaml uses DeepSpeed with gradient
19
+ # clipping 1.0, which the official recipe does not have.
20
+ /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope python main.py \
21
  trainer@TrainerConfig=image/npr \
22
  model@ModelConfig=image/npr \
23
+ dataset@DatasetConfig=image/cnndetection \
24
+ DatasetConfig.root_dir=$DATA \
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 \
33
+ logger.mode=offline \
34
+ "logger.tags=[SynthScope,NPR,progan4]"
experiments/image/pgc.sh CHANGED
@@ -1,27 +1,34 @@
1
  #!/bin/bash
2
- export TRITON_CACHE_DIR=/nvmestore/ssalehi/.triton
3
- export HF_HOME=/nvmestore/ssalehi/.cache/huggingface
4
- export TORCH_HOME=/nvmestore/ssalehi/.cache/torch
5
- export CUDA_VISIBLE_DEVICES=1,2
 
 
 
 
 
 
 
 
 
6
  export OMP_NUM_THREADS=1
 
 
7
 
8
- # PGC (Zhou et al., 2026): LoRA-tuned DINOv2-Large RGB stream + YCbCr-quantization-residual CNN
9
- # stream, fused by peak-guided calibration. The residual is computed INSIDE the model, so the
10
- # dataset feeds a normal ImageNet-normalized image (normalize_dinov2 = what DINOv2 expects). The
11
- # residual is resolution-sensitive, so we use CROP-only transforms (no resize) to preserve the
12
- # per-pixel quantization pattern. Trained here on ProGAN (CNNDetection); swap the dataset for
13
- # GenImage to match the paper's SDv1.4 setting.
14
- /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope accelerate launch --config_file accelerate_config.yaml --num_processes 2 main.py \
15
  trainer@TrainerConfig=image/pgc \
16
  model@ModelConfig=image/pgc \
17
  dataset@DatasetConfig=image/cnndetection \
18
- ModelConfig.backbone_name=facebook/dinov2-large \
19
  DatasetConfig.train_batch_size=32 \
20
  DatasetConfig.val_batch_size=64 \
21
- DatasetConfig.test_batch_size=64 \
22
- DatasetConfig.train_transform_list=[random_crop_224,random_horizontal_flip,to_dtype,normalize_dinov2] \
23
- DatasetConfig.eval_transform_list=[center_crop_224,to_dtype,normalize_dinov2] \
24
- metrics=[real_accuracy,fake_accuracy,AP,AUC] \
25
  logger.name=pgc \
26
- logger.mode=online \
27
- logger.tags=[SynthScope,PGC,cnndetection]
 
1
  #!/bin/bash
2
+ # PGC (Zhou et al., 2026): the official ProGAN training recipe, reproduced in SynthScope.
3
+ # data ForenSynths / CNNDetection ProGAN train, all 20 classes (720k images = the UniversalFakeDetect train set)
4
+ # train flip -> ColorJitter(0.15, 0.15, 0.15, 0.05) p0.2 -> pad + RandomCrop 224 (no resize: the YCbCr residual
5
+ # is resolution-sensitive) -> ImageNet normalization; the residual is computed inside the model
6
+ # optimizer AdamW lr 5e-5 (constant over this budget), weight decay 0.05, label smoothing 0.1, batch 32 x 4
7
+ # accumulation on ONE GPU, gradient clipping 1.0, seed 42
8
+ # budget 2000 micro-batches with a checkpoint every 100 (model_step<N>.pth): the released checkpoint is ~400 steps
9
+ # in and transfer to unseen generators erodes within the first epoch, so the early curve is evaluated
10
+ # Evaluate: python benchmark/bench_eval.py --model pgc --ckpt <save_path>/model_step<N>.pth --benchmark forensynths --data <CNNDetection/test>
11
+ export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/nvmestore/ssalehi/.triton}
12
+ export HF_HOME=${HF_HOME:-/nvmestore/ssalehi/.cache/huggingface}
13
+ export TORCH_HOME=${TORCH_HOME:-/nvmestore/ssalehi/.cache/torch}
14
+ export CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}
15
  export OMP_NUM_THREADS=1
16
+ DATA=${DATA:-/nvmestore/ssalehi/SynthScope/CNNDetection}
17
+ SAVE=${SAVE:-/nvmestore/ssalehi/SynthScope/models/pgc}
18
 
19
+ # Plain single-process run (no `accelerate launch`): gradient accumulation and clipping are done by the trainer's
20
+ # step budget exactly as in the official code.
21
+ /home/ssalehi/miniconda3/bin/conda run --no-capture-output -n SynthScope python main.py \
 
 
 
 
22
  trainer@TrainerConfig=image/pgc \
23
  model@ModelConfig=image/pgc \
24
  dataset@DatasetConfig=image/cnndetection \
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 \
33
+ logger.mode=offline \
34
+ "logger.tags=[SynthScope,PGC,progan20]"
interface/backend/build_repro_full.py CHANGED
@@ -50,21 +50,37 @@ VAL = {
50
  ("dda", "RealChain"): {"official": {"map": 76.46, "acc": 67.23, "auroc": 74.97}},
51
  ("dda", "RRDataset"): {"official": {"map": 85.18, "acc": 75.99, "auroc": 81.74}},
52
  # D3QE: only the official checkpoint is evaluated (no SynthScope-trained number). acc = plain = balanced (balanced subsets).
53
- ("d3qe", "ARForensics"): {"official": {"map": 92.22, "acc": 81.74, "auroc": 91.91}},
54
- ("d3qe", "ForenSynths"): {"official": {"map": 92.23, "acc": 83.45, "auroc": 90.65}},
55
- ("d3qe", "GenImage"): {"official": {"map": 90.94, "acc": 79.44, "auroc": 90.49}},
 
56
  ("d3qe", "SynthWildX"): {"official": {"map": 49.3, "acc": 49.58, "auroc": 45.47}},
57
  ("d3qe", "WildRF"): {"official": {"map": 63.65, "acc": 53.03, "auroc": 63.89}},
58
  ("d3qe", "BFree-Online"): {"official": {"map": 68.77, "acc": 47.03, "auroc": 64.0}},
59
  ("d3qe", "RealChain"): {"official": {"map": 47.24, "acc": 49.49, "auroc": 43.49}},
60
  ("d3qe", "EvalGEN"): {"official": {"map": 91.48, "acc": 62.21, "auroc": 83.94}},
61
  ("d3qe", "RRDataset"): {"official": {"map": 59.25, "acc": 54.95, "auroc": 55.11}},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  ("fat_former", "4-class"): {"official": {"map": 99.7, "acc": 98.39}},
63
  ("fat_former", "Diffusion-model"): {"official": {"map": 98.6, "acc": 93.72}},
64
- ("npr", "Table 1"): {"official": {"map": 94.22, "acc": 92.30}},
 
65
  ("npr", "Table 2"): {"official": {"map": 96.49, "acc": 94.81}},
66
  ("npr", "Table 3"): {"official": {"map": 99.4, "acc": 95.03}},
67
- ("npr", "Table 4"): {"official": {"map": 96.69, "acc": 93.94}},
68
  ("npr", "Table 5"): {"official": {"map": 83.47, "acc": 79.13}},
69
  # SAFE now SERVES the SynthScope-trained checkpoint (retrain v2 / model_38), which reproduces the paper.
70
  ("safe", "ForenSynths"): {"official": {"map": 98.78, "acc": 96.2}, "synthscope": {"map": 97.79, "acc": 95.1}},
@@ -74,8 +90,9 @@ VAL = {
74
  ("safe", "DiTFake"): {"synthscope": {"map": 99.96, "acc": 99.2}},
75
  ("safe", "DiffusionForensics"): {"synthscope": {"map": 99.34, "acc": 91.3}},
76
  ("pgc", "GenImage"): {"official": {"map": 100.0, "acc": 99.49}, "synthscope": {"map": 97.62, "acc": 78.6}},
77
- ("pgc", "UniversalFakeDetect"): {"official": {"map": 99.34, "acc": 94.16}, "synthscope": {"map": 99.01}},
78
- ("pgc", "ForenSynths"): {"official": {"map": 96.25, "acc": 83.73}, "synthscope": {"map": 89.03}},
 
79
  ("pgc", "AIGIBench"): {"official": {"map": 95.36, "acc": 88.33}, "synthscope": {"map": 86.88}},
80
  ("pgc", "CommGen15"): {"official": {"map": 99.26, "acc": 97.39}, "synthscope": {"map": 98.61, "acc": 66.66}},
81
  # SPAI now SERVES official weights remapped into the SynthScope class, scored at NATIVE resolution
 
50
  ("dda", "RealChain"): {"official": {"map": 76.46, "acc": 67.23, "auroc": 74.97}},
51
  ("dda", "RRDataset"): {"official": {"map": 85.18, "acc": 75.99, "auroc": 81.74}},
52
  # D3QE: only the official checkpoint is evaluated (no SynthScope-trained number). acc = plain = balanced (balanced subsets).
53
+ # synthscope = trained in SynthScope with the official recipe (experiments/image/d3qe.sh; job 265064, eval 265826).
54
+ ("d3qe", "ARForensics"): {"official": {"map": 92.22, "acc": 81.74, "auroc": 91.91}, "synthscope": {"map": 91.73, "acc": 79.83, "auroc": 91.49}},
55
+ ("d3qe", "ForenSynths"): {"official": {"map": 92.23, "acc": 83.45, "auroc": 90.65}, "synthscope": {"map": 91.35, "acc": 82.14, "auroc": 90.29}},
56
+ ("d3qe", "GenImage"): {"official": {"map": 90.94, "acc": 79.44, "auroc": 90.49}, "synthscope": {"map": 90.11, "acc": 77.01, "auroc": 89.63}},
57
  ("d3qe", "SynthWildX"): {"official": {"map": 49.3, "acc": 49.58, "auroc": 45.47}},
58
  ("d3qe", "WildRF"): {"official": {"map": 63.65, "acc": 53.03, "auroc": 63.89}},
59
  ("d3qe", "BFree-Online"): {"official": {"map": 68.77, "acc": 47.03, "auroc": 64.0}},
60
  ("d3qe", "RealChain"): {"official": {"map": 47.24, "acc": 49.49, "auroc": 43.49}},
61
  ("d3qe", "EvalGEN"): {"official": {"map": 91.48, "acc": 62.21, "auroc": 83.94}},
62
  ("d3qe", "RRDataset"): {"official": {"map": 59.25, "acc": 54.95, "auroc": 55.11}},
63
+ # OmniAID: official checkpoints only. Mirage-Test acc = the paper's metric (4 balanced categories + Anime fake accuracy).
64
+ ("omniaid", "GenImage (8 subsets) — GenImage-SD1.4"): {"official": {"map": 99.02, "acc": 95.82, "auroc": 99.16}},
65
+ ("omniaid", "AIGCDetectBenchmark (17) — GenImage-SD1.4"): {"official": {"map": 97.88, "acc": 88.87, "auroc": 98.2}},
66
+ ("omniaid", "Mirage-Test (5 categories) — GenImage-SD1.4"): {"official": {"acc": 51.1}},
67
+ ("omniaid", "GenImage (8 subsets) — Mirage-Train v1"): {"official": {"map": 99.2, "acc": 97.16, "auroc": 99.12}},
68
+ ("omniaid", "AIGCDetectBenchmark (17) — Mirage-Train v1"): {"official": {"map": 98.93, "acc": 92.88, "auroc": 98.99}},
69
+ ("omniaid", "Mirage-Test (5 categories) — Mirage-Train v1"): {"official": {"acc": 88.39}},
70
+ ("omniaid", "Mirage-Test (v2"): {"official": {"map": 96.48, "acc": 78.46, "auroc": 96.65}},
71
+ ("omniaid", "SynthWildX (v2"): {"official": {"map": 95.52, "acc": 86.02, "auroc": 95.31}},
72
+ ("omniaid", "WildRF (v2"): {"official": {"map": 97.28, "acc": 88.36, "auroc": 96.91}},
73
+ ("omniaid", "BFree-Online (v2"): {"official": {"map": 91.7, "acc": 70.26, "auroc": 87.18}},
74
+ ("omniaid", "RealChain (v2"): {"official": {"map": 84.02, "acc": 61.71, "auroc": 83.63}},
75
+ ("omniaid", "EvalGEN (v2"): {"official": {"map": 96.46, "acc": 74.14, "auroc": 93.08}},
76
+ ("omniaid", "RRDataset (v2"): {"official": {"map": 84.19, "acc": 71.75, "auroc": 82.32}},
77
  ("fat_former", "4-class"): {"official": {"map": 99.7, "acc": 98.39}},
78
  ("fat_former", "Diffusion-model"): {"official": {"map": 98.6, "acc": 93.72}},
79
+ # synthscope = trained in SynthScope with the official recipe (experiments/image/npr.sh; job 265720, eval 265808).
80
+ ("npr", "Table 1"): {"official": {"map": 94.22, "acc": 92.30}, "synthscope": {"map": 93.83, "acc": 84.09, "auroc": 94.94}},
81
  ("npr", "Table 2"): {"official": {"map": 96.49, "acc": 94.81}},
82
  ("npr", "Table 3"): {"official": {"map": 99.4, "acc": 95.03}},
83
+ ("npr", "Table 4"): {"official": {"map": 96.69, "acc": 93.94}, "synthscope": {"map": 97.49, "acc": 88.91, "auroc": 97.23}},
84
  ("npr", "Table 5"): {"official": {"map": 83.47, "acc": 79.13}},
85
  # SAFE now SERVES the SynthScope-trained checkpoint (retrain v2 / model_38), which reproduces the paper.
86
  ("safe", "ForenSynths"): {"official": {"map": 98.78, "acc": 96.2}, "synthscope": {"map": 97.79, "acc": 95.1}},
 
90
  ("safe", "DiTFake"): {"synthscope": {"map": 99.96, "acc": 99.2}},
91
  ("safe", "DiffusionForensics"): {"synthscope": {"map": 99.34, "acc": 91.3}},
92
  ("pgc", "GenImage"): {"official": {"map": 100.0, "acc": 99.49}, "synthscope": {"map": 97.62, "acc": 78.6}},
93
+ # synthscope UFD / ForenSynths = the official recipe in SynthScope, step 500 (experiments/image/pgc.sh; jobs 266276, 266282, 266660).
94
+ ("pgc", "UniversalFakeDetect"): {"official": {"map": 99.34, "acc": 94.16}, "synthscope": {"map": 98.99, "acc": 91.96, "auroc": 98.92}},
95
+ ("pgc", "ForenSynths"): {"official": {"map": 96.25, "acc": 83.73}, "synthscope": {"map": 97.41, "acc": 85.89, "auroc": 97.63}},
96
  ("pgc", "AIGIBench"): {"official": {"map": 95.36, "acc": 88.33}, "synthscope": {"map": 86.88}},
97
  ("pgc", "CommGen15"): {"official": {"map": 99.26, "acc": 97.39}, "synthscope": {"map": 98.61, "acc": 66.66}},
98
  # SPAI now SERVES official weights remapped into the SynthScope class, scored at NATIVE resolution
interface/backend/datasets.py CHANGED
@@ -120,6 +120,9 @@ DATASET_DEFS: list[DatasetDef] = [
120
  DatasetDef("rrdataset", "RRDataset (original / transfer / redigital)", "image",
121
  ["/hddstore/ssalehi/SynthScope/RRDataset"], ["test"],
122
  "Real vs AI images as created, after sharing online, and after re-digitization (ICCV 2025)."),
 
 
 
123
  DatasetDef("arforensics", "ARForensics (7 autoregressive generators)", "image",
124
  ["/hddstore/ssalehi/SynthScope/ARForensics"], ["test"],
125
  "LlamaGen, VAR, Infinity, Janus-Pro, RAR, Switti and Open-MAGVIT2 vs ImageNet reals, 6000 each (D3QE, ICCV 2025)."),
 
120
  DatasetDef("rrdataset", "RRDataset (original / transfer / redigital)", "image",
121
  ["/hddstore/ssalehi/SynthScope/RRDataset"], ["test"],
122
  "Real vs AI images as created, after sharing online, and after re-digitization (ICCV 2025)."),
123
+ DatasetDef("mirage_test", "Mirage-Test (human, animal, object, scene, anime)", "image",
124
+ ["/hddstore/ssalehi/SynthScope/MirageTest"], ["test"],
125
+ "Recent generators tuned for realism, in five content categories (OmniAID); Anime is fakes only."),
126
  DatasetDef("arforensics", "ARForensics (7 autoregressive generators)", "image",
127
  ["/hddstore/ssalehi/SynthScope/ARForensics"], ["test"],
128
  "LlamaGen, VAR, Infinity, Janus-Pro, RAR, Switti and Open-MAGVIT2 vs ImageNet reals, 6000 each (D3QE, ICCV 2025)."),
interface/backend/engine.py CHANGED
@@ -107,6 +107,9 @@ def _build_model(cfg):
107
  if model_type == "dda":
108
  from models.image.dda.dda import SynthScopeDDAModel
109
  return SynthScopeDDAModel(cfg)
 
 
 
110
  if model_type == "d3qe":
111
  from models.image.d3qe.d3qe import SynthScopeD3QEModel
112
  return SynthScopeD3QEModel(cfg)
 
107
  if model_type == "dda":
108
  from models.image.dda.dda import SynthScopeDDAModel
109
  return SynthScopeDDAModel(cfg)
110
+ if model_type == "omniaid":
111
+ from models.image.omniaid.omniaid import SynthScopeOmniAIDModel
112
+ return SynthScopeOmniAIDModel(cfg)
113
  if model_type == "d3qe":
114
  from models.image.d3qe.d3qe import SynthScopeD3QEModel
115
  return SynthScopeD3QEModel(cfg)
interface/backend/registry.py CHANGED
@@ -347,6 +347,26 @@ _add(ModelSpec(
347
  "tokenizer, read by a small transformer, fused with frozen CLIP ViT-L/14 features. Trained on LlamaGen vs "
348
  "ImageNet only. Uses the OFFICIAL released checkpoint.",
349
  ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  _add(ModelSpec(
351
  key="npr", name="NPR", paper="Tan et al., CVPR'24",
352
  modality="image", predict_kind="bce_npr", checkpoint="npr.pth",
@@ -417,6 +437,9 @@ INPUT_NOTES: dict[str, tuple[str, str]] = {
417
  "images are padded with black; the rest of a larger image is not seen."),
418
  "d3qe": ("Resized to 256, center crop", "The shorter side is scaled to 256 and the central 256×256 square is used; "
419
  "its CLIP branch looks at the central 224×224 of that square."),
 
 
 
420
  "cure": ("Paper-specific pipeline", "As in the paper: PNG images are re-saved as JPEG, the shorter side is scaled to "
421
  "390 and center-cropped, resized to 336, then JPEG-compressed again (quality 70)."),
422
  }
 
347
  "tokenizer, read by a small transformer, fused with frozen CLIP ViT-L/14 features. Trained on LlamaGen vs "
348
  "ImageNet only. Uses the OFFICIAL released checkpoint.",
349
  ))
350
+ # OmniAID's official eval preprocessing: v2 resizes straight to 336 (data/datasets.py); the v1-era releases used a
351
+ # double resize (512x512, then 336x336) - the commented-out pipeline that the official code says to use for v1 weights.
352
+ omniaid_v1_transform = T.Compose([T.Resize((512, 512)), T.Resize((336, 336)), T.ToTensor(), _norm(CLIP_MEAN, CLIP_STD)])
353
+ for _key, _name, _ckpt, _tf, _note in [
354
+ ("omniaid", "OmniAID", "omniaid.pth", tf_square_resize(336, None, CLIP_MEAN, CLIP_STD),
355
+ "the recommended v2 release, trained on Mirage-Train (the authors' modern in-the-wild dataset)"),
356
+ ("omniaid_v1", "OmniAID (v1)", "omniaid_v1.pth", omniaid_v1_transform,
357
+ "the v1 release trained on Mirage-Train, the paper's OmniAID-Mirage"),
358
+ ("omniaid_genimage", "OmniAID (GenImage)", "omniaid_genimage.pth", omniaid_v1_transform,
359
+ "the release trained on GenImage SD v1.4 only, for the paper's academic GenImage protocol"),
360
+ ]:
361
+ _add(ModelSpec(
362
+ key=_key, name=_name, paper="Guo et al., arXiv 2511.08423",
363
+ modality="image", predict_kind="ce", checkpoint=_ckpt, ckpt_kind="official_omniaid", weights="official",
364
+ # <key>.pth = the official ckpt/checkpoint_omniaid_*.pth unchanged (HF Yunncheng/OmniAID); full model, strict load.
365
+ overrides=[f"model@ModelConfig=image/{_key}"],
366
+ transform=_tf,
367
+ notes="Hybrid mixture of experts on CLIP ViT-L/14@336: a router picks semantic experts (human, animal, object, "
368
+ "scene, ...) and an always-on artifact expert sees every image. Uses the OFFICIAL checkpoint: " + _note + ".",
369
+ ))
370
  _add(ModelSpec(
371
  key="npr", name="NPR", paper="Tan et al., CVPR'24",
372
  modality="image", predict_kind="bce_npr", checkpoint="npr.pth",
 
437
  "images are padded with black; the rest of a larger image is not seen."),
438
  "d3qe": ("Resized to 256, center crop", "The shorter side is scaled to 256 and the central 256×256 square is used; "
439
  "its CLIP branch looks at the central 224×224 of that square."),
440
+ "omniaid": ("Squashed to 336×336", "The whole image is stretched to a 336×336 square (the aspect ratio changes)."),
441
+ "omniaid_v1": ("Squashed to 512, then 336", "The image is stretched to 512×512 and then to 336×336, as in the v1 release."),
442
+ "omniaid_genimage": ("Squashed to 512, then 336", "The image is stretched to 512×512 and then to 336×336, as in the v1-era releases."),
443
  "cure": ("Paper-specific pipeline", "As in the paper: PNG images are re-saved as JPEG, the shorter side is scaled to "
444
  "390 and center-cropped, resized to 336, then JPEG-compressed again (quality 70)."),
445
  }
interface/backend/repro_full.json CHANGED
@@ -286,8 +286,8 @@
286
  "benchmark": "ARForensics (7 autoregressive generators, full test set)",
287
  "metric": "accuracy / AP (mean over generators)",
288
  "paper": "acc=82.11 / map=92.07",
289
- "ours": "mAP=92.22 / acc=81.74 / AUROC=91.91 (official wts)",
290
- "ours_synthscope": "",
291
  "ours_official": "mAP=92.22 / acc=81.74 / AUROC=91.91",
292
  "status": "official",
293
  "note": "Official checkpoint through SynthScope: 81.74 acc / 92.22 AP (AUROC 91.91) on all 84,000 images; LlamaGen 97.16/99.58, VAR 84.46/95.16, Infinity 63.06/80.26, Janus-Pro 92.39/97.85, RAR 91.70/97.88, Switti 74.23/88.71, Open-MAGVIT2 69.21/86.12, every generator within 1.1 pt acc (tag d3qe_arforensics, job 264795)."
@@ -296,20 +296,20 @@
296
  "benchmark": "ForenSynths (paper's 7 GANs, cross-paradigm)",
297
  "metric": "accuracy / AP (mean over generators)",
298
  "paper": "acc=83.73 / map=92.23",
299
- "ours": "mAP=92.23 / acc=83.45 / AUROC=90.65 (official wts)",
300
- "ours_synthscope": "",
301
  "ours_official": "mAP=92.23 / acc=83.45 / AUROC=90.65",
302
- "status": "official",
303
  "note": "Official checkpoint through SynthScope (bench_eval --dataset cnndetection --select the 7 GANs): 83.45 acc / 92.23 AP (AUROC 90.65); ProGAN 95.10, StyleGAN 76.21, StyleGAN2 75.08, BigGAN 86.07, CycleGAN 83.27, StarGAN 73.64, GauGAN 94.76, every GAN within 1.5 pt (tag d3qe_cnndetection, job 264795)."
304
  },
305
  {
306
  "benchmark": "GenImage (paper's 6 diffusion subsets, cross-paradigm)",
307
  "metric": "accuracy / AP (mean over generators)",
308
  "paper": "acc=78.61 / map=89.6",
309
- "ours": "mAP=90.94 / acc=79.44 / AUROC=90.49 (official wts)",
310
- "ours_synthscope": "",
311
  "ours_official": "mAP=90.94 / acc=79.44 / AUROC=90.49",
312
- "status": "official",
313
  "note": "Official checkpoint through SynthScope (bench_eval --dataset genimage_full --select the 6 subsets): 79.44 acc / 90.94 AP (AUROC 90.49), the mean within 0.9 pt; per subset the gaps are larger (ADM 63.67, -6.8; GLIDE 91.13; Midjourney 63.80; SD-v1.4 82.21; SD-v1.5 85.10; Wukong 90.73, +6.3), so the paper's GenImage test sample probably differs from ours (tag d3qe_genimage_full, job 264795)."
314
  },
315
  {
@@ -542,8 +542,8 @@
542
  "benchmark": "Table 1 \u2014 ForenSynths / Cross-GAN-Sources (8 GANs) \u2014 HEADLINE",
543
  "metric": "AP (and acc@0.5)",
544
  "paper": "map=96.1 / acc=92.5",
545
- "ours": "mAP=94.22 / acc=92.3 (official wts)",
546
- "ours_synthscope": "",
547
  "ours_official": "mAP=94.22 / acc=92.3",
548
  "status": "official",
549
  "note": "Our npr_official_forensynths.json logs the full 13-gen mean 79.45 mAP; the 8-GAN subset {progan,cyclegan,biggan,gaugan,stargan,stylegan,stylegan2,deepfake} computes to 92.81 mAP / 91.83 acc."
@@ -572,8 +572,8 @@
572
  "benchmark": "Table 4 \u2014 UniversalFakeDetect / Ojha",
573
  "metric": "AP (and acc@0.5)",
574
  "paper": "acc=95.2",
575
- "ours": "mAP=96.69 / acc=93.94 (official wts)",
576
- "ours_synthscope": "",
577
  "ours_official": "mAP=96.69 / acc=93.94",
578
  "status": "official",
579
  "note": "n/a \u2014 96.69 mAP / 93.94 acc, close to paper acc 95.2."
@@ -589,6 +589,148 @@
589
  "note": "n/a \u2014 83.47 mAP / 79.13 acc \u2248 paper acc 80.1."
590
  }
591
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  "pgc": [
593
  {
594
  "benchmark": "GenImage",
@@ -604,21 +746,21 @@
604
  "benchmark": "UniversalFakeDetect (UFD)",
605
  "metric": "AP",
606
  "paper": "map=98.0",
607
- "ours": "SynthScope mAP=99.01 \u00b7 official mAP=99.34 / acc=94.16",
608
- "ours_synthscope": "mAP=99.01",
609
  "ours_official": "mAP=99.34 / acc=94.16",
610
  "status": "reproduced",
611
- "note": "Reproduced (official-wts 99.34, SynthScope retrain 99.01) >= paper 98.0; no gap."
612
  },
613
  {
614
  "benchmark": "ForenSynths (CNNDetection test)",
615
  "metric": "AP",
616
  "paper": "\u2014",
617
- "ours": "SynthScope mAP=89.03 \u00b7 official mAP=96.25 / acc=83.73",
618
- "ours_synthscope": "mAP=89.03",
619
  "ours_official": "mAP=96.25 / acc=83.73",
620
  "status": "reference",
621
- "note": "Official-weights-in-SynthScope reproduces (96.25 mAP). SynthScope retrain (89.03) below on the GAN generators (ProGAN-trained LoRA-DINOv2 transfers less to cyclegan/gaugan); residual cross-generator gap, not a code defect."
622
  },
623
  {
624
  "benchmark": "AIGIBench (AIGI)",
 
286
  "benchmark": "ARForensics (7 autoregressive generators, full test set)",
287
  "metric": "accuracy / AP (mean over generators)",
288
  "paper": "acc=82.11 / map=92.07",
289
+ "ours": "SynthScope mAP=91.73 / acc=79.83 / AUROC=91.49 \u00b7 official mAP=92.22 / acc=81.74 / AUROC=91.91",
290
+ "ours_synthscope": "mAP=91.73 / acc=79.83 / AUROC=91.49",
291
  "ours_official": "mAP=92.22 / acc=81.74 / AUROC=91.91",
292
  "status": "official",
293
  "note": "Official checkpoint through SynthScope: 81.74 acc / 92.22 AP (AUROC 91.91) on all 84,000 images; LlamaGen 97.16/99.58, VAR 84.46/95.16, Infinity 63.06/80.26, Janus-Pro 92.39/97.85, RAR 91.70/97.88, Switti 74.23/88.71, Open-MAGVIT2 69.21/86.12, every generator within 1.1 pt acc (tag d3qe_arforensics, job 264795)."
 
296
  "benchmark": "ForenSynths (paper's 7 GANs, cross-paradigm)",
297
  "metric": "accuracy / AP (mean over generators)",
298
  "paper": "acc=83.73 / map=92.23",
299
+ "ours": "SynthScope mAP=91.35 / acc=82.14 / AUROC=90.29 \u00b7 official mAP=92.23 / acc=83.45 / AUROC=90.65",
300
+ "ours_synthscope": "mAP=91.35 / acc=82.14 / AUROC=90.29",
301
  "ours_official": "mAP=92.23 / acc=83.45 / AUROC=90.65",
302
+ "status": "reproduced",
303
  "note": "Official checkpoint through SynthScope (bench_eval --dataset cnndetection --select the 7 GANs): 83.45 acc / 92.23 AP (AUROC 90.65); ProGAN 95.10, StyleGAN 76.21, StyleGAN2 75.08, BigGAN 86.07, CycleGAN 83.27, StarGAN 73.64, GauGAN 94.76, every GAN within 1.5 pt (tag d3qe_cnndetection, job 264795)."
304
  },
305
  {
306
  "benchmark": "GenImage (paper's 6 diffusion subsets, cross-paradigm)",
307
  "metric": "accuracy / AP (mean over generators)",
308
  "paper": "acc=78.61 / map=89.6",
309
+ "ours": "SynthScope mAP=90.11 / acc=77.01 / AUROC=89.63 \u00b7 official mAP=90.94 / acc=79.44 / AUROC=90.49",
310
+ "ours_synthscope": "mAP=90.11 / acc=77.01 / AUROC=89.63",
311
  "ours_official": "mAP=90.94 / acc=79.44 / AUROC=90.49",
312
+ "status": "reproduced",
313
  "note": "Official checkpoint through SynthScope (bench_eval --dataset genimage_full --select the 6 subsets): 79.44 acc / 90.94 AP (AUROC 90.49), the mean within 0.9 pt; per subset the gaps are larger (ADM 63.67, -6.8; GLIDE 91.13; Midjourney 63.80; SD-v1.4 82.21; SD-v1.5 85.10; Wukong 90.73, +6.3), so the paper's GenImage test sample probably differs from ours (tag d3qe_genimage_full, job 264795)."
314
  },
315
  {
 
542
  "benchmark": "Table 1 \u2014 ForenSynths / Cross-GAN-Sources (8 GANs) \u2014 HEADLINE",
543
  "metric": "AP (and acc@0.5)",
544
  "paper": "map=96.1 / acc=92.5",
545
+ "ours": "SynthScope mAP=93.83 / acc=84.09 / AUROC=94.94 \u00b7 official mAP=94.22 / acc=92.3",
546
+ "ours_synthscope": "mAP=93.83 / acc=84.09 / AUROC=94.94",
547
  "ours_official": "mAP=94.22 / acc=92.3",
548
  "status": "official",
549
  "note": "Our npr_official_forensynths.json logs the full 13-gen mean 79.45 mAP; the 8-GAN subset {progan,cyclegan,biggan,gaugan,stargan,stylegan,stylegan2,deepfake} computes to 92.81 mAP / 91.83 acc."
 
572
  "benchmark": "Table 4 \u2014 UniversalFakeDetect / Ojha",
573
  "metric": "AP (and acc@0.5)",
574
  "paper": "acc=95.2",
575
+ "ours": "SynthScope mAP=97.49 / acc=88.91 / AUROC=97.23 \u00b7 official mAP=96.69 / acc=93.94",
576
+ "ours_synthscope": "mAP=97.49 / acc=88.91 / AUROC=97.23",
577
  "ours_official": "mAP=96.69 / acc=93.94",
578
  "status": "official",
579
  "note": "n/a \u2014 96.69 mAP / 93.94 acc, close to paper acc 95.2."
 
589
  "note": "n/a \u2014 83.47 mAP / 79.13 acc \u2248 paper acc 80.1."
590
  }
591
  ],
592
+ "omniaid": [
593
+ {
594
+ "benchmark": "GenImage (8 subsets) \u2014 GenImage-SD1.4 release",
595
+ "metric": "accuracy (mean over subsets)",
596
+ "paper": "acc=95.94",
597
+ "ours": "mAP=99.02 / acc=95.82 / AUROC=99.16 (official wts)",
598
+ "ours_synthscope": "",
599
+ "ours_official": "mAP=99.02 / acc=95.82 / AUROC=99.16",
600
+ "status": "official",
601
+ "note": "omniaid_genimage through SynthScope: 95.82 (mAP 99.02), every subset within 1.5 pt; v1-era 512->336 preprocessing (tag omniaid_genimage_genimage_full, job 265856)."
602
+ },
603
+ {
604
+ "benchmark": "AIGCDetectBenchmark (17) \u2014 GenImage-SD1.4 release",
605
+ "metric": "accuracy (mean over subsets)",
606
+ "paper": "acc=88.87",
607
+ "ours": "mAP=97.88 / acc=88.87 / AUROC=98.2 (official wts)",
608
+ "ours_synthscope": "",
609
+ "ours_official": "mAP=97.88 / acc=88.87 / AUROC=98.2",
610
+ "status": "official",
611
+ "note": "omniaid_genimage through SynthScope: 88.87 (mAP 97.88), exactly the paper's number (tag omniaid_genimage_aigcdetect, job 265856)."
612
+ },
613
+ {
614
+ "benchmark": "Mirage-Test (5 categories) \u2014 GenImage-SD1.4 release",
615
+ "metric": "accuracy (mean of Human/Animal/Object/Scene balanced + Anime fake accuracy)",
616
+ "paper": "acc=51.1",
617
+ "ours": "acc=51.1 (official wts)",
618
+ "ours_synthscope": "",
619
+ "ours_official": "acc=51.1",
620
+ "status": "official",
621
+ "note": "omniaid_genimage through SynthScope: 51.10, every category equal to the paper (Scene 46.52) (tag omniaid_genimage_mirage_test, job 265935)."
622
+ },
623
+ {
624
+ "benchmark": "GenImage (8 subsets) \u2014 Mirage-Train v1 release (OmniAID-Mirage)",
625
+ "metric": "accuracy (mean over subsets)",
626
+ "paper": "acc=97.24",
627
+ "ours": "mAP=99.2 / acc=97.16 / AUROC=99.12 (official wts)",
628
+ "ours_synthscope": "",
629
+ "ours_official": "mAP=99.2 / acc=97.16 / AUROC=99.12",
630
+ "status": "official",
631
+ "note": "omniaid_v1 through SynthScope: 97.16 (mAP 99.20), every subset within 0.6 pt (tag omniaid_v1_genimage_full, job 265857)."
632
+ },
633
+ {
634
+ "benchmark": "AIGCDetectBenchmark (17) \u2014 Mirage-Train v1 release (OmniAID-Mirage)",
635
+ "metric": "accuracy (mean over subsets)",
636
+ "paper": "acc=92.88",
637
+ "ours": "mAP=98.93 / acc=92.88 / AUROC=98.99 (official wts)",
638
+ "ours_synthscope": "",
639
+ "ours_official": "mAP=98.93 / acc=92.88 / AUROC=98.99",
640
+ "status": "official",
641
+ "note": "omniaid_v1 through SynthScope: 92.88 (mAP 98.93), exactly the paper's number (tag omniaid_v1_aigcdetect, job 265857)."
642
+ },
643
+ {
644
+ "benchmark": "Mirage-Test (5 categories) \u2014 Mirage-Train v1 release (OmniAID-Mirage)",
645
+ "metric": "accuracy (mean of Human/Animal/Object/Scene balanced + Anime fake accuracy)",
646
+ "paper": "acc=88.39",
647
+ "ours": "acc=88.39 (official wts)",
648
+ "ours_synthscope": "",
649
+ "ours_official": "acc=88.39",
650
+ "status": "official",
651
+ "note": "omniaid_v1 through SynthScope: 88.39, every category equal to the paper (tag omniaid_v1_mirage_test, job 265936)."
652
+ },
653
+ {
654
+ "benchmark": "Chameleon / DRCT-2M",
655
+ "metric": "accuracy",
656
+ "paper": "acc=77.35",
657
+ "ours": "",
658
+ "ours_synthscope": "",
659
+ "ours_official": "",
660
+ "status": "missing",
661
+ "note": "Chameleon is released by request only; DRCT-2M is not in SynthScope yet."
662
+ },
663
+ {
664
+ "benchmark": "Mirage-Test (v2 release, served)",
665
+ "metric": "balanced accuracy",
666
+ "paper": "\u2014",
667
+ "ours": "mAP=96.48 / acc=78.46 / AUROC=96.65 (official wts)",
668
+ "ours_synthscope": "",
669
+ "ours_official": "mAP=96.48 / acc=78.46 / AUROC=96.65",
670
+ "status": "reference",
671
+ "note": "omniaid (v2) through SynthScope: 78.46 in the paper's metric (balanced acc 80.37, mAP 96.48) (tag omniaid_mirage_test, job 265937)."
672
+ },
673
+ {
674
+ "benchmark": "SynthWildX (v2 release, served)",
675
+ "metric": "balanced accuracy",
676
+ "paper": "\u2014",
677
+ "ours": "mAP=95.52 / acc=86.02 / AUROC=95.31 (official wts)",
678
+ "ours_synthscope": "",
679
+ "ours_official": "mAP=95.52 / acc=86.02 / AUROC=95.31",
680
+ "status": "reference",
681
+ "note": "omniaid (v2) through SynthScope: 86.02 (mAP 95.52, AUROC 95.31) (tag omniaid_synthwildx, job 265858)."
682
+ },
683
+ {
684
+ "benchmark": "WildRF (v2 release, served)",
685
+ "metric": "balanced accuracy",
686
+ "paper": "\u2014",
687
+ "ours": "mAP=97.28 / acc=88.36 / AUROC=96.91 (official wts)",
688
+ "ours_synthscope": "",
689
+ "ours_official": "mAP=97.28 / acc=88.36 / AUROC=96.91",
690
+ "status": "reference",
691
+ "note": "omniaid (v2) through SynthScope: 88.36 (mAP 97.28, AUROC 96.91) (tag omniaid_wildrf, job 265858)."
692
+ },
693
+ {
694
+ "benchmark": "BFree-Online (v2 release, served)",
695
+ "metric": "balanced accuracy",
696
+ "paper": "\u2014",
697
+ "ours": "mAP=91.7 / acc=70.26 / AUROC=87.18 (official wts)",
698
+ "ours_synthscope": "",
699
+ "ours_official": "mAP=91.7 / acc=70.26 / AUROC=87.18",
700
+ "status": "reference",
701
+ "note": "omniaid (v2) through SynthScope: 70.26 (mAP 91.70, AUROC 87.18) (tag omniaid_bfree_online, job 265858)."
702
+ },
703
+ {
704
+ "benchmark": "RealChain (v2 release, served)",
705
+ "metric": "balanced accuracy",
706
+ "paper": "\u2014",
707
+ "ours": "mAP=84.02 / acc=61.71 / AUROC=83.63 (official wts)",
708
+ "ours_synthscope": "",
709
+ "ours_official": "mAP=84.02 / acc=61.71 / AUROC=83.63",
710
+ "status": "reference",
711
+ "note": "omniaid (v2) through SynthScope: 61.71 (mAP 84.02, AUROC 83.63) (tag omniaid_realchain, job 265858)."
712
+ },
713
+ {
714
+ "benchmark": "EvalGEN (v2 release, served)",
715
+ "metric": "balanced accuracy",
716
+ "paper": "\u2014",
717
+ "ours": "mAP=96.46 / acc=74.14 / AUROC=93.08 (official wts)",
718
+ "ours_synthscope": "",
719
+ "ours_official": "mAP=96.46 / acc=74.14 / AUROC=93.08",
720
+ "status": "reference",
721
+ "note": "omniaid (v2) through SynthScope: 74.14 (mAP 96.46, AUROC 93.08) (tag omniaid_evalgen, job 265858)."
722
+ },
723
+ {
724
+ "benchmark": "RRDataset (v2 release, served)",
725
+ "metric": "balanced accuracy",
726
+ "paper": "\u2014",
727
+ "ours": "mAP=84.19 / acc=71.75 / AUROC=82.32 (official wts)",
728
+ "ours_synthscope": "",
729
+ "ours_official": "mAP=84.19 / acc=71.75 / AUROC=82.32",
730
+ "status": "reference",
731
+ "note": "omniaid (v2) through SynthScope: 71.75 (mAP 84.19, AUROC 82.32) (tag omniaid_rrdataset, job 265858)."
732
+ }
733
+ ],
734
  "pgc": [
735
  {
736
  "benchmark": "GenImage",
 
746
  "benchmark": "UniversalFakeDetect (UFD)",
747
  "metric": "AP",
748
  "paper": "map=98.0",
749
+ "ours": "SynthScope mAP=98.99 / acc=91.96 / AUROC=98.92 \u00b7 official mAP=99.34 / acc=94.16",
750
+ "ours_synthscope": "mAP=98.99 / acc=91.96 / AUROC=98.92",
751
  "ours_official": "mAP=99.34 / acc=94.16",
752
  "status": "reproduced",
753
+ "note": "Reproduced (official weights 99.34; the official recipe trained in SynthScope, step 500: 98.99) >= paper 98.0."
754
  },
755
  {
756
  "benchmark": "ForenSynths (CNNDetection test)",
757
  "metric": "AP",
758
  "paper": "\u2014",
759
+ "ours": "SynthScope mAP=97.41 / acc=85.89 / AUROC=97.63 \u00b7 official mAP=96.25 / acc=83.73",
760
+ "ours_synthscope": "mAP=97.41 / acc=85.89 / AUROC=97.63",
761
  "ours_official": "mAP=96.25 / acc=83.73",
762
  "status": "reference",
763
+ "note": "Not a paper number. The official recipe trained in SynthScope (step 500) gives FS-8 97.41 mAP, above the official checkpoint (96.41 on the same evaluation); the earlier 89.03 retrain overshot the early transfer peak."
764
  },
765
  {
766
  "benchmark": "AIGIBench (AIGI)",
models/image/d3qe/README.md CHANGED
@@ -32,7 +32,7 @@ is downloaded to `~/.cache/clip` (or `$SYNTHSCOPE_CLIP_DIR`) and the LlamaGen VQ
32
  | `official/` = the official `networks/D3QE.py`, `vq_model.py` and `clip/model.py`, **byte-identical bodies** | `SynthScopeD3QEModel` subclasses the official `D3QE`, so the checkpoint loads key-for-key; MIT attribution in the headers. |
33
  | `official/clip/clip.py` reduced to the official `load()` path D3QE uses (CPU, non-JIT) | The text tokenizer is not needed; `from pkg_resources import packaging`, which current setuptools no longer provides, became `import packaging.version`. |
34
  | `pretrained_prefixes = ("clip_model.", "vq_model.")` | The checkpoint does not contain the frozen CLIP and VQ weights (the official `eval.py` falls back to `strict=False`). The engine accepts missing keys only under these prefixes; every trained tensor still loads strictly. |
35
- | `compute_loss` = BCE on the logit, labels passed to `forward` | The official objective; the forward pass updates the codebook statistics during training as in the official code. ARForensics training is not reproduced here. |
36
 
37
  Two properties of the released code are kept on purpose, because they decide the released scores:
38
  * the VQ tokenizer sees the [0, 1] image: `forward` builds a [-1, 1] copy but encodes the unnormalized one;
@@ -55,9 +55,9 @@ accuracy equals SynthScope's balanced accuracy.
55
 
56
  | Benchmark | Paper (acc / AP) | SynthScope_official (acc / AP) | SynthScope_train | Status |
57
  |---|---|---|---|---|
58
- | **ARForensics** (7 AR generators, all 84,000 test images) | 82.11 / 92.07 | **81.74 / 92.22** (AUROC 91.91) | — | **reproduced** |
59
- | **ForenSynths** (the paper's 7 GANs) | 83.73 / 92.23 | **83.45 / 92.23** (AUROC 90.65) | — | **reproduced** |
60
- | **GenImage** (the paper's 6 subsets) | 78.61 / 89.60 | **79.44 / 90.94** (AUROC 90.49) | — | **reproduced** on the mean; per-subset gaps up to 6.8 pt |
61
 
62
  Per generator (SynthScope vs paper, accuracy):
63
 
@@ -78,6 +78,23 @@ BFree-Online 47.03, RealChain 49.49, EvalGEN 62.21 (balanced; AUROC 83.94), RRDa
78
  Runs: jobs 264795 (ARForensics, ForenSynths, GenImage) and 264796 (in-the-wild sets); results in
79
  `repro/bench_results/d3qe_*.json`.
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  ## Usage
82
 
83
  ```bash
 
32
  | `official/` = the official `networks/D3QE.py`, `vq_model.py` and `clip/model.py`, **byte-identical bodies** | `SynthScopeD3QEModel` subclasses the official `D3QE`, so the checkpoint loads key-for-key; MIT attribution in the headers. |
33
  | `official/clip/clip.py` reduced to the official `load()` path D3QE uses (CPU, non-JIT) | The text tokenizer is not needed; `from pkg_resources import packaging`, which current setuptools no longer provides, became `import packaging.version`. |
34
  | `pretrained_prefixes = ("clip_model.", "vq_model.")` | The checkpoint does not contain the frozen CLIP and VQ weights (the official `eval.py` falls back to `strict=False`). The engine accepts missing keys only under these prefixes; every trained tensor still loads strictly. |
35
+ | `compute_loss` = BCE on the logit, labels passed to `forward` | The official objective; the forward pass updates the codebook statistics during training as in the official code. The full official recipe is `experiments/image/d3qe.sh` (see *Training in SynthScope* below). |
36
 
37
  Two properties of the released code are kept on purpose, because they decide the released scores:
38
  * the VQ tokenizer sees the [0, 1] image: `forward` builds a [-1, 1] copy but encodes the unnormalized one;
 
55
 
56
  | Benchmark | Paper (acc / AP) | SynthScope_official (acc / AP) | SynthScope_train | Status |
57
  |---|---|---|---|---|
58
+ | **ARForensics** (7 AR generators, all 84,000 test images) | 82.11 / 92.07 | **81.74 / 92.22** (AUROC 91.91) | 79.83 / 91.73 | official **reproduced**; trained: AP matches, acc 2.3 below |
59
+ | **ForenSynths** (the paper's 7 GANs) | 83.73 / 92.23 | **83.45 / 92.23** (AUROC 90.65) | **82.14 / 91.35** | **reproduced** (official and trained) |
60
+ | **GenImage** (the paper's 6 subsets) | 78.61 / 89.60 | **79.44 / 90.94** (AUROC 90.49) | **77.01 / 90.11** | **reproduced** on the mean (official and trained); per-subset gaps up to 6.8 pt |
61
 
62
  Per generator (SynthScope vs paper, accuracy):
63
 
 
78
  Runs: jobs 264795 (ARForensics, ForenSynths, GenImage) and 264796 (in-the-wild sets); results in
79
  `repro/bench_results/d3qe_*.json`.
80
 
81
+ ## Training in SynthScope
82
+
83
+ `experiments/image/d3qe.sh` runs the official recipe (`train.py` and its `TrainOptions` defaults) through SynthScope's
84
+ trainer: ARForensics train (100k LlamaGen vs 100k ImageNet) and val; short side 256, then the CNNDetection blur/JPEG
85
+ augmentation (p 0.1 each; `CNNDetAugment`, identical to the official `data_augment` over 300 seeded draws), RandomCrop
86
+ 256, flip, no normalization; AdamW lr 8e-5, weight decay 0.01, batch 32 on one GPU, 10 epochs; the best-validation
87
+ checkpoint is kept and the learning rate is divided by 10 after 5 epochs without improvement (trainer `plateau`). It runs
88
+ as a plain single process, not through the repo's DeepSpeed accelerate config, whose gradient clipping the official
89
+ recipe does not have.
90
+
91
+ Our run (job 265064, 15 h on one A5000) improved its validation accuracy every epoch (88.5 → 94.8), so, as in the
92
+ released checkpoint (62,500 steps), the last epoch is the best and the learning rate never dropped. Against the paper it
93
+ gives 79.83 / 91.73 (ARForensics), 82.14 / 91.35 (ForenSynths-7) and 77.01 / 90.11 (GenImage-6): AP reproduces everywhere
94
+ (within 0.9 pt), accuracy is 1.6 to 2.3 pt lower. Per generator the trained model matches the official one closely
95
+ (LlamaGen 97.49 vs 97.16) but calls a few more unseen-generator fakes real (real accuracy 97.4 vs 96.4), which moves the
96
+ fixed-threshold accuracy, not the ranking. Results: `repro/bench_results/d3qe_trained_*.json` (job 265826).
97
+
98
  ## Usage
99
 
100
  ```bash
models/image/d3qe/d3qe.py CHANGED
@@ -26,6 +26,7 @@ Input contract: ``forward`` takes the tensor of the official test transform —
26
  import os
27
  from pathlib import Path
28
 
 
29
  import torch.nn as nn
30
  from omegaconf import DictConfig
31
 
@@ -58,6 +59,7 @@ class SynthScopeD3QEModel(D3QE):
58
  # Official objective (networks/trainer.py): BCE on the single logit; passing the labels lets the forward pass
59
  # update the codebook-frequency statistics during training, as in the official code.
60
  images, labels, _folder_ids = batch
61
- outputs = self(images, labels)
62
- loss = self.criterion(outputs.squeeze(1), labels.float())
63
- return {"loss": loss, "outputs": outputs}
 
 
26
  import os
27
  from pathlib import Path
28
 
29
+ import torch
30
  import torch.nn as nn
31
  from omegaconf import DictConfig
32
 
 
59
  # Official objective (networks/trainer.py): BCE on the single logit; passing the labels lets the forward pass
60
  # update the codebook-frequency statistics during training, as in the official code.
61
  images, labels, _folder_ids = batch
62
+ logits = self(images, labels).squeeze(1)
63
+ loss = self.criterion(logits, labels.float())
64
+ # Two-column [-logit, logit] so the trainer's metrics (argmax / softmax[:, 1]) read the single logit right.
65
+ return {"loss": loss, "outputs": torch.stack([-logits, logits], dim=1)}
models/image/dda/dda.py CHANGED
@@ -62,6 +62,7 @@ class SynthScopeDDAModel(nn.Module):
62
 
63
  def compute_loss(self, batch):
64
  images, labels, _folder_ids = batch
65
- outputs = self.forward(images)
66
- loss = self.criterion(outputs.squeeze(1), labels.float())
67
- return {"loss": loss, "outputs": outputs}
 
 
62
 
63
  def compute_loss(self, batch):
64
  images, labels, _folder_ids = batch
65
+ logits = self.forward(images).squeeze(1)
66
+ loss = self.criterion(logits, labels.float())
67
+ # Two-column [-logit, logit] so the trainer's metrics (argmax / softmax[:, 1]) read the single logit right.
68
+ return {"loss": loss, "outputs": torch.stack([-logits, logits], dim=1)}
models/image/npr/README.md CHANGED
@@ -69,18 +69,43 @@ The paper reports both accuracy and average precision, with the main result show
69
 
70
  All benchmarks the paper reports, with our numbers (verified, traced to `repro/bench_results/` + `repro/benchmark_specs/`).
71
 
72
- Values are `mAP / acc`. **SynthScope_official** = the authors' released weights run through the SynthScope class / eval. **SynthScope_train** = the SynthScope class trained from scratch by us. For all NPR entries we did not train from scratch, so SynthScope_train is `—`.
73
 
74
  | Benchmark | Paper | SynthScope_official | SynthScope_train | Status |
75
  |---|---|---|---|---|
76
- | Table 1 — ForenSynths / Cross-GAN (8) | 96.1 / 92.5 | 94.22 / 92.3 | — | official |
77
  | Table 2 — GANGen-Detection (9) | 96.6 / 93.2 | 96.49 / 94.81 | — | official |
78
  | Table 3 — DiffusionForensics / DIRE | 99.8 / 95.3 | 99.4 / 95.03 | — | official |
79
- | Table 4 — UniversalFakeDetect / Ojha | acc 95.2 | 96.69 / 93.94 | — | official |
80
  | Table 5 — Diffusion1kStep | acc 80.1 | 83.47 / 79.13 | — | official |
81
 
82
  **reproduced** = SynthScope-trained checkpoint matches the paper · **official** = official weights loaded into the SynthScope class match (the SynthScope-trained number is shown alongside where we trained from scratch) · **below** = gap remains · **reference** = paper reports no comparable number · **missing** = not yet evaluated.
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  ## Model details
85
 
86
  The SynthScope implementation lives in:
 
69
 
70
  All benchmarks the paper reports, with our numbers (verified, traced to `repro/bench_results/` + `repro/benchmark_specs/`).
71
 
72
+ Values are `mAP / acc`. **SynthScope_official** = the authors' released weights run through the SynthScope class / eval. **SynthScope_train** = the SynthScope class trained from scratch by us with the official recipe (`experiments/image/npr.sh`; see *Training in SynthScope* below), evaluated so far on Tables 1 and 4.
73
 
74
  | Benchmark | Paper | SynthScope_official | SynthScope_train | Status |
75
  |---|---|---|---|---|
76
+ | Table 1 — ForenSynths / Cross-GAN (8) | 96.1 / 92.5 | 94.22 / 92.3 | 93.83 / 84.09 | official; trained: AP close, acc below (as the official training code here) |
77
  | Table 2 — GANGen-Detection (9) | 96.6 / 93.2 | 96.49 / 94.81 | — | official |
78
  | Table 3 — DiffusionForensics / DIRE | 99.8 / 95.3 | 99.4 / 95.03 | — | official |
79
+ | Table 4 — UniversalFakeDetect / Ojha | acc 95.2 | 96.69 / 93.94 | 97.49 / 88.91 | official; trained: AP matches, acc below |
80
  | Table 5 — Diffusion1kStep | acc 80.1 | 83.47 / 79.13 | — | official |
81
 
82
  **reproduced** = SynthScope-trained checkpoint matches the paper · **official** = official weights loaded into the SynthScope class match (the SynthScope-trained number is shown alongside where we trained from scratch) · **below** = gap remains · **reference** = paper reports no comparable number · **missing** = not yet evaluated.
83
 
84
+ ## Training in SynthScope
85
+
86
+ `experiments/image/npr.sh` runs the official recipe (the README's `train.py` command and `networks/trainer.py`): ProGAN
87
+ 4-class (car, cat, chair, horse; 144,024 images), `Resize((256, 256))` → RandomCrop 224 → flip → ImageNet normalization
88
+ (no blur/JPEG: the official `data_augment` call is commented out), Adam lr 2e-4 without weight decay, batch 32 on one
89
+ GPU, 50 epochs, learning rate ×0.9 after epochs 10, 20, 30 and 40, last-epoch checkpoint, seed 100. The SynthScope NPR
90
+ uses the official convolution initialization for this.
91
+
92
+ To tell our pipeline apart from run-to-run variance, we also ran the **official `train.py`, unmodified**, on the same
93
+ data and machine, and scored every checkpoint with the same `bench_eval` command (`mAP / acc`):
94
+
95
+ | Checkpoint | Table 1 — ForenSynths (8 GANs) | Table 4 — UniversalFakeDetect |
96
+ |---|---|---|
97
+ | official `NPR.pth` (served as `npr.pth`) | 94.22 / 92.30 | 97.16 / 94.57 |
98
+ | official `model_epoch_last_3090.pth` | 92.81 / 91.83 | 96.69 / 93.94 |
99
+ | official `train.py` rerun here | 92.72 / 81.47 | 96.56 / 87.61 |
100
+ | **SynthScope recipe (ours)** | **93.83 / 84.09** | **97.49 / 88.91** |
101
+
102
+ Our recipe matches the official code's own rerun on every number (it is slightly better), so the SynthScope pipeline is
103
+ faithful; neither run reaches the released checkpoints' accuracy. AP — how well real and fake are ranked — reproduces;
104
+ the accuracy gap is a threshold shift (our models call more unseen-generator fakes real, e.g. CycleGAN fake recall 48 vs
105
+ 96 for `NPR.pth`), which the released checkpoints did not show and which this environment (PyTorch 2.11 instead of 1.8,
106
+ another GPU) did not reproduce. Runs: training job 265720, official-code rerun 265815, evaluation 265808/265814/266028
107
+ (`repro/bench_results/npr_{official_nprpth,official3090,officialcode_rerun,trained_last,trained_best}_*.json`).
108
+
109
  ## Model details
110
 
111
  The SynthScope implementation lives in:
models/image/npr/npr.py CHANGED
@@ -39,6 +39,13 @@ class NPR(nn.Module):
39
  )
40
  self.head = nn.Linear(layer2_ch, 1)
41
  self.criterion = torch.nn.BCEWithLogitsLoss()
 
 
 
 
 
 
 
42
 
43
 
44
  def NPR_transform(self, x: torch.Tensor) -> torch.Tensor:
 
39
  )
40
  self.head = nn.Linear(layer2_ch, 1)
41
  self.criterion = torch.nn.BCEWithLogitsLoss()
42
+ # Official initialization (networks/resnet.py ResNet.__init__); only matters when training from scratch.
43
+ for m in self.modules():
44
+ if isinstance(m, nn.Conv2d):
45
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
46
+ elif isinstance(m, nn.BatchNorm2d):
47
+ nn.init.constant_(m.weight, 1)
48
+ nn.init.constant_(m.bias, 0)
49
 
50
 
51
  def NPR_transform(self, x: torch.Tensor) -> torch.Tensor:
models/image/omniaid/README.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OmniAID (`omniaid`, `omniaid_v1`, `omniaid_genimage`)
2
+
3
+ **OmniAID: Decoupling Semantics and Artifacts for Universal AI-Generated Image Detection in the Wild** —
4
+ [paper (arXiv 2511.08423)](https://arxiv.org/abs/2511.08423) · [official code](https://github.com/yunncheng/OmniAID) ·
5
+ [Mirage-Test](https://huggingface.co/datasets/Yunncheng/Mirage-Test)
6
+
7
+ A detector trained on one kind of content (say, landscapes) often fails on another (faces), because it mixes up *what*
8
+ is shown with *how* it was made. OmniAID splits these apart with a hybrid mixture of experts on CLIP ViT-L/14@336:
9
+ semantic experts (human, animal, object, scene, ...) are routed per image, and an artifact expert is always on. The
10
+ authors also built **Mirage**, a train/test set of images from recent generators tuned for realism.
11
+
12
+ ## Pipeline (official, reproduced exactly)
13
+
14
+ ```
15
+ image ─ Resize((336, 336)) [v2] | Resize((512, 512)) → Resize((336, 336)) [v1-era releases]
16
+ ─ ToTensor → CLIP normalization
17
+ ├ frozen CLIP vision tower → pooler output → router → top-k semantic experts
18
+ └ MoE ViT-L/14: every attention projection = shared main weights (SVD) + the routed semantic experts
19
+ + the always-on artifact expert
20
+ → ln_post(CLS) → Linear(1024, 2) → logits; P(fake) = softmax(logits)[:, 1] (the official `prob`)
21
+ ```
22
+
23
+ Three official releases (Hugging Face `Yunncheng/OmniAID`, MIT), each a full model loaded with `strict=True` as in the
24
+ official eval:
25
+
26
+ | key | release | trained on | experts × rank, router, top-k | preprocessing |
27
+ |---|---|---|---|---|
28
+ | `omniaid` | `checkpoint_omniaid_v2.pth` (recommended; served on the demo) | Mirage-Train | 6 × 1, 256, top-2 | single resize to 336 |
29
+ | `omniaid_v1` | `checkpoint_omniaid_v1.pth` (the paper's OmniAID-Mirage) | Mirage-Train | 6 × 8, 256, top-2 | 512 → 336 |
30
+ | `omniaid_genimage` | `checkpoint_omniaid_genimage_sd14.pth` (the paper's academic tables) | GenImage SD v1.4 | 3 × 4, 64, top-1 | 512 → 336 |
31
+
32
+ The expert counts, ranks and router widths were read from each checkpoint's tensor shapes (the README's
33
+ `config_omniaid_v1/v2.json` are not in the repo); top-k comes from the official configs and `reward/clean_test.py`.
34
+ The v1-era double resize is the pipeline the official code says to use for v1 weights; for the GenImage release it is
35
+ confirmed by the results below (it reproduces the paper).
36
+
37
+ ## SynthScope adaptations
38
+
39
+ | choice | reason |
40
+ |---|---|
41
+ | `official_arch.py` = the official `models/OmniAID.py`, **byte-identical body** | `SynthScopeOmniAIDModel` subclasses the official `OmniAID`, so the checkpoints load key-for-key; MIT attribution in the header. |
42
+ | `forward` returns the 2-class logits (`cls`) | SynthScope's `ce` convention gives softmax[:, 1], the official `prob`. |
43
+ | one registry key per release | Each release has its own expert configuration and preprocessing. |
44
+ | `compute_loss` = cross-entropy on the logits | **The official recipe trains in two stages** (each semantic expert with hard routing, then the router with the artifact expert fixed) with orthogonality, load-balancing and gating losses; that recipe is not reproduced here. |
45
+
46
+ ## Fidelity check (identical to the official model)
47
+
48
+ The official `OmniAID` (built from the release's config and loaded `strict=True`, as `main_finetune.py` does) and
49
+ SynthScope's class, for all three releases on an A5000: **every state-dict tensor identical** (2804 / 2804 / 1940, max
50
+ |diff| 0.0), the registry transforms equal the official v2 / v1 pipelines pixel for pixel, and **max |logit diff| 0.0
51
+ with 0 verdict flips** on 24 GenImage images (ADM, BigGAN, Midjourney, Wukong; real and fake). Job 265851.
52
+
53
+ ## Results
54
+
55
+ Paper metric: accuracy at 0.5 per subset, mean over subsets (Tables 2, 3, 6). Every subset used here is balanced except
56
+ Mirage-Test's Anime (fakes only), so accuracy equals SynthScope's balanced accuracy.
57
+
58
+ | Benchmark | Release | Paper | SynthScope_official | SynthScope_train | Status |
59
+ |---|---|---|---|---|---|
60
+ | **GenImage** (8 subsets) | `omniaid_genimage` | 95.94 | **95.82** (AP 99.02) | — | **reproduced** (every subset within 1.5 pt) |
61
+ | **AIGCDetectBenchmark** (17) | `omniaid_genimage` | 88.87 | **88.87** (AP 97.88) | — | **reproduced** exactly |
62
+ | **Mirage-Test** (5 categories) | `omniaid_genimage` | 51.10 | **51.10** | — | **reproduced** exactly (every category) |
63
+ | **GenImage** (8 subsets) | `omniaid_v1` | 97.24 | **97.16** (AP 99.20) | — | **reproduced** (every subset within 0.6 pt) |
64
+ | **AIGCDetectBenchmark** (17) | `omniaid_v1` | 92.88 | **92.88** (AP 98.93) | — | **reproduced** exactly |
65
+ | **Mirage-Test** (5 categories) | `omniaid_v1` | 88.39 | **88.39** | — | **reproduced** exactly |
66
+ | Chameleon / DRCT-2M | `omniaid_genimage` | 77.35 / 88.21 | — | — | missing (Chameleon is by request; DRCT-2M is not in SynthScope) |
67
+
68
+ GenImage per subset (SynthScope vs paper): `omniaid_genimage` Midjourney 87.20 vs 85.7 · SD v1.4 98.92 vs 98.9 · SD v1.5
69
+ 98.77 vs 98.8 · ADM 90.40 vs 91.4 · GLIDE 98.97 vs 98.7 · Wukong 97.50 vs 98.1 · VQDM 96.07 vs 97.3 · BigGAN 98.70 vs
70
+ 98.7; `omniaid_v1` Midjourney 98.20 vs 98.0 · SD v1.4 98.69 vs 98.7 · SD v1.5 98.63 vs 98.4 · ADM 88.90 vs 89.5 · GLIDE
71
+ 98.20 vs 98.3 · Wukong 98.43 vs 98.6 · VQDM 98.40 vs 98.4 · BigGAN 97.87 vs 98.1.
72
+
73
+ **Mirage-Test protocol.** The paper's mean averages the balanced accuracy ("Overall") of Human, Animal, Object and Scene with the fake accuracy of Anime, which has no reals; the loader pairs Anime with the other categories' reals, and SynthScope reports the same mean. `omniaid_genimage`: Human 59.38, Animal 55.90, Object 49.02, Scene 46.52, Anime 44.67 (paper Table 3: 59.38, 55.90, 49.02, 46.53, 44.67); `omniaid_v1`: Human 93.69, Animal 81.37, Object 84.92, Scene 87.07, Anime 94.92 (paper: 93.69, 81.37, 84.92, 87.07, 94.92). The recommended v2 release (`omniaid`, not in the paper) gives 78.46 (AP 96.48): Human 88.82, Animal 77.20, Object 75.09, Scene 70.89, Anime 80.28.
74
+
75
+ **In the wild (`omniaid`, v2; new measurements, not in the paper)** — balanced accuracy / AP: SynthWildX 86.02 / 95.52,
76
+ WildRF 88.36 / 97.28, BFree-Online 70.26 / 91.70, RealChain 61.71 / 84.02, EvalGEN 74.14 / 96.46, RRDataset 71.75 /
77
+ 84.19. See [docs/WILD_BENCHMARKS.md](../../../docs/WILD_BENCHMARKS.md).
78
+
79
+ Runs: jobs 265856 (`omniaid_genimage`), 265857 (`omniaid_v1`), 265858 (`omniaid`), 265935-265937 (Mirage-Test); results in
80
+ `repro/bench_results/omniaid*.json`.
81
+
82
+ ## Usage
83
+
84
+ ```bash
85
+ synthscope detect photo.png --model omniaid # v2; needs omniaid.pth in $SYNTHSCOPE_CKPT_DIR + HF access for CLIP
86
+ python benchmark/bench_eval.py --model omniaid_genimage --dataset genimage_full --root /data/GenImage_full
87
+ python benchmark/bench_eval.py --model omniaid_v1 --dataset mirage_test --root /data/MirageTest
88
+ ```
models/image/omniaid/__init__.py ADDED
File without changes
models/image/omniaid/official_arch.py ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vendored from github.com/yunncheng/OmniAID (MIT License per its README and Hugging Face model card), file models/OmniAID.py.
2
+ # Unchanged below this header, so the released checkpoints load key-for-key (strict=True, as the official eval does).
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from typing import Optional, Tuple, Dict
7
+ from transformers import CLIPModel, CLIPVisionConfig
8
+ from transformers.models.clip.modeling_clip import CLIPVisionEmbeddings, CLIPMLP
9
+ from torch.utils.checkpoint import checkpoint
10
+
11
+ class OmniAID(nn.Module):
12
+ def __init__(self, config=None):
13
+ super(OmniAID, self).__init__()
14
+ self.config = config
15
+ self.num_experts = config.num_experts
16
+ self.rank_per_expert = config.rank_per_expert
17
+
18
+ self.moe_lambda_orth = config.moe_lambda_orth
19
+ self.moe_lambda_balance = config.moe_lambda_balance
20
+ self.moe_lambda_gating_cls = config.moe_lambda_gating_cls
21
+ self.svd_dropout = config.dropout
22
+
23
+ self.moe_router_hidden_dim = config.moe_router_hidden_dim
24
+ self.top_k = config.moe_top_k
25
+
26
+ self.gradient_checkpointing = config.gradient_checkpointing_enable
27
+
28
+ self.gating_criterion = torch.nn.CrossEntropyLoss()
29
+
30
+ self.training_mode = "standard" # Default runtime mode, can be changed by set_training_mode
31
+ self.active_expert_idx = None
32
+
33
+ # This property is fixed after initialization
34
+ self.is_hybrid = config.is_hybrid
35
+
36
+ if self.is_hybrid:
37
+ print("Initializing in HYBRID MoE architectural mode.")
38
+ # Use the last expert as the artifact expert
39
+ self.artifact_expert_idx = config.num_experts - 1
40
+ self.num_semantic_experts = config.num_experts - 1
41
+ if self.num_semantic_experts <= 0:
42
+ raise ValueError("num_experts must be at least 2 for the Hybrid MoE model (1 fixed, >=1 semantic).")
43
+ gating_num_experts = self.num_semantic_experts
44
+ else:
45
+ print("Initializing in STANDARD MoE architectural mode.")
46
+ self.artifact_expert_idx = -1 # A value that will never match an expert index
47
+ gating_num_experts = self.num_experts
48
+
49
+ pretrained_path = config.CLIP_path
50
+
51
+ # Initialize an independent Vision Model as the feature extractor. This model will be used exclusively to generate features for the router.
52
+ self.feature_extractor = CLIPModel.from_pretrained(pretrained_path).vision_model
53
+ self.feature_extractor.eval()
54
+
55
+ vision_config = self.feature_extractor.config
56
+ self.hidden_size = vision_config.hidden_size
57
+
58
+ # Calculate rank allocation
59
+ total_rank = vision_config.hidden_size
60
+ residual_rank = self.rank_per_expert
61
+
62
+ if residual_rank >= total_rank:
63
+ raise ValueError(
64
+ f"The total rank for experts ({residual_rank}) must be less than the total rank ({total_rank}). "
65
+ f"Please reduce num_experts ({self.num_experts}) or rank_per_expert ({self.rank_per_expert})."
66
+ )
67
+
68
+ r_main = total_rank - residual_rank
69
+ print(f"Rank allocation: total_rank={total_rank}, r_main={r_main}, num_experts={self.num_experts}, rank_per_expert={self.rank_per_expert}")
70
+
71
+ # Build the MoE backbone network
72
+ self.embeddings = CLIPVisionEmbeddings(vision_config)
73
+ self.ln_pre = nn.LayerNorm(vision_config.hidden_size)
74
+ self.encoder_layers = nn.ModuleList([
75
+ ViTMoELayer(vision_config, self.num_experts, r_main, self.rank_per_expert, self.artifact_expert_idx, self.svd_dropout)
76
+ for _ in range(vision_config.num_hidden_layers)
77
+ ])
78
+ self.ln_post = nn.LayerNorm(self.hidden_size, eps=vision_config.layer_norm_eps)
79
+ self.gating_network = GatingNetwork(
80
+ input_dim=self.hidden_size,
81
+ num_experts=gating_num_experts,
82
+ hidden_dim=self.moe_router_hidden_dim,
83
+ top_k=self.top_k
84
+ )
85
+ self.head = nn.Linear(self.hidden_size, 2)
86
+
87
+ self.load_and_replace_from_pretrained(self.feature_extractor)
88
+
89
+ def load_and_replace_from_pretrained(self, pretrained_model):
90
+ self.embeddings.load_state_dict(pretrained_model.embeddings.state_dict())
91
+ self.ln_pre.load_state_dict(pretrained_model.pre_layrnorm.state_dict())
92
+ self.ln_post.load_state_dict(pretrained_model.post_layernorm.state_dict())
93
+
94
+ for moe_layer, pretrained_layer in zip(self.encoder_layers, pretrained_model.encoder.layers):
95
+ for proj_name in ['q_proj', 'k_proj', 'v_proj', 'out_proj']:
96
+ self._replace_linear_with_svd_moe(
97
+ getattr(pretrained_layer.self_attn, proj_name),
98
+ getattr(moe_layer.self_attn, proj_name)
99
+ )
100
+ moe_layer.layer_norm1.load_state_dict(pretrained_layer.layer_norm1.state_dict())
101
+ moe_layer.mlp.load_state_dict(pretrained_layer.mlp.state_dict())
102
+ moe_layer.layer_norm2.load_state_dict(pretrained_layer.layer_norm2.state_dict())
103
+
104
+ # Freeze non-MoE parameters
105
+ for name, param in self.named_parameters():
106
+ if 'experts' not in name and 'gating' not in name and 'head' not in name:
107
+ param.requires_grad = False
108
+
109
+ def _replace_linear_with_svd_moe(self, original_module: nn.Linear, moe_module):
110
+ original_weight = original_module.weight.data
111
+ moe_module.weight_original_fnorm.data.copy_(torch.norm(original_weight, p='fro'))
112
+ if original_module.bias is not None:
113
+ moe_module.bias.data.copy_(original_module.bias.data)
114
+
115
+ U, S, Vh = torch.linalg.svd(original_weight, full_matrices=False)
116
+
117
+ # Initialize the main (shared) weight matrix
118
+ r = moe_module.r_main
119
+ U_r, S_r, Vh_r = U[:, :r], S[:r], Vh[:r, :]
120
+ moe_module.weight_main.data.copy_(U_r @ torch.diag(S_r) @ Vh_r)
121
+
122
+ moe_module.U_r.data.copy_(U_r)
123
+ moe_module.V_r.data.copy_(Vh_r)
124
+
125
+ # Define the SINGLE, SHARED chunk from ALL remaining singular values
126
+ chunk_start_rank = r
127
+
128
+ # Check if there are any singular values left
129
+ if chunk_start_rank >= len(S):
130
+ # If not, initialize all experts to zero
131
+ for i in range(moe_module.num_experts):
132
+ moe_module.U_experts[i].data.zero_()
133
+ moe_module.S_experts[i].data.zero_()
134
+ moe_module.V_experts[i].data.zero_()
135
+ return
136
+
137
+ U_chunk, S_chunk, Vh_chunk = U[:, chunk_start_rank:], S[chunk_start_rank:], Vh[chunk_start_rank:, :]
138
+
139
+ # The expert's parameter dimension is fixed at rank_per_expert. We must pad the chunk if its actual rank is smaller
140
+ actual_chunk_rank = U_chunk.shape[1]
141
+
142
+ if actual_chunk_rank < moe_module.rank_per_expert:
143
+ pad_rank = moe_module.rank_per_expert - actual_chunk_rank
144
+ U_chunk = F.pad(U_chunk, (0, pad_rank))
145
+ S_chunk = F.pad(S_chunk, (0, pad_rank))
146
+ Vh_chunk = F.pad(Vh_chunk, (0, 0, 0, pad_rank))
147
+
148
+ # Assign the SAME shared chunk to all experts
149
+ for i in range(moe_module.num_experts):
150
+ moe_module.U_experts[i].data.copy_(U_chunk)
151
+ moe_module.S_experts[i].data.copy_(S_chunk)
152
+ moe_module.V_experts[i].data.copy_(Vh_chunk)
153
+
154
+ def set_training_mode(self, mode: str, active_expert_idx: int = None, trainable_expert_indices: list = None):
155
+ """
156
+ Sets the training mode of the model, allowing specific parts of the parameters to be frozen/unfrozen.
157
+
158
+ Args:
159
+ mode (str): Training mode. Possible values are:
160
+ - 'hard_sampling': Stage 1. Unfreezes one active expert and the head.
161
+ - 'router_training': Stage 2. Unfreezes the router and the head. Optionally unfreezes specified experts.
162
+ - 'standard': End-to-end. Unfreezes the router, head, and all experts.
163
+ active_expert_idx (int, optional): The index of the expert to be trained in 'hard_sampling' mode. Required for this mode.
164
+ trainable_expert_indices (list, optional): A list of expert indices to also unfreeze during 'router_training' mode.
165
+ Allows for joint training of the router and specific experts.
166
+ """
167
+ if mode not in ['hard_sampling', 'router_training', 'standard']:
168
+ raise ValueError("Training mode must be one of 'hard_sampling', 'router_training', or 'standard'")
169
+
170
+ if mode == 'hard_sampling' and (active_expert_idx is None or not isinstance(active_expert_idx, int) or active_expert_idx < 0):
171
+ raise ValueError("A valid integer active_expert_idx must be provided for 'hard_sampling' mode.")
172
+
173
+ if mode == 'router_training' and trainable_expert_indices is not None and not isinstance(trainable_expert_indices, list):
174
+ raise ValueError("'trainable_expert_indices' must be a list of integers.")
175
+
176
+
177
+ # Store the state in the model instance
178
+ self.training_mode = mode
179
+ self.active_expert_idx = active_expert_idx if mode == 'hard_sampling' else None
180
+
181
+ for param in self.parameters():
182
+ param.requires_grad = False
183
+
184
+ if mode == 'standard':
185
+ # In standard MoE training, train the router, all experts, and the final head
186
+ for param in self.gating_network.parameters():
187
+ param.requires_grad = True
188
+
189
+ for param in self.head.parameters():
190
+ param.requires_grad = True
191
+
192
+ for layer in self.encoder_layers:
193
+ for proj_name in ['q_proj', 'k_proj', 'v_proj', 'out_proj']:
194
+ moe_linear_layer = getattr(layer.self_attn, proj_name)
195
+ for param in moe_linear_layer.U_experts:
196
+ param.requires_grad = True
197
+ for param in moe_linear_layer.S_experts:
198
+ param.requires_grad = True
199
+ for param in moe_linear_layer.V_experts:
200
+ param.requires_grad = True
201
+
202
+ elif mode == 'router_training':
203
+ print("Unfreezing: Gating Network and Head.")
204
+ for param in self.gating_network.parameters():
205
+ param.requires_grad = True
206
+ for param in self.head.parameters():
207
+ param.requires_grad = True
208
+
209
+ # Optionally unfreeze specific experts during router training
210
+ if trainable_expert_indices:
211
+ print(f"Additionally unfreezing experts with indices: {trainable_expert_indices}")
212
+ for expert_idx in trainable_expert_indices:
213
+ if not 0 <= expert_idx < self.num_experts:
214
+ raise ValueError(f"trainable_expert_index ({expert_idx}) is out of bounds for num_experts ({self.num_experts}).")
215
+
216
+ for layer in self.encoder_layers:
217
+ for proj_name in ['q_proj', 'k_proj', 'v_proj', 'out_proj']:
218
+ moe_linear_layer = getattr(layer.self_attn, proj_name)
219
+ moe_linear_layer.U_experts[expert_idx].requires_grad = True
220
+ moe_linear_layer.S_experts[expert_idx].requires_grad = True
221
+ moe_linear_layer.V_experts[expert_idx].requires_grad = True
222
+
223
+ elif mode == 'hard_sampling':
224
+ # In hard_sampling stage, unfreeze only the parameters of the single active expert
225
+ if self.active_expert_idx >= self.num_experts:
226
+ raise ValueError(f"active_expert_idx ({self.active_expert_idx}) is out of bounds for num_experts ({self.num_experts}).")
227
+
228
+ for layer in self.encoder_layers:
229
+ for proj_name in ['q_proj', 'k_proj', 'v_proj', 'out_proj']:
230
+ moe_linear_layer = getattr(layer.self_attn, proj_name)
231
+
232
+ # Unfreeze the parameters (U, S, V) for the specified active expert by direct indexing
233
+ moe_linear_layer.U_experts[self.active_expert_idx].requires_grad = True
234
+ moe_linear_layer.S_experts[self.active_expert_idx].requires_grad = True
235
+ moe_linear_layer.V_experts[self.active_expert_idx].requires_grad = True
236
+
237
+ for param in self.head.parameters():
238
+ param.requires_grad = True
239
+
240
+ print(f"Successfully set training mode to '{self.training_mode}'" + (f" (active expert: {self.active_expert_idx})" if self.training_mode == 'hard_sampling' else ""))
241
+
242
+ # print("Currently trainable parameters:")
243
+ # for name, param in self.named_parameters():
244
+ # if param.requires_grad:
245
+ # print(f"- {name}")
246
+
247
+ # For checkpoint
248
+ @staticmethod
249
+ def run_moe_layer(module, hidden_states, gating_outputs):
250
+ return module(hidden_states, gating_outputs=gating_outputs)[0]
251
+
252
+ def forward(self, images) -> dict:
253
+ batch_size = images.size(0)
254
+
255
+ with torch.no_grad():
256
+ # Use the pooler_output as the routing feature
257
+ routing_features = self.feature_extractor(images, output_hidden_states=False).pooler_output
258
+
259
+ hidden_states = self.embeddings(images)
260
+ hidden_states = self.ln_pre(hidden_states)
261
+
262
+ gating_outputs = {}
263
+
264
+ # Stage 1 expert training ('hard_sampling')
265
+ if self.training_mode == 'hard_sampling':
266
+ if self.active_expert_idx is None:
267
+ raise ValueError("In hard_sampling mode, active_expert_idx must be set.")
268
+
269
+ hard_sampling_target = torch.full((batch_size,), self.active_expert_idx, dtype=torch.long, device=hidden_states.device)
270
+ gating_outputs['top_k_indices'] = hard_sampling_target.unsqueeze(1)
271
+ gating_outputs['top_k_gates'] = torch.ones_like(gating_outputs['top_k_indices'], dtype=hidden_states.dtype)
272
+ gating_outputs['balance_loss'] = torch.tensor(0.0, device=hidden_states.device)
273
+ gating_outputs['gating_logits'] = None
274
+
275
+ # Stage 2 routing ('router_training') or inference for a Hybrid model
276
+ elif self.is_hybrid:
277
+ # Create a "pass-through" gating output for the fixed artifact expert (the last expert)
278
+ artifact_expert_indices = torch.full(
279
+ (batch_size, 1),
280
+ self.artifact_expert_idx, # Use the last expert index
281
+ dtype=torch.long,
282
+ device=hidden_states.device
283
+ )
284
+ artifact_expert_gates = torch.ones(
285
+ (batch_size, 1),
286
+ dtype=hidden_states.dtype,
287
+ device=hidden_states.device
288
+ )
289
+
290
+ # Get routing decisions for the SEMANTIC experts (experts 0, 1, ..., N-2)
291
+ semantic_gating_outputs = self.gating_network(routing_features)
292
+
293
+ semantic_expert_indices = semantic_gating_outputs['top_k_indices']
294
+
295
+ # Combine the routed semantic experts and the fixed artifact expert
296
+ gating_outputs['top_k_indices'] = torch.cat([semantic_expert_indices, artifact_expert_indices], dim=1)
297
+ gating_outputs['top_k_gates'] = torch.cat([semantic_gating_outputs['top_k_gates'], artifact_expert_gates], dim=1)
298
+
299
+ gating_outputs['balance_loss'] = semantic_gating_outputs['balance_loss']
300
+ gating_outputs['gating_logits'] = semantic_gating_outputs['gating_logits']
301
+
302
+ # Standard end-to-end training or inference for a Standard MoE model
303
+ else:
304
+ gating_outputs = self.gating_network(routing_features)
305
+
306
+ final_gates = torch.zeros(batch_size, self.num_experts, device=hidden_states.device, dtype=gating_outputs['top_k_gates'].dtype)
307
+
308
+ final_gates.scatter_(-1, gating_outputs['top_k_indices'], gating_outputs['top_k_gates'])
309
+
310
+ for layer_module in self.encoder_layers:
311
+ if self.gradient_checkpointing and self.training:
312
+ hidden_states = checkpoint(
313
+ self.run_moe_layer,
314
+ layer_module,
315
+ hidden_states,
316
+ gating_outputs,
317
+ use_reentrant=False
318
+ )
319
+ else:
320
+ hidden_states = layer_module(
321
+ hidden_states,
322
+ gating_outputs=gating_outputs,
323
+ )[0]
324
+
325
+ pooled_output = self.ln_post(hidden_states[:, 0, :])
326
+ pred = self.head(pooled_output)
327
+ prob = torch.softmax(pred, dim=1)[:, 1]
328
+
329
+ return {
330
+ 'cls': pred,
331
+ 'prob': prob,
332
+ 'balance_loss': gating_outputs['balance_loss'],
333
+ 'gating_logits': gating_outputs['gating_logits'],
334
+ 'final_gates': final_gates
335
+ }
336
+
337
+ def get_losses(self, pred_dict: dict, labels, expert_domain_labels, criterion) -> dict:
338
+ pred = pred_dict['cls']
339
+ classification_loss = criterion(pred, labels)
340
+
341
+ orth_loss, load_balancing_loss = torch.tensor(0.0, device=pred.device), torch.tensor(0.0, device=pred.device)
342
+ gating_classification_loss = torch.tensor(0.0, device=pred.device)
343
+
344
+ num_moe_layers = sum(1 for module in self.modules() if isinstance(module, SVDMoeLinear))
345
+
346
+ # Orthogonal loss is not calculated during router_training
347
+ if self.training_mode != 'router_training' and num_moe_layers > 0:
348
+ current_orth_loss = 0.0
349
+ for module in self.modules():
350
+ if isinstance(module, SVDMoeLinear):
351
+ if self.training_mode == 'hard_sampling':
352
+ current_orth_loss += module.compute_targeted_orthogonal_loss(self.active_expert_idx)
353
+ elif self.training_mode == 'standard':
354
+ current_orth_loss += module.compute_full_orthogonal_loss()
355
+ orth_loss = current_orth_loss / num_moe_layers
356
+
357
+ # Balance loss is only relevant when the router is active
358
+ if self.training_mode == 'router_training' or self.training_mode == 'standard':
359
+ load_balancing_loss = pred_dict['balance_loss']
360
+
361
+ if self.training_mode == 'router_training' or self.training_mode == 'standard':
362
+ gating_logits = pred_dict['gating_logits']
363
+
364
+ if gating_logits is not None and expert_domain_labels is not None:
365
+
366
+ valid_mask = (expert_domain_labels >= 0) & (expert_domain_labels < self.gating_network.num_experts)
367
+
368
+ filtered_logits = gating_logits[valid_mask]
369
+ filtered_labels = expert_domain_labels[valid_mask]
370
+
371
+ if filtered_logits.shape[0] > 0:
372
+ gating_classification_loss = self.gating_criterion(filtered_logits, filtered_labels)
373
+
374
+
375
+ total_loss = classification_loss + \
376
+ self.moe_lambda_orth * orth_loss + \
377
+ self.moe_lambda_balance * load_balancing_loss + \
378
+ self.moe_lambda_gating_cls * gating_classification_loss
379
+
380
+ return {
381
+ 'overall_loss': total_loss,
382
+ 'classification_loss': classification_loss.detach(),
383
+ 'orth_loss': orth_loss.detach(),
384
+ 'balance_loss': load_balancing_loss.detach(),
385
+ 'gating_cls_loss': gating_classification_loss.detach(),
386
+ }
387
+
388
+
389
+ class ViTMoEAttention(nn.Module):
390
+ def __init__(self, config: CLIPVisionConfig, num_experts: int, r_main: int, rank_per_expert: int, artifact_expert_idx: int, svd_dropout: float):
391
+ super().__init__()
392
+ self.embed_dim = config.hidden_size
393
+ self.num_heads = config.num_attention_heads
394
+ self.head_dim = self.embed_dim // self.num_heads
395
+ if self.head_dim * self.num_heads != self.embed_dim:
396
+ raise ValueError(
397
+ f"embed_dim must be divisible by num_heads (got `embed_dim`: {self.embed_dim} and `num_heads`:"
398
+ f" {self.num_heads})."
399
+ )
400
+
401
+ self.scale = self.head_dim**-0.5
402
+ self.dropout = config.attention_dropout
403
+
404
+ self.q_proj = SVDMoeLinear(self.embed_dim, self.embed_dim, r_main, num_experts, rank_per_expert, artifact_expert_idx, svd_dropout)
405
+ self.k_proj = SVDMoeLinear(self.embed_dim, self.embed_dim, r_main, num_experts, rank_per_expert, artifact_expert_idx, svd_dropout)
406
+ self.v_proj = SVDMoeLinear(self.embed_dim, self.embed_dim, r_main, num_experts, rank_per_expert, artifact_expert_idx, svd_dropout)
407
+ self.out_proj = SVDMoeLinear(self.embed_dim, self.embed_dim, r_main, num_experts, rank_per_expert, artifact_expert_idx, svd_dropout)
408
+
409
+ def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int):
410
+ return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).transpose(1, 2).contiguous()
411
+
412
+ def forward(
413
+ self,
414
+ hidden_states: torch.Tensor,
415
+ gating_outputs: Dict[str, torch.Tensor],
416
+ attention_mask: Optional[torch.Tensor] = None,
417
+ output_attentions: Optional[bool] = False,
418
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
419
+ """
420
+ Input shape: Batch x Time x Channel
421
+ attention_mask: (batch, 1, a_seq_len, b_seq_len)
422
+ """
423
+ bsz, tgt_len, embed_dim = hidden_states.size()
424
+
425
+ query_states = self.q_proj(hidden_states, gating_outputs) * self.scale
426
+ key_states = self.k_proj(hidden_states, gating_outputs)
427
+ value_states = self.v_proj(hidden_states, gating_outputs)
428
+
429
+ query_states = self._shape(query_states, tgt_len, bsz)
430
+ key_states = self._shape(key_states, -1, bsz)
431
+ value_states = self._shape(value_states, -1, bsz)
432
+
433
+ proj_shape = (bsz * self.num_heads, -1, self.head_dim)
434
+ query_states = query_states.reshape(*proj_shape)
435
+ key_states = key_states.reshape(*proj_shape)
436
+ value_states = value_states.reshape(*proj_shape)
437
+
438
+ src_len = key_states.size(1)
439
+ attn_weights = torch.bmm(query_states, key_states.transpose(1, 2))
440
+
441
+ if attention_mask is not None:
442
+ if attention_mask.size() != (bsz, 1, tgt_len, src_len):
443
+ raise ValueError(
444
+ f"Attention mask should be of size {(bsz, 1, tgt_len, src_len)}, but is {attention_mask.size()}"
445
+ )
446
+ attn_weights = attn_weights.view(bsz, self.num_heads, tgt_len, src_len)
447
+ attn_weights = attn_weights + attention_mask
448
+ attn_weights = attn_weights.view(bsz * self.num_heads, tgt_len, src_len)
449
+
450
+ attn_weights = nn.functional.softmax(attn_weights, dim=-1)
451
+
452
+ attn_weights_reshaped = None
453
+ if output_attentions:
454
+ attn_weights_reshaped = attn_weights.view(bsz, self.num_heads, tgt_len, src_len)
455
+ attn_weights = attn_weights_reshaped.view(bsz * self.num_heads, tgt_len, src_len)
456
+
457
+ attn_probs = nn.functional.dropout(attn_weights, p=self.dropout, training=self.training)
458
+
459
+ attn_output = torch.bmm(attn_probs, value_states)
460
+
461
+ attn_output = attn_output.view(bsz, self.num_heads, tgt_len, self.head_dim)
462
+ attn_output = attn_output.transpose(1, 2)
463
+ attn_output = attn_output.reshape(bsz, tgt_len, embed_dim)
464
+
465
+ attn_output = self.out_proj(attn_output, gating_outputs)
466
+
467
+ return attn_output, attn_weights_reshaped
468
+
469
+
470
+ class ViTMoELayer(nn.Module):
471
+ def __init__(self, config: CLIPVisionConfig, num_experts: int, r_main: int, rank_per_expert: int, artifact_expert_idx: int, svd_dropout: float):
472
+ super().__init__()
473
+ self.self_attn = ViTMoEAttention(config, num_experts, r_main, rank_per_expert, artifact_expert_idx, svd_dropout)
474
+ self.layer_norm1 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
475
+ self.mlp = CLIPMLP(config)
476
+ self.layer_norm2 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
477
+
478
+ def forward(self, hidden_states: torch.Tensor, gating_outputs: Dict[str, torch.Tensor], attention_mask: Optional[torch.Tensor] = None):
479
+ residual = hidden_states
480
+ hidden_states = self.layer_norm1(hidden_states)
481
+
482
+ # Pass attention_mask to self_attn
483
+ hidden_states, attn_weights = self.self_attn(
484
+ hidden_states=hidden_states,
485
+ gating_outputs=gating_outputs,
486
+ attention_mask=attention_mask
487
+ )
488
+
489
+ hidden_states = residual + hidden_states
490
+
491
+ residual = hidden_states
492
+ hidden_states = self.layer_norm2(hidden_states)
493
+ hidden_states = self.mlp(hidden_states)
494
+ hidden_states = residual + hidden_states
495
+
496
+ # Maintain the return value format consistent with the official EncoderLayer (tuple)
497
+ return (hidden_states, )
498
+
499
+
500
+ class SVDMoeLinear(nn.Module):
501
+ """
502
+ SVD orthogonal subspace linear layer supporting multiple experts and dynamic gating.
503
+ """
504
+ def __init__(self, in_features, out_features, r_main, num_experts, rank_per_expert, artifact_expert_idx, svd_dropout=0.0, bias=True):
505
+ super(SVDMoeLinear, self).__init__()
506
+ self.in_features = in_features
507
+ self.out_features = out_features
508
+ self.r_main = r_main
509
+ self.num_experts = num_experts
510
+ self.rank_per_expert = rank_per_expert
511
+ self.artifact_expert_idx = artifact_expert_idx
512
+
513
+ self.register_buffer('weight_main', torch.zeros(out_features, in_features))
514
+ self.register_buffer('U_r', torch.zeros(out_features, r_main))
515
+ self.register_buffer('V_r', torch.zeros(r_main, in_features))
516
+
517
+ self.U_experts = nn.ParameterList([nn.Parameter(torch.zeros(out_features, rank_per_expert)) for _ in range(num_experts)])
518
+ self.S_experts = nn.ParameterList([nn.Parameter(torch.zeros(rank_per_expert)) for _ in range(num_experts)])
519
+ self.V_experts = nn.ParameterList([nn.Parameter(torch.zeros(rank_per_expert, in_features)) for _ in range(num_experts)])
520
+
521
+ self.register_buffer('weight_original_fnorm', torch.tensor(0.0))
522
+
523
+ if bias:
524
+ self.bias = nn.Parameter(torch.Tensor(out_features))
525
+ nn.init.zeros_(self.bias)
526
+ else:
527
+ self.register_parameter('bias', None)
528
+
529
+ if svd_dropout > 0.0:
530
+ self.dropout = nn.Dropout(p=svd_dropout)
531
+ else:
532
+ self.dropout = nn.Identity()
533
+
534
+ def forward(self, x: torch.Tensor, gating_outputs: Dict[str, torch.Tensor]) -> torch.Tensor:
535
+ output_main = F.linear(x, self.weight_main, None)
536
+
537
+ x = self.dropout(x)
538
+
539
+ top_k_indices = gating_outputs['top_k_indices'] # Shape: [B, k]
540
+ top_k_gates = gating_outputs['top_k_gates'] # Shape: [B, k]
541
+ k = top_k_indices.size(1)
542
+
543
+ expert_output = torch.zeros_like(output_main)
544
+ U_all = torch.stack([p for p in self.U_experts])
545
+ S_all = torch.stack([p for p in self.S_experts])
546
+ V_all = torch.stack([p for p in self.V_experts])
547
+
548
+ # Record the original input dimension (2 or 3)
549
+ original_dim = x.dim()
550
+
551
+ # We use torch.bmm which requires 3D inputs
552
+ # If x is 2D (Batch, In_features), we unsqueeze it to 3D (Batch, 1, In_features)
553
+ if original_dim == 2:
554
+ x = x.unsqueeze(1) # Shape: [B, I] -> [B, 1, I]
555
+ # We do the same for expert_output to match dimensions for accumulation
556
+ expert_output = expert_output.unsqueeze(1) # Shape: [B, O] -> [B, 1, O]
557
+
558
+ # Loop over the k choices
559
+ for i in range(k):
560
+ chosen_expert_indices = top_k_indices[:, i] # Shape: [B]
561
+ gate_values = top_k_gates[:, i].unsqueeze(-1) # Shape: [B, 1]
562
+
563
+ U_batch = U_all[chosen_expert_indices] # Shape: [B, O, r]
564
+ S_batch = S_all[chosen_expert_indices] # Shape: [B, r]
565
+ V_batch = V_all[chosen_expert_indices] # Shape: [B, r, I]
566
+
567
+ # 1. (x @ V.T)
568
+ # Shape: [B, S, I] @ [B, I, r] -> [B, S, r]
569
+ x_v = torch.bmm(x, V_batch.transpose(1, 2))
570
+
571
+ # 2. (x @ V.T) * S
572
+ # This is equivalent to (x @ V.T) @ diag(S)
573
+ # Shape: [B, S, r] * [B, 1, r] (broadcast) -> [B, S, r]
574
+ x_v_s = x_v * S_batch.unsqueeze(1)
575
+
576
+ # 3. ((x @ V.T) * S) @ U.T is equivalent to `output = x @ (U @ diag(S) @ V).T`
577
+ # Shape: [B, S, r] @ [B, r, O] -> [B, S, O]
578
+ current_expert_output = torch.bmm(x_v_s, U_batch.transpose(1, 2))
579
+
580
+ # 4. Apply gate weights
581
+ # Shape: [B, S, O] * [B, 1, 1] (broadcast) -> [B, S, O]
582
+ expert_output += current_expert_output * gate_values.unsqueeze(-1)
583
+
584
+ # If the input was 2D, squeeze the sequence dimension (S=1) back out
585
+ if original_dim == 2:
586
+ expert_output = expert_output.squeeze(1) # Shape: [B, 1, O] -> [B, O]
587
+
588
+ final_output = output_main + expert_output
589
+ if self.bias is not None:
590
+ final_output = final_output + self.bias
591
+ return final_output
592
+
593
+ def _calculate_pairwise_loss(self, base1, base2):
594
+ error = base1.t() @ base2
595
+ return torch.norm(error, p='fro')
596
+
597
+ def compute_targeted_orthogonal_loss(self, active_expert_idx: int) -> torch.Tensor:
598
+ """
599
+ Efficient orthogonal loss for the hard sampling stage.
600
+ - If the active expert is the artifact expert, computes loss only against the main space.
601
+ - If the active expert is a semantic expert, computes loss against the main space
602
+ and previously trained *semantic* experts, skipping the artifact expert.
603
+ """
604
+ loss = torch.tensor(0.0, device=self.weight_main.device)
605
+ num_pairs = 0
606
+
607
+ active_U = F.normalize(self.U_experts[active_expert_idx], dim=0)
608
+ active_V_t = F.normalize(self.V_experts[active_expert_idx], dim=1).t()
609
+
610
+ # Loss vs. Main space
611
+ loss += self._calculate_pairwise_loss(self.U_r, active_U)
612
+ loss += self._calculate_pairwise_loss(self.V_r.t(), active_V_t)
613
+ num_pairs += 1
614
+
615
+ if active_expert_idx == self.artifact_expert_idx:
616
+ return loss / num_pairs
617
+
618
+ # Loss vs. previously trained semantic experts
619
+ for i in range(active_expert_idx):
620
+ # Skip comparison with the artifact expert
621
+ if i == self.artifact_expert_idx:
622
+ continue
623
+
624
+ prev_U = F.normalize(self.U_experts[i].detach(), dim=0)
625
+ prev_V_t = F.normalize(self.V_experts[i].detach(), dim=1).t()
626
+ loss += self._calculate_pairwise_loss(prev_U, active_U)
627
+ loss += self._calculate_pairwise_loss(prev_V_t, active_V_t)
628
+ num_pairs += 1
629
+
630
+ return loss / num_pairs if num_pairs > 0 else torch.tensor(0.0, device=self.weight_main.device)
631
+
632
+ def compute_full_orthogonal_loss(self) -> torch.Tensor:
633
+ """
634
+ Computes the full pairwise orthogonal loss between all components.
635
+ Used for end-to-end finetuning stages.
636
+ """
637
+ all_U_bases = [self.U_r] + [F.normalize(u, dim=0) for u in self.U_experts]
638
+ all_V_bases_t = [self.V_r.t()] + [F.normalize(v, dim=1).t() for v in self.V_experts]
639
+
640
+ loss = torch.tensor(0.0, device=self.weight_main.device)
641
+ num_pairs = 0
642
+
643
+ # Iterate over all unique pairs of basis matrices
644
+ for i in range(len(all_U_bases)):
645
+ for j in range(i + 1, len(all_U_bases)):
646
+ loss += self._calculate_pairwise_loss(all_U_bases[i], all_U_bases[j])
647
+ loss += self._calculate_pairwise_loss(all_V_bases_t[i], all_V_bases_t[j])
648
+ num_pairs += 1
649
+
650
+ return loss / num_pairs if num_pairs > 0 else torch.tensor(0.0, device=self.weight_main.device)
651
+
652
+
653
+ class GatingNetwork(nn.Module):
654
+ def __init__(self, input_dim: int, num_experts: int, hidden_dim: int = 256, top_k: int = 2):
655
+ super(GatingNetwork, self).__init__()
656
+ self.num_experts = num_experts
657
+ self.top_k = top_k
658
+ self.network = nn.Sequential(
659
+ nn.Linear(input_dim, hidden_dim),
660
+ nn.ReLU(),
661
+ nn.Linear(hidden_dim, num_experts)
662
+ )
663
+
664
+ def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:
665
+ logits = self.network(x) # Shape: [B, N]
666
+
667
+ top_k_logits, top_k_indices = torch.topk(logits, self.top_k, dim=-1) # Shape: [B, k]
668
+ top_k_gates = F.softmax(top_k_logits, dim=-1) # Shape: [B, k]
669
+
670
+ router_probs = F.softmax(logits, dim=-1)
671
+ sparse_mask = torch.zeros_like(logits).scatter_(-1, top_k_indices, 1.0)
672
+ tokens_per_expert = torch.mean(sparse_mask.float(), dim=0)
673
+ router_prob_per_expert = torch.mean(router_probs, dim=0)
674
+ load_balancing_loss = self.num_experts * torch.sum(tokens_per_expert * router_prob_per_expert)
675
+
676
+ return {
677
+ 'gating_logits': logits,
678
+ 'top_k_indices': top_k_indices, # The indices of the chosen experts
679
+ 'top_k_gates': top_k_gates, # The weights for the chosen experts
680
+ 'balance_loss': load_balancing_loss
681
+ }
models/image/omniaid/omniaid.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OmniAID (Decoupling Semantics and Artifacts for Universal AI-Generated Image Detection in the Wild, arXiv 2511.08423).
2
+
3
+ Official repo: github.com/yunncheng/OmniAID (MIT). A CLIP ViT-L/14@336 whose attention projections are split by SVD into
4
+ a shared main subspace plus small expert subspaces (a hybrid mixture of experts):
5
+
6
+ route = frozen CLIP vision tower (pooler output) → router → top-k SEMANTIC experts (e.g. human/animal, object/scene)
7
+ tokens = MoE ViT: every layer uses the routed semantic experts + the always-on ARTIFACT expert
8
+ logits = Linear(ln_post(CLS)) → [B, 2]; P(fake) = softmax(logits)[:, 1] (the official ``prob``)
9
+
10
+ ``SynthScopeOmniAIDModel`` *is* the official ``OmniAID`` class (vendored unchanged in ``official_arch.py``); the released
11
+ checkpoints hold the full model and load key-for-key with ``strict=True`` as in the official eval. The constructor, as in
12
+ the official code, first builds the experts from the CLIP weights (``openai/clip-vit-large-patch14-336``), which the
13
+ checkpoint then overwrites. Each release has its own expert configuration (number of experts, rank, router width,
14
+ top-k), set per checkpoint in the model configs.
15
+
16
+ Training: the official recipe trains in two stages (each semantic expert with hard routing, then the router with the
17
+ artifact expert fixed) with orthogonality, load-balancing and gating losses; ``compute_loss`` here is only the
18
+ classification loss and does NOT reproduce that recipe.
19
+ """
20
+ import types
21
+
22
+ import torch.nn as nn
23
+ from omegaconf import DictConfig
24
+
25
+ from .official_arch import OmniAID
26
+
27
+ CONFIG_KEYS = ("CLIP_path", "num_experts", "rank_per_expert", "moe_lambda_orth", "moe_lambda_balance",
28
+ "moe_lambda_gating_cls", "moe_router_hidden_dim", "moe_top_k", "dropout",
29
+ "gradient_checkpointing_enable", "is_hybrid")
30
+
31
+
32
+ class SynthScopeOmniAIDModel(OmniAID):
33
+ """The official OmniAID model, configured from SynthScope's ModelConfig."""
34
+
35
+ def __init__(self, config: DictConfig):
36
+ mc = config.ModelConfig
37
+ super().__init__(types.SimpleNamespace(**{k: mc[k] for k in CONFIG_KEYS}))
38
+ self.criterion = nn.CrossEntropyLoss()
39
+
40
+ def forward(self, images):
41
+ return super().forward(images)["cls"]
42
+
43
+ def compute_loss(self, batch):
44
+ images, labels, _folder_ids = batch
45
+ logits = self.forward(images)
46
+ return {"loss": self.criterion(logits, labels), "outputs": logits}
models/image/pgc/README.md CHANGED
@@ -58,9 +58,9 @@ The residual is resolution-sensitive, so the experiment uses **crop-only** trans
58
  ## Training setup
59
 
60
  ```bash
61
- conf/model/image/pgc.yaml # model_type: pgc, backbone_name, lora_*, tau_rgb/res
62
- conf/trainer/image/pgc.yaml # Adam, lr 5e-5, weight_decay 0.05
63
- experiments/image/pgc.sh # trains on ProGAN (CNNDetection); swap dataset for GenImage SDv1.4
64
  ```
65
 
66
  Registered in `trainer.py` under `model_type == "pgc"`. Trainable params: LoRA adapters + residual
@@ -68,6 +68,37 @@ stream + PGCM + classifier (DINOv2 backbone frozen). The paper's benchmarks are
68
  AIGI, and UniversalFakeDetect; CommGen15 (the paper's new dataset) is optional — the SynthScope integration
69
  trains on the existing ProGAN/GenImage data.
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  ## Reproduced benchmark results
72
 
73
  All benchmarks the paper reports, with our numbers (verified, traced to `repro/bench_results/` + `repro/benchmark_specs/`).
@@ -75,14 +106,14 @@ All benchmarks the paper reports, with our numbers (verified, traced to `repro/b
75
  | Benchmark | Paper | SynthScope_official | SynthScope_train | Status |
76
  |---|---|---|---|---|
77
  | GenImage | map=100.0 / acc=98.3 | mAP=100.0 / acc=99.49 | mAP=97.62 / acc=78.6 | official |
78
- | UniversalFakeDetect (UFD) | map=98.0 | mAP=99.34 / acc=94.16 | mAP=99.01 | reproduced |
79
- | ForenSynths-8 (CNNDetection test) | mAP=96.25 | mAP=96.39 / acc=76.29 | mAP=96.05 / acc=71.12 | reproduced |
80
  | AIGIBench (AIGI) | map=95.0 / acc=86.6 | mAP=95.36 / acc=88.33 | mAP=86.88 | official |
81
  | CommGen15 (commercial in-the-wild) | acc=97.9 | mAP=99.26 / acc=97.39 | mAP=98.61 / acc=66.66 | official |
82
 
83
  > **ForenSynths hard generators — reproduced via early-stopping (resolved 2026-06-19).** The released ProGAN checkpoint is trained for only **~400 steps (< 1 epoch)** — confirmed by the authors on our issue. DINOv2's strong priors overfit ProGAN within a few epochs (loss → floor), which keeps easy generators high but **erodes hard-generator transfer**; our earlier epoch-level checkpoints (ep10+) all overshot the peak. Re-running the official `train.py` verbatim with fine-grained eval shows the hard-3 (deepfake/san/seeingdark) **mean AUROC climb to 94.56 at step ~900 ≈ official 94.35**, then decay with longer training. So PGC **does** reproduce from the published recipe+data with early-stopping inside the first epoch. (Served PGC uses the official weights.) See `repro/investigate/pgc_gap_investigation.md`.
84
 
85
- > **Eval transform — `PadCenterCrop(224)`, no resize (verified 2026-06-19).** PGC's YCbCr residual is resolution-sensitive, so the eval transform is center-crop-224 with **no resize** (= the official `PadCenterCrop`; byte-identical to the genuine repo's). The earlier `Resize→CenterCrop` registry transform wiped the forensic signal on super-res/low-light generators (san 73.3, seeingdark 72.5 — same eval-transform family as the SAFE/AIDE/SPAI fixes). With the fix, SynthScope `bench_eval` reproduces ForenSynths-8 (96.39 mAP official / 96.05 from-scratch vs paper 96.25) and san recovers to 98.65 AUROC. One generator, **seeingdark**, stays ~10 pt below the genuine repo (77.9 vs 87.49) on identical images/weights with now-bit-exact preprocessing — isolated to the model forward, not the transform; it is not part of the reported ForenSynths-8.
86
 
87
  **reproduced** = SynthScope-trained checkpoint matches the paper · **official** = official weights loaded into the SynthScope class match (the SynthScope-trained number is shown alongside where we trained from scratch) · **below** = gap remains · **reference** = paper reports no comparable number · **missing** = not yet evaluated.
88
 
@@ -92,4 +123,4 @@ All benchmarks the paper reports, with our numbers (verified, traced to `repro/b
92
  |---|---|
93
  | Residual built in-model (not in the dataset transform) | Keeps the SynthScope dataset/transform pipeline standard (3-channel in). Same residual math (BT.601 quantization, Eq. 1) — `_quantization_residual` snaps `round(x01*255)` to the uint8 grid so the residual is **bit-exact** to the official 6-channel transform despite the in-model ImageNet de-normalization (verified zero-diff, `repro/_residual_path_diff.py`). |
94
  | DINOv2-Large loaded via HF name | No local-checkpoint requirement; LoRA targets use the HF DINOv2 module names. |
95
- | Reduced training schedule for the bundled checkpoint | The shipped checkpoint is trained for turnaround on a ProGAN subset; raise epochs / use GenImage for a full reproduction. |
 
58
  ## Training setup
59
 
60
  ```bash
61
+ conf/model/image/pgc.yaml # model_type: pgc, backbone_name, lora_*, tau_rgb/res, label smoothing 0.1
62
+ conf/trainer/image/pgc.yaml # AdamW 5e-5, wd 0.05, 32 x 4 accumulation, clip 1.0, step budget (checkpoint every 100)
63
+ experiments/image/pgc.sh # the official ProGAN recipe (all 20 classes); see "Training in SynthScope" below
64
  ```
65
 
66
  Registered in `trainer.py` under `model_type == "pgc"`. Trainable params: LoRA adapters + residual
 
68
  AIGI, and UniversalFakeDetect; CommGen15 (the paper's new dataset) is optional — the SynthScope integration
69
  trains on the existing ProGAN/GenImage data.
70
 
71
+ ## Training in SynthScope
72
+
73
+ `experiments/image/pgc.sh` runs the official ProGAN recipe (`train.py` + `engine/trainer.py`, README 3.2) through
74
+ SynthScope's trainer: all 20 ProGAN classes (720k images, = the UniversalFakeDetect train set); flip → ColorJitter
75
+ (0.15, 0.15, 0.15, 0.05) p 0.2 → pad + RandomCrop 224 → ImageNet normalization, residual computed in the model; AdamW
76
+ 5e-5, weight decay 0.05, label smoothing 0.1, micro-batch 32 × 4 accumulation, gradient clipping 1.0, seed 42, one GPU.
77
+ The authors' released checkpoint is only **~400 steps** in, because DINOv2's prior overfits ProGAN within the first
78
+ epoch, so the run is a budget of 2000 micro-batches (trainer `step_budget`) with a checkpoint every 100, and the early
79
+ curve is evaluated. (The official cosine schedule spans 100 epochs and is stepped per optimizer step, so the learning
80
+ rate stays at 5e-5 here.) The run takes 38 minutes on one A5000 (job 266276).
81
+
82
+ ForenSynths with the same `bench_eval` command for every checkpoint (job 266282; FS-8 = the paper's 8 GANs, hard-3 =
83
+ deepfake, SAN, SeeingDark):
84
+
85
+ | checkpoint | FS-8 mAP | FS-8 acc | hard-3 AUROC | FS-13 mAP |
86
+ |---|---|---|---|---|
87
+ | official `PGC_train_progan_ckpt.pth` | 96.41 | 76.34 | 90.87 | 93.80 |
88
+ | step 300 | 97.60 | 90.82 | 92.88 | 95.07 |
89
+ | **step 500** | **97.41** | **85.89** | **93.93** | **95.78** |
90
+ | step 900 | 97.82 | 85.71 | 93.88 | 97.12 |
91
+ | step 1300 | 97.63 | 85.61 | 92.76 | 97.03 |
92
+ | step 1700 | 97.21 | 84.06 | 91.03 | 96.69 |
93
+ | step 1900 | — | 74.49 (FS-13) | — | 95.97 |
94
+
95
+ The paper reports no standalone ForenSynths number, so the reference is the official checkpoint: every checkpoint from
96
+ step 300 on matches or exceeds it on ForenSynths-8 mAP (96.41 on this evaluation); transfer to the hard generators peaks around steps 500-900 and then erodes, as the authors
97
+ describe. **Step 500**, the saved checkpoint nearest the authors' ~400 steps (picked by that rule, not by test score),
98
+ is the SynthScope-trained number in the table below; on UniversalFakeDetect, which the paper does report, it gives 98.99 mAP / 91.96 acc (paper
99
+ 98.0 — reproduced; official 99.17 / 83.57 on the same run, job 266660). The GenImage, AIGIBench and CommGen15 SynthScope_train entries are
100
+ from an earlier SynthScope recipe (2 GPUs, full cosine over epochs) and have not been rerun with this one.
101
+
102
  ## Reproduced benchmark results
103
 
104
  All benchmarks the paper reports, with our numbers (verified, traced to `repro/bench_results/` + `repro/benchmark_specs/`).
 
106
  | Benchmark | Paper | SynthScope_official | SynthScope_train | Status |
107
  |---|---|---|---|---|
108
  | GenImage | map=100.0 / acc=98.3 | mAP=100.0 / acc=99.49 | mAP=97.62 / acc=78.6 | official |
109
+ | UniversalFakeDetect (UFD) | map=98.0 | mAP=99.34 / acc=94.16 | **mAP=98.99 / acc=91.96** | **reproduced** (step 500) |
110
+ | ForenSynths-8 (CNNDetection test) | — (not reported separately; official checkpoint 96.25) | mAP=96.39 / acc=76.29 | **mAP=97.41 / acc=85.89** | matches/exceeds the official checkpoint (step 500) |
111
  | AIGIBench (AIGI) | map=95.0 / acc=86.6 | mAP=95.36 / acc=88.33 | mAP=86.88 | official |
112
  | CommGen15 (commercial in-the-wild) | acc=97.9 | mAP=99.26 / acc=97.39 | mAP=98.61 / acc=66.66 | official |
113
 
114
  > **ForenSynths hard generators — reproduced via early-stopping (resolved 2026-06-19).** The released ProGAN checkpoint is trained for only **~400 steps (< 1 epoch)** — confirmed by the authors on our issue. DINOv2's strong priors overfit ProGAN within a few epochs (loss → floor), which keeps easy generators high but **erodes hard-generator transfer**; our earlier epoch-level checkpoints (ep10+) all overshot the peak. Re-running the official `train.py` verbatim with fine-grained eval shows the hard-3 (deepfake/san/seeingdark) **mean AUROC climb to 94.56 at step ~900 ≈ official 94.35**, then decay with longer training. So PGC **does** reproduce from the published recipe+data with early-stopping inside the first epoch. (Served PGC uses the official weights.) See `repro/investigate/pgc_gap_investigation.md`.
115
 
116
+ > **Eval transform — `PadCenterCrop(224)`, no resize (verified 2026-06-19).** PGC's YCbCr residual is resolution-sensitive, so the eval transform is center-crop-224 with **no resize** (= the official `PadCenterCrop`; byte-identical to the genuine repo's). The earlier `Resize→CenterCrop` registry transform wiped the forensic signal on super-res/low-light generators (san 73.3, seeingdark 72.5 — same eval-transform family as the SAFE/AIDE/SPAI fixes). With the fix, SynthScope `bench_eval` gives ForenSynths-8 96.39 mAP with the official weights and 96.05 with the official code retrained (the paper reports no ForenSynths number; 96.25 was the earlier official-weights measurement) and san recovers to 98.65 AUROC. One generator, **seeingdark**, stays ~10 pt below the genuine repo (77.9 vs 87.49) on identical images/weights with now-bit-exact preprocessing — isolated to the model forward, not the transform; it is not part of the reported ForenSynths-8.
117
 
118
  **reproduced** = SynthScope-trained checkpoint matches the paper · **official** = official weights loaded into the SynthScope class match (the SynthScope-trained number is shown alongside where we trained from scratch) · **below** = gap remains · **reference** = paper reports no comparable number · **missing** = not yet evaluated.
119
 
 
123
  |---|---|
124
  | Residual built in-model (not in the dataset transform) | Keeps the SynthScope dataset/transform pipeline standard (3-channel in). Same residual math (BT.601 quantization, Eq. 1) — `_quantization_residual` snaps `round(x01*255)` to the uint8 grid so the residual is **bit-exact** to the official 6-channel transform despite the in-model ImageNet de-normalization (verified zero-diff, `repro/_residual_path_diff.py`). |
125
  | DINOv2-Large loaded via HF name | No local-checkpoint requirement; LoRA targets use the HF DINOv2 module names. |
126
+ | Training budget | The official checkpoint is ~400 steps in; `experiments/image/pgc.sh` saves every 100 micro-batches up to 2000 instead of training 100 epochs, which overfits ProGAN. |
pyproject.toml CHANGED
@@ -46,6 +46,7 @@ dependencies = [
46
  "pytorch_wavelets>=1.3",
47
  "PyWavelets>=1.9",
48
  "opencv-python-headless", # CuRe preprocessing (JPEG round-trips, resizing)
 
49
  "setuptools<81", # pytorch_wavelets (SAFE) still imports pkg_resources, removed in setuptools 81
50
  ]
51
 
 
46
  "pytorch_wavelets>=1.3",
47
  "PyWavelets>=1.9",
48
  "opencv-python-headless", # CuRe preprocessing (JPEG round-trips, resizing)
49
+ "scipy", # CNNDetection-style blur augmentation (transformations.CNNDetAugment)
50
  "setuptools<81", # pytorch_wavelets (SAFE) still imports pkg_resources, removed in setuptools 81
51
  ]
52
 
repro/MASTER_RESULTS.jsonl CHANGED
@@ -296,3 +296,42 @@
296
  {"date": "2026-10-26T09:48:10", "tag": "d3qe_cnndetection", "model": "d3qe", "ckpt": "registry-default", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 7, "means": {"acc": 83.45, "map": 92.23, "mauroc": 90.65}}
297
  {"date": "2026-10-26T10:02:32", "tag": "d3qe_genimage_full", "model": "d3qe", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenImage_full/test", "n_generators": 6, "means": {"acc": 79.44, "map": 90.94, "mauroc": 90.49}}
298
  {"date": "2026-10-26T10:14:34", "tag": "d3qe_rrdataset", "model": "d3qe", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RRDataset/test", "n_generators": 3, "means": {"acc": 54.95, "map": 59.25, "mauroc": 55.11}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  {"date": "2026-10-26T09:48:10", "tag": "d3qe_cnndetection", "model": "d3qe", "ckpt": "registry-default", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 7, "means": {"acc": 83.45, "map": 92.23, "mauroc": 90.65}}
297
  {"date": "2026-10-26T10:02:32", "tag": "d3qe_genimage_full", "model": "d3qe", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenImage_full/test", "n_generators": 6, "means": {"acc": 79.44, "map": 90.94, "mauroc": 90.49}}
298
  {"date": "2026-10-26T10:14:34", "tag": "d3qe_rrdataset", "model": "d3qe", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RRDataset/test", "n_generators": 3, "means": {"acc": 54.95, "map": 59.25, "mauroc": 55.11}}
299
+ {"date": "2026-10-26T21:42:06", "tag": "d3qe_trained_ep3_arforensics", "model": "d3qe", "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/snapshot_ep3_best.pth", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/ARForensics/test", "n_generators": 7, "means": {"acc": 81.11, "map": 90.77, "mauroc": 90.97}}
300
+ {"date": "2026-10-27T06:02:46", "tag": "d3qe_trained_arforensics", "model": "d3qe", "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/model_best.pth", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/ARForensics/test", "n_generators": 7, "means": {"acc": 79.83, "map": 91.73, "mauroc": 91.49}}
301
+ {"date": "2026-10-27T06:28:33", "tag": "d3qe_trained_cnndetection", "model": "d3qe", "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/model_best.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 7, "means": {"acc": 82.14, "map": 91.35, "mauroc": 90.29}}
302
+ {"date": "2026-10-27T06:42:56", "tag": "d3qe_trained_genimage_full", "model": "d3qe", "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/model_best.pth", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenImage_full/test", "n_generators": 6, "means": {"acc": 77.01, "map": 90.11, "mauroc": 89.63}}
303
+ {"date": "2026-10-27T02:49:02", "tag": "npr_official_nprpth_forensynths8", "model": "npr", "ckpt": "registry-default", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 8, "means": {"acc": 92.3, "map": 94.22, "mauroc": 96.08}}
304
+ {"date": "2026-10-27T02:49:53", "tag": "npr_official_nprpth_ufd", "model": "npr", "ckpt": "registry-default", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 94.57, "map": 97.16, "mauroc": 97.68}}
305
+ {"date": "2026-10-27T02:53:54", "tag": "npr_trained_last_forensynths8", "model": "npr", "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_last.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 8, "means": {"acc": 84.09, "map": 93.83, "mauroc": 94.94}}
306
+ {"date": "2026-10-27T02:54:44", "tag": "npr_trained_last_ufd", "model": "npr", "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_last.pth", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 88.91, "map": 97.49, "mauroc": 97.23}}
307
+ {"date": "2026-10-27T02:58:43", "tag": "npr_trained_best_forensynths8", "model": "npr", "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_best.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 8, "means": {"acc": 84.3, "map": 91.81, "mauroc": 93.3}}
308
+ {"date": "2026-10-27T02:59:33", "tag": "npr_trained_best_ufd", "model": "npr", "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_best.pth", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 85.34, "map": 95.56, "mauroc": 95.3}}
309
+ {"date": "2026-10-27T03:25:16", "tag": "npr_official3090_forensynths8", "model": "npr", "ckpt": "/ivi/zfs/s0/original_homes/ssalehi/SynthScope_shared/npr_ref/model_epoch_last_3090.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 8, "means": {"acc": 91.83, "map": 92.81, "mauroc": 95.18}}
310
+ {"date": "2026-10-27T03:26:05", "tag": "npr_official3090_ufd", "model": "npr", "ckpt": "/ivi/zfs/s0/original_homes/ssalehi/SynthScope_shared/npr_ref/model_epoch_last_3090.pth", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 93.94, "map": 96.69, "mauroc": 97.26}}
311
+ {"date": "2026-10-27T13:08:53", "tag": "npr_officialcode_rerun_forensynths8", "model": "npr", "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_official_rerun/4class-resnet-car-cat-chair-horse2026_10_27_03_27_09/model_epoch_last.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 8, "means": {"acc": 81.47, "map": 92.72, "mauroc": 93.97}}
312
+ {"date": "2026-10-27T13:09:44", "tag": "npr_officialcode_rerun_ufd", "model": "npr", "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_official_rerun/4class-resnet-car-cat-chair-horse2026_10_27_03_27_09/model_epoch_last.pth", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 87.61, "map": 96.56, "mauroc": 96.43}}
313
+ {"date": "2026-10-27T18:34:34", "tag": "pgc_official_forensynths13", "model": "pgc", "ckpt": "registry-default", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 70.58, "map": 93.8, "mauroc": 94.37}}
314
+ {"date": "2026-10-27T19:07:32", "tag": "pgc_trained_step300_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step300.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 79.88, "map": 95.07, "mauroc": 95.66}}
315
+ {"date": "2026-10-27T19:39:25", "tag": "pgc_trained_step500_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step500.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 82.98, "map": 95.78, "mauroc": 96.15}}
316
+ {"date": "2026-10-27T20:11:21", "tag": "pgc_trained_step700_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step700.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 80.35, "map": 96.67, "mauroc": 96.6}}
317
+ {"date": "2026-10-27T20:43:13", "tag": "pgc_trained_step900_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step900.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 79.92, "map": 97.12, "mauroc": 97.01}}
318
+ {"date": "2026-10-27T21:15:31", "tag": "pgc_trained_step1100_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step1100.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 75.44, "map": 96.74, "mauroc": 96.5}}
319
+ {"date": "2026-10-27T21:47:29", "tag": "pgc_trained_step1300_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step1300.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 78.24, "map": 97.03, "mauroc": 96.8}}
320
+ {"date": "2026-10-27T22:19:20", "tag": "pgc_trained_step1500_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step1500.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 77.2, "map": 96.85, "mauroc": 96.54}}
321
+ {"date": "2026-10-27T22:51:19", "tag": "pgc_trained_step1700_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step1700.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 74.18, "map": 96.69, "mauroc": 96.32}}
322
+ {"date": "2026-10-27T23:23:08", "tag": "pgc_trained_step1900_forensynths13", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step1900.pth", "benchmark": "forensynths", "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test", "n_generators": 13, "means": {"acc": 74.49, "map": 95.97, "mauroc": 95.35}}
323
+ {"date": "2026-10-28T00:46:03", "tag": "pgc_official_ufd8", "model": "pgc", "ckpt": "registry-default", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 83.57, "map": 99.17, "mauroc": 99.2}}
324
+ {"date": "2026-10-28T00:50:55", "tag": "pgc_trained_step500_ufd8", "model": "pgc", "ckpt": "/ssdstore/ssalehi/SynthScope/models/pgc_v1/model_step500.pth", "benchmark": "ufd", "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test", "n_generators": 8, "means": {"acc": 91.96, "map": 98.99, "mauroc": 98.92}}
325
+ {"date": "2026-10-27T08:37:18", "tag": "omniaid_synthwildx", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/SynthWildX/test", "n_generators": 3, "means": {"acc": 86.02, "map": 95.52, "mauroc": 95.31}}
326
+ {"date": "2026-10-27T08:42:46", "tag": "omniaid_wildrf", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/WildRF/test", "n_generators": 3, "means": {"acc": 88.36, "map": 97.28, "mauroc": 96.91}}
327
+ {"date": "2026-10-27T08:44:32", "tag": "omniaid_bfree_online", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/BFreeOnline/test", "n_generators": 1, "means": {"acc": 70.26, "map": 91.7, "mauroc": 87.18}}
328
+ {"date": "2026-10-27T09:09:34", "tag": "omniaid_realchain", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RealChain/test", "n_generators": 7, "means": {"acc": 61.71, "map": 84.02, "mauroc": 83.63}}
329
+ {"date": "2026-10-27T09:20:42", "tag": "omniaid_genimage_genimage_full", "model": "omniaid_genimage", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenImage_full/test", "n_generators": 8, "means": {"acc": 95.82, "map": 99.02, "mauroc": 99.16}}
330
+ {"date": "2026-10-27T09:22:13", "tag": "omniaid_v1_genimage_full", "model": "omniaid_v1", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenImage_full/test", "n_generators": 8, "means": {"acc": 97.16, "map": 99.2, "mauroc": 99.12}}
331
+ {"date": "2026-10-27T11:05:31", "tag": "omniaid_evalgen", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/EvalGEN/test", "n_generators": 5, "means": {"acc": 74.14, "map": 96.46, "mauroc": 93.08}}
332
+ {"date": "2026-10-27T12:23:42", "tag": "omniaid_rrdataset", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RRDataset/test", "n_generators": 3, "means": {"acc": 71.75, "map": 84.19, "mauroc": 82.32}}
333
+ {"date": "2026-10-27T13:04:46", "tag": "omniaid_genimage_aigcdetect", "model": "omniaid_genimage", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/AIGCDetect/test", "n_generators": 17, "means": {"acc": 88.87, "map": 97.88, "mauroc": 98.2}}
334
+ {"date": "2026-10-27T13:11:33", "tag": "omniaid_v1_aigcdetect", "model": "omniaid_v1", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/AIGCDetect/test", "n_generators": 17, "means": {"acc": 92.88, "map": 98.93, "mauroc": 98.99}}
335
+ {"date": "2026-10-27T15:40:59", "tag": "omniaid_genimage_mirage_test", "model": "omniaid_genimage", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/MirageTest/test", "n_generators": 5, "means": {"acc": 54.66, "map": 50.6, "mauroc": 50.74}}
336
+ {"date": "2026-10-27T15:49:26", "tag": "omniaid_v1_mirage_test", "model": "omniaid_v1", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/MirageTest/test", "n_generators": 5, "means": {"acc": 88.63, "map": 97.04, "mauroc": 97.97}}
337
+ {"date": "2026-10-27T16:20:10", "tag": "omniaid_mirage_test", "model": "omniaid", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/MirageTest/test", "n_generators": 5, "means": {"acc": 80.37, "map": 96.48, "mauroc": 96.65}}
repro/RESULTS_SUMMARY.md CHANGED
@@ -11,17 +11,18 @@
11
  | Method | Headline benchmark | Paper | SynthScope_official | SynthScope_train | Verdict |
12
  |---|---|---:|---:|---:|---|
13
  | FatFormer | ForenSynths-8 | 99.7 | 99.70 | — | ✅ official match |
14
- | NPR | ForenSynths-8 / GANGen | 96.1 / 96.6 | 94.22 / 96.49 | — | ✅ official match |
15
  | CLIP/UnivFD | ForenSynths | — | 93.14 | 92.99 | ✅ match |
16
  | CNNDetection | ForenSynths | 90.8 | 91.15 | 91.05 | ✅ match |
17
  | SAFE | GenImage | 99.3 | 99.32 | 99.51 | ✅ retrain reproduces |
18
  | AIDE | GenImage (SD-v1.4) | 86.9 acc | 97.12 | 96.42 | ✅ reproduced (native-res retrain) |
19
  | SPAI | GenImage | ~91 AUC | 94.22 | 88.80 | ✅ served native-res (beats vendored 91.33) |
20
- | PGC | GenImage | 100 AP | 100.0 | 97.62 | ✅ near |
21
  | VIB-Net | GenImage (SD-v1.4) | 95.5 AP | *none released* | 83.26 | ⚠️ recipe/arch gap |
22
  | Co-Spy | Co-Spy-Bench-110 | 96.0 AP | 86.65 | 96.60 | ✅ beats official |
23
  | DDA | AIGCDetect / ForenSynths / EvalGEN / SynthWildX / WildRF | 87.8 / 81.4 / 97.2 / 90.9 / 90.3 acc | 87.84 / 81.43 / 96.86 / 90.80 / 90.20 acc | — | ✅ official match (GenImage 2.3 pt below) |
24
- | D3QE | ARForensics / ForenSynths-7 / GenImage-6 | 82.11 / 83.73 / 78.61 acc | 81.74 / 83.45 / 79.44 acc | — | ✅ official match |
 
25
  | D3 (video) | GenVideo-Val | 98.5 AP | 97.93 (official code) | 99.14 | ✅ reproduced (D3 protocol; preprocessing matched to the official code) |
26
  | ReStraV (video) | GenVidBench | 93.0 | — | 92.70 | ✅ match |
27
  | STALL (video) | GenVideo-Val | ~80 AUC | — | 83.5 AUROC | ✅ match |
@@ -46,12 +47,12 @@ SPAI now SERVES official weights remapped into the SynthScope class at NATIVE re
46
  | Benchmark | Paper | SynthScope_official | SynthScope_train |
47
  |---|---:|---:|---:|
48
  | GenImage-8 | 100 AP | 100.0 (99.5) | **97.62 (78.6)** |
49
- | UniversalFakeDetect-8 | — | 99.34 (94.2) | 99.01 |
50
- | ForenSynths-13 | — | 96.25 (83.7) | 94.57 (83.4) — see hard-3 caveat |
51
  | AIGIBench-25 | — | 95.36 (88.3) | 86.88 |
52
  | CommGen15-15 | — | 99.26 (97.4) | **98.61 (66.7)** |
53
 
54
- *PGC ForenSynths caveat:* the 13-gen SynthScope_train mean (94.57) is close to official (96.25), but only because the 10 easy generators (~99) dominate. On the three hard generators (deepfake/san/seeingdark) SynthScope_train is **75.87 mAUROC vs 94.35** official. Running the authors' `train.py` verbatim reproduces the same plateau → the released checkpoint is not reproducible from the published recipe+data. See `repro/investigate/pgc_gap_investigation.md`.
55
 
56
  ### Co-Spy
57
  | Benchmark | Paper | SynthScope_official | SynthScope_train |
@@ -148,7 +149,22 @@ Fidelity: SynthScope's class == the official `D3QE` (871/871 state-dict tensors
148
  | GenImage, the paper's 6 subsets (`--dataset genimage_full --select ...`) | **79.44** | 90.94 | **78.61 / 89.60** | mean reproduced; per subset up to 6.8 pt off in both directions (ADM 63.7 vs 70.4, Wukong 90.7 vs 84.4), likely a different test sample |
149
  | In the wild: SynthWildX / WildRF / BFree-Online / RealChain / EvalGEN / RRDataset | 49.6 / 53.0 / 47.0 / 49.5 / 62.2 / 55.0 | – | — | new measurements: it calls nearly every recent fake real (AR-only training) |
150
 
151
- **Verdict: faithful port (identical to the official model); all 3 paper tables reproduced on the mean** (ARForensics and ForenSynths per generator too). Note: in the released code the codebook-frequency bias is zero at inference (`freq_log_counter` is not saved), so the reported scores come from the quantization-error and CLIP branches.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  ### Video detectors (no official weights released)
154
  | Method · Benchmark | Paper | SynthScope_train |
 
11
  | Method | Headline benchmark | Paper | SynthScope_official | SynthScope_train | Verdict |
12
  |---|---|---:|---:|---:|---|
13
  | FatFormer | ForenSynths-8 | 99.7 | 99.70 | — | ✅ official match |
14
+ | NPR | ForenSynths-8 / GANGen | 96.1 / 96.6 | 94.22 / 96.49 | 93.83 / — | ✅ official match; trained (official recipe): AP close, acc 84.1 — the official train.py rerun here gets 81.5 |
15
  | CLIP/UnivFD | ForenSynths | — | 93.14 | 92.99 | ✅ match |
16
  | CNNDetection | ForenSynths | 90.8 | 91.15 | 91.05 | ✅ match |
17
  | SAFE | GenImage | 99.3 | 99.32 | 99.51 | ✅ retrain reproduces |
18
  | AIDE | GenImage (SD-v1.4) | 86.9 acc | 97.12 | 96.42 | ✅ reproduced (native-res retrain) |
19
  | SPAI | GenImage | ~91 AUC | 94.22 | 88.80 | ✅ served native-res (beats vendored 91.33) |
20
+ | PGC | GenImage / ForenSynths-8 / UFD | 100 / — / 98.0 AP | 100.0 / 96.41 / 99.17 | 97.62 / **97.41** / **98.99** | ✅ UFD reproduced by the official recipe in SynthScope (step 500); ForenSynths (not in the paper) above the official checkpoint |
21
  | VIB-Net | GenImage (SD-v1.4) | 95.5 AP | *none released* | 83.26 | ⚠️ recipe/arch gap |
22
  | Co-Spy | Co-Spy-Bench-110 | 96.0 AP | 86.65 | 96.60 | ✅ beats official |
23
  | DDA | AIGCDetect / ForenSynths / EvalGEN / SynthWildX / WildRF | 87.8 / 81.4 / 97.2 / 90.9 / 90.3 acc | 87.84 / 81.43 / 96.86 / 90.80 / 90.20 acc | — | ✅ official match (GenImage 2.3 pt below) |
24
+ | D3QE | ARForensics / ForenSynths-7 / GenImage-6 | 82.11 / 83.73 / 78.61 acc | 81.74 / 83.45 / 79.44 acc | 79.83 / 82.14 / 77.01 acc | ✅ official match; trained in SynthScope: AP matches, acc 1.6-2.3 below |
25
+ | OmniAID | GenImage / AIGCDetect / Mirage-Test (GenImage release) | 95.94 / 88.87 / 51.10 acc | 95.82 / 88.87 / 51.10 acc | — | ✅ official match (v1 release too: 97.16 / 92.88 / 88.39 vs 97.24 / 92.88 / 88.39) |
26
  | D3 (video) | GenVideo-Val | 98.5 AP | 97.93 (official code) | 99.14 | ✅ reproduced (D3 protocol; preprocessing matched to the official code) |
27
  | ReStraV (video) | GenVidBench | 93.0 | — | 92.70 | ✅ match |
28
  | STALL (video) | GenVideo-Val | ~80 AUC | — | 83.5 AUROC | ✅ match |
 
47
  | Benchmark | Paper | SynthScope_official | SynthScope_train |
48
  |---|---:|---:|---:|
49
  | GenImage-8 | 100 AP | 100.0 (99.5) | **97.62 (78.6)** |
50
+ | UniversalFakeDetect-8 | 98.0 AP | 99.34 (94.2) | **98.99 (92.0)** |
51
+ | ForenSynths-13 | — | 96.25 (83.7) | **95.78** (FS-8 97.41 / 85.9) |
52
  | AIGIBench-25 | — | 95.36 (88.3) | 86.88 |
53
  | CommGen15-15 | — | 99.26 (97.4) | **98.61 (66.7)** |
54
 
55
+ *PGC training:* the released ProGAN checkpoint is only ~400 steps in (the authors, on our issue), and DINOv2's prior overfits ProGAN within the first epoch, so the earlier epoch-level retrains (GenImage, AIGIBench, CommGen15 rows above, and the old 94.57 / hard-3 75.87 ForenSynths entry) overshot the transfer peak. The official recipe run in SynthScope with a 2000-micro-batch budget (`experiments/image/pgc.sh`, checkpoint every 100) matches or exceeds the official checkpoint at every step from 300 on: step 500 (the saved step nearest ~400) gives FS-8 97.41 mAP and hard-3 AUROC 93.93 (official 96.41 / 90.87 on the same evaluation) and UFD 98.99 (paper 98.0); transfer peaks around steps 500-900 and erodes after. GenImage, AIGIBench and CommGen15 have not been rerun with this recipe. See `repro/investigate/pgc_gap_investigation.md` and `models/image/pgc/README.md`.
56
 
57
  ### Co-Spy
58
  | Benchmark | Paper | SynthScope_official | SynthScope_train |
 
149
  | GenImage, the paper's 6 subsets (`--dataset genimage_full --select ...`) | **79.44** | 90.94 | **78.61 / 89.60** | mean reproduced; per subset up to 6.8 pt off in both directions (ADM 63.7 vs 70.4, Wukong 90.7 vs 84.4), likely a different test sample |
150
  | In the wild: SynthWildX / WildRF / BFree-Online / RealChain / EvalGEN / RRDataset | 49.6 / 53.0 / 47.0 / 49.5 / 62.2 / 55.0 | – | — | new measurements: it calls nearly every recent fake real (AR-only training) |
151
 
152
+ **Verdict: faithful port (identical to the official model); all 3 paper tables reproduced on the mean** (ARForensics and ForenSynths per generator too). **Trained in SynthScope with the official recipe** (`experiments/image/d3qe.sh`, job 265064): 79.83 / 91.73, 82.14 / 91.35, 77.01 / 90.11 (acc / AP) — AP within 0.9 pt of the paper everywhere, accuracy 1.6-2.3 pt lower (a slightly more conservative threshold, not a weaker ranking). Note: in the released code the codebook-frequency bias is zero at inference (`freq_log_counter` is not saved), so the reported scores come from the quantization-error and CLIP branches.
153
+
154
+ ### OmniAID (official checkpoints)
155
+ Fidelity: SynthScope's class == the official `OmniAID` for all three releases (state dicts identical, logits identical on 24 GenImage images, 0 verdict flips; job 265851). Accuracy at 0.5, mean over subsets; Mirage-Test mean = 4 balanced categories + Anime fake accuracy (the paper's Table 3).
156
+
157
+ | Benchmark | release | acc | mAP | Paper | Note |
158
+ |---|---|---:|---:|---|---|
159
+ | GenImage, 8 subsets (`--dataset genimage_full`) | `omniaid_genimage` | **95.82** | 99.02 | **95.94** | **reproduced**, every subset within 1.5 pt; confirms the v1-era 512 -> 336 preprocessing |
160
+ | AIGCDetect, 17 (`--dataset aigcdetect`) | `omniaid_genimage` | **88.87** | 97.88 | **88.87** | **reproduced** exactly |
161
+ | Mirage-Test, 5 categories (`--dataset mirage_test`) | `omniaid_genimage` | **51.10** | — | **51.10** | **reproduced** exactly, every category |
162
+ | GenImage, 8 subsets | `omniaid_v1` | **97.16** | 99.20 | **97.24** | **reproduced**, every subset within 0.6 pt |
163
+ | AIGCDetect, 17 | `omniaid_v1` | **92.88** | 98.93 | **92.88** | **reproduced** exactly |
164
+ | Mirage-Test, 5 categories | `omniaid_v1` | **88.39** | — | **88.39** | **reproduced** exactly |
165
+ | Mirage-Test / SynthWildX / WildRF / BFree-Online / RealChain / EvalGEN / RRDataset | `omniaid` (v2, served) | 78.46 / 86.02 / 88.36 / 70.26 / 61.71 / 74.14 / 71.75 | — | — | new measurements |
166
+
167
+ **Verdict: faithful port; every paper number SynthScope can run is reproduced** (Chameleon and DRCT-2M are not available).
168
 
169
  ### Video detectors (no official weights released)
170
  | Method · Benchmark | Paper | SynthScope_train |
repro/bench_results/d3qe_trained_arforensics.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "d3qe_trained_arforensics",
3
+ "model": "d3qe",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/model_best.pth",
5
+ "benchmark": "pergen",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/ARForensics/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 7,
9
+ "means": {
10
+ "acc": 79.83,
11
+ "map": 91.73,
12
+ "mauroc": 91.49
13
+ },
14
+ "per_generator": {
15
+ "Infinity": {
16
+ "ap": 78.61,
17
+ "auroc": 79.4,
18
+ "real_acc": 97.38,
19
+ "fake_acc": 20.68,
20
+ "acc": 59.03
21
+ },
22
+ "Janus_Pro": {
23
+ "ap": 97.31,
24
+ "auroc": 97.06,
25
+ "real_acc": 97.6,
26
+ "fake_acc": 82.52,
27
+ "acc": 90.06
28
+ },
29
+ "LlamaGen": {
30
+ "ap": 99.62,
31
+ "auroc": 99.66,
32
+ "real_acc": 97.8,
33
+ "fake_acc": 97.18,
34
+ "acc": 97.49
35
+ },
36
+ "Open_MAGVIT2": {
37
+ "ap": 86.39,
38
+ "auroc": 85.63,
39
+ "real_acc": 97.42,
40
+ "fake_acc": 39.08,
41
+ "acc": 68.25
42
+ },
43
+ "RAR": {
44
+ "ap": 97.71,
45
+ "auroc": 97.67,
46
+ "real_acc": 97.47,
47
+ "fake_acc": 83.62,
48
+ "acc": 90.54
49
+ },
50
+ "Switti": {
51
+ "ap": 87.59,
52
+ "auroc": 86.6,
53
+ "real_acc": 97.4,
54
+ "fake_acc": 44.62,
55
+ "acc": 71.01
56
+ },
57
+ "VAR": {
58
+ "ap": 94.89,
59
+ "auroc": 94.42,
60
+ "real_acc": 97.43,
61
+ "fake_acc": 67.42,
62
+ "acc": 82.43
63
+ }
64
+ },
65
+ "date": "2026-10-27T06:02:46",
66
+ "synthscope_version": "0.1.0"
67
+ }
repro/bench_results/d3qe_trained_cnndetection.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "d3qe_trained_cnndetection",
3
+ "model": "d3qe",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/model_best.pth",
5
+ "benchmark": "forensynths",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 7,
9
+ "means": {
10
+ "acc": 82.14,
11
+ "map": 91.35,
12
+ "mauroc": 90.29
13
+ },
14
+ "per_generator": {
15
+ "biggan": {
16
+ "ap": 94.39,
17
+ "auroc": 93.54,
18
+ "real_acc": 97.6,
19
+ "fake_acc": 69.65,
20
+ "acc": 83.63
21
+ },
22
+ "cyclegan": {
23
+ "ap": 93.08,
24
+ "auroc": 92.91,
25
+ "real_acc": 95.46,
26
+ "fake_acc": 66.92,
27
+ "acc": 81.19
28
+ },
29
+ "gaugan": {
30
+ "ap": 97.36,
31
+ "auroc": 97.86,
32
+ "real_acc": 97.02,
33
+ "fake_acc": 87.44,
34
+ "acc": 92.23
35
+ },
36
+ "progan": {
37
+ "ap": 97.37,
38
+ "auroc": 97.68,
39
+ "real_acc": 94.85,
40
+ "fake_acc": 92.98,
41
+ "acc": 93.91
42
+ },
43
+ "stargan": {
44
+ "ap": 83.68,
45
+ "auroc": 82.03,
46
+ "real_acc": 96.25,
47
+ "fake_acc": 50.18,
48
+ "acc": 73.21
49
+ },
50
+ "stylegan": {
51
+ "ap": 85.88,
52
+ "auroc": 82.53,
53
+ "real_acc": 95.19,
54
+ "fake_acc": 55.68,
55
+ "acc": 75.44
56
+ },
57
+ "stylegan2": {
58
+ "ap": 87.68,
59
+ "auroc": 85.46,
60
+ "real_acc": 95.82,
61
+ "fake_acc": 54.88,
62
+ "acc": 75.35
63
+ }
64
+ },
65
+ "date": "2026-10-27T06:28:33",
66
+ "synthscope_version": "0.1.0"
67
+ }
repro/bench_results/d3qe_trained_ep3_arforensics.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "d3qe_trained_ep3_arforensics",
3
+ "model": "d3qe",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/snapshot_ep3_best.pth",
5
+ "benchmark": "pergen",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/ARForensics/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 7,
9
+ "means": {
10
+ "acc": 81.11,
11
+ "map": 90.77,
12
+ "mauroc": 90.97
13
+ },
14
+ "per_generator": {
15
+ "Infinity": {
16
+ "ap": 79.06,
17
+ "auroc": 80.41,
18
+ "real_acc": 94.67,
19
+ "fake_acc": 33.8,
20
+ "acc": 64.23
21
+ },
22
+ "Janus_Pro": {
23
+ "ap": 96.19,
24
+ "auroc": 96.36,
25
+ "real_acc": 95.4,
26
+ "fake_acc": 85.65,
27
+ "acc": 90.53
28
+ },
29
+ "LlamaGen": {
30
+ "ap": 98.59,
31
+ "auroc": 98.89,
32
+ "real_acc": 95.38,
33
+ "fake_acc": 95.78,
34
+ "acc": 95.58
35
+ },
36
+ "Open_MAGVIT2": {
37
+ "ap": 85.27,
38
+ "auroc": 84.87,
39
+ "real_acc": 95.13,
40
+ "fake_acc": 45.12,
41
+ "acc": 70.13
42
+ },
43
+ "RAR": {
44
+ "ap": 96.75,
45
+ "auroc": 97.04,
46
+ "real_acc": 94.87,
47
+ "fake_acc": 86.83,
48
+ "acc": 90.85
49
+ },
50
+ "Switti": {
51
+ "ap": 86.43,
52
+ "auroc": 86.26,
53
+ "real_acc": 94.68,
54
+ "fake_acc": 53.87,
55
+ "acc": 74.27
56
+ },
57
+ "VAR": {
58
+ "ap": 93.07,
59
+ "auroc": 92.93,
60
+ "real_acc": 95.3,
61
+ "fake_acc": 69.12,
62
+ "acc": 82.21
63
+ }
64
+ },
65
+ "date": "2026-10-26T21:42:06",
66
+ "synthscope_version": "0.1.0"
67
+ }
repro/bench_results/d3qe_trained_genimage_full.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "d3qe_trained_genimage_full",
3
+ "model": "d3qe",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/d3qe_v1/model_best.pth",
5
+ "benchmark": "pergen",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/GenImage_full/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 6,
9
+ "means": {
10
+ "acc": 77.01,
11
+ "map": 90.11,
12
+ "mauroc": 89.63
13
+ },
14
+ "per_generator": {
15
+ "adm": {
16
+ "ap": 77.51,
17
+ "auroc": 73.53,
18
+ "real_acc": 97.8,
19
+ "fake_acc": 27.53,
20
+ "acc": 62.67
21
+ },
22
+ "glide": {
23
+ "ap": 97.62,
24
+ "auroc": 98.08,
25
+ "real_acc": 97.67,
26
+ "fake_acc": 81.13,
27
+ "acc": 89.4
28
+ },
29
+ "midjourney": {
30
+ "ap": 80.15,
31
+ "auroc": 81.72,
32
+ "real_acc": 97.2,
33
+ "fake_acc": 25.93,
34
+ "acc": 61.57
35
+ },
36
+ "sdv1.4": {
37
+ "ap": 93.68,
38
+ "auroc": 92.81,
39
+ "real_acc": 97.72,
40
+ "fake_acc": 61.12,
41
+ "acc": 79.42
42
+ },
43
+ "sdv1.5": {
44
+ "ap": 94.38,
45
+ "auroc": 94.5,
46
+ "real_acc": 96.2,
47
+ "fake_acc": 67.13,
48
+ "acc": 81.67
49
+ },
50
+ "wukong": {
51
+ "ap": 97.3,
52
+ "auroc": 97.16,
53
+ "real_acc": 97.73,
54
+ "fake_acc": 76.93,
55
+ "acc": 87.33
56
+ }
57
+ },
58
+ "date": "2026-10-27T06:42:56",
59
+ "synthscope_version": "0.1.0"
60
+ }
repro/bench_results/npr_official3090_forensynths8.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_official3090_forensynths8",
3
+ "model": "npr",
4
+ "ckpt": "/ivi/zfs/s0/original_homes/ssalehi/SynthScope_shared/npr_ref/model_epoch_last_3090.pth",
5
+ "benchmark": "forensynths",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 91.83,
11
+ "map": 92.81,
12
+ "mauroc": 95.18
13
+ },
14
+ "per_generator": {
15
+ "biggan": {
16
+ "ap": 85.59,
17
+ "auroc": 90.86,
18
+ "real_acc": 69.75,
19
+ "fake_acc": 98.15,
20
+ "acc": 83.95
21
+ },
22
+ "cyclegan": {
23
+ "ap": 98.12,
24
+ "auroc": 98.86,
25
+ "real_acc": 96.9,
26
+ "fake_acc": 93.49,
27
+ "acc": 95.19
28
+ },
29
+ "deepfake": {
30
+ "ap": 76.06,
31
+ "auroc": 83.04,
32
+ "real_acc": 61.17,
33
+ "fake_acc": 93.25,
34
+ "acc": 77.21
35
+ },
36
+ "gaugan": {
37
+ "ap": 82.97,
38
+ "auroc": 88.99,
39
+ "real_acc": 62.72,
40
+ "fake_acc": 99.16,
41
+ "acc": 80.94
42
+ },
43
+ "progan": {
44
+ "ap": 99.98,
45
+ "auroc": 99.97,
46
+ "real_acc": 99.9,
47
+ "fake_acc": 99.9,
48
+ "acc": 99.9
49
+ },
50
+ "stargan": {
51
+ "ap": 100.0,
52
+ "auroc": 100.0,
53
+ "real_acc": 99.8,
54
+ "fake_acc": 99.75,
55
+ "acc": 99.77
56
+ },
57
+ "stylegan": {
58
+ "ap": 99.78,
59
+ "auroc": 99.77,
60
+ "real_acc": 99.93,
61
+ "fake_acc": 96.19,
62
+ "acc": 98.06
63
+ },
64
+ "stylegan2": {
65
+ "ap": 99.95,
66
+ "auroc": 99.97,
67
+ "real_acc": 99.95,
68
+ "fake_acc": 99.26,
69
+ "acc": 99.61
70
+ }
71
+ },
72
+ "date": "2026-10-27T03:25:16",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_official3090_ufd.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_official3090_ufd",
3
+ "model": "npr",
4
+ "ckpt": "/ivi/zfs/s0/original_homes/ssalehi/SynthScope_shared/npr_ref/model_epoch_last_3090.pth",
5
+ "benchmark": "ufd",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 93.94,
11
+ "map": 96.69,
12
+ "mauroc": 97.26
13
+ },
14
+ "per_generator": {
15
+ "dalle": {
16
+ "ap": 98.08,
17
+ "auroc": 98.08,
18
+ "real_acc": 98.0,
19
+ "fake_acc": 83.8,
20
+ "acc": 90.9
21
+ },
22
+ "glide_100_10": {
23
+ "ap": 99.52,
24
+ "auroc": 99.54,
25
+ "real_acc": 98.0,
26
+ "fake_acc": 97.6,
27
+ "acc": 97.8
28
+ },
29
+ "glide_100_27": {
30
+ "ap": 99.53,
31
+ "auroc": 99.61,
32
+ "real_acc": 98.0,
33
+ "fake_acc": 96.7,
34
+ "acc": 97.35
35
+ },
36
+ "glide_50_27": {
37
+ "ap": 99.55,
38
+ "auroc": 99.61,
39
+ "real_acc": 98.0,
40
+ "fake_acc": 96.9,
41
+ "acc": 97.45
42
+ },
43
+ "guided": {
44
+ "ap": 78.12,
45
+ "auroc": 82.46,
46
+ "real_acc": 62.9,
47
+ "fake_acc": 85.0,
48
+ "acc": 73.95
49
+ },
50
+ "ldm_100": {
51
+ "ap": 99.57,
52
+ "auroc": 99.61,
53
+ "real_acc": 98.0,
54
+ "fake_acc": 97.9,
55
+ "acc": 97.95
56
+ },
57
+ "ldm_200": {
58
+ "ap": 99.63,
59
+ "auroc": 99.69,
60
+ "real_acc": 98.0,
61
+ "fake_acc": 98.3,
62
+ "acc": 98.15
63
+ },
64
+ "ldm_200_cfg": {
65
+ "ap": 99.49,
66
+ "auroc": 99.48,
67
+ "real_acc": 98.0,
68
+ "fake_acc": 97.9,
69
+ "acc": 97.95
70
+ }
71
+ },
72
+ "date": "2026-10-27T03:26:05",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_official_nprpth_forensynths8.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_official_nprpth_forensynths8",
3
+ "model": "npr",
4
+ "ckpt": "registry-default",
5
+ "benchmark": "forensynths",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 92.3,
11
+ "map": 94.22,
12
+ "mauroc": 96.08
13
+ },
14
+ "per_generator": {
15
+ "biggan": {
16
+ "ap": 87.8,
17
+ "auroc": 92.36,
18
+ "real_acc": 70.0,
19
+ "fake_acc": 98.7,
20
+ "acc": 84.35
21
+ },
22
+ "cyclegan": {
23
+ "ap": 98.45,
24
+ "auroc": 99.04,
25
+ "real_acc": 96.06,
26
+ "fake_acc": 96.14,
27
+ "acc": 96.1
28
+ },
29
+ "deepfake": {
30
+ "ap": 82.4,
31
+ "auroc": 86.85,
32
+ "real_acc": 70.52,
33
+ "fake_acc": 89.96,
34
+ "acc": 80.24
35
+ },
36
+ "gaugan": {
37
+ "ap": 85.49,
38
+ "auroc": 90.75,
39
+ "real_acc": 65.54,
40
+ "fake_acc": 99.46,
41
+ "acc": 82.5
42
+ },
43
+ "progan": {
44
+ "ap": 99.99,
45
+ "auroc": 99.99,
46
+ "real_acc": 99.83,
47
+ "fake_acc": 99.75,
48
+ "acc": 99.79
49
+ },
50
+ "stargan": {
51
+ "ap": 99.94,
52
+ "auroc": 99.96,
53
+ "real_acc": 98.75,
54
+ "fake_acc": 99.95,
55
+ "acc": 99.35
56
+ },
57
+ "stylegan": {
58
+ "ap": 99.78,
59
+ "auroc": 99.75,
60
+ "real_acc": 99.65,
61
+ "fake_acc": 95.76,
62
+ "acc": 97.7
63
+ },
64
+ "stylegan2": {
65
+ "ap": 99.94,
66
+ "auroc": 99.94,
67
+ "real_acc": 99.74,
68
+ "fake_acc": 97.02,
69
+ "acc": 98.38
70
+ }
71
+ },
72
+ "date": "2026-10-27T02:49:02",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_official_nprpth_ufd.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_official_nprpth_ufd",
3
+ "model": "npr",
4
+ "ckpt": "registry-default",
5
+ "benchmark": "ufd",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 94.57,
11
+ "map": 97.16,
12
+ "mauroc": 97.68
13
+ },
14
+ "per_generator": {
15
+ "dalle": {
16
+ "ap": 98.96,
17
+ "auroc": 98.96,
18
+ "real_acc": 98.5,
19
+ "fake_acc": 89.2,
20
+ "acc": 93.85
21
+ },
22
+ "glide_100_10": {
23
+ "ap": 99.63,
24
+ "auroc": 99.64,
25
+ "real_acc": 98.5,
26
+ "fake_acc": 96.7,
27
+ "acc": 97.6
28
+ },
29
+ "glide_100_27": {
30
+ "ap": 99.55,
31
+ "auroc": 99.56,
32
+ "real_acc": 98.5,
33
+ "fake_acc": 95.7,
34
+ "acc": 97.1
35
+ },
36
+ "glide_50_27": {
37
+ "ap": 99.61,
38
+ "auroc": 99.62,
39
+ "real_acc": 98.5,
40
+ "fake_acc": 96.6,
41
+ "acc": 97.55
42
+ },
43
+ "guided": {
44
+ "ap": 80.28,
45
+ "auroc": 84.23,
46
+ "real_acc": 65.8,
47
+ "fake_acc": 84.4,
48
+ "acc": 75.1
49
+ },
50
+ "ldm_100": {
51
+ "ap": 99.8,
52
+ "auroc": 99.87,
53
+ "real_acc": 98.5,
54
+ "fake_acc": 98.8,
55
+ "acc": 98.65
56
+ },
57
+ "ldm_200": {
58
+ "ap": 99.76,
59
+ "auroc": 99.82,
60
+ "real_acc": 98.5,
61
+ "fake_acc": 98.4,
62
+ "acc": 98.45
63
+ },
64
+ "ldm_200_cfg": {
65
+ "ap": 99.73,
66
+ "auroc": 99.76,
67
+ "real_acc": 98.5,
68
+ "fake_acc": 98.1,
69
+ "acc": 98.3
70
+ }
71
+ },
72
+ "date": "2026-10-27T02:49:53",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_officialcode_rerun_forensynths8.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_officialcode_rerun_forensynths8",
3
+ "model": "npr",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_official_rerun/4class-resnet-car-cat-chair-horse2026_10_27_03_27_09/model_epoch_last.pth",
5
+ "benchmark": "forensynths",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 81.47,
11
+ "map": 92.72,
12
+ "mauroc": 93.97
13
+ },
14
+ "per_generator": {
15
+ "biggan": {
16
+ "ap": 90.16,
17
+ "auroc": 91.79,
18
+ "real_acc": 91.95,
19
+ "fake_acc": 65.2,
20
+ "acc": 78.58
21
+ },
22
+ "cyclegan": {
23
+ "ap": 97.81,
24
+ "auroc": 98.64,
25
+ "real_acc": 99.24,
26
+ "fake_acc": 45.19,
27
+ "acc": 72.22
28
+ },
29
+ "deepfake": {
30
+ "ap": 73.75,
31
+ "auroc": 75.52,
32
+ "real_acc": 95.57,
33
+ "fake_acc": 20.53,
34
+ "acc": 58.05
35
+ },
36
+ "gaugan": {
37
+ "ap": 80.85,
38
+ "auroc": 86.74,
39
+ "real_acc": 90.64,
40
+ "fake_acc": 42.6,
41
+ "acc": 66.62
42
+ },
43
+ "progan": {
44
+ "ap": 100.0,
45
+ "auroc": 100.0,
46
+ "real_acc": 100.0,
47
+ "fake_acc": 99.87,
48
+ "acc": 99.94
49
+ },
50
+ "stargan": {
51
+ "ap": 99.99,
52
+ "auroc": 99.99,
53
+ "real_acc": 100.0,
54
+ "fake_acc": 81.54,
55
+ "acc": 90.77
56
+ },
57
+ "stylegan": {
58
+ "ap": 99.34,
59
+ "auroc": 99.23,
60
+ "real_acc": 99.98,
61
+ "fake_acc": 83.07,
62
+ "acc": 91.53
63
+ },
64
+ "stylegan2": {
65
+ "ap": 99.89,
66
+ "auroc": 99.87,
67
+ "real_acc": 99.99,
68
+ "fake_acc": 88.17,
69
+ "acc": 94.08
70
+ }
71
+ },
72
+ "date": "2026-10-27T13:08:53",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_officialcode_rerun_ufd.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_officialcode_rerun_ufd",
3
+ "model": "npr",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_official_rerun/4class-resnet-car-cat-chair-horse2026_10_27_03_27_09/model_epoch_last.pth",
5
+ "benchmark": "ufd",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 87.61,
11
+ "map": 96.56,
12
+ "mauroc": 96.43
13
+ },
14
+ "per_generator": {
15
+ "dalle": {
16
+ "ap": 92.55,
17
+ "auroc": 92.27,
18
+ "real_acc": 99.1,
19
+ "fake_acc": 38.4,
20
+ "acc": 68.75
21
+ },
22
+ "glide_100_10": {
23
+ "ap": 99.17,
24
+ "auroc": 99.15,
25
+ "real_acc": 99.1,
26
+ "fake_acc": 89.6,
27
+ "acc": 94.35
28
+ },
29
+ "glide_100_27": {
30
+ "ap": 98.95,
31
+ "auroc": 98.95,
32
+ "real_acc": 99.1,
33
+ "fake_acc": 84.9,
34
+ "acc": 92.0
35
+ },
36
+ "glide_50_27": {
37
+ "ap": 99.0,
38
+ "auroc": 98.94,
39
+ "real_acc": 99.1,
40
+ "fake_acc": 87.0,
41
+ "acc": 93.05
42
+ },
43
+ "guided": {
44
+ "ap": 85.77,
45
+ "auroc": 85.06,
46
+ "real_acc": 89.5,
47
+ "fake_acc": 56.5,
48
+ "acc": 73.0
49
+ },
50
+ "ldm_100": {
51
+ "ap": 99.03,
52
+ "auroc": 99.1,
53
+ "real_acc": 99.1,
54
+ "fake_acc": 87.8,
55
+ "acc": 93.45
56
+ },
57
+ "ldm_200": {
58
+ "ap": 99.08,
59
+ "auroc": 99.12,
60
+ "real_acc": 99.1,
61
+ "fake_acc": 87.5,
62
+ "acc": 93.3
63
+ },
64
+ "ldm_200_cfg": {
65
+ "ap": 98.95,
66
+ "auroc": 98.85,
67
+ "real_acc": 99.1,
68
+ "fake_acc": 86.9,
69
+ "acc": 93.0
70
+ }
71
+ },
72
+ "date": "2026-10-27T13:09:44",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_trained_best_forensynths8.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_trained_best_forensynths8",
3
+ "model": "npr",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_best.pth",
5
+ "benchmark": "forensynths",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 84.3,
11
+ "map": 91.81,
12
+ "mauroc": 93.3
13
+ },
14
+ "per_generator": {
15
+ "biggan": {
16
+ "ap": 89.71,
17
+ "auroc": 90.53,
18
+ "real_acc": 90.3,
19
+ "fake_acc": 68.7,
20
+ "acc": 79.5
21
+ },
22
+ "cyclegan": {
23
+ "ap": 97.55,
24
+ "auroc": 98.21,
25
+ "real_acc": 98.49,
26
+ "fake_acc": 66.62,
27
+ "acc": 82.55
28
+ },
29
+ "deepfake": {
30
+ "ap": 69.88,
31
+ "auroc": 74.96,
32
+ "real_acc": 70.37,
33
+ "fake_acc": 66.53,
34
+ "acc": 68.45
35
+ },
36
+ "gaugan": {
37
+ "ap": 78.39,
38
+ "auroc": 83.97,
39
+ "real_acc": 89.12,
40
+ "fake_acc": 44.8,
41
+ "acc": 66.96
42
+ },
43
+ "progan": {
44
+ "ap": 100.0,
45
+ "auroc": 100.0,
46
+ "real_acc": 100.0,
47
+ "fake_acc": 99.65,
48
+ "acc": 99.83
49
+ },
50
+ "stargan": {
51
+ "ap": 99.99,
52
+ "auroc": 99.99,
53
+ "real_acc": 100.0,
54
+ "fake_acc": 75.49,
55
+ "acc": 87.74
56
+ },
57
+ "stylegan": {
58
+ "ap": 99.04,
59
+ "auroc": 98.85,
60
+ "real_acc": 99.93,
61
+ "fake_acc": 84.51,
62
+ "acc": 92.22
63
+ },
64
+ "stylegan2": {
65
+ "ap": 99.9,
66
+ "auroc": 99.88,
67
+ "real_acc": 99.94,
68
+ "fake_acc": 94.29,
69
+ "acc": 97.11
70
+ }
71
+ },
72
+ "date": "2026-10-27T02:58:43",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_trained_best_ufd.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_trained_best_ufd",
3
+ "model": "npr",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_best.pth",
5
+ "benchmark": "ufd",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 85.34,
11
+ "map": 95.56,
12
+ "mauroc": 95.3
13
+ },
14
+ "per_generator": {
15
+ "dalle": {
16
+ "ap": 92.85,
17
+ "auroc": 92.7,
18
+ "real_acc": 98.5,
19
+ "fake_acc": 44.5,
20
+ "acc": 71.5
21
+ },
22
+ "glide_100_10": {
23
+ "ap": 98.54,
24
+ "auroc": 98.22,
25
+ "real_acc": 98.5,
26
+ "fake_acc": 90.7,
27
+ "acc": 94.6
28
+ },
29
+ "glide_100_27": {
30
+ "ap": 97.85,
31
+ "auroc": 97.35,
32
+ "real_acc": 98.5,
33
+ "fake_acc": 86.0,
34
+ "acc": 92.25
35
+ },
36
+ "glide_50_27": {
37
+ "ap": 98.08,
38
+ "auroc": 97.6,
39
+ "real_acc": 98.5,
40
+ "fake_acc": 88.6,
41
+ "acc": 93.55
42
+ },
43
+ "guided": {
44
+ "ap": 84.94,
45
+ "auroc": 83.39,
46
+ "real_acc": 89.7,
47
+ "fake_acc": 58.6,
48
+ "acc": 74.15
49
+ },
50
+ "ldm_100": {
51
+ "ap": 97.13,
52
+ "auroc": 97.49,
53
+ "real_acc": 98.5,
54
+ "fake_acc": 69.3,
55
+ "acc": 83.9
56
+ },
57
+ "ldm_200": {
58
+ "ap": 97.01,
59
+ "auroc": 97.37,
60
+ "real_acc": 98.5,
61
+ "fake_acc": 67.6,
62
+ "acc": 83.05
63
+ },
64
+ "ldm_200_cfg": {
65
+ "ap": 98.09,
66
+ "auroc": 98.25,
67
+ "real_acc": 98.5,
68
+ "fake_acc": 80.9,
69
+ "acc": 89.7
70
+ }
71
+ },
72
+ "date": "2026-10-27T02:59:33",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_trained_last_forensynths8.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_trained_last_forensynths8",
3
+ "model": "npr",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_last.pth",
5
+ "benchmark": "forensynths",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/CNNDetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 84.09,
11
+ "map": 93.83,
12
+ "mauroc": 94.94
13
+ },
14
+ "per_generator": {
15
+ "biggan": {
16
+ "ap": 91.45,
17
+ "auroc": 92.23,
18
+ "real_acc": 91.6,
19
+ "fake_acc": 68.9,
20
+ "acc": 80.25
21
+ },
22
+ "cyclegan": {
23
+ "ap": 99.07,
24
+ "auroc": 99.42,
25
+ "real_acc": 99.55,
26
+ "fake_acc": 47.84,
27
+ "acc": 73.69
28
+ },
29
+ "deepfake": {
30
+ "ap": 74.32,
31
+ "auroc": 78.49,
32
+ "real_acc": 90.47,
33
+ "fake_acc": 35.36,
34
+ "acc": 62.91
35
+ },
36
+ "gaugan": {
37
+ "ap": 86.04,
38
+ "auroc": 89.64,
39
+ "real_acc": 89.12,
40
+ "fake_acc": 59.62,
41
+ "acc": 74.37
42
+ },
43
+ "progan": {
44
+ "ap": 100.0,
45
+ "auroc": 100.0,
46
+ "real_acc": 100.0,
47
+ "fake_acc": 99.62,
48
+ "acc": 99.81
49
+ },
50
+ "stargan": {
51
+ "ap": 99.99,
52
+ "auroc": 99.99,
53
+ "real_acc": 100.0,
54
+ "fake_acc": 80.49,
55
+ "acc": 90.25
56
+ },
57
+ "stylegan": {
58
+ "ap": 99.83,
59
+ "auroc": 99.8,
60
+ "real_acc": 100.0,
61
+ "fake_acc": 90.1,
62
+ "acc": 95.05
63
+ },
64
+ "stylegan2": {
65
+ "ap": 99.93,
66
+ "auroc": 99.92,
67
+ "real_acc": 100.0,
68
+ "fake_acc": 92.79,
69
+ "acc": 96.39
70
+ }
71
+ },
72
+ "date": "2026-10-27T02:53:54",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/npr_trained_last_ufd.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "npr_trained_last_ufd",
3
+ "model": "npr",
4
+ "ckpt": "/ssdstore/ssalehi/SynthScope/models/npr_v1/model_last.pth",
5
+ "benchmark": "ufd",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/universialfakedetection/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 8,
9
+ "means": {
10
+ "acc": 88.91,
11
+ "map": 97.49,
12
+ "mauroc": 97.23
13
+ },
14
+ "per_generator": {
15
+ "dalle": {
16
+ "ap": 95.29,
17
+ "auroc": 94.52,
18
+ "real_acc": 99.7,
19
+ "fake_acc": 38.1,
20
+ "acc": 68.9
21
+ },
22
+ "glide_100_10": {
23
+ "ap": 99.73,
24
+ "auroc": 99.73,
25
+ "real_acc": 99.7,
26
+ "fake_acc": 94.1,
27
+ "acc": 96.9
28
+ },
29
+ "glide_100_27": {
30
+ "ap": 99.55,
31
+ "auroc": 99.5,
32
+ "real_acc": 99.7,
33
+ "fake_acc": 89.7,
34
+ "acc": 94.7
35
+ },
36
+ "glide_50_27": {
37
+ "ap": 99.64,
38
+ "auroc": 99.6,
39
+ "real_acc": 99.7,
40
+ "fake_acc": 92.3,
41
+ "acc": 96.0
42
+ },
43
+ "guided": {
44
+ "ap": 86.62,
45
+ "auroc": 85.23,
46
+ "real_acc": 90.0,
47
+ "fake_acc": 59.9,
48
+ "acc": 74.95
49
+ },
50
+ "ldm_100": {
51
+ "ap": 99.71,
52
+ "auroc": 99.79,
53
+ "real_acc": 99.7,
54
+ "fake_acc": 87.0,
55
+ "acc": 93.35
56
+ },
57
+ "ldm_200": {
58
+ "ap": 99.7,
59
+ "auroc": 99.78,
60
+ "real_acc": 99.7,
61
+ "fake_acc": 85.3,
62
+ "acc": 92.5
63
+ },
64
+ "ldm_200_cfg": {
65
+ "ap": 99.7,
66
+ "auroc": 99.71,
67
+ "real_acc": 99.7,
68
+ "fake_acc": 88.3,
69
+ "acc": 94.0
70
+ }
71
+ },
72
+ "date": "2026-10-27T02:54:44",
73
+ "synthscope_version": "0.1.0"
74
+ }
repro/bench_results/omniaid_bfree_online.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag": "omniaid_bfree_online",
3
+ "model": "omniaid",
4
+ "ckpt": "registry-default",
5
+ "benchmark": "pergen",
6
+ "data_root": "/ssdstore/ssalehi/SynthScope/BFreeOnline/test",
7
+ "jpeg_fakes": null,
8
+ "n_generators": 1,
9
+ "means": {
10
+ "acc": 70.26,
11
+ "map": 91.7,
12
+ "mauroc": 87.18
13
+ },
14
+ "per_generator": {
15
+ "viral": {
16
+ "ap": 91.7,
17
+ "auroc": 87.18,
18
+ "real_acc": 94.8,
19
+ "fake_acc": 45.72,
20
+ "acc": 70.26
21
+ }
22
+ },
23
+ "date": "2026-10-27T08:44:32",
24
+ "synthscope_version": "0.1.0"
25
+ }