From 55a1087e913f697bb431f9a148721806c892b909 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 17 Sep 2026 16:43:45 +0800 Subject: [PATCH] feat(ml): windowed long-input translate (the 1400-byte protocol, client-side) Models train on <=1400-byte windows; longer inputs were reaching them out-of-distribution and degrading. splitWindows reproduces the published Python harness split exactly (word-boundary, byte-budget, Python split semantics - the fixture is generated from the reference and caught the empty-token difference). translateWindowed routes both IMFModel.translate and SpeculativeModel.translate: single-window inputs (every golden-set size) pass straight through, so cross- runtime byte parity is untouched; long inputs decode as the join of their window decodes. translateDirect is the former body. 7 split-parity specs + 3 behavior specs; full suite 286. --- src/ml/imf/model.ts | 7 +++++ src/ml/imf/speculative.ts | 5 ++++ src/ml/imf/windows.ts | 51 ++++++++++++++++++++++++++++++++ test/fixtures/windows-split.json | 38 ++++++++++++++++++++++++ test/windows-split.test.ts | 35 ++++++++++++++++++++++ test/windows-translate.test.ts | 41 +++++++++++++++++++++++++ 6 files changed, 177 insertions(+) create mode 100644 src/ml/imf/windows.ts create mode 100644 test/fixtures/windows-split.json create mode 100644 test/windows-split.test.ts create mode 100644 test/windows-translate.test.ts diff --git a/src/ml/imf/model.ts b/src/ml/imf/model.ts index 69384c9..6a3bff2 100644 --- a/src/ml/imf/model.ts +++ b/src/ml/imf/model.ts @@ -12,6 +12,7 @@ import { verifyAndRead, parseManifest, type IMFManifest } from "./loader.js" import { resolve } from "./registry.js" import { normalizeArabicInput, repetitionGuardCut } from "./guards.js" import { EOS_ID, PAD_ID, decode, encode } from "./tokens.js" +import { translateWindowed } from "./windows.js" interface InputMeta { readonly name: string @@ -90,6 +91,12 @@ export class IMFModel { } async translate(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise { + // long inputs are out-of-distribution: split at the training + // budget (single-window inputs pass straight through) + return translateWindowed(this, text, maxLen, opts) + } + + async translateDirect(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise { const hidden = await this.encode(text, opts) if (!hidden) return "" const tokens = this.kv diff --git a/src/ml/imf/speculative.ts b/src/ml/imf/speculative.ts index 09f175e..13c4c5a 100644 --- a/src/ml/imf/speculative.ts +++ b/src/ml/imf/speculative.ts @@ -31,6 +31,7 @@ import { normalizeArabicInput, repetitionGuardCut } from "./guards.js" import { EOS_ID, decode, encode } from "./tokens.js" import type { IMFModel, DecodeOptions, DecodeCursor } from "./model.js" +import { translateWindowed } from "./windows.js" export interface SpeculativeOptions { /** draft tokens per verifier pass (default 8) */ @@ -105,6 +106,10 @@ export class SpeculativeModel { } async translate(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise { + return translateWindowed(this, text, maxLen, opts) + } + + async translateDirect(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise { const normalized = opts.raw === true ? text : normalizeArabicInput(text) this.lastNormalized = normalized if (encode(normalized).length === 1) return "" diff --git a/src/ml/imf/windows.ts b/src/ml/imf/windows.ts new file mode 100644 index 0000000..fadfb39 --- /dev/null +++ b/src/ml/imf/windows.ts @@ -0,0 +1,51 @@ +/** + * Windowed long-input handling (the runtime side of the 1400-byte + * protocol): models train on <=1400-byte windows, so longer inputs are + * out-of-distribution and degrade. splitWindows reproduces the + * published harness split exactly (word-boundary, byte-budget); + * translateWithWindows decodes each window and joins with spaces. + */ + +export const BYTE_BUDGET = 1400 + +/** Split at word boundaries so no window exceeds the byte budget. + * Identical to the Python harness's split_windows. */ +export function splitWindows(text: string, budget: number = BYTE_BUDGET): string[] { + if (new TextEncoder().encode(text).length <= budget) return [text] + const windows: string[] = [] + let current: string[] = [] + let n = 0 + // Python str.split(): runs of whitespace, no empty tokens + for (const word of text.split(/\s+/).filter((w) => w !== "")) { + const cost = new TextEncoder().encode(word).length + 1 + if (current.length > 0 && n + cost > budget) { + windows.push(current.join(" ")) + current = [] + n = 0 + } + current.push(word) + n += cost + } + if (current.length > 0) windows.push(current.join(" ")) + return windows +} + +import type { DecodeOptions } from "./model.js" + +/** Decode text through `model`, splitting at the training budget when + * the input is long. Single-window inputs (all golden-set sizes) pass + * through untouched — cross-runtime parity is unaffected. Both + * IMFModel.translate and SpeculativeModel.translate route through + * this, so long inputs never reach a model out-of-distribution. */ +export async function translateWindowed( + model: { translateDirect(text: string, maxLen?: number, opts?: DecodeOptions): Promise }, + text: string, + maxLen: number, + opts: DecodeOptions = {}, +): Promise { + const windows = splitWindows(text) + if (windows.length <= 1) return model.translateDirect(text, maxLen, opts) + const parts: string[] = [] + for (const w of windows) parts.push(await model.translateDirect(w, maxLen, opts)) + return parts.join(" ") +} diff --git a/test/fixtures/windows-split.json b/test/fixtures/windows-split.json new file mode 100644 index 0000000..c4acc98 --- /dev/null +++ b/test/fixtures/windows-split.json @@ -0,0 +1,38 @@ +{ + "budget": 1400, + "cases": { + "short": [ + "قوله فحكمها" + ], + "boundary-exact": [ + "abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc" + ], + "just-over": [ + "abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc", + "abc" + ], + "long-arabic": [ + "السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة", + "الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام", + "عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله", + "وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم", + "ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته", + "السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته" + ], + "single-long-word": [ + "اااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااا" + ], + "mixed-lengths": [ + "كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة", + "طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة" + ] + }, + "inputs": { + "short": "قوله فحكمها", + "boundary-exact": "abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc", + "just-over": "abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc", + "long-arabic": "السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته السلام عليكم ورحمة الله وبركاته ", + "single-long-word": "اااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااااا", + "mixed-lengths": "كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة كلمة x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة طويل-جدا-لكلمة-واحدة-طويلة" + } +} \ No newline at end of file diff --git a/test/windows-split.test.ts b/test/windows-split.test.ts new file mode 100644 index 0000000..8d7e7c5 --- /dev/null +++ b/test/windows-split.test.ts @@ -0,0 +1,35 @@ +/** + * Windowed long-input handling — parity with the published Python + * harness (ml-models src/harness/sadeed.py). The fixture in + * test/fixtures/windows-split.json is generated from the Python + * split_windows; the parity test pins this port to it. + */ +import { readFileSync } from "node:fs" +import { describe, expect, it } from "vitest" +import { splitWindows, BYTE_BUDGET } from "../src/ml/imf/windows.js" + +const fixture = JSON.parse(readFileSync("test/fixtures/windows-split.json", "utf8")) as { + budget: number + inputs: Record + cases: Record +} + +describe("splitWindows parity with the Python harness", () => { + for (const [name, expected] of Object.entries(fixture.cases)) { + it(`matches on ${name}`, () => { + expect(splitWindows(fixture.inputs[name], fixture.budget)).toEqual(expected) + }) + } + + it("keeps every window within the byte budget", () => { + for (const windows of Object.values(fixture.cases)) { + for (const w of windows) { + // unbreakable single words exceed the budget in the Python + // reference too — the split cannot cut inside a word + if (w.split(" ").length > 1) { + expect(new TextEncoder().encode(w).length).toBeLessThanOrEqual(fixture.budget) + } + } + } + }) +}) diff --git a/test/windows-translate.test.ts b/test/windows-translate.test.ts new file mode 100644 index 0000000..4142be7 --- /dev/null +++ b/test/windows-translate.test.ts @@ -0,0 +1,41 @@ +/** + * Windowed translate behavior: long inputs decode as the join of + * their windows' decodes; short inputs are byte-identical to the + * direct path (the golden-parity guarantee is untouched). + */ +import { readFileSync } from "node:fs" +import { describe, expect, it } from "vitest" +import { IMFModel, SpeculativeModel } from "../src/ml/imf/index.js" +import { splitWindows } from "../src/ml/imf/windows.js" + +const fixtureZip = new Uint8Array(readFileSync("test/fixtures/tiny-imf.zip")) + +describe("translateWindowed over the tiny fixture", () => { + it("short inputs match the direct path byte-for-byte", async () => { + const model = await IMFModel.fromZipBytes(fixtureZip) + const text = "rok dto roki" + expect(await model.translate(text, 64)).toBe(await model.translateDirect(text, 64)) + await model.dispose() + }, 60_000) + + it("a long input decodes as the join of its window decodes", async () => { + const model = await IMFModel.fromZipBytes(fixtureZip) + const text = "rok dto roki ".repeat(200).trim() // > 1400 bytes + const windows = splitWindows(text) + expect(windows.length).toBeGreaterThan(1) + const expected = (await Promise.all(windows.map((w) => model.translateDirect(w, 64)))).join(" ") + expect(await model.translate(text, 64)).toBe(expected) + await model.dispose() + }, 120_000) + + it("the speculative path windows the same way", async () => { + const model = await IMFModel.fromZipBytes(fixtureZip) + const spec = new SpeculativeModel(model, model) + const text = "rok dto roki ".repeat(200).trim() + const expected = ( + await Promise.all(splitWindows(text).map((w) => model.translateDirect(w, 64))) + ).join(" ") + expect(await spec.translate(text, 64)).toBe(expected) + await model.dispose() + }, 120_000) +})