// Tokenizer loading + batch encoding, on our self-written SpmTokenizer // (src/engine/spm_tokenizer.js) — byte-exact with the transformers.js // tokenizer it replaced (test/tokenizer_parity.test.js). import { SpmTokenizer } from './spm_tokenizer.js'; import { bucketFor } from './shapes.js'; import { SRC_CAP, PAD, EOS } from './constants.js'; // /model/* is served immutable (1-year HTTP cache) but the file NAMES are // fixed, unlike the content-addressed weight shards — so when the asset // changes (tokenizer audit v4.0.1) every browser that ever cached it keeps // the old bytes forever. ?v= (from the model registry) rolls the URL // with the content; no rev (bench/debug dev paths) keeps the bare URL. export const tokenizerAssetUrl = (modelId, file, tokRev = null) => `/model/${modelId}/${file}${tokRev ? `?v=${encodeURIComponent(tokRev)}` : ''}`; // modelId is the directory under /model/ holding the HF tokenizer assets // (tokenizer.json + tokenizer_config.json). Default keeps every existing // caller (bench, debug suite) on MoxhiMT-30. export async function loadTokenizer(modelId = 'moxhi', tokRev = null) { const fetchJson = async (file) => { const res = await fetch(tokenizerAssetUrl(modelId, file, tokRev)); if (!res.ok) throw new Error(`fetch ${modelId}/${file}: HTTP ${res.status}`); return res.json(); }; const [tokenizerJson, config] = await Promise.all([ fetchJson('tokenizer.json'), fetchJson('tokenizer_config.json'), ]); return new SpmTokenizer(tokenizerJson, config); } // Tokenize a batch of source texts into a right-padded id matrix. // // Each text is tokenized individually (raw ids incl. eos, no padding); rows // longer than SRC_CAP are truncated (keeping a trailing eos). `truncated` // lists their batch indices — the caller decides what a cut source means // (the app blanks the row, skips its TM write and split-retries it; silent // truncation is exactly the bug this field exists to prevent). S is the // bucketed max row length. // // Returns {ids: Uint32Array [B·S] PAD-right-padded, lens: Uint32Array [B], // B, S, truncated: number[]}. export async function tokenizeBatch(tok, texts) { const B = texts.length; const rows = []; const truncated = []; for (const text of texts) { let row = tok.encode(text); if (row.length > SRC_CAP) { console.warn(`tokenizeBatch: truncating row of ${row.length} tokens to SRC_CAP=${SRC_CAP}`); truncated.push(rows.length); row = row.slice(0, SRC_CAP); row[SRC_CAP - 1] = EOS; } rows.push(row); } const lens = Uint32Array.from(rows, (r) => r.length); const S = bucketFor(Math.max(...lens)); const ids = new Uint32Array(B * S).fill(PAD); rows.forEach((row, b) => ids.set(row, b * S)); return { ids, lens, B, S, truncated }; }