File size: 16,699 Bytes
cd24b4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463a482
cd24b4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463a482
cd24b4a
463a482
cd24b4a
 
 
463a482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd24b4a
 
 
 
 
 
b91ce50
 
 
 
 
 
 
 
 
 
cd24b4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dca285c
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
"""Dataset explorer backend: discover on-disk datasets, sample random batches, serve
thumbnails, and report which dataset each detector was trained on.

The dataset *configs* (conf/dataset/*.yaml) point at paths that are partly stale on the GPU
node (the /nvmestore roots are not mounted on cn023, and the video sets live in ad-hoc repro
dirs). So this module keeps an explicit registry of candidate roots and auto-detects which
ones actually exist, rather than trusting the configs. Labels are inferred from path
components (``0_real``/``real``/``nature`` -> real, ``1_fake``/``fake``/``ai`` -> fake), which
covers every on-disk layout (ForenSynths ``<gen>/{0_real,1_fake}``, GenImage ``{ai,nature}``,
GenVideo ``Real`` + ``Fake/<gen>``, ReStraV ``genvideo/{0_real,1_fake}``).
"""
from __future__ import annotations

import os
import _synthscope_env  # noqa: F401  (maps legacy AGID_* variables to SYNTHSCOPE_*)
import random
from dataclasses import dataclass, field
from pathlib import Path

IMAGE_EXTS = {".jpg", ".jpeg", ".jfif", ".png", ".webp", ".bmp", ".tif", ".tiff"}
VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v"}

# Directory-name tokens that identify a sample's ground-truth label.
REAL_TOKENS = {"0_real", "real", "reals", "nature", "natural", "coco", "imagenet"}
FAKE_TOKENS = {"1_fake", "fake", "fakes", "ai", "generated", "gen", "synthesis"}

# How many paths to keep in the in-memory pool per label (reservoir-sampled over the full tree)
# and the hard cap on tree entries scanned, so sampling stays fast on 100k+ -image datasets.
POOL_PER_LABEL = 4000
SCAN_CAP = 400_000


@dataclass
class DatasetDef:
    key: str
    name: str
    modality: str                 # "image" | "video"
    roots: list[str]              # candidate roots; first existing one wins
    splits: list[str] = field(default_factory=lambda: ["train", "val", "test"])
    note: str = ""


# Explicit registry (auto-detected against disk at request time).
DATASET_DEFS: list[DatasetDef] = [
    DatasetDef("cnndetection", "CNNDetection / ForenSynths (ProGAN)", "image",
               ["/hddstore/ssalehi/SynthScope/CNNDetection"], ["train", "val", "test"],
               "ProGAN-family GAN images; the ForenSynths benchmark."),
    DatasetDef("genimage", "GenImage (SD-v1.4)", "image",
               ["/hddstore/ssalehi/SynthScope/GenImage"], ["train", "test"],
               "ImageNet-class diffusion (SD-v1.4) vs real ImageNet."),
    DatasetDef("cospy", "CoSpyBench", "image",
               ["/hddstore/ssalehi/SynthScope/CoSpyBench"], ["test"],
               "Co-Spy cross-generator evaluation benchmark."),
    DatasetDef("cospy_subset", "Co-Spy ProGAN subset", "image",
               ["/hddstore/ssalehi/SynthScope/spai_train/cospy_subset"], ["train", "val", "test"]),
    DatasetDef("spai_ldm", "SPAI LDM trainset (vs COCO)", "image",
               ["/hddstore/ssalehi/SynthScope/spai_train/staged"], ["train", "val", "test"],
               "Latent-diffusion fakes vs COCO reals (SPAI from-scratch)."),
    DatasetDef("genvideo_val", "GenVideo-Val", "video",
               ["/hddstore/ssalehi/SynthScope/d3_repro/genvideo_val/GenVideo-Val"], ["."],
               "GenVideo validation pool (msrvtt reals + 10 generators)."),
    DatasetDef("restrav_data", "ReStraV data (VidProM β†’ GenVideo split)", "video",
               ["/hddstore/ssalehi/SynthScope/restrav_data"], ["train", "val", "test"],
               "Train = VidProM/DeMamba (VSC reals); test = GenVideo (msrvtt reals)."),
    DatasetDef("ditfake", "DiTFake (DiT/FLUX/SD3 images)", "image",
               ["/hddstore/ssalehi/SynthScope/DiTFake"], ["test"],
               "DiT/FLUX/SD3-generated images vs COCO reals."),
    DatasetDef("aigibench", "AIGIBench (multi-generator)", "image",
               ["/hddstore/ssalehi/SynthScope/AIGIBench"], ["test"],
               "25-generator AI-image benchmark (GAN/diffusion/face-swap) vs real."),
    DatasetDef("evalcrafter", "EvalCrafter (T2V, generated)", "video",
               ["/hddstore/ssalehi/SynthScope/EvalCrafter"], ["test"],
               "14 text-to-video generators β€” all AI-generated (no reals)."),
    DatasetDef("genvidbench", "GenVidBench", "video",
               ["/hddstore/ssalehi/SynthScope/GenVidBench"], ["test"],
               "Real (Vript, HD-VG-130M) vs 8 video generators."),
    DatasetDef("freqnetdetection", "FreqNet test set", "image",
               ["/hddstore/ssalehi/SynthScope/freqnet"], ["test"],
               "FreqNet evaluation set (ForenSynths-style real/fake)."),
    DatasetDef("universialfakedetection", "UniversalFakeDetect (Ojha)", "image",
               ["/hddstore/ssalehi/SynthScope/universialfakedetection"], ["test"],
               "Ojha et al. CVPR'23 cross-generator test set."),
    # Cross-generator benchmarks added for the full reproduction suite.
    DatasetDef("genimage_full", "GenImage (8 generators)", "image",
               ["/hddstore/ssalehi/SynthScope/GenImage_full"], ["test"],
               "Full 8-generator GenImage test (Midjourney, SD-v1.4/1.5, ADM, GLIDE, Wukong, VQDM, BigGAN)."),
    DatasetDef("synthbuster", "Synthbuster (9 diffusion generators)", "image",
               ["/hddstore/ssalehi/SynthScope/Synthbuster"], ["test"],
               "Synthbuster (Zenodo 10066460) β€” DALL-E 2/3, Firefly, GLIDE, Midjourney-v5, SD 1.3/1.4/2/XL; "
               "used by SPAI. Proxy reals (GenImage nature) since Synthbuster ships fakes only."),
    DatasetDef("diffusionforensics", "DiffusionForensics / DIRE", "image",
               ["/hddstore/ssalehi/SynthScope/DiffusionForensics"], ["test"],
               "8 diffusion subsets (ADM, DDPM, IDDPM, LDM, PNDM, SDv1, SDv2, VQ-Diffusion); NPR Table 3."),
    DatasetDef("diffusion1kstep", "Diffusion-1kStep", "image",
               ["/hddstore/ssalehi/SynthScope/Diffusion1kStep"], ["test"],
               "DALLE, DDPM, guided-diffusion, improved-diffusion, midjourney; NPR Table 5."),
    DatasetDef("aigcdetect", "AIGCDetectBenchmark (17 gen)", "image",
               ["/hddstore/ssalehi/SynthScope/AIGCDetectBenchmark"], ["test"],
               "17-generator GAN+diffusion benchmark; Co-Spy / AIDE / CuRe cross-generator table."),
    DatasetDef("commgen15", "CommGen15 (15 commercial gen)", "image",
               ["/hddstore/ssalehi/SynthScope/CommGen15"], ["test"],
               "15 commercial generators (Midjourney, Sora, Flux, Kling, ...); PGC Table 3."),
    DatasetDef("realchain", "RealChain (7 recent generators)", "image",
               ["/hddstore/ssalehi/SynthScope/RealChain"], ["test"],
               "Flux1, SDv3.5, QwenImage, Hunyuan3, NanoBanana, Seedream4, i2i vs 7000 reals, all JPEG q60; "
               "CuRe Table 1. CC BY-NC 4.0 (non-commercial)."),
    DatasetDef("evalgen", "EvalGEN (5 diffusion + AR gen)", "image",
               ["/hddstore/ssalehi/SynthScope/EvalGEN"], ["test"],
               "Flux, GoT, Infinity, OmniGen, NOVA on GenEval prompts (DDA); paired with MSCOCO val2017 reals."),
    DatasetDef("synthwildx", "SynthWildX (images from X)", "image",
               ["/hddstore/ssalehi/SynthScope/SynthWildX"], ["test"],
               "DALL-E 3, Firefly, Midjourney v5 vs real, all posted on X; fetched from X, so deleted posts are missing."),
    DatasetDef("wildrf", "WildRF (Facebook, Reddit, Twitter)", "image",
               ["/hddstore/ssalehi/SynthScope/WildRF"], ["test"],
               "In-the-wild real and fake images from three social platforms (LaDeDa paper); one folder per platform."),
    DatasetDef("bfree_online", "BFree-Online (viral images)", "image",
               ["/hddstore/ssalehi/SynthScope/BFreeOnline"], ["test"],
               "Real and fake images that went viral, several web copies each (B-Free); fetched from the URL list, "
               "dead links skipped. Nonprofit use only."),
    DatasetDef("rrdataset", "RRDataset (original / transfer / redigital)", "image",
               ["/hddstore/ssalehi/SynthScope/RRDataset"], ["test"],
               "Real vs AI images as created, after sharing online, and after re-digitization (ICCV 2025)."),
    DatasetDef("videofeedback", "VideoFeedback (TIGER-Lab)", "video",
               ["/hddstore/ssalehi/SynthScope/VideoFeedback"], ["test"],
               "11 video generators vs DiDeMo+Panda70M real; STALL benchmark."),
    DatasetDef("comgenvid", "ComGenVid (Sora/VEO3)", "video",
               ["/hddstore/ssalehi/SynthScope/ComGenVid"], ["test"],
               "Sora + VEO3 fakes vs MSVD real; STALL's new benchmark."),
    DatasetDef("genvideo", "GenVideo-Val (10 generators)", "video",
               ["/hddstore/ssalehi/SynthScope/GenVideo"], ["test"],
               "DeMamba's GenVideo-Val per generator vs 10,000 MSR-VTT reals; D3 Table 1, STALL."),
    DatasetDef("videophy", "VideoPhy (text-to-video)", "video",
               ["/hddstore/ssalehi/SynthScope/VideoPhy"], ["test"],
               "VideoPhy test clips from up to 10 generators vs MSR-VTT reals; D3 Table 3."),
    DatasetDef("vidprom", "VidProM (4 generators, subsample)", "video",
               ["/hddstore/ssalehi/SynthScope/VidProM"], ["test"],
               "First 1,000 clips per generator (ModelScope, Pika, Text2Video-Zero, VideoCrafter2) vs MSR-VTT reals; "
               "CC BY-NC 4.0."),
]

# Which dataset each detector was trained on (best-effort, from interface/scripts/train_model.sh
# and experiments/*). `official` flags checkpoints whose deployed weights are the authors' (the
# SynthScope from-scratch dataset is noted, but the served weights are the official ProGAN-trained ones).
MODEL_TRAIN_DATASET: dict[str, dict] = {
    "clip": {"dataset": "cnndetection"},
    "cnn": {"dataset": "cnndetection"},
    "dinov2": {"dataset": "cnndetection"},
    "dinov3": {"dataset": "cnndetection"},
    "adapted_dino": {"dataset": "cnndetection"},
    "safe": {"dataset": "cnndetection"},
    "pgc": {"dataset": "cospy_subset"},
    "co_spy": {"dataset": "cospy_subset"},
    "vib_net": {"dataset": "genimage"},
    "aide": {"dataset": "genimage", "official": True},
    "npr": {"dataset": "genimage", "official": True},
    "spai": {"dataset": "spai_ldm"},
    "fat_former": {"dataset": "cnndetection", "official": True},
    "restrav": {"dataset": "restrav_data"},
    "d3": {"dataset": None, "training_free": True},
    "stall": {"dataset": None, "training_free": True},
}


# Node-local scratch differs per node (cn023 -> /hddstore, cees -> /ssdstore, cn030/31 -> /nvmestore).
# Try each configured root, then the same tail remapped onto the other scratch mounts, so the explorer
# works wherever the data was staged. Override the prefix set with SYNTHSCOPE_DATA_PREFIXES (colon-separated).
_SCRATCH_PREFIXES = tuple(p for p in (
    os.environ.get("SYNTHSCOPE_DATA_PREFIXES") or "/hddstore:/ssdstore:/nvmestore").split(":") if p)


def _root_candidates(r: str) -> list[str]:
    cands = [r]
    # SYNTHSCOPE_DATA_DIR: one folder holding datasets by their usual folder name (e.g. <dir>/CommGen15), as on a Space
    if os.environ.get("SYNTHSCOPE_DATA_DIR"):
        cands.insert(0, str(Path(os.environ["SYNTHSCOPE_DATA_DIR"]).expanduser() / Path(r).name))
    for src in _SCRATCH_PREFIXES:
        if r.startswith(src + "/"):
            tail = r[len(src):]
            cands += [pre + tail for pre in _SCRATCH_PREFIXES if pre != src]
            break
    return cands


def _resolve_root(d: DatasetDef) -> Path | None:
    for r in d.roots:
        for c in _root_candidates(r):
            p = Path(c)
            if p.is_dir():
                return p
    return None


def _exts_for(modality: str) -> set[str]:
    return VIDEO_EXTS if modality == "video" else IMAGE_EXTS


def _split_dir(root: Path, split: str) -> Path:
    return root if split in (".", "", None) else root / split


def label_of(rel: Path) -> str | None:
    """Infer 'real'/'fake' from a relative path's directory tokens (None if unknown)."""
    parts = [p.lower() for p in rel.parts]
    for p in parts:
        if p in FAKE_TOKENS:
            return "fake"
    for p in parts:
        if p in REAL_TOKENS:
            return "real"
    return None


def available_splits(d: DatasetDef, root: Path) -> list[str]:
    out = []
    for s in d.splits:
        if _split_dir(root, s).is_dir():
            out.append(s)
    return out


def list_datasets() -> list[dict]:
    """All registry datasets whose data is present on disk, with resolved path + splits."""
    out = []
    for d in DATASET_DEFS:
        root = _resolve_root(d)
        if root is None:
            continue
        out.append({
            "key": d.key,
            "name": d.name,
            "modality": d.modality,
            "note": d.note,
            "path": str(root),
            "splits": available_splits(d, root),
        })
    return out


def get_def(key: str) -> DatasetDef:
    for d in DATASET_DEFS:
        if d.key == key:
            return d
    raise KeyError(f"Unknown dataset '{key}'")


def _iter_files(base: Path, exts: set[str]):
    scanned = 0
    for dirpath, _dirs, files in os.walk(base):
        for fn in files:
            if Path(fn).suffix.lower() in exts:
                yield Path(dirpath) / fn
                scanned += 1
                if scanned >= SCAN_CAP:
                    return


def _pools(base: Path, exts: set[str]) -> dict[str, list[Path]]:
    """Reservoir-sample up to POOL_PER_LABEL paths per label across the whole tree."""
    pools: dict[str, list[Path]] = {"real": [], "fake": []}
    seen: dict[str, int] = {"real": 0, "fake": 0}
    rng = random.Random(1234)
    for fp in _iter_files(base, exts):
        lab = label_of(fp.relative_to(base))
        if lab is None:
            continue
        seen[lab] += 1
        pool = pools[lab]
        if len(pool) < POOL_PER_LABEL:
            pool.append(fp)
        else:                                   # reservoir replacement
            j = rng.randint(0, seen[lab] - 1)
            if j < POOL_PER_LABEL:
                pool[j] = fp
    return pools


def sample_batch(key: str, split: str, n: int, seed: int) -> list[dict]:
    """Return ``n`` random items (balanced real/fake when possible) from ``key``/``split``."""
    d = get_def(key)
    root = _resolve_root(d)
    if root is None:
        raise FileNotFoundError(f"Dataset '{key}' not present on disk")
    base = _split_dir(root, split)
    if not base.is_dir():
        raise FileNotFoundError(f"Split '{split}' not found for '{key}'")
    pools = _pools(base, _exts_for(d.modality))
    rng = random.Random(seed)
    half = max(1, n // 2)
    items: list[Path] = []
    for lab in ("real", "fake"):
        p = pools[lab]
        rng.shuffle(p)
        items += [(fp, lab) for fp in p[:half]]
    # top up if one class is short
    if len(items) < n:
        extra = [(fp, lab) for lab in ("real", "fake") for fp in pools[lab][half:]]
        rng.shuffle(extra)
        items += extra[: n - len(items)]
    rng.shuffle(items)
    items = items[:n]
    return [
        {"rel": str(fp.relative_to(root)), "label": lab,
         "name": fp.name, "modality": d.modality}
        for fp, lab in items
    ]


def resolve_media(key: str, rel: str) -> Path:
    """Resolve ``rel`` under the dataset root, guarding against ``..`` traversal.

    Some datasets (e.g. ``restrav_data``) populate their split dirs with *symlinks* into other
    SynthScope data folders, so we must NOT ``.resolve()`` and then reject by the symlink's target
    (that would 403 every restrav_data thumbnail). Instead we forbid absolute paths and ``..``
    components in the user-supplied ``rel`` β€” which keeps the addressed path lexically under the
    dataset root β€” then follow the symlink (``is_file``) to serve the actual file.
    """
    d = get_def(key)
    root = _resolve_root(d)
    if root is None:
        raise FileNotFoundError(f"Dataset '{key}' not present on disk")
    rel_path = Path(rel)
    if rel_path.is_absolute() or ".." in rel_path.parts:
        raise PermissionError("Path escapes dataset root")
    target = root / rel_path
    if not target.is_file():            # follows the symlink to the real video/image
        raise FileNotFoundError(str(target))
    return target


# Hydra dataset configs that are training sets, not benchmarks (excluded from the benchmark count shown on the page).
_TRAINING_ONLY = {"cospy_subset", "spai_ldm"}


def supported_counts() -> dict:
    """How many benchmark datasets SynthScope can download and lay out (one Hydra config each)."""
    conf = Path(__file__).resolve().parents[2] / "conf" / "dataset"
    return {m: sum(1 for p in (conf / m).glob("*.yaml") if p.stem not in _TRAINING_ONLY) for m in ("image", "video")}