MetaDiffusion-Demo / index.html
CodeSoft's picture
Update index.html
4e8b17d verified
Raw
History Blame Contribute Delete
40.3 kB
<!DOCTYPE html>
<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> &mdash; masked diffusion language model</div>
<div class="status-line">
<span id="badgeEngine">engine: &mdash;</span>
<span id="badgeGpu">gpu: probing&hellip;</span>
<span id="badgeWeights">tokenizer: &mdash;</span>
</div>
<div class="layout">
<div class="col-left">
<fieldset>
<legend>Model weights</legend>
<div id="cacheInfo">Checking local cache&hellip;</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 &amp; 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>&#9654; Denoise</button>
<button id="stopBtn" disabled>&#9632; Stop</button>
</div>
<div id="status">Initializing&hellip;</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> &nbsp;&middot;&nbsp;
noise <b id="mT">1.00</b> &nbsp;&middot;&nbsp;
masks <b id="mMasks">0</b> &nbsp;&middot;&nbsp;
<b id="mTps">&ndash;</b> tok/s &nbsp;
<span id="mEta"></span>
</div>
<div id="progressWrap"><div id="progressFill"></div></div>
<div class="sparkhead"><span>step time</span><b id="mMs">&ndash;</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>
&middot; Apache-2.0 &middot; int8 ONNX &middot;
<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">
"use strict";
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>
"use strict";
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>