CoolFace
Apppublic

davanstrien/diffusiongemma-ocr-correction

sourceHugging Faceapache-2.0updated 3mo agoView on Hugging Face
22likes
index.html598 linesDownload Raw Back to root
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 &amp; 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