Spaces:
Running on Zero
Running on Zero
davanstrien HF Staff
v2 results: add parameter-matched Gemma-4-26B MoE arm to ledger; update standfirst claim accordingly
297f63d verified | <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 ; transition: none ; } } | |
| </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 & 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> | |