Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>MetaDiffusion-150M - Live Denoising</title> | |
| <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2313141a'/%3E%3Crect x='4' y='4' width='8' height='8' rx='1.5' fill='%236e9fd4'/%3E%3C/svg%3E"> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| max-width: 980px; | |
| margin: 0 auto; | |
| padding: 24px 20px 40px; | |
| color: #c9d1d9; | |
| line-height: 1.55; | |
| background: #13141a; | |
| } | |
| .subtitle { color: #7d8590; font-size: 0.85em; margin-bottom: 14px; } | |
| .subtitle code { | |
| font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; | |
| font-size: 0.95em; color: #9cc4ee; background: #1c1d26; | |
| padding: 1px 5px; border-radius: 3px; | |
| } | |
| .status-line { | |
| font-size: 0.78em; color: #7d8590; margin-bottom: 18px; | |
| padding: 6px 10px; background: #1c1d26; border: 1px solid #2d2e3a; | |
| border-radius: 4px; display: inline-block; max-width: 100%; | |
| } | |
| .status-line span { | |
| margin-right: 16px; max-width: 280px; overflow: hidden; | |
| text-overflow: ellipsis; white-space: nowrap; | |
| display: inline-block; vertical-align: bottom; | |
| } | |
| .status-line span:last-child { margin-right: 0; } | |
| .layout { display: flex; gap: 20px; flex-wrap: wrap; align-items: flex-start; } | |
| .col-left { width: 310px; flex-shrink: 0; } | |
| .col-right { flex: 1; min-width: 320px; } | |
| fieldset { | |
| border: 1px solid #2d2e3a; border-radius: 6px; | |
| padding: 16px; margin: 0 0 14px; background: #1a1b23; | |
| } | |
| legend { font-weight: 600; font-size: 0.82em; padding: 0 6px; color: #9ca3af; } | |
| label { display: block; font-size: 0.8em; margin: 12px 0 3px; color: #9ca3af; font-weight: 500; } | |
| label .val { float: right; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-weight: 400; color: #e6edf3; } | |
| input[type=range] { width: 100%; margin: 2px 0; cursor: pointer; accent-color: #6e9fd4; } | |
| textarea { | |
| width: 100%; height: 72px; resize: vertical; | |
| font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; | |
| font-size: 13px; padding: 8px 10px; | |
| border: 1px solid #2d2e3a; border-radius: 4px; | |
| background: #111218; color: #c9d1d9; line-height: 1.5; | |
| } | |
| textarea:focus { outline: none; border-color: #6e9fd4; box-shadow: 0 0 0 2px rgba(110,159,212,0.15); } | |
| .seg { display: flex; border: 1px solid #2d2e3a; border-radius: 4px; overflow: hidden; margin-top: 4px; } | |
| .seg button { | |
| flex: 1; padding: 6px 0; font-size: 0.78em; font-weight: 600; | |
| background: #1e1f28; color: #9ca3af; border: 0; cursor: pointer; | |
| border-right: 1px solid #2d2e3a; transition: background 0.12s, color 0.12s; | |
| } | |
| .seg button:last-child { border-right: 0; } | |
| .seg button:hover { background: #282936; color: #c9d1d9; } | |
| .seg button.active { background: #2b3a52; color: #bcd4ee; } | |
| .seg button:disabled { opacity: 0.38; cursor: not-allowed; } | |
| .seg button:disabled:hover { background: #1e1f28; color: #9ca3af; } | |
| .btn-row { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; } | |
| .btn-row button { | |
| padding: 8px 16px; cursor: pointer; font-size: 0.83em; font-weight: 600; | |
| border: 1px solid #2d2e3a; border-radius: 4px; background: #22232e; color: #c9d1d9; | |
| transition: background 0.12s; | |
| } | |
| .btn-row button:hover { background: #2a2b38; } | |
| .btn-row button:disabled { opacity: 0.4; cursor: not-allowed; } | |
| #runBtn { background: #3b6ea5; color: #e6edf3; border-color: #3b6ea5; } | |
| #runBtn:hover { background: #4a7fb8; } | |
| #runBtn:disabled { background: #3b6ea5; } | |
| #stopBtn { color: #e57373; border-color: #5c3333; background: #1e1518; } | |
| #stopBtn:hover { background: #2a1c20; } | |
| #getWeightsBtn { background: #3b6ea5; color: #e6edf3; border-color: #3b6ea5; } | |
| #getWeightsBtn:hover { background: #4a7fb8; } | |
| #presets { margin-top: 12px; } | |
| #presets button { | |
| font-size: 0.75em; padding: 3px 9px; margin: 0 5px 5px 0; | |
| cursor: pointer; border: 1px solid #2d2e3a; border-radius: 3px; | |
| background: #1e1f28; color: #9ca3af; transition: background 0.12s, color 0.12s; | |
| } | |
| #presets button:hover { background: #282936; border-color: #4a4b5c; color: #c9d1d9; } | |
| #status { font-size: 0.78em; color: #7d8590; margin-top: 12px; font-family: Consolas, Menlo, monospace; } | |
| #status .ok { color: #7ee787; font-weight: 600; } | |
| #status .err { color: #f47067; font-weight: 600; } | |
| #dlbar { margin-top: 8px; height: 4px; background: #22232e; border-radius: 2px; overflow: hidden; } | |
| #dlbar > div { height: 100%; width: 0%; background: #6e9fd4; transition: width 0.2s; } | |
| #cacheInfo { | |
| font-size: 0.78em; color: #7d8590; font-family: Consolas, Menlo, monospace; | |
| margin-bottom: 4px; min-height: 2.6em; | |
| } | |
| #cacheInfo b { color: #e6edf3; font-weight: 600; } | |
| #probe { | |
| min-height: 140px; max-height: 300px; overflow-y: auto; | |
| border: 1px solid #2d2e3a; border-radius: 4px; | |
| padding: 12px 14px; margin-bottom: 10px; | |
| font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; | |
| font-size: 13px; line-height: 1.8; | |
| background: #111218; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| transition: border-color 0.15s; | |
| } | |
| #probe:hover { border-color: #3a3c4d; } | |
| .tok { display: inline; white-space: pre-wrap; word-break: break-word; } | |
| .tok.prompt { color: #7ee787; } | |
| .tok.mask { color: #3d3f50; } | |
| .tok.fresh { background: #263352; border-radius: 2px; } | |
| #meta { | |
| font-size: 0.76em; color: #7d8590; | |
| font-family: Consolas, Menlo, monospace; | |
| margin: 8px 0; padding: 0 2px; | |
| } | |
| #meta b { color: #e6edf3; font-weight: 600; } | |
| #progressWrap { height: 5px; background: #22232e; border-radius: 3px; margin: 8px 0; overflow: hidden; } | |
| #progressFill { height: 100%; width: 0%; background: #6e9fd4; border-radius: 3px; transition: width 0.15s; } | |
| .sparkhead { | |
| display: flex; justify-content: space-between; | |
| font-size: 0.72em; color: #7d8590; | |
| font-family: Consolas, Menlo, monospace; | |
| margin-top: 10px; padding: 0 2px; | |
| } | |
| .sparkhead b { color: #e6edf3; font-weight: 600; } | |
| #spark { | |
| width: 100%; height: 40px; display: block; | |
| background: #111218; border: 1px solid #2d2e3a; border-radius: 4px; | |
| margin-top: 4px; | |
| } | |
| #finalText { | |
| display: none; border: 1px solid #2d2e3a; border-radius: 4px; | |
| padding: 12px 14px; margin-top: 12px; | |
| font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; | |
| font-size: 13px; line-height: 1.7; | |
| white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; | |
| max-height: 240px; overflow-y: auto; background: #111218; color: #c9d1d9; | |
| } | |
| #copyBtn { | |
| display: none; margin-top: 8px; font-size: 0.78em; | |
| padding: 5px 12px; cursor: pointer; | |
| border: 1px solid #2d2e3a; border-radius: 3px; background: #22232e; color: #9ca3af; | |
| transition: background 0.12s, color 0.12s; | |
| } | |
| #copyBtn:hover { background: #2a2b38; color: #c9d1d9; } | |
| footer { | |
| margin-top: 32px; padding-top: 14px; | |
| border-top: 1px solid #2d2e3a; | |
| font-size: 0.76em; color: #555b66; | |
| } | |
| footer a { color: #6e9fd4; text-decoration: none; } | |
| footer a:hover { text-decoration: underline; } | |
| @media (max-width: 700px) { | |
| .layout { flex-direction: column; } | |
| .col-left { width: 100%; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="subtitle"><code>CodeSoft/MetaDiffusion-150M-exp</code> — masked diffusion language model</div> | |
| <div class="status-line"> | |
| <span id="badgeEngine">engine: —</span> | |
| <span id="badgeGpu">gpu: probing…</span> | |
| <span id="badgeWeights">tokenizer: —</span> | |
| </div> | |
| <div class="layout"> | |
| <div class="col-left"> | |
| <fieldset> | |
| <legend>Model weights</legend> | |
| <div id="cacheInfo">Checking local cache…</div> | |
| <div class="btn-row"> | |
| <button id="getWeightsBtn" style="display:none">Download weights</button> | |
| <button id="clearCacheBtn" style="display:none">Clear cache</button> | |
| </div> | |
| <div id="dlbar"><div id="dlfill"></div></div> | |
| </fieldset> | |
| <fieldset> | |
| <legend>Prompt & Controls</legend> | |
| <label for="prompt">Prompt text</label> | |
| <textarea id="prompt" spellcheck="false">The cat sat on the</textarea> | |
| <label>Sequence length <span class="val" id="vSeq">96</span></label> | |
| <input type="range" id="seqLen" min="32" max="256" step="16" value="96"> | |
| <label>Denoising steps <span class="val" id="vSteps">64</span></label> | |
| <input type="range" id="numSteps" min="16" max="128" step="8" value="64"> | |
| <label>Temperature <span class="val" id="vTemp">0.60</span></label> | |
| <input type="range" id="temperature" min="0.3" max="1.5" step="0.05" value="0.6"> | |
| <label>Repetition penalty <span class="val" id="vRep">1.50</span></label> | |
| <input type="range" id="repPenalty" min="1.0" max="2.0" step="0.05" value="1.5"> | |
| <label>Backend</label> | |
| <div class="seg" id="backendSeg"> | |
| <button type="button" data-backend="auto" class="active">Auto</button> | |
| <button type="button" data-backend="webgpu">WebGPU</button> | |
| <button type="button" data-backend="wasm">WASM</button> | |
| </div> | |
| <div id="presets"> | |
| <button data-preset='{"prompt":"Once upon a time,","seqLen":96,"numSteps":64,"temperature":0.6,"repPenalty":1.5}'>Fairy tale</button> | |
| <button data-preset='{"prompt":"The science of","seqLen":128,"numSteps":96,"temperature":0.5,"repPenalty":1.6}'>Science</button> | |
| <button data-preset='{"prompt":"def fibonacci(n):","seqLen":96,"numSteps":64,"temperature":0.2,"repPenalty":1.8}'>Code</button> | |
| <button data-preset='{"prompt":"The cat sat on the","seqLen":160,"numSteps":112,"temperature":0.8,"repPenalty":1.3}'>Creative</button> | |
| </div> | |
| <div class="btn-row"> | |
| <button id="runBtn" disabled>▶ Denoise</button> | |
| <button id="stopBtn" disabled>■ Stop</button> | |
| </div> | |
| <div id="status">Initializing…</div> | |
| </fieldset> | |
| </div> | |
| <div class="col-right"> | |
| <fieldset> | |
| <legend>Live denoising output</legend> | |
| <div id="probe" aria-live="polite"></div> | |
| <div id="meta"> | |
| step <b id="mStep">0</b>/<span id="mSteps">64</span> · | |
| noise <b id="mT">1.00</b> · | |
| masks <b id="mMasks">0</b> · | |
| <b id="mTps">–</b> tok/s | |
| <span id="mEta"></span> | |
| </div> | |
| <div id="progressWrap"><div id="progressFill"></div></div> | |
| <div class="sparkhead"><span>step time</span><b id="mMs">–</b></div> | |
| <canvas id="spark"></canvas> | |
| <div id="finalText"></div> | |
| <button id="copyBtn">Copy output</button> | |
| </fieldset> | |
| </div> | |
| </div> | |
| <footer> | |
| <a href="https://huggingface.co/CodeSoft/MetaDiffusion-150M-exp" target="_blank" rel="noopener">CodeSoft/MetaDiffusion-150M-exp</a> | |
| · Apache-2.0 · int8 ONNX · | |
| <a href="https://onnxruntime.ai" target="_blank" rel="noopener">onnxruntime-web</a> | |
| </footer> | |
| <!-- The ORT bundle is loaded dynamically in loadModel(): ort.webgpu.min.js | |
| (wasm + webgpu EP) when navigator.gpu exists, else the lean ort.min.js. | |
| The default ort.min.js does NOT contain the WebGPU EP. --> | |
| <script id="engine"> | |
| ; | |
| const MD_MASK_TOKEN = 32000; | |
| const MD_SPECIAL_IDS = new Set([0, 1, 2, 3]); | |
| function buildByteMap() { | |
| const map = new Array(256); | |
| const bs = []; | |
| for (let b = 33; b <= 126; b++) bs.push(b); | |
| for (let b = 161; b <= 172; b++) bs.push(b); | |
| for (let b = 174; b <= 255; b++) bs.push(b); | |
| const cs = bs.slice(); | |
| let n = 0; | |
| for (let b = 0; b < 256; b++) { | |
| if (!bs.includes(b)) { bs.push(b); cs.push(256 + n); n++; } | |
| } | |
| for (let i = 0; i < 256; i++) map[bs[i]] = String.fromCharCode(cs[i]); | |
| return map; | |
| } | |
| const BYTE_MAP = buildByteMap(); | |
| const CHAR_TO_BYTE = new Map(); | |
| for (let b = 0; b < 256; b++) CHAR_TO_BYTE.set(BYTE_MAP[b], b); | |
| function strToUtf8Bytes(s) { | |
| return Array.from(new TextEncoder().encode(s)); | |
| } | |
| function bytesToStr(bytes) { | |
| return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes)); | |
| } | |
| class ByteLevelTokenizer { | |
| constructor(json) { | |
| const model = json.model; | |
| this.vocab = new Map(Object.entries(model.vocab)); | |
| this.idToToken = new Array(model.vocab ? Object.keys(model.vocab).length : 0); | |
| for (const [tok, id] of this.vocab) this.idToToken[id] = tok; | |
| this.unkId = model.unk_token ? this.vocab.get(model.unk_token) : 3; | |
| this.merges = new Map(); | |
| (model.merges || []).forEach((m, rank) => { | |
| const pair = Array.isArray(m) ? m[0] + m[1] : m.indexOf(" ") >= 0 ? m.replace(" ", "") : m; | |
| this.merges.set(pair, rank); | |
| }); | |
| const pt = (json.pre_tokenizer || {}); | |
| this.regex = pt.use_regex === false | |
| ? null | |
| : /'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu; | |
| } | |
| byteEncode(str) { | |
| let out = ""; | |
| for (const b of strToUtf8Bytes(str)) out += BYTE_MAP[b]; | |
| return out; | |
| } | |
| bpe(word) { | |
| if (word.length <= 1) return [word]; | |
| let parts = Array.from(word); | |
| while (parts.length > 1) { | |
| let bestRank = Infinity, bestIdx = -1; | |
| for (let i = 0; i < parts.length - 1; i++) { | |
| const rank = this.merges.get(parts[i] + parts[i + 1]); | |
| if (rank !== undefined && rank < bestRank) { bestRank = rank; bestIdx = i; } | |
| } | |
| if (bestIdx === -1) break; | |
| parts.splice(bestIdx, 2, parts[bestIdx] + parts[bestIdx + 1]); | |
| } | |
| return parts; | |
| } | |
| encode(text) { | |
| const ids = []; | |
| if (this.regex) { | |
| const matches = text.match(this.regex); | |
| if (matches) { | |
| for (const piece of matches) { | |
| const encoded = this.byteEncode(piece); | |
| for (const sub of this.bpe(encoded)) { | |
| const id = this.vocab.get(sub); | |
| ids.push(id === undefined ? this.unkId : id); | |
| } | |
| } | |
| } | |
| } else { | |
| for (const sub of this.bpe(this.byteEncode(text))) { | |
| const id = this.vocab.get(sub); | |
| ids.push(id === undefined ? this.unkId : id); | |
| } | |
| } | |
| return ids; | |
| } | |
| decode(ids, skipSpecial = true) { | |
| const bytes = []; | |
| for (const id of ids) { | |
| if (skipSpecial && MD_SPECIAL_IDS.has(id)) continue; | |
| const tok = this.idToToken[id]; | |
| if (tok === undefined) continue; | |
| for (const ch of tok) { | |
| const b = CHAR_TO_BYTE.get(ch); | |
| if (b !== undefined) bytes.push(b); | |
| } | |
| } | |
| return bytesToStr(bytes); | |
| } | |
| } | |
| function cumulativeUnmaskFrac(i, n) { | |
| return 0.5 * (1 - Math.cos(Math.PI * i / n)); | |
| } | |
| async function* denoise(env, params) { | |
| const { prompt, seqLen, numSteps, temperature, repPenalty, runStep, greedy } = params; | |
| const tok = env.tokenizer; | |
| const promptIds = tok.encode(prompt); | |
| if (promptIds.length >= seqLen) { | |
| throw new Error("Prompt is " + promptIds.length + " tokens, must be < seq len " + seqLen + "."); | |
| } | |
| const total = seqLen; | |
| const genLen = total - promptIds.length; | |
| const vocab = env.vocabSize; | |
| const inputIds = new Int32Array(total).fill(MD_MASK_TOKEN); | |
| for (let i = 0; i < promptIds.length; i++) inputIds[i] = promptIds[i]; | |
| const positionIds = new Int32Array(total); | |
| for (let i = 0; i < total; i++) positionIds[i] = i; | |
| const logits = new Float32Array(total * vocab); | |
| const startTime = performance.now(); | |
| for (let step = 0; step < numSteps; step++) { | |
| if (env.cancelled) throw new Error("cancelled"); | |
| const fracNow = cumulativeUnmaskFrac(step, numSteps); | |
| const fracNext = cumulativeUnmaskFrac(step + 1, numSteps); | |
| let nMasked = 0; | |
| for (let i = promptIds.length; i < total; i++) if (inputIds[i] === MD_MASK_TOKEN) nMasked++; | |
| const nTotalToUnmask = Math.round((fracNext - fracNow) * genLen); | |
| let nUnmask; | |
| if (step === numSteps - 1) nUnmask = nMasked; | |
| else nUnmask = nMasked > 0 ? Math.max(nTotalToUnmask, 1) : 0; | |
| const t = 1 - fracNow; | |
| await runStep(inputIds, positionIds, t, logits); | |
| const maskPositions = []; | |
| const seenTokens = new Set(); | |
| for (let i = 0; i < total; i++) { | |
| const row = i * vocab; | |
| logits[row + MD_MASK_TOKEN] = -1e9; | |
| if (inputIds[i] === MD_MASK_TOKEN) maskPositions.push(i); | |
| else seenTokens.add(inputIds[i]); | |
| } | |
| if (repPenalty !== 1.0 && seenTokens.size > 0) { | |
| for (const tokId of seenTokens) { | |
| for (let i = 0; i < total; i++) { | |
| const v = logits[i * vocab + tokId]; | |
| logits[i * vocab + tokId] = v < 0 ? v * repPenalty : v / repPenalty; | |
| } | |
| } | |
| } | |
| const nMask = maskPositions.length; | |
| const rowMaxProb = new Float32Array(total); | |
| const invTemp = 1 / temperature; | |
| for (let i = 0; i < total; i++) { | |
| const row = i * vocab; | |
| let mx = -Infinity; | |
| for (let k = 0; k < vocab; k++) mx = Math.max(mx, logits[row + k]); | |
| let sum = 0; | |
| for (let k = 0; k < vocab; k++) { | |
| const v = Math.exp((logits[row + k] - mx) * invTemp); | |
| logits[row + k] = v; | |
| sum += v; | |
| } | |
| const invSum = 1 / sum; | |
| let pmax = 0; | |
| for (let k = 0; k < vocab; k++) { | |
| const p = logits[row + k] * invSum; | |
| logits[row + k] = p; | |
| if (p > pmax) pmax = p; | |
| } | |
| rowMaxProb[i] = pmax; | |
| } | |
| const sampled = new Int32Array(nMask); | |
| const confidence = new Float32Array(nMask); | |
| for (let j = 0; j < nMask; j++) { | |
| const row = maskPositions[j] * vocab; | |
| if (greedy) { | |
| let best = 0; | |
| for (let k = 1; k < vocab; k++) if (logits[row + k] > logits[row + best]) best = k; | |
| sampled[j] = best; | |
| } else { | |
| const u = Math.random(); | |
| let cdf = 0; | |
| let chosen = vocab - 1; | |
| for (let k = 0; k < vocab; k++) { | |
| cdf += logits[row + k]; | |
| if (cdf >= u) { chosen = k; break; } | |
| } | |
| sampled[j] = chosen; | |
| } | |
| confidence[j] = rowMaxProb[maskPositions[j]]; | |
| } | |
| const order = Array.from({ length: nMask }, (_, j) => j); | |
| order.sort((a, b) => confidence[b] - confidence[a]); | |
| const nFill = Math.min(nUnmask, nMask); | |
| for (let k = 0; k < nFill; k++) { | |
| inputIds[maskPositions[order[k]]] = sampled[order[k]]; | |
| } | |
| yield { | |
| step, numSteps, t, nMasked: nMask, | |
| inputIds: inputIds.slice(), | |
| promptLen: promptIds.length, | |
| elapsed: performance.now() - startTime, | |
| }; | |
| } | |
| } | |
| </script> | |
| <script> | |
| ; | |
| var $ = function(id) { return document.getElementById(id); }; | |
| var els = { | |
| prompt: $("prompt"), seqLen: $("seqLen"), numSteps: $("numSteps"), | |
| temperature: $("temperature"), repPenalty: $("repPenalty"), | |
| vSeq: $("vSeq"), vSteps: $("vSteps"), vTemp: $("vTemp"), vRep: $("vRep"), | |
| runBtn: $("runBtn"), stopBtn: $("stopBtn"), status: $("status"), | |
| dlfill: $("dlfill"), badgeEngine: $("badgeEngine"), badgeWeights: $("badgeWeights"), | |
| badgeGpu: $("badgeGpu"), | |
| cacheInfo: $("cacheInfo"), getWeightsBtn: $("getWeightsBtn"), clearCacheBtn: $("clearCacheBtn"), | |
| probe: $("probe"), mStep: $("mStep"), mSteps: $("mSteps"), mT: $("mT"), | |
| mMasks: $("mMasks"), mEta: $("mEta"), mTps: $("mTps"), mMs: $("mMs"), | |
| spark: $("spark"), | |
| progressFill: $("progressFill"), finalText: $("finalText"), copyBtn: $("copyBtn"), | |
| }; | |
| var ORT_CDN = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/"; | |
| var env = { | |
| tokenizer: null, vocabSize: 32001, session: null, engine: "loading", cancelled: false, | |
| modelBuf: null, backend: "auto", engineName: "-", threads: 1, | |
| hasWebGpuBundle: false, idbOk: true, | |
| cacheState: "checking", cachedSize: 0, cachedSavedAt: 0, modelSize: 0, | |
| stepTimes: [], lastNumSteps: 64, | |
| }; | |
| function setStatus(html) { | |
| els.status.innerHTML = html; | |
| } | |
| function fmtMB(n) { return (n / 1e6).toFixed(0) + " MB"; } | |
| /* ================= IndexedDB weight cache ================= */ | |
| var IDB_NAME = "metadiffusion-cache", IDB_STORE = "weights", IDB_VER = 1; | |
| function openDB() { | |
| return new Promise(function(ok, no) { | |
| var rq = indexedDB.open(IDB_NAME, IDB_VER); | |
| rq.onupgradeneeded = function() { | |
| var db = rq.result; | |
| if (!db.objectStoreNames.contains(IDB_STORE)) db.createObjectStore(IDB_STORE); | |
| }; | |
| rq.onsuccess = function() { ok(rq.result); }; | |
| rq.onerror = function() { no(rq.error); }; | |
| }); | |
| } | |
| function idbGet(key) { | |
| return openDB().then(function(db) { | |
| return new Promise(function(ok, no) { | |
| var rq = db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(key); | |
| rq.onsuccess = function() { ok(rq.result); }; | |
| rq.onerror = function() { no(rq.error); }; | |
| }); | |
| }); | |
| } | |
| function idbPut(key, val) { | |
| return openDB().then(function(db) { | |
| return new Promise(function(ok, no) { | |
| var tx = db.transaction(IDB_STORE, "readwrite"); | |
| tx.objectStore(IDB_STORE).put(val, key); | |
| tx.oncomplete = function() { ok(); }; | |
| tx.onerror = function() { no(tx.error); }; | |
| }); | |
| }); | |
| } | |
| function idbDel(key) { | |
| return openDB().then(function(db) { | |
| return new Promise(function(ok, no) { | |
| var tx = db.transaction(IDB_STORE, "readwrite"); | |
| tx.objectStore(IDB_STORE).delete(key); | |
| tx.oncomplete = function() { ok(); }; | |
| tx.onerror = function() { no(tx.error); }; | |
| }); | |
| }); | |
| } | |
| // 'meta' is a tiny record checked on every load; 'model' is the big ArrayBuffer, | |
| // only read when the user explicitly loads from cache. | |
| function getCachedMeta() { return idbGet("meta"); } | |
| function getWeightsBuffer() { return idbGet("model"); } | |
| function putCached(buf) { | |
| return idbPut("model", buf).then(function() { | |
| return idbPut("meta", { size: buf.byteLength, savedAt: Date.now() }); | |
| }); | |
| } | |
| function clearCached() { | |
| return idbDel("model").then(function() { return idbDel("meta"); }); | |
| } | |
| /* ================= cache UI state machine ================= */ | |
| function updateCacheUI() { | |
| var info = els.cacheInfo, btn = els.getWeightsBtn, clr = els.clearCacheBtn; | |
| var canClear = env.idbOk && (env.cacheState === "cached" || env.cacheState === "ready"); | |
| clr.style.display = canClear ? "" : "none"; | |
| switch (env.cacheState) { | |
| case "checking": | |
| info.textContent = "Checking local cache\u2026"; | |
| btn.style.display = "none"; | |
| break; | |
| case "absent": | |
| info.innerHTML = env.idbOk | |
| ? "Not cached." + (env.modelSize ? " ~" + fmtMB(env.modelSize) + " download." : "") | |
| : "Local cache unavailable here; weights re-download each visit."; | |
| btn.style.display = ""; | |
| btn.textContent = "Download weights" + (env.modelSize ? " (" + fmtMB(env.modelSize) + ")" : ""); | |
| break; | |
| case "cached": | |
| var when = env.cachedSavedAt ? " \u00b7 saved " + new Date(env.cachedSavedAt).toLocaleDateString() : ""; | |
| info.innerHTML = "Cached locally" + (env.cachedSize ? " (" + fmtMB(env.cachedSize) + ")" : "") + when + ". Not loaded yet."; | |
| btn.style.display = ""; | |
| btn.textContent = "Load from cache"; | |
| break; | |
| case "downloading": | |
| info.textContent = "Downloading\u2026"; | |
| btn.style.display = "none"; | |
| break; | |
| case "loading": | |
| info.textContent = "Building inference session\u2026"; | |
| btn.style.display = "none"; | |
| break; | |
| case "ready": | |
| info.innerHTML = "Loaded on <b>" + env.engineName + "</b>."; | |
| btn.style.display = "none"; | |
| break; | |
| } | |
| } | |
| /* ================= ORT helpers ================= */ | |
| function loadScript(src) { | |
| return new Promise(function(ok, no) { | |
| var s = document.createElement("script"); | |
| s.src = src; | |
| s.onload = function() { ok(); }; | |
| s.onerror = function() { no(new Error("failed to load " + src)); }; | |
| document.head.appendChild(s); | |
| }); | |
| } | |
| function engineLabel() { | |
| var base = env.engineName; | |
| if (base.indexOf("wasm") >= 0) base += " \u00b7 " + env.threads + "t"; | |
| return "engine: " + base; | |
| } | |
| async function detectGpu() { | |
| if (!navigator.gpu) { els.badgeGpu.textContent = "gpu: not available"; return; } | |
| try { | |
| var adapter = await navigator.gpu.requestAdapter(); | |
| if (!adapter) { els.badgeGpu.textContent = "gpu: no adapter"; return; } | |
| var info = adapter.info || (adapter.requestAdapterInfo ? await adapter.requestAdapterInfo() : null); | |
| var label = ""; | |
| if (info) label = info.description || [info.vendor, info.architecture].filter(Boolean).join(" "); | |
| els.badgeGpu.textContent = "gpu: " + (label || "available"); | |
| } catch (e) { | |
| els.badgeGpu.textContent = "gpu: available"; | |
| } | |
| } | |
| async function createSession(backend) { | |
| var buf = env.modelBuf; | |
| if (backend === "wasm") { | |
| var s = await ort.InferenceSession.create(buf, { executionProviders: ["wasm"] }); | |
| env.engineName = "wasm"; | |
| return s; | |
| } | |
| if (backend === "webgpu") { | |
| if (!env.hasWebGpuBundle) throw new Error("WebGPU backend not loaded (no navigator.gpu, or page not in a secure context)"); | |
| var s2 = await ort.InferenceSession.create(buf, { executionProviders: ["webgpu"] }); | |
| env.engineName = "webgpu"; | |
| return s2; | |
| } | |
| // auto | |
| if (env.hasWebGpuBundle) { | |
| try { | |
| var s3 = await ort.InferenceSession.create(buf, { executionProviders: ["webgpu", "wasm"] }); | |
| env.engineName = "webgpu+wasm"; | |
| return s3; | |
| } catch (e) { | |
| console.warn("webgpu session init failed, using wasm:", e); | |
| var s4 = await ort.InferenceSession.create(buf, { executionProviders: ["wasm"] }); | |
| env.engineName = "wasm"; | |
| return s4; | |
| } | |
| } | |
| var s5 = await ort.InferenceSession.create(buf, { executionProviders: ["wasm"] }); | |
| env.engineName = "wasm"; | |
| return s5; | |
| } | |
| async function headSize(url) { | |
| try { | |
| var r = await fetch(url, { method: "HEAD" }); | |
| var cl = r.headers.get("content-length"); | |
| return cl ? Number(cl) : 0; | |
| } catch (e) { return 0; } | |
| } | |
| async function fetchWithProgress(url) { | |
| var res = await fetch(url); | |
| if (!res.ok) throw new Error("HTTP " + res.status + " for " + url); | |
| var total = Number(res.headers.get("content-length")) || 0; | |
| if (!total || !res.body) return res.arrayBuffer(); | |
| var reader = res.body.getReader(); | |
| var chunks = []; | |
| var received = 0; | |
| while (true) { | |
| var r = await reader.read(); | |
| if (r.done) break; | |
| chunks.push(r.value); | |
| received += r.value.length; | |
| els.dlfill.style.width = Math.min(100, (received / total) * 100).toFixed(1) + "%"; | |
| setStatus("Downloading model weights\u2026 " + (received / 1e6).toFixed(0) + " / " + (total / 1e6).toFixed(0) + " MB"); | |
| } | |
| var buf = new Uint8Array(received); | |
| var off = 0; | |
| for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], off); off += chunks[i].length; } | |
| return buf.buffer; | |
| } | |
| /* ================= weight acquisition (user-initiated) ================= */ | |
| async function buildSessionFromBuffer() { | |
| env.cacheState = "loading"; updateCacheUI(); | |
| setStatus("Initializing runtime (" + env.backend + ")\u2026"); | |
| env.session = await createSession(env.backend); | |
| els.badgeEngine.textContent = engineLabel(); | |
| env.engine = "ready"; | |
| env.cacheState = "ready"; updateCacheUI(); | |
| els.runBtn.disabled = false; | |
| var note = ""; | |
| if (env.engineName.indexOf("webgpu") >= 0) note = " GPU active."; | |
| else if (navigator.gpu) note = " <span style='color:#7d8590'>(webgpu present but session fell back to wasm)</span>"; | |
| if (!window.crossOriginIsolated) note += " <span style='color:#555b66'>(serve with COOP/COEP for multi-threaded wasm)</span>"; | |
| setStatus('<span class="ok">Ready.</span> ' + env.engineName + '.' + note); | |
| } | |
| async function downloadWeights() { | |
| env.cacheState = "downloading"; updateCacheUI(); | |
| els.dlfill.style.width = "0%"; | |
| setStatus("Downloading model weights\u2026"); | |
| try { | |
| var buf = await fetchWithProgress("model.onnx"); | |
| env.modelBuf = buf; | |
| els.dlfill.style.width = "100%"; | |
| if (env.idbOk) { | |
| setStatus("Saving to local cache\u2026"); | |
| try { | |
| await putCached(buf); | |
| env.cachedSize = buf.byteLength; | |
| env.cachedSavedAt = Date.now(); | |
| } catch (e2) { | |
| console.warn("cache save failed:", e2); | |
| env.idbOk = false; | |
| } | |
| } | |
| await buildSessionFromBuffer(); | |
| } catch (e) { | |
| console.error(e); | |
| env.cacheState = "absent"; updateCacheUI(); | |
| setStatus('<span class="err">Download failed: ' + e.message + '</span>'); | |
| } | |
| } | |
| async function loadFromCache() { | |
| env.cacheState = "loading"; updateCacheUI(); | |
| setStatus("Reading weights from local cache\u2026"); | |
| try { | |
| var buf = await getWeightsBuffer(); | |
| if (!buf) { | |
| env.cacheState = "absent"; updateCacheUI(); | |
| setStatus("Cache entry missing. Download the weights instead."); | |
| return; | |
| } | |
| env.modelBuf = buf; | |
| await buildSessionFromBuffer(); | |
| } catch (e) { | |
| console.error(e); | |
| env.cacheState = "absent"; updateCacheUI(); | |
| setStatus('<span class="err">Cache load failed: ' + e.message + '</span>'); | |
| } | |
| } | |
| els.getWeightsBtn.addEventListener("click", function() { | |
| if (env.cacheState === "absent") downloadWeights(); | |
| else if (env.cacheState === "cached") loadFromCache(); | |
| }); | |
| els.clearCacheBtn.addEventListener("click", async function() { | |
| try { await clearCached(); } catch (e) { console.warn(e); } | |
| env.modelBuf = null; | |
| env.session = null; | |
| env.engine = "awaiting-weights"; | |
| env.cacheState = "absent"; | |
| env.cachedSize = 0; env.cachedSavedAt = 0; | |
| els.runBtn.disabled = true; | |
| els.badgeEngine.textContent = "engine: \u2014"; | |
| els.modelSize = await headSize("model.onnx"); | |
| updateCacheUI(); | |
| setStatus("Cache cleared. Download the weights to continue."); | |
| }); | |
| /* ================= init (no auto weight download) ================= */ | |
| async function loadModel() { | |
| detectGpu(); | |
| var gpuPresent = !!navigator.gpu; | |
| if (!gpuPresent) { | |
| var wg = document.querySelector('#backendSeg button[data-backend="webgpu"]'); | |
| if (wg) { wg.disabled = true; wg.title = "WebGPU unavailable (needs https or localhost)"; } | |
| } | |
| setStatus("Loading tokenizer\u2026"); | |
| var tokRes = await fetch("tokenizer.json"); | |
| env.tokenizer = new ByteLevelTokenizer(await tokRes.json()); | |
| env.vocabSize = env.tokenizer.idToToken.length + 1; | |
| els.badgeWeights.textContent = "tokenizer: ok"; | |
| env.hasWebGpuBundle = false; | |
| setStatus("Loading ONNX runtime (" + (gpuPresent ? "webgpu+wasm" : "wasm") + ")\u2026"); | |
| try { | |
| if (gpuPresent) { | |
| await loadScript(ORT_CDN + "ort.webgpu.min.js"); | |
| env.hasWebGpuBundle = true; | |
| } else { | |
| await loadScript(ORT_CDN + "ort.min.js"); | |
| } | |
| } catch (e1) { | |
| console.warn("primary ORT bundle failed, falling back to wasm build:", e1); | |
| await loadScript(ORT_CDN + "ort.min.js"); | |
| env.hasWebGpuBundle = false; | |
| } | |
| if (typeof ort === "undefined") throw new Error("onnxruntime-web failed to load from CDN"); | |
| ort.env.wasm.wasmPaths = ORT_CDN; | |
| if (window.crossOriginIsolated) { | |
| env.threads = Math.min(navigator.hardwareConcurrency || 4, 8); | |
| ort.env.wasm.numThreads = env.threads; | |
| } else { | |
| env.threads = 1; | |
| } | |
| // Check the cache (cheap meta read only). Do NOT download automatically. | |
| env.cacheState = "checking"; updateCacheUI(); | |
| try { | |
| await openDB(); | |
| env.idbOk = true; | |
| } catch (e) { | |
| env.idbOk = false; | |
| } | |
| els.runBtn.disabled = true; | |
| env.engine = "awaiting-weights"; | |
| if (env.idbOk) { | |
| var meta = await getCachedMeta(); | |
| if (meta && meta.size) { | |
| env.cachedSize = meta.size; | |
| env.cachedSavedAt = meta.savedAt || 0; | |
| env.cacheState = "cached"; | |
| updateCacheUI(); | |
| setStatus("Weights are cached. <b>Load from cache</b> to start (no download)."); | |
| return; | |
| } | |
| } | |
| env.cacheState = "absent"; | |
| env.modelSize = await headSize("model.onnx"); | |
| updateCacheUI(); | |
| setStatus(env.idbOk | |
| ? "Weights not cached. Download them once; they'll be stored locally for next time." | |
| : "Local cache unavailable here. Download the weights to begin."); | |
| } | |
| /* ================= backend switcher ================= */ | |
| document.querySelectorAll("#backendSeg button").forEach(function(btn) { | |
| btn.addEventListener("click", async function() { | |
| if (running || btn.disabled || btn.classList.contains("active")) return; | |
| var prev = document.querySelector("#backendSeg .active"); | |
| if (prev) prev.classList.remove("active"); | |
| btn.classList.add("active"); | |
| env.backend = btn.dataset.backend; | |
| if (!env.modelBuf) { | |
| setStatus("Backend set to <b>" + env.backend + "</b> \u2014 applies when weights are loaded."); | |
| return; | |
| } | |
| env.engine = "loading"; | |
| setStatus("Switching backend to <b>" + env.backend + "</b>\u2026"); | |
| try { | |
| env.session = await createSession(env.backend); | |
| els.badgeEngine.textContent = engineLabel(); | |
| env.engine = "ready"; | |
| env.cacheState = "ready"; updateCacheUI(); | |
| setStatus('<span class="ok">Ready.</span> Running on <b>' + env.engineName + '</b>.'); | |
| } catch (e) { | |
| console.warn(e); | |
| env.engine = "ready"; | |
| setStatus('<span class="err">' + env.backend + ' failed: ' + e.message + '</span> \u2014 still on ' + env.engineName + '.'); | |
| } | |
| }); | |
| }); | |
| /* ================= live view ================= */ | |
| function renderTokens(state) { | |
| var tok = env.tokenizer; | |
| var ids = state.inputIds; | |
| var frag = document.createDocumentFragment(); | |
| var fresh = new Set(); | |
| if (state.prevMasked) { | |
| for (var i = 0; i < ids.length; i++) { | |
| if (state.prevMasked[i] && ids[i] !== MD_MASK_TOKEN) fresh.add(i); | |
| } | |
| } | |
| var prevMasked = new Array(ids.length); | |
| for (var i = 0; i < ids.length; i++) { | |
| var id = ids[i]; | |
| var span = document.createElement("span"); | |
| if (id === MD_MASK_TOKEN) { | |
| span.className = "tok mask"; | |
| span.textContent = "\u25ae"; | |
| prevMasked[i] = true; | |
| } else { | |
| var isPrompt = i < state.promptLen; | |
| span.className = "tok" + (isPrompt ? " prompt" : "") + (fresh.has(i) ? " fresh" : ""); | |
| span.textContent = tok.idToToken[id] !== undefined ? tok.decode([id], true) : "\ufffd"; | |
| prevMasked[i] = false; | |
| } | |
| frag.appendChild(span); | |
| } | |
| els.probe.replaceChildren(frag); | |
| return prevMasked; | |
| } | |
| function updateMeta(state) { | |
| els.mStep.textContent = String(state.step + 1); | |
| els.mSteps.textContent = String(state.numSteps); | |
| els.mT.textContent = state.t.toFixed(3); | |
| els.mMasks.textContent = String(state.nMasked); | |
| els.progressFill.style.width = ((state.step + 1) / state.numSteps * 100).toFixed(1) + "%"; | |
| var perStep = state.elapsed / (state.step + 1); | |
| var left = (state.numSteps - state.step - 1) * perStep; | |
| els.mEta.textContent = left > 0 ? "~" + Math.round(left / 1000) + "s left" : ""; | |
| } | |
| function updatePerf(state) { | |
| var times = env.stepTimes; | |
| if (times.length) { | |
| var recent = times.slice(-5); | |
| var avg = 0; | |
| for (var i = 0; i < recent.length; i++) avg += recent[i]; | |
| avg /= recent.length; | |
| els.mMs.textContent = avg.toFixed(1) + " ms"; | |
| } | |
| var genLen = state.inputIds.length - state.promptLen; | |
| var revealed = genLen - state.nMasked; | |
| var tps = state.elapsed > 0 ? revealed / (state.elapsed / 1000) : 0; | |
| els.mTps.textContent = tps.toFixed(1); | |
| } | |
| function drawSparkline() { | |
| var c = els.spark; | |
| var dpr = window.devicePixelRatio || 1; | |
| var w = c.clientWidth || 300, h = c.clientHeight || 40; | |
| if (c.width !== Math.round(w * dpr) || c.height !== Math.round(h * dpr)) { | |
| c.width = Math.round(w * dpr); | |
| c.height = Math.round(h * dpr); | |
| } | |
| var ctx = c.getContext("2d"); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| ctx.clearRect(0, 0, w, h); | |
| var times = env.stepTimes; | |
| if (!times.length) return; | |
| var slots = Math.max(times.length, env.lastNumSteps); | |
| var slot = (w - 8) / slots; | |
| var bw = Math.max(1.5, Math.min(slot - 1.5, 8)); | |
| var max = 0; | |
| for (var i = 0; i < times.length; i++) if (times[i] > max) max = times[i]; | |
| if (max <= 0) max = 1; | |
| for (var i = 0; i < times.length; i++) { | |
| var bh = Math.max(2, (times[i] / max) * (h - 8)); | |
| ctx.fillStyle = (i === times.length - 1) ? "#9cc4ee" : "#4f7bb0"; | |
| ctx.fillRect(4 + i * slot, h - 4 - bh, bw, bh); | |
| } | |
| } | |
| /* ================= run ================= */ | |
| var running = false; | |
| async function run() { | |
| if (running) return; | |
| if (env.engine !== "ready") { setStatus('Weights not loaded yet \u2014 download or load from cache first.'); return; } | |
| var params = { | |
| prompt: els.prompt.value, | |
| seqLen: parseInt(els.seqLen.value, 10), | |
| numSteps: parseInt(els.numSteps.value, 10), | |
| temperature: parseFloat(els.temperature.value), | |
| repPenalty: parseFloat(els.repPenalty.value), | |
| }; | |
| running = true; | |
| env.cancelled = false; | |
| env.stepTimes = []; | |
| env.lastNumSteps = params.numSteps; | |
| els.runBtn.disabled = true; | |
| els.stopBtn.disabled = false; | |
| els.finalText.style.display = "none"; | |
| els.copyBtn.style.display = "none"; | |
| els.probe.replaceChildren(); | |
| els.progressFill.style.width = "0%"; | |
| els.mTps.textContent = "\u2013"; | |
| els.mMs.textContent = "\u2013"; | |
| drawSparkline(); | |
| var session = env.session; | |
| var prevMasked = null; | |
| var lastElapsed = 0; | |
| var runStep = async function(inputIds, positionIds, t, logitsOut) { | |
| var seq = inputIds.length; | |
| var idsBig = new BigInt64Array(seq); | |
| for (var i = 0; i < seq; i++) idsBig[i] = BigInt(inputIds[i]); | |
| var posBig = new BigInt64Array(seq); | |
| for (var i = 0; i < seq; i++) posBig[i] = BigInt(positionIds[i]); | |
| var feeds = { | |
| input_ids: new ort.Tensor("int64", idsBig, [1, seq]), | |
| timesteps: new ort.Tensor("float32", new Float32Array([t]), [1]), | |
| position_ids: new ort.Tensor("int64", posBig, [1, seq]), | |
| }; | |
| var out = await session.run(feeds); | |
| logitsOut.set(out.logits.data); | |
| }; | |
| try { | |
| var state; | |
| var gen = denoise(env, Object.assign({}, params, { runStep: runStep })); | |
| var first = true; | |
| while (true) { | |
| var result = await gen.next(); | |
| if (result.done) break; | |
| state = result.value; | |
| if (first) { els.mSteps.textContent = String(state.numSteps); first = false; } | |
| state.prevMasked = prevMasked; | |
| prevMasked = renderTokens(state); | |
| updateMeta(state); | |
| var stepMs = state.elapsed - lastElapsed; | |
| lastElapsed = state.elapsed; | |
| env.stepTimes.push(stepMs); | |
| drawSparkline(); | |
| updatePerf(state); | |
| setStatus("Denoising on <b>" + env.engineName + "</b>\u2026 step <b>" + (state.step + 1) + "</b>/" + state.numSteps + " \u00b7 noise <b>" + state.t.toFixed(2) + "</b>"); | |
| await new Promise(function(r) { setTimeout(r, 0); }); | |
| } | |
| var finalText = env.tokenizer.decode(state.inputIds, true); | |
| els.finalText.textContent = finalText; | |
| els.finalText.style.display = "block"; | |
| els.copyBtn.style.display = "inline-block"; | |
| setStatus('<span class="ok">Done.</span> ' + env.engineName + ' \u00b7 ' + els.mMs.textContent + '/step avg \u00b7 denoised text below.'); | |
| } catch (e) { | |
| if (e && e.message === "cancelled") setStatus("Stopped."); | |
| else { console.error(e); setStatus('<span class="err">Error: ' + e.message + '</span>'); } | |
| } finally { | |
| running = false; | |
| els.runBtn.disabled = false; | |
| els.stopBtn.disabled = true; | |
| } | |
| } | |
| els.runBtn.addEventListener("click", run); | |
| els.stopBtn.addEventListener("click", function() { env.cancelled = true; }); | |
| function bindSlider(input, label, fmt) { | |
| var upd = function() { label.textContent = fmt ? fmt(parseFloat(input.value)) : input.value; }; | |
| input.addEventListener("input", upd); | |
| upd(); | |
| } | |
| bindSlider(els.seqLen, els.vSeq, function(v) { return String(v); }); | |
| bindSlider(els.numSteps, els.vSteps, function(v) { return String(v); }); | |
| bindSlider(els.temperature, els.vTemp, function(v) { return v.toFixed(2); }); | |
| bindSlider(els.repPenalty, els.vRep, function(v) { return v.toFixed(2); }); | |
| document.querySelectorAll("#presets button").forEach(function(btn) { | |
| btn.addEventListener("click", function() { | |
| var p = JSON.parse(btn.dataset.preset); | |
| els.prompt.value = p.prompt; | |
| els.seqLen.value = p.seqLen; els.vSeq.textContent = p.seqLen; | |
| els.numSteps.value = p.numSteps; els.vSteps.textContent = p.numSteps; | |
| els.temperature.value = p.temperature; els.vTemp.textContent = p.temperature.toFixed(2); | |
| els.repPenalty.value = p.repPenalty; els.vRep.textContent = p.repPenalty.toFixed(2); | |
| }); | |
| }); | |
| els.copyBtn.addEventListener("click", async function() { | |
| try { | |
| await navigator.clipboard.writeText(els.finalText.textContent); | |
| els.copyBtn.textContent = "Copied!"; | |
| setTimeout(function() { els.copyBtn.textContent = "Copy output"; }, 1500); | |
| } catch (e) {} | |
| }); | |
| loadModel().catch(function(e) { | |
| console.error(e); | |
| setStatus('<span class="err">Failed to initialize: ' + e.message + '</span>'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |