davanstrien/diffusiongemma-ocr-correction
22
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="utf-8" />5<meta name="viewport" content="width=device-width, initial-scale=1" />6<title>The Post-OCR Gazette — DiffusionGemma vs Gemma-4</title>7<!-- The Space renders inside an iframe on huggingface.co, which blocks being8 framed (frame-ancestors) — links must escape the frame or they show9 "refused to connect". No in-page anchors exist, so a blanket base is safe. -->10<base target="_blank" />11<link rel="preconnect" href="https://fonts.googleapis.com" />12<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />13<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" />14<style>15 :root {16 --paper: #f4ecd8;17 --paper-deep: #ece1c6;18 --ink: #1d1a14;19 --ink-soft: #4a4339;20 --rule: #2a251c;21 --oxblood: #7a1f1f;22 --ochre-bg: #e8d27a66;23 --ochre-edge: #a8842a;24 --green-ink: #2e5d34;25 --red-ink: #9c2b22;26 }27 * { box-sizing: border-box; }28 html { background: #d9cdaf; }29 body {30 margin: 0 auto; max-width: 1080px; padding: 2rem 2.2rem 3.5rem;31 background:32 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"),33 var(--paper);34 color: var(--ink);35 font-family: "Old Standard TT", "Iowan Old Style", Georgia, serif;36 font-size: 17px; line-height: 1.55;37 box-shadow: 0 0 50px rgba(0,0,0,.3); min-height: 100vh;38 }39 40 /* ---------- masthead ---------- */41 header { text-align: center; animation: settle .6s ease-out both; }42 .gazette-name { font-family: "UnifrakturCook", serif; font-size: clamp(2.2rem, 5vw, 3.4rem); margin: 0; }43 .dateline {44 display: flex; align-items: center; gap: 1rem; margin: .5rem 0 0;45 font-variant: small-caps; letter-spacing: .2em; font-size: .78rem; color: var(--ink-soft);46 }47 .dateline::before, .dateline::after { content: ""; flex: 1; border-top: 1px solid var(--rule); }48 .double-rule { border: 0; border-top: 3px double var(--rule); margin: .7rem 0 0; }49 .standfirst { max-width: 62ch; margin: .9rem auto 0; color: var(--ink); font-size: 1.04rem; line-height: 1.65; }50 .standfirst a { color: var(--oxblood); }51 52 /* ---------- desk ---------- */53 .desk { margin-top: 1.7rem; animation: settle .6s .1s ease-out both; }54 .chips { display: flex; flex-wrap: wrap; gap: .45rem; justify-content: center; }55 .chip {56 font-family: inherit; font-size: .8rem; cursor: pointer; background: transparent; color: var(--ink);57 border: 1px solid var(--rule); padding: .28rem .65rem; transition: background .15s, color .15s;58 }59 .chip:hover { background: var(--ink); color: var(--paper); }60 .chip.active { background: var(--oxblood); color: var(--paper); border-color: var(--oxblood); }61 textarea {62 width: 100%; min-height: 7.5rem; margin-top: .9rem; padding: .9rem 1rem;63 font-family: "Special Elite", "Courier New", monospace; font-size: .9rem; line-height: 1.65;64 color: var(--ink); background: var(--paper-deep);65 border: 1px solid var(--rule); outline: none; resize: vertical;66 box-shadow: inset 0 1px 5px rgba(0,0,0,.1);67 }68 textarea:focus { border-color: var(--oxblood); }69 .authority { margin-top: .7rem; border: 1px solid var(--rule); background: var(--paper-deep); display: none; }70 .authority.visible { display: block; }71 .authority summary { cursor: pointer; padding: .4rem .8rem; font-variant: small-caps; letter-spacing: .15em; font-size: .76rem; color: var(--ink-soft); }72 .authority .gold-text { padding: 0 1rem .7rem; font-size: .93rem; line-height: 1.65; }73 .controls { display: flex; flex-wrap: wrap; align-items: center; gap: 1.1rem; margin-top: .8rem; }74 .run-btn {75 font-family: inherit; font-variant: small-caps; letter-spacing: .15em; font-size: 1rem;76 background: var(--ink); color: var(--paper); border: 1px solid var(--ink);77 padding: .5rem 1.5rem; cursor: pointer; transition: background .15s;78 }79 .run-btn:hover { background: var(--oxblood); border-color: var(--oxblood); }80 .run-btn[disabled] { opacity: .45; cursor: wait; }81 label.toggle { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--ink-soft); cursor: pointer; }82 label.toggle input { accent-color: var(--oxblood); }83 .charcount { margin-left: auto; font-size: .76rem; color: var(--ink-soft); }84 .charcount.over { color: var(--red-ink); }85 86 /* ---------- tabs ---------- */87 .tabs { display: flex; justify-content: center; gap: 0; margin-top: 2rem; border-bottom: 3px double var(--rule); animation: settle .6s .18s ease-out both; }88 .tabs button {89 font-family: inherit; font-variant: small-caps; letter-spacing: .14em; font-size: .92rem;90 background: transparent; color: var(--ink-soft); border: 0; padding: .5rem 1.3rem; cursor: pointer;91 border-bottom: 3px solid transparent; margin-bottom: -3px;92 }93 .tabs button.active { color: var(--ink); border-bottom-color: var(--oxblood); font-weight: 700; }94 .pane { display: none; padding-top: 1.5rem; }95 .pane.active { display: block; }96 97 /* ---------- the press (hero) ---------- */98 .press-stage { max-width: 760px; margin: 0 auto; }99 .press-status {100 display: flex; justify-content: space-between; align-items: baseline;101 font-variant: small-caps; letter-spacing: .15em; font-size: .78rem; color: var(--ink-soft); min-height: 1.6rem;102 }103 .press-status b { font-size: 1.1rem; color: var(--oxblood); font-variant: normal; letter-spacing: 0; }104 .press-canvas {105 margin-top: .5rem; padding: 1.1rem 1.2rem; min-height: 11rem;106 background: var(--paper-deep); border: 1px solid var(--rule);107 font-size: 1rem; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word;108 box-shadow: inset 0 1px 6px rgba(0,0,0,.08);109 }110 .press-canvas.settling { font-family: "Special Elite", monospace; font-size: .88rem; }111 .press-canvas .placeholder { color: var(--ink-soft); font-style: italic; opacity: .75; }112 .press-controls { display: flex; align-items: center; gap: .9rem; margin-top: .6rem; }113 .press-controls input[type=range] { flex: 1; accent-color: var(--oxblood); }114 .press-controls button {115 font-family: inherit; background: transparent; border: 1px solid var(--rule); cursor: pointer;116 font-size: .8rem; padding: .2rem .7rem;117 }118 .press-stats { text-align: center; margin-top: .8rem; font-size: .85rem; color: var(--ink-soft); min-height: 1.4rem; }119 .press-stats b { color: var(--ink); font-size: 1.05rem; }120 121 /* ---------- comparison ---------- */122 .mode-switch { display: flex; justify-content: center; margin-bottom: 1.2rem; }123 .mode-switch button {124 font-family: inherit; font-variant: small-caps; letter-spacing: .1em; font-size: .76rem;125 background: transparent; color: var(--ink); border: 1px solid var(--rule); padding: .28rem .8rem; cursor: pointer;126 }127 .mode-switch button.active { background: var(--ink); color: var(--paper); }128 .mode-switch button[disabled] { opacity: .4; cursor: not-allowed; }129 .no-gold-note {130 display: none; text-align: center; font-size: .82rem; font-style: italic; color: var(--red-ink);131 margin: 0 auto 1rem; max-width: 64ch;132 }133 .no-gold-note.visible { display: block; }134 .columns { display: grid; grid-template-columns: 1fr 1fr; gap: 0 2.4rem; position: relative; }135 .columns::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; border-left: 1px solid var(--rule); }136 @media (max-width: 720px) { .columns { grid-template-columns: 1fr; gap: 2rem; } .columns::before { display: none; } }137 .col-head { border-bottom: 1px solid var(--rule); padding-bottom: .35rem; text-align: center; }138 .col-head h2 { margin: 0; font-size: 1.15rem; }139 .col-head .col-sub { font-size: .74rem; color: var(--ink-soft); }140 .col-head .col-sub a { color: inherit; }141 .statline {142 display: flex; justify-content: center; gap: 1.4rem; padding: .5rem 0; border-bottom: 1px solid var(--rule);143 font-size: .78rem; color: var(--ink-soft); min-height: 2rem; align-items: baseline;144 }145 .statline b { font-size: 1.15rem; color: var(--ink); }146 .statline .cached-tag { color: var(--oxblood); font-style: italic; }147 .proof { padding: .9rem .1rem 0; min-height: 7rem; font-size: .98rem; line-height: 1.75; word-wrap: break-word; }148 .proof .placeholder { color: var(--ink-soft); font-style: italic; opacity: .7; }149 .proof .spinner { font-style: italic; color: var(--oxblood); }150 .proof .spinner::after { content: ""; animation: dots 1.2s steps(4) infinite; }151 @keyframes dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }152 .seg-changed { background: var(--ochre-bg); border-bottom: 2px solid var(--ochre-edge); }153 .seg-added { color: var(--green-ink); border-bottom: 2px solid var(--green-ink); font-weight: 700; }154 .seg-removed { color: var(--red-ink); text-decoration: line-through; opacity: .85; }155 .error-box { border: 1px solid var(--red-ink); color: var(--red-ink); padding: .55rem .85rem; font-style: italic; margin-top: .9rem; }156 .legend { display: flex; gap: 1.4rem; justify-content: center; margin-top: 1.6rem; font-size: .78rem; color: var(--ink-soft); flex-wrap: wrap; }157 158 /* ---------- ledger ---------- */159 table { border-collapse: collapse; margin: 0 auto; font-size: .9rem; }160 th, td { padding: .42rem .85rem; border-bottom: 1px solid #00000022; text-align: right; }161 th:first-child, td:first-child { text-align: left; }162 thead th { border-bottom: 2px solid var(--rule); font-variant: small-caps; letter-spacing: .05em; }163 thead th .hint { display: block; font-variant: normal; letter-spacing: 0; font-weight: 400; font-style: italic; font-size: .68rem; color: var(--ink-soft); }164 tbody tr.baseline td { color: var(--ink-soft); font-style: italic; }165 tbody tr.dimmed td { color: var(--ink-soft); opacity: .7; }166 td.win { font-weight: 700; }167 .footnotes { max-width: 72ch; margin: 1.2rem auto 0; font-size: .84rem; color: var(--ink-soft); }168 .footnotes p { margin: .3rem 0; }169 170 footer { margin-top: 3rem; border-top: 3px double var(--rule); padding-top: .9rem; text-align: center; font-size: .8rem; color: var(--ink-soft); }171 footer a { color: var(--oxblood); }172 173 /* ---------- small screens (from rendered review at 375/768/1366) ---------- */174 #results-table { overflow-x: auto; -webkit-overflow-scrolling: touch; }175 @media (max-width: 720px) {176 #results-table table { font-size: .85rem; }177 #results-table th, #results-table td { padding: .42rem .5rem; }178 .chips { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; padding-bottom: .35rem; }179 .chip { white-space: nowrap; flex: 0 0 auto; padding: .55rem .8rem; }180 textarea { font-size: 1rem; } /* <16px triggers iOS Safari focus-zoom */181 .mode-switch button { padding: .5rem .8rem; }182 .press-controls button { padding: .45rem .9rem; }183 label.toggle input { width: 1.1rem; height: 1.1rem; }184 }185 @media (max-width: 480px) {186 body { padding: 1.5rem 1rem 2.5rem; }187 .tabs button { padding: .55rem .55rem; letter-spacing: .08em; white-space: nowrap; }188 }189 190 @keyframes settle { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }191 @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }192</style>193</head>194<body>195 196<header>197 <h1 class="gazette-name">The Post‑OCR Gazette</h1>198 <div class="dateline"><span>Diffusion <span style="font-variant:normal">vs</span> Autoregression · June 2026</span></div>199 <hr class="double-rule" />200 <p class="standfirst">201 <a href="https://huggingface.co/google/diffusiongemma-26B-A4B-it">DiffusionGemma</a> is an experimental202 language model that generates text by <em>denoising</em> 256 tokens in parallel rather than writing one203 token at a time. This demo uses it to correct noisy OCR from historical newspapers, head‑to‑head against a204 conventional autoregressive model (<a href="https://huggingface.co/google/gemma-4-E4B-it">Gemma‑4‑E4B</a>).205 </p>206 <p class="standfirst" style="margin-top:.5rem">207 <b>How to use it:</b> pick a passage below (or paste your own), press <b>Correct this text</b>, and watch the208 correction emerge step by step. On a 75‑passage benchmark the most accurate corrector was the209 parameter‑matched autoregressive model (Gemma‑4‑26B MoE, see the results ledger) — but the diffusion210 model came close, roughly <em>10× faster</em>, and beat the smaller AR baseline on both quality and speed.211 </p>212 <p class="standfirst" style="margin-top:.5rem; font-size:.88rem; font-style:italic; color:var(--ink-soft)">213 All experiments ran on <a href="https://huggingface.co/docs/hub/jobs">Hugging Face Jobs</a>214 — benchmark scripts & README <a href="https://huggingface.co/buckets/davanstrien/diffusiongemma-ocr-bench">in this bucket</a>.215 </p>216</header>217 218<section class="desk">219 <div class="chips" id="chips"></div>220 <textarea id="ocr-input" spellcheck="false" placeholder="Paste noisy OCR text here, or pick a passage above…"></textarea>221 <details class="authority" id="authority">222 <summary>Human transcription of this passage</summary>223 <div class="gold-text" id="gold-text"></div>224 </details>225 <details class="authority" id="page-image">226 <summary>View the original page (full page — the passage is one excerpt from it)</summary>227 <div class="gold-text" style="text-align:center">228 <img id="page-img" loading="lazy" alt="Scanned source page" style="max-width:100%;border:1px solid var(--rule)" />229 <div id="page-caption" style="font-size:.78rem;font-style:italic;margin-top:.4rem"></div>230 </div>231 </details>232 <div class="controls">233 <button class="run-btn" id="run">Correct this text</button>234 <label class="toggle">235 <input type="checkbox" id="canvas-toggle" />236 seed canvas with OCR text <em style="font-size:.78em">(experimental — barely edits)</em>237 </label>238 <span class="charcount" id="charcount"></span>239 </div>240</section>241 242<nav class="tabs">243 <button class="active" data-pane="pane-press">Watch it denoise</button>244 <button data-pane="pane-compare">Side by side</button>245 <button data-pane="pane-ledger">Benchmark results</button>246</nav>247 248<section class="pane active" id="pane-press">249 <div class="press-stage">250 <div class="press-status">251 <span id="press-label">DiffusionGemma's intermediate output at each denoising step</span>252 <span id="press-step"></span>253 </div>254 <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>255 <div class="press-controls">256 <button id="press-replay" disabled>replay</button>257 <input type="range" id="press-slider" min="0" max="0" value="0" step="1" disabled />258 </div>259 <div class="press-stats" id="press-stats"></div>260 <div class="no-gold-note" id="no-gold-note-press">261 ⚠ No human transcription for this passage — corrections cannot be verified.262 Fluent output is not necessarily <em>correct</em> output.263 </div>264 </div>265</section>266 267<section class="pane" id="pane-compare">268 <div class="mode-switch" id="mode-switch">269 <button class="active" data-mode="diff">marks vs the OCR input</button>270 <button data-mode="diff_gold" id="gold-mode-btn" disabled>marks vs the human transcription</button>271 </div>272 <div class="no-gold-note" id="no-gold-note">273 ⚠ No human transcription exists for this passage, so corrections cannot be verified —274 fluent output is not necessarily <em>correct</em> output. Models may invent plausible readings.275 </div>276 <div class="columns">277 <div class="col">278 <div class="col-head">279 <h2>DiffusionGemma</h2>280 <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>281 </div>282 <div class="statline" id="stats-dg"><span class="placeholder">no run yet</span></div>283 <div class="proof" id="proof-dg"><span class="placeholder">Corrected text appears here.</span></div>284 </div>285 <div class="col">286 <div class="col-head">287 <h2>Gemma‑4‑E4B</h2>288 <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>289 </div>290 <div class="statline" id="stats-g4"><span class="placeholder">no run yet</span></div>291 <div class="proof" id="proof-g4"><span class="placeholder">Corrected text appears here.</span></div>292 </div>293 </div>294 <div class="legend">295 <span><span class="seg-changed">changed</span></span>296 <span><span class="seg-added">added</span></span>297 <span><span class="seg-removed">removed</span></span>298 <span id="legend-note">relative to the OCR input</span>299 </div>300</section>301 302<section class="pane" id="pane-ledger">303 <div class="footnotes" style="margin-bottom:1.4rem">304 <p><b>The data.</b> 75 passages from <a href="https://doi.org/10.15131/shef.data.25439023">BLN600</a>,305 a corpus of 600 excerpts of 19th‑century London newspapers (largely crime reporting) from the British306 Library's collections, each paired with both the original OCR and a careful <em>human transcription</em>.307 That human transcription is the “right answer” every number below is measured against. Passages longer308 than DiffusionGemma's 256‑token output block were trimmed at a point where OCR and transcription align,309 so the pairs stay parallel. (BLN600 is CC‑BY‑NC, so the passages themselves aren't republished here — only310 these metrics.)</p>311 <p><b>The task.</b> Both models got the identical instruction — fix recognition errors only, don't modernise312 or rephrase — one passage at a time on the same A100 GPU. <b>CER / WER</b>: how far the output remains from313 the human transcription, by character / by word (the “OCR input” row is the damage before any correction).314 <b>Relative CER reduction</b>: how much of that damage the model repaired.315 <b>Over‑correction</b>: how much text that was already right the model needlessly changed.316 <b>Fix rate</b>: how much of what was actually wrong it fixed.</p>317 <p><b>The full record.</b> Every experiment behind these numbers — scripts, configs, findings318 (including the negative results) — is logged in a public319 <a href="https://huggingface.co/buckets/davanstrien/diffusiongemma-ocr-bench">experiment-log bucket</a>;320 all runs executed on <a href="https://huggingface.co/docs/hub/jobs">Hugging Face Jobs</a>.</p>321 </div>322 <div id="results-table"><p style="text-align:center;font-style:italic">Fetching the ledger…</p></div>323 <div class="footnotes" id="results-notes"></div>324</section>325 326<footer>327 <p>328 Benchmark: 75 passages of <a href="https://doi.org/10.15131/shef.data.25439023">BLN600</a> (CC‑BY‑NC; metrics only republished).329 Demo passages: ICDAR2019 (CC‑BY‑4.0). DiffusionGemma is experimental; single run, no significance testing.330 </p>331 <p>Powered by ZeroGPU · by <a href="https://huggingface.co/davanstrien">davanstrien</a></p>332</footer>333 334<script type="module">335import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";336 337const $ = (id) => document.getElementById(id);338const MAX_CHARS = 1200;339let client = null, examples = [], activeExample = null;340let diffMode = "diff";341let lastResults = { dg: null, g4: null };342let snapshots = [], replayTimer = null;343 344let clientPromise = null;345function getClient() {346 // re-creatable on failure: one flaky connect must not brick the run button347 if (!clientPromise) {348 clientPromise = Client.connect(window.location.origin)349 .catch((err) => { clientPromise = null; throw err; });350 }351 return clientPromise;352}353 354/* ---------- tabs ---------- */355document.querySelectorAll(".tabs button").forEach(b => b.addEventListener("click", () => {356 document.querySelectorAll(".tabs button").forEach(x => x.classList.toggle("active", x === b));357 document.querySelectorAll(".pane").forEach(p => p.classList.toggle("active", p.id === b.dataset.pane));358}));359function switchTab(paneId) { document.querySelector(`.tabs button[data-pane=${paneId}]`).click(); }360 361/* ---------- diff rendering ---------- */362function renderDiff(el, segs) {363 el.innerHTML = "";364 for (const s of segs) {365 const span = document.createElement("span");366 if (s.op !== "same") span.className = "seg-" + s.op;367 span.textContent = s.op === "removed" ? (s.text.trim() ? s.text : " ⌫ ") : s.text;368 el.appendChild(span);369 }370}371function renderProofs() {372 for (const [key, el] of [["dg", $("proof-dg")], ["g4", $("proof-g4")]]) {373 const r = lastResults[key];374 if (!r) continue;375 renderDiff(el, diffMode === "diff_gold" && r.diff_gold ? r.diff_gold : r.diff);376 }377 $("legend-note").textContent = diffMode === "diff_gold"378 ? "relative to the human transcription (remaining errors)" : "relative to the OCR input";379}380function setMode(mode) {381 diffMode = mode;382 document.querySelectorAll("#mode-switch button").forEach(b => b.classList.toggle("active", b.dataset.mode === mode));383 renderProofs();384}385document.querySelectorAll("#mode-switch button").forEach(b =>386 b.addEventListener("click", () => { if (!b.disabled) setMode(b.dataset.mode); }));387function setGoldAvailable(gold) {388 const btn = $("gold-mode-btn");389 if (gold) { btn.disabled = false; $("authority").classList.add("visible"); $("gold-text").textContent = gold; }390 else { btn.disabled = true; $("authority").classList.remove("visible"); if (diffMode === "diff_gold") setMode("diff"); }391 for (const id of ["no-gold-note", "no-gold-note-press"])392 $(id).classList.toggle("visible", !gold);393}394function setPageImage(e) {395 const panel = $("page-image");396 if (e && e.image) {397 panel.classList.add("visible");398 panel.open = false;399 $("page-img").src = e.image;400 $("page-caption").textContent = e.image_caption || "";401 } else {402 panel.classList.remove("visible");403 $("page-img").removeAttribute("src");404 }405}406function currentGold() {407 return activeExample && $("ocr-input").value === activeExample.ocr_input ? (activeExample.gold || "") : "";408}409function stats(el, r, extra = "") {410 el.innerHTML = `<span><b>${r.seconds}</b> s</span><span><b>${r.tokens_per_second}</b> tok/s</span>` +411 (r.denoising_steps ? `<span><b>${r.denoising_steps}</b> steps</span>` : "") + extra;412}413function spinner(el, msg) { el.innerHTML = `<span class="spinner">${msg}</span>`; }414function showError(el, msg) { el.innerHTML = `<div class="error-box">${msg}</div>`; }415 416/* ---------- the press (hero replay) ---------- */417function pressShow(i) {418 const canvas = $("press-canvas");419 const last = i >= snapshots.length - 1;420 canvas.classList.toggle("settling", !last);421 if (last && lastResults.dg) { renderDiff(canvas, lastResults.dg.diff); }422 else { canvas.textContent = snapshots[i] || ""; }423 $("press-step").textContent = snapshots.length ? `step ${Math.min(i + 1, snapshots.length)} of ${snapshots.length}` : "";424 $("press-slider").value = i;425}426function pressReplay() {427 if (!snapshots.length) return;428 clearInterval(replayTimer);429 let i = 0;430 pressShow(0);431 replayTimer = setInterval(() => {432 i += 1;433 if (i >= snapshots.length) { clearInterval(replayTimer); pressShow(snapshots.length - 1); return; }434 pressShow(i);435 }, 450);436}437function pressLoad(snaps, dgResult) {438 snapshots = snaps || [];439 clearInterval(replayTimer);440 const slider = $("press-slider");441 slider.disabled = $("press-replay").disabled = !snapshots.length;442 slider.max = Math.max(snapshots.length - 1, 0);443 $("press-stats").innerHTML = dgResult444 ? `corrected <b>${dgResult.tokens_per_second ? "" : ""}256 tokens</b> in <b>${dgResult.denoising_steps}</b> parallel steps · <b>${dgResult.seconds}s</b>` +445 (dgResult.canvas_init ? " · <em>OCR-seeded canvas</em>" : "")446 : "";447 if (snapshots.length) pressReplay();448}449$("press-slider").addEventListener("input", (e) => { clearInterval(replayTimer); pressShow(+e.target.value); });450$("press-replay").addEventListener("click", pressReplay);451 452/* ---------- examples ---------- */453async function loadExamples() {454 const res = await fetch("data/examples");455 examples = await res.json();456 const chips = $("chips");457 examples.forEach((e, i) => {458 const b = document.createElement("button");459 b.className = "chip"; b.textContent = e.label;460 b.onclick = () => {461 document.querySelectorAll(".chip").forEach(c => c.classList.remove("active"));462 b.classList.add("active");463 $("ocr-input").value = e.ocr_input;464 activeExample = e;465 updateCount();466 setGoldAvailable(e.gold || "");467 setPageImage(e);468 if (e.cached) renderCached(e);469 };470 chips.appendChild(b);471 if (i === 0) b.click();472 });473}474function renderCached(e) {475 const dg = e.cached.diffusiongemma, g4 = e.cached.gemma4;476 const tag = `<span class="cached-tag">— pre‑computed</span>`;477 if (dg) {478 lastResults.dg = dg;479 stats($("stats-dg"), { seconds: dg.seconds, tokens_per_second: Math.round(dg.tokens_generated / dg.seconds), denoising_steps: dg.denoising_steps }, tag);480 }481 if (g4) {482 lastResults.g4 = g4;483 stats($("stats-g4"), { seconds: g4.seconds, tokens_per_second: Math.round(g4.tokens_generated / g4.seconds) }, tag);484 }485 renderProofs();486 pressLoad([], null);487 $("press-canvas").classList.add("settling");488 $("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>`;489 $("press-stats").innerHTML = ""; $("press-step").textContent = "";490}491 492function updateCount() {493 const n = $("ocr-input").value.length;494 const el = $("charcount");495 el.textContent = `${n} / ${MAX_CHARS}`;496 el.classList.toggle("over", n > MAX_CHARS);497}498$("ocr-input").addEventListener("input", () => { activeExample = null; updateCount(); setGoldAvailable(""); setPageImage(null); });499updateCount();500 501/* ---------- run ---------- */502$("run").addEventListener("click", async () => {503 const text = $("ocr-input").value.trim();504 if (!text) return;505 if (text.length > MAX_CHARS) {506 switchTab("pane-press");507 $("press-canvas").innerHTML = `<div class="error-box">Input too long (${text.length} chars; cap ${MAX_CHARS} — DiffusionGemma writes a single 256‑token block).</div>`;508 return;509 }510 const btn = $("run");511 btn.disabled = true;512 switchTab("pane-press");513 clearInterval(replayTimer);514 $("press-canvas").classList.add("settling");515 $("press-canvas").innerHTML = `<span class="spinner" style="font-style:italic;color:var(--oxblood)">denoising (the steps will replay here when it finishes)</span>`;516 $("press-step").textContent = ""; $("press-stats").innerHTML = "";517 spinner($("proof-dg"), "denoising"); $("stats-dg").innerHTML = "";518 spinner($("proof-g4"), "queued"); $("stats-g4").innerHTML = "";519 const gold = currentGold();520 try {521 client = await getClient();522 const dg = (await client.predict("/run_diffusiongemma", {523 ocr_text: text, canvas_init: $("canvas-toggle").checked, gold: gold,524 })).data[0];525 if (dg.error) { showError($("proof-dg"), dg.error); $("press-canvas").innerHTML = `<div class="error-box">${dg.error}</div>`; }526 else {527 lastResults.dg = dg;528 stats($("stats-dg"), dg, dg.canvas_init ? `<span class="cached-tag">— OCR‑seeded canvas</span>` : "");529 renderProofs();530 pressLoad(dg.snapshots, dg);531 }532 spinner($("proof-g4"), "composing, token by token");533 const g4 = (await client.predict("/run_gemma4", { ocr_text: text, gold: gold })).data[0];534 if (g4.error) showError($("proof-g4"), g4.error);535 else { lastResults.g4 = g4; stats($("stats-g4"), g4); renderProofs(); }536 } catch (err) {537 $("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>`;538 } finally {539 btn.disabled = false;540 }541});542 543/* ---------- ledger ---------- */544function columnDirection(header) {545 if (/reduction|Fix rate|tok\/s|↑/i.test(header)) return "higher";546 if (/CER|WER|Over|Median|↓/i.test(header)) return "lower";547 return null;548}549const parseNum = (c) => { const m = c.replace(/[*%]/g, "").match(/-?\d+(\.\d+)?/); return m ? parseFloat(m[0]) : null; };550 551async function loadResults() {552 try {553 const res = await fetch("data/results");554 const data = await res.json();555 const lines = data.summary_md.split("\n");556 const tableLines = lines.filter(l => l.trim().startsWith("|"));557 if (tableLines.length > 2) {558 const parse = (l) => l.split("|").slice(1, -1).map(c => c.trim());559 const head = parse(tableLines[0]);560 const rows = tableLines.slice(2).map(parse);561 const isBaseline = (r) => /uncorrected/i.test(r[0]);562 const isCanvas = (r) => /canvas/i.test(r[0]);563 const contenders = rows.filter(r => !isBaseline(r) && !isCanvas(r));564 const best = head.map((h, j) => {565 const dir = columnDirection(h);566 if (!dir || j === 0) return null;567 const vals = contenders.map(r => parseNum(r[j])).filter(v => v !== null);568 return vals.length ? (dir === "lower" ? Math.min(...vals) : Math.max(...vals)) : null;569 });570 const headHtml = head.map((h, j) => {571 const dir = columnDirection(h);572 return `<th>${h.replace(/[↓↑]/g, "").trim()}${dir && j > 0 ? `<span class="hint">${dir} is better</span>` : ""}</th>`;573 }).join("");574 let html = `<table><thead><tr>${headHtml}</tr></thead><tbody>`;575 for (const r of rows) {576 const cls = isBaseline(r) ? "baseline" : isCanvas(r) ? "dimmed" : "";577 html += `<tr class="${cls}">` + r.map((c, j) => {578 const win = cls === "" && best[j] !== null && parseNum(c) === best[j];579 return `<td${win ? ' class="win"' : ""}>${c.replace(/\*\*/g, "")}</td>`;580 }).join("") + "</tr>";581 }582 $("results-table").innerHTML = html + "</tbody></table>";583 }584 const notes = lines.filter(l => /^(Micro|Mean)/.test(l)).map(l => `<p>${l}</p>`).join("");585 $("results-notes").innerHTML = notes +586 `<p><b>Bold</b> marks the better of the two main models on each measure.587 The OCR‑seeded‑canvas row is greyed out: it converges fastest but barely edits anything588 (a negative result — see the repo notes), so highlighting its numbers would mislead.589 Single run, one prompt, no significance testing — a pragmatic day‑one benchmark, not a study.</p>`;590 } catch { $("results-table").innerHTML = "<p style='text-align:center;font-style:italic'>The ledger could not be fetched.</p>"; }591}592 593loadExamples();594loadResults();595</script>596</body>597</html>598 