File size: 4,776 Bytes
cd24b4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// API client for the SynthScope backend. In dev, Vite proxies /api to the FastAPI server.
const BASE = import.meta.env.VITE_API_BASE || "";

export async function fetchModels() {
  const r = await fetch(`${BASE}/api/models`);
  if (!r.ok) throw new Error(`models: HTTP ${r.status}`);
  return r.json();
}

export async function predict(file, model) {
  const fd = new FormData();
  fd.append("file", file);
  fd.append("model", model);
  const r = await fetch(`${BASE}/api/predict`, { method: "POST", body: fd });
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || `HTTP ${r.status}`);
  return d;
}

// --- dataset explorer --------------------------------------------------------
export async function fetchDatasets() {
  const r = await fetch(`${BASE}/api/datasets`);
  if (!r.ok) throw new Error(`datasets: HTTP ${r.status}`);
  return r.json();
}

export async function sampleDataset(key, split, n, seed) {
  const u = `${BASE}/api/datasets/${key}/sample?split=${encodeURIComponent(split)}&n=${n}&seed=${seed}`;
  const r = await fetch(u);
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || `HTTP ${r.status}`);
  return d;
}

export function datasetMediaUrl(key, rel, thumb) {
  return `${BASE}/api/datasets/${key}/media?rel=${encodeURIComponent(rel)}${thumb ? "&thumb=1" : ""}`;
}

export async function predictDataset(dataset, rel, model) {
  const fd = new FormData();
  fd.append("dataset", dataset);
  fd.append("rel", rel);
  fd.append("model", model);
  const r = await fetch(`${BASE}/api/predict_dataset`, { method: "POST", body: fd });
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || `HTTP ${r.status}`);
  return d;
}

export async function scoreBatch(dataset, model, rels) {
  const r = await fetch(`${BASE}/api/score_batch`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ dataset, model, rels }),
  });
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || `HTTP ${r.status}`);
  return d;
}

// --- threshold calibration helpers (shared by Detect + Explorer) ---
export const verdictAt = (p, thr) => (p >= thr ? "fake" : "real");

export function autoCalibrate(scored) {
  let best = 0.5, bestAcc = -1;
  for (let t = 0; t <= 1.0001; t += 0.01) {
    const reals = scored.filter((s) => s.gt === "real"), fakes = scored.filter((s) => s.gt === "fake");
    const tnr = reals.length ? reals.filter((s) => s.p < t).length / reals.length : 0;
    const tpr = fakes.length ? fakes.filter((s) => s.p >= t).length / fakes.length : 0;
    const acc = (tpr + tnr) / 2;
    if (acc > bestAcc) { bestAcc = acc; best = t; }
  }
  return Math.round(best * 100) / 100;
}

export function targetFprThreshold(scored, fpr) {
  const reals = scored.filter((s) => s.gt === "real").map((s) => s.p).sort((a, b) => a - b);
  if (!reals.length) return null;
  const idx = Math.min(reals.length - 1, Math.max(0, Math.ceil((1 - fpr) * reals.length) - 1));
  return Math.round(reals[idx] * 100) / 100;
}

export function batchStats(scored, thr) {
  if (!scored.length) return null;
  const reals = scored.filter((s) => s.gt === "real"), fakes = scored.filter((s) => s.gt === "fake");
  return {
    n: scored.length,
    acc: scored.filter((s) => verdictAt(s.p, thr) === s.gt).length / scored.length,
    racc: reals.length ? reals.filter((s) => s.p < thr).length / reals.length : null,
    facc: fakes.length ? fakes.filter((s) => s.p >= thr).length / fakes.length : null,
  };
}

// --- SOTA generator vs detector comparison ----------------------------------
export async function sotaGenerators() {
  const r = await fetch(`${BASE}/api/sota/generators`);
  if (!r.ok) throw new Error(`generators: HTTP ${r.status}`);
  return r.json();
}
export async function sotaRealImage() {
  const r = await fetch(`${BASE}/api/sota/real_image`);
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || `HTTP ${r.status}`);
  return d;
}
export async function fetchLeaderboard() {
  const r = await fetch(`${BASE}/api/leaderboard`);
  if (!r.ok) throw new Error(`leaderboard: HTTP ${r.status}`);
  return r.json();
}
export async function fetchGeneratorBenchmarks() {
  const r = await fetch(`${BASE}/api/generator_benchmarks`);
  if (!r.ok) throw new Error(`generator benchmarks: HTTP ${r.status}`);
  return r.json();
}
export async function sotaCompare(file, generator, detectors, prompt) {
  const fd = new FormData();
  fd.append("file", file);
  fd.append("generator", generator);
  fd.append("detectors", detectors.join(","));
  fd.append("prompt", prompt);
  const r = await fetch(`${BASE}/api/sota/compare`, { method: "POST", body: fd });
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || `HTTP ${r.status}`);
  return d;
}