PeterPinetree commited on
Commit
0ac3e50
·
verified ·
1 Parent(s): 1434c28

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

Files changed (1) hide show
  1. 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
- // Load everything locally (no Hub fetch)
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);} 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); }
 
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
- // IMPORTANT:
307
- // - For local Qwen: pass the URL object directly (prevents HF prefixing).
308
- // - For distilgpt2 (Hub): pass the repo id string.
309
- const tokBase = (key === "qwen")
310
- ? MODELS.qwen.local // URL object (local directory)
311
- : MODELS[key].remote; // "Xenova/distilgpt2"
312
-
313
- tokenizer = await AutoTokenizer.from_pretrained(tokBase, {
314
- progress_callback: onProgress,
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' (Qwen3-0.6B)
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
  })();