T4-Annotation-Review / index.html
ahmedyradwan02's picture
Sign-in must grant the Deep-Fake org
91c3d77 verified
Raw History Blame Contribute Delete
13 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MOZAIC T4 Review</title>
<style>
:root { --bg: #f7f7f8; --card: #fff; --text: #1d1d1f; --muted: #6b6b70; --line: #e2e2e6;
--accent: #2f6fde; --ok: #1e8e3e; --warn: #c26b00; --bad: #c5221f; }
@media (prefers-color-scheme: dark) {
:root { --bg: #151517; --card: #1f1f22; --text: #ececf0; --muted: #a0a0a8; --line: #33333a;
--accent: #6d9cff; --ok: #5bc77a; --warn: #f0a640; --bad: #ff6b66; }
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 system-ui, sans-serif; }
main { max-width: 1200px; margin: 0 auto; padding: 16px; }
h1 { font-size: 20px; margin: 0 0 12px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
@media (max-width: 760px) { .row { grid-template-columns: 1fr; } }
video { width: 100%; max-height: 360px; background: #000; border-radius: 8px; }
.label { font-size: 13px; color: var(--muted); margin-bottom: 6px; }
.bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; }
.pill { padding: 2px 10px; border-radius: 99px; border: 1px solid var(--line); font-size: 13px; }
.done { color: var(--ok); border-color: var(--ok); }
h3 { margin: 0 0 6px; font-size: 16px; }
.fake { color: var(--bad); font-weight: 600; } .real { color: var(--ok); font-weight: 600; }
ul { margin: 4px 0 10px; padding-left: 18px; }
.choices { display: flex; gap: 8px; flex-wrap: wrap; }
.choices label { border: 1px solid var(--line); border-radius: 8px; padding: 6px 12px; cursor: pointer; }
.choices input { margin-right: 6px; }
.choices label:has(input:checked) { border-color: var(--accent); outline: 1px solid var(--accent); }
textarea, input[type=number] { width: 100%; padding: 8px; border: 1px solid var(--line); border-radius: 8px;
background: var(--card); color: var(--text); font: inherit; }
button { padding: 8px 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--card);
color: var(--text); font: inherit; cursor: pointer; }
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.nav { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 14px; }
.nav input { width: 90px; }
#msg { color: var(--warn); min-height: 1.5em; margin-top: 8px; }
#sync { margin-left: auto; font-size: 13px; color: var(--muted); }
details { margin-bottom: 14px; }
.hidden { display: none !important; }
</style>
</head>
<body>
<main>
<h1>MOZAIC T4 review</h1>
<div id="login" class="card hidden">
<p id="loginMsg">Sign in with your Hugging Face account to start.</p>
<button class="primary" id="signin">Sign in with Hugging Face</button>
</div>
<div id="app" class="hidden">
<details class="card">
<summary>How to review</summary>
<p>Watch the clip, and the reference (the same person's real clip) to compare. The facts say
what is fake; they are correct. Judge only what is written, per part:</p>
<ul>
<li><b>accurate</b>: it is there, in the region the facts name (for a REAL part: no fake
artifact is claimed and none is obvious)</li>
<li><b>ungrounded</b>: vague, generic, or in the wrong region or moment</li>
<li><b>invented</b>: describes something that is not in the clip</li>
</ul>
<p>Every move saves in this browser; your file goes to Hugging Face at least once a minute.
Close the tab any time: you resume at your first unfinished clip. Use one tab at a time.</p>
</details>
<div class="bar"><span id="header"></span><span id="sync"></span></div>
<div class="row">
<div class="card"><div class="label">Clip to review</div><video id="clip" controls></video></div>
<div class="card" id="refCard"><div class="label">Reference: the pair's real clip</div>
<video id="ref" controls></video></div>
</div>
<div class="row">
<div class="card" id="part-video"></div>
<div class="card" id="part-audio"></div>
</div>
<div class="card">
<div class="label">Note (optional)</div>
<textarea id="note" rows="2"></textarea>
<div class="nav">
<button data-go="prev">⬅ Previous</button>
<button class="primary" data-go="next">Save &amp; next ➡</button>
<button data-go="todo">Next unfinished ⏭</button>
<input id="goto" type="number" min="1" placeholder="Clip #">
<button data-go="goto">Go</button>
<button id="pushNow">Save to HF now</button>
</div>
<div id="msg"></div>
</div>
</div>
</main>
<script type="module">
import { oauthLoginUrl, oauthHandleRedirectIfPresent, downloadFile, uploadFile }
from "https://cdn.jsdelivr.net/npm/@huggingface/hub@2.16.3/+esm";
const LABELS = ["accurate", "ungrounded", "invented"];
const PUSH_EVERY_MS = 60_000; // at most one commit a minute per reviewer
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;"}[c]));
const done = (c) => c.review.video && c.review.audio;
const store = { // localStorage can be blocked: the page still works, it just cannot keep unsent work
get(k) { try { return JSON.parse(localStorage.getItem(k)); } catch { return null; } },
set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} },
del(k) { try { localStorage.removeItem(k); } catch {} },
};
const config = await (await fetch("data/config.json")).json();
let auth = store.get("t4:auth"), name = "", clips = [], idx = 0, dirty = false, lastPush = 0, pushing = null;
// ---------------------------------------------------------------- sign in
const redirect = await oauthHandleRedirectIfPresent();
if (redirect) {
auth = {token: redirect.accessToken, expires: redirect.accessTokenExpiresAt, user: redirect.userInfo.preferred_username};
store.set("t4:auth", auth);
}
$("signin").onclick = async () => { location.href = await oauthLoginUrl(); };
if (!auth || new Date(auth.expires) <= new Date()) signIn("Sign in with your Hugging Face account to start.");
else start();
function signIn(text) {
$("loginMsg").textContent = text;
$("login").classList.remove("hidden");
$("app").classList.add("hidden");
}
// ---------------------------------------------------------------- load
const file = () => `reviews/golden_${name}.json`;
const repo = () => ({type: "dataset", name: config.dataset});
function merge(...versions) {
// per clip, the review with the latest time wins (seed, saved on HF, unsent in this browser)
const base = versions[0].map((c) => ({...c}));
for (const v of versions.slice(1)) {
if (!v) continue;
const byId = Object.fromEntries(v.map((c) => [c.id, c.review]));
for (const c of base) {
const r = byId[c.id];
if (r && (r.time || "") > (c.review.time || "")) c.review = r;
}
}
return base;
}
async function remote() {
// the reviewer's file on HF; null before their first save
let blob;
try { blob = await downloadFile({repo: repo(), path: file(), accessToken: auth.token}); }
catch (e) { if (e.statusCode === 404 || String(e).includes("status 404")) return null; throw e; }
return blob ? JSON.parse(await blob.text()) : null;
}
async function start() {
name = config.reviewers[auth.user];
if (!name) return signIn(`${auth.user} is not on the reviewer list. Ask for your account to be added.`);
try {
const seed = await (await fetch(`data/reviews/golden_${name}.json`)).json();
const local = store.get(`t4:work:${name}`);
clips = merge(seed, await remote(), local);
dirty = Boolean(local);
} catch (e) {
if (String(e).includes("401")) return signIn("Your sign-in expired. Sign in again.");
return signIn("Could not load your file: " + e.message);
}
idx = Math.max(0, clips.findIndex((c) => !done(c)));
$("login").classList.add("hidden");
$("app").classList.remove("hidden");
show();
syncLabel();
setInterval(() => push(false), 15_000);
}
// ---------------------------------------------------------------- show
function part(c, p) {
const fake = c.facts[p] !== "REAL";
const ev = c.evidence[p].map((e) => `<li>${esc(e)}</li>`).join("") || "<li>(none)</li>";
const radios = LABELS.map((l) => `<label><input type="radio" name="${p}" value="${l}"
${c.review[p] === l ? "checked" : ""}>${l}</label>`).join("");
return `<h3>${p[0].toUpperCase() + p.slice(1)}</h3>
<div><b>Facts:</b> <span class="${fake ? "fake" : "real"}">${esc(c.facts[p])}</span></div>
<p><b>Explanation:</b> ${esc(c.explanation[p])}</p>
<div><b>Evidence</b> <span class="label">(artifact | where | when)</span></div><ul>${ev}</ul>
<div class="label">${p} label</div><div class="choices">${radios}</div>`;
}
function show() {
const c = clips[idx];
$("header").innerHTML = `<b>${esc(name)}</b>
<span class="pill">clip ${idx + 1} / ${clips.length}</span><span class="pill">${clips.filter(done).length} done</span>
<span class="pill">${esc(c.label)}</span><span class="pill">${esc(c.generator)}</span>
<span class="pill">${c.frames}f</span>${done(c) ? '<span class="pill done">✓ reviewed</span>' : ""}`;
$("clip").src = c.clip;
$("refCard").classList.toggle("hidden", !c.reference);
$("ref").src = c.reference || "";
$("part-video").innerHTML = part(c, "video");
$("part-audio").innerHTML = part(c, "audio");
$("note").value = c.review.note || "";
}
// ---------------------------------------------------------------- save
const picked = (p) => document.querySelector(`input[name=${p}]:checked`)?.value || null;
function saveLocal() {
if (!clips.length) return false; // not signed in yet
const c = clips[idx];
const r = {video: picked("video"), audio: picked("audio"), note: $("note").value.trim()};
if (r.video === c.review.video && r.audio === c.review.audio && r.note === (c.review.note || "")) return false;
c.review = {...r, time: new Date().toISOString()};
store.set(`t4:work:${name}`, clips);
dirty = true;
syncLabel();
return true;
}
function push(now) {
if (!dirty || pushing || (!now && Date.now() - lastPush < PUSH_EVERY_MS)) return pushing;
pushing = (async () => {
try {
clips = merge(clips, await remote()); // keeps anything saved from another tab
await uploadFile({repo: repo(), accessToken: auth.token, commitTitle: `review: ${name}`,
file: {path: file(), content: new Blob([JSON.stringify(clips, null, 1)])}});
dirty = false;
lastPush = Date.now();
store.del(`t4:work:${name}`);
} catch (e) {
const text = String(e);
if (text.includes("401") || text.includes("not found")) {
// expired, or the sign-in did not grant the Deep-Fake org: sign in again (work stays in this browser)
store.del("t4:auth");
$("msg").innerHTML = '⚠️ Your sign-in cannot write to the dataset. Your work is kept in this browser. ' +
'<a href="" onclick="location.reload()">Reload and sign in again</a>, and allow access to the organization.';
} else {
$("msg").textContent = "⚠️ Could not save to HF (kept in this browser, will retry): " + e.message;
}
} finally {
pushing = null;
syncLabel();
}
})();
return pushing;
}
function syncLabel() {
$("sync").textContent = dirty ? "● changes not on HF yet" : lastPush ? `☁️ saved to HF ${new Date(lastPush).toLocaleTimeString()}` : "☁️ up to date";
}
function go(where) {
let msg = saveLocal() ? "💾 saved" : "";
if (where === "prev") idx = Math.max(0, idx - 1);
if (where === "next") {
if (!done(clips[idx])) msg = "⚠️ saved, but one part has no label yet";
idx = Math.min(clips.length - 1, idx + 1);
}
if (where === "todo") {
const todo = clips.map((_, i) => i).filter((i) => !done(clips[i]));
if (!todo.length) msg = "🎉 all clips done";
else idx = todo.find((i) => i > idx) ?? todo[0];
}
if (where === "goto") {
const n = parseInt($("goto").value, 10);
if (!(n >= 1 && n <= clips.length)) { $("msg").textContent = `❌ clip number must be 1 to ${clips.length}`; return; }
idx = n - 1;
}
$("msg").textContent = msg;
show();
window.scrollTo({top: 0});
push(false);
}
for (const b of document.querySelectorAll("[data-go]")) b.onclick = () => go(b.dataset.go);
$("pushNow").onclick = async () => { saveLocal(); await push(true); if (!dirty) $("msg").textContent = "☁️ saved to HF"; };
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { saveLocal(); push(true); } });
window.addEventListener("beforeunload", (e) => { if (dirty) e.preventDefault(); });
</script>
</body>
</html>