shainaraza's picture
Add T4 tab: explanation pilot (facts + annotator evidence, reference clip)
c9c9700 verified
Raw History Blame Contribute Delete
13.3 kB
<!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>
<style>
:root {
--bg: #f7f7f8; --card: #ffffff; --text: #1b1c1f; --muted: #62656e; --line: #e2e3e7;
--accent: #4f46e5; --ok: #15803d; --bad: #b91c1c; --code: #f0f0f3;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #111214; --card: #1a1b1f; --text: #ececf0; --muted: #a0a3ad; --line: #2c2e34;
--accent: #8b85ff; --ok: #4ade80; --bad: #f87171; --code: #24262c;
}
}
:root[data-theme="dark"] {
--bg: #111214; --card: #1a1b1f; --text: #ececf0; --muted: #a0a3ad; --line: #2c2e34;
--accent: #8b85ff; --ok: #4ade80; --bad: #f87171; --code: #24262c;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
main { max-width: 1040px; margin: 0 auto; padding: 28px 16px 48px; }
h1 { font-size: 1.7rem; margin: 0 0 6px; }
.lede { color: var(--muted); margin: 0 0 4px; }
.fine { color: var(--muted); font-size: .82rem; margin: 0 0 20px; }
code { background: var(--code); padding: 1px 6px; border-radius: 5px; font-size: .88em; }
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 18px; overflow-x: auto; }
.tabs button { background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted);
padding: 9px 12px; font: inherit; cursor: pointer; white-space: nowrap; }
.tabs button[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.task-q { font-weight: 600; margin: 0; }
.task-in { color: var(--muted); margin: 2px 0 14px; }
.note { color: var(--muted); font-size: .82rem; margin: 0 0 6px; }
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 18px; }
.stat { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
.stat b { display: block; font-size: 1.45rem; font-variant-numeric: tabular-nums; }
.stat span { color: var(--muted); font-size: .82rem; }
.nav { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; }
.nav select { flex: 1; min-width: 0; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
background: var(--card); color: var(--text); font: inherit; }
.nav button { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--line); background: var(--card);
color: var(--text); font: inherit; cursor: pointer; }
.clips { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 16px; }
figure { margin: 0; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 10px; }
figure video { width: 100%; max-height: 320px; background: #000; border-radius: 6px; display: block; }
figcaption { color: var(--muted); font-size: .84rem; margin-top: 6px; }
.grid { display: grid; grid-template-columns: 3fr 2fr; gap: 16px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; }
.card h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin: 0 0 6px; }
blockquote { margin: 0 0 12px; padding-left: 12px; border-left: 3px solid var(--line); }
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 500; font-size: .85rem; }
.ok { color: var(--ok); font-weight: 600; } .bad { color: var(--bad); font-weight: 600; }
details { margin-top: 14px; } summary { cursor: pointer; color: var(--muted); }
pre { background: var(--code); padding: 10px; border-radius: 8px; overflow-x: auto; font-size: .85rem; }
ul { margin: 0; padding-left: 18px; }
@media (max-width: 680px) {
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main>
<h1>MOZAIC sample explorer</h1>
<p class="lede">MOZAIC is an audio-visual deepfake benchmark built on VoxCeleb2. Every clip pairs a real or fake face
with a real or fake voice. This page shows a small pilot: <code id="model">…</code> answering MOZAIC questions
zero-shot, with example clips per task.</p>
<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>
<div class="tabs" role="tablist" id="tabs"></div>
<section id="panel"></section>
</main>
<script>
const TASKS = {
t1: ["T1 · Spot the fake", "Which part is fake: the video, the audio, both, or neither?",
"The model saw 32 frames and heard the audio."],
t2: ["T2 · Manipulation type", "Which part of this video, if any, was generated or altered by AI?", "The model saw 32 frames only, no sound."],
t3: ["T3 · Identity check", "Is clip 1 real or fake, and is it the same person as clip 2?",
"Face questions: frames of both clips, no sound. Voice questions: sound of both clips, no frames."],
t4: ["T4 · Explanation (ground truth)", "What is fake, where, and what in the clip shows it?",
"Reference answers being built: facts from metadata + evidence written by the annotator (told the facts), with the pair's real clip as a reference. Not checked by people yet."],
};
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: "camera (real)", facefusion: "FaceFusion face swap", s2v: "Wan2.2-S2V (fully generated)",
animate: "Wan2.2-Animate (full-body swap)", seed_vc: "Seed-VC voice conversion" };
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
function clipDesc(clip) {
const [label, method, frames, arm] = clip.split("__");
const parts = [LABELS[label] || label, METHODS[method] || method, frames.replace("f", " frames")];
if (arm !== "main") parts.push(`${arm} audio`);
return parts.join(" · ");
}
let DATA, current = { task: null, i: 0 };
function renderTabs() {
const tabs = document.getElementById("tabs");
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 === current.task);
b.onclick = () => { current = { task: key, i: 0 }; renderTabs(); renderPanel(); };
tabs.appendChild(b);
}
}
function renderT4() {
const task = DATA.tasks.t4, s = task.summary, it = task.items[current.i];
const [, question, inputs] = TASKS.t4;
const opts = task.items.map((x, i) =>
`<option value="${i}" ${i === current.i ? "selected" : ""}>#${i + 1} ${esc(clipDesc(x.clips[0]))}</option>`).join("");
const clips = it.clips.map((c, n) => `
<figure><video controls preload="metadata" playsinline src="clips/${encodeURIComponent(c)}"></video>
<figcaption>${n === 0 ? "Clip to explain" : "Reference: the original real recording"}</figcaption></figure>`).join("");
const part = p => {
const f = it.facts[p], tags = it.artifacts[p] || [];
return `<tr><td><b>${p}</b></td>
<td class="${f.fake ? "bad" : "ok"}">${f.fake ? "fake · " + esc(f.region) : "real"}</td>
<td>${esc(it.evidence[p])}${tags.length ? `<div style="margin-top:6px">${tags.map(t => `<code>${esc(t)}</code>`).join(" ")}</div>` : ""}</td></tr>`;
};
const top = (s.top_artifacts || []).map(([t, n]) => `<code>${esc(t)}</code> ${n}`).join(" · ");
document.getElementById("panel").innerHTML = `
<p class="task-q">${esc(question)}</p>
<p class="task-in">${esc(inputs)}</p>
<div class="stats">
<div class="stat"><b>${s.n}</b><span>clips with a reference answer</span></div>
<div class="stat"><b>${Object.keys(s.per_cell).length}</b><span>kinds (label × generator)</span></div>
<div class="stat"><b>${s.with_reference}</b><span>with the real clip as reference</span></div>
<div class="stat"><b>${s.no_evidence}</b><span>"no visible evidence"</span></div>
</div>
<p class="note">Annotator <code>${esc(s.annotator)}</code>, prompt ${esc(s.version)}. Most used artifact tags: ${top}</p>
<div class="nav">
<button id="prev" aria-label="Previous sample">◀</button>
<select id="pick" aria-label="Sample">${opts}</select>
<button id="next" aria-label="Next sample">▶</button>
</div>
<div class="clips">${clips}</div>
<div class="card">
<h3>Reference answer</h3>
<blockquote>${esc(it.question)}</blockquote>
<div>Regions: ${it.regions.map(r => `<code>${esc(r)}</code>`).join(" · ")} <span class="note">(from metadata)</span></div>
<table>
<tr><th>Part</th><th>Truth (metadata)</th><th>Evidence and artifact tags (annotator)</th></tr>
${part("video")}${part("audio")}
</table>
<details><summary>What 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>
</div>`;
const go = d => { current.i = (current.i + d + task.items.length) % task.items.length; renderT4(); };
document.getElementById("prev").onclick = () => go(-1);
document.getElementById("next").onclick = () => go(1);
document.getElementById("pick").onchange = e => { current.i = +e.target.value; renderT4(); };
}
function renderPanel() {
if (current.task === "t4") return renderT4();
const key = current.task, task = DATA.tasks[key], s = task.scores, it = task.items[current.i];
const [, question, inputs] = TASKS[key];
const right = Math.round(s.accuracy * s.n);
const opts = task.items.map((x, i) =>
`<option value="${i}" ${i === current.i ? "selected" : ""}>${x.correct ? "✓" : "✗"} #${i + 1}${x.part ? ` [${x.part}]` : ""} ${esc(clipDesc(x.clips[0]))}</option>`).join("");
const clips = it.clips.map((c, n) => `
<figure><video controls preload="metadata" playsinline src="clips/${encodeURIComponent(c)}"></video>
<figcaption>Clip ${n + 1}${it.clips.length > 1 && n === 1 ? " (real reference)" : ""}</figcaption></figure>`).join("");
document.getElementById("panel").innerHTML = `
<p class="task-q">${esc(question)}</p>
<p class="task-in">${esc(inputs)}</p>
<p class="note">Scores over all ${s.n} questions Gemini answered in this pilot run; ${task.items.length} shown below.</p>
<div class="stats">
<div class="stat"><b>${right}/${s.n}</b><span>correct</span></div>
<div class="stat"><b>${s.accuracy.toFixed(2)}</b><span>accuracy (95% CI ${s.ci95[0].toFixed(2)}–${s.ci95[1].toFixed(2)})</span></div>
<div class="stat"><b>${s.balanced_accuracy.toFixed(2)}</b><span>balanced accuracy</span></div>
<div class="stat"><b>${s.cohen_kappa.toFixed(2)}</b><span>Cohen's κ</span></div>
</div>
<div class="nav">
<button id="prev" aria-label="Previous sample">◀</button>
<select id="pick" aria-label="Sample">${opts}</select>
<button id="next" aria-label="Next sample">▶</button>
</div>
<div class="clips">${clips}</div>
<div class="grid">
<div class="card">
<h3>Question</h3>
<blockquote>${esc(it.question)}</blockquote>
<div>Options, in the order shown: ${it.options.map(o => `<code>${esc(o)}</code>`).join(" · ")}</div>
<table>
<tr><th>Correct answer</th><th>Gemini answered</th><th></th></tr>
<tr><td><b>${esc(it.gt_answer)}</b></td><td><b>${esc(it.pred_answer ?? "— unreadable —")}</b></td>
<td class="${it.correct ? "ok" : "bad"}">${it.correct ? "✓ Correct" : "✗ Wrong"}</td></tr>
</table>
</div>
<div class="card">
<h3>About the clip(s), hidden from the model</h3>
<ul>${it.clips.map((c, n) => `<li><b>Clip ${n + 1}:</b> ${esc(clipDesc(c))}</li>`).join("")}</ul>
<details><summary>Gemini's raw reply</summary><pre>${esc((it.raw || "").trim())}</pre></details>
</div>
</div>`;
const go = d => { current.i = (current.i + d + task.items.length) % task.items.length; renderPanel(); };
document.getElementById("prev").onclick = () => go(-1);
document.getElementById("next").onclick = () => go(1);
document.getElementById("pick").onchange = e => { current.i = +e.target.value; renderPanel(); };
}
fetch("samples.json").then(r => r.json()).then(d => {
DATA = d;
document.getElementById("model").textContent = d.model;
current.task = Object.keys(TASKS).find(k => d.tasks[k] && d.tasks[k].items.length);
renderTabs(); renderPanel();
}).catch(e => { document.getElementById("panel").textContent = "Could not load samples.json: " + e; });
</script>
</body>
</html>