File size: 9,809 Bytes
b454846
264e8fb
 
 
 
 
0e1903e
264e8fb
 
 
0e1903e
 
 
 
 
 
 
264e8fb
 
 
 
 
0e1903e
 
 
 
264e8fb
 
 
0e1903e
264e8fb
0e1903e
264e8fb
 
0e1903e
264e8fb
0e1903e
264e8fb
 
 
0e1903e
 
 
 
 
 
 
 
264e8fb
 
0e1903e
264e8fb
 
 
0e1903e
 
 
264e8fb
 
 
0e1903e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9c9700
0e1903e
 
 
 
c9c9700
0e1903e
 
c9c9700
0e1903e
 
 
 
 
 
 
 
 
c9c9700
0e1903e
c9c9700
 
0e1903e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264e8fb
0e1903e
 
 
 
264e8fb
0e1903e
 
 
 
 
 
 
 
 
264e8fb
0e1903e
 
 
 
 
 
 
 
 
 
264e8fb
 
0e1903e
 
264e8fb
 
0e1903e
 
 
 
264e8fb
 
b454846
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MOZAIC Sample Explorer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
  <header>
    <h1>MOZAIC sample explorer</h1>
    <p>An audio-visual deepfake benchmark: every clip pairs a real or fake face with a real or fake voice.</p>
    <span class="model">Evaluated model <code id="model">…</code></span>
  </header>
  <nav class="tabs" role="tablist" id="tabs"></nav>
  <section id="panel"></section>
  <p class="fine">Research demo. Fake clips are synthetic media made for detection research; they do not show anything
    the people in them said or did. A pilot this small is far too few for conclusions.</p>
</main>
<script>
const TASKS = {
  t1: ["T1 · Spot the fake", "Which part is fake: the video, the audio, both, or neither?", "32 frames and the audio."],
  t2: ["T2 · Manipulation type", "Which part of this video, if any, was generated or altered by AI?", "32 frames, no sound."],
  t3: ["T3 · Identity check", "Is clip 1 real or fake, and is it the same person as clip 2?", "Face: frames of both clips. Voice: sound of both clips."],
  t4: ["T4 · Explanation", "What is fake, where, and what in the clip shows it?", "Reference answers, annotated twice on the same clips: v5 and v7."],
};
const LABELS = { RVRA: "real video · real audio", RVFA: "real video · fake audio",
                 FVRA: "fake video · real audio", FVFA: "fake video · fake audio" };
const METHODS = { real: "real", facefusion: "FaceFusion", s2v: "Wan2.2-S2V", animate: "Wan2.2-Animate", seed_vc: "Seed-VC" };
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const $ = id => document.getElementById(id);

function clipDesc(clip) {
  const [label, method, frames, arm] = clip.replace(/\.mp4$/, "").split("__");
  const parts = [LABELS[label] || label, METHODS[method] || method, frames.replace("f", " frames")];
  if (arm && arm !== "main") parts.push(`${arm} audio`);
  return parts.join(" · ");
}

function video(src, caption) {
  return `<figure><video controls preload="metadata" playsinline src="clips/${encodeURIComponent(src)}"></video>
    <figcaption>${esc(caption)}</figcaption></figure>`;
}

function stat(value, label) { return `<div class="stat"><b>${value}</b><span>${esc(label)}</span></div>`; }

let DATA, state = { task: null, i: 0, cell: "", flagged: false };

function renderTabs() {
  $("tabs").innerHTML = "";
  for (const [key, [title]] of Object.entries(TASKS)) {
    if (!DATA.tasks[key] || !DATA.tasks[key].items.length) continue;
    const b = document.createElement("button");
    b.role = "tab"; b.textContent = title; b.setAttribute("aria-selected", key === state.task);
    b.onclick = () => { state = { task: key, i: 0, cell: "", flagged: false }; renderTabs(); render(); };
    $("tabs").appendChild(b);
  }
}

function navBar(items, label, extra = "") {
  const opts = items.map(([i, text]) => `<option value="${i}" ${i === state.i ? "selected" : ""}>${esc(text)}</option>`).join("");
  return `<div class="bar">${extra}
    <button id="prev" aria-label="Previous">◀</button>
    <select id="pick" aria-label="${esc(label)}">${opts}</select>
    <button id="next" aria-label="Next">▶</button></div>`;
}

function wireNav(order, redraw) {
  const pos = Math.max(0, order.indexOf(state.i));
  const go = d => { state.i = order[(pos + d + order.length) % order.length]; redraw(); };
  $("prev").onclick = () => go(-1);
  $("next").onclick = () => go(1);
  $("pick").onchange = e => { state.i = +e.target.value; redraw(); };
}

/* ---------- T1 to T3: questions with a right answer ---------- */
function renderScored() {
  const key = state.task, task = DATA.tasks[key], s = task.scores, it = task.items[state.i];
  const [, question, inputs] = TASKS[key];
  const order = task.items.map((_, i) => i);
  const nav = navBar(task.items.map((x, i) =>
    [i, `${x.correct ? "✓" : "✗"}  #${i + 1}${x.part ? ` [${x.part}]` : ""}  ${clipDesc(x.clips[0])}`]), "Sample");
  $("panel").innerHTML = `
    <p class="intro">${esc(question)} <span>Input: ${esc(inputs)}</span></p>
    <div class="stats">
      ${stat(`${Math.round(s.accuracy * s.n)}/${s.n}`, "correct")}
      ${stat(s.accuracy.toFixed(2), `accuracy (95% CI ${s.ci95[0].toFixed(2)}–${s.ci95[1].toFixed(2)})`)}
      ${stat(s.balanced_accuracy.toFixed(2), "balanced accuracy")}
      ${stat(s.cohen_kappa.toFixed(2), "Cohen's κ")}
    </div>
    ${nav}
    <div class="clips">${it.clips.map((c, n) => video(c, it.clips.length > 1 ? `Clip ${n + 1}` : clipDesc(c))).join("")}</div>
    <div class="card">
      <div class="chips">${it.options.map(o => `<span class="chip">${esc(o)}</span>`).join("")}</div>
      <div class="answer">
        <div><span>Correct answer</span><b>${esc(it.gt_answer)}</b></div>
        <div><span>Model answered</span><b>${esc(it.pred_answer ?? "unreadable")}</b></div>
        <div class="result ${it.correct ? "ok" : "bad"}">${it.correct ? "✓ correct" : "✗ wrong"}</div>
      </div>
      <details><summary>About the clips (hidden from the model)</summary>
        <ul>${it.clips.map((c, n) => `<li>Clip ${n + 1}: ${esc(clipDesc(c))}</li>`).join("")}</ul></details>
      <details><summary>Model's raw reply</summary><pre>${esc((it.raw || "").trim())}</pre></details>
    </div>`;
  wireNav(order, renderScored);
}

/* ---------- T4: v5 and v7 side by side ---------- */
function partBlock(name, part, withWhere) {
  if (!part) return `<div class="part"><div class="part-name">${name}</div><p class="none">no reply yet</p></div>`;
  const chips = part.artifacts.length
    ? `<div class="chips">${part.artifacts.map(a => withWhere
        ? `<span class="chip">${esc(a.artifact)}<small>${esc(a.where)} · ${esc(a.when)}</small></span>`
        : `<span class="chip">${esc(a)}</span>`).join("")}</div>`
    : `<span class="none">no artifact</span>`;
  return `<div class="part"><div class="part-name">${name}</div><p>${esc(part.text)}</p>${chips}</div>`;
}

function renderT4() {
  const task = DATA.tasks.t4, s = task.summary;
  const cells = [...new Set(task.items.map(x => x.cell))].sort();
  const order = task.items.map((x, i) => [x, i])
    .filter(([x]) => (!state.cell || x.cell === state.cell) && (!state.flagged || x.flags.length))
    .map(([, i]) => i);
  if (!order.length) { state.flagged = false; state.cell = ""; return renderT4(); }
  if (!order.includes(state.i)) state.i = order[0];
  const it = task.items[state.i];
  const filters = `
    <select id="cell" aria-label="Kind"><option value="">All kinds (${task.items.length})</option>
      ${cells.map(c => `<option value="${c}" ${c === state.cell ? "selected" : ""}>${esc(c.replace("_", " · "))}</option>`).join("")}
    </select>
    <label><input type="checkbox" id="flagged" ${state.flagged ? "checked" : ""}> flagged only</label>`;
  const nav = navBar(order.map(i => {
    const x = task.items[i];
    return [i, `${x.flags.length ? "⚠ " : ""}#${i + 1}  ${clipDesc(x.clips[0])}`];
  }), "Clip", filters);
  const fact = p => {
    const f = it.facts[p];
    return `<span class="badge ${f.fake ? "fake" : "real"}">${p} · ${f.fake ? "fake · " + esc(f.region) : "real"}</span>`;
  };
  const flagCount = Object.values(s.flags).reduce((a, b) => a + b, 0);
  $("panel").innerHTML = `
    <p class="intro">The same ${s.n} clips, annotated twice. What is fake comes from metadata; the evidence is written
      by the annotator (<code>${esc(s.annotator)}</code>), who is told the facts.</p>
    <div class="stats">
      ${stat(s.n, "clips")}
      ${stat(`${s.v7_answered}/${s.n}`, "answered by v7")}
      ${stat(s.flags["v5: voice-face mismatch, but face and voice are one person"] || 0, "v5 voice-face tags that are wrong")}
      ${stat(flagCount, "clips flagged")}
    </div>
    ${nav}
    <div class="clips">${it.clips.map((c, n) => video(c, n === 0 ? `Clip to explain · ${clipDesc(c)}` : "Real recording it was made from (reference)")).join("")}</div>
    <div class="facts">${fact("video")}${fact("audio")}</div>
    ${it.flags.map(f => `<div class="flag">⚠ ${esc(f)}</div>`).join("")}
    <div class="compare">
      <div class="card"><h3>v5<small>previous annotation</small></h3>
        ${partBlock("Video", it.v5.video, false)}${partBlock("Audio", it.v5.audio, false)}</div>
      <div class="card"><h3>v7<small>current annotation</small></h3>
        ${partBlock("Video", it.v7 && it.v7.video, true)}${partBlock("Audio", it.v7 && it.v7.audio, true)}</div>
    </div>
    <details><summary>What was changed, from metadata</summary><ul>
      ${["video", "audio"].map(p => `<li><b>${p}:</b> ${esc(it.facts[p].fake ? it.facts[p].changed + "; " + it.facts[p].original : it.facts[p].original)}</li>`).join("")}
    </ul></details>
    <details><summary>Artifact categories over all ${s.n} clips, v5 vs v7</summary>
      <table><tr><th>Category</th><th class="num">v5</th><th class="num">v7</th></tr>
        ${s.categories.map(([c, a, b]) => `<tr><td>${esc(c)}</td><td class="num">${a}</td><td class="num">${b}</td></tr>`).join("")}
      </table></details>`;
  $("cell").onchange = e => { state.cell = e.target.value; renderT4(); };
  $("flagged").onchange = e => { state.flagged = e.target.checked; renderT4(); };
  wireNav(order, renderT4);
}

function render() { return state.task === "t4" ? renderT4() : renderScored(); }

fetch("samples.json").then(r => r.json()).then(d => {
  DATA = d;
  $("model").textContent = d.model;
  state.task = Object.keys(TASKS).find(k => d.tasks[k] && d.tasks[k].items.length);
  renderTabs(); render();
}).catch(e => { $("panel").textContent = "Could not load samples.json: " + e; });
</script>
</body>
</html>