Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions e2e/playground-spec-live.spec.ts
Original file line number Diff line number Diff line change
@@ -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 })
})
20 changes: 4 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/pages/openapi.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
49 changes: 37 additions & 12 deletions src/pages/playground.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
import Base from "../layouts/Base.astro"
import catalogue from "../data/maps-catalogue.json"

const systems = Object.entries(catalogue as Record<string, { data: { name: string; language: string } }>)
.map(([code, m]) => ({ code, name: m.data.name, lang: m.data.language }))
.sort((a, b) => a.name.localeCompare(b.name))
---

<Base title="Playground">
Expand All @@ -30,6 +27,9 @@ const systems = Object.entries(catalogue as Record<string, { data: { name: strin
<option value="">Off — deterministic maps only</option>
<option value="ara-diac-layerdrop-1.0-int4">Arabic lite (int4, 95 MB, downloads once)</option>
<option value="ara-diac-small-2.1-int8">Arabic 2.1 (int8, 264 MB, downloads once)</option>
<option value="spec:ara-diac-small-2.1-int8">
Arabic 2.1 + speculative (int4 drafts it, 359 MB, downloads once)
</option>
</select>

<label for="filter">System</label>
Expand Down Expand Up @@ -168,6 +168,10 @@ const systems = Object.entries(catalogue as Record<string, { data: { name: strin
if (!voc) {
return `import { transliterateAsync } from "interscript"\n\nconst out = await transliterateAsync(\n "${code}",\n "…",\n)`
}
if (voc.startsWith("spec:")) {
const verifier = voc.slice(5)
return `import { transliterateAsync } from "interscript"\nimport { imf } from "interscript/ml"\n\nconst drafter = await imf.IMFModel.fromZipBytes(\n (await imf.resolve("ara-diac-layerdrop-1.0-int4")).bytes,\n)\nconst verifierModel = await imf.IMFModel.fromZipBytes(\n (await imf.resolve("${verifier}")).bytes,\n)\nconst spec = new imf.SpeculativeModel(drafter, verifierModel)\nconst vocalized = await spec.translate("…")\nconst out = await transliterateAsync(\n "${code}",\n vocalized,\n)`
}
return `import { transliterateAsync } from "interscript"\nimport { imf } from "interscript/ml"\n\nconst resolved = await imf.resolve("${voc}")\nconst model = await imf.IMFModel.fromZipBytes(resolved.bytes)\nconst vocalized = await model.translate("…")\nconst out = await transliterateAsync(\n "${code}",\n vocalized,\n)`
}
$("snippet").textContent = snippetText()
Expand All @@ -185,15 +189,27 @@ const systems = Object.entries(catalogue as Record<string, { data: { name: strin
async function ensureModel(id: string) {
if (mlModel && mlModelId === id) return mlModel
const status = $("status")
status.hidden = false
status.textContent = `Downloading ${id} (once per browser)…`
const resolved = await imf.resolve(id, ASSET_INDEX, {
onProgress: (f: number, b: number) => {
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<string> }
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<string, Awaited<ReturnType<typeof imf.IMFModel.fromZipBytes>>> = {}
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<string>
stats(): { accepted: number; drafted: number; blocks: number } | undefined
})
: (models[verifierId] as unknown as { translate(t: string, m?: number): Promise<string> })
mlModelId = id
status.hidden = true
return mlModel
Expand All @@ -218,6 +234,15 @@ const systems = Object.entries(catalogue as Record<string, { data: { name: strin
}
const result = await worker.transliterate(systemSelect.value, text)
out.textContent = result
if (modelId?.startsWith("spec:") && mlModel) {
const stats = (
mlModel as { stats?: () => { 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,
Expand Down
12 changes: 6 additions & 6 deletions test/round3-features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})