Spaces:
Sleeping
Sleeping
Update SynthScope Space from the GitHub repository
Browse files- LICENSES.md +2 -0
- benchmark/bench_eval_video.py +31 -3
- conf/model/video/split.yaml +12 -0
- dataset.py +40 -2
- docs/CAPABILITIES.md +1 -0
- interface/backend/build_repro_full.py +5 -2
- interface/backend/engine.py +26 -11
- interface/backend/registry.py +11 -0
- interface/backend/repro_full.json +35 -3
- interface/scripts/calibrate_split.py +65 -0
- models/video/d3/README.md +5 -2
- models/video/d3/d3.py +2 -0
- models/video/split/README.md +95 -0
- models/video/split/__init__.py +0 -0
- models/video/split/split.py +92 -0
- models/video/split/split_calibration.json +13 -0
- repro/MASTER_RESULTS.jsonl +6 -0
- repro/RESULTS_SUMMARY.md +2 -2
- repro/bench_results/d3_genvideo.json +79 -19
- repro/bench_results/d3_genvideo_fpr.json +158 -0
- repro/bench_results/split_genvideo_fpr.json +158 -0
- repro/bench_results/split_official_genvideo.json +156 -0
- repro/benchmark_specs/d3.json +8 -8
- repro/benchmark_specs/split.json +233 -0
- repro/official_evals/split_official_eval.py +139 -0
- tests/test_split_smoke.py +92 -0
- tests/test_video_gif_decode.py +41 -0
- trainer.py +3 -0
LICENSES.md
CHANGED
|
@@ -40,6 +40,7 @@ third-party components, and every **pretrained checkpoint / backbone** SynthScop
|
|
| 40 |
| `dinov3` | DINOv3 probe | facebookresearch/dinov3 | DINOv3 License (gated) | DINOv3 backbone | **gated** — DINOv3 License |
|
| 41 |
| `adapted_dino` | Adapter-tuned DINOv2 (SynthScope) | this repo | Apache-2.0 | — (SynthScope-trained) | n/a |
|
| 42 |
| `d3` | D3 (ICCV'25) | ⚠ verify (official repo) | ⚠ verify | training-free (no weights) | n/a |
|
|
|
|
| 43 |
| `restrav` | ReStraV | ⚠ verify (official repo) | ⚠ verify | — (SynthScope-trained) | n/a |
|
| 44 |
| `stall` | STALL (CVPR'26) | ⚠ verify (official repo) | ⚠ verify | training-free; needs DINOv3 backbone | gated (DINOv3) |
|
| 45 |
|
|
@@ -55,6 +56,7 @@ third-party components, and every **pretrained checkpoint / backbone** SynthScop
|
|
| 55 |
| SigLIP | `co_spy` | Apache-2.0 (verify) |
|
| 56 |
| MFM ViT-B/16 | `spai` | ⚠ verify |
|
| 57 |
| Stable Diffusion VAE | `co_spy` | CreativeML Open RAIL-M (verify) |
|
|
|
|
| 58 |
| LlamaGen VQ-16 tokenizer (`vq_ds16_c2i.pt`, HF `FoundationVision/LlamaGen`) | `d3qe` | MIT |
|
| 59 |
| Perception Encoder Core L/14-336 (`facebook/PE-Core-L14-336`) | `cure` | Apache-2.0 (Meta); `official_arch.py` vendors the CuRe/Meta backbone code — see its header |
|
| 60 |
|
|
|
|
| 40 |
| `dinov3` | DINOv3 probe | facebookresearch/dinov3 | DINOv3 License (gated) | DINOv3 backbone | **gated** — DINOv3 License |
|
| 41 |
| `adapted_dino` | Adapter-tuned DINOv2 (SynthScope) | this repo | Apache-2.0 | — (SynthScope-trained) | n/a |
|
| 42 |
| `d3` | D3 (ICCV'25) | ⚠ verify (official repo) | ⚠ verify | training-free (no weights) | n/a |
|
| 43 |
+
| `split` | SPLIT (ECCV'26) | mldljyh/SPLIT | no LICENSE file in the repo: **no code copied**; `models/video/split/split.py` is SynthScope's own implementation of the paper's equations | training-free (no weights) | n/a |
|
| 44 |
| `restrav` | ReStraV | ⚠ verify (official repo) | ⚠ verify | — (SynthScope-trained) | n/a |
|
| 45 |
| `stall` | STALL (CVPR'26) | ⚠ verify (official repo) | ⚠ verify | training-free; needs DINOv3 backbone | gated (DINOv3) |
|
| 46 |
|
|
|
|
| 56 |
| SigLIP | `co_spy` | Apache-2.0 (verify) |
|
| 57 |
| MFM ViT-B/16 | `spai` | ⚠ verify |
|
| 58 |
| Stable Diffusion VAE | `co_spy` | CreativeML Open RAIL-M (verify) |
|
| 59 |
+
| X-CLIP B/16 (`microsoft/xclip-base-patch16`) | `d3`, `split` | MIT (HF model card) |
|
| 60 |
| LlamaGen VQ-16 tokenizer (`vq_ds16_c2i.pt`, HF `FoundationVision/LlamaGen`) | `d3qe` | MIT |
|
| 61 |
| Perception Encoder Core L/14-336 (`facebook/PE-Core-L14-336`) | `cure` | Apache-2.0 (Meta); `official_arch.py` vendors the CuRe/Meta backbone code — see its header |
|
| 62 |
|
benchmark/bench_eval_video.py
CHANGED
|
@@ -27,6 +27,7 @@ import json
|
|
| 27 |
import sys
|
| 28 |
from pathlib import Path
|
| 29 |
|
|
|
|
| 30 |
import torch
|
| 31 |
|
| 32 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "interface"))
|
|
@@ -103,20 +104,32 @@ def metrics(fake_probs: torch.Tensor, labels: torch.Tensor):
|
|
| 103 |
"fake_acc": round(fake_acc, 2), "acc": round((real_acc + fake_acc) / 2, 2)}
|
| 104 |
|
| 105 |
|
|
|
|
|
|
|
|
|
|
| 106 |
@torch.no_grad()
|
| 107 |
-
def score_videos(engine, spec, paths):
|
|
|
|
|
|
|
| 108 |
probs = []
|
| 109 |
for p in paths:
|
| 110 |
try:
|
| 111 |
-
probs.append(engine.predict_video_clip(spec, str(p))["fake_probability"])
|
| 112 |
except Exception as e: # noqa: BLE001
|
| 113 |
print(f" bad video {p.name}: {e}")
|
| 114 |
return probs
|
| 115 |
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
def main(argv=None) -> int:
|
| 118 |
ap = argparse.ArgumentParser()
|
| 119 |
-
ap.add_argument("-m", "--model", required=True, choices=["d3", "restrav", "stall"])
|
| 120 |
ap.add_argument("--benchmark", choices=["genvideo", "fakeonly", "pergen"], help="layout (not needed with --dataset)")
|
| 121 |
ap.add_argument("--data", help="benchmark root (not needed with --dataset)")
|
| 122 |
ap.add_argument("--dataset", help="a supported video dataset (genvideo, videophy, vidprom, evalcrafter, ...): downloads "
|
|
@@ -155,6 +168,8 @@ def main(argv=None) -> int:
|
|
| 155 |
f"| device={engine.device} ===", flush=True)
|
| 156 |
|
| 157 |
per_gen, agg = {}, {"ap": [], "ap_real": [], "auroc": [], "acc": []}
|
|
|
|
|
|
|
| 158 |
# score the shared real pool once if it is identical across generators
|
| 159 |
real_cache = {}
|
| 160 |
for name, reals, fakes in gens:
|
|
@@ -169,11 +184,18 @@ def main(argv=None) -> int:
|
|
| 169 |
rp = score_videos(engine, spec, reals)
|
| 170 |
real_cache[key] = rp
|
| 171 |
fp = score_videos(engine, spec, fakes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
probs = torch.tensor(rp + fp)
|
| 173 |
labels = torch.tensor([0] * len(rp) + [1] * len(fp))
|
| 174 |
m = metrics(probs, labels)
|
| 175 |
if m is None:
|
| 176 |
print(f" skip {name}: one class"); continue
|
|
|
|
|
|
|
| 177 |
per_gen[name] = m
|
| 178 |
for k in agg:
|
| 179 |
agg[k].append(m[k])
|
|
@@ -186,6 +208,12 @@ def main(argv=None) -> int:
|
|
| 186 |
return round(sum(xs) / len(xs), 2)
|
| 187 |
means = {"acc": mean(agg["acc"]), "map": mean(agg["ap"]), "map_real": mean(agg["ap_real"]),
|
| 188 |
"mauroc": mean(agg["auroc"])}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
result = {"tag": args.tag, "model": args.model, "ckpt": "registry-default",
|
| 190 |
"benchmark": f"video:{args.benchmark}", "data_root": str(root),
|
| 191 |
"n_generators": len(per_gen), "means": means, "per_generator": per_gen,
|
|
|
|
| 27 |
import sys
|
| 28 |
from pathlib import Path
|
| 29 |
|
| 30 |
+
import numpy as np
|
| 31 |
import torch
|
| 32 |
|
| 33 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "interface"))
|
|
|
|
| 104 |
"fake_acc": round(fake_acc, 2), "acc": round((real_acc + fake_acc) / 2, 2)}
|
| 105 |
|
| 106 |
|
| 107 |
+
FPRS = (0.001, 0.01, 0.05)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
@torch.no_grad()
|
| 111 |
+
def score_videos(engine, spec, paths, raw=False):
|
| 112 |
+
"""P(fake) per video; with ``raw`` (training-free detectors with a ``score``), the raw score oriented so that higher
|
| 113 |
+
means generated instead, which keeps the order at the extremes where probabilities saturate."""
|
| 114 |
probs = []
|
| 115 |
for p in paths:
|
| 116 |
try:
|
| 117 |
+
probs.append(engine.video_raw_score(spec, str(p)) if raw else engine.predict_video_clip(spec, str(p))["fake_probability"])
|
| 118 |
except Exception as e: # noqa: BLE001
|
| 119 |
print(f" bad video {p.name}: {e}")
|
| 120 |
return probs
|
| 121 |
|
| 122 |
|
| 123 |
+
def fake_recall_at_fpr(real_scores, fake_scores):
|
| 124 |
+
"""SPLIT's operating-point metric (official eval.py): the threshold is the (1 - FPR) quantile of the real scores
|
| 125 |
+
(numpy method "higher"); fake recall = share of fakes scoring at or above it. In percent, per FPR."""
|
| 126 |
+
real, fake = np.asarray(real_scores, dtype="float64"), np.asarray(fake_scores, dtype="float64")
|
| 127 |
+
return {f"{a * 100:g}": round(float((fake >= np.quantile(real, 1 - a, method="higher")).mean()) * 100, 2) for a in FPRS}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
def main(argv=None) -> int:
|
| 131 |
ap = argparse.ArgumentParser()
|
| 132 |
+
ap.add_argument("-m", "--model", required=True, choices=["d3", "split", "restrav", "stall"])
|
| 133 |
ap.add_argument("--benchmark", choices=["genvideo", "fakeonly", "pergen"], help="layout (not needed with --dataset)")
|
| 134 |
ap.add_argument("--data", help="benchmark root (not needed with --dataset)")
|
| 135 |
ap.add_argument("--dataset", help="a supported video dataset (genvideo, videophy, vidprom, evalcrafter, ...): downloads "
|
|
|
|
| 168 |
f"| device={engine.device} ===", flush=True)
|
| 169 |
|
| 170 |
per_gen, agg = {}, {"ap": [], "ap_real": [], "auroc": [], "acc": []}
|
| 171 |
+
pooled_fake_raw = [] # every generator's fakes, for the pooled ("Overall") recall
|
| 172 |
+
raw = hasattr(engine._get_model(spec), "score") # training-free detectors: also fake recall at fixed FPR
|
| 173 |
# score the shared real pool once if it is identical across generators
|
| 174 |
real_cache = {}
|
| 175 |
for name, reals, fakes in gens:
|
|
|
|
| 184 |
rp = score_videos(engine, spec, reals)
|
| 185 |
real_cache[key] = rp
|
| 186 |
fp = score_videos(engine, spec, fakes)
|
| 187 |
+
if raw:
|
| 188 |
+
rraw = real_cache.setdefault(("raw",) + key, None) or score_videos(engine, spec, reals, raw=True)
|
| 189 |
+
real_cache[("raw",) + key] = rraw
|
| 190 |
+
fraw = score_videos(engine, spec, fakes, raw=True)
|
| 191 |
+
pooled_fake_raw += fraw
|
| 192 |
probs = torch.tensor(rp + fp)
|
| 193 |
labels = torch.tensor([0] * len(rp) + [1] * len(fp))
|
| 194 |
m = metrics(probs, labels)
|
| 195 |
if m is None:
|
| 196 |
print(f" skip {name}: one class"); continue
|
| 197 |
+
if raw:
|
| 198 |
+
m["fake_recall_at_fpr"] = fake_recall_at_fpr(rraw, fraw)
|
| 199 |
per_gen[name] = m
|
| 200 |
for k in agg:
|
| 201 |
agg[k].append(m[k])
|
|
|
|
| 208 |
return round(sum(xs) / len(xs), 2)
|
| 209 |
means = {"acc": mean(agg["acc"]), "map": mean(agg["ap"]), "map_real": mean(agg["ap_real"]),
|
| 210 |
"mauroc": mean(agg["auroc"])}
|
| 211 |
+
if raw:
|
| 212 |
+
means["fake_recall_at_fpr"] = {k: mean([g["fake_recall_at_fpr"][k] for g in per_gen.values()])
|
| 213 |
+
for k in next(iter(per_gen.values()))["fake_recall_at_fpr"]}
|
| 214 |
+
# Overall: all fakes against the real pool(s) (each distinct pool once), as the SPLIT paper's "Overall" row
|
| 215 |
+
pooled_real_raw = [s for k, v in real_cache.items() if k[0] == "raw" for s in v]
|
| 216 |
+
means["overall_fake_recall_at_fpr"] = fake_recall_at_fpr(pooled_real_raw, pooled_fake_raw)
|
| 217 |
result = {"tag": args.tag, "model": args.model, "ckpt": "registry-default",
|
| 218 |
"benchmark": f"video:{args.benchmark}", "data_root": str(root),
|
| 219 |
"n_generators": len(per_gen), "means": means, "per_generator": per_gen,
|
conf/model/video/split.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- /model/default
|
| 3 |
+
- _self_
|
| 4 |
+
|
| 5 |
+
# SPLIT (mldljyh/SPLIT, ECCV 2026): training-free; frozen X-CLIP patch tokens -> two-step temporal roughness ** gamma
|
| 6 |
+
# x spatial patch-level motion incoherence. Higher score = more likely generated.
|
| 7 |
+
model_type: split
|
| 8 |
+
encoder: xclip-16
|
| 9 |
+
gamma: 8.0
|
| 10 |
+
# Decision threshold on the raw score for argmax accuracy (the engine uses models/video/split/split_calibration.json).
|
| 11 |
+
threshold: 0.0
|
| 12 |
+
num_frames: 16
|
dataset.py
CHANGED
|
@@ -25,7 +25,7 @@ from decord import VideoReader, cpu
|
|
| 25 |
from torch.utils.data import DataLoader
|
| 26 |
from transformations import AIGITransformations
|
| 27 |
from torchvision.io import decode_image, ImageReadMode
|
| 28 |
-
from PIL import Image, ImageFile
|
| 29 |
from config_utils import compose_config
|
| 30 |
from models.image.dda.official_train_data import MEAN as DDA_MEAN, STD as DDA_STD, build_item as dda_build_item, \
|
| 31 |
create_train_transforms as dda_train_transforms
|
|
@@ -71,6 +71,40 @@ def _has_videos(directory: Path) -> bool:
|
|
| 71 |
return directory.is_dir() and any(_iter_videos(directory))
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
def sample_video_frames(
|
| 75 |
path: str | Path,
|
| 76 |
num_frames: int,
|
|
@@ -108,7 +142,11 @@ def sample_video_frames(
|
|
| 108 |
reader = VideoReader(str(path), ctx=cpu(0))
|
| 109 |
num_source = len(reader)
|
| 110 |
except Exception:
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
if num_source == 0:
|
| 113 |
return torch.zeros(num_frames, 3, size, size)
|
| 114 |
|
|
|
|
| 25 |
from torch.utils.data import DataLoader
|
| 26 |
from transformations import AIGITransformations
|
| 27 |
from torchvision.io import decode_image, ImageReadMode
|
| 28 |
+
from PIL import Image, ImageFile, ImageSequence
|
| 29 |
from config_utils import compose_config
|
| 30 |
from models.image.dda.official_train_data import MEAN as DDA_MEAN, STD as DDA_STD, build_item as dda_build_item, \
|
| 31 |
create_train_transforms as dda_train_transforms
|
|
|
|
| 71 |
return directory.is_dir() and any(_iter_videos(directory))
|
| 72 |
|
| 73 |
|
| 74 |
+
class _PILFrames:
|
| 75 |
+
"""The part of decord's ``VideoReader`` API that ``sample_video_frames`` uses, for animated images decord cannot open
|
| 76 |
+
(GIFs: decord fails with "broken metadata" on them). Every frame is composited to RGB at the canvas size, and the
|
| 77 |
+
frame rate is the frame count over the summed frame durations (100 ms per frame when a GIF stores none)."""
|
| 78 |
+
|
| 79 |
+
class _Array:
|
| 80 |
+
def __init__(self, a: np.ndarray):
|
| 81 |
+
self._a = a
|
| 82 |
+
|
| 83 |
+
def asnumpy(self) -> np.ndarray:
|
| 84 |
+
return self._a
|
| 85 |
+
|
| 86 |
+
def __init__(self, path):
|
| 87 |
+
with Image.open(path) as im:
|
| 88 |
+
frames, ms = [], 0.0
|
| 89 |
+
for f in ImageSequence.Iterator(im):
|
| 90 |
+
frames.append(np.asarray(f.convert("RGB")))
|
| 91 |
+
ms += float(f.info.get("duration") or 100)
|
| 92 |
+
self._frames = np.stack(frames)
|
| 93 |
+
self._fps = len(frames) / (ms / 1000.0)
|
| 94 |
+
|
| 95 |
+
def __len__(self) -> int:
|
| 96 |
+
return len(self._frames)
|
| 97 |
+
|
| 98 |
+
def get_avg_fps(self) -> float:
|
| 99 |
+
return self._fps
|
| 100 |
+
|
| 101 |
+
def get_batch(self, indices):
|
| 102 |
+
return self._Array(self._frames[indices])
|
| 103 |
+
|
| 104 |
+
def __getitem__(self, i):
|
| 105 |
+
return self._Array(self._frames[i])
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def sample_video_frames(
|
| 109 |
path: str | Path,
|
| 110 |
num_frames: int,
|
|
|
|
| 142 |
reader = VideoReader(str(path), ctx=cpu(0))
|
| 143 |
num_source = len(reader)
|
| 144 |
except Exception:
|
| 145 |
+
try: # animated GIFs: decord cannot read them, PIL can
|
| 146 |
+
reader = _PILFrames(path)
|
| 147 |
+
num_source = len(reader)
|
| 148 |
+
except Exception:
|
| 149 |
+
return torch.zeros(num_frames, 3, size, size)
|
| 150 |
if num_source == 0:
|
| 151 |
return torch.zeros(num_frames, 3, size, size)
|
| 152 |
|
docs/CAPABILITIES.md
CHANGED
|
@@ -36,6 +36,7 @@ matching `conf/model/<modality>/<key>.yaml`, a `trainer.py._build_model` branch,
|
|
| 36 |
| `effort` (+ `effort_chameleon`) | image | ce | official | – | CLIP ViT-L/14 + SVD-residual attention (orthogonal subspace decomposition) |
|
| 37 |
| `vib_net` | image | vib | synthscope | – | Variational information bottleneck on frozen CLIP ViT-L/14 (trained in SynthScope; reproduces the paper) |
|
| 38 |
| `d3` | video | video | training-free | – | Disentangled detail dynamics (X-CLIP) |
|
|
|
|
| 39 |
| `restrav` | video | video | synthscope | – | Representation-trajectory geometry (DINOv2) |
|
| 40 |
| `stall` | video | video | training-free | – | Spatio-temporal whitened log-likelihoods (DINOv3) |
|
| 41 |
|
|
|
|
| 36 |
| `effort` (+ `effort_chameleon`) | image | ce | official | – | CLIP ViT-L/14 + SVD-residual attention (orthogonal subspace decomposition) |
|
| 37 |
| `vib_net` | image | vib | synthscope | – | Variational information bottleneck on frozen CLIP ViT-L/14 (trained in SynthScope; reproduces the paper) |
|
| 38 |
| `d3` | video | video | training-free | – | Disentangled detail dynamics (X-CLIP) |
|
| 39 |
+
| `split` | video | video | training-free | – | Temporal roughness x spatial patch-level motion incoherence (X-CLIP) |
|
| 40 |
| `restrav` | video | video | synthscope | – | Representation-trajectory geometry (DINOv2) |
|
| 41 |
| `stall` | video | video | training-free | – | Spatio-temporal whitened log-likelihoods (DINOv3) |
|
| 42 |
|
interface/backend/build_repro_full.py
CHANGED
|
@@ -118,11 +118,14 @@ VAL = {
|
|
| 118 |
# robust metrics (the mAP harness is degenerate: 10000 reals : as few as 56 fakes/generator).
|
| 119 |
# D3 (training-free): "synthscope" = SynthScope's D3, "official" = the official D3 code end to end on the same data
|
| 120 |
# (repro/official_evals/d3_official_eval.py). mAP = AP with real as the positive class, as the paper reports.
|
| 121 |
-
("d3", "GenVideo"): {"synthscope": {"map":
|
| 122 |
("d3", "EvalCrafter"): {"synthscope": {"map": 99.3, "auroc": 98.62, "acc": 96.59}, "official": {"map": 98.7, "auroc": 97.81}},
|
| 123 |
("d3", "VideoPhy"): {"synthscope": {"map": 99.45, "auroc": 98.09, "acc": 95.19}, "official": {"map": 98.84, "auroc": 96.28}},
|
| 124 |
("d3", "VidProM"): {"synthscope": {"map": 92.15, "auroc": 90.86, "acc": 84.86}, "official": {"map": 84.91, "auroc": 82.83}},
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
| 126 |
("stall", "GenVideo"): {"synthscope": {"map": 82.38, "auroc": 83.51, "acc": 56.88}},
|
| 127 |
("stall", "VideoFeedback"): {"synthscope": {"map": 83.07, "auroc": 85.1, "acc": 62.12}},
|
| 128 |
("stall", "ComGenVid"): {"synthscope": {"map": 82.59, "auroc": 84.0, "acc": 57.34}},
|
|
|
|
| 118 |
# robust metrics (the mAP harness is degenerate: 10000 reals : as few as 56 fakes/generator).
|
| 119 |
# D3 (training-free): "synthscope" = SynthScope's D3, "official" = the official D3 code end to end on the same data
|
| 120 |
# (repro/official_evals/d3_official_eval.py). mAP = AP with real as the positive class, as the paper reports.
|
| 121 |
+
("d3", "GenVideo"): {"synthscope": {"map": 98.95, "auroc": 98.23, "acc": 95.68}, "official": {"map": 97.93, "auroc": 97.04}},
|
| 122 |
("d3", "EvalCrafter"): {"synthscope": {"map": 99.3, "auroc": 98.62, "acc": 96.59}, "official": {"map": 98.7, "auroc": 97.81}},
|
| 123 |
("d3", "VideoPhy"): {"synthscope": {"map": 99.45, "auroc": 98.09, "acc": 95.19}, "official": {"map": 98.84, "auroc": 96.28}},
|
| 124 |
("d3", "VidProM"): {"synthscope": {"map": 92.15, "auroc": 90.86, "acc": 84.86}, "official": {"map": 84.91, "auroc": 82.83}},
|
| 125 |
+
# SPLIT (training-free): the paper's metric is fake recall at fixed FPR (in the spec note); these are AP (fake positive) /
|
| 126 |
+
# AUROC / acc at the calibrated threshold. synthscope = job 270384, official = official code end to end (job 270323).
|
| 127 |
+
("split", "GenVideo"): {"synthscope": {"map": 99.79, "auroc": 99.97, "acc": 99.41}, "official": {"map": 96.12, "auroc": 99.16}},
|
| 128 |
+
("restrav", "GenVidBench"): {"synthscope": {"map": 92.7, "auroc": 95.9, "acc": 86.87}},
|
| 129 |
("stall", "GenVideo"): {"synthscope": {"map": 82.38, "auroc": 83.51, "acc": 56.88}},
|
| 130 |
("stall", "VideoFeedback"): {"synthscope": {"map": 83.07, "auroc": 85.1, "acc": 62.12}},
|
| 131 |
("stall", "ComGenVid"): {"synthscope": {"map": 82.59, "auroc": 84.0, "acc": 57.34}},
|
interface/backend/engine.py
CHANGED
|
@@ -125,6 +125,9 @@ def _build_model(cfg):
|
|
| 125 |
if model_type == "d3":
|
| 126 |
from models.video.d3.d3 import SynthScopeD3Model
|
| 127 |
return SynthScopeD3Model(cfg)
|
|
|
|
|
|
|
|
|
|
| 128 |
if model_type == "restrav":
|
| 129 |
from models.video.restrav.restrav import SynthScopeReStraVModel
|
| 130 |
return SynthScopeReStraVModel(cfg)
|
|
@@ -261,25 +264,29 @@ class Engine:
|
|
| 261 |
return torch.sigmoid(logit.reshape(x.shape[0]))
|
| 262 |
raise ValueError(f"Unexpected image predict_kind: {kind}")
|
| 263 |
|
| 264 |
-
def
|
| 265 |
-
"""Load (and cache)
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
| 267 |
import json
|
| 268 |
-
|
| 269 |
-
p = config.CHECKPOINTS_DIR / "d3_calibration.json"
|
| 270 |
if not p.exists():
|
| 271 |
-
p = config.REPO_ROOT / "models" / "video" /
|
| 272 |
-
self.
|
| 273 |
-
return self.
|
| 274 |
|
| 275 |
@torch.no_grad()
|
| 276 |
def _video_fake_prob(self, model, spec: ModelSpec, clip: torch.Tensor) -> torch.Tensor:
|
| 277 |
"""Return P(fake) for a single normalized clip tensor [T,3,H,W]."""
|
| 278 |
frames = clip.unsqueeze(0).to(self.device) # [1,T,3,H,W]
|
| 279 |
-
if spec.key
|
| 280 |
-
cal = self.
|
| 281 |
if cal is not None:
|
| 282 |
-
s = model.score(frames) # [1] raw volatility
|
|
|
|
|
|
|
| 283 |
z = (s - cal["threshold"]) / cal["scale"]
|
| 284 |
if not cal["higher_is_fake"]:
|
| 285 |
z = -z
|
|
@@ -408,6 +415,14 @@ class Engine:
|
|
| 408 |
res["num_frames"] = int(clip.shape[0])
|
| 409 |
return res
|
| 410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
def _result(self, spec: ModelSpec, fake_prob: float, elapsed: float) -> dict:
|
| 412 |
fake_prob = max(0.0, min(1.0, fake_prob))
|
| 413 |
return {
|
|
|
|
| 125 |
if model_type == "d3":
|
| 126 |
from models.video.d3.d3 import SynthScopeD3Model
|
| 127 |
return SynthScopeD3Model(cfg)
|
| 128 |
+
if model_type == "split":
|
| 129 |
+
from models.video.split.split import SynthScopeSPLITModel
|
| 130 |
+
return SynthScopeSPLITModel(cfg)
|
| 131 |
if model_type == "restrav":
|
| 132 |
from models.video.restrav.restrav import SynthScopeReStraVModel
|
| 133 |
return SynthScopeReStraVModel(cfg)
|
|
|
|
| 264 |
return torch.sigmoid(logit.reshape(x.shape[0]))
|
| 265 |
raise ValueError(f"Unexpected image predict_kind: {kind}")
|
| 266 |
|
| 267 |
+
def _video_calibration(self, key: str):
|
| 268 |
+
"""Load (and cache) a training-free video detector's raw score -> probability calibration, if it has one: a
|
| 269 |
+
``<key>_calibration.json`` in the checkpoint folder overrides the one shipped in ``models/video/<key>/``."""
|
| 270 |
+
if not hasattr(self, "_video_cal"):
|
| 271 |
+
self._video_cal = {}
|
| 272 |
+
if key not in self._video_cal:
|
| 273 |
import json
|
| 274 |
+
p = config.CHECKPOINTS_DIR / f"{key}_calibration.json"
|
|
|
|
| 275 |
if not p.exists():
|
| 276 |
+
p = config.REPO_ROOT / "models" / "video" / key / f"{key}_calibration.json"
|
| 277 |
+
self._video_cal[key] = json.loads(p.read_text()) if p.exists() else None
|
| 278 |
+
return self._video_cal[key]
|
| 279 |
|
| 280 |
@torch.no_grad()
|
| 281 |
def _video_fake_prob(self, model, spec: ModelSpec, clip: torch.Tensor) -> torch.Tensor:
|
| 282 |
"""Return P(fake) for a single normalized clip tensor [T,3,H,W]."""
|
| 283 |
frames = clip.unsqueeze(0).to(self.device) # [1,T,3,H,W]
|
| 284 |
+
if spec.key in ("d3", "split"):
|
| 285 |
+
cal = self._video_calibration(spec.key)
|
| 286 |
if cal is not None:
|
| 287 |
+
s = model.score(frames) # [1] raw score (D3 volatility, SPLIT roughness x incoherence)
|
| 288 |
+
if cal.get("log_score"): # SPLIT's score spans many orders of magnitude: calibrated on its log
|
| 289 |
+
s = s.clamp_min(1e-30).log()
|
| 290 |
z = (s - cal["threshold"]) / cal["scale"]
|
| 291 |
if not cal["higher_is_fake"]:
|
| 292 |
z = -z
|
|
|
|
| 415 |
res["num_frames"] = int(clip.shape[0])
|
| 416 |
return res
|
| 417 |
|
| 418 |
+
@torch.no_grad()
|
| 419 |
+
def video_raw_score(self, spec: ModelSpec, path) -> float:
|
| 420 |
+
"""A training-free video detector's raw score for one clip, oriented so that higher means more likely generated
|
| 421 |
+
(for fake recall at a fixed false-positive rate, where saturated probabilities would tie)."""
|
| 422 |
+
model = self._get_model(spec)
|
| 423 |
+
s = float(model.score(self.video_clip(spec, path).unsqueeze(0).to(self.device)).item())
|
| 424 |
+
return s if getattr(model, "higher_is_fake", True) else -s
|
| 425 |
+
|
| 426 |
def _result(self, spec: ModelSpec, fake_prob: float, elapsed: float) -> dict:
|
| 427 |
fake_prob = max(0.0, min(1.0, fake_prob))
|
| 428 |
return {
|
interface/backend/registry.py
CHANGED
|
@@ -461,6 +461,17 @@ _add(ModelSpec(
|
|
| 461 |
"Official preprocessing (D3 utils/video2frame.py + data/datasets.py): 16 frames at 8 fps (8 for clips under "
|
| 462 |
"2 s), BGR channels as cv2 reads them, 10% crop of the longer side, resize 224, ImageNet normalization.",
|
| 463 |
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
_add(ModelSpec(
|
| 465 |
key="restrav", name="ReStraV (video)", paper="2025",
|
| 466 |
modality="video", predict_kind="video", checkpoint="restrav.pth", ckpt_kind="synthscope",
|
|
|
|
| 461 |
"Official preprocessing (D3 utils/video2frame.py + data/datasets.py): 16 frames at 8 fps (8 for clips under "
|
| 462 |
"2 s), BGR channels as cv2 reads them, 10% crop of the longer side, resize 224, ImageNet normalization.",
|
| 463 |
))
|
| 464 |
+
_add(ModelSpec(
|
| 465 |
+
key="split", name="SPLIT (video)", paper="ECCV'26",
|
| 466 |
+
modality="video", predict_kind="video", checkpoint=None, ckpt_kind="none", weights="training_free",
|
| 467 |
+
overrides=["model@ModelConfig=video/split"],
|
| 468 |
+
num_frames=16, fps=8.0, frame_size=224, video_norm=(IMAGENET_MEAN, IMAGENET_STD), edge_crop=0.1,
|
| 469 |
+
edge_crop_long_side=True, short_clip_frames=8, fps_nearest=True, bgr=True,
|
| 470 |
+
notes="Training-free: frozen X-CLIP-B/16 patch tokens; two-step temporal roughness (gamma 8) times spatial "
|
| 471 |
+
"patch-level motion incoherence is the score. Official preprocessing (the same video2frame / read_video as "
|
| 472 |
+
"D3): 16 frames at 8 fps (8 for clips under 2 s), BGR, 10% crop of the longer side, resize 224, ImageNet "
|
| 473 |
+
"normalization.",
|
| 474 |
+
))
|
| 475 |
_add(ModelSpec(
|
| 476 |
key="restrav", name="ReStraV (video)", paper="2025",
|
| 477 |
modality="video", predict_kind="video", checkpoint="restrav.pth", ckpt_kind="synthscope",
|
interface/backend/repro_full.json
CHANGED
|
@@ -346,11 +346,11 @@
|
|
| 346 |
"benchmark": "GenVideo (DeMamba GenVideo-Val, Table 1)",
|
| 347 |
"metric": "AP",
|
| 348 |
"paper": "mAP=98.46",
|
| 349 |
-
"ours": "SynthScope mAP=
|
| 350 |
-
"ours_synthscope": "mAP=
|
| 351 |
"ours_official": "mAP=97.93 / AUROC=97.04",
|
| 352 |
"status": "reproduced",
|
| 353 |
-
"note": "D3 protocol (--limit 1000: first 1000 clips per class per generator; AP with real as the positive class). SynthScope engine
|
| 354 |
},
|
| 355 |
{
|
| 356 |
"benchmark": "EvalCrafter (Table 2, 14 generators, fake-only)",
|
|
@@ -1113,6 +1113,38 @@
|
|
| 1113 |
"note": "SERVED native-res path (official weights in SynthScope class). bench_eval --model spai (tag spai_served_synthbuster, 9 gens, limit 200/gen) = 96.75 mAP / 96.97 mAUROC (was 55.67 via the old fixed-448 path)."
|
| 1114 |
}
|
| 1115 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
"stall": [
|
| 1117 |
{
|
| 1118 |
"benchmark": "GenVideo (DeMamba GenVideo-Val, 10 generators + MSR-VTT)",
|
|
|
|
| 346 |
"benchmark": "GenVideo (DeMamba GenVideo-Val, Table 1)",
|
| 347 |
"metric": "AP",
|
| 348 |
"paper": "mAP=98.46",
|
| 349 |
+
"ours": "SynthScope mAP=98.95 / acc=95.68 / AUROC=98.23 \u00b7 official mAP=97.93 / AUROC=97.04",
|
| 350 |
+
"ours_synthscope": "mAP=98.95 / acc=95.68 / AUROC=98.23",
|
| 351 |
"ours_official": "mAP=97.93 / AUROC=97.04",
|
| 352 |
"status": "reproduced",
|
| 353 |
+
"note": "D3 protocol (--limit 1000: first 1000 clips per class per generator; AP with real as the positive class). SynthScope engine 98.95 AP (AUROC 98.23, balanced acc 95.68 at the refitted calibration; tag d3_genvideo, job 270428, after the GIF decode fix: WildScrape 96.33 vs paper 96.49, was 98.21 when GIFs decoded as black clips). Official D3 code end to end: 97.93 (tag d3_official_genvideo, job 252271"
|
| 354 |
},
|
| 355 |
{
|
| 356 |
"benchmark": "EvalCrafter (Table 2, 14 generators, fake-only)",
|
|
|
|
| 1113 |
"note": "SERVED native-res path (official weights in SynthScope class). bench_eval --model spai (tag spai_served_synthbuster, 9 gens, limit 200/gen) = 96.75 mAP / 96.97 mAUROC (was 55.67 via the old fixed-448 path)."
|
| 1114 |
}
|
| 1115 |
],
|
| 1116 |
+
"split": [
|
| 1117 |
+
{
|
| 1118 |
+
"benchmark": "GenVideo (GenVideo-Val, MSR-VTT real calibration, supp. Table 18)",
|
| 1119 |
+
"metric": "fake recall @ FPR 0.1/1/5% (Overall = all fakes pooled)",
|
| 1120 |
+
"paper": "fake_recall_fpr0.1=82.13 / fake_recall_fpr1=91.69 / fake_recall_fpr5=97.2",
|
| 1121 |
+
"ours": "SynthScope mAP=99.79 / acc=99.41 / AUROC=99.97 \u00b7 official mAP=96.12 / AUROC=99.16",
|
| 1122 |
+
"ours_synthscope": "mAP=99.79 / acc=99.41 / AUROC=99.97",
|
| 1123 |
+
"ours_official": "mAP=96.12 / AUROC=99.16",
|
| 1124 |
+
"status": "reproduced",
|
| 1125 |
+
"note": "Official code on our data (JPEG frame round-trip, job 270323) reproduces the paper: Overall fake recall 82.22/90.17/96.89 vs 82.13/91.69/97.20. SynthScope decodes frames directly (no JPEG) and reaches 98.87/99.86/99.98 (job 270384; mAP 99.79, AUROC 99.97, acc 99.41 at the calibrated threshold)."
|
| 1126 |
+
},
|
| 1127 |
+
{
|
| 1128 |
+
"benchmark": "FakeParts (partial edits; Tables 1, 15-16)",
|
| 1129 |
+
"metric": "fake recall @ FPR",
|
| 1130 |
+
"paper": "fake_recall_fpr0.1=74.45",
|
| 1131 |
+
"ours": "",
|
| 1132 |
+
"ours_synthscope": "",
|
| 1133 |
+
"ours_official": "",
|
| 1134 |
+
"status": "missing",
|
| 1135 |
+
"note": "not in SynthScope yet"
|
| 1136 |
+
},
|
| 1137 |
+
{
|
| 1138 |
+
"benchmark": "ViF-Bench (Tables 3, 19)",
|
| 1139 |
+
"metric": "fake recall @ FPR",
|
| 1140 |
+
"paper": "fake_recall_fpr0.1=84.52",
|
| 1141 |
+
"ours": "",
|
| 1142 |
+
"ours_synthscope": "",
|
| 1143 |
+
"ours_official": "",
|
| 1144 |
+
"status": "missing",
|
| 1145 |
+
"note": "not in SynthScope yet"
|
| 1146 |
+
}
|
| 1147 |
+
],
|
| 1148 |
"stall": [
|
| 1149 |
{
|
| 1150 |
"benchmark": "GenVideo (DeMamba GenVideo-Val, 10 generators + MSR-VTT)",
|
interface/scripts/calibrate_split.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Calibrate the training-free SPLIT video detector so its output is a usable probability.
|
| 2 |
+
|
| 3 |
+
SPLIT's raw score (TTR ** 8 * motion incoherence) spans many orders of magnitude, so the calibration works on its
|
| 4 |
+
logarithm and follows the paper's real-only protocol: the decision threshold is the score at a target false-positive
|
| 5 |
+
rate on real videos alone (default 1%, numpy's ``quantile(..., method="higher")`` as the official ``eval.py``), and the
|
| 6 |
+
logistic scale puts the median real video at P(fake) = 0.05. The engine then reports
|
| 7 |
+
``sigmoid((log(score) - threshold) / scale)`` as P(fake). Fakes are scored only to report the recall at that threshold.
|
| 8 |
+
|
| 9 |
+
conda run -n SynthScope python interface/scripts/calibrate_split.py [--n-real 2000] [--n-fake 1000] [--fpr 0.01]
|
| 10 |
+
[--data DIR] [--out FILE]
|
| 11 |
+
|
| 12 |
+
Clips go through the engine's own SPLIT preprocessing (``Engine.video_clip``), so the calibration matches what the web
|
| 13 |
+
app and benchmarks feed the model. ``--data`` takes GenVideo-Val as released (``Real/`` + ``Fake/<gen>/``) or as laid out
|
| 14 |
+
by the ``genvideo`` loader (``test/<gen>/{0_real,1_fake}/``).
|
| 15 |
+
"""
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import math
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
sys.path[:0] = [str(Path(__file__).resolve().parents[1]), str(Path(__file__).resolve().parent)]
|
| 25 |
+
from backend import config # noqa: E402
|
| 26 |
+
from backend.engine import get_engine # noqa: E402
|
| 27 |
+
from backend.registry import get_spec # noqa: E402
|
| 28 |
+
from calibrate_d3 import GENVIDEO, collect, score_paths # noqa: E402
|
| 29 |
+
|
| 30 |
+
EPS = 1e-30
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main():
|
| 34 |
+
ap = argparse.ArgumentParser()
|
| 35 |
+
ap.add_argument("--n-real", type=int, default=2000)
|
| 36 |
+
ap.add_argument("--n-fake", type=int, default=1000)
|
| 37 |
+
ap.add_argument("--fpr", type=float, default=0.01, help="target false-positive rate on real videos")
|
| 38 |
+
ap.add_argument("--data", default=str(GENVIDEO), help="GenVideo-Val root (released layout or the genvideo loader's)")
|
| 39 |
+
ap.add_argument("--out", default=None, help="where to write the JSON (default: <CKPT_DIR>/split_calibration.json)")
|
| 40 |
+
args = ap.parse_args()
|
| 41 |
+
|
| 42 |
+
engine = get_engine()
|
| 43 |
+
spec = get_spec("split")
|
| 44 |
+
model = engine._get_model(spec)
|
| 45 |
+
reals, _ = collect(args.n_real, args.data)
|
| 46 |
+
_, fakes = collect(args.n_fake, args.data)
|
| 47 |
+
print(f"scoring {len(reals)} real + {len(fakes)} fake clips on {engine.device}...")
|
| 48 |
+
rs = np.log(np.maximum(score_paths(engine, spec, model, reals), EPS))
|
| 49 |
+
fs = np.log(np.maximum(score_paths(engine, spec, model, fakes), EPS))
|
| 50 |
+
|
| 51 |
+
threshold = float(np.quantile(rs, 1 - args.fpr, method="higher"))
|
| 52 |
+
scale = max((threshold - float(np.median(rs))) / math.log(19), 1e-6) # median real -> sigmoid(-ln 19) = 0.05
|
| 53 |
+
cal = {"threshold": threshold, "scale": scale, "higher_is_fake": True, "log_score": True,
|
| 54 |
+
"target_fpr": args.fpr, "real_fpr": float((rs >= threshold).mean()),
|
| 55 |
+
"fake_recall": float((fs >= threshold).mean()), "median_log_real": float(np.median(rs)),
|
| 56 |
+
"median_log_fake": float(np.median(fs)), "n_real": int(len(rs)), "n_fake": int(len(fs))}
|
| 57 |
+
out = Path(args.out) if args.out else config.CHECKPOINTS_DIR / "split_calibration.json"
|
| 58 |
+
out.write_text(json.dumps(cal, indent=2) + "\n")
|
| 59 |
+
print(f"threshold(log)={threshold:.4f} scale={scale:.4f} real FPR={cal['real_fpr']:.4f} "
|
| 60 |
+
f"fake recall={cal['fake_recall']:.4f}")
|
| 61 |
+
print(f"wrote {out}")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
if __name__ == "__main__":
|
| 65 |
+
main()
|
models/video/d3/README.md
CHANGED
|
@@ -149,7 +149,7 @@ frame extraction, reader and model run end to end on the same data (`repro/offic
|
|
| 149 |
|
| 150 |
| Benchmark (AP, real positive) | Paper | Official code, our data | SynthScope | Status |
|
| 151 |
|---|---:|---:|---:|---|
|
| 152 |
-
| GenVideo-Val (Table 1, 10 generators) | 98.46 | 97.93 | **
|
| 153 |
| EvalCrafter (Table 2, 14 generators) | 98.87 | 98.70 | **99.30** | reproduced |
|
| 154 |
| VideoPhy (Table 3; 8 of 10 generators still downloadable) | 99.16 (8 shown: 99.22) | 98.84 | **99.45** | reproduced |
|
| 155 |
| VidProM (Table 4; 4 of 6 generators, first 1000 clips each) | 88.46 (those 4: 84.95) | 84.91 | 92.15 | reference (subsample) |
|
|
@@ -158,11 +158,14 @@ frame extraction, reader and model run end to end on the same data (`repro/offic
|
|
| 158 |
are within 0.25 AP; VidProM's four generators average 84.91 vs 84.95 (Text2Video-Zero 46.36 vs 45.11, the paper's
|
| 159 |
noted failure case). The one outlier is GenVideo's WildScrape (92.50 vs 96.49): its 282 GIFs cannot be timed by
|
| 160 |
decord, so their frames start at 0 instead of the official random offset.
|
|
|
|
|
|
|
|
|
|
| 161 |
- **SynthScope's D3 is the official model**: on the frames the official code extracts it returns identical scores
|
| 162 |
(max |diff| 0). Its numbers differ from the official column only because the official pipeline writes frames to JPEG
|
| 163 |
with ffmpeg and reads them back; SynthScope decodes frames directly. That matters most for low-quality clips
|
| 164 |
(VidProM Text2Video-Zero: 71.28 in SynthScope vs 46.36 with the official JPEG round-trip).
|
| 165 |
-
- AUROC and balanced accuracy (at the calibrated threshold) for SynthScope: GenVideo 98.
|
| 166 |
98.62 / 96.59, VideoPhy 98.09 / 95.19, VidProM 90.86 / 84.86. Per-generator numbers are in
|
| 167 |
`repro/benchmark_specs/d3.json` and `repro/bench_results/d3_*.json`.
|
| 168 |
|
|
|
|
| 149 |
|
| 150 |
| Benchmark (AP, real positive) | Paper | Official code, our data | SynthScope | Status |
|
| 151 |
|---|---:|---:|---:|---|
|
| 152 |
+
| GenVideo-Val (Table 1, 10 generators) | 98.46 | 97.93 | **98.95** | reproduced |
|
| 153 |
| EvalCrafter (Table 2, 14 generators) | 98.87 | 98.70 | **99.30** | reproduced |
|
| 154 |
| VideoPhy (Table 3; 8 of 10 generators still downloadable) | 99.16 (8 shown: 99.22) | 98.84 | **99.45** | reproduced |
|
| 155 |
| VidProM (Table 4; 4 of 6 generators, first 1000 clips each) | 88.46 (those 4: 84.95) | 84.91 | 92.15 | reference (subsample) |
|
|
|
|
| 158 |
are within 0.25 AP; VidProM's four generators average 84.91 vs 84.95 (Text2Video-Zero 46.36 vs 45.11, the paper's
|
| 159 |
noted failure case). The one outlier is GenVideo's WildScrape (92.50 vs 96.49): its 282 GIFs cannot be timed by
|
| 160 |
decord, so their frames start at 0 instead of the official random offset.
|
| 161 |
+
- **GIFs (2026-11-01):** decord cannot open GIFs at all, and SynthScope's decoder used to return a black clip for them, which
|
| 162 |
+
D3 (flat = generated) called fake for free. Decoded with PIL now, WildScrape is 96.33 AP (paper 96.49; it was 98.21 on
|
| 163 |
+
black clips) and GenVideo 98.95 (was 99.14); the other nine generators are unchanged (job 270428).
|
| 164 |
- **SynthScope's D3 is the official model**: on the frames the official code extracts it returns identical scores
|
| 165 |
(max |diff| 0). Its numbers differ from the official column only because the official pipeline writes frames to JPEG
|
| 166 |
with ffmpeg and reads them back; SynthScope decodes frames directly. That matters most for low-quality clips
|
| 167 |
(VidProM Text2Video-Zero: 71.28 in SynthScope vs 46.36 with the official JPEG round-trip).
|
| 168 |
+
- AUROC and balanced accuracy (at the calibrated threshold) for SynthScope: GenVideo 98.23 / 95.68, EvalCrafter
|
| 169 |
98.62 / 96.59, VideoPhy 98.09 / 95.19, VidProM 90.86 / 84.86. Per-generator numbers are in
|
| 170 |
`repro/benchmark_specs/d3.json` and `repro/bench_results/d3_*.json`.
|
| 171 |
|
models/video/d3/d3.py
CHANGED
|
@@ -100,6 +100,8 @@ class _ResnetEncoder(nn.Module):
|
|
| 100 |
class SynthScopeD3Model(nn.Module):
|
| 101 |
"""Training-free second-order video detector (D3)."""
|
| 102 |
|
|
|
|
|
|
|
| 103 |
def __init__(self, config: DictConfig):
|
| 104 |
super().__init__()
|
| 105 |
self.config = config
|
|
|
|
| 100 |
class SynthScopeD3Model(nn.Module):
|
| 101 |
"""Training-free second-order video detector (D3)."""
|
| 102 |
|
| 103 |
+
higher_is_fake = False # real videos have the larger second-order volatility
|
| 104 |
+
|
| 105 |
def __init__(self, config: DictConfig):
|
| 106 |
super().__init__()
|
| 107 |
self.config = config
|
models/video/split/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPLIT (`split`)
|
| 2 |
+
|
| 3 |
+
**SPLIT: Training-Free AI-Generated and Partially Edited Video Detection via Spatial Patch-Level Incoherence and
|
| 4 |
+
Temporal Roughness** (Jongyeop Hyun, Hyounghun Kim; ECCV 2026) — [paper (arXiv 2607.02886)](https://arxiv.org/abs/2607.02886) ·
|
| 5 |
+
[official code](https://github.com/mldljyh/SPLIT)
|
| 6 |
+
|
| 7 |
+
A training-free video detector. Real motion is smooth in a frozen encoder's patch-feature space and moves coherently
|
| 8 |
+
across neighbouring patches; generated (or partially edited) video jitters from frame to frame and moves incoherently.
|
| 9 |
+
SPLIT measures both on patch tokens and multiplies them. The paper evaluates it where deployment needs it: fake recall
|
| 10 |
+
at a fixed, very low false-positive rate on real videos (0.1 / 1 / 5%), with the threshold calibrated on real videos only.
|
| 11 |
+
|
| 12 |
+
## Pipeline
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
video ─ 16 frames at 8 fps (8 for clips under 2 s), BGR, 10% crop of the longer side, resize 224, ImageNet norm
|
| 16 |
+
→ frozen X-CLIP B/16 vision tower → patch tokens P[t, n] (CLS dropped)
|
| 17 |
+
→ TTR = mean over patches of log2(L1 / L2) L1 = Σ‖P[t+1] − P[t]‖ (1-frame steps)
|
| 18 |
+
L2 = Σ‖P[t+2] − P[t]‖ / 2 · (T−1)/(T−2) (2-frame steps)
|
| 19 |
+
→ LSMI = mean ‖spatial difference of the motion field P[t+1] − P[t] on the patch grid‖ (x and y averaged)
|
| 20 |
+
→ score = TTR ** 8 · LSMI (higher = more likely generated)
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## SynthScope adaptations
|
| 24 |
+
|
| 25 |
+
| choice | reason |
|
| 26 |
+
|---|---|
|
| 27 |
+
| `SynthScopeSPLITModel` is SynthScope's own implementation of the paper's equations | The official repository has no license, so no code is copied. It returns the official `SPLIT_model.forward_score` on the same frames (relative difference ≤ 7e-6, job 269966). |
|
| 28 |
+
| Registry entry reuses D3's video preprocessing | SPLIT's released code reads frames exactly as D3 does (the same `video2frame` / `read_video`): 16 frames at 8 fps, BGR, 10% crop of the long side, resize 224, ImageNet normalization. |
|
| 29 |
+
| Frames are decoded directly (decord; PIL for GIFs), not written to JPEG by ffmpeg and read back | The official pipeline's lossy JPEG round-trip is the one remaining difference from the official code, and it changes the numbers a lot (see Results). |
|
| 30 |
+
| Clips longer than 3 s start at 0 | The official extraction picks a random start in a multiprocessing pool, so its frames are not reproducible anyway. |
|
| 31 |
+
| `split_calibration.json` (P(fake) for the web app) | The raw score spans many orders of magnitude, so it is calibrated on its log, by the paper's real-only rule (see below). |
|
| 32 |
+
| One dummy `_noop` parameter | Lets the SynthScope trainer run on a training-free model; it has no effect. |
|
| 33 |
+
|
| 34 |
+
## Results
|
| 35 |
+
|
| 36 |
+
Paper metric: **fake recall (%) at FPR 0.1 / 1 / 5%**, the threshold being the (1 − FPR) quantile of the real scores
|
| 37 |
+
(official `eval.py`). GenVideo-Val: its 10,000 MSR-VTT real videos calibrate the threshold, and all 8,584 fakes of the
|
| 38 |
+
10 generators are scored (the official reader drops 11 clips with fewer than 8 frames) (paper Table 18, "thresholds calibrated using the MSR-VTT real set only").
|
| 39 |
+
|
| 40 |
+
Two columns besides the paper: **official code, our data** runs the official frame extraction (ffmpeg JPEG frames),
|
| 41 |
+
reader, `SPLIT_model` and `evaluate_split` end to end on our copy of GenVideo-Val
|
| 42 |
+
(`repro/official_evals/split_official_eval.py`, job 270323); **SynthScope** is this implementation
|
| 43 |
+
(`benchmark/bench_eval_video.py -m split --dataset genvideo`, job 270384).
|
| 44 |
+
|
| 45 |
+
| GenVideo (fake recall @ FPR 0.1 / 1 / 5%) | Paper | Official code, our data | SynthScope |
|
| 46 |
+
|---|---|---|---|
|
| 47 |
+
| **Overall** | 82.13 / 91.69 / 97.20 | **82.22 / 90.17 / 96.89** | 98.87 / 99.86 / 99.98 |
|
| 48 |
+
| Crafter | 88.84 / 96.78 / 99.64 | 89.99 / 96.21 / 99.64 | 100 / 100 / 100 |
|
| 49 |
+
| Gen2 | 53.26 / 77.61 / 94.71 | 56.16 / 76.30 / 94.64 | 100 / 100 / 100 |
|
| 50 |
+
| HotShot | 99.86 / 100 / 100 | 99.86 / 100 / 100 | 100 / 100 / 100 |
|
| 51 |
+
| Lavie | 92.50 / 96.36 / 98.79 | 93.21 / 96.07 / 98.71 | 99.93 / 99.93 / 100 |
|
| 52 |
+
| ModelScope | 86.71 / 93.57 / 97.86 | 87.29 / 93.14 / 97.86 | 99.57 / 99.86 / 100 |
|
| 53 |
+
| MoonValley | 49.36 / 76.36 / 95.37 | 52.88 / 74.76 / 95.37 | 100 / 100 / 100 |
|
| 54 |
+
| MorphStudio | 94.71 / 98.43 / 99.71 | 95.00 / 98.00 / 99.71 | 100 / 100 / 100 |
|
| 55 |
+
| Show_1 | 96.86 / 99.86 / 100 | 97.86 / 99.71 / 100 | 100 / 100 / 100 |
|
| 56 |
+
| Sora | 87.50 / 98.21 / 100 | 91.07 / 98.21 / 100 | 100 / 100 / 100 |
|
| 57 |
+
| WildScrape | 71.66 / 79.76 / 85.89 | 73.27 / 79.74 / 86.42 | 89.94 / 98.92 / 99.78 |
|
| 58 |
+
| D3 Overall (the paper's baseline row) | 3.00 / 55.75 / 87.42 | — | 0.24 / 63.32 / 96.41 |
|
| 59 |
+
|
| 60 |
+
- **The official code reproduces the paper on our data**: Overall within 1.6 points at every FPR, every generator within
|
| 61 |
+
0.6 at FPR 5%, and the larger 0.1% differences (Gen2, MoonValley, Sora with 56 fakes) are the random clip start the
|
| 62 |
+
official extraction draws.
|
| 63 |
+
- **SynthScope scores higher because it skips the JPEG round-trip.** The official `video2frame.py` writes every frame
|
| 64 |
+
with ffmpeg's default JPEG settings and reads it back; the compression noise differs from frame to frame, which adds
|
| 65 |
+
temporal roughness to real videos and stretches the real-score tail that sets the threshold. On the same videos,
|
| 66 |
+
decoded directly, SPLIT separates real and generated almost perfectly (AP 99.79, AUROC 99.97 averaged over generators).
|
| 67 |
+
The **official code, our data** column is the like-for-like comparison with the paper.
|
| 68 |
+
- AP / AUROC / balanced accuracy at the calibrated threshold (SynthScope, mean over generators): 99.79 / 99.97 / 99.41.
|
| 69 |
+
Per-generator numbers: `repro/bench_results/split_genvideo_fpr.json`, `split_official_genvideo.json`.
|
| 70 |
+
- Not run: FakeParts and ViF-Bench (the paper's other two benchmarks; not in SynthScope yet), and the cross-real
|
| 71 |
+
threshold transfer to ROVI (needs FakeParts' real set).
|
| 72 |
+
|
| 73 |
+
## Probability calibration (`split_calibration.json`)
|
| 74 |
+
|
| 75 |
+
The raw score (TTR⁸ · LSMI) has a median of about 1e-10 on real GenVideo videos and about 1 on generated ones, so the engine maps
|
| 76 |
+
its logarithm to P(fake) = sigmoid((log score − threshold) / scale). Following the paper's real-only protocol, the
|
| 77 |
+
threshold is the log score at FPR 1% on 2,000 GenVideo-Val real videos (P(fake) = 0.5 there), and the scale puts the
|
| 78 |
+
median real video at P(fake) = 0.05 (`interface/scripts/calibrate_split.py`, through the engine's own preprocessing; on
|
| 79 |
+
1,000 random GenVideo fakes the recall at that threshold is 100%). A `split_calibration.json` in `$SYNTHSCOPE_CKPT_DIR`
|
| 80 |
+
overrides the shipped one. AP, AUROC and fake recall at fixed FPR do not depend on it.
|
| 81 |
+
|
| 82 |
+
## Usage
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
synthscope detect path/to/video.mp4 -m split
|
| 86 |
+
python benchmark/bench_eval_video.py -m split --dataset genvideo --tag split_genvideo_fpr
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
`bench_eval_video.py` reports fake recall at FPR 0.1 / 1 / 5% (per generator, their mean, and pooled over all
|
| 90 |
+
generators) for training-free detectors that expose a raw `score`.
|
| 91 |
+
|
| 92 |
+
## License
|
| 93 |
+
|
| 94 |
+
SynthScope's implementation is Apache-2.0. The official SPLIT repository has no license; no code from it is included.
|
| 95 |
+
The X-CLIP weights (`microsoft/xclip-base-patch16`) are MIT.
|
models/video/split/__init__.py
ADDED
|
File without changes
|
models/video/split/split.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SPLIT integrated into SynthScope.
|
| 2 |
+
|
| 3 |
+
Paper: "SPLIT: Training-Free AI-Generated and Partially Edited Video Detection via Spatial Patch-Level Incoherence and
|
| 4 |
+
Temporal Roughness" (Hyun & Kim, ECCV 2026). https://github.com/mldljyh/SPLIT
|
| 5 |
+
|
| 6 |
+
Training-free. A frozen vision encoder (X-CLIP B/16 by default) turns every frame into patch tokens P[t, n] (CLS
|
| 7 |
+
dropped). Two signals are computed on them:
|
| 8 |
+
|
| 9 |
+
* Two-step Temporal Roughness (TTR), per patch: the path length of its token trajectory with 1-frame steps,
|
| 10 |
+
L1 = sum_t ||P[t+1] - P[t]||, against the path length with 2-frame steps rescaled to the same number of steps,
|
| 11 |
+
L2 = (sum_t ||P[t+2] - P[t]|| / 2) * (T-1) / (T-2); TTR = log2(L1 / L2). Real motion is smooth (TTR near 0); generated
|
| 12 |
+
video jitters from frame to frame, so 1-frame steps are long compared with 2-frame steps. Averaged over patches.
|
| 13 |
+
* Spatial patch-level motion incoherence: the motion field M[t] = P[t+1] - P[t] on the patch grid and its spatial
|
| 14 |
+
differences between neighbouring patches; the mean of their L2 norms over time and space, averaged over x and y.
|
| 15 |
+
|
| 16 |
+
score = TTR ** gamma * incoherence (gamma = 8); higher means more likely generated.
|
| 17 |
+
|
| 18 |
+
This is SynthScope's own implementation of the paper's equations (the official repository has no license); it is
|
| 19 |
+
checked against the official ``SPLIT_model`` on the same frames (repro/split parity script, identical scores).
|
| 20 |
+
The released code reads frames exactly as D3 does (the same ``video2frame`` / ``read_video``), so the registry entry
|
| 21 |
+
reuses D3's preprocessing: 16 frames at 8 fps, BGR, 10% crop of the long side, resize 224, ImageNet normalization.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
import torch.nn as nn
|
| 30 |
+
from omegaconf import DictConfig
|
| 31 |
+
from transformers import XCLIPVisionModel
|
| 32 |
+
|
| 33 |
+
ENCODERS = {"xclip-16": "microsoft/xclip-base-patch16", "xclip-32": "microsoft/xclip-base-patch32"}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class SynthScopeSPLITModel(nn.Module):
|
| 37 |
+
"""Training-free SPLIT detector on a frozen X-CLIP vision tower."""
|
| 38 |
+
|
| 39 |
+
higher_is_fake = True # orientation of ``score`` (benchmark/bench_eval_video.py fake recall at fixed FPR)
|
| 40 |
+
|
| 41 |
+
def __init__(self, config: DictConfig):
|
| 42 |
+
super().__init__()
|
| 43 |
+
cfg = config.ModelConfig
|
| 44 |
+
self.encoder = XCLIPVisionModel.from_pretrained(ENCODERS[str(cfg.get("encoder", "xclip-16")).lower()])
|
| 45 |
+
self.encoder.requires_grad_(False)
|
| 46 |
+
self.encoder.eval()
|
| 47 |
+
self.gamma = float(cfg.get("gamma", 8.0))
|
| 48 |
+
self.threshold = float(cfg.get("threshold", 0.0))
|
| 49 |
+
self._noop = nn.Parameter(torch.zeros(1)) # the trainer's AdamW needs one parameter; training is a no-op
|
| 50 |
+
|
| 51 |
+
def train(self, mode: bool = True):
|
| 52 |
+
super().train(mode)
|
| 53 |
+
self.encoder.eval() # the encoder stays frozen in eval mode
|
| 54 |
+
return self
|
| 55 |
+
|
| 56 |
+
def patch_tokens(self, frames: torch.Tensor) -> torch.Tensor:
|
| 57 |
+
"""``frames`` [B, T, 3, H, W] (normalized) -> patch tokens [B, T, N, D] (CLS dropped)."""
|
| 58 |
+
b, t = frames.shape[:2]
|
| 59 |
+
with torch.no_grad():
|
| 60 |
+
tokens = self.encoder(pixel_values=frames.reshape(b * t, *frames.shape[2:])).last_hidden_state[:, 1:]
|
| 61 |
+
return tokens.reshape(b, t, *tokens.shape[1:]).float()
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
def temporal_roughness(p: torch.Tensor) -> torch.Tensor:
|
| 65 |
+
"""Mean over patches of TTR = log2(L1 / L2); ``p`` [B, T, N, D] -> [B]."""
|
| 66 |
+
t = p.shape[1]
|
| 67 |
+
l1 = (p[:, 1:] - p[:, :-1]).norm(dim=-1).sum(dim=1) # [B, N]
|
| 68 |
+
l2 = (p[:, 2:] - p[:, :-2]).norm(dim=-1).sum(dim=1) / 2 * (t - 1) / (t - 2)
|
| 69 |
+
return ((torch.log(l1 + 1e-8) - torch.log(l2 + 1e-8)) / math.log(2)).mean(dim=1)
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def motion_incoherence(p: torch.Tensor) -> torch.Tensor:
|
| 73 |
+
"""Mean L2 norm of the spatial differences of the motion field on the patch grid; ``p`` [B, T, N, D] -> [B]."""
|
| 74 |
+
b, t, n, d = p.shape
|
| 75 |
+
g = int(round(n ** 0.5))
|
| 76 |
+
m = (p[:, 1:] - p[:, :-1]).reshape(b, t - 1, g, g, d) # motion field on the grid
|
| 77 |
+
dx = (m[:, :, :, :-1] - m[:, :, :, 1:]).norm(dim=-1).mean(dim=(1, 2, 3))
|
| 78 |
+
dy = (m[:, :, :-1] - m[:, :, 1:]).norm(dim=-1).mean(dim=(1, 2, 3))
|
| 79 |
+
return (dx + dy) / 2
|
| 80 |
+
|
| 81 |
+
def score(self, frames: torch.Tensor) -> torch.Tensor:
|
| 82 |
+
"""Per-video SPLIT score [B] (higher = more likely generated)."""
|
| 83 |
+
p = self.patch_tokens(frames)
|
| 84 |
+
return self.temporal_roughness(p) ** self.gamma * self.motion_incoherence(p)
|
| 85 |
+
|
| 86 |
+
def forward(self, frames: torch.Tensor) -> torch.Tensor:
|
| 87 |
+
s = self.score(frames)
|
| 88 |
+
return torch.stack([self.threshold - s, s - self.threshold], dim=1)
|
| 89 |
+
|
| 90 |
+
def compute_loss(self, batch):
|
| 91 |
+
frames, _labels, _folder_ids = batch
|
| 92 |
+
return {"loss": (self._noop * 0.0).sum(), "outputs": self.forward(frames)}
|
models/video/split/split_calibration.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"threshold": -12.128589335463142,
|
| 3 |
+
"scale": 3.5924194383097205,
|
| 4 |
+
"higher_is_fake": true,
|
| 5 |
+
"log_score": true,
|
| 6 |
+
"target_fpr": 0.01,
|
| 7 |
+
"real_fpr": 0.01,
|
| 8 |
+
"fake_recall": 1.0,
|
| 9 |
+
"median_log_real": -22.706249159137492,
|
| 10 |
+
"median_log_fake": 0.12425237563573246,
|
| 11 |
+
"n_real": 2000,
|
| 12 |
+
"n_fake": 1000
|
| 13 |
+
}
|
repro/MASTER_RESULTS.jsonl
CHANGED
|
@@ -370,3 +370,9 @@
|
|
| 370 |
{"date": "2026-10-30T22:33:19", "tag": "effort_evalgen", "model": "effort", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/EvalGEN/test", "n_generators": 5, "means": {"acc": 92.16, "map": 99.55, "mauroc": 99.16}}
|
| 371 |
{"date": "2026-10-30T22:48:42", "tag": "effort_realchain", "model": "effort", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RealChain/test", "n_generators": 7, "means": {"acc": 50.56, "map": 60.38, "mauroc": 64.22}}
|
| 372 |
{"date": "2026-10-30T23:19:04", "tag": "effort_rrdataset", "model": "effort", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RRDataset/test", "n_generators": 3, "means": {"acc": 64.59, "map": 69.78, "mauroc": 68.6}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
{"date": "2026-10-30T22:33:19", "tag": "effort_evalgen", "model": "effort", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/EvalGEN/test", "n_generators": 5, "means": {"acc": 92.16, "map": 99.55, "mauroc": 99.16}}
|
| 371 |
{"date": "2026-10-30T22:48:42", "tag": "effort_realchain", "model": "effort", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RealChain/test", "n_generators": 7, "means": {"acc": 50.56, "map": 60.38, "mauroc": 64.22}}
|
| 372 |
{"date": "2026-10-30T23:19:04", "tag": "effort_rrdataset", "model": "effort", "ckpt": "registry-default", "benchmark": "pergen", "data_root": "/ssdstore/ssalehi/SynthScope/RRDataset/test", "n_generators": 3, "means": {"acc": 64.59, "map": 69.78, "mauroc": 68.6}}
|
| 373 |
+
{"date": "2026-10-31T17:58:11", "tag": "split_genvideo_fpr", "model": "split", "ckpt": "registry-default", "benchmark": "video:pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test", "n_generators": 10, "means": {"acc": 50.0, "map": 97.05, "map_real": 99.72, "mauroc": 98.42, "fake_recall_at_fpr": {"0.1": 96.66, "1": 96.91, "5": 96.93}}}
|
| 374 |
+
{"date": "2026-10-31T19:33:09", "tag": "split_official_genvideo", "model": "split", "ckpt": "official SPLIT code (XCLIP-16), training-free", "benchmark": "video:official-split", "data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test", "n_generators": 10, "means": {"overall_fake_recall_at_fpr": {"0.1": 82.22, "1": 90.17, "5": 96.89}, "mean_fake_recall_at_fpr": {"0.1": 83.66, "1": 91.21, "5": 97.23}, "map": 96.12}}
|
| 375 |
+
{"date": "2026-10-31T20:38:46", "tag": "split_genvideo_fpr", "model": "split", "ckpt": "registry-default", "benchmark": "video:pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test", "n_generators": 10, "means": {"acc": 50.0, "map": 97.05, "map_real": 99.72, "mauroc": 98.42, "fake_recall_at_fpr": {"0.1": 96.66, "1": 96.91, "5": 96.93}, "overall_fake_recall_at_fpr": {"0.1": 96.41, "1": 96.67, "5": 96.69}}}
|
| 376 |
+
{"date": "2026-10-31T22:35:53", "tag": "split_genvideo_fpr", "model": "split", "ckpt": "registry-default", "benchmark": "video:pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test", "n_generators": 10, "means": {"acc": 99.41, "map": 99.79, "map_real": 100.0, "mauroc": 99.97, "fake_recall_at_fpr": {"0.1": 98.94, "1": 99.87, "5": 99.98}, "overall_fake_recall_at_fpr": {"0.1": 98.87, "1": 99.86, "5": 99.98}}}
|
| 377 |
+
{"date": "2026-10-31T23:42:33", "tag": "d3_genvideo_fpr", "model": "d3", "ckpt": "registry-default", "benchmark": "video:pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test", "n_generators": 10, "means": {"acc": 95.53, "map": 68.84, "map_real": 99.87, "mauroc": 98.45, "fake_recall_at_fpr": {"0.1": 0.21, "1": 62.03, "5": 96.52}, "overall_fake_recall_at_fpr": {"0.1": 0.24, "1": 63.32, "5": 96.41}}}
|
| 378 |
+
{"date": "2026-11-01T00:56:39", "tag": "d3_genvideo", "model": "d3", "ckpt": "registry-default", "benchmark": "video:pergen", "data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test", "n_generators": 10, "means": {"acc": 95.68, "map": 91.6, "map_real": 98.95, "mauroc": 98.23, "fake_recall_at_fpr": {"0.1": 0.0, "1": 33.77, "5": 96.71}, "overall_fake_recall_at_fpr": {"0.1": 0.0, "1": 35.2, "5": 96.34}}}
|
repro/RESULTS_SUMMARY.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
| 23 |
| DDA | AIGCDetect / ForenSynths / EvalGEN / SynthWildX / WildRF / GenImage | 87.8 / 81.4 / 97.2 / 90.9 / 90.3 / 91.7 acc | 87.84 / 81.43 / 96.86 / 90.80 / 90.20 / 89.37 acc | 89.58 / 80.79 / 96.79 / 88.00 / 90.56 / **91.35** acc | ✅ official match; trained in SynthScope reproduces 6 of 7 incl. GenImage (SynthWildX 2.9 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) |
|
| 27 |
| ReStraV (video) | GenVidBench | 93.0 | — | 92.70 | ✅ match |
|
| 28 |
| STALL (video) | GenVideo-Val | ~80 AUC | — | 83.5 AUROC | ✅ match |
|
| 29 |
|
|
@@ -185,7 +185,7 @@ Fidelity: SynthScope's class == the official `OmniAID` for all three releases (s
|
|
| 185 |
### Video detectors (no official weights released)
|
| 186 |
| Method · Benchmark | Paper | SynthScope_train |
|
| 187 |
|---|---:|---:|
|
| 188 |
-
| D3 · GenVideo-Val-10 | 98.46 |
|
| 189 |
| D3 · EvalCrafter-14 | 98.87 | 99.30 (96.59); official code 98.70 |
|
| 190 |
| D3 · VideoPhy (8 of 10 generators online) | 99.16 | 99.45 (95.19); official code 98.84 |
|
| 191 |
| D3 · VidProM (4 of 6 generators, 1000-clip subsample) | 88.46 (84.95 on those 4) | 92.15 (84.86); official code 84.91 |
|
|
|
|
| 23 |
| DDA | AIGCDetect / ForenSynths / EvalGEN / SynthWildX / WildRF / GenImage | 87.8 / 81.4 / 97.2 / 90.9 / 90.3 / 91.7 acc | 87.84 / 81.43 / 96.86 / 90.80 / 90.20 / 89.37 acc | 89.58 / 80.79 / 96.79 / 88.00 / 90.56 / **91.35** acc | ✅ official match; trained in SynthScope reproduces 6 of 7 incl. GenImage (SynthWildX 2.9 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) | 98.95 | ✅ 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 |
|
| 29 |
|
|
|
|
| 185 |
### Video detectors (no official weights released)
|
| 186 |
| Method · Benchmark | Paper | SynthScope_train |
|
| 187 |
|---|---:|---:|
|
| 188 |
+
| D3 · GenVideo-Val-10 | 98.46 | 98.95 (95.68); official code 97.93 |
|
| 189 |
| D3 · EvalCrafter-14 | 98.87 | 99.30 (96.59); official code 98.70 |
|
| 190 |
| D3 · VideoPhy (8 of 10 generators online) | 99.16 | 99.45 (95.19); official code 98.84 |
|
| 191 |
| D3 · VidProM (4 of 6 generators, 1000-clip subsample) | 88.46 (84.95 on those 4) | 92.15 (84.86); official code 84.91 |
|
repro/bench_results/d3_genvideo.json
CHANGED
|
@@ -6,10 +6,20 @@
|
|
| 6 |
"data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test",
|
| 7 |
"n_generators": 10,
|
| 8 |
"means": {
|
| 9 |
-
"acc":
|
| 10 |
-
"map":
|
| 11 |
-
"map_real":
|
| 12 |
-
"mauroc": 98.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
},
|
| 14 |
"per_generator": {
|
| 15 |
"Crafter": {
|
|
@@ -18,7 +28,12 @@
|
|
| 18 |
"auroc": 98.69,
|
| 19 |
"real_acc": 94.3,
|
| 20 |
"fake_acc": 98.8,
|
| 21 |
-
"acc": 96.55
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
},
|
| 23 |
"Gen2": {
|
| 24 |
"ap": 97.01,
|
|
@@ -26,7 +41,12 @@
|
|
| 26 |
"auroc": 98.99,
|
| 27 |
"real_acc": 94.3,
|
| 28 |
"fake_acc": 100.0,
|
| 29 |
-
"acc": 97.15
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
},
|
| 31 |
"HotShot": {
|
| 32 |
"ap": 93.95,
|
|
@@ -34,7 +54,12 @@
|
|
| 34 |
"auroc": 98.42,
|
| 35 |
"real_acc": 94.3,
|
| 36 |
"fake_acc": 98.71,
|
| 37 |
-
"acc": 96.51
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
},
|
| 39 |
"Lavie": {
|
| 40 |
"ap": 96.21,
|
|
@@ -42,7 +67,12 @@
|
|
| 42 |
"auroc": 98.32,
|
| 43 |
"real_acc": 94.3,
|
| 44 |
"fake_acc": 97.4,
|
| 45 |
-
"acc": 95.85
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
},
|
| 47 |
"ModelScope": {
|
| 48 |
"ap": 92.89,
|
|
@@ -50,7 +80,12 @@
|
|
| 50 |
"auroc": 97.7,
|
| 51 |
"real_acc": 94.3,
|
| 52 |
"fake_acc": 94.71,
|
| 53 |
-
"acc": 94.51
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
},
|
| 55 |
"MoonValley": {
|
| 56 |
"ap": 96.08,
|
|
@@ -58,7 +93,12 @@
|
|
| 58 |
"auroc": 98.98,
|
| 59 |
"real_acc": 94.3,
|
| 60 |
"fake_acc": 99.84,
|
| 61 |
-
"acc": 97.07
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
},
|
| 63 |
"MorphStudio": {
|
| 64 |
"ap": 94.1,
|
|
@@ -66,7 +106,12 @@
|
|
| 66 |
"auroc": 98.49,
|
| 67 |
"real_acc": 94.3,
|
| 68 |
"fake_acc": 99.14,
|
| 69 |
-
"acc": 96.72
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
},
|
| 71 |
"Show_1": {
|
| 72 |
"ap": 94.62,
|
|
@@ -74,7 +119,12 @@
|
|
| 74 |
"auroc": 98.72,
|
| 75 |
"real_acc": 94.3,
|
| 76 |
"fake_acc": 99.57,
|
| 77 |
-
"acc": 96.94
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
},
|
| 79 |
"Sora": {
|
| 80 |
"ap": 62.39,
|
|
@@ -82,17 +132,27 @@
|
|
| 82 |
"auroc": 98.45,
|
| 83 |
"real_acc": 94.3,
|
| 84 |
"fake_acc": 98.21,
|
| 85 |
-
"acc": 96.26
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
},
|
| 87 |
"WildScrape": {
|
| 88 |
-
"ap":
|
| 89 |
-
"ap_real":
|
| 90 |
-
"auroc":
|
| 91 |
"real_acc": 94.3,
|
| 92 |
-
"fake_acc":
|
| 93 |
-
"acc":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
},
|
| 96 |
-
"date": "2026-
|
| 97 |
"synthscope_version": "video"
|
| 98 |
}
|
|
|
|
| 6 |
"data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test",
|
| 7 |
"n_generators": 10,
|
| 8 |
"means": {
|
| 9 |
+
"acc": 95.68,
|
| 10 |
+
"map": 91.6,
|
| 11 |
+
"map_real": 98.95,
|
| 12 |
+
"mauroc": 98.23,
|
| 13 |
+
"fake_recall_at_fpr": {
|
| 14 |
+
"0.1": 0.0,
|
| 15 |
+
"1": 33.77,
|
| 16 |
+
"5": 96.71
|
| 17 |
+
},
|
| 18 |
+
"overall_fake_recall_at_fpr": {
|
| 19 |
+
"0.1": 0.0,
|
| 20 |
+
"1": 35.2,
|
| 21 |
+
"5": 96.34
|
| 22 |
+
}
|
| 23 |
},
|
| 24 |
"per_generator": {
|
| 25 |
"Crafter": {
|
|
|
|
| 28 |
"auroc": 98.69,
|
| 29 |
"real_acc": 94.3,
|
| 30 |
"fake_acc": 98.8,
|
| 31 |
+
"acc": 96.55,
|
| 32 |
+
"fake_recall_at_fpr": {
|
| 33 |
+
"0.1": 0.0,
|
| 34 |
+
"1": 44.6,
|
| 35 |
+
"5": 98.5
|
| 36 |
+
}
|
| 37 |
},
|
| 38 |
"Gen2": {
|
| 39 |
"ap": 97.01,
|
|
|
|
| 41 |
"auroc": 98.99,
|
| 42 |
"real_acc": 94.3,
|
| 43 |
"fake_acc": 100.0,
|
| 44 |
+
"acc": 97.15,
|
| 45 |
+
"fake_recall_at_fpr": {
|
| 46 |
+
"0.1": 0.0,
|
| 47 |
+
"1": 56.4,
|
| 48 |
+
"5": 100.0
|
| 49 |
+
}
|
| 50 |
},
|
| 51 |
"HotShot": {
|
| 52 |
"ap": 93.95,
|
|
|
|
| 54 |
"auroc": 98.42,
|
| 55 |
"real_acc": 94.3,
|
| 56 |
"fake_acc": 98.71,
|
| 57 |
+
"acc": 96.51,
|
| 58 |
+
"fake_recall_at_fpr": {
|
| 59 |
+
"0.1": 0.0,
|
| 60 |
+
"1": 23.29,
|
| 61 |
+
"5": 98.43
|
| 62 |
+
}
|
| 63 |
},
|
| 64 |
"Lavie": {
|
| 65 |
"ap": 96.21,
|
|
|
|
| 67 |
"auroc": 98.32,
|
| 68 |
"real_acc": 94.3,
|
| 69 |
"fake_acc": 97.4,
|
| 70 |
+
"acc": 95.85,
|
| 71 |
+
"fake_recall_at_fpr": {
|
| 72 |
+
"0.1": 0.0,
|
| 73 |
+
"1": 36.3,
|
| 74 |
+
"5": 96.7
|
| 75 |
+
}
|
| 76 |
},
|
| 77 |
"ModelScope": {
|
| 78 |
"ap": 92.89,
|
|
|
|
| 80 |
"auroc": 97.7,
|
| 81 |
"real_acc": 94.3,
|
| 82 |
"fake_acc": 94.71,
|
| 83 |
+
"acc": 94.51,
|
| 84 |
+
"fake_recall_at_fpr": {
|
| 85 |
+
"0.1": 0.0,
|
| 86 |
+
"1": 19.57,
|
| 87 |
+
"5": 93.86
|
| 88 |
+
}
|
| 89 |
},
|
| 90 |
"MoonValley": {
|
| 91 |
"ap": 96.08,
|
|
|
|
| 93 |
"auroc": 98.98,
|
| 94 |
"real_acc": 94.3,
|
| 95 |
"fake_acc": 99.84,
|
| 96 |
+
"acc": 97.07,
|
| 97 |
+
"fake_recall_at_fpr": {
|
| 98 |
+
"0.1": 0.0,
|
| 99 |
+
"1": 59.58,
|
| 100 |
+
"5": 99.84
|
| 101 |
+
}
|
| 102 |
},
|
| 103 |
"MorphStudio": {
|
| 104 |
"ap": 94.1,
|
|
|
|
| 106 |
"auroc": 98.49,
|
| 107 |
"real_acc": 94.3,
|
| 108 |
"fake_acc": 99.14,
|
| 109 |
+
"acc": 96.72,
|
| 110 |
+
"fake_recall_at_fpr": {
|
| 111 |
+
"0.1": 0.0,
|
| 112 |
+
"1": 25.14,
|
| 113 |
+
"5": 99.0
|
| 114 |
+
}
|
| 115 |
},
|
| 116 |
"Show_1": {
|
| 117 |
"ap": 94.62,
|
|
|
|
| 119 |
"auroc": 98.72,
|
| 120 |
"real_acc": 94.3,
|
| 121 |
"fake_acc": 99.57,
|
| 122 |
+
"acc": 96.94,
|
| 123 |
+
"fake_recall_at_fpr": {
|
| 124 |
+
"0.1": 0.0,
|
| 125 |
+
"1": 38.71,
|
| 126 |
+
"5": 99.57
|
| 127 |
+
}
|
| 128 |
},
|
| 129 |
"Sora": {
|
| 130 |
"ap": 62.39,
|
|
|
|
| 132 |
"auroc": 98.45,
|
| 133 |
"real_acc": 94.3,
|
| 134 |
"fake_acc": 98.21,
|
| 135 |
+
"acc": 96.26,
|
| 136 |
+
"fake_recall_at_fpr": {
|
| 137 |
+
"0.1": 0.0,
|
| 138 |
+
"1": 23.21,
|
| 139 |
+
"5": 98.21
|
| 140 |
+
}
|
| 141 |
},
|
| 142 |
"WildScrape": {
|
| 143 |
+
"ap": 91.88,
|
| 144 |
+
"ap_real": 96.33,
|
| 145 |
+
"auroc": 95.55,
|
| 146 |
"real_acc": 94.3,
|
| 147 |
+
"fake_acc": 84.09,
|
| 148 |
+
"acc": 89.2,
|
| 149 |
+
"fake_recall_at_fpr": {
|
| 150 |
+
"0.1": 0.0,
|
| 151 |
+
"1": 10.93,
|
| 152 |
+
"5": 83.01
|
| 153 |
+
}
|
| 154 |
}
|
| 155 |
},
|
| 156 |
+
"date": "2026-11-01T00:56:39",
|
| 157 |
"synthscope_version": "video"
|
| 158 |
}
|
repro/bench_results/d3_genvideo_fpr.json
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tag": "d3_genvideo_fpr",
|
| 3 |
+
"model": "d3",
|
| 4 |
+
"ckpt": "registry-default",
|
| 5 |
+
"benchmark": "video:pergen",
|
| 6 |
+
"data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test",
|
| 7 |
+
"n_generators": 10,
|
| 8 |
+
"means": {
|
| 9 |
+
"acc": 95.53,
|
| 10 |
+
"map": 68.84,
|
| 11 |
+
"map_real": 99.87,
|
| 12 |
+
"mauroc": 98.45,
|
| 13 |
+
"fake_recall_at_fpr": {
|
| 14 |
+
"0.1": 0.21,
|
| 15 |
+
"1": 62.03,
|
| 16 |
+
"5": 96.52
|
| 17 |
+
},
|
| 18 |
+
"overall_fake_recall_at_fpr": {
|
| 19 |
+
"0.1": 0.24,
|
| 20 |
+
"1": 63.32,
|
| 21 |
+
"5": 96.41
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"per_generator": {
|
| 25 |
+
"Crafter": {
|
| 26 |
+
"ap": 84.96,
|
| 27 |
+
"ap_real": 99.86,
|
| 28 |
+
"auroc": 98.9,
|
| 29 |
+
"real_acc": 93.99,
|
| 30 |
+
"fake_acc": 98.93,
|
| 31 |
+
"acc": 96.46,
|
| 32 |
+
"fake_recall_at_fpr": {
|
| 33 |
+
"0.1": 0.57,
|
| 34 |
+
"1": 69.24,
|
| 35 |
+
"5": 98.57
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"Gen2": {
|
| 39 |
+
"ap": 85.37,
|
| 40 |
+
"ap_real": 99.9,
|
| 41 |
+
"auroc": 99.17,
|
| 42 |
+
"real_acc": 93.99,
|
| 43 |
+
"fake_acc": 100.0,
|
| 44 |
+
"acc": 96.99,
|
| 45 |
+
"fake_recall_at_fpr": {
|
| 46 |
+
"0.1": 0.07,
|
| 47 |
+
"1": 84.28,
|
| 48 |
+
"5": 100.0
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
"HotShot": {
|
| 52 |
+
"ap": 69.37,
|
| 53 |
+
"ap_real": 99.91,
|
| 54 |
+
"auroc": 98.65,
|
| 55 |
+
"real_acc": 93.99,
|
| 56 |
+
"fake_acc": 98.71,
|
| 57 |
+
"acc": 96.35,
|
| 58 |
+
"fake_recall_at_fpr": {
|
| 59 |
+
"0.1": 0.0,
|
| 60 |
+
"1": 56.29,
|
| 61 |
+
"5": 98.29
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"Lavie": {
|
| 65 |
+
"ap": 82.22,
|
| 66 |
+
"ap_real": 99.79,
|
| 67 |
+
"auroc": 98.52,
|
| 68 |
+
"real_acc": 93.99,
|
| 69 |
+
"fake_acc": 97.43,
|
| 70 |
+
"acc": 95.71,
|
| 71 |
+
"fake_recall_at_fpr": {
|
| 72 |
+
"0.1": 0.36,
|
| 73 |
+
"1": 62.36,
|
| 74 |
+
"5": 96.29
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"ModelScope": {
|
| 78 |
+
"ap": 66.32,
|
| 79 |
+
"ap_real": 99.85,
|
| 80 |
+
"auroc": 97.93,
|
| 81 |
+
"real_acc": 93.99,
|
| 82 |
+
"fake_acc": 94.71,
|
| 83 |
+
"acc": 94.35,
|
| 84 |
+
"fake_recall_at_fpr": {
|
| 85 |
+
"0.1": 0.0,
|
| 86 |
+
"1": 48.57,
|
| 87 |
+
"5": 93.86
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
"MoonValley": {
|
| 91 |
+
"ap": 76.11,
|
| 92 |
+
"ap_real": 99.95,
|
| 93 |
+
"auroc": 99.15,
|
| 94 |
+
"real_acc": 93.99,
|
| 95 |
+
"fake_acc": 99.84,
|
| 96 |
+
"acc": 96.92,
|
| 97 |
+
"fake_recall_at_fpr": {
|
| 98 |
+
"0.1": 1.12,
|
| 99 |
+
"1": 81.95,
|
| 100 |
+
"5": 99.84
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"MorphStudio": {
|
| 104 |
+
"ap": 69.42,
|
| 105 |
+
"ap_real": 99.91,
|
| 106 |
+
"auroc": 98.71,
|
| 107 |
+
"real_acc": 93.99,
|
| 108 |
+
"fake_acc": 99.14,
|
| 109 |
+
"acc": 96.57,
|
| 110 |
+
"fake_recall_at_fpr": {
|
| 111 |
+
"0.1": 0.0,
|
| 112 |
+
"1": 51.57,
|
| 113 |
+
"5": 98.57
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
"Show_1": {
|
| 117 |
+
"ap": 72.83,
|
| 118 |
+
"ap_real": 99.93,
|
| 119 |
+
"auroc": 98.93,
|
| 120 |
+
"real_acc": 93.99,
|
| 121 |
+
"fake_acc": 99.57,
|
| 122 |
+
"acc": 96.78,
|
| 123 |
+
"fake_recall_at_fpr": {
|
| 124 |
+
"0.1": 0.0,
|
| 125 |
+
"1": 68.29,
|
| 126 |
+
"5": 99.57
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"Sora": {
|
| 130 |
+
"ap": 19.13,
|
| 131 |
+
"ap_real": 99.99,
|
| 132 |
+
"auroc": 98.69,
|
| 133 |
+
"real_acc": 93.99,
|
| 134 |
+
"fake_acc": 98.21,
|
| 135 |
+
"acc": 96.1,
|
| 136 |
+
"fake_recall_at_fpr": {
|
| 137 |
+
"0.1": 0.0,
|
| 138 |
+
"1": 64.29,
|
| 139 |
+
"5": 98.21
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"WildScrape": {
|
| 143 |
+
"ap": 62.68,
|
| 144 |
+
"ap_real": 99.58,
|
| 145 |
+
"auroc": 95.82,
|
| 146 |
+
"real_acc": 93.99,
|
| 147 |
+
"fake_acc": 84.09,
|
| 148 |
+
"acc": 89.04,
|
| 149 |
+
"fake_recall_at_fpr": {
|
| 150 |
+
"0.1": 0.0,
|
| 151 |
+
"1": 33.44,
|
| 152 |
+
"5": 82.03
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"date": "2026-10-31T23:42:33",
|
| 157 |
+
"synthscope_version": "video"
|
| 158 |
+
}
|
repro/bench_results/split_genvideo_fpr.json
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tag": "split_genvideo_fpr",
|
| 3 |
+
"model": "split",
|
| 4 |
+
"ckpt": "registry-default",
|
| 5 |
+
"benchmark": "video:pergen",
|
| 6 |
+
"data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test",
|
| 7 |
+
"n_generators": 10,
|
| 8 |
+
"means": {
|
| 9 |
+
"acc": 99.41,
|
| 10 |
+
"map": 99.79,
|
| 11 |
+
"map_real": 100.0,
|
| 12 |
+
"mauroc": 99.97,
|
| 13 |
+
"fake_recall_at_fpr": {
|
| 14 |
+
"0.1": 98.94,
|
| 15 |
+
"1": 99.87,
|
| 16 |
+
"5": 99.98
|
| 17 |
+
},
|
| 18 |
+
"overall_fake_recall_at_fpr": {
|
| 19 |
+
"0.1": 98.87,
|
| 20 |
+
"1": 99.86,
|
| 21 |
+
"5": 99.98
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"per_generator": {
|
| 25 |
+
"Crafter": {
|
| 26 |
+
"ap": 99.98,
|
| 27 |
+
"ap_real": 100.0,
|
| 28 |
+
"auroc": 100.0,
|
| 29 |
+
"real_acc": 98.87,
|
| 30 |
+
"fake_acc": 100.0,
|
| 31 |
+
"acc": 99.43,
|
| 32 |
+
"fake_recall_at_fpr": {
|
| 33 |
+
"0.1": 100.0,
|
| 34 |
+
"1": 100.0,
|
| 35 |
+
"5": 100.0
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"Gen2": {
|
| 39 |
+
"ap": 99.84,
|
| 40 |
+
"ap_real": 100.0,
|
| 41 |
+
"auroc": 99.98,
|
| 42 |
+
"real_acc": 98.87,
|
| 43 |
+
"fake_acc": 100.0,
|
| 44 |
+
"acc": 99.43,
|
| 45 |
+
"fake_recall_at_fpr": {
|
| 46 |
+
"0.1": 100.0,
|
| 47 |
+
"1": 100.0,
|
| 48 |
+
"5": 100.0
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
"HotShot": {
|
| 52 |
+
"ap": 100.0,
|
| 53 |
+
"ap_real": 100.0,
|
| 54 |
+
"auroc": 100.0,
|
| 55 |
+
"real_acc": 98.87,
|
| 56 |
+
"fake_acc": 100.0,
|
| 57 |
+
"acc": 99.43,
|
| 58 |
+
"fake_recall_at_fpr": {
|
| 59 |
+
"0.1": 100.0,
|
| 60 |
+
"1": 100.0,
|
| 61 |
+
"5": 100.0
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"Lavie": {
|
| 65 |
+
"ap": 99.96,
|
| 66 |
+
"ap_real": 100.0,
|
| 67 |
+
"auroc": 99.99,
|
| 68 |
+
"real_acc": 98.87,
|
| 69 |
+
"fake_acc": 99.93,
|
| 70 |
+
"acc": 99.4,
|
| 71 |
+
"fake_recall_at_fpr": {
|
| 72 |
+
"0.1": 99.93,
|
| 73 |
+
"1": 99.93,
|
| 74 |
+
"5": 100.0
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"ModelScope": {
|
| 78 |
+
"ap": 99.89,
|
| 79 |
+
"ap_real": 100.0,
|
| 80 |
+
"auroc": 99.99,
|
| 81 |
+
"real_acc": 98.87,
|
| 82 |
+
"fake_acc": 100.0,
|
| 83 |
+
"acc": 99.43,
|
| 84 |
+
"fake_recall_at_fpr": {
|
| 85 |
+
"0.1": 99.57,
|
| 86 |
+
"1": 99.86,
|
| 87 |
+
"5": 100.0
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
"MoonValley": {
|
| 91 |
+
"ap": 99.62,
|
| 92 |
+
"ap_real": 100.0,
|
| 93 |
+
"auroc": 99.98,
|
| 94 |
+
"real_acc": 98.87,
|
| 95 |
+
"fake_acc": 100.0,
|
| 96 |
+
"acc": 99.43,
|
| 97 |
+
"fake_recall_at_fpr": {
|
| 98 |
+
"0.1": 100.0,
|
| 99 |
+
"1": 100.0,
|
| 100 |
+
"5": 100.0
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"MorphStudio": {
|
| 104 |
+
"ap": 99.97,
|
| 105 |
+
"ap_real": 100.0,
|
| 106 |
+
"auroc": 100.0,
|
| 107 |
+
"real_acc": 98.87,
|
| 108 |
+
"fake_acc": 100.0,
|
| 109 |
+
"acc": 99.43,
|
| 110 |
+
"fake_recall_at_fpr": {
|
| 111 |
+
"0.1": 100.0,
|
| 112 |
+
"1": 100.0,
|
| 113 |
+
"5": 100.0
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
"Show_1": {
|
| 117 |
+
"ap": 99.96,
|
| 118 |
+
"ap_real": 100.0,
|
| 119 |
+
"auroc": 100.0,
|
| 120 |
+
"real_acc": 98.87,
|
| 121 |
+
"fake_acc": 100.0,
|
| 122 |
+
"acc": 99.43,
|
| 123 |
+
"fake_recall_at_fpr": {
|
| 124 |
+
"0.1": 100.0,
|
| 125 |
+
"1": 100.0,
|
| 126 |
+
"5": 100.0
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"Sora": {
|
| 130 |
+
"ap": 99.63,
|
| 131 |
+
"ap_real": 100.0,
|
| 132 |
+
"auroc": 100.0,
|
| 133 |
+
"real_acc": 98.87,
|
| 134 |
+
"fake_acc": 100.0,
|
| 135 |
+
"acc": 99.43,
|
| 136 |
+
"fake_recall_at_fpr": {
|
| 137 |
+
"0.1": 100.0,
|
| 138 |
+
"1": 100.0,
|
| 139 |
+
"5": 100.0
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"WildScrape": {
|
| 143 |
+
"ap": 99.09,
|
| 144 |
+
"ap_real": 99.97,
|
| 145 |
+
"auroc": 99.79,
|
| 146 |
+
"real_acc": 98.87,
|
| 147 |
+
"fake_acc": 99.57,
|
| 148 |
+
"acc": 99.22,
|
| 149 |
+
"fake_recall_at_fpr": {
|
| 150 |
+
"0.1": 89.94,
|
| 151 |
+
"1": 98.92,
|
| 152 |
+
"5": 99.78
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"date": "2026-10-31T22:35:53",
|
| 157 |
+
"synthscope_version": "video"
|
| 158 |
+
}
|
repro/bench_results/split_official_genvideo.json
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tag": "split_official_genvideo",
|
| 3 |
+
"model": "split",
|
| 4 |
+
"ckpt": "official SPLIT code (XCLIP-16), training-free",
|
| 5 |
+
"benchmark": "video:official-split",
|
| 6 |
+
"data_root": "/ssdstore/ssalehi/SynthScope/GenVideo/test",
|
| 7 |
+
"n_generators": 10,
|
| 8 |
+
"means": {
|
| 9 |
+
"overall_fake_recall_at_fpr": {
|
| 10 |
+
"0.1": 82.22,
|
| 11 |
+
"1": 90.17,
|
| 12 |
+
"5": 96.89
|
| 13 |
+
},
|
| 14 |
+
"mean_fake_recall_at_fpr": {
|
| 15 |
+
"0.1": 83.66,
|
| 16 |
+
"1": 91.21,
|
| 17 |
+
"5": 97.23
|
| 18 |
+
},
|
| 19 |
+
"map": 96.12
|
| 20 |
+
},
|
| 21 |
+
"overall": {
|
| 22 |
+
"fake_recall_at_fpr": {
|
| 23 |
+
"0.1": 82.22,
|
| 24 |
+
"1": 90.17,
|
| 25 |
+
"5": 96.89
|
| 26 |
+
},
|
| 27 |
+
"ap": 99.19,
|
| 28 |
+
"ap_real": 98.99,
|
| 29 |
+
"auroc": 99.08,
|
| 30 |
+
"n_real": 10000,
|
| 31 |
+
"n_fake": 8573
|
| 32 |
+
},
|
| 33 |
+
"per_generator": {
|
| 34 |
+
"Crafter": {
|
| 35 |
+
"fake_recall_at_fpr": {
|
| 36 |
+
"0.1": 89.99,
|
| 37 |
+
"1": 96.21,
|
| 38 |
+
"5": 99.64
|
| 39 |
+
},
|
| 40 |
+
"ap": 99.25,
|
| 41 |
+
"ap_real": 99.98,
|
| 42 |
+
"auroc": 99.88,
|
| 43 |
+
"n_real": 10000,
|
| 44 |
+
"n_fake": 1398
|
| 45 |
+
},
|
| 46 |
+
"Gen2": {
|
| 47 |
+
"fake_recall_at_fpr": {
|
| 48 |
+
"0.1": 56.16,
|
| 49 |
+
"1": 76.3,
|
| 50 |
+
"5": 94.64
|
| 51 |
+
},
|
| 52 |
+
"ap": 94.42,
|
| 53 |
+
"ap_real": 99.88,
|
| 54 |
+
"auroc": 99.1,
|
| 55 |
+
"n_real": 10000,
|
| 56 |
+
"n_fake": 1380
|
| 57 |
+
},
|
| 58 |
+
"HotShot": {
|
| 59 |
+
"fake_recall_at_fpr": {
|
| 60 |
+
"0.1": 99.86,
|
| 61 |
+
"1": 100.0,
|
| 62 |
+
"5": 100.0
|
| 63 |
+
},
|
| 64 |
+
"ap": 100.0,
|
| 65 |
+
"ap_real": 100.0,
|
| 66 |
+
"auroc": 100.0,
|
| 67 |
+
"n_real": 10000,
|
| 68 |
+
"n_fake": 700
|
| 69 |
+
},
|
| 70 |
+
"Lavie": {
|
| 71 |
+
"fake_recall_at_fpr": {
|
| 72 |
+
"0.1": 93.21,
|
| 73 |
+
"1": 96.07,
|
| 74 |
+
"5": 98.71
|
| 75 |
+
},
|
| 76 |
+
"ap": 98.94,
|
| 77 |
+
"ap_real": 99.96,
|
| 78 |
+
"auroc": 99.73,
|
| 79 |
+
"n_real": 10000,
|
| 80 |
+
"n_fake": 1400
|
| 81 |
+
},
|
| 82 |
+
"ModelScope": {
|
| 83 |
+
"fake_recall_at_fpr": {
|
| 84 |
+
"0.1": 87.29,
|
| 85 |
+
"1": 93.14,
|
| 86 |
+
"5": 97.86
|
| 87 |
+
},
|
| 88 |
+
"ap": 97.2,
|
| 89 |
+
"ap_real": 99.97,
|
| 90 |
+
"auroc": 99.58,
|
| 91 |
+
"n_real": 10000,
|
| 92 |
+
"n_fake": 700
|
| 93 |
+
},
|
| 94 |
+
"MoonValley": {
|
| 95 |
+
"fake_recall_at_fpr": {
|
| 96 |
+
"0.1": 52.88,
|
| 97 |
+
"1": 74.76,
|
| 98 |
+
"5": 95.37
|
| 99 |
+
},
|
| 100 |
+
"ap": 89.71,
|
| 101 |
+
"ap_real": 99.94,
|
| 102 |
+
"auroc": 99.11,
|
| 103 |
+
"n_real": 10000,
|
| 104 |
+
"n_fake": 626
|
| 105 |
+
},
|
| 106 |
+
"MorphStudio": {
|
| 107 |
+
"fake_recall_at_fpr": {
|
| 108 |
+
"0.1": 95.0,
|
| 109 |
+
"1": 98.0,
|
| 110 |
+
"5": 99.71
|
| 111 |
+
},
|
| 112 |
+
"ap": 99.39,
|
| 113 |
+
"ap_real": 100.0,
|
| 114 |
+
"auroc": 99.94,
|
| 115 |
+
"n_real": 10000,
|
| 116 |
+
"n_fake": 700
|
| 117 |
+
},
|
| 118 |
+
"Show_1": {
|
| 119 |
+
"fake_recall_at_fpr": {
|
| 120 |
+
"0.1": 97.86,
|
| 121 |
+
"1": 99.71,
|
| 122 |
+
"5": 100.0
|
| 123 |
+
},
|
| 124 |
+
"ap": 99.85,
|
| 125 |
+
"ap_real": 100.0,
|
| 126 |
+
"auroc": 99.99,
|
| 127 |
+
"n_real": 10000,
|
| 128 |
+
"n_fake": 700
|
| 129 |
+
},
|
| 130 |
+
"Sora": {
|
| 131 |
+
"fake_recall_at_fpr": {
|
| 132 |
+
"0.1": 91.07,
|
| 133 |
+
"1": 98.21,
|
| 134 |
+
"5": 100.0
|
| 135 |
+
},
|
| 136 |
+
"ap": 94.72,
|
| 137 |
+
"ap_real": 100.0,
|
| 138 |
+
"auroc": 99.91,
|
| 139 |
+
"n_real": 10000,
|
| 140 |
+
"n_fake": 56
|
| 141 |
+
},
|
| 142 |
+
"WildScrape": {
|
| 143 |
+
"fake_recall_at_fpr": {
|
| 144 |
+
"0.1": 73.27,
|
| 145 |
+
"1": 79.74,
|
| 146 |
+
"5": 86.42
|
| 147 |
+
},
|
| 148 |
+
"ap": 87.71,
|
| 149 |
+
"ap_real": 99.26,
|
| 150 |
+
"auroc": 94.31,
|
| 151 |
+
"n_real": 10000,
|
| 152 |
+
"n_fake": 913
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"date": "2026-10-31T19:33:09"
|
| 156 |
+
}
|
repro/benchmark_specs/d3.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
"paper": "D3: Training-Free AI-Generated Video Detection Using Second-Order Features (Zheng et al., ICCV 2025)",
|
| 4 |
"arxiv_id": "n/a (CVF Open Access ICCV 2025; code github.com/Zig-HS/D3)",
|
| 5 |
"official_repo": "https://github.com/Zig-HS/D3",
|
| 6 |
-
"training_set_in_paper": "NONE
|
| 7 |
"encoder_in_paper": "XCLIP-B/16 (X-CLIP ViT-B/16, Ni et al. ECCV'22, paper ref [34]) as the frozen per-frame visual encoder; first-order = L2 distance. CLIP-B/16 is the ablation runner-up (~97 on GenVideo, Table 7).",
|
| 8 |
"paper_protocol": "Extract up to a 2 s segment, sample frames at 8 fps, JPEG; crop 10% of the longer edge then resize to 224x224. Score = std of 2nd-order central-difference of inter-frame L2 distances (Eq.8). Metric reported: AP (real-class); AUROC in supplementary. Hardware RTX 4090. (paper Sec 4.1 'Implementation Details')",
|
| 9 |
"synthscope_encoder": "X-CLIP-B/16 (microsoft/xclip-base-patch16), as in the paper. SynthScope's D3 gives the same score as the official D3_model on the same frames (max |diff| 0, repro check 2026-10-11).",
|
|
@@ -45,11 +45,11 @@
|
|
| 45 |
"official_eval_protocol": "XCLIP-B/16 frozen, 8 fps over <=2 s, JPEG, 10% longer-edge crop, resize 224x224, L2 first-order, std of 2nd-order central diff; AP computed real-class (positive=real, score=+std), max_len=1000 per class (official eval.py).",
|
| 46 |
"our_status": "reproduced",
|
| 47 |
"our_number": {
|
| 48 |
-
"map":
|
| 49 |
-
"auroc": 98.
|
| 50 |
-
"acc":
|
| 51 |
"official_code_map": 97.93,
|
| 52 |
-
"note": "D3 protocol (--limit 1000: first 1000 clips per class per generator; AP with real as the positive class). SynthScope engine
|
| 53 |
},
|
| 54 |
"our_per_generator_ap": {
|
| 55 |
"Crafter": 99.14,
|
|
@@ -61,10 +61,10 @@
|
|
| 61 |
"MorphStudio": 99.27,
|
| 62 |
"Show_1": 99.37,
|
| 63 |
"Sora": 99.91,
|
| 64 |
-
"WildScrape":
|
| 65 |
},
|
| 66 |
"dataset_source": "on-disk: /hddstore/ssalehi/SynthScope/d3_repro/genvideo_val/GenVideo-Val (14 GB; downloaded from ModelScope cccnju/Gen-Video GenVideo-Val.zip). The real source is MSR-VTT.",
|
| 67 |
-
"xclip_weights_downloadable": "YES
|
| 68 |
"official_code_per_generator_ap": {
|
| 69 |
"Crafter": 98.28,
|
| 70 |
"Gen2": 99.24,
|
|
@@ -281,4 +281,4 @@
|
|
| 281 |
"n_benchmarks_reported": 4,
|
| 282 |
"key_finding": "Since 2026-10-11 SynthScope's D3 follows the official preprocessing (16 frames at 8 fps, 8 for clips under 2 s; BGR; 10% crop of the longer side; ImageNet normalization) and is scored with D3's protocol (first 1000 clips per class, AP with real as the positive class). The official D3 code run end to end on the same data (repro/official_evals/d3_official_eval.py) reproduces GenVideo 97.93, EvalCrafter 98.70, VideoPhy 98.84 (8 of 10 generators) and VidProM 84.91 (4 of 6 generators, paper 84.95 on those 4); SynthScope's engine gives 99.14 / 99.30 / 99.45 / 92.15. The remaining engine-vs-official difference is only the official JPEG frame round-trip."
|
| 283 |
}
|
| 284 |
-
}
|
|
|
|
| 3 |
"paper": "D3: Training-Free AI-Generated Video Detection Using Second-Order Features (Zheng et al., ICCV 2025)",
|
| 4 |
"arxiv_id": "n/a (CVF Open Access ICCV 2025; code github.com/Zig-HS/D3)",
|
| 5 |
"official_repo": "https://github.com/Zig-HS/D3",
|
| 6 |
+
"training_set_in_paper": "NONE — D3 is training-free (inference only). Baselines (re-implemented for comparison) were trained on real videos from Youku-mPLUG + Pika fakes, following DeMamba (paper Sec 4.1).",
|
| 7 |
"encoder_in_paper": "XCLIP-B/16 (X-CLIP ViT-B/16, Ni et al. ECCV'22, paper ref [34]) as the frozen per-frame visual encoder; first-order = L2 distance. CLIP-B/16 is the ablation runner-up (~97 on GenVideo, Table 7).",
|
| 8 |
"paper_protocol": "Extract up to a 2 s segment, sample frames at 8 fps, JPEG; crop 10% of the longer edge then resize to 224x224. Score = std of 2nd-order central-difference of inter-frame L2 distances (Eq.8). Metric reported: AP (real-class); AUROC in supplementary. Hardware RTX 4090. (paper Sec 4.1 'Implementation Details')",
|
| 9 |
"synthscope_encoder": "X-CLIP-B/16 (microsoft/xclip-base-patch16), as in the paper. SynthScope's D3 gives the same score as the official D3_model on the same frames (max |diff| 0, repro check 2026-10-11).",
|
|
|
|
| 45 |
"official_eval_protocol": "XCLIP-B/16 frozen, 8 fps over <=2 s, JPEG, 10% longer-edge crop, resize 224x224, L2 first-order, std of 2nd-order central diff; AP computed real-class (positive=real, score=+std), max_len=1000 per class (official eval.py).",
|
| 46 |
"our_status": "reproduced",
|
| 47 |
"our_number": {
|
| 48 |
+
"map": 98.95,
|
| 49 |
+
"auroc": 98.23,
|
| 50 |
+
"acc": 95.68,
|
| 51 |
"official_code_map": 97.93,
|
| 52 |
+
"note": "D3 protocol (--limit 1000: first 1000 clips per class per generator; AP with real as the positive class). SynthScope engine 98.95 AP (AUROC 98.23, balanced acc 95.68 at the refitted calibration; tag d3_genvideo, job 270428, after the GIF decode fix: WildScrape 96.33 vs paper 96.49, was 98.21 when GIFs decoded as black clips). Official D3 code end to end: 97.93 (tag d3_official_genvideo, job 252271): 9 of 10 generators within 0.25 of the paper; WildScrape 92.50 vs 96.49 (282 GIFs whose length decord cannot read start at 0 instead of a random offset). The old 63.93 compared 10,000 reals with as few as 56 fakes and used AP with fake as positive."
|
| 53 |
},
|
| 54 |
"our_per_generator_ap": {
|
| 55 |
"Crafter": 99.14,
|
|
|
|
| 61 |
"MorphStudio": 99.27,
|
| 62 |
"Show_1": 99.37,
|
| 63 |
"Sora": 99.91,
|
| 64 |
+
"WildScrape": 96.33
|
| 65 |
},
|
| 66 |
"dataset_source": "on-disk: /hddstore/ssalehi/SynthScope/d3_repro/genvideo_val/GenVideo-Val (14 GB; downloaded from ModelScope cccnju/Gen-Video GenVideo-Val.zip). The real source is MSR-VTT.",
|
| 67 |
+
"xclip_weights_downloadable": "YES — microsoft/xclip-base-patch16 on HuggingFace (Kinetics-400 trained, NOT gated). Matches paper ref [34]. The gap-closing fix (swap CLIP->XCLIP) is unblocked by available weights.",
|
| 68 |
"official_code_per_generator_ap": {
|
| 69 |
"Crafter": 98.28,
|
| 70 |
"Gen2": 99.24,
|
|
|
|
| 281 |
"n_benchmarks_reported": 4,
|
| 282 |
"key_finding": "Since 2026-10-11 SynthScope's D3 follows the official preprocessing (16 frames at 8 fps, 8 for clips under 2 s; BGR; 10% crop of the longer side; ImageNet normalization) and is scored with D3's protocol (first 1000 clips per class, AP with real as the positive class). The official D3 code run end to end on the same data (repro/official_evals/d3_official_eval.py) reproduces GenVideo 97.93, EvalCrafter 98.70, VideoPhy 98.84 (8 of 10 generators) and VidProM 84.91 (4 of 6 generators, paper 84.95 on those 4); SynthScope's engine gives 99.14 / 99.30 / 99.45 / 92.15. The remaining engine-vs-official difference is only the official JPEG frame round-trip."
|
| 283 |
}
|
| 284 |
+
}
|
repro/benchmark_specs/split.json
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "split",
|
| 3 |
+
"paper": "SPLIT: Training-Free AI-Generated and Partially Edited Video Detection via Spatial Patch-Level Incoherence and Temporal Roughness (Hyun & Kim, ECCV 2026)",
|
| 4 |
+
"arxiv_id": "2607.02886",
|
| 5 |
+
"official_repo": "https://github.com/mldljyh/SPLIT",
|
| 6 |
+
"training_set_in_paper": "NONE - SPLIT is training-free (frozen encoder, real-only threshold calibration).",
|
| 7 |
+
"encoder_in_paper": "X-CLIP B/16 (microsoft/xclip-base-patch16) patch tokens; CLIP, DINO and CNN encoders are ablations.",
|
| 8 |
+
"paper_protocol": "Official video2frame.py: ffmpeg JPEG frames, 8 fps over 3 s (random start for longer clips); read_video: 16 frames (8 if fewer than 16), cv2 BGR, 10% crop of the longer side, resize 224, ImageNet normalization. Score = TTR**8 * LSMI. Metric: fake recall at FPR 0.1/1/5%, threshold = (1-FPR) quantile of the real scores (eval.py evaluate_split).",
|
| 9 |
+
"synthscope_encoder": "X-CLIP B/16, as in the paper. SynthScope's own implementation (the official repo has no license) returns the official SPLIT_model.forward_score on the same frames (relative difference <= 7e-6, job 269966).",
|
| 10 |
+
"benchmarks": [
|
| 11 |
+
{
|
| 12 |
+
"name": "GenVideo (GenVideo-Val, MSR-VTT real calibration, supp. Table 18)",
|
| 13 |
+
"datasets": [
|
| 14 |
+
"GenVideo-Val"
|
| 15 |
+
],
|
| 16 |
+
"subsets_or_generators": [
|
| 17 |
+
"Crafter",
|
| 18 |
+
"Gen2",
|
| 19 |
+
"HotShot",
|
| 20 |
+
"Lavie",
|
| 21 |
+
"ModelScope",
|
| 22 |
+
"MoonValley",
|
| 23 |
+
"MorphStudio",
|
| 24 |
+
"Show_1",
|
| 25 |
+
"Sora",
|
| 26 |
+
"WildScrape"
|
| 27 |
+
],
|
| 28 |
+
"metric": "fake recall @ FPR 0.1/1/5% (Overall = all fakes pooled)",
|
| 29 |
+
"paper_number": {
|
| 30 |
+
"fake_recall_fpr0.1": 82.13,
|
| 31 |
+
"fake_recall_fpr1": 91.69,
|
| 32 |
+
"fake_recall_fpr5": 97.2
|
| 33 |
+
},
|
| 34 |
+
"paper_per_generator": {
|
| 35 |
+
"Crafter": {
|
| 36 |
+
"fpr_0.1": 88.84,
|
| 37 |
+
"fpr_1": 96.78,
|
| 38 |
+
"fpr_5": 99.64
|
| 39 |
+
},
|
| 40 |
+
"Gen2": {
|
| 41 |
+
"fpr_0.1": 53.26,
|
| 42 |
+
"fpr_1": 77.61,
|
| 43 |
+
"fpr_5": 94.71
|
| 44 |
+
},
|
| 45 |
+
"HotShot": {
|
| 46 |
+
"fpr_0.1": 99.86,
|
| 47 |
+
"fpr_1": 100,
|
| 48 |
+
"fpr_5": 100
|
| 49 |
+
},
|
| 50 |
+
"Lavie": {
|
| 51 |
+
"fpr_0.1": 92.5,
|
| 52 |
+
"fpr_1": 96.36,
|
| 53 |
+
"fpr_5": 98.79
|
| 54 |
+
},
|
| 55 |
+
"ModelScope": {
|
| 56 |
+
"fpr_0.1": 86.71,
|
| 57 |
+
"fpr_1": 93.57,
|
| 58 |
+
"fpr_5": 97.86
|
| 59 |
+
},
|
| 60 |
+
"MoonValley": {
|
| 61 |
+
"fpr_0.1": 49.36,
|
| 62 |
+
"fpr_1": 76.36,
|
| 63 |
+
"fpr_5": 95.37
|
| 64 |
+
},
|
| 65 |
+
"MorphStudio": {
|
| 66 |
+
"fpr_0.1": 94.71,
|
| 67 |
+
"fpr_1": 98.43,
|
| 68 |
+
"fpr_5": 99.71
|
| 69 |
+
},
|
| 70 |
+
"Show_1": {
|
| 71 |
+
"fpr_0.1": 96.86,
|
| 72 |
+
"fpr_1": 99.86,
|
| 73 |
+
"fpr_5": 100
|
| 74 |
+
},
|
| 75 |
+
"Sora": {
|
| 76 |
+
"fpr_0.1": 87.5,
|
| 77 |
+
"fpr_1": 98.21,
|
| 78 |
+
"fpr_5": 100
|
| 79 |
+
},
|
| 80 |
+
"WildScrape": {
|
| 81 |
+
"fpr_0.1": 71.66,
|
| 82 |
+
"fpr_1": 79.76,
|
| 83 |
+
"fpr_5": 85.89
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"official_eval_protocol": "All 10,000 MSR-VTT reals calibrate the threshold; all fakes of the 10 generators scored (official eval.py, no max_len).",
|
| 87 |
+
"our_status": "reproduced",
|
| 88 |
+
"our_number": {
|
| 89 |
+
"official_code_our_data": {
|
| 90 |
+
"fpr_0.1": 82.22,
|
| 91 |
+
"fpr_1": 90.17,
|
| 92 |
+
"fpr_5": 96.89
|
| 93 |
+
},
|
| 94 |
+
"synthscope": {
|
| 95 |
+
"fpr_0.1": 98.87,
|
| 96 |
+
"fpr_1": 99.86,
|
| 97 |
+
"fpr_5": 99.98
|
| 98 |
+
},
|
| 99 |
+
"note": "Official code on our data (JPEG frame round-trip, job 270323) reproduces the paper: Overall fake recall 82.22/90.17/96.89 vs 82.13/91.69/97.20. SynthScope decodes frames directly (no JPEG) and reaches 98.87/99.86/99.98 (job 270384; mAP 99.79, AUROC 99.97, acc 99.41 at the calibrated threshold)."
|
| 100 |
+
},
|
| 101 |
+
"official_code_per_generator": {
|
| 102 |
+
"Crafter": {
|
| 103 |
+
"fpr_0.1": 89.99,
|
| 104 |
+
"fpr_1": 96.21,
|
| 105 |
+
"fpr_5": 99.64
|
| 106 |
+
},
|
| 107 |
+
"Gen2": {
|
| 108 |
+
"fpr_0.1": 56.16,
|
| 109 |
+
"fpr_1": 76.3,
|
| 110 |
+
"fpr_5": 94.64
|
| 111 |
+
},
|
| 112 |
+
"HotShot": {
|
| 113 |
+
"fpr_0.1": 99.86,
|
| 114 |
+
"fpr_1": 100.0,
|
| 115 |
+
"fpr_5": 100.0
|
| 116 |
+
},
|
| 117 |
+
"Lavie": {
|
| 118 |
+
"fpr_0.1": 93.21,
|
| 119 |
+
"fpr_1": 96.07,
|
| 120 |
+
"fpr_5": 98.71
|
| 121 |
+
},
|
| 122 |
+
"ModelScope": {
|
| 123 |
+
"fpr_0.1": 87.29,
|
| 124 |
+
"fpr_1": 93.14,
|
| 125 |
+
"fpr_5": 97.86
|
| 126 |
+
},
|
| 127 |
+
"MoonValley": {
|
| 128 |
+
"fpr_0.1": 52.88,
|
| 129 |
+
"fpr_1": 74.76,
|
| 130 |
+
"fpr_5": 95.37
|
| 131 |
+
},
|
| 132 |
+
"MorphStudio": {
|
| 133 |
+
"fpr_0.1": 95.0,
|
| 134 |
+
"fpr_1": 98.0,
|
| 135 |
+
"fpr_5": 99.71
|
| 136 |
+
},
|
| 137 |
+
"Show_1": {
|
| 138 |
+
"fpr_0.1": 97.86,
|
| 139 |
+
"fpr_1": 99.71,
|
| 140 |
+
"fpr_5": 100.0
|
| 141 |
+
},
|
| 142 |
+
"Sora": {
|
| 143 |
+
"fpr_0.1": 91.07,
|
| 144 |
+
"fpr_1": 98.21,
|
| 145 |
+
"fpr_5": 100.0
|
| 146 |
+
},
|
| 147 |
+
"WildScrape": {
|
| 148 |
+
"fpr_0.1": 73.27,
|
| 149 |
+
"fpr_1": 79.74,
|
| 150 |
+
"fpr_5": 86.42
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
"our_per_generator": {
|
| 154 |
+
"Crafter": {
|
| 155 |
+
"fpr_0.1": 100.0,
|
| 156 |
+
"fpr_1": 100.0,
|
| 157 |
+
"fpr_5": 100.0
|
| 158 |
+
},
|
| 159 |
+
"Gen2": {
|
| 160 |
+
"fpr_0.1": 100.0,
|
| 161 |
+
"fpr_1": 100.0,
|
| 162 |
+
"fpr_5": 100.0
|
| 163 |
+
},
|
| 164 |
+
"HotShot": {
|
| 165 |
+
"fpr_0.1": 100.0,
|
| 166 |
+
"fpr_1": 100.0,
|
| 167 |
+
"fpr_5": 100.0
|
| 168 |
+
},
|
| 169 |
+
"Lavie": {
|
| 170 |
+
"fpr_0.1": 99.93,
|
| 171 |
+
"fpr_1": 99.93,
|
| 172 |
+
"fpr_5": 100.0
|
| 173 |
+
},
|
| 174 |
+
"ModelScope": {
|
| 175 |
+
"fpr_0.1": 99.57,
|
| 176 |
+
"fpr_1": 99.86,
|
| 177 |
+
"fpr_5": 100.0
|
| 178 |
+
},
|
| 179 |
+
"MoonValley": {
|
| 180 |
+
"fpr_0.1": 100.0,
|
| 181 |
+
"fpr_1": 100.0,
|
| 182 |
+
"fpr_5": 100.0
|
| 183 |
+
},
|
| 184 |
+
"MorphStudio": {
|
| 185 |
+
"fpr_0.1": 100.0,
|
| 186 |
+
"fpr_1": 100.0,
|
| 187 |
+
"fpr_5": 100.0
|
| 188 |
+
},
|
| 189 |
+
"Show_1": {
|
| 190 |
+
"fpr_0.1": 100.0,
|
| 191 |
+
"fpr_1": 100.0,
|
| 192 |
+
"fpr_5": 100.0
|
| 193 |
+
},
|
| 194 |
+
"Sora": {
|
| 195 |
+
"fpr_0.1": 100.0,
|
| 196 |
+
"fpr_1": 100.0,
|
| 197 |
+
"fpr_5": 100.0
|
| 198 |
+
},
|
| 199 |
+
"WildScrape": {
|
| 200 |
+
"fpr_0.1": 89.94,
|
| 201 |
+
"fpr_1": 98.92,
|
| 202 |
+
"fpr_5": 99.78
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"dataset_source": "genvideo loader (ModelScope cccnju/Gen-Video GenVideo-Val.zip); reals = MSR-VTT."
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"name": "FakeParts (partial edits; Tables 1, 15-16)",
|
| 209 |
+
"datasets": [
|
| 210 |
+
"FakeParts"
|
| 211 |
+
],
|
| 212 |
+
"metric": "fake recall @ FPR",
|
| 213 |
+
"paper_number": {
|
| 214 |
+
"fake_recall_fpr0.1": 74.45
|
| 215 |
+
},
|
| 216 |
+
"our_status": "missing",
|
| 217 |
+
"dataset_source": "not in SynthScope yet"
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"name": "ViF-Bench (Tables 3, 19)",
|
| 221 |
+
"datasets": [
|
| 222 |
+
"ViF-Bench"
|
| 223 |
+
],
|
| 224 |
+
"metric": "fake recall @ FPR",
|
| 225 |
+
"paper_number": {
|
| 226 |
+
"fake_recall_fpr0.1": 84.52
|
| 227 |
+
},
|
| 228 |
+
"our_status": "missing",
|
| 229 |
+
"dataset_source": "not in SynthScope yet"
|
| 230 |
+
}
|
| 231 |
+
],
|
| 232 |
+
"summary": "Training-free; exact parity with the official model. The official pipeline on our GenVideo copy reproduces the paper's MSR-VTT-calibrated table; SynthScope's direct decoding (no JPEG round-trip) is much stronger at low FPR."
|
| 233 |
+
}
|
repro/official_evals/split_official_eval.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the OFFICIAL SPLIT pipeline (Hyun & Kim, ECCV 2026; github.com/mldljyh/SPLIT) on a SynthScope video dataset.
|
| 2 |
+
|
| 3 |
+
SynthScope's SPLIT gives the official ``SPLIT_model`` scores on the same frames (relative difference ~1e-6), but the
|
| 4 |
+
official numbers also depend on the official frame extraction, which SPLIT shares with D3: ``utils/video2frame.py``
|
| 5 |
+
writes JPEG frames with ffmpeg (8 fps over 3 s, random start for longer clips), ``data/datasets.py`` reads them back
|
| 6 |
+
with ``cv2.imread`` (BGR), crops 10% of the longer side, resizes to 224 and normalizes with ImageNet statistics. The
|
| 7 |
+
frames are extracted exactly as ``d3_official_eval.py`` does (same seeded start, same frame folders, so the two share a
|
| 8 |
+
cache), and the metrics come from the official ``eval.py``'s ``evaluate_split`` unchanged: fake recall at FPR 0.1 / 1 /
|
| 9 |
+
5 % with the threshold set on the real scores alone (``np.quantile(real, 1 - fpr, method="higher")``).
|
| 10 |
+
|
| 11 |
+
Like the paper's GenVideo table, every real clip (GenVideo-Val's MSR-VTT reals) calibrates the threshold and every fake
|
| 12 |
+
clip is scored, per generator and pooled over all generators ("Overall"); there is no per-class limit unless ``--limit``
|
| 13 |
+
is given.
|
| 14 |
+
|
| 15 |
+
python repro/official_evals/split_official_eval.py --dataset-root /data/GenVideo/test --name genvideo \\
|
| 16 |
+
--official-repo /path/to/SPLIT --albumentations-path /path/to/site-packages --ffmpeg /path/to/ffmpeg \\
|
| 17 |
+
--frames /scratch/d3_frames/genvideo
|
| 18 |
+
|
| 19 |
+
Output: repro/bench_results/<tag>.json and a line in repro/MASTER_RESULTS.jsonl.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import datetime
|
| 25 |
+
import hashlib
|
| 26 |
+
import importlib.util
|
| 27 |
+
import json
|
| 28 |
+
import random
|
| 29 |
+
import sys
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
|
| 32 |
+
import numpy as np
|
| 33 |
+
import torch
|
| 34 |
+
|
| 35 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 36 |
+
from d3_official_eval import extract, videos # noqa: E402 (the shared official frame extraction)
|
| 37 |
+
|
| 38 |
+
REPRO = Path(__file__).resolve().parents[1]
|
| 39 |
+
RESULTS_LOG = REPRO / "MASTER_RESULTS.jsonl"
|
| 40 |
+
FPRS = (0.001, 0.01, 0.05)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def load_official_eval(repo: str):
|
| 44 |
+
"""The official ``eval.py`` as a module (its ``evaluate_split`` computes the paper's metrics)."""
|
| 45 |
+
spec = importlib.util.spec_from_file_location("split_official_eval_py", Path(repo) / "eval.py")
|
| 46 |
+
module = importlib.util.module_from_spec(spec)
|
| 47 |
+
spec.loader.exec_module(module)
|
| 48 |
+
return module
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@torch.no_grad()
|
| 52 |
+
def scores(model, read_video, trans, paths, frames_root: Path, ffmpeg: str, device) -> list[float]:
|
| 53 |
+
out = []
|
| 54 |
+
for p in paths:
|
| 55 |
+
folder = extract(p, frames_root / hashlib.md5(str(p.resolve()).encode()).hexdigest()[:16], ffmpeg)
|
| 56 |
+
try:
|
| 57 |
+
x = read_video(str(folder), trans=trans).unsqueeze(0).to(device) # official reader: needs >= 8 frames
|
| 58 |
+
except Exception as e: # noqa: BLE001
|
| 59 |
+
print(f" skip {p.name}: {e}")
|
| 60 |
+
continue
|
| 61 |
+
out.append(float(model.forward_score(x).item()))
|
| 62 |
+
return out
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def summarize(official, real: list[float], fake: list[float]) -> dict:
|
| 66 |
+
m = official.evaluate_split(np.array(real), np.array(fake))
|
| 67 |
+
return {"fake_recall_at_fpr": {f"{a * 100:g}": round(m["fake_recall_at_fpr"][a] * 100, 2) for a in FPRS},
|
| 68 |
+
"ap": round(m["ap_fake"] * 100, 2), "ap_real": round(m["ap_real"] * 100, 2),
|
| 69 |
+
"auroc": round(m["roc_auc"] * 100, 2), "n_real": len(real), "n_fake": len(fake)}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def main(argv=None) -> int:
|
| 73 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 74 |
+
ap.add_argument("--dataset-root", required=True, help="<generator>/{0_real,1_fake} tree")
|
| 75 |
+
ap.add_argument("--name", required=True, help="dataset name for the tag, e.g. genvideo")
|
| 76 |
+
ap.add_argument("--official-repo", required=True, help="clone of github.com/mldljyh/SPLIT")
|
| 77 |
+
ap.add_argument("--albumentations-path", default=None, help="extra sys.path entry providing albumentations")
|
| 78 |
+
ap.add_argument("--ffmpeg", default="ffmpeg")
|
| 79 |
+
ap.add_argument("--frames", required=True, help="scratch folder for the extracted JPEG frames")
|
| 80 |
+
ap.add_argument("--limit", type=int, default=0, help="clips per class per generator (0 = all)")
|
| 81 |
+
ap.add_argument("--encoder", default="XCLIP-16")
|
| 82 |
+
ap.add_argument("--select", default=None, help="comma list: only these generators")
|
| 83 |
+
ap.add_argument("--tag", default=None)
|
| 84 |
+
args = ap.parse_args(argv)
|
| 85 |
+
|
| 86 |
+
sys.path[:0] = [args.official_repo] + ([args.albumentations_path] if args.albumentations_path else [])
|
| 87 |
+
from data.datasets import read_video, set_preprocessing # official SPLIT
|
| 88 |
+
from models.SPLIT_model import SPLIT_model # official SPLIT
|
| 89 |
+
official = load_official_eval(args.official_repo)
|
| 90 |
+
|
| 91 |
+
random.seed(42); np.random.seed(42); torch.manual_seed(42)
|
| 92 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 93 |
+
model = SPLIT_model(encoder_type=args.encoder, loss_type="l2").to(device).eval()
|
| 94 |
+
trans = set_preprocessing(None, None)
|
| 95 |
+
root, frames_root = Path(args.dataset_root), Path(args.frames)
|
| 96 |
+
tag = args.tag or f"split_official_{args.name}"
|
| 97 |
+
cap = (lambda xs: xs[:args.limit]) if args.limit else (lambda xs: xs)
|
| 98 |
+
gens = sorted(p for p in root.iterdir() if p.is_dir()
|
| 99 |
+
and (not args.select or p.name in args.select.split(",")))
|
| 100 |
+
print(f"=== split_official_eval {tag} | {len(gens)} generators | {args.encoder} | device={device} ===", flush=True)
|
| 101 |
+
|
| 102 |
+
cache: dict = {}
|
| 103 |
+
per_gen, all_real, all_fake = {}, None, []
|
| 104 |
+
for g in gens:
|
| 105 |
+
reals, fakes = cap(videos(g / "0_real")), cap(videos(g / "1_fake"))
|
| 106 |
+
if not reals or not fakes:
|
| 107 |
+
continue
|
| 108 |
+
key = tuple(p.name for p in reals) # GenVideo copies the same MSR-VTT reals into every generator folder
|
| 109 |
+
if key not in cache:
|
| 110 |
+
cache[key] = scores(model, read_video, trans, reals, frames_root, args.ffmpeg, device)
|
| 111 |
+
rs = cache[key]
|
| 112 |
+
fs = scores(model, read_video, trans, fakes, frames_root, args.ffmpeg, device)
|
| 113 |
+
per_gen[g.name] = summarize(official, rs, fs)
|
| 114 |
+
all_real = rs if all_real is None or len(rs) > len(all_real) else all_real
|
| 115 |
+
all_fake += fs
|
| 116 |
+
print(f" {g.name}: {len(rs)} real + {len(fs)} fake fake recall @ FPR 0.1/1/5% = "
|
| 117 |
+
f"{list(per_gen[g.name]['fake_recall_at_fpr'].values())} AP={per_gen[g.name]['ap']}", flush=True)
|
| 118 |
+
|
| 119 |
+
overall = summarize(official, all_real, all_fake)
|
| 120 |
+
mean = lambda f: round(sum(v["fake_recall_at_fpr"][f] for v in per_gen.values()) / len(per_gen), 2) # noqa: E731
|
| 121 |
+
means = {"overall_fake_recall_at_fpr": overall["fake_recall_at_fpr"],
|
| 122 |
+
"mean_fake_recall_at_fpr": {f: mean(f) for f in overall["fake_recall_at_fpr"]},
|
| 123 |
+
"map": round(sum(v["ap"] for v in per_gen.values()) / len(per_gen), 2)}
|
| 124 |
+
result = {"tag": tag, "model": "split", "ckpt": f"official SPLIT code ({args.encoder}), training-free",
|
| 125 |
+
"benchmark": "video:official-split", "data_root": str(root), "n_generators": len(per_gen),
|
| 126 |
+
"means": means, "overall": overall, "per_generator": per_gen,
|
| 127 |
+
"date": datetime.datetime.now().isoformat(timespec="seconds")}
|
| 128 |
+
out = REPRO / "bench_results" / f"{tag}.json"
|
| 129 |
+
out.write_text(json.dumps(result, indent=2) + "\n")
|
| 130 |
+
with RESULTS_LOG.open("a") as f:
|
| 131 |
+
f.write(json.dumps({k: result[k] for k in ("date", "tag", "model", "ckpt", "benchmark", "data_root",
|
| 132 |
+
"n_generators", "means")}) + "\n")
|
| 133 |
+
print(f"\n=== RESULT {tag}: Overall fake recall @ FPR 0.1/1/5% = {list(overall['fake_recall_at_fpr'].values())} "
|
| 134 |
+
f"over {len(per_gen)} generators -> {out} ===", flush=True)
|
| 135 |
+
return 0
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
raise SystemExit(main())
|
tests/test_split_smoke.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke test for the SPLIT integration (no weight downloads: a tiny randomly initialized X-CLIP vision tower stands in
|
| 2 |
+
for X-CLIP B/16, saved to a temporary directory and loaded through the model's normal ``from_pretrained`` path).
|
| 3 |
+
|
| 4 |
+
Checks, on CPU:
|
| 5 |
+
* temporal roughness: a straight constant-speed token trajectory gives TTR = 0, a back-and-forth (jittery) one gives
|
| 6 |
+
TTR > 0, matching the paper's definition log2(L1 / L2),
|
| 7 |
+
* motion incoherence: a motion field that is the same for every patch gives 0, a varying one gives > 0,
|
| 8 |
+
* the registry entry is training-free and uses D3's video preprocessing (16 frames, 224, BGR, ImageNet normalization),
|
| 9 |
+
* ``forward`` -> [B, 2] logits whose margin is ``score - threshold``; ``compute_loss`` returns a zero loss that
|
| 10 |
+
backpropagates (the trainer runs, nothing is learned) and the encoder stays frozen in train mode.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import tempfile
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
from omegaconf import OmegaConf
|
| 21 |
+
from transformers import XCLIPVisionConfig, XCLIPVisionModel
|
| 22 |
+
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 24 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "interface"))
|
| 25 |
+
|
| 26 |
+
from models.video.split import split as split_module # noqa: E402
|
| 27 |
+
from models.video.split.split import SynthScopeSPLITModel # noqa: E402
|
| 28 |
+
from backend.registry import get_spec # noqa: E402
|
| 29 |
+
|
| 30 |
+
TINY = XCLIPVisionConfig(hidden_size=32, intermediate_size=64, num_hidden_layers=2, num_attention_heads=2,
|
| 31 |
+
image_size=64, patch_size=16, mit_hidden_size=32, mit_intermediate_size=64,
|
| 32 |
+
mit_num_hidden_layers=1, mit_num_attention_heads=2, num_frames=4)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def build_model(tmp: str) -> SynthScopeSPLITModel:
|
| 36 |
+
XCLIPVisionModel(TINY).save_pretrained(tmp)
|
| 37 |
+
split_module.ENCODERS["tiny"] = tmp
|
| 38 |
+
cfg = OmegaConf.create({"ModelConfig": {"model_type": "split", "encoder": "tiny", "gamma": 8.0, "threshold": 0.5},
|
| 39 |
+
"TrainerConfig": {}})
|
| 40 |
+
return SynthScopeSPLITModel(cfg)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_temporal_roughness() -> None:
|
| 44 |
+
print("[A] temporal roughness ...")
|
| 45 |
+
t, n, d = 8, 4, 16
|
| 46 |
+
direction = torch.randn(n, d)
|
| 47 |
+
smooth = torch.arange(t, dtype=torch.float32)[None, :, None, None] * direction[None, None] # [1, T, N, D]
|
| 48 |
+
ttr = SynthScopeSPLITModel.temporal_roughness(smooth)
|
| 49 |
+
assert ttr.shape == (1,) and abs(float(ttr)) < 1e-4, float(ttr)
|
| 50 |
+
jitter = smooth.clone()
|
| 51 |
+
jitter[:, 1::2] += 5 * torch.randn(n, d) # odd frames jump away and back: long 1-step, short 2-step paths
|
| 52 |
+
assert float(SynthScopeSPLITModel.temporal_roughness(jitter)) > 0.5
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_motion_incoherence() -> None:
|
| 56 |
+
print("[B] motion incoherence ...")
|
| 57 |
+
t, g, d = 5, 3, 8
|
| 58 |
+
uniform = torch.arange(t, dtype=torch.float32)[None, :, None, None] * torch.randn(d)[None, None, None]
|
| 59 |
+
uniform = uniform.expand(1, t, g * g, d) # every patch moves by the same vector
|
| 60 |
+
assert float(SynthScopeSPLITModel.motion_incoherence(uniform)) < 1e-5
|
| 61 |
+
assert float(SynthScopeSPLITModel.motion_incoherence(torch.randn(1, t, g * g, d))) > 0.1
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_registry() -> None:
|
| 65 |
+
print("[C] registry entry ...")
|
| 66 |
+
spec = get_spec("split")
|
| 67 |
+
assert spec.modality == "video" and spec.checkpoint is None and spec.weights == "training_free"
|
| 68 |
+
assert spec.num_frames == 16 and spec.frame_size == 224 and spec.bgr and spec.edge_crop == 0.1
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_forward_and_loss(model: SynthScopeSPLITModel) -> None:
|
| 72 |
+
print("[D] forward / compute_loss ...")
|
| 73 |
+
frames = torch.randn(2, 6, 3, 64, 64)
|
| 74 |
+
model.eval()
|
| 75 |
+
with torch.no_grad():
|
| 76 |
+
logits, score = model(frames), model.score(frames)
|
| 77 |
+
assert logits.shape == (2, 2) and torch.isfinite(logits).all() and (score >= 0).all()
|
| 78 |
+
assert torch.allclose(logits[:, 1] - logits[:, 0], 2 * (score - model.threshold))
|
| 79 |
+
model.train()
|
| 80 |
+
assert not model.encoder.training and not any(p.requires_grad for p in model.encoder.parameters())
|
| 81 |
+
out = model.compute_loss((frames, torch.tensor([0, 1]), torch.tensor([0, 0])))
|
| 82 |
+
out["loss"].backward()
|
| 83 |
+
assert out["loss"].item() == 0.0 and model._noop.grad is not None
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
test_temporal_roughness()
|
| 88 |
+
test_motion_incoherence()
|
| 89 |
+
test_registry()
|
| 90 |
+
with tempfile.TemporaryDirectory() as d:
|
| 91 |
+
test_forward_and_loss(build_model(d))
|
| 92 |
+
print("SPLIT smoke test passed")
|
tests/test_video_gif_decode.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression test: animated GIFs decode to their real frames, not a black clip.
|
| 2 |
+
|
| 3 |
+
decord cannot open GIFs ("broken metadata"), and ``sample_video_frames`` used to return an all-zero clip for them, so
|
| 4 |
+
every video detector saw GIFs (e.g. 282 of GenVideo's WildScrape fakes) as black, motionless video. The PIL fallback
|
| 5 |
+
must return the GIF's own frames at the requested sampling rate.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import tempfile
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
from PIL import Image
|
| 16 |
+
|
| 17 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 18 |
+
|
| 19 |
+
from dataset import sample_video_frames # noqa: E402
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def make_gif(path: str, n: int = 24, duration_ms: int = 100) -> None:
|
| 23 |
+
"""``n`` frames at 10 fps (GIF stores durations in 10 ms steps); frame k is a solid gray of level 10 * k."""
|
| 24 |
+
frames = [Image.fromarray(np.full((64, 96, 3), 10 * k, dtype=np.uint8)) for k in range(n)]
|
| 25 |
+
frames[0].save(path, save_all=True, append_images=frames[1:], duration=duration_ms, loop=0)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_gif_frames() -> None:
|
| 29 |
+
with tempfile.TemporaryDirectory() as d:
|
| 30 |
+
path = os.path.join(d, "clip.gif")
|
| 31 |
+
make_gif(path)
|
| 32 |
+
clip = sample_video_frames(path, num_frames=16, fps=10.0, size=32, fps_nearest=True)
|
| 33 |
+
assert clip.shape == (16, 3, 32, 32), clip.shape
|
| 34 |
+
assert float(clip.max()) > 0, "GIF decoded as a black clip"
|
| 35 |
+
levels = (clip[:, 0, 0, 0] * 255).round().tolist()
|
| 36 |
+
assert levels == [10.0 * k for k in range(16)], levels # source fps = target fps: frames 0..15 in order
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
test_gif_frames()
|
| 41 |
+
print("GIF decode test passed")
|
trainer.py
CHANGED
|
@@ -18,6 +18,7 @@ from models.image.effort.effort import SynthScopeEffortModel
|
|
| 18 |
from models.image.bfree.bfree import SynthScopeBFreeModel
|
| 19 |
from models.image.d3qe.d3qe import SynthScopeD3QEModel
|
| 20 |
from models.video.d3.d3 import SynthScopeD3Model
|
|
|
|
| 21 |
from models.video.restrav.restrav import SynthScopeReStraVModel
|
| 22 |
from models.video.stall.stall import SynthScopeSTALLModel
|
| 23 |
from metrics import AIGIMetrics
|
|
@@ -105,6 +106,8 @@ class SynthScopeModelTrainer:
|
|
| 105 |
return SynthScopeD3QEModel(config)
|
| 106 |
if model_type == "d3":
|
| 107 |
return SynthScopeD3Model(config)
|
|
|
|
|
|
|
| 108 |
if model_type == "restrav":
|
| 109 |
return SynthScopeReStraVModel(config)
|
| 110 |
if model_type == "stall":
|
|
|
|
| 18 |
from models.image.bfree.bfree import SynthScopeBFreeModel
|
| 19 |
from models.image.d3qe.d3qe import SynthScopeD3QEModel
|
| 20 |
from models.video.d3.d3 import SynthScopeD3Model
|
| 21 |
+
from models.video.split.split import SynthScopeSPLITModel
|
| 22 |
from models.video.restrav.restrav import SynthScopeReStraVModel
|
| 23 |
from models.video.stall.stall import SynthScopeSTALLModel
|
| 24 |
from metrics import AIGIMetrics
|
|
|
|
| 106 |
return SynthScopeD3QEModel(config)
|
| 107 |
if model_type == "d3":
|
| 108 |
return SynthScopeD3Model(config)
|
| 109 |
+
if model_type == "split":
|
| 110 |
+
return SynthScopeSPLITModel(config)
|
| 111 |
if model_type == "restrav":
|
| 112 |
return SynthScopeReStraVModel(config)
|
| 113 |
if model_type == "stall":
|