Spaces:
Paused
Paused
Download index.html from LiquidAI/masked-diffusion: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/LiquidAI/masked-diffusion/resolve/main/index.html
- Command line
-
hf download hf://spaces/LiquidAI/masked-diffusion/index.html
-
curl -L -o index.html https://huggingface.co/spaces/LiquidAI/masked-diffusion/resolve/main/index.html
14.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Masked-diffusion chat · LFM2.5 Encoder</title> | |
| <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=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="style.css"> | |
| <script> | |
| // Private Space auth: HF signs the embedded iframe URL with ?__sign=<token>. | |
| // Incognito / Safari block the third-party .hf.space cookie, so that token is | |
| // the only credential — carry it on same-origin API requests, else /api/* | |
| // returns 404 and the model-ready poll hangs at 92%. | |
| (() => { | |
| const sign = new URLSearchParams(location.search).get("__sign"); | |
| if (!sign) return; | |
| const orig = self.fetch.bind(self); | |
| self.fetch = (input, init) => { | |
| try { | |
| const u = new URL(typeof input === "string" ? input : input.url, location.href); | |
| if (u.origin === location.origin && !u.searchParams.has("__sign")) { | |
| u.searchParams.set("__sign", sign); | |
| input = u.toString(); | |
| } | |
| } catch (e) { /* non-URL input: pass through */ } | |
| return orig(input, init); | |
| }; | |
| })(); | |
| </script> | |
| </head> | |
| <body> | |
| <div class="wrap" id="app"> | |
| <header class="site-intro"> | |
| <h1>Masked-<em>diffusion</em> chat</h1> | |
| <p class="sub">Watch LFM2.5 Encoder turn masked tokens into an answer through iterative denoising.</p> | |
| </header> | |
| <section class="model-strip is-loading" id="model-strip" aria-label="Model status"> | |
| <span class="model-logo-frame" aria-hidden="true"> | |
| <img id="model-logo" class="model-logo" width="56" height="59" alt="" src="lliquid.gif"> | |
| </span> | |
| <div class="model-copy"> | |
| <div class="model-name-row"> | |
| <a href="https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M" target="_blank" rel="noopener">LFM2.5 Encoder</a> | |
| <span class="model-variant">350M parameters</span> | |
| </div> | |
| </div> | |
| <div class="load-block"> | |
| <div class="load-meta"> | |
| <span id="status" role="status" aria-live="polite">Connecting to server…</span> | |
| <span id="load-percent">0%</span> | |
| </div> | |
| <div class="load-track" role="progressbar" aria-label="Model loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="load-progress"></span></div> | |
| </div> | |
| </section> | |
| <main> | |
| <section class="diffusion-workspace" id="workspace" aria-label="Masked-diffusion generator"> | |
| <article class="workspace-card prompt-panel"> | |
| <header class="panel-header"> | |
| <div><h2>Prompt</h2><p>Enter an instruction or question</p></div> | |
| </header> | |
| <form id="composer"> | |
| <label class="sr-only" for="prompt">Prompt</label> | |
| <div class="prompt-stage"> | |
| <textarea id="prompt" rows="5" placeholder="Ask LFM2.5 Encoder…">Give three tips for staying healthy.</textarea> | |
| <div class="prompt-scan" aria-hidden="true"></div> | |
| </div> | |
| <div class="composer-row"> | |
| <span>Enter to diffuse · Shift + Enter for a new line</span> | |
| <button id="go" type="submit" disabled><span>Diffuse</span><i aria-hidden="true">→</i></button> | |
| </div> | |
| </form> | |
| <div class="settings-heading"><h3>Generation settings</h3><span>Customize the denoising path</span></div> | |
| <div class="settings-grid"> | |
| <label class="range-control" for="steps"><span>Step budget</span><output id="stepsV" for="steps">32</output><input type="range" id="steps" min="8" max="128" step="8" value="32"></label> | |
| <label class="range-control" for="mnt"><span>Max tokens</span><output id="mntV" for="mnt">64</output><input type="range" id="mnt" min="16" max="192" step="16" value="64"></label> | |
| <label class="range-control" for="blk"><span>Block size</span><output id="blkV" for="blk">16</output><input type="range" id="blk" min="8" max="64" step="8" value="16"></label> | |
| <label class="range-control" for="temp"><span>Temperature</span><output id="tempV" for="temp">0.00</output><input type="range" id="temp" min="0" max="100" step="5" value="0"></label> | |
| <label class="range-control range-wide" for="tau"><span>Confidence reveal</span><output id="tauV" for="tau">0.90</output><input type="range" id="tau" min="50" max="100" step="5" value="90"></label> | |
| </div> | |
| </article> | |
| <article class="workspace-card response-panel"> | |
| <header class="panel-header response-heading"> | |
| <div><h2>Response</h2></div> | |
| <span class="activity-pill" id="activity"><i></i><span id="activityText">Ready</span></span> | |
| </header> | |
| <div class="response-stage" id="responseStage" aria-live="polite"> | |
| <div class="response-scan" aria-hidden="true"></div> | |
| <div class="empty-state" id="emptyState"> | |
| <div class="idle-orbit" aria-hidden="true"><span></span><i></i></div> | |
| <strong>Ready to denoise</strong> | |
| <p>Your answer will emerge here, token by token.</p> | |
| <div class="idle-masks" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></div> | |
| </div> | |
| <article class="assistant-message" id="assistantMessage" hidden> | |
| <div id="canvas" class="canvas"></div> | |
| <div class="generation-meta"> | |
| <div class="step-track"><span id="stepProgress"></span></div> | |
| <div class="stats" id="live"></div> | |
| </div> | |
| </article> | |
| </div> | |
| </article> | |
| </section> | |
| <section class="examples-panel" aria-label="Example prompts"> | |
| <span class="examples-label">Examples</span> | |
| <div class="example-list"> | |
| <button type="button" class="ex active" data-q="Give three tips for staying healthy.">Health tips</button> | |
| <button type="button" class="ex" data-q="Explain what a black hole is to a five-year-old.">Black hole</button> | |
| <button type="button" class="ex" data-q="Write a short poem about the ocean.">Ocean poem</button> | |
| <button type="button" class="ex" data-q="What are the primary colors?">Primary colors</button> | |
| <button type="button" class="ex" data-q="Give me a concise guide to brewing green tea.">Green tea</button> | |
| <button type="button" class="ex" data-q="Explain why this loop never stops: while (x > 0) { x++; }">Debug a loop</button> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| <script type="module"> | |
| // The model runs on the server (CPU). This script only drives the UI and | |
| // streams denoising frames from /api/generate as newline-delimited JSON. | |
| const MASK = null; // the server sends null for a still-masked position | |
| const $ = (id) => document.getElementById(id); | |
| const setLoadProgress = (percent) => { | |
| const value = Math.max(0, Math.min(100, percent)); | |
| $("load-progress").style.width = `${value}%`; | |
| $("load-percent").textContent = `${Math.round(value)}%`; | |
| document.querySelector(".load-track").setAttribute("aria-valuenow", String(Math.round(value))); | |
| }; | |
| const setStatus = (message, state = "loading") => { | |
| $("status").textContent = message; | |
| const badge = $("model-strip"); | |
| badge.classList.toggle("is-loading", state === "loading"); | |
| badge.classList.toggle("is-preparing", state === "preparing"); | |
| badge.classList.toggle("is-ready", state === "ready"); | |
| badge.classList.toggle("is-error", state === "error"); | |
| }; | |
| function updateRangeFill(el) { | |
| const pct = ((+el.value - +el.min) / (+el.max - +el.min)) * 100; | |
| el.style.setProperty("--pct", `${pct}%`); | |
| } | |
| for (const [s, l] of [["steps","stepsV"],["mnt","mntV"],["blk","blkV"]]) | |
| $(s).oninput = () => { updateRangeFill($(s)); $(l).textContent = $(s).value; }; | |
| $("temp").oninput = () => { updateRangeFill($("temp")); $("tempV").textContent = ($("temp").value / 100).toFixed(2); }; | |
| $("tau").oninput = () => { updateRangeFill($("tau")); $("tauV").textContent = ($("tau").value / 100).toFixed(2); }; | |
| document.querySelectorAll("input[type=range]").forEach(updateRangeFill); | |
| // ---------- wait for the server-side model to be ready ---------- | |
| async function waitForServer() { | |
| setStatus("Connecting to server…", "loading"); | |
| setLoadProgress(12); | |
| for (let attempt = 0; attempt < 900; attempt++) { | |
| try { | |
| const r = await fetch("api/ready", { cache: "no-store" }); | |
| if (r.ok && (await r.json()).ready) return true; | |
| } catch (e) { /* server still starting */ } | |
| setStatus("Warming up model on the server…", "preparing"); | |
| setLoadProgress(Math.min(92, 12 + attempt * 4)); | |
| await new Promise((r) => setTimeout(r, 1000)); | |
| } | |
| return false; | |
| } | |
| (async () => { | |
| const ok = await waitForServer(); | |
| if (ok) { | |
| setLoadProgress(100); | |
| setStatus("Model ready", "ready"); | |
| $("go").disabled = false; | |
| } else { | |
| setStatus("Model unavailable", "error"); | |
| setLoadProgress(100); | |
| $("load-percent").textContent = "Error"; | |
| } | |
| })(); | |
| // ---------- rendering ---------- | |
| const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<"); | |
| function render(tokens, revealed) { | |
| const revealedSet = new Set(revealed); | |
| const parts = []; | |
| for (let i = 0; i < tokens.length; i++) { | |
| if (tokens[i] === MASK) { | |
| parts.push(`<span class="mask" style="--token-index:${i}" aria-label="masked token"><MASK></span>`); | |
| continue; | |
| } | |
| const esc = escapeHtml(tokens[i] || "·"); | |
| parts.push(`<span class="tok${revealedSet.has(i) ? " new" : ""}" style="--token-index:${i}">${esc}</span>`); | |
| } | |
| $("canvas").innerHTML = parts.join(""); | |
| } | |
| // ---------- streamed generation ---------- | |
| async function generate(userText) { | |
| const maxNew = +$("mnt").value; | |
| const params = new URLSearchParams({ | |
| prompt: userText, | |
| mnt: $("mnt").value, steps: $("steps").value, blk: $("blk").value, | |
| temp: $("temp").value, tau: $("tau").value, | |
| }); | |
| const t0 = performance.now(); | |
| const resp = await fetch("api/generate?" + params.toString()); | |
| if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`); | |
| const reader = resp.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = "", steps = 0, finalText = null; | |
| for (;;) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| let nl; | |
| while ((nl = buffer.indexOf("\n")) >= 0) { | |
| const line = buffer.slice(0, nl).trim(); | |
| buffer = buffer.slice(nl + 1); | |
| if (!line) continue; | |
| const msg = JSON.parse(line); | |
| if (msg.error) throw new Error(msg.error); | |
| if (msg.done) { finalText = msg.text; continue; } | |
| steps = msg.step; | |
| render(msg.tokens, msg.revealed); | |
| const doneCount = msg.tokens.filter((t) => t !== MASK).length; | |
| const dt = (performance.now() - t0) / 1000; | |
| const progress = doneCount / maxNew * 100; | |
| $("stepProgress").style.width = `${progress}%`; | |
| $("activityText").textContent = `Denoising · ${Math.round(progress)}%`; | |
| $("live").textContent = `Step ${msg.step} · ${doneCount}/${maxNew} tokens · ${dt.toFixed(2)}s · ${(doneCount / Math.max(dt, .01)).toFixed(0)} tok/s`; | |
| await new Promise((r) => requestAnimationFrame(r)); | |
| } | |
| } | |
| const totalS = (performance.now() - t0) / 1000; | |
| return { text: finalText ?? "", totalS, steps, msPerStep: totalS * 1000 / Math.max(steps, 1), tokPerS: maxNew / totalS }; | |
| } | |
| $("composer").addEventListener("submit", async (event) => { | |
| event.preventDefault(); | |
| const userText = $("prompt").value.trim(); | |
| if (!userText || $("go").disabled) return; | |
| $("assistantMessage").hidden = false; | |
| $("emptyState").hidden = true; | |
| $("canvas").innerHTML = Array.from({ length: +$("mnt").value }, (_, i) => `<span class="mask" style="--token-index:${i}" aria-label="masked token"><MASK></span>`).join(""); | |
| $("live").textContent = "Starting diffusion…"; | |
| $("stepProgress").style.width = "0%"; | |
| $("assistantMessage").setAttribute("aria-busy", "true"); | |
| $("workspace").classList.add("is-generating"); | |
| $("responseStage").classList.add("is-denoising"); | |
| $("responseStage").classList.remove("is-complete"); | |
| $("activity").className = "activity-pill is-running"; | |
| $("activityText").textContent = "Building canvas"; | |
| $("go").disabled = true; | |
| $("go").querySelector("span").textContent = "Denoising"; | |
| try { | |
| const r = await generate(userText); | |
| $("canvas").textContent = r.text || "(empty)"; | |
| $("stepProgress").style.width = "100%"; | |
| $("live").textContent = `${r.steps} steps · ${r.totalS.toFixed(2)}s · ${r.msPerStep.toFixed(0)} ms/step · ${r.tokPerS.toFixed(0)} tok/s`; | |
| $("activity").className = "activity-pill is-done"; | |
| $("activityText").textContent = "Complete"; | |
| $("responseStage").classList.add("is-complete"); | |
| window.__LAST_GEN = r; | |
| } catch (e) { | |
| $("canvas").textContent = "Error: " + (e.message || e); | |
| $("activity").className = "activity-pill is-error"; | |
| $("activityText").textContent = "Could not finish"; | |
| console.error(e); | |
| } | |
| $("assistantMessage").removeAttribute("aria-busy"); | |
| $("workspace").classList.remove("is-generating"); | |
| $("responseStage").classList.remove("is-denoising"); | |
| $("go").disabled = false; | |
| $("go").querySelector("span").textContent = "Diffuse"; | |
| setTimeout(() => $("responseStage").classList.remove("is-complete"), 900); | |
| }); | |
| $("prompt").addEventListener("keydown", (event) => { | |
| if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { | |
| event.preventDefault(); | |
| $("composer").requestSubmit(); | |
| } | |
| }); | |
| document.querySelector(".example-list").addEventListener("click", (event) => { | |
| const option = event.target.closest(".ex"); | |
| if (!option) return; | |
| document.querySelectorAll(".ex").forEach((el) => el.classList.toggle("active", el === option)); | |
| $("prompt").value = option.dataset.q || ""; | |
| $("prompt").dispatchEvent(new Event("input", { bubbles: true })); | |
| document.querySelector(".prompt-panel").classList.remove("example-changed"); | |
| requestAnimationFrame(() => document.querySelector(".prompt-panel").classList.add("example-changed")); | |
| }); | |
| $("prompt").addEventListener("input", () => { | |
| const active = document.querySelector(".ex.active"); | |
| if (!active?.dataset.q || active.dataset.q !== $("prompt").value) { | |
| document.querySelectorAll(".ex").forEach((el) => el.classList.remove("active")); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |