import React, { useEffect, useRef, useState } from "react"; import { fetchModels, predict, fetchDatasets, sampleDataset, datasetMediaUrl, predictDataset, scoreBatch, verdictAt, autoCalibrate, targetFprThreshold, batchStats, sotaGenerators, sotaRealImage, sotaCompare, fetchLeaderboard, fetchGeneratorBenchmarks, } from "./api.js"; // Which weights back the default inference checkpoint for each detector (shown in the UI). const WEIGHTS_LABEL = { official: "official weights", synthscope_trained: "SynthScope-trained", training_free: "training-free", }; const wlabel = (m) => WEIGHTS_LABEL[m && m.weights] || "SynthScope-trained"; const REPRO_LEGEND = (
reproduced = SynthScope-trained checkpoint matches the paper · official = official weights loaded into the SynthScope class match (SynthScope-trained shown alongside where trained from scratch) · below = gap remains · reference = paper reports no comparable number · missing = not yet evaluated.
); function LeaderboardView() { const [groups, setGroups] = useState([]); const [sel, setSel] = useState(""); const [err, setErr] = useState(""); useEffect(() => { fetchLeaderboard().then((d) => setGroups(d.groups || [])).catch((e) => setErr(e.message)); }, []); const shown = sel ? groups.filter((g) => g.benchmark === sel) : groups; return (

Every benchmark each paper reports, with the paper number, our SynthScope-trained checkpoint, and official weights loaded into the SynthScope class. Grouped by benchmark so methods that share one can be compared head-to-head (sorted best-first).

{err &&
{err}
} {shown.map((g) => (

{g.benchmark} {g.n_methods} method{g.n_methods > 1 ? "s" : ""}

{g.rows.map((r, i) => ( ))}
MethodPaperSynthScope (trained)Official wtsStatus
{r.method}
{r.benchmark}
{r.paper}{r.synthscope || "—"}{r.official || "—"} {r.status}
))} {REPRO_LEGEND}
); } function TopGeneratorsView() { const [bm, setBm] = useState([]); const [err, setErr] = useState(""); useEffect(() => { fetchGeneratorBenchmarks().then((d) => setBm(d.benchmarks || [])).catch((e) => setErr(e.message)); }, []); const medal = (i) => (i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : i + 1); return (

How our detectors hold up against today's SOTA image generators. Each: a balanced real-vs-generated set scored by every SynthScope detector (promoted checkpoints), ranked by AUROC. None of the detectors were trained on these generators — cross-generator generalization.

{err &&
{err}
} {!bm.length && !err &&
No generator benchmarks yet.
} {bm.map((b) => (

vs {b.generator} {b.n_real}+{b.n_fake} imgs · {b.settings}

{b.note}
{b.rows.map((r, i) => ( ))}
#DetectorAUROCAPacc@.5fake-detect %real-FP %
{medal(i)}{r.name || r.detector} {r.auroc} {r.ap}{r.acc}{r.fake_detect_rate}{r.real_false_pos}
))}
); } function b64ToFile(b64, name) { const bin = atob(b64), arr = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); return new File([arr], name, { type: "image/png" }); } // SOTA generator vs detector comparison: pick/upload a real image -> a current SOTA generator makes a // "conceptually similar" image -> the selected SynthScope detectors score it (best/promoted checkpoints). function SotaView({ models }) { const [gens, setGens] = useState([]); const [gen, setGen] = useState(""); const [prompt, setPrompt] = useState("make another image conceptually similar to this"); const [realB64, setRealB64] = useState(""); const [realSrc, setRealSrc] = useState(null); const [picked, setPicked] = useState({}); const [busy, setBusy] = useState(false); const [res, setRes] = useState(null); const [err, setErr] = useState(""); const imgDetectors = (models || []).filter((m) => m.modality === "image" && m.available); useEffect(() => { sotaGenerators().then((d) => { setGens(d.generators || []); const a = (d.generators || []).find((g) => g.available); if (a) setGen(a.key); if (d.default_prompt) setPrompt(d.default_prompt); }).catch((e) => setErr(e.message)); }, []); async function rollImage() { setErr(""); setRes(null); try { const d = await sotaRealImage(); setRealB64(d.image_b64); setRealSrc(d.source); } catch (e) { setErr(e.message); } } function onUpload(f) { if (!f) return; const rd = new FileReader(); rd.onload = () => { setRealB64(rd.result.split(",")[1]); setRealSrc(["upload", f.name]); setRes(null); }; rd.readAsDataURL(f); } async function run() { setErr(""); setBusy(true); setRes(null); try { const dets = Object.keys(picked).filter((k) => picked[k]); if (!realB64) throw new Error("Pick or upload a real image first."); if (!gen) throw new Error("Select a generator."); if (!dets.length) throw new Error("Select at least one detector."); const d = await sotaCompare(b64ToFile(realB64, "real.png"), gen, dets, prompt); setRes(d); } catch (e) { setErr(e.message); } finally { setBusy(false); } } return (

Send a real image from our datasets to a current SOTA generator, then see whether the SynthScope detectors catch the freshly-generated image. Generators need an API key (greyed-out = key not set on the server).

Detectors: {imgDetectors.map((m) => ( ))}
{err &&
{err}
}
{realB64 &&
real
Real {realSrc && "· " + realSrc.join("/")}
} {res && res.generated_b64 &&
generated
Generated · {res.generator}{res.prompt && <>
prompt: {res.prompt}}
}
{res && ( {res.results.map((r, i) => ( ))}
DetectorP(fake) realP(fake) generatedCaught?
{r.name || r.detector} {r.error ? "—" : r.real_fake_prob} {r.error ? "—" : r.gen_fake_prob} {r.error ? r.error : (r.caught ? "✓ fake" : "✗ missed")}
)}
); } const isVideoName = (name = "", type = "") => type.startsWith("video") || /\.(mp4|mov|avi|mkv|webm|m4v)$/i.test(name); const optLabel = (m) => `${m.name} — ${m.paper} · ${wlabel(m)}${m.available ? "" : " (no checkpoint)"}`; function ModelSelect({ models, model, setModel, onChange }) { const img = models.filter((m) => m.modality === "image"); const vid = models.filter((m) => m.modality === "video"); return ( ); } function ReproTable({ m }) { const rep = m && m.repro; const full = m && m.repro_full; if (!rep && !(full && full.length)) return null; return (
{rep && (
Inference weights{wlabel(m)}
Headline{rep.benchmark}
Paper{rep.paper}
Reproduced{rep.ours}
)} {full && full.length > 0 && (
All {full.length} reported benchmarks — paper vs SynthScope vs official
{full.map((r, i) => ( ))}
BenchmarkPaperSynthScope (trained)Official wtsStatus
{r.benchmark} {r.paper} {r.ours_synthscope || "—"} {r.ours_official || "—"} {r.status}
reproduced = SynthScope-trained checkpoint matches the paper · official = official weights loaded into the SynthScope class match (SynthScope-trained number shown where we trained from scratch) · below = gap remains · reference = paper reports no comparable number · missing = not yet evaluated.
)}
); } function ThresholdSlider({ threshold, setThreshold, children }) { return (
Decision thresholdP(fake) ≥ {threshold.toFixed(2)} → fake
setThreshold(parseFloat(e.target.value))} /> {children}
); } export default function App() { const [models, setModels] = useState([]); const [device, setDevice] = useState(""); const [model, setModel] = useState(""); const [tab, setTab] = useState("detect"); const [threshold, setThreshold] = useState(0.5); const [error, setError] = useState(""); useEffect(() => { fetchModels().then((d) => { setModels(d.models || []); setDevice(d.device || ""); const first = (d.models || []).find((m) => m.available); if (first) setModel(first.key); }).catch((e) => setError("Could not reach backend: " + e.message)); }, []); const shared = { models, model, setModel, threshold, setThreshold }; return (

SynthScope — AI-Generated Media Detector

Detect AI-generated media, or explore the datasets each detector was trained / tested on. {device && device: {device}}

{error &&
{error}
} {tab === "detect" && } {tab === "explore" && } {tab === "sota" && } {tab === "leaderboard" && } {tab === "topgen" && }
); } function DetectView({ models, model, setModel, threshold, setThreshold }) { const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(""); const [isVideo, setIsVideo] = useState(false); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(""); const [drag, setDrag] = useState(false); const inputRef = useRef(); const selected = models.find((m) => m.key === model); function onPick(f) { if (!f) return; setFile(f); setResult(null); setError(""); setIsVideo(isVideoName(f.name, f.type)); if (previewUrl) URL.revokeObjectURL(previewUrl); setPreviewUrl(URL.createObjectURL(f)); } async function run() { if (!file || !model) return; setBusy(true); setResult(null); setError(""); try { setResult(await predict(file, model)); } catch (e) { setError(e.message); } finally { setBusy(false); } } return (

Input

setResult(null)} /> {selected && (
{selected.notes} {selected.modality === "image" ? " · On a video, runs frame-by-frame and averages." : " · Native video model."}
)} {selected && }
inputRef.current.click()} onDragOver={(e) => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); onPick(e.dataTransfer.files[0]); }}> {file ? {file.name} ({(file.size / 1048576).toFixed(2)} MB) : "Click or drop an image / video here"}
onPick(e.target.files[0])} /> {previewUrl &&
{isVideo ?
} {error &&
{error}
}

Result

{!result && !busy &&
No result yet.
} {busy &&
Running {selected ? selected.name : ""}… first run loads the model and may take a while.
} {result && } {result && }
); } function ExploreView({ models, model, setModel, threshold, setThreshold }) { const [datasets, setDatasets] = useState([]); const [trainMap, setTrainMap] = useState({}); const [dataset, setDataset] = useState(""); const [split, setSplit] = useState(""); const [seed, setSeed] = useState(1); const [items, setItems] = useState([]); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [sel, setSel] = useState(null); const [preds, setPreds] = useState({}); const [scoring, setScoring] = useState(false); const [fpr, setFpr] = useState(5); const batchN = 24; const curDs = datasets.find((d) => d.key === dataset); const trainDsKey = (trainMap[model] || {}).dataset; useEffect(() => { fetchDatasets().then((d) => { setDatasets(d.datasets || []); setTrainMap(d.model_train || {}); const first = (d.datasets || [])[0]; if (first) { setDataset(first.key); setSplit(first.splits[first.splits.length - 1] || "."); } }).catch((e) => setError("Could not load datasets: " + e.message)); }, []); useEffect(() => { if (!curDs) return; const s = curDs.splits.includes(split) ? split : curDs.splits[curDs.splits.length - 1] || "."; if (s !== split) setSplit(s); loadBatch(dataset, s, seed); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataset]); async function loadBatch(dsKey, sp, sd) { if (!dsKey) return; setBusy(true); setError(""); setItems([]); setSel(null); setPreds({}); try { setItems((await sampleDataset(dsKey, sp, batchN, sd)).items || []); } catch (e) { setError(e.message); } finally { setBusy(false); } } function newBatch() { const sd = seed + 1; setSeed(sd); loadBatch(dataset, split, sd); } async function pickItem(it) { setSel(it.rel); if (!model || preds[it.rel]) return; try { setPreds((p) => ({ ...p, [it.rel]: await predictDataset(dataset, it.rel, model) })); } catch (e) { setPreds((p) => ({ ...p, [it.rel]: { error: e.message } })); } } async function scoreAll() { if (!model || !items.length) return []; setScoring(true); setError(""); try { const d = await scoreBatch(dataset, model, items.map((i) => i.rel)); const m = {}; for (const it of d.items) m[it.rel] = it; setPreds((p) => ({ ...p, ...m })); return d.items.filter((x) => x.ground_truth && typeof x.fake_probability === "number") .map((x) => ({ p: x.fake_probability, gt: x.ground_truth })); } catch (e) { setError(e.message); return []; } finally { setScoring(false); } } const scored = items.map((i) => preds[i.rel]) .filter((x) => x && typeof x.fake_probability === "number" && x.ground_truth) .map((x) => ({ p: x.fake_probability, gt: x.ground_truth })); async function calibrate() { const s = scored.length ? scored : await scoreAll(); if (s.length) setThreshold(autoCalibrate(s)); } async function applyFpr() { const s = scored.length ? scored : await scoreAll(); const t = targetFprThreshold(s, fpr / 100); if (t != null) setThreshold(t); else setError("Need scored real videos for target-FPR."); } const selItem = items.find((i) => i.rel === sel); const selRes = sel ? preds[sel] : null; const stats = batchStats(scored, threshold); const mediaUrl = (it, thumb) => datasetMediaUrl(dataset, it.rel, thumb); return (

Datasets

{trainDsKey && (
Trained on {(datasets.find((d) => d.key === trainDsKey) || {}).name || trainDsKey} {(trainMap[model] || {}).official && official wts}
)} m.key === model)} />
setFpr(parseFloat(e.target.value) || 0)} />%
{stats && (
batch ({stats.n} scored) @{threshold.toFixed(2)}: acc {(stats.acc * 100).toFixed(0)}% {stats.racc != null ? ` · real ${(stats.racc * 100).toFixed(0)}%` : ""} {stats.facc != null ? ` · fake ${(stats.facc * 100).toFixed(0)}%` : ""}
)}
{error &&
{error}
} {busy &&
Sampling…
}
{items.map((it) => { const pr = preds[it.rel]; const has = pr && typeof pr.fake_probability === "number"; const v = has ? verdictAt(pr.fake_probability, threshold) : null; const ok = has && v === it.label; return (
pickItem(it)} title={it.name}> {it.label} {it.modality === "video" && ▶} {has && {(pr.fake_probability * 100).toFixed(0)}% {ok ? "✓" : "✗"}}
); })}

Model output

{!selItem &&
Click a sample to run {(models.find((m) => m.key === model) || {}).name || "the model"} on it. Use "Score batch" + Auto-calibrate / Target FPR to set the threshold.
} {selItem && (
{selItem.modality === "video" ?
{selItem.name} · ground truth: {selItem.label}
{selRes && selRes.error &&
{selRes.error}
} {selRes && !selRes.error && typeof selRes.fake_probability === "number" && }
)}
); } function Result({ r, threshold, gt }) { const fakePct = r.fake_probability * 100, realPct = (1 - r.fake_probability) * 100; const verdict = verdictAt(r.fake_probability, threshold); const correct = gt ? verdict === gt : null; return (
{verdict === "fake" ? "AI-GENERATED" : "REAL"}
P(fake) {fakePct.toFixed(1)}% vs threshold {threshold.toFixed(2)} {correct != null && <> · {correct ? "correct" : "wrong"}}
{fakePct >= 12 ? `fake ${fakePct.toFixed(0)}%` : ""} {realPct >= 12 ? `real ${realPct.toFixed(0)}%` : ""}
{r.model_name && } {r.media_type && } {r.elapsed_ms && }
{r.per_frame && r.per_frame.length > 1 && (
Per-frame P(fake) — {r.per_frame.length} frames sampled (dashed line = 50% threshold):
{r.per_frame.map((v, i) => ( = 0.5 ? "var(--fake)" : "var(--accent)" }} /> ))}
min {(Math.min(...r.per_frame) * 100).toFixed(0)}% mean {(r.per_frame.reduce((a, b) => a + b, 0) / r.per_frame.length * 100).toFixed(0)}% max {(Math.max(...r.per_frame) * 100).toFixed(0)}%
)} {r.interpret_map && (
Interpretability — {({safe_hf: "DWT high-frequency band (SAFE)", npr_residual: "neighboring-pixel residual (NPR)", pgc_peak: "peak-guided residual fake-evidence (PGC)", aide_dct: "native-res DCT patch-energy (AIDE)", spai_patch: "per-patch spectral fake-evidence (SPAI)"})[r.interpret_kind] || "artifact map"} · warmer = stronger artifact signal
{r.interpret_overlay && (
overlay on image
overlay on image
)}
heatmap
heatmap
)}
); } const Row = ({ k, v }) => (
{k}{v}
);