Skip to content

Commit 518f4b2

Browse files
committed
feat: neural models demo — local browser inference (TODO.client-work 08/11)
Model picker with sizes, verified-download progress bar, streaming decode output, per-step confidence readout, cache clear. Runs interscript@5.1.0 client-side — no server round-trip.
1 parent 9dfe3c9 commit 518f4b2

3 files changed

Lines changed: 152 additions & 5 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"astro": "^7.0.0",
3636
"tailwindcss": "^4.0.0",
3737
"vue": "^3.5.0",
38-
"interscript": "^5.0.0"
38+
"interscript": "^5.1.0"
3939
},
4040
"devDependencies": {
4141
"@astrojs/check": "^0.9.10",

src/pages/neural.astro

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
// Neural models demo (TODO.client-work 08/11): live diacritization in
3+
// the browser — verified Release fetch, download-once cache, streaming
4+
// decode with per-step confidence. No server round-trip.
5+
import Base from "../layouts/Base.astro"
6+
---
7+
8+
<Base title="Neural models demo">
9+
<section class="page-narrow">
10+
<h1>Neural models demo</h1>
11+
<p>
12+
Authority-backed diacritization running locally in your browser. The model
13+
downloads once (content-verified), then works offline.
14+
</p>
15+
<noscript><p>This demo needs JavaScript.</p></noscript>
16+
17+
<label for="model">Model</label>
18+
<select id="model">
19+
<option value="ara-diac-small-2.0-int8">Arabic diacritization — 2.0 (int8, 264 MB)</option>
20+
<option value="ara-diac-small-1.0-int8">Arabic diacritization — 1.0 (int8, 264 MB)</option>
21+
<option value="tha-g2p-small-1.0">Thai G2P — small (int8, 202 MB)</option>
22+
</select>
23+
24+
<label for="input">Input</label>
25+
<textarea id="input" rows="4" dir="auto">السلام عليكم ورحمة الله</textarea>
26+
27+
<button id="run" class="btn">Transliterate</button>
28+
<button id="clear-cache" class="btn">Clear cached models</button>
29+
30+
<div id="progress" hidden>
31+
<div class="progress-track"><div id="progress-bar" class="progress-fill"></div></div>
32+
<span id="progress-label"></span>
33+
</div>
34+
35+
<label for="output">Output</label>
36+
<output id="output" dir="auto" readonly></output>
37+
<p id="confidence" class="muted" hidden></p>
38+
</section>
39+
</Base>
40+
41+
<style>
42+
.progress-track {
43+
height: 8px;
44+
background: rgba(128, 128, 128, 0.25);
45+
border-radius: 4px;
46+
overflow: hidden;
47+
margin: 0.75rem 0;
48+
}
49+
.progress-fill {
50+
height: 100%;
51+
width: 0%;
52+
background: #008075;
53+
transition: width 0.2s ease;
54+
}
55+
#output {
56+
display: block;
57+
min-height: 3rem;
58+
padding: 0.6rem;
59+
border: 1px solid rgba(128, 128, 128, 0.4);
60+
border-radius: 6px;
61+
font-size: 1.15rem;
62+
white-space: pre-wrap;
63+
}
64+
.low-conf {
65+
background: rgba(200, 120, 0, 0.25);
66+
border-radius: 3px;
67+
}
68+
.muted {
69+
color: #5c6470;
70+
font-size: 0.85rem;
71+
}
72+
label {
73+
display: block;
74+
margin: 0.75rem 0 0.25rem;
75+
font-weight: 600;
76+
}
77+
select,
78+
textarea {
79+
width: 100%;
80+
padding: 0.5rem;
81+
border-radius: 6px;
82+
border: 1px solid rgba(128, 128, 128, 0.4);
83+
}
84+
.btn {
85+
margin: 0.75rem 0.5rem 0.75rem 0;
86+
}
87+
</style>
88+
89+
<script>
90+
import { imf } from "interscript/ml"
91+
92+
const $ = (id: string) => document.getElementById(id)!
93+
let current: { dispose?(): Promise<void> } | null = null
94+
95+
async function load(modelId: string): Promise<{ translate(t: string, m?: number, o?: object): Promise<string> }> {
96+
const progress = $("progress")
97+
const bar = $("progress-bar")
98+
const label = $("progress-label")
99+
progress.hidden = false
100+
label.textContent = "Resolving verified index…"
101+
const resolved = await imf.resolve(modelId, undefined, {
102+
onProgress: (fraction, bytes) => {
103+
bar.style.width = `${Math.round(fraction * 100)}%`
104+
label.textContent = `${(bytes / 1e6).toFixed(0)} MB (${Math.round(fraction * 100)}%)`
105+
},
106+
})
107+
label.textContent = "Opening session…"
108+
const model = await imf.IMFModel.fromZipBytes(resolved.bytes)
109+
progress.hidden = true
110+
return model
111+
}
112+
113+
$("run").addEventListener("click", async () => {
114+
const out = $("output")
115+
const conf = $("confidence")
116+
out.textContent = ""
117+
try {
118+
const model = await load(($("model") as HTMLSelectElement).value)
119+
current?.dispose?.()
120+
current = model as unknown as { dispose?(): Promise<void> }
121+
const gaps: number[] = []
122+
const parts: string[] = []
123+
const text = ($("input") as HTMLTextAreaElement).value
124+
const result = await model.translate(text, 2048, {
125+
onToken: (token: number) => {
126+
parts.push(imf.decode([token]))
127+
out.textContent = parts.join("")
128+
},
129+
onConfidence: (gap: number) => gaps.push(gap),
130+
})
131+
out.textContent = result
132+
if (gaps.length) {
133+
const min = Math.min(...gaps)
134+
conf.hidden = false
135+
conf.textContent = `Decode confidence: min top-2 gap ${min.toFixed(2)} across ${gaps.length} steps (lower = less certain)`
136+
}
137+
} catch (e) {
138+
out.textContent = `Error: ${(e as Error).message}`
139+
}
140+
})
141+
142+
$("clear-cache").addEventListener("click", async () => {
143+
if (typeof caches !== "undefined") await caches.delete("interscript-imf-models-v1")
144+
$("confidence").hidden = true
145+
$("confidence").textContent = "Model cache cleared — next run re-downloads."
146+
})
147+
</script>

0 commit comments

Comments
 (0)