Download conformance/make.mjs from TheREZOR/TinyDecide: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/TheREZOR/TinyDecide/resolve/main/conformance/make.mjs
- Command line
-
hf download hf://TheREZOR/TinyDecide/conformance/make.mjs
-
curl -L -o make.mjs https://huggingface.co/TheREZOR/TinyDecide/resolve/main/conformance/make.mjs
14.8 kB
| // 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: "noul_with_options", state: "The meeting moved to room 4B.", questions: [ | |
| { type: "noul", text: "The message gives a room.", options: ["no", "yes"] }, | |
| { 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)) }] }, | |
| ]; | |
| // 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 }, | |
| ]; | |
| // ------------------------------------------------------------------ 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)); | |