davanstrien's picture
davanstrien HF Staff
v2 results: add parameter-matched Gemma-4-26B MoE arm to ledger; update standfirst claim accordingly
297f63d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Post-OCR Gazette — DiffusionGemma vs Gemma-4</title>
<!-- The Space renders inside an iframe on huggingface.co, which blocks being
framed (frame-ancestors) — links must escape the frame or they show
"refused to connect". No in-page anchors exist, so a blanket base is safe. -->
<base target="_blank" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&family=Old+Standard+TT:ital,wght@0,400;0,700;1,400&family=Special+Elite&display=swap" rel="stylesheet" />
<style>
:root {
--paper: #f4ecd8;
--paper-deep: #ece1c6;
--ink: #1d1a14;
--ink-soft: #4a4339;
--rule: #2a251c;
--oxblood: #7a1f1f;
--ochre-bg: #e8d27a66;
--ochre-edge: #a8842a;
--green-ink: #2e5d34;
--red-ink: #9c2b22;
}
* { box-sizing: border-box; }
html { background: #d9cdaf; }
body {
margin: 0 auto; max-width: 1080px; padding: 2rem 2.2rem 3.5rem;
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E"),
var(--paper);
color: var(--ink);
font-family: "Old Standard TT", "Iowan Old Style", Georgia, serif;
font-size: 17px; line-height: 1.55;
box-shadow: 0 0 50px rgba(0,0,0,.3); min-height: 100vh;
}
/* ---------- masthead ---------- */
header { text-align: center; animation: settle .6s ease-out both; }
.gazette-name { font-family: "UnifrakturCook", serif; font-size: clamp(2.2rem, 5vw, 3.4rem); margin: 0; }
.dateline {
display: flex; align-items: center; gap: 1rem; margin: .5rem 0 0;
font-variant: small-caps; letter-spacing: .2em; font-size: .78rem; color: var(--ink-soft);
}
.dateline::before, .dateline::after { content: ""; flex: 1; border-top: 1px solid var(--rule); }
.double-rule { border: 0; border-top: 3px double var(--rule); margin: .7rem 0 0; }
.standfirst { max-width: 62ch; margin: .9rem auto 0; color: var(--ink); font-size: 1.04rem; line-height: 1.65; }
.standfirst a { color: var(--oxblood); }
/* ---------- desk ---------- */
.desk { margin-top: 1.7rem; animation: settle .6s .1s ease-out both; }
.chips { display: flex; flex-wrap: wrap; gap: .45rem; justify-content: center; }
.chip {
font-family: inherit; font-size: .8rem; cursor: pointer; background: transparent; color: var(--ink);
border: 1px solid var(--rule); padding: .28rem .65rem; transition: background .15s, color .15s;
}
.chip:hover { background: var(--ink); color: var(--paper); }
.chip.active { background: var(--oxblood); color: var(--paper); border-color: var(--oxblood); }
textarea {
width: 100%; min-height: 7.5rem; margin-top: .9rem; padding: .9rem 1rem;
font-family: "Special Elite", "Courier New", monospace; font-size: .9rem; line-height: 1.65;
color: var(--ink); background: var(--paper-deep);
border: 1px solid var(--rule); outline: none; resize: vertical;
box-shadow: inset 0 1px 5px rgba(0,0,0,.1);
}
textarea:focus { border-color: var(--oxblood); }
.authority { margin-top: .7rem; border: 1px solid var(--rule); background: var(--paper-deep); display: none; }
.authority.visible { display: block; }
.authority summary { cursor: pointer; padding: .4rem .8rem; font-variant: small-caps; letter-spacing: .15em; font-size: .76rem; color: var(--ink-soft); }
.authority .gold-text { padding: 0 1rem .7rem; font-size: .93rem; line-height: 1.65; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 1.1rem; margin-top: .8rem; }
.run-btn {
font-family: inherit; font-variant: small-caps; letter-spacing: .15em; font-size: 1rem;
background: var(--ink); color: var(--paper); border: 1px solid var(--ink);
padding: .5rem 1.5rem; cursor: pointer; transition: background .15s;
}
.run-btn:hover { background: var(--oxblood); border-color: var(--oxblood); }
.run-btn[disabled] { opacity: .45; cursor: wait; }
label.toggle { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--ink-soft); cursor: pointer; }
label.toggle input { accent-color: var(--oxblood); }
.charcount { margin-left: auto; font-size: .76rem; color: var(--ink-soft); }
.charcount.over { color: var(--red-ink); }
/* ---------- tabs ---------- */
.tabs { display: flex; justify-content: center; gap: 0; margin-top: 2rem; border-bottom: 3px double var(--rule); animation: settle .6s .18s ease-out both; }
.tabs button {
font-family: inherit; font-variant: small-caps; letter-spacing: .14em; font-size: .92rem;
background: transparent; color: var(--ink-soft); border: 0; padding: .5rem 1.3rem; cursor: pointer;
border-bottom: 3px solid transparent; margin-bottom: -3px;
}
.tabs button.active { color: var(--ink); border-bottom-color: var(--oxblood); font-weight: 700; }
.pane { display: none; padding-top: 1.5rem; }
.pane.active { display: block; }
/* ---------- the press (hero) ---------- */
.press-stage { max-width: 760px; margin: 0 auto; }
.press-status {
display: flex; justify-content: space-between; align-items: baseline;
font-variant: small-caps; letter-spacing: .15em; font-size: .78rem; color: var(--ink-soft); min-height: 1.6rem;
}
.press-status b { font-size: 1.1rem; color: var(--oxblood); font-variant: normal; letter-spacing: 0; }
.press-canvas {
margin-top: .5rem; padding: 1.1rem 1.2rem; min-height: 11rem;
background: var(--paper-deep); border: 1px solid var(--rule);
font-size: 1rem; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word;
box-shadow: inset 0 1px 6px rgba(0,0,0,.08);
}
.press-canvas.settling { font-family: "Special Elite", monospace; font-size: .88rem; }
.press-canvas .placeholder { color: var(--ink-soft); font-style: italic; opacity: .75; }
.press-controls { display: flex; align-items: center; gap: .9rem; margin-top: .6rem; }
.press-controls input[type=range] { flex: 1; accent-color: var(--oxblood); }
.press-controls button {
font-family: inherit; background: transparent; border: 1px solid var(--rule); cursor: pointer;
font-size: .8rem; padding: .2rem .7rem;
}
.press-stats { text-align: center; margin-top: .8rem; font-size: .85rem; color: var(--ink-soft); min-height: 1.4rem; }
.press-stats b { color: var(--ink); font-size: 1.05rem; }
/* ---------- comparison ---------- */
.mode-switch { display: flex; justify-content: center; margin-bottom: 1.2rem; }
.mode-switch button {
font-family: inherit; font-variant: small-caps; letter-spacing: .1em; font-size: .76rem;
background: transparent; color: var(--ink); border: 1px solid var(--rule); padding: .28rem .8rem; cursor: pointer;
}
.mode-switch button.active { background: var(--ink); color: var(--paper); }
.mode-switch button[disabled] { opacity: .4; cursor: not-allowed; }
.no-gold-note {
display: none; text-align: center; font-size: .82rem; font-style: italic; color: var(--red-ink);
margin: 0 auto 1rem; max-width: 64ch;
}
.no-gold-note.visible { display: block; }
.columns { display: grid; grid-template-columns: 1fr 1fr; gap: 0 2.4rem; position: relative; }
.columns::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; border-left: 1px solid var(--rule); }
@media (max-width: 720px) { .columns { grid-template-columns: 1fr; gap: 2rem; } .columns::before { display: none; } }
.col-head { border-bottom: 1px solid var(--rule); padding-bottom: .35rem; text-align: center; }
.col-head h2 { margin: 0; font-size: 1.15rem; }
.col-head .col-sub { font-size: .74rem; color: var(--ink-soft); }
.col-head .col-sub a { color: inherit; }
.statline {
display: flex; justify-content: center; gap: 1.4rem; padding: .5rem 0; border-bottom: 1px solid var(--rule);
font-size: .78rem; color: var(--ink-soft); min-height: 2rem; align-items: baseline;
}
.statline b { font-size: 1.15rem; color: var(--ink); }
.statline .cached-tag { color: var(--oxblood); font-style: italic; }
.proof { padding: .9rem .1rem 0; min-height: 7rem; font-size: .98rem; line-height: 1.75; word-wrap: break-word; }
.proof .placeholder { color: var(--ink-soft); font-style: italic; opacity: .7; }
.proof .spinner { font-style: italic; color: var(--oxblood); }
.proof .spinner::after { content: ""; animation: dots 1.2s steps(4) infinite; }
@keyframes dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }
.seg-changed { background: var(--ochre-bg); border-bottom: 2px solid var(--ochre-edge); }
.seg-added { color: var(--green-ink); border-bottom: 2px solid var(--green-ink); font-weight: 700; }
.seg-removed { color: var(--red-ink); text-decoration: line-through; opacity: .85; }
.error-box { border: 1px solid var(--red-ink); color: var(--red-ink); padding: .55rem .85rem; font-style: italic; margin-top: .9rem; }
.legend { display: flex; gap: 1.4rem; justify-content: center; margin-top: 1.6rem; font-size: .78rem; color: var(--ink-soft); flex-wrap: wrap; }
/* ---------- ledger ---------- */
table { border-collapse: collapse; margin: 0 auto; font-size: .9rem; }
th, td { padding: .42rem .85rem; border-bottom: 1px solid #00000022; text-align: right; }
th:first-child, td:first-child { text-align: left; }
thead th { border-bottom: 2px solid var(--rule); font-variant: small-caps; letter-spacing: .05em; }
thead th .hint { display: block; font-variant: normal; letter-spacing: 0; font-weight: 400; font-style: italic; font-size: .68rem; color: var(--ink-soft); }
tbody tr.baseline td { color: var(--ink-soft); font-style: italic; }
tbody tr.dimmed td { color: var(--ink-soft); opacity: .7; }
td.win { font-weight: 700; }
.footnotes { max-width: 72ch; margin: 1.2rem auto 0; font-size: .84rem; color: var(--ink-soft); }
.footnotes p { margin: .3rem 0; }
footer { margin-top: 3rem; border-top: 3px double var(--rule); padding-top: .9rem; text-align: center; font-size: .8rem; color: var(--ink-soft); }
footer a { color: var(--oxblood); }
/* ---------- small screens (from rendered review at 375/768/1366) ---------- */
#results-table { overflow-x: auto; -webkit-overflow-scrolling: touch; }
@media (max-width: 720px) {
#results-table table { font-size: .85rem; }
#results-table th, #results-table td { padding: .42rem .5rem; }
.chips { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; padding-bottom: .35rem; }
.chip { white-space: nowrap; flex: 0 0 auto; padding: .55rem .8rem; }
textarea { font-size: 1rem; } /* <16px triggers iOS Safari focus-zoom */
.mode-switch button { padding: .5rem .8rem; }
.press-controls button { padding: .45rem .9rem; }
label.toggle input { width: 1.1rem; height: 1.1rem; }
}
@media (max-width: 480px) {
body { padding: 1.5rem 1rem 2.5rem; }
.tabs button { padding: .55rem .55rem; letter-spacing: .08em; white-space: nowrap; }
}
@keyframes settle { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
</style>
</head>
<body>
<header>
<h1 class="gazette-name">The Post‑OCR Gazette</h1>
<div class="dateline"><span>Diffusion <span style="font-variant:normal">vs</span> Autoregression · June 2026</span></div>
<hr class="double-rule" />
<p class="standfirst">
<a href="https://huggingface.co/google/diffusiongemma-26B-A4B-it">DiffusionGemma</a> is an experimental
language model that generates text by <em>denoising</em> 256 tokens in parallel rather than writing one
token at a time. This demo uses it to correct noisy OCR from historical newspapers, head‑to‑head against a
conventional autoregressive model (<a href="https://huggingface.co/google/gemma-4-E4B-it">Gemma‑4‑E4B</a>).
</p>
<p class="standfirst" style="margin-top:.5rem">
<b>How to use it:</b> pick a passage below (or paste your own), press <b>Correct this text</b>, and watch the
correction emerge step by step. On a 75‑passage benchmark the most accurate corrector was the
parameter‑matched autoregressive model (Gemma‑4‑26B MoE, see the results ledger) — but the diffusion
model came close, roughly <em>10× faster</em>, and beat the smaller AR baseline on both quality and speed.
</p>
<p class="standfirst" style="margin-top:.5rem; font-size:.88rem; font-style:italic; color:var(--ink-soft)">
All experiments ran on <a href="https://huggingface.co/docs/hub/jobs">Hugging Face Jobs</a>
— benchmark scripts &amp; README <a href="https://huggingface.co/buckets/davanstrien/diffusiongemma-ocr-bench">in this bucket</a>.
</p>
</header>
<section class="desk">
<div class="chips" id="chips"></div>
<textarea id="ocr-input" spellcheck="false" placeholder="Paste noisy OCR text here, or pick a passage above…"></textarea>
<details class="authority" id="authority">
<summary>Human transcription of this passage</summary>
<div class="gold-text" id="gold-text"></div>
</details>
<details class="authority" id="page-image">
<summary>View the original page (full page — the passage is one excerpt from it)</summary>
<div class="gold-text" style="text-align:center">
<img id="page-img" loading="lazy" alt="Scanned source page" style="max-width:100%;border:1px solid var(--rule)" />
<div id="page-caption" style="font-size:.78rem;font-style:italic;margin-top:.4rem"></div>
</div>
</details>
<div class="controls">
<button class="run-btn" id="run">Correct this text</button>
<label class="toggle">
<input type="checkbox" id="canvas-toggle" />
seed canvas with OCR text <em style="font-size:.78em">(experimental — barely edits)</em>
</label>
<span class="charcount" id="charcount"></span>
</div>
</section>
<nav class="tabs">
<button class="active" data-pane="pane-press">Watch it denoise</button>
<button data-pane="pane-compare">Side by side</button>
<button data-pane="pane-ledger">Benchmark results</button>
</nav>
<section class="pane active" id="pane-press">
<div class="press-stage">
<div class="press-status">
<span id="press-label">DiffusionGemma's intermediate output at each denoising step</span>
<span id="press-step"></span>
</div>
<div class="press-canvas settling" id="press-canvas"><span class="placeholder">Run a passage and watch DiffusionGemma refine all 256 tokens at once — from rough draft to corrected text in a handful of steps. The final step shows what changed, highlighted against the input.</span></div>
<div class="press-controls">
<button id="press-replay" disabled>replay</button>
<input type="range" id="press-slider" min="0" max="0" value="0" step="1" disabled />
</div>
<div class="press-stats" id="press-stats"></div>
<div class="no-gold-note" id="no-gold-note-press">
⚠ No human transcription for this passage — corrections cannot be verified.
Fluent output is not necessarily <em>correct</em> output.
</div>
</div>
</section>
<section class="pane" id="pane-compare">
<div class="mode-switch" id="mode-switch">
<button class="active" data-mode="diff">marks vs the OCR input</button>
<button data-mode="diff_gold" id="gold-mode-btn" disabled>marks vs the human transcription</button>
</div>
<div class="no-gold-note" id="no-gold-note">
⚠ No human transcription exists for this passage, so corrections cannot be verified —
fluent output is not necessarily <em>correct</em> output. Models may invent plausible readings.
</div>
<div class="columns">
<div class="col">
<div class="col-head">
<h2>DiffusionGemma</h2>
<div class="col-sub"><a href="https://huggingface.co/google/diffusiongemma-26B-A4B-it">26B‑A4B‑it</a> · diffusion: denoises 256 tokens in parallel</div>
</div>
<div class="statline" id="stats-dg"><span class="placeholder">no run yet</span></div>
<div class="proof" id="proof-dg"><span class="placeholder">Corrected text appears here.</span></div>
</div>
<div class="col">
<div class="col-head">
<h2>Gemma‑4‑E4B</h2>
<div class="col-sub"><a href="https://huggingface.co/google/gemma-4-E4B-it">E4B‑it</a> · autoregressive: one token at a time, greedy</div>
</div>
<div class="statline" id="stats-g4"><span class="placeholder">no run yet</span></div>
<div class="proof" id="proof-g4"><span class="placeholder">Corrected text appears here.</span></div>
</div>
</div>
<div class="legend">
<span><span class="seg-changed">changed</span></span>
<span><span class="seg-added">added</span></span>
<span><span class="seg-removed">removed</span></span>
<span id="legend-note">relative to the OCR input</span>
</div>
</section>
<section class="pane" id="pane-ledger">
<div class="footnotes" style="margin-bottom:1.4rem">
<p><b>The data.</b> 75 passages from <a href="https://doi.org/10.15131/shef.data.25439023">BLN600</a>,
a corpus of 600 excerpts of 19th‑century London newspapers (largely crime reporting) from the British
Library's collections, each paired with both the original OCR and a careful <em>human transcription</em>.
That human transcription is the “right answer” every number below is measured against. Passages longer
than DiffusionGemma's 256‑token output block were trimmed at a point where OCR and transcription align,
so the pairs stay parallel. (BLN600 is CC‑BY‑NC, so the passages themselves aren't republished here — only
these metrics.)</p>
<p><b>The task.</b> Both models got the identical instruction — fix recognition errors only, don't modernise
or rephrase — one passage at a time on the same A100 GPU. <b>CER / WER</b>: how far the output remains from
the human transcription, by character / by word (the “OCR input” row is the damage before any correction).
<b>Relative CER reduction</b>: how much of that damage the model repaired.
<b>Over‑correction</b>: how much text that was already right the model needlessly changed.
<b>Fix rate</b>: how much of what was actually wrong it fixed.</p>
<p><b>The full record.</b> Every experiment behind these numbers — scripts, configs, findings
(including the negative results) — is logged in a public
<a href="https://huggingface.co/buckets/davanstrien/diffusiongemma-ocr-bench">experiment-log bucket</a>;
all runs executed on <a href="https://huggingface.co/docs/hub/jobs">Hugging Face Jobs</a>.</p>
</div>
<div id="results-table"><p style="text-align:center;font-style:italic">Fetching the ledger…</p></div>
<div class="footnotes" id="results-notes"></div>
</section>
<footer>
<p>
Benchmark: 75 passages of <a href="https://doi.org/10.15131/shef.data.25439023">BLN600</a> (CC‑BY‑NC; metrics only republished).
Demo passages: ICDAR2019 (CC‑BY‑4.0). DiffusionGemma is experimental; single run, no significance testing.
</p>
<p>Powered by ZeroGPU · by <a href="https://huggingface.co/davanstrien">davanstrien</a></p>
</footer>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const $ = (id) => document.getElementById(id);
const MAX_CHARS = 1200;
let client = null, examples = [], activeExample = null;
let diffMode = "diff";
let lastResults = { dg: null, g4: null };
let snapshots = [], replayTimer = null;
let clientPromise = null;
function getClient() {
// re-creatable on failure: one flaky connect must not brick the run button
if (!clientPromise) {
clientPromise = Client.connect(window.location.origin)
.catch((err) => { clientPromise = null; throw err; });
}
return clientPromise;
}
/* ---------- tabs ---------- */
document.querySelectorAll(".tabs button").forEach(b => b.addEventListener("click", () => {
document.querySelectorAll(".tabs button").forEach(x => x.classList.toggle("active", x === b));
document.querySelectorAll(".pane").forEach(p => p.classList.toggle("active", p.id === b.dataset.pane));
}));
function switchTab(paneId) { document.querySelector(`.tabs button[data-pane=${paneId}]`).click(); }
/* ---------- diff rendering ---------- */
function renderDiff(el, segs) {
el.innerHTML = "";
for (const s of segs) {
const span = document.createElement("span");
if (s.op !== "same") span.className = "seg-" + s.op;
span.textContent = s.op === "removed" ? (s.text.trim() ? s.text : " ⌫ ") : s.text;
el.appendChild(span);
}
}
function renderProofs() {
for (const [key, el] of [["dg", $("proof-dg")], ["g4", $("proof-g4")]]) {
const r = lastResults[key];
if (!r) continue;
renderDiff(el, diffMode === "diff_gold" && r.diff_gold ? r.diff_gold : r.diff);
}
$("legend-note").textContent = diffMode === "diff_gold"
? "relative to the human transcription (remaining errors)" : "relative to the OCR input";
}
function setMode(mode) {
diffMode = mode;
document.querySelectorAll("#mode-switch button").forEach(b => b.classList.toggle("active", b.dataset.mode === mode));
renderProofs();
}
document.querySelectorAll("#mode-switch button").forEach(b =>
b.addEventListener("click", () => { if (!b.disabled) setMode(b.dataset.mode); }));
function setGoldAvailable(gold) {
const btn = $("gold-mode-btn");
if (gold) { btn.disabled = false; $("authority").classList.add("visible"); $("gold-text").textContent = gold; }
else { btn.disabled = true; $("authority").classList.remove("visible"); if (diffMode === "diff_gold") setMode("diff"); }
for (const id of ["no-gold-note", "no-gold-note-press"])
$(id).classList.toggle("visible", !gold);
}
function setPageImage(e) {
const panel = $("page-image");
if (e && e.image) {
panel.classList.add("visible");
panel.open = false;
$("page-img").src = e.image;
$("page-caption").textContent = e.image_caption || "";
} else {
panel.classList.remove("visible");
$("page-img").removeAttribute("src");
}
}
function currentGold() {
return activeExample && $("ocr-input").value === activeExample.ocr_input ? (activeExample.gold || "") : "";
}
function stats(el, r, extra = "") {
el.innerHTML = `<span><b>${r.seconds}</b> s</span><span><b>${r.tokens_per_second}</b> tok/s</span>` +
(r.denoising_steps ? `<span><b>${r.denoising_steps}</b> steps</span>` : "") + extra;
}
function spinner(el, msg) { el.innerHTML = `<span class="spinner">${msg}</span>`; }
function showError(el, msg) { el.innerHTML = `<div class="error-box">${msg}</div>`; }
/* ---------- the press (hero replay) ---------- */
function pressShow(i) {
const canvas = $("press-canvas");
const last = i >= snapshots.length - 1;
canvas.classList.toggle("settling", !last);
if (last && lastResults.dg) { renderDiff(canvas, lastResults.dg.diff); }
else { canvas.textContent = snapshots[i] || ""; }
$("press-step").textContent = snapshots.length ? `step ${Math.min(i + 1, snapshots.length)} of ${snapshots.length}` : "";
$("press-slider").value = i;
}
function pressReplay() {
if (!snapshots.length) return;
clearInterval(replayTimer);
let i = 0;
pressShow(0);
replayTimer = setInterval(() => {
i += 1;
if (i >= snapshots.length) { clearInterval(replayTimer); pressShow(snapshots.length - 1); return; }
pressShow(i);
}, 450);
}
function pressLoad(snaps, dgResult) {
snapshots = snaps || [];
clearInterval(replayTimer);
const slider = $("press-slider");
slider.disabled = $("press-replay").disabled = !snapshots.length;
slider.max = Math.max(snapshots.length - 1, 0);
$("press-stats").innerHTML = dgResult
? `corrected <b>${dgResult.tokens_per_second ? "" : ""}256 tokens</b> in <b>${dgResult.denoising_steps}</b> parallel steps · <b>${dgResult.seconds}s</b>` +
(dgResult.canvas_init ? " · <em>OCR-seeded canvas</em>" : "")
: "";
if (snapshots.length) pressReplay();
}
$("press-slider").addEventListener("input", (e) => { clearInterval(replayTimer); pressShow(+e.target.value); });
$("press-replay").addEventListener("click", pressReplay);
/* ---------- examples ---------- */
async function loadExamples() {
const res = await fetch("data/examples");
examples = await res.json();
const chips = $("chips");
examples.forEach((e, i) => {
const b = document.createElement("button");
b.className = "chip"; b.textContent = e.label;
b.onclick = () => {
document.querySelectorAll(".chip").forEach(c => c.classList.remove("active"));
b.classList.add("active");
$("ocr-input").value = e.ocr_input;
activeExample = e;
updateCount();
setGoldAvailable(e.gold || "");
setPageImage(e);
if (e.cached) renderCached(e);
};
chips.appendChild(b);
if (i === 0) b.click();
});
}
function renderCached(e) {
const dg = e.cached.diffusiongemma, g4 = e.cached.gemma4;
const tag = `<span class="cached-tag">— pre‑computed</span>`;
if (dg) {
lastResults.dg = dg;
stats($("stats-dg"), { seconds: dg.seconds, tokens_per_second: Math.round(dg.tokens_generated / dg.seconds), denoising_steps: dg.denoising_steps }, tag);
}
if (g4) {
lastResults.g4 = g4;
stats($("stats-g4"), { seconds: g4.seconds, tokens_per_second: Math.round(g4.tokens_generated / g4.seconds) }, tag);
}
renderProofs();
pressLoad([], null);
$("press-canvas").classList.add("settling");
$("press-canvas").innerHTML = `<span class="placeholder">A pre‑computed result for this example is in the Side by side tab — press “Correct this text” to run it live and watch the denoising here.</span>`;
$("press-stats").innerHTML = ""; $("press-step").textContent = "";
}
function updateCount() {
const n = $("ocr-input").value.length;
const el = $("charcount");
el.textContent = `${n} / ${MAX_CHARS}`;
el.classList.toggle("over", n > MAX_CHARS);
}
$("ocr-input").addEventListener("input", () => { activeExample = null; updateCount(); setGoldAvailable(""); setPageImage(null); });
updateCount();
/* ---------- run ---------- */
$("run").addEventListener("click", async () => {
const text = $("ocr-input").value.trim();
if (!text) return;
if (text.length > MAX_CHARS) {
switchTab("pane-press");
$("press-canvas").innerHTML = `<div class="error-box">Input too long (${text.length} chars; cap ${MAX_CHARS} — DiffusionGemma writes a single 256‑token block).</div>`;
return;
}
const btn = $("run");
btn.disabled = true;
switchTab("pane-press");
clearInterval(replayTimer);
$("press-canvas").classList.add("settling");
$("press-canvas").innerHTML = `<span class="spinner" style="font-style:italic;color:var(--oxblood)">denoising (the steps will replay here when it finishes)</span>`;
$("press-step").textContent = ""; $("press-stats").innerHTML = "";
spinner($("proof-dg"), "denoising"); $("stats-dg").innerHTML = "";
spinner($("proof-g4"), "queued"); $("stats-g4").innerHTML = "";
const gold = currentGold();
try {
client = await getClient();
const dg = (await client.predict("/run_diffusiongemma", {
ocr_text: text, canvas_init: $("canvas-toggle").checked, gold: gold,
})).data[0];
if (dg.error) { showError($("proof-dg"), dg.error); $("press-canvas").innerHTML = `<div class="error-box">${dg.error}</div>`; }
else {
lastResults.dg = dg;
stats($("stats-dg"), dg, dg.canvas_init ? `<span class="cached-tag">— OCR‑seeded canvas</span>` : "");
renderProofs();
pressLoad(dg.snapshots, dg);
}
spinner($("proof-g4"), "composing, token by token");
const g4 = (await client.predict("/run_gemma4", { ocr_text: text, gold: gold })).data[0];
if (g4.error) showError($("proof-g4"), g4.error);
else { lastResults.g4 = g4; stats($("stats-g4"), g4); renderProofs(); }
} catch (err) {
$("press-canvas").innerHTML = `<div class="error-box">Request failed: ${err.message || err}. Try again — the first request after a cold start can time out.</div>`;
} finally {
btn.disabled = false;
}
});
/* ---------- ledger ---------- */
function columnDirection(header) {
if (/reduction|Fix rate|tok\/s|↑/i.test(header)) return "higher";
if (/CER|WER|Over|Median|↓/i.test(header)) return "lower";
return null;
}
const parseNum = (c) => { const m = c.replace(/[*%]/g, "").match(/-?\d+(\.\d+)?/); return m ? parseFloat(m[0]) : null; };
async function loadResults() {
try {
const res = await fetch("data/results");
const data = await res.json();
const lines = data.summary_md.split("\n");
const tableLines = lines.filter(l => l.trim().startsWith("|"));
if (tableLines.length > 2) {
const parse = (l) => l.split("|").slice(1, -1).map(c => c.trim());
const head = parse(tableLines[0]);
const rows = tableLines.slice(2).map(parse);
const isBaseline = (r) => /uncorrected/i.test(r[0]);
const isCanvas = (r) => /canvas/i.test(r[0]);
const contenders = rows.filter(r => !isBaseline(r) && !isCanvas(r));
const best = head.map((h, j) => {
const dir = columnDirection(h);
if (!dir || j === 0) return null;
const vals = contenders.map(r => parseNum(r[j])).filter(v => v !== null);
return vals.length ? (dir === "lower" ? Math.min(...vals) : Math.max(...vals)) : null;
});
const headHtml = head.map((h, j) => {
const dir = columnDirection(h);
return `<th>${h.replace(/[↓↑]/g, "").trim()}${dir && j > 0 ? `<span class="hint">${dir} is better</span>` : ""}</th>`;
}).join("");
let html = `<table><thead><tr>${headHtml}</tr></thead><tbody>`;
for (const r of rows) {
const cls = isBaseline(r) ? "baseline" : isCanvas(r) ? "dimmed" : "";
html += `<tr class="${cls}">` + r.map((c, j) => {
const win = cls === "" && best[j] !== null && parseNum(c) === best[j];
return `<td${win ? ' class="win"' : ""}>${c.replace(/\*\*/g, "")}</td>`;
}).join("") + "</tr>";
}
$("results-table").innerHTML = html + "</tbody></table>";
}
const notes = lines.filter(l => /^(Micro|Mean)/.test(l)).map(l => `<p>${l}</p>`).join("");
$("results-notes").innerHTML = notes +
`<p><b>Bold</b> marks the better of the two main models on each measure.
The OCR‑seeded‑canvas row is greyed out: it converges fastest but barely edits anything
(a negative result — see the repo notes), so highlighting its numbers would mislead.
Single run, one prompt, no significance testing — a pragmatic day‑one benchmark, not a study.</p>`;
} catch { $("results-table").innerHTML = "<p style='text-align:center;font-style:italic'>The ledger could not be fetched.</p>"; }
}
loadExamples();
loadResults();
</script>
</body>
</html>