Update index.html
Browse files**fix: patch tokenizer load for Qwen**
* Use local tokenizer path first; fall back to Hub tokenizer if local fails
* Prevent `t.replace is not a function` crash during `from_pretrained`
* Keep Qwen model loading local; only tokenizer may use Hub fallback
- index.html +61 -49
index.html
CHANGED
|
@@ -114,13 +114,12 @@
|
|
| 114 |
</section>
|
| 115 |
</div>
|
| 116 |
</main>
|
| 117 |
-
|
| 118 |
<script type="module">
|
| 119 |
const { env, AutoTokenizer, AutoModelForCausalLM } = window.HF;
|
| 120 |
-
|
| 121 |
// Return a URL object pointing to an app-relative path
|
| 122 |
const ABS = (p) => new URL(p, window.location.href);
|
| 123 |
-
|
| 124 |
/* ---------- ONNX Runtime Web backend selection (compat mode) ---------- */
|
| 125 |
env.backends.onnx.webgpu = { enabled: false }; // disable WebGPU
|
| 126 |
env.backends.onnx.preferredBackend = "wasm";
|
|
@@ -131,7 +130,7 @@
|
|
| 131 |
}
|
| 132 |
env.backends.onnx.wasm.wasmPaths =
|
| 133 |
"https://cdn.jsdelivr.net/npm/onnxruntime-web@1.17.1/dist/";
|
| 134 |
-
|
| 135 |
/* ---------- DOM helpers ---------- */
|
| 136 |
const $ = (s) => document.querySelector(s);
|
| 137 |
const statusEl = $('#status'), barEl = $('#bar'), errEl = $('#error');
|
|
@@ -144,17 +143,17 @@
|
|
| 144 |
function setErr(e){ errEl.textContent = e || ""; }
|
| 145 |
function showToken(s){ if (s === "\n") return "⏎"; if (s.trim() === "") return `␣${s.length>1 ? "×"+s.length : ""}`; return s; }
|
| 146 |
const PUNC_ONLY = /^[\s.,;:!?—-]+$/;
|
| 147 |
-
|
| 148 |
/* ---------- Byte-accurate progress with speed + ETA ---------- */
|
| 149 |
const files = new Map(); // fileURL -> { loaded, total, startedAt, done, cached }
|
| 150 |
let phase = ""; // "Tokenizer" | "Model"
|
| 151 |
let lastTickBytes = 0, lastTickTime = 0;
|
| 152 |
const STALL_SECS = 20;
|
| 153 |
-
|
| 154 |
function humanMB(b){ return (b/1024/1024).toFixed(1) + " MB"; }
|
| 155 |
function humanSpeed(bps){ if (!bps || !isFinite(bps)) return ""; const mbps=bps/1024/1024; return (mbps>=1?mbps.toFixed(1)+" MB/s":(bps/1024).toFixed(0)+" kB/s"); }
|
| 156 |
function humanETA(eta){ return eta>0 ? Math.max(1, Math.round(eta))+"s" : ""; }
|
| 157 |
-
|
| 158 |
function resetProgress(nextPhase){
|
| 159 |
phase = nextPhase || "";
|
| 160 |
files.clear();
|
|
@@ -163,7 +162,7 @@
|
|
| 163 |
setStatus(`${phase ? phase+": " : ""}Starting…`);
|
| 164 |
setErr("");
|
| 165 |
}
|
| 166 |
-
|
| 167 |
// transformers.js will call this for every file chunk
|
| 168 |
function onProgress(evt){
|
| 169 |
if (evt.file) {
|
|
@@ -173,7 +172,7 @@
|
|
| 173 |
if (evt.status === "ready") { f.done = true; if (f.total === 0) f.cached = true; }
|
| 174 |
files.set(evt.file, f);
|
| 175 |
}
|
| 176 |
-
|
| 177 |
let loadedSum = 0, totalSum = 0, anyTotals = false, allDone = true, anyCached = false;
|
| 178 |
for (const f of files.values()){
|
| 179 |
loadedSum += f.loaded || 0;
|
|
@@ -182,18 +181,18 @@
|
|
| 182 |
allDone &&= !!f.done;
|
| 183 |
anyCached ||= f.cached;
|
| 184 |
}
|
| 185 |
-
|
| 186 |
const now = performance.now();
|
| 187 |
const dt = Math.max(1, now - lastTickTime) / 1000;
|
| 188 |
const dBytes = loadedSum - lastTickBytes;
|
| 189 |
const bps = dBytes / dt;
|
| 190 |
lastTickBytes = loadedSum; lastTickTime = now;
|
| 191 |
-
|
| 192 |
if (evt.status === "downloading" || (!evt.status && anyTotals)) {
|
| 193 |
let pct = 0;
|
| 194 |
if (totalSum > 0) pct = Math.min(100, Math.floor((loadedSum / totalSum) * 100));
|
| 195 |
if (barEl) barEl.style.width = (totalSum>0 ? pct : 10) + "%";
|
| 196 |
-
|
| 197 |
const parts = [
|
| 198 |
phase ? `${phase}:` : "Downloading…",
|
| 199 |
anyTotals ? `${humanMB(loadedSum)} / ${humanMB(totalSum)} (${pct}%)` : `${humanMB(loadedSum)}…`,
|
|
@@ -206,13 +205,13 @@
|
|
| 206 |
if (anyCached) parts.push("(cached)");
|
| 207 |
setStatus(parts.filter(Boolean).join(" "));
|
| 208 |
}
|
| 209 |
-
|
| 210 |
if (evt.status === "ready" || allDone) {
|
| 211 |
if (barEl) barEl.style.width = "100%";
|
| 212 |
setStatus(`${phase ? phase+": " : ""}Ready`);
|
| 213 |
}
|
| 214 |
}
|
| 215 |
-
|
| 216 |
// Stall detector (UI hint)
|
| 217 |
setInterval(() => {
|
| 218 |
const now = performance.now();
|
|
@@ -222,17 +221,20 @@
|
|
| 222 |
setErr("Download seems stalled. If you use ad/tracker blockers, disable them for this page or try the smaller model.");
|
| 223 |
}
|
| 224 |
}, 4000);
|
| 225 |
-
|
| 226 |
/* ---------- Model registry ---------- */
|
| 227 |
const MODELS = {
|
| 228 |
qwen: {
|
| 229 |
-
//
|
| 230 |
local: ABS("assets/models/qwen/"),
|
| 231 |
file_name: "onnx/model_q4f16.onnx",
|
|
|
|
| 232 |
emb: {
|
| 233 |
coords: ABS("assets/embeddings/qwen_pca_top5k_coords.json"),
|
| 234 |
nbrs: ABS("assets/embeddings/qwen_neighbors_top5k_k40.json")
|
| 235 |
-
}
|
|
|
|
|
|
|
| 236 |
},
|
| 237 |
distilgpt2: {
|
| 238 |
local: ABS("assets/models/distilgpt2/"),
|
|
@@ -244,13 +246,13 @@
|
|
| 244 |
}
|
| 245 |
}
|
| 246 |
};
|
| 247 |
-
|
| 248 |
/* ---------- Qwen3 config shim (treat as Qwen2 in JS) ---------- */
|
| 249 |
const QWEN3_CONFIG_FIX = {
|
| 250 |
model_type: "qwen2",
|
| 251 |
architectures: ["Qwen2ForCausalLM"]
|
| 252 |
};
|
| 253 |
-
|
| 254 |
/* ---------- Embedding viewer ---------- */
|
| 255 |
const Emb = (() => {
|
| 256 |
let coordsPath = "", nbrsPath = "";
|
|
@@ -284,42 +286,52 @@
|
|
| 284 |
function bounds(){ let xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity; for(const p of points){ if(p.x<xmin)xmin=p.x; if(p.x>xmax)xmax=p.x; if(p.y<ymin)ymin=p.y; if(p.y>ymax)ymax=p.y; } if(!isFinite(xmin)){xmin=0;xmax=1;ymin=0;ymax=1;} return {xmin,xmax,ymin,ymax}; }
|
| 285 |
function toCanvas(x,y){ const pad=24,w=embCanvas.width,h=embCanvas.height; const {xmin,xmax,ymin,ymax}=bounds(); const tx=pad+(x-xmin)/Math.max(1e-9,(xmax-xmin))*(w-pad*2); const ty=pad+(1-(y-ymin)/Math.max(1e-9,(ymax-ymin)))*(h-pad*2); return [tx,ty]; }
|
| 286 |
function drawBase(){ const ctx=embCtx; ctx.clearRect(0,0,embCanvas.width,embCanvas.height); ctx.fillStyle="#0b1327"; ctx.fillRect(0,0,embCanvas.width,embCanvas.height); ctx.fillStyle="#5f7aa5"; for(const p of points){ const [x,y]=toCanvas(p.x,p.y); ctx.fillRect(x,y,2,2); } }
|
| 287 |
-
function highlight(token){ if(!baseDrawn) drawBase(); const ctx=embCtx; const base=index.get(token); if(!base) return; const nbrs=neighbors.get(token)||[]; ctx.strokeStyle="#38bdf8"; ctx.lineWidth=1; const [bx,by]=toCanvas(base.x,base.y); for(const t of nbrs){ const p=index.get(t); if(!p) continue; const [x,y]=toCanvas(p.x,p.y); ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(x,y); ctx.stroke(); ctx.fillStyle="#9bd7ff"; ctx.fillRect(x-2,y-2,4,4);}
|
|
|
|
| 288 |
return { setSources, load, drawBase, highlight };
|
| 289 |
})();
|
| 290 |
-
|
| 291 |
/* ---------- Core model state ---------- */
|
| 292 |
let tokenizer=null, model=null;
|
| 293 |
let loadSeq = 0;
|
| 294 |
-
|
| 295 |
async function loadModel(key){
|
| 296 |
const mySeq = ++loadSeq;
|
| 297 |
-
|
| 298 |
// Embeddings (unchanged)
|
| 299 |
Emb.setSources(key);
|
| 300 |
try { await Emb.load(); } catch { embStatus.textContent = "Map failed to load"; }
|
| 301 |
-
|
| 302 |
// --- Tokenizer ---
|
| 303 |
resetProgress("Tokenizer");
|
| 304 |
setStatus("Tokenizer: starting…");
|
| 305 |
try {
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
} catch (e) {
|
| 317 |
console.error("Tokenizer load failed:", e);
|
| 318 |
setErr("Tokenizer failed to load.");
|
| 319 |
return;
|
| 320 |
}
|
| 321 |
if (mySeq !== loadSeq) return;
|
| 322 |
-
|
| 323 |
// --- Model ---
|
| 324 |
resetProgress("Model");
|
| 325 |
setStatus("Model: starting…");
|
|
@@ -342,7 +354,7 @@
|
|
| 342 |
return;
|
| 343 |
}
|
| 344 |
if (mySeq !== loadSeq) return;
|
| 345 |
-
|
| 346 |
// --- Warm-up (guarded) ---
|
| 347 |
setStatus("Warming up…");
|
| 348 |
try {
|
|
@@ -359,20 +371,20 @@
|
|
| 359 |
if (mySeq !== loadSeq) return;
|
| 360 |
setStatus("Ready");
|
| 361 |
}
|
| 362 |
-
|
| 363 |
/* ---------- Next-token logic ---------- */
|
| 364 |
async function greedyNext(text, topK = 10) {
|
| 365 |
if (!tokenizer || !model) {
|
| 366 |
setErr("Model not loaded yet — check the status bar.");
|
| 367 |
return { rows: [], dt: 0 };
|
| 368 |
}
|
| 369 |
-
|
| 370 |
// 1) Tokenize
|
| 371 |
let enc = await tokenizer(text ?? " ", {
|
| 372 |
add_special_tokens: false,
|
| 373 |
return_attention_mask: true
|
| 374 |
});
|
| 375 |
-
|
| 376 |
// retry with BOS if empty
|
| 377 |
const len = enc?.input_ids?.dims?.at(-1) ?? 0;
|
| 378 |
if (!len || len <= 0) {
|
|
@@ -381,10 +393,10 @@
|
|
| 381 |
return_attention_mask: true
|
| 382 |
});
|
| 383 |
}
|
| 384 |
-
|
| 385 |
const eosId = tokenizer?.eos_token_id ?? model?.config?.eos_token_id ?? undefined;
|
| 386 |
const padId = tokenizer?.pad_token_id ?? eosId;
|
| 387 |
-
|
| 388 |
const t0 = performance.now();
|
| 389 |
let gen;
|
| 390 |
try {
|
|
@@ -411,18 +423,18 @@
|
|
| 411 |
});
|
| 412 |
}
|
| 413 |
const dt = (performance.now() - t0) | 0;
|
| 414 |
-
|
| 415 |
// logits -> softmax -> Top-K
|
| 416 |
const logitsT = gen.scores[0];
|
| 417 |
const data = logitsT.data;
|
| 418 |
let m = -Infinity; for (let i = 0; i < data.length; i++) if (data[i] > m) m = data[i];
|
| 419 |
const exps = new Float32Array(data.length); let Z = 0;
|
| 420 |
for (let i = 0; i < data.length; i++) { const e = Math.exp(data[i] - m); exps[i] = e; Z += e; }
|
| 421 |
-
|
| 422 |
const K = Math.min(parseInt(topkSel.value, 10) || topK, data.length);
|
| 423 |
const idx = Array.from({ length: data.length }, (_, i) => [exps[i] / Z, i])
|
| 424 |
.sort((a, b) => b[0] - a[0]).slice(0, K);
|
| 425 |
-
|
| 426 |
const rows = [];
|
| 427 |
for (const [p, i] of idx) {
|
| 428 |
const tok = await tokenizer.decode([i], { skip_special_tokens: false });
|
|
@@ -430,7 +442,7 @@
|
|
| 430 |
}
|
| 431 |
return { rows, dt };
|
| 432 |
}
|
| 433 |
-
|
| 434 |
function renderRows(rows){
|
| 435 |
klistEl.innerHTML = "";
|
| 436 |
const hide = hidePunc.checked;
|
|
@@ -445,7 +457,7 @@
|
|
| 445 |
klistEl.appendChild(row);
|
| 446 |
}
|
| 447 |
}
|
| 448 |
-
|
| 449 |
async function predict(){
|
| 450 |
try{
|
| 451 |
setErr(""); predictBtn.disabled = true;
|
|
@@ -459,7 +471,7 @@
|
|
| 459 |
predictBtn.disabled = false;
|
| 460 |
}
|
| 461 |
}
|
| 462 |
-
|
| 463 |
/* ---------- UI ---------- */
|
| 464 |
predictBtn.addEventListener('click', predict);
|
| 465 |
textEl.addEventListener('input', (() => { let to; return () => { clearTimeout(to); to = setTimeout(predict, 250); }; })());
|
|
@@ -467,10 +479,10 @@
|
|
| 467 |
topkSel.addEventListener('change', predict);
|
| 468 |
demoBtn.addEventListener('click', () => { textEl.value = "Twinkle, twinkle, little "; predict(); });
|
| 469 |
modelSel.addEventListener('change', async (e) => { await loadModel(e.target.value); predict(); });
|
| 470 |
-
|
| 471 |
/* ---------- Boot ---------- */
|
| 472 |
(async function init(){
|
| 473 |
-
await loadModel(modelSel.value); // defaults to 'qwen'
|
| 474 |
if (!textEl.value) textEl.value = "Twinkle, twinkle, little ";
|
| 475 |
await predict();
|
| 476 |
})();
|
|
|
|
| 114 |
</section>
|
| 115 |
</div>
|
| 116 |
</main>
|
|
|
|
| 117 |
<script type="module">
|
| 118 |
const { env, AutoTokenizer, AutoModelForCausalLM } = window.HF;
|
| 119 |
+
|
| 120 |
// Return a URL object pointing to an app-relative path
|
| 121 |
const ABS = (p) => new URL(p, window.location.href);
|
| 122 |
+
|
| 123 |
/* ---------- ONNX Runtime Web backend selection (compat mode) ---------- */
|
| 124 |
env.backends.onnx.webgpu = { enabled: false }; // disable WebGPU
|
| 125 |
env.backends.onnx.preferredBackend = "wasm";
|
|
|
|
| 130 |
}
|
| 131 |
env.backends.onnx.wasm.wasmPaths =
|
| 132 |
"https://cdn.jsdelivr.net/npm/onnxruntime-web@1.17.1/dist/";
|
| 133 |
+
|
| 134 |
/* ---------- DOM helpers ---------- */
|
| 135 |
const $ = (s) => document.querySelector(s);
|
| 136 |
const statusEl = $('#status'), barEl = $('#bar'), errEl = $('#error');
|
|
|
|
| 143 |
function setErr(e){ errEl.textContent = e || ""; }
|
| 144 |
function showToken(s){ if (s === "\n") return "⏎"; if (s.trim() === "") return `␣${s.length>1 ? "×"+s.length : ""}`; return s; }
|
| 145 |
const PUNC_ONLY = /^[\s.,;:!?—-]+$/;
|
| 146 |
+
|
| 147 |
/* ---------- Byte-accurate progress with speed + ETA ---------- */
|
| 148 |
const files = new Map(); // fileURL -> { loaded, total, startedAt, done, cached }
|
| 149 |
let phase = ""; // "Tokenizer" | "Model"
|
| 150 |
let lastTickBytes = 0, lastTickTime = 0;
|
| 151 |
const STALL_SECS = 20;
|
| 152 |
+
|
| 153 |
function humanMB(b){ return (b/1024/1024).toFixed(1) + " MB"; }
|
| 154 |
function humanSpeed(bps){ if (!bps || !isFinite(bps)) return ""; const mbps=bps/1024/1024; return (mbps>=1?mbps.toFixed(1)+" MB/s":(bps/1024).toFixed(0)+" kB/s"); }
|
| 155 |
function humanETA(eta){ return eta>0 ? Math.max(1, Math.round(eta))+"s" : ""; }
|
| 156 |
+
|
| 157 |
function resetProgress(nextPhase){
|
| 158 |
phase = nextPhase || "";
|
| 159 |
files.clear();
|
|
|
|
| 162 |
setStatus(`${phase ? phase+": " : ""}Starting…`);
|
| 163 |
setErr("");
|
| 164 |
}
|
| 165 |
+
|
| 166 |
// transformers.js will call this for every file chunk
|
| 167 |
function onProgress(evt){
|
| 168 |
if (evt.file) {
|
|
|
|
| 172 |
if (evt.status === "ready") { f.done = true; if (f.total === 0) f.cached = true; }
|
| 173 |
files.set(evt.file, f);
|
| 174 |
}
|
| 175 |
+
|
| 176 |
let loadedSum = 0, totalSum = 0, anyTotals = false, allDone = true, anyCached = false;
|
| 177 |
for (const f of files.values()){
|
| 178 |
loadedSum += f.loaded || 0;
|
|
|
|
| 181 |
allDone &&= !!f.done;
|
| 182 |
anyCached ||= f.cached;
|
| 183 |
}
|
| 184 |
+
|
| 185 |
const now = performance.now();
|
| 186 |
const dt = Math.max(1, now - lastTickTime) / 1000;
|
| 187 |
const dBytes = loadedSum - lastTickBytes;
|
| 188 |
const bps = dBytes / dt;
|
| 189 |
lastTickBytes = loadedSum; lastTickTime = now;
|
| 190 |
+
|
| 191 |
if (evt.status === "downloading" || (!evt.status && anyTotals)) {
|
| 192 |
let pct = 0;
|
| 193 |
if (totalSum > 0) pct = Math.min(100, Math.floor((loadedSum / totalSum) * 100));
|
| 194 |
if (barEl) barEl.style.width = (totalSum>0 ? pct : 10) + "%";
|
| 195 |
+
|
| 196 |
const parts = [
|
| 197 |
phase ? `${phase}:` : "Downloading…",
|
| 198 |
anyTotals ? `${humanMB(loadedSum)} / ${humanMB(totalSum)} (${pct}%)` : `${humanMB(loadedSum)}…`,
|
|
|
|
| 205 |
if (anyCached) parts.push("(cached)");
|
| 206 |
setStatus(parts.filter(Boolean).join(" "));
|
| 207 |
}
|
| 208 |
+
|
| 209 |
if (evt.status === "ready" || allDone) {
|
| 210 |
if (barEl) barEl.style.width = "100%";
|
| 211 |
setStatus(`${phase ? phase+": " : ""}Ready`);
|
| 212 |
}
|
| 213 |
}
|
| 214 |
+
|
| 215 |
// Stall detector (UI hint)
|
| 216 |
setInterval(() => {
|
| 217 |
const now = performance.now();
|
|
|
|
| 221 |
setErr("Download seems stalled. If you use ad/tracker blockers, disable them for this page or try the smaller model.");
|
| 222 |
}
|
| 223 |
}, 4000);
|
| 224 |
+
|
| 225 |
/* ---------- Model registry ---------- */
|
| 226 |
const MODELS = {
|
| 227 |
qwen: {
|
| 228 |
+
// Local ONNX graph
|
| 229 |
local: ABS("assets/models/qwen/"),
|
| 230 |
file_name: "onnx/model_q4f16.onnx",
|
| 231 |
+
// Embeddings (UI only)
|
| 232 |
emb: {
|
| 233 |
coords: ABS("assets/embeddings/qwen_pca_top5k_coords.json"),
|
| 234 |
nbrs: ABS("assets/embeddings/qwen_neighbors_top5k_k40.json")
|
| 235 |
+
},
|
| 236 |
+
// Fallback tokenizer on HF Hub (compat with Qwen2 tokenizer)
|
| 237 |
+
hub_tokenizer: "Qwen/Qwen2.5-0.5B"
|
| 238 |
},
|
| 239 |
distilgpt2: {
|
| 240 |
local: ABS("assets/models/distilgpt2/"),
|
|
|
|
| 246 |
}
|
| 247 |
}
|
| 248 |
};
|
| 249 |
+
|
| 250 |
/* ---------- Qwen3 config shim (treat as Qwen2 in JS) ---------- */
|
| 251 |
const QWEN3_CONFIG_FIX = {
|
| 252 |
model_type: "qwen2",
|
| 253 |
architectures: ["Qwen2ForCausalLM"]
|
| 254 |
};
|
| 255 |
+
|
| 256 |
/* ---------- Embedding viewer ---------- */
|
| 257 |
const Emb = (() => {
|
| 258 |
let coordsPath = "", nbrsPath = "";
|
|
|
|
| 286 |
function bounds(){ let xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity; for(const p of points){ if(p.x<xmin)xmin=p.x; if(p.x>xmax)xmax=p.x; if(p.y<ymin)ymin=p.y; if(p.y>ymax)ymax=p.y; } if(!isFinite(xmin)){xmin=0;xmax=1;ymin=0;ymax=1;} return {xmin,xmax,ymin,ymax}; }
|
| 287 |
function toCanvas(x,y){ const pad=24,w=embCanvas.width,h=embCanvas.height; const {xmin,xmax,ymin,ymax}=bounds(); const tx=pad+(x-xmin)/Math.max(1e-9,(xmax-xmin))*(w-pad*2); const ty=pad+(1-(y-ymin)/Math.max(1e-9,(ymax-ymin)))*(h-pad*2); return [tx,ty]; }
|
| 288 |
function drawBase(){ const ctx=embCtx; ctx.clearRect(0,0,embCanvas.width,embCanvas.height); ctx.fillStyle="#0b1327"; ctx.fillRect(0,0,embCanvas.width,embCanvas.height); ctx.fillStyle="#5f7aa5"; for(const p of points){ const [x,y]=toCanvas(p.x,p.y); ctx.fillRect(x,y,2,2); } }
|
| 289 |
+
function highlight(token){ if(!baseDrawn) drawBase(); const ctx=embCtx; const base=index.get(token); if(!base) return; const nbrs=neighbors.get(token)||[]; ctx.strokeStyle="#38bdf8"; ctx.lineWidth=1; const [bx,by]=toCanvas(base.x,base.y); for(const t of nbrs){ const p=index.get(t); if(!p) continue; const [x,y]=toCanvas(p.x,p.y); ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(x,y); ctx.stroke(); ctx.fillStyle="#9bd7ff"; ctx.fillRect(x-2,y-2,4-0,4); } // small dot
|
| 290 |
+
ctx.fillStyle="#ffd166"; ctx.beginPath(); ctx.arc(bx,by,5,0,Math.PI*2); ctx.fill(); ctx.fillStyle="#e6f1ff"; ctx.font="12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; ctx.fillText(showToken(token), bx+8, by-8); }
|
| 291 |
return { setSources, load, drawBase, highlight };
|
| 292 |
})();
|
| 293 |
+
|
| 294 |
/* ---------- Core model state ---------- */
|
| 295 |
let tokenizer=null, model=null;
|
| 296 |
let loadSeq = 0;
|
| 297 |
+
|
| 298 |
async function loadModel(key){
|
| 299 |
const mySeq = ++loadSeq;
|
| 300 |
+
|
| 301 |
// Embeddings (unchanged)
|
| 302 |
Emb.setSources(key);
|
| 303 |
try { await Emb.load(); } catch { embStatus.textContent = "Map failed to load"; }
|
| 304 |
+
|
| 305 |
// --- Tokenizer ---
|
| 306 |
resetProgress("Tokenizer");
|
| 307 |
setStatus("Tokenizer: starting…");
|
| 308 |
try {
|
| 309 |
+
if (key === "qwen") {
|
| 310 |
+
// Try local tokenizer first (URL object keeps it local)
|
| 311 |
+
try {
|
| 312 |
+
tokenizer = await AutoTokenizer.from_pretrained(MODELS.qwen.local, {
|
| 313 |
+
progress_callback: onProgress,
|
| 314 |
+
});
|
| 315 |
+
} catch (e) {
|
| 316 |
+
console.warn("Local Qwen tokenizer failed; falling back to Hub:", e);
|
| 317 |
+
// Fallback to a Hub tokenizer compatible with Qwen2 vocab
|
| 318 |
+
tokenizer = await AutoTokenizer.from_pretrained(MODELS.qwen.hub_tokenizer, {
|
| 319 |
+
progress_callback: onProgress,
|
| 320 |
+
});
|
| 321 |
+
}
|
| 322 |
+
} else {
|
| 323 |
+
// distilgpt2: use Hub repo id (string)
|
| 324 |
+
tokenizer = await AutoTokenizer.from_pretrained(MODELS[key].remote, {
|
| 325 |
+
progress_callback: onProgress,
|
| 326 |
+
});
|
| 327 |
+
}
|
| 328 |
} catch (e) {
|
| 329 |
console.error("Tokenizer load failed:", e);
|
| 330 |
setErr("Tokenizer failed to load.");
|
| 331 |
return;
|
| 332 |
}
|
| 333 |
if (mySeq !== loadSeq) return;
|
| 334 |
+
|
| 335 |
// --- Model ---
|
| 336 |
resetProgress("Model");
|
| 337 |
setStatus("Model: starting…");
|
|
|
|
| 354 |
return;
|
| 355 |
}
|
| 356 |
if (mySeq !== loadSeq) return;
|
| 357 |
+
|
| 358 |
// --- Warm-up (guarded) ---
|
| 359 |
setStatus("Warming up…");
|
| 360 |
try {
|
|
|
|
| 371 |
if (mySeq !== loadSeq) return;
|
| 372 |
setStatus("Ready");
|
| 373 |
}
|
| 374 |
+
|
| 375 |
/* ---------- Next-token logic ---------- */
|
| 376 |
async function greedyNext(text, topK = 10) {
|
| 377 |
if (!tokenizer || !model) {
|
| 378 |
setErr("Model not loaded yet — check the status bar.");
|
| 379 |
return { rows: [], dt: 0 };
|
| 380 |
}
|
| 381 |
+
|
| 382 |
// 1) Tokenize
|
| 383 |
let enc = await tokenizer(text ?? " ", {
|
| 384 |
add_special_tokens: false,
|
| 385 |
return_attention_mask: true
|
| 386 |
});
|
| 387 |
+
|
| 388 |
// retry with BOS if empty
|
| 389 |
const len = enc?.input_ids?.dims?.at(-1) ?? 0;
|
| 390 |
if (!len || len <= 0) {
|
|
|
|
| 393 |
return_attention_mask: true
|
| 394 |
});
|
| 395 |
}
|
| 396 |
+
|
| 397 |
const eosId = tokenizer?.eos_token_id ?? model?.config?.eos_token_id ?? undefined;
|
| 398 |
const padId = tokenizer?.pad_token_id ?? eosId;
|
| 399 |
+
|
| 400 |
const t0 = performance.now();
|
| 401 |
let gen;
|
| 402 |
try {
|
|
|
|
| 423 |
});
|
| 424 |
}
|
| 425 |
const dt = (performance.now() - t0) | 0;
|
| 426 |
+
|
| 427 |
// logits -> softmax -> Top-K
|
| 428 |
const logitsT = gen.scores[0];
|
| 429 |
const data = logitsT.data;
|
| 430 |
let m = -Infinity; for (let i = 0; i < data.length; i++) if (data[i] > m) m = data[i];
|
| 431 |
const exps = new Float32Array(data.length); let Z = 0;
|
| 432 |
for (let i = 0; i < data.length; i++) { const e = Math.exp(data[i] - m); exps[i] = e; Z += e; }
|
| 433 |
+
|
| 434 |
const K = Math.min(parseInt(topkSel.value, 10) || topK, data.length);
|
| 435 |
const idx = Array.from({ length: data.length }, (_, i) => [exps[i] / Z, i])
|
| 436 |
.sort((a, b) => b[0] - a[0]).slice(0, K);
|
| 437 |
+
|
| 438 |
const rows = [];
|
| 439 |
for (const [p, i] of idx) {
|
| 440 |
const tok = await tokenizer.decode([i], { skip_special_tokens: false });
|
|
|
|
| 442 |
}
|
| 443 |
return { rows, dt };
|
| 444 |
}
|
| 445 |
+
|
| 446 |
function renderRows(rows){
|
| 447 |
klistEl.innerHTML = "";
|
| 448 |
const hide = hidePunc.checked;
|
|
|
|
| 457 |
klistEl.appendChild(row);
|
| 458 |
}
|
| 459 |
}
|
| 460 |
+
|
| 461 |
async function predict(){
|
| 462 |
try{
|
| 463 |
setErr(""); predictBtn.disabled = true;
|
|
|
|
| 471 |
predictBtn.disabled = false;
|
| 472 |
}
|
| 473 |
}
|
| 474 |
+
|
| 475 |
/* ---------- UI ---------- */
|
| 476 |
predictBtn.addEventListener('click', predict);
|
| 477 |
textEl.addEventListener('input', (() => { let to; return () => { clearTimeout(to); to = setTimeout(predict, 250); }; })());
|
|
|
|
| 479 |
topkSel.addEventListener('change', predict);
|
| 480 |
demoBtn.addEventListener('click', () => { textEl.value = "Twinkle, twinkle, little "; predict(); });
|
| 481 |
modelSel.addEventListener('change', async (e) => { await loadModel(e.target.value); predict(); });
|
| 482 |
+
|
| 483 |
/* ---------- Boot ---------- */
|
| 484 |
(async function init(){
|
| 485 |
+
await loadModel(modelSel.value); // defaults to 'qwen'
|
| 486 |
if (!textEl.value) textEl.value = "Twinkle, twinkle, little ";
|
| 487 |
await predict();
|
| 488 |
})();
|