From 511b7f8b620850486a5a6bd325b369c9a39187fe Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Fri, 11 Sep 2026 22:06:33 +0800 Subject: [PATCH] feat(playground): speculative vocalization tier (2.1 quality, int4 drafts) Third vocalization option: ara-diac-layerdrop-1.0-int4 drafts, ara-diac-small-2.1-int8 verifies in one pass per block - measured acceptance 0.9886 at 8.85 tokens per verifier pass (RESULTS.md 2026-09-11), output-preserving by construction. Status line reports the acceptance after each run. Requires interscript ^5.4.0 (SpeculativeModel). Live e2e (SPEC_E2E=1): downloads both models in a fresh profile, vocalizes, and shows the readout (1.6m). Also: stale CLI-name test expectations (interscript-ts -> interscript, pre-existing red on main), dead frontmatter var + unused SITE (lint), playground.astro added to .prettierignore (hand-wrapped prose). --- .prettierignore | 3 ++ e2e/playground-spec-live.spec.ts | 25 ++++++++++++++++ package-lock.json | 20 +++---------- package.json | 2 +- src/pages/openapi.json.ts | 2 +- src/pages/playground.astro | 49 ++++++++++++++++++++++++-------- test/round3-features.test.ts | 12 ++++---- 7 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 e2e/playground-spec-live.spec.ts diff --git a/.prettierignore b/.prettierignore index d3f5478..e770d32 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,3 +27,6 @@ TODO.complete/ src/pages/compare.astro src/pages/maps/\[systemCode\].astro src/pages/api-docs.astro + +# hand-wrapped prose (Astro whitespace trap) — see CLAUDE.md +src/pages/playground.astro diff --git a/e2e/playground-spec-live.spec.ts b/e2e/playground-spec-live.spec.ts new file mode 100644 index 0000000..369d46f --- /dev/null +++ b/e2e/playground-spec-live.spec.ts @@ -0,0 +1,25 @@ +// LIVE playground speculative-tier verification — downloads the real +// drafter+verifier pair through the assets proxy and decodes in the +// browser with the verifier deciding every token. Opt-in (SPEC_E2E=1): +// SPEC_E2E=1 npx playwright test e2e/playground-spec-live.spec.ts +import { expect, test } from "@playwright/test" + +test("speculative tier downloads both models and vocalizes in the browser", async ({ page }) => { + test.skip(!process.env.SPEC_E2E, "live download test — set SPEC_E2E=1") + test.setTimeout(900_000) + await page.goto(`${process.env.SPEC_E2E_BASE ?? "http://localhost:4321"}/playground`) + await page.fill("#input", "قوله فحكمها في الوفاة") + await page.selectOption("#vocalize", "spec:ara-diac-small-2.1-int8") + await page.click("#run") + // both downloads stream progress, then the pair decodes + await expect(page.locator("#status")).toBeVisible({ timeout: 30_000 }) + const vocalized = page.locator("#vocalized") + // fresh profile = 359 MB total; budget for a slow network + await expect(vocalized).not.toBeEmpty({ timeout: 840_000 }) + const text = (await vocalized.textContent()) ?? "" + expect(text).toMatch(/[ً-ْٰٓ-ٕ]/) + expect(text).not.toMatch(/Error|cause:/) + // the acceptance readout surfaces once the worker call resolves + // after the vocalized text — poll rather than read once + await expect(page.locator("#status")).toContainText("accepted", { timeout: 60_000 }) +}) diff --git a/package-lock.json b/package-lock.json index 53be33d..78c5e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@litertjs/core": "^2.5.3", "@tailwindcss/vite": "^4.0.0", "astro": "^7.3.1", - "interscript": "^5.2.1", + "interscript": "^5.4.0", "onnxruntime-web": "^1.29.0", "svgo": "^4.1.0", "tailwindcss": "^4.0.0", @@ -947,9 +947,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -963,9 +960,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -979,9 +973,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -995,9 +986,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6562,9 +6550,9 @@ "license": "ISC" }, "node_modules/interscript": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/interscript/-/interscript-5.2.1.tgz", - "integrity": "sha512-iZJa5ErX4gMpFB1j4MLyrWccLJa6MTLAosXW3Pi2t1BcMGrl8Q+E8dV3I+R6NNoGgFsc2B0GK4BEuKP3ARZvoA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/interscript/-/interscript-5.4.0.tgz", + "integrity": "sha512-ZBmkOyO5zr1XCurMSETbsuNnpkrrWC4WERsC7d5gE5Zf03P7xDbeIiy42/vujIxvV6lXQph3wJ38CEwSg9iJcA==", "license": "BSD-2-Clause", "dependencies": { "fflate": "^0.8.3", diff --git a/package.json b/package.json index 6423a79..2ec7c61 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@litertjs/core": "^2.5.3", "@tailwindcss/vite": "^4.0.0", "astro": "^7.3.1", - "interscript": "^5.2.1", + "interscript": "^5.4.0", "onnxruntime-web": "^1.29.0", "svgo": "^4.1.0", "tailwindcss": "^4.0.0", diff --git a/src/pages/openapi.json.ts b/src/pages/openapi.json.ts index 34dec39..485acf9 100644 --- a/src/pages/openapi.json.ts +++ b/src/pages/openapi.json.ts @@ -13,7 +13,7 @@ import type { APIRoute } from "astro" export const prerender = true -const SITE = "https://interscript.org" +const _SITE = "https://interscript.org" const spec = { openapi: "3.1.0", diff --git a/src/pages/playground.astro b/src/pages/playground.astro index 20c1377..8da25df 100644 --- a/src/pages/playground.astro +++ b/src/pages/playground.astro @@ -6,9 +6,6 @@ import Base from "../layouts/Base.astro" import catalogue from "../data/maps-catalogue.json" -const systems = Object.entries(catalogue as Record) - .map(([code, m]) => ({ code, name: m.data.name, lang: m.data.language })) - .sort((a, b) => a.name.localeCompare(b.name)) --- @@ -30,6 +27,9 @@ const systems = Object.entries(catalogue as RecordOff — deterministic maps only + @@ -168,6 +168,10 @@ const systems = Object.entries(catalogue as Record { - status.textContent = `${id}: ${(b / 1e6).toFixed(0)} MB (${Math.round(f * 100)}%)` - }, - }) - const model = await imf.IMFModel.fromZipBytes(resolved.bytes) - mlModel = model as unknown as { translate(t: string, m?: number): Promise } + const spec = id.startsWith("spec:") + const verifierId = spec ? id.slice(5) : id + const drafterId = spec ? "ara-diac-layerdrop-1.0-int4" : "" + const wanted = spec ? [drafterId, verifierId] : [verifierId] + const models: Record>> = {} + for (const wantedId of wanted) { + status.hidden = false + status.textContent = `Downloading ${wantedId} (once per browser)…` + const resolved = await imf.resolve(wantedId, ASSET_INDEX, { + onProgress: (f: number, b: number) => { + status.textContent = `${wantedId}: ${(b / 1e6).toFixed(0)} MB (${Math.round(f * 100)}%)` + }, + }) + models[wantedId] = await imf.IMFModel.fromZipBytes(resolved.bytes) + } + mlModel = spec + ? (new imf.SpeculativeModel(models[drafterId]!, models[verifierId]!) as unknown as { + translate(t: string, m?: number): Promise + stats(): { accepted: number; drafted: number; blocks: number } | undefined + }) + : (models[verifierId] as unknown as { translate(t: string, m?: number): Promise }) mlModelId = id status.hidden = true return mlModel @@ -218,6 +234,15 @@ const systems = Object.entries(catalogue as Record { accepted: number; drafted: number; blocks: number } } + ).stats?.() + if (stats) { + status.hidden = false + status.textContent = `Speculative: ${stats.accepted}/${stats.drafted} draft tokens accepted across ${stats.blocks} verifier passes` + } + } const share = new URLSearchParams({ q: ($("input") as HTMLTextAreaElement).value, sys: systemSelect.value, diff --git a/test/round3-features.test.ts b/test/round3-features.test.ts index 3700292..2a7cd3d 100644 --- a/test/round3-features.test.ts +++ b/test/round3-features.test.ts @@ -144,19 +144,19 @@ describe("API page additions (CLI + GitHub Action)", () => { const html = readHtml("api/index.html") it("mentions the CLI install command", () => { - expect(html).toContain("npm install -g interscript-ts") - expect(html).toContain("npx interscript-ts") + expect(html).toContain("npm install -g interscript") + expect(html).toContain("npx interscript") }) it("shows single-call + batch CLI usage", () => { - expect(html).toContain("interscript-ts t ") - expect(html).toContain("interscript-ts b ") - expect(html).toContain("interscript-ts list ") + expect(html).toContain("interscript t ") + expect(html).toContain("interscript b ") + expect(html).toContain("interscript list ") }) it("includes a GitHub Action snippet", () => { expect(html).toContain("name: Romanize names") expect(html).toContain("uses: actions/setup-node") - expect(html).toContain("npx interscript-ts@latest") + expect(html).toContain("npx interscript@latest") }) })