File size: 15,220 Bytes
f2878d0 4d03824 f2878d0 4d03824 f2878d0 4d03824 f2878d0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | // Builds the conformance fixture every port must pass, from the reference engine (tinydecide.js,
// which matches PyTorch to 3e-6 on ref.json).
// node make.mjs [S768_dir] [full_dir]
// Writes cases.json (inputs, shared by both builds) and expected.<build>.json (reference outputs).
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";
const here = new URL(".", import.meta.url).pathname;
// Two layouts: this repo (sdk/js next to sdk/conformance) and the Hugging Face repo (engine at the root).
const HF = !existsSync(here + "../js/tinydecide.js");
const JS = here + (HF ? "../" : "../js/");
const { TinyDecide, WordPiece } = await import(JS + "tinydecide.js");
const { makeProtos } = await import(JS + "corrections.js");
const BUILDS = { S768: process.argv[2] || here + (HF ? ".." : "../../web_S768"), full: process.argv[3] || here + (HF ? "../full" : "../../web_fc") };
// ------------------------------------------------------------------ tokenizer strings
const TOK = [
"", " ", "Hello, World!", " leading and trailing ", "tab\there\nnewline\r\nCRLF",
"Café crème brûlée, naïve résumé, jalapeño, Ångström", "é à ö combining marks",
"10 km by the thin space ideographic", "line
separator
paragraph",
"don’t “quote” ‘single’ «guillemets» — en–dash … ellipsis", "zerowidthbomsoftzwj",
"bell\u0007 escape\u001b null\u0000 del\u007f c1\u0085", "replacement � char",
"我爱北京 Tokyo 東京タワー ひらがな 한국어", "pizza 🍕 tonight 🎉🎉 👍🏽 family 👨👩👧",
"İstanbul ŞEHİR ı i̇ I", "ΣΊΣΥΦΟΣ ελληνικά Ωμέγα", "Straße STRASSE ß ẞ", "file flow ligatures",
"ABC fullwidth 123 !?", "Привет, мир! Ёлка Йод Їжак ґанок", "مرحبا بالعالم", "שלום עולם",
"नमस्ते दुनिया", "สวัสดีชาวโลก", "Ελλάδα: Ά Έ Ή Ί Ό Ύ Ώ ΐ ΰ", "DŽ Dž dž LJ titlecase", "ŁÓDŹ Gdańsk Żółć",
"Øresund Æble Œuvre Ðorđe Þór", "x".repeat(101), "a".repeat(100), "abc" + "é".repeat(99),
"unbelievably-hyphenated_words/with\\slashes", "3.14 $1,000.50 €20 £5 ¥300 50% #tag @user",
"email a.b+c@ex-ample.co.uk url https://x.io/a?b=c&d=e#f", "Hey!!! What?!?! ...", "´`^¨~ spacing accents",
"math 𝐀𝐁𝐂 𝑥𝑦 ℝ ℕ ∑ ∫ √ ≤ ≥ ≠", "© ® ™ § ¶ † ‡ • ° ± × ÷", "private use", "ꞯ ɐ ʃ IPA ŋ",
"Ⅻ Ⅳ roman numerals ①②③", "Džungla Lj Nj", "tschüß MÜNCHEN Öl Äpfel", "ÀÁÂÃÄÅàáâãäå ÈÉÊËèéêë",
"Ċ ċ Ġ ġ Ħ ħ IJ ij Ŀ ŀ ʼn Ŋ ŋ ſ", "Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ", "Ạ ạ Ả ả Ấ ấ Việt Nam tiếng Việt",
];
// Seeded fuzz over long-assigned blocks (no lone surrogates, nothing new in Unicode 16/17).
let seed = 1234567;
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 4294967296);
const POOLS = [[0x20, 0x7e], [0xa0, 0x17f], [0x180, 0x24f], [0x250, 0x2ff], [0x300, 0x36f], [0x370, 0x3ff],
[0x400, 0x4ff], [0x530, 0x58f], [0x5d0, 0x5ea], [0x600, 0x6ff], [0x900, 0x97f], [0xe00, 0xe5b], [0x1e00, 0x1fff],
[0x2000, 0x206f], [0x2100, 0x214f], [0x2190, 0x23ff], [0x2460, 0x24ff], [0x3000, 0x30ff], [0x4e00, 0x4fff],
[0xac00, 0xad00], [0xfb00, 0xfb06], [0xff01, 0xff5e], [0x1d400, 0x1d4ff], [0x1f300, 0x1f64f], [0x1f900, 0x1f9ff]];
const RE_CN = /\p{Cn}/u;
for (let i = 0; i < 240; i++) {
let s = "";
const n = 3 + Math.floor(rnd() * 30);
while ([...s].length < n) {
const [a, b] = rnd() < 0.35 ? POOLS[0] : POOLS[Math.floor(rnd() * POOLS.length)];
const cp = a + Math.floor(rnd() * (b - a + 1));
const ch = String.fromCodePoint(cp);
if (!RE_CN.test(ch)) s += ch;
if (rnd() < 0.15) s += " ";
}
TOK.push(s);
}
// ------------------------------------------------------------------ requests
const LONG = "Dear team, I wanted to give you a full update on the warehouse move before the weekend. " +
"The trucks are booked for Saturday morning at 6:30 and the first load will leave the old site around 7:15. " +
"We still need four more volunteers for the afternoon shift, so please reply if you can help. " +
"Parking at the new building is behind the main entrance, and the code for the side gate is 4471. " +
"Lunch will be provided by the Italian place on Market Street, and there will be vegetarian options. " +
"If anything breaks during the move, write it down on the damage sheet next to the loading bay and tell Maria. " +
"The internet at the new site will not be live until Tuesday, so download anything you need beforehand. " +
"Thanks again for all the hard work this month, it has been a long one and I really appreciate it.";
const APPS = ["reminders", "music", "calendar", "restaurants", "weather"];
const README_QS = [
{ type: "choice", text: "Which app should handle this?", options: APPS },
{ type: "noul", text: "The message is urgent." },
{ type: "score", text: "How positive is the tone?", options: ["negative", "neutral", "positive"] },
{ type: "span", text: "Extract the time." },
];
const NOTE_Q = { type: "choice", text: "What kind of note is this?", options: ["something to buy", "a task to do", "an event with a date or time", "a phone number or email"] };
const SCORE_Q = { type: "score", text: "How urgent is this?", options: ["not urgent", "somewhat urgent", "very urgent"] };
const NOUL_Q = { type: "noul", text: "The message is about food." };
const REQ = [
{ name: "readme", state: "Book a table for 4 at an Italian place near the station on Friday at 7:30", questions: README_QS },
{ name: "multi7", state: "Hey, can you remind me to call Dr. Patel at 555-0142 tomorrow at 9am? It's about my test results.",
questions: [
{ type: "choice", text: "Which app should handle this?", options: ["reminders", "music", "calendar", "phone", "weather"] },
{ type: "noul", text: "The message is urgent." },
{ type: "noul", text: "The message mentions a phone number." },
{ type: "score", text: "How worried does the writer sound?", options: ["calm", "a little worried", "very worried"] },
{ type: "span", text: "Extract the phone number." },
{ type: "span", text: "Extract the time." },
{ type: "choice", text: "What is the topic?", options: ["health", "money", "travel", "work", "family", "sport", "food", "weather"] },
] },
{ name: "long_truncated", state: LONG, questions: [
{ type: "choice", text: "What is this message about?", options: ["a move", "a party", "a complaint", "a job offer"] },
{ type: "span", text: "Extract the gate code." },
{ type: "span", text: "Extract when the trucks leave." },
{ type: "noul", text: "The writer is thankful." } ] },
{ name: "empty_state", state: "", questions: [{ type: "noul", text: "The message is empty." }, { type: "span", text: "Extract the name." },
{ type: "choice", text: "Pick one.", options: ["yes", "no"] }] },
{ name: "unicode_state", state: "Réservez une table pour 2 au Café Müller — demain à 19h30, s’il vous plaît! 🍷 Tél: +33 6 12 34 56 78",
questions: [ { type: "choice", text: "Which language is this?", options: ["French", "German", "English", "Spanish"] },
{ type: "span", text: "Extract the phone number." }, { type: "span", text: "Extract the restaurant name." },
{ type: "noul", text: "The writer is polite." } ] },
{ name: "unicode_question", state: "Привет! Купи молоко и хлеб, пожалуйста. Встреча в 18:00 у метро «Киевская».",
questions: [ { type: "choice", text: "Qu’est-ce que c’est — “shopping” or “meeting”?", options: ["achats 🛒", "réunion", "naïve idée"] },
{ type: "span", text: "Extract the time." }, { type: "span", text: "Extract the place." } ] },
{ name: "cjk_emoji", state: "明天下午3点在東京駅见面 🚄 don't be late!!", questions: [
{ type: "noul", text: "This is about meeting someone." }, { type: "span", text: "Extract the time." },
{ type: "choice", text: "Which script is mostly used?", options: ["Chinese characters", "Latin letters", "Cyrillic"] } ] },
{ name: "room", state: "The meeting moved to room 4B.", questions: [
{ type: "noul", text: "The message gives a room." }, { type: "span", text: "Extract the room." } ] },
{ name: "choice32", state: "I'd like a large pepperoni pizza delivered to 12 Oak Lane please.", questions: [
{ type: "choice", text: "What does the person want?", options: Array.from({ length: 32 }, (_, i) => ["pizza", "sushi", "tacos", "a taxi", "a doctor", "a plumber", "flowers", "a haircut"][i % 8] + " " + (i + 1)) } ] },
{ name: "score5", state: "This is the worst service I've ever had. Never again.", questions: [
{ type: "score", text: "How satisfied is the customer?", options: ["very unhappy", "unhappy", "neutral", "happy", "very happy"] },
{ type: "noul", text: "The customer will come back." } ] },
{ name: "span_numbers", state: "Order #A-77812 shipped on 2026-10-03, total $149.99, tracking 1Z999AA10123456784.", questions: [
{ type: "span", text: "Extract the order number." }, { type: "span", text: "Extract the total." },
{ type: "span", text: "Extract the date." }, { type: "span", text: "Extract the tracking number." }, { type: "span", text: "Extract the email address." } ] },
{ name: "whitespace", state: " \t buy milk \n\n and
eggs ", questions: [NOTE_Q, { type: "span", text: "Extract the items." }] },
{ name: "too_long_question", state: "hello", questions: [{ type: "choice", text: "Which one? " + "very ".repeat(200), options: ["a", "b"] }] },
{ name: "one_option", state: "hello there", questions: [{ type: "choice", text: "Pick one.", options: ["only"] }] },
{ name: "score_no_levels", state: "hello there", questions: [{ type: "noul", text: "It is a greeting." }, { type: "score", text: "How formal is it?", options: [] }] },
{ name: "choice33", state: "hello there", questions: [{ type: "choice", text: "Which number?", options: Array.from({ length: 33 }, (_, i) => String(i + 1)) }] },
{ name: "noul_with_options", state: "The meeting moved to room 4B.", questions: [{ type: "noul", text: "The message gives a room.", options: ["no", "yes"] }] },
{ name: "span_with_options", state: "The meeting moved to room 4B.", questions: [{ type: "span", text: "Extract the room.", options: ["4B", "5C"] }] },
];
// protos cases: examples come from the model's own answers on training notes (built per build below)
const PROTO_TRAIN = {
note: [["buy oat milk", "get coffee beans and bread"], ["call the plumber", "fix the bike"], ["dentist thursday 4pm"], []],
score: [["whenever you have time", "no rush"], ["by end of week please"], ["ASAP the server is down", "urgent: fire alarm"]],
noul: [["the car needs new tyres", "meeting at noon"], ["pizza for dinner", "grab some sushi", "bake a cake"]],
};
const PROTO_ASK = [
{ name: "protos_choice", state: "pick up eggs and butter", q: NOTE_Q, kind: "note" },
{ name: "protos_choice_event", state: "lunch with Sam on friday 1pm", q: NOTE_Q, kind: "note" },
{ name: "protos_score", state: "please reply today, the client is waiting", q: SCORE_Q, kind: "score" },
{ name: "protos_noul", state: "make pasta tonight", q: NOUL_Q, kind: "noul" },
{ name: "protos_one_example", state: "buy stamps", q: NOTE_Q, kind: "note", only: 1 },
{ name: "protos_no_center", state: "pick up eggs and butter", q: NOTE_Q, kind: "note", noCenter: true },
{ name: "protos_noul_no_center", state: "make pasta tonight", q: NOUL_Q, kind: "noul", noCenter: true },
];
// ------------------------------------------------------------------ run
const r6 = (x) => Math.round(x * 1e6) / 1e6;
const arr = (a) => Array.from(a, r6);
function outAnswer(a) {
if (a.type === "choice" || a.type === "score") {
const o = { type: a.type, probs: arr(a.probs), pick: a.pick, confidence: r6(a.confidence), z0: arr(a.z0), qvec: arr(a.qvec) };
if (a.type === "score") o.score = r6(a.score);
return o;
}
if (a.type === "noul") return { type: "noul", p: r6(a.p), z0: arr(a.z0), qvec: arr(a.qvec) };
return { type: "span", p_present: r6(a.p_present), p_span: r6(a.p_span), tok: a.tok, text: a.text };
}
const ser = (p) => p && { vec: arr(p.vec), cnt: Array.from(p.cnt), center: p.center ? arr(p.center) : null, lam: p.lam };
const cases = { version: 1, engine_sha256: sha(readFileSync(JS + "tinydecide.js")), tokenizer: TOK, requests: [], protos: [] };
function sha(b) { return createHash("sha256").update(b).digest("hex"); }
let first = true;
for (const [build, dir] of Object.entries(BUILDS)) {
const meta = JSON.parse(readFileSync(dir + "/meta.json", "utf8"));
const bin = readFileSync(dir + "/model.bin");
const m = new TinyDecide(meta, bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength));
const tok = new WordPiece(meta.tokenizer);
const exp = { build, model_sha256: sha(bin), meta_sha256: sha(readFileSync(dir + "/meta.json")), tokenizer: [], requests: [], protos: [] };
for (const s of TOK) exp.tokenizer.push(tok.encode(s).ids);
// ref.json inputs are the same for both builds (the Hugging Face repo keeps one, at the root)
const ref = JSON.parse(readFileSync(BUILDS.S768 + "/ref.json", "utf8"));
const all = [...REQ, ...ref.map((r, i) => ({ name: "ref" + i, state: r.state, questions: r.questions }))];
for (const r of all) {
if (first) cases.requests.push({ name: r.name, state: r.state, questions: r.questions });
try {
const got = m.answer({ state: r.state, questions: r.questions });
exp.requests.push({ name: r.name, ids: got.ids, truncated: got.truncated, tokens: got.tokens, answers: got.answers.map(outAnswer) });
} catch (e) {
exp.requests.push({ name: r.name, error: e.message });
}
}
// corrections: examples -> protos (makeProtos) -> answers with protos
for (const c of PROTO_ASK) {
const lists = PROTO_TRAIN[c.kind].map((l, k) => (c.only !== undefined ? (k === 0 ? l.slice(0, c.only) : []) : l).map((st) => {
const a = m.answer({ state: st, questions: [c.q] }).answers[0];
return { v: arr(a.qvec), z: arr(a.z0) };
}));
let p = makeProtos(c.q.type, lists, meta.beta);
if (c.noCenter) p = { vec: p.vec, cnt: p.cnt, center: null, lam: p.lam };
if (first) cases.protos.push({ name: c.name, state: c.state, question: c.q, no_center: !!c.noCenter, examples_from: PROTO_TRAIN[c.kind], only: c.only ?? null });
const got = m.answer({ state: c.state, questions: [c.q] }, [p]);
exp.protos.push({ name: c.name, examples: lists, protos: ser(p), ids: got.ids, answers: got.answers.map(outAnswer) });
}
writeFileSync(here + `expected.${build}.json`, JSON.stringify(exp));
console.log(`${build}: ${exp.tokenizer.length} tokenizer strings, ${exp.requests.length} requests ` +
`(${exp.requests.filter((r) => r.error).length} errors, ${exp.requests.filter((r) => r.truncated).length} truncated), ${exp.protos.length} protos cases`);
first = false;
}
writeFileSync(here + "cases.json", JSON.stringify(cases, null, 1));
|