Reza2kn commited on
Commit
f2940ad
·
verified ·
1 Parent(s): 5fcde64

Use progressive W2 ONNX model in WebGPU demo

Browse files
Files changed (6) hide show
  1. README.md +14 -7
  2. SPACE_DEPLOY.md +19 -0
  3. app.js +491 -0
  4. app.py +222 -0
  5. requirements.txt +1 -0
  6. tokens.json +1031 -0
README.md CHANGED
@@ -1,13 +1,20 @@
1
  ---
2
- title: Visualears Fastconformer Fa Webgpu
3
- emoji: 👁
4
- colorFrom: indigo
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
+ title: VisualEars FastConformer FA W2 WebGPU
3
+ emoji: 🎙️
4
+ colorFrom: green
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.35.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
+ short_description: Browser WebGPU mic demo for VisualEars Persian ASR W2 ONNX
12
  ---
13
 
14
+ # VisualEars FastConformer Persian ASR WebGPU Demo
15
+
16
+ Minimal Gradio Space for trying `Reza2kn/visualears-fastconformer-fa-full-ab-onnx-w2-progressive` directly in the browser with WebGPU.
17
+
18
+ The ONNX artifact is a fixed-frame acoustic CTC core. It expects precomputed NeMo-style 80-bin log-mel features shaped `[1, 80, 2005]`, so this Space captures mic audio in the browser, computes approximate log-mel features client-side, runs ONNX Runtime WebGPU, and greedily decodes the CTC output.
19
+
20
+ This is a rolling-window live test harness, not a true cache-aware streaming export. It is meant for quick real-life/noisy microphone experiments and latency/RTF visibility.
SPACE_DEPLOY.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploy
2
+
3
+ Create a new Hugging Face Space with SDK `gradio`, then upload this folder:
4
+
5
+ ```bash
6
+ huggingface-cli repo create visualears-fastconformer-fa-webgpu --type space --space_sdk gradio
7
+ git clone https://huggingface.co/spaces/Reza2kn/visualears-fastconformer-fa-webgpu
8
+ rsync -av --delete visualears-webgpu-space/ visualears-fastconformer-fa-webgpu/
9
+ cd visualears-fastconformer-fa-webgpu
10
+ git add .
11
+ git commit -m "Add VisualEars WebGPU ASR demo"
12
+ git push
13
+ ```
14
+
15
+ The app downloads the ONNX model from:
16
+
17
+ `Reza2kn/visualears-fastconformer-fa-full-ab-onnx-fp`
18
+
19
+ The first browser load fetches about 461 MB. The browser should cache later loads.
app.js ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const MODEL_URL = "https://huggingface.co/Reza2kn/visualears-fastconformer-fa-full-ab-onnx-w2-progressive/resolve/main/fastconformer_ctc_fixed2005.onnx";
2
+ const MODEL_DATA_URL = "https://huggingface.co/Reza2kn/visualears-fastconformer-fa-full-ab-onnx-w2-progressive/resolve/main/fastconformer_ctc_fixed2005.onnx.data";
3
+ const SAMPLE_RATE = 16000;
4
+ const N_FFT = 512;
5
+ const WIN_LENGTH = 400;
6
+ const HOP_LENGTH = 160;
7
+ const N_MELS = 80;
8
+ const FIXED_FRAMES = 2005;
9
+ const OUTPUT_STRIDE = 8;
10
+
11
+ const $ = (id) => document.getElementById(id);
12
+ let ortRuntime = null;
13
+
14
+ const state = {
15
+ session: null,
16
+ tokens: null,
17
+ blankId: 1024,
18
+ audioContext: null,
19
+ source: null,
20
+ processor: null,
21
+ mediaStream: null,
22
+ utterancePcm: new Float32Array(0),
23
+ finalTranscript: "",
24
+ partialTranscript: "",
25
+ speechActive: false,
26
+ silenceMs: 0,
27
+ finalizing: false,
28
+ recording: false,
29
+ decoding: false,
30
+ decodeTimer: null,
31
+ lastDecodeAt: 0,
32
+ fftPlan: null,
33
+ melFilters: null,
34
+ gpuName: "-",
35
+ };
36
+
37
+ function setStatus(message) {
38
+ $("status").textContent = message;
39
+ }
40
+
41
+ function setText(id, value) {
42
+ $(id).textContent = value;
43
+ }
44
+
45
+ function formatMs(ms) {
46
+ if (!Number.isFinite(ms)) return "-";
47
+ return ms < 1000 ? `${ms.toFixed(0)} ms` : `${(ms / 1000).toFixed(2)} s`;
48
+ }
49
+
50
+ function updateHeap() {
51
+ const mem = performance.memory;
52
+ if (!mem) {
53
+ setText("stat-heap", "unavailable");
54
+ return;
55
+ }
56
+ setText("stat-heap", `${(mem.usedJSHeapSize / 1048576).toFixed(0)} / ${(mem.jsHeapSizeLimit / 1048576).toFixed(0)} MB`);
57
+ }
58
+
59
+ function renderTranscript() {
60
+ $("transcript").textContent = `Final:\n${state.finalTranscript || "..."}\n\nPartial:\n${state.partialTranscript || "..."}`;
61
+ }
62
+
63
+ function appendToUtterance(chunk) {
64
+ const maxSeconds = Number($("window-seconds")?.value || 20);
65
+ const maxSamples = Math.ceil(maxSeconds * SAMPLE_RATE);
66
+ const merged = new Float32Array(Math.min(maxSamples, state.utterancePcm.length + chunk.length));
67
+ const keep = Math.max(0, merged.length - chunk.length);
68
+ if (keep > 0) merged.set(state.utterancePcm.subarray(state.utterancePcm.length - keep), 0);
69
+ merged.set(chunk.subarray(Math.max(0, chunk.length - merged.length)), keep);
70
+ state.utterancePcm = merged;
71
+ }
72
+
73
+ function resampleLinear(input, fromRate, toRate) {
74
+ if (fromRate === toRate) return new Float32Array(input);
75
+ const outLen = Math.max(1, Math.round(input.length * toRate / fromRate));
76
+ const output = new Float32Array(outLen);
77
+ const ratio = (input.length - 1) / Math.max(1, outLen - 1);
78
+ for (let i = 0; i < outLen; i++) {
79
+ const x = i * ratio;
80
+ const j = Math.floor(x);
81
+ const frac = x - j;
82
+ output[i] = input[j] * (1 - frac) + input[Math.min(j + 1, input.length - 1)] * frac;
83
+ }
84
+ return output;
85
+ }
86
+
87
+ function hzToMel(hz) {
88
+ return 2595 * Math.log10(1 + hz / 700);
89
+ }
90
+
91
+ function melToHz(mel) {
92
+ return 700 * (10 ** (mel / 2595) - 1);
93
+ }
94
+
95
+ function createMelFilters() {
96
+ const minMel = hzToMel(0);
97
+ const maxMel = hzToMel(SAMPLE_RATE / 2);
98
+ const melPoints = new Array(N_MELS + 2).fill(0).map((_, i) => minMel + (maxMel - minMel) * i / (N_MELS + 1));
99
+ const bins = melPoints.map((m) => Math.floor((N_FFT + 1) * melToHz(m) / SAMPLE_RATE));
100
+ const filters = [];
101
+ for (let m = 1; m <= N_MELS; m++) {
102
+ const filter = new Float32Array(N_FFT / 2 + 1);
103
+ const left = bins[m - 1], center = bins[m], right = bins[m + 1];
104
+ for (let k = left; k < center; k++) filter[k] = (k - left) / Math.max(1, center - left);
105
+ for (let k = center; k < right; k++) filter[k] = (right - k) / Math.max(1, right - center);
106
+ filters.push(filter);
107
+ }
108
+ return filters;
109
+ }
110
+
111
+ function createFftPlan(n) {
112
+ const cos = new Float32Array(n / 2);
113
+ const sin = new Float32Array(n / 2);
114
+ for (let i = 0; i < n / 2; i++) {
115
+ cos[i] = Math.cos(-2 * Math.PI * i / n);
116
+ sin[i] = Math.sin(-2 * Math.PI * i / n);
117
+ }
118
+ return { n, cos, sin };
119
+ }
120
+
121
+ function fftRealPower(frame, plan) {
122
+ const n = plan.n;
123
+ const re = new Float32Array(n);
124
+ const im = new Float32Array(n);
125
+ re.set(frame);
126
+ let j = 0;
127
+ for (let i = 1; i < n; i++) {
128
+ let bit = n >> 1;
129
+ for (; j & bit; bit >>= 1) j ^= bit;
130
+ j ^= bit;
131
+ if (i < j) {
132
+ const tr = re[i]; re[i] = re[j]; re[j] = tr;
133
+ const ti = im[i]; im[i] = im[j]; im[j] = ti;
134
+ }
135
+ }
136
+ for (let len = 2; len <= n; len <<= 1) {
137
+ const half = len >> 1;
138
+ const step = n / len;
139
+ for (let i = 0; i < n; i += len) {
140
+ for (let k = 0; k < half; k++) {
141
+ const idx = k * step;
142
+ const wr = plan.cos[idx];
143
+ const wi = plan.sin[idx];
144
+ const ur = re[i + k];
145
+ const ui = im[i + k];
146
+ const vr = re[i + k + half] * wr - im[i + k + half] * wi;
147
+ const vi = re[i + k + half] * wi + im[i + k + half] * wr;
148
+ re[i + k] = ur + vr;
149
+ im[i + k] = ui + vi;
150
+ re[i + k + half] = ur - vr;
151
+ im[i + k + half] = ui - vi;
152
+ }
153
+ }
154
+ }
155
+ const power = new Float32Array(n / 2 + 1);
156
+ for (let i = 0; i < power.length; i++) power[i] = (re[i] * re[i] + im[i] * im[i]) / n;
157
+ return power;
158
+ }
159
+
160
+ function pcmToLogMel(pcm) {
161
+ if (!state.fftPlan) state.fftPlan = createFftPlan(N_FFT);
162
+ if (!state.melFilters) state.melFilters = createMelFilters();
163
+
164
+ const frameCount = Math.max(1, Math.min(FIXED_FRAMES, Math.floor((pcm.length - WIN_LENGTH) / HOP_LENGTH) + 1));
165
+ const features = new Float32Array(N_MELS * FIXED_FRAMES);
166
+ const hann = new Float32Array(WIN_LENGTH);
167
+ for (let i = 0; i < WIN_LENGTH; i++) hann[i] = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (WIN_LENGTH - 1));
168
+
169
+ for (let t = 0; t < frameCount; t++) {
170
+ const offset = t * HOP_LENGTH;
171
+ const frame = new Float32Array(N_FFT);
172
+ for (let i = 0; i < WIN_LENGTH; i++) frame[i] = (pcm[offset + i] || 0) * hann[i];
173
+ const power = fftRealPower(frame, state.fftPlan);
174
+ for (let m = 0; m < N_MELS; m++) {
175
+ const filter = state.melFilters[m];
176
+ let energy = 0;
177
+ for (let k = 0; k < filter.length; k++) energy += power[k] * filter[k];
178
+ features[m * FIXED_FRAMES + t] = Math.log(Math.max(energy, 1e-20));
179
+ }
180
+ }
181
+
182
+ for (let m = 0; m < N_MELS; m++) {
183
+ let mean = 0;
184
+ const base = m * FIXED_FRAMES;
185
+ for (let t = 0; t < frameCount; t++) mean += features[base + t];
186
+ mean /= frameCount;
187
+ let variance = 0;
188
+ for (let t = 0; t < frameCount; t++) {
189
+ const d = features[base + t] - mean;
190
+ variance += d * d;
191
+ }
192
+ const invStd = 1 / Math.sqrt(variance / frameCount + 1e-5);
193
+ for (let t = 0; t < frameCount; t++) features[base + t] = (features[base + t] - mean) * invStd;
194
+ }
195
+
196
+ return { features, frameCount };
197
+ }
198
+
199
+ function decodeCtc(logits, timeSteps, vocabSize) {
200
+ const tokens = state.tokens;
201
+ const blank = state.blankId;
202
+ let previous = -1;
203
+ const pieces = [];
204
+ for (let t = 0; t < timeSteps; t++) {
205
+ let best = 0;
206
+ let bestValue = -Infinity;
207
+ const base = t * vocabSize;
208
+ for (let i = 0; i < vocabSize; i++) {
209
+ const v = logits[base + i];
210
+ if (v > bestValue) {
211
+ bestValue = v;
212
+ best = i;
213
+ }
214
+ }
215
+ if (best !== blank && best !== previous && best !== 0) pieces.push(tokens[best] || "");
216
+ previous = best;
217
+ }
218
+ return pieces.join("").replaceAll("▁", " ").replace(/\s+/g, " ").trim();
219
+ }
220
+
221
+ async function inspectGpu() {
222
+ if (!navigator.gpu) {
223
+ state.gpuName = "not exposed";
224
+ setText("stat-gpu", state.gpuName);
225
+ return;
226
+ }
227
+ try {
228
+ const adapter = await navigator.gpu.requestAdapter();
229
+ if (!adapter) {
230
+ state.gpuName = "unavailable";
231
+ } else if (adapter.info) {
232
+ state.gpuName = [adapter.info.vendor, adapter.info.architecture, adapter.info.device].filter(Boolean).join(" ") || "available";
233
+ } else if (adapter.requestAdapterInfo) {
234
+ const info = await adapter.requestAdapterInfo();
235
+ state.gpuName = [info.vendor, info.architecture, info.device].filter(Boolean).join(" ") || "available";
236
+ } else {
237
+ state.gpuName = "available";
238
+ }
239
+ } catch (err) {
240
+ state.gpuName = `unavailable: ${err.message}`;
241
+ }
242
+ setText("stat-gpu", state.gpuName);
243
+ }
244
+
245
+ async function loadModel() {
246
+ $("load-model").disabled = true;
247
+ setStatus("Loading tokenizer and ONNX Runtime WebGPU...");
248
+ updateHeap();
249
+ await inspectGpu();
250
+
251
+ const tokenJson = window.VISUALEARS_TOKENS;
252
+ if (!tokenJson?.tokens?.length) throw new Error("Missing embedded CTC tokens");
253
+ state.tokens = tokenJson.tokens;
254
+ state.blankId = tokenJson.blank_id;
255
+
256
+ const ort = ortRuntime;
257
+ if (!ort) throw new Error("ONNX Runtime Web did not initialize");
258
+ ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0/dist/";
259
+ ort.env.wasm.numThreads = Math.max(1, Math.min(4, navigator.hardwareConcurrency || 1));
260
+ if (ort.env.webgpu) {
261
+ ort.env.webgpu.profiling = false;
262
+ }
263
+
264
+ const provider = $("provider").value;
265
+ setStatus(`Downloading/opening ${provider.toUpperCase()} session. First load is large; cached reloads should be much faster.`);
266
+ const start = performance.now();
267
+ state.session = await ort.InferenceSession.create(MODEL_URL, {
268
+ executionProviders: [provider],
269
+ externalData: [
270
+ {
271
+ path: "fastconformer_ctc_fixed2005.onnx.data",
272
+ data: MODEL_DATA_URL,
273
+ },
274
+ ],
275
+ graphOptimizationLevel: "all",
276
+ enableMemPattern: false,
277
+ enableCpuMemArena: true,
278
+ });
279
+ const elapsed = performance.now() - start;
280
+ setText("stat-provider", provider);
281
+ setText("stat-decode", "-");
282
+ setText("stat-rtf", "-");
283
+ setText("stat-speed", "-");
284
+ updateHeap();
285
+ setStatus(`Model ready in ${formatMs(elapsed)}. Start the microphone when you are ready.`);
286
+ $("start-mic").disabled = false;
287
+ }
288
+
289
+ async function startMic() {
290
+ if (!state.session) return;
291
+ state.mediaStream = await navigator.mediaDevices.getUserMedia({
292
+ audio: {
293
+ channelCount: 1,
294
+ echoCancellation: true,
295
+ noiseSuppression: true,
296
+ autoGainControl: true,
297
+ },
298
+ });
299
+ state.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
300
+ state.source = state.audioContext.createMediaStreamSource(state.mediaStream);
301
+ state.processor = state.audioContext.createScriptProcessor(4096, 1, 1);
302
+ state.source.connect(state.processor);
303
+ state.processor.connect(state.audioContext.destination);
304
+ state.utterancePcm = new Float32Array(0);
305
+ state.finalTranscript = "";
306
+ state.partialTranscript = "";
307
+ state.speechActive = false;
308
+ state.silenceMs = 0;
309
+ state.finalizing = false;
310
+ renderTranscript();
311
+ state.recording = true;
312
+ $("start-mic").disabled = true;
313
+ $("stop-mic").disabled = false;
314
+ setStatus("Listening. Partial text can move while you speak; final text freezes after silence.");
315
+
316
+ state.processor.onaudioprocess = (event) => {
317
+ if (!state.recording) return;
318
+ const input = event.inputBuffer.getChannelData(0);
319
+ let rms = 0;
320
+ for (let i = 0; i < input.length; i++) rms += input[i] * input[i];
321
+ rms = Math.sqrt(rms / input.length);
322
+ $("level-bar").style.width = `${Math.min(100, rms * 900).toFixed(1)}%`;
323
+ const gate = Number($("noise-gate").value);
324
+ const speechy = rms >= gate;
325
+ const chunk = resampleLinear(input, state.audioContext.sampleRate, SAMPLE_RATE);
326
+ const chunkMs = chunk.length / SAMPLE_RATE * 1000;
327
+
328
+ if (speechy) {
329
+ if (!state.speechActive) {
330
+ state.utterancePcm = new Float32Array(0);
331
+ state.partialTranscript = "";
332
+ state.silenceMs = 0;
333
+ state.speechActive = true;
334
+ state.finalizing = false;
335
+ renderTranscript();
336
+ }
337
+ state.silenceMs = 0;
338
+ appendToUtterance(chunk);
339
+ return;
340
+ }
341
+
342
+ if (!state.speechActive) return;
343
+ state.silenceMs += chunkMs;
344
+ if (state.silenceMs <= 350) appendToUtterance(chunk);
345
+ if (state.silenceMs >= 900 && !state.finalizing) {
346
+ state.finalizing = true;
347
+ finalizeUtterance();
348
+ }
349
+ };
350
+
351
+ scheduleDecode();
352
+ }
353
+
354
+ function stopMic() {
355
+ state.recording = false;
356
+ clearTimeout(state.decodeTimer);
357
+ state.decodeTimer = null;
358
+ if (state.processor) state.processor.disconnect();
359
+ if (state.source) state.source.disconnect();
360
+ if (state.mediaStream) state.mediaStream.getTracks().forEach((track) => track.stop());
361
+ if (state.audioContext) state.audioContext.close();
362
+ state.processor = null;
363
+ state.source = null;
364
+ state.mediaStream = null;
365
+ state.audioContext = null;
366
+ $("level-bar").style.width = "0%";
367
+ $("start-mic").disabled = !state.session;
368
+ $("stop-mic").disabled = true;
369
+ setStatus("Stopped.");
370
+ }
371
+
372
+ function scheduleDecode() {
373
+ clearTimeout(state.decodeTimer);
374
+ if (!state.recording) return;
375
+ const delay = Number($("decode-every").value) * 1000;
376
+ state.decodeTimer = setTimeout(async () => {
377
+ await runDecode();
378
+ scheduleDecode();
379
+ }, delay);
380
+ }
381
+
382
+ async function runDecode() {
383
+ if (!state.session || state.decoding || state.finalizing || !state.speechActive || state.utterancePcm.length < SAMPLE_RATE * 0.4) return;
384
+ await decodeUtterance(false);
385
+ }
386
+
387
+ async function finalizeUtterance() {
388
+ if (state.decoding) {
389
+ setTimeout(finalizeUtterance, 150);
390
+ return;
391
+ }
392
+ if (!state.session || state.utterancePcm.length < SAMPLE_RATE * 0.25) {
393
+ state.speechActive = false;
394
+ state.finalizing = false;
395
+ return;
396
+ }
397
+ await decodeUtterance(true);
398
+ }
399
+
400
+ async function decodeUtterance(final) {
401
+ state.decoding = true;
402
+ try {
403
+ const maxSeconds = Number($("window-seconds").value);
404
+ const samples = Math.min(state.utterancePcm.length, Math.floor(maxSeconds * SAMPLE_RATE));
405
+ const audio = state.utterancePcm.subarray(state.utterancePcm.length - samples);
406
+ const preprocessStarted = performance.now();
407
+ const { features, frameCount } = pcmToLogMel(audio);
408
+ const ort = ortRuntime;
409
+ if (!ort) throw new Error("ONNX Runtime Web did not initialize");
410
+ const tensor = new ort.Tensor("float32", features, [1, N_MELS, FIXED_FRAMES]);
411
+ const inferStarted = performance.now();
412
+ const output = await state.session.run({ processed_signal: tensor });
413
+ const inferElapsed = performance.now() - inferStarted;
414
+ const elapsed = performance.now() - preprocessStarted;
415
+ const logits = output.logits.data;
416
+ const dims = output.logits.dims;
417
+ const vocabSize = dims[2] || state.tokens.length;
418
+ const usableSteps = Math.max(1, Math.min(dims[1], Math.ceil(frameCount / OUTPUT_STRIDE)));
419
+ const transcript = decodeCtc(logits, usableSteps, vocabSize);
420
+ const audioSeconds = samples / SAMPLE_RATE;
421
+ const rtf = (elapsed / 1000) / Math.max(0.001, audioSeconds);
422
+ const speed = Math.max(0.001, audioSeconds) / Math.max(0.001, elapsed / 1000);
423
+ if (final) {
424
+ if (transcript) {
425
+ state.finalTranscript = state.finalTranscript ? `${state.finalTranscript}\n${transcript}` : transcript;
426
+ }
427
+ state.partialTranscript = "";
428
+ state.speechActive = false;
429
+ state.finalizing = false;
430
+ state.silenceMs = 0;
431
+ state.utterancePcm = new Float32Array(0);
432
+ } else {
433
+ state.partialTranscript = transcript;
434
+ }
435
+ renderTranscript();
436
+ setText("stat-decode", `${formatMs(elapsed)} total / ${formatMs(inferElapsed)} infer`);
437
+ setText("stat-rtf", `${rtf.toFixed(3)}`);
438
+ setText("stat-speed", `${speed.toFixed(1)}x`);
439
+ setText("stat-audio", `${audioSeconds.toFixed(1)} s`);
440
+ setText("stat-frames", `${frameCount} / ${FIXED_FRAMES}`);
441
+ setText("stat-provider", $("provider").value);
442
+ setText("stat-last", new Date().toLocaleTimeString());
443
+ updateHeap();
444
+ setStatus(`${final ? "Finalized" : "Decoded partial"} ${audioSeconds.toFixed(1)} s utterance with ${usableSteps} CTC steps.`);
445
+ } catch (err) {
446
+ console.error(err);
447
+ setStatus(`Decode failed: ${err.message}`);
448
+ if (final) {
449
+ state.speechActive = false;
450
+ state.finalizing = false;
451
+ }
452
+ } finally {
453
+ state.decoding = false;
454
+ }
455
+ }
456
+
457
+ async function boot() {
458
+ if (!$("load-model")) {
459
+ setTimeout(boot, 100);
460
+ return;
461
+ }
462
+ ortRuntime = await import("https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0/dist/ort.webgpu.min.mjs");
463
+ $("load-model").addEventListener("click", () => loadModel().catch((err) => {
464
+ console.error(err);
465
+ setStatus(`Load failed: ${err.message}`);
466
+ $("load-model").disabled = false;
467
+ }));
468
+ $("start-mic").addEventListener("click", () => startMic().catch((err) => {
469
+ console.error(err);
470
+ setStatus(`Mic failed: ${err.message}`);
471
+ stopMic();
472
+ }));
473
+ $("stop-mic").addEventListener("click", stopMic);
474
+ $("provider").addEventListener("change", () => {
475
+ if (state.session) {
476
+ state.session = null;
477
+ $("start-mic").disabled = true;
478
+ $("load-model").disabled = false;
479
+ setStatus("Provider changed. Load the model again.");
480
+ }
481
+ });
482
+
483
+ setText("stat-provider", "-");
484
+ setText("stat-gpu", "-");
485
+ updateHeap();
486
+ }
487
+
488
+ boot().catch((err) => {
489
+ console.error(err);
490
+ setStatus(`Startup failed: ${err.message}`);
491
+ });
app.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import json
3
+
4
+ import gradio as gr
5
+
6
+
7
+ ROOT = Path(__file__).parent
8
+
9
+
10
+ def main() -> gr.Blocks:
11
+ tokens = json.loads((ROOT / "tokens.json").read_text(encoding="utf-8"))
12
+ app_js = (ROOT / "app.js").read_text(encoding="utf-8")
13
+ head = (
14
+ "<script type=\"module\">\n"
15
+ f"window.VISUALEARS_TOKENS = {json.dumps(tokens, ensure_ascii=False)};\n"
16
+ f"{app_js}\n"
17
+ "</script>"
18
+ )
19
+ with gr.Blocks(
20
+ title="VisualEars WebGPU ASR",
21
+ head=head,
22
+ css="""
23
+ .gradio-container { max-width: 1180px !important; }
24
+ #visualears-root {
25
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ color: #111827;
27
+ }
28
+ #visualears-root .topbar {
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: space-between;
32
+ gap: 16px;
33
+ margin-bottom: 16px;
34
+ }
35
+ #visualears-root h1 {
36
+ font-size: 28px;
37
+ line-height: 1.15;
38
+ margin: 0;
39
+ letter-spacing: 0;
40
+ }
41
+ #visualears-root .subtle {
42
+ color: #4b5563;
43
+ margin: 6px 0 0;
44
+ font-size: 14px;
45
+ }
46
+ #visualears-root .controls {
47
+ display: grid;
48
+ grid-template-columns: repeat(4, minmax(0, 1fr));
49
+ gap: 10px;
50
+ align-items: end;
51
+ margin: 14px 0;
52
+ }
53
+ #visualears-root button, #visualears-root select, #visualears-root input {
54
+ width: 100%;
55
+ min-height: 40px;
56
+ border: 1px solid #cbd5e1;
57
+ border-radius: 7px;
58
+ background: white;
59
+ color: #111827;
60
+ font: inherit;
61
+ padding: 8px 10px;
62
+ }
63
+ #visualears-root button.primary {
64
+ background: #111827;
65
+ color: white;
66
+ border-color: #111827;
67
+ }
68
+ #visualears-root button:disabled {
69
+ opacity: 0.45;
70
+ cursor: not-allowed;
71
+ }
72
+ #visualears-root label {
73
+ display: block;
74
+ font-size: 12px;
75
+ color: #475569;
76
+ margin-bottom: 5px;
77
+ }
78
+ #visualears-root .status {
79
+ min-height: 42px;
80
+ overflow-wrap: anywhere;
81
+ border: 1px solid #d1d5db;
82
+ background: #f8fafc;
83
+ border-radius: 7px;
84
+ padding: 10px 12px;
85
+ margin: 10px 0 14px;
86
+ }
87
+ #visualears-root .transcript {
88
+ min-height: 150px;
89
+ direction: rtl;
90
+ text-align: right;
91
+ font-size: 26px;
92
+ line-height: 1.65;
93
+ border: 1px solid #cbd5e1;
94
+ border-radius: 8px;
95
+ padding: 18px;
96
+ background: #ffffff;
97
+ overflow-wrap: anywhere;
98
+ white-space: pre-wrap;
99
+ }
100
+ #visualears-root .stats {
101
+ display: grid;
102
+ grid-template-columns: repeat(4, minmax(0, 1fr));
103
+ gap: 10px;
104
+ margin-top: 14px;
105
+ }
106
+ #visualears-root .stat {
107
+ border: 1px solid #d1d5db;
108
+ border-radius: 8px;
109
+ padding: 12px;
110
+ background: #ffffff;
111
+ min-height: 74px;
112
+ }
113
+ #visualears-root .stat b {
114
+ display: block;
115
+ font-size: 12px;
116
+ color: #64748b;
117
+ margin-bottom: 8px;
118
+ font-weight: 600;
119
+ }
120
+ #visualears-root .stat span {
121
+ font-size: 20px;
122
+ color: #111827;
123
+ overflow-wrap: anywhere;
124
+ }
125
+ #visualears-root .meter {
126
+ height: 8px;
127
+ background: #e5e7eb;
128
+ overflow: hidden;
129
+ border-radius: 999px;
130
+ margin-top: 8px;
131
+ }
132
+ #visualears-root .meter > div {
133
+ height: 100%;
134
+ width: 0%;
135
+ background: #10b981;
136
+ transition: width 120ms linear;
137
+ }
138
+ @media (max-width: 820px) {
139
+ #visualears-root .topbar { align-items: flex-start; flex-direction: column; }
140
+ #visualears-root .controls, #visualears-root .stats { grid-template-columns: 1fr 1fr; }
141
+ #visualears-root .transcript { font-size: 22px; }
142
+ }
143
+ @media (max-width: 520px) {
144
+ #visualears-root .controls, #visualears-root .stats { grid-template-columns: 1fr; }
145
+ }
146
+ """,
147
+ ) as demo:
148
+ gr.HTML(
149
+ """
150
+ <div id="visualears-root">
151
+ <div class="topbar">
152
+ <div>
153
+ <h1>VisualEars Persian ASR W2</h1>
154
+ <p class="subtle">Browser-only WebGPU decode with the progressive W2 QAT ONNX export.</p>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="controls">
159
+ <button id="load-model" class="primary">Load WebGPU model</button>
160
+ <button id="start-mic" disabled>Start mic</button>
161
+ <button id="stop-mic" disabled>Stop</button>
162
+ <div>
163
+ <label for="decode-every">Decode every</label>
164
+ <select id="decode-every">
165
+ <option value="1.5">1.5 s</option>
166
+ <option value="2.5" selected>2.5 s</option>
167
+ <option value="4">4 s</option>
168
+ </select>
169
+ </div>
170
+ <div>
171
+ <label for="window-seconds">Max utterance</label>
172
+ <select id="window-seconds">
173
+ <option value="8">8 s</option>
174
+ <option value="12" selected>12 s</option>
175
+ <option value="18">18 s</option>
176
+ <option value="20">20 s</option>
177
+ </select>
178
+ </div>
179
+ <div>
180
+ <label for="provider">Provider</label>
181
+ <select id="provider">
182
+ <option value="webgpu" selected>WebGPU</option>
183
+ <option value="wasm">WASM fallback</option>
184
+ </select>
185
+ </div>
186
+ <div>
187
+ <label for="noise-gate">Speech gate</label>
188
+ <input id="noise-gate" type="range" min="0" max="0.04" value="0.006" step="0.001" />
189
+ </div>
190
+ <div>
191
+ <label>Input level</label>
192
+ <div class="meter"><div id="level-bar"></div></div>
193
+ </div>
194
+ </div>
195
+
196
+ <div id="status" class="status">Idle. Load the W2 model first; the first load downloads about 461 MB and then uses browser cache.</div>
197
+ <div id="transcript" class="transcript">Final:
198
+ ...
199
+
200
+ Partial:
201
+ ...</div>
202
+
203
+ <div class="stats">
204
+ <div class="stat"><b>Decode Time</b><span id="stat-decode">-</span></div>
205
+ <div class="stat"><b>RTF</b><span id="stat-rtf">-</span></div>
206
+ <div class="stat"><b>Realtime Speed</b><span id="stat-speed">-</span></div>
207
+ <div class="stat"><b>Audio Window</b><span id="stat-audio">-</span></div>
208
+ <div class="stat"><b>Feature Frames</b><span id="stat-frames">-</span></div>
209
+ <div class="stat"><b>Provider</b><span id="stat-provider">-</span></div>
210
+ <div class="stat"><b>Heap Used</b><span id="stat-heap">-</span></div>
211
+ <div class="stat"><b>GPU Adapter</b><span id="stat-gpu">-</span></div>
212
+ <div class="stat"><b>Last Decode</b><span id="stat-last">-</span></div>
213
+ </div>
214
+ </div>
215
+ """
216
+ )
217
+
218
+ return demo
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main().launch()
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio>=5.0,<6.0
tokens.json ADDED
@@ -0,0 +1,1031 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "blank_id": 1024,
3
+ "unk_id": 0,
4
+ "tokens": [
5
+ "<unk>",
6
+ "▁ب",
7
+ "▁م",
8
+ "▁ا",
9
+ "▁ک",
10
+ "▁د",
11
+ "ار",
12
+ "ست",
13
+ "ان",
14
+ "▁ر",
15
+ "▁می",
16
+ "▁ت",
17
+ "ای",
18
+ "▁ن",
19
+ "▁خ",
20
+ "ین",
21
+ "▁و",
22
+ "▁ه",
23
+ "▁ش",
24
+ "ید",
25
+ "ند",
26
+ "▁به",
27
+ "رد",
28
+ "▁س",
29
+ "▁آ",
30
+ "▁چ",
31
+ "ود",
32
+ "▁پ",
33
+ "ون",
34
+ "▁را",
35
+ "▁با",
36
+ "▁من",
37
+ "اد",
38
+ "▁این",
39
+ "▁ی",
40
+ "▁در",
41
+ "یم",
42
+ "▁از",
43
+ "▁بر",
44
+ "▁کن",
45
+ "▁است",
46
+ "ام",
47
+ "▁خو",
48
+ "▁که",
49
+ "ال",
50
+ "اه",
51
+ "▁تو",
52
+ "▁دار",
53
+ "▁گ",
54
+ "▁ج",
55
+ "ور",
56
+ "یر",
57
+ "▁ف",
58
+ "▁ز",
59
+ "▁رو",
60
+ "▁ح",
61
+ "شت",
62
+ "فت",
63
+ "لی",
64
+ "▁هم",
65
+ "یا",
66
+ "▁او",
67
+ "▁ق",
68
+ "▁کرد",
69
+ "▁ع",
70
+ "▁یک",
71
+ "ما",
72
+ "ده",
73
+ "می",
74
+ "های",
75
+ "اب",
76
+ "▁بود",
77
+ "ها",
78
+ "ری",
79
+ "▁دو",
80
+ "لا",
81
+ "رو",
82
+ "را",
83
+ "جا",
84
+ "سی",
85
+ "▁آن",
86
+ "▁ل",
87
+ "کن",
88
+ "▁ما",
89
+ "▁کار",
90
+ "تر",
91
+ "▁هست",
92
+ "▁ص",
93
+ "▁چی",
94
+ "▁چه",
95
+ "زی",
96
+ "▁بی",
97
+ "▁مو",
98
+ "تی",
99
+ "▁خود",
100
+ "▁ام",
101
+ "شه",
102
+ "▁شما",
103
+ "رم",
104
+ "اشت",
105
+ "وی",
106
+ "▁کنم",
107
+ "▁نمی",
108
+ "▁تا",
109
+ "▁شد",
110
+ "نگ",
111
+ "اس",
112
+ "▁یه",
113
+ "انی",
114
+ "گه",
115
+ "فا",
116
+ "اید",
117
+ "خت",
118
+ "یست",
119
+ "▁اون",
120
+ "اده",
121
+ "اری",
122
+ "ول",
123
+ "از",
124
+ "ات",
125
+ "▁دی",
126
+ "وان",
127
+ "▁آیا",
128
+ "دی",
129
+ "▁بگ",
130
+ "اق",
131
+ "بی",
132
+ "اره",
133
+ "▁ان",
134
+ "ایی",
135
+ "▁خی",
136
+ "گر",
137
+ "ره",
138
+ "▁باش",
139
+ "گی",
140
+ "▁ند",
141
+ "مان",
142
+ "▁پی",
143
+ "خو",
144
+ "انه",
145
+ "▁هی",
146
+ "وم",
147
+ "در",
148
+ "اهم",
149
+ "▁لط",
150
+ "▁دست",
151
+ "رف",
152
+ "اش",
153
+ "▁ط",
154
+ "▁سر",
155
+ "گاه",
156
+ "انم",
157
+ "وش",
158
+ "▁داد",
159
+ "نی",
160
+ "▁خوب",
161
+ "قی",
162
+ "▁شده",
163
+ "کر",
164
+ "▁حال",
165
+ "▁ات",
166
+ "▁وق",
167
+ "▁مت",
168
+ "له",
169
+ "▁بز",
170
+ "▁نه",
171
+ "دا",
172
+ "▁ده",
173
+ "▁بب",
174
+ "▁بد",
175
+ "▁پر",
176
+ "▁هیچ",
177
+ "ونه",
178
+ "بت",
179
+ "نده",
180
+ "▁گفت",
181
+ "شک",
182
+ "▁تر",
183
+ "▁غ",
184
+ "▁پس",
185
+ "▁مح",
186
+ "▁همه",
187
+ "شم",
188
+ "▁ال",
189
+ "وه",
190
+ "عی",
191
+ "▁هر",
192
+ "▁رفت",
193
+ "▁بع",
194
+ "▁سا",
195
+ "▁کم",
196
+ "▁مع",
197
+ "فر",
198
+ "▁گر",
199
+ "▁چند",
200
+ "▁ای",
201
+ "▁کجا",
202
+ "است",
203
+ "▁میک",
204
+ "▁بخ",
205
+ "▁اس",
206
+ "▁مرد",
207
+ "▁یا",
208
+ "دم",
209
+ "توان",
210
+ "▁روز",
211
+ "به",
212
+ "شن",
213
+ "▁ها",
214
+ "عت",
215
+ "▁فر",
216
+ "وری",
217
+ "کت",
218
+ "که",
219
+ "▁فکر",
220
+ "▁کش",
221
+ "چه",
222
+ "نج",
223
+ "▁نش",
224
+ "▁باز",
225
+ "▁مر",
226
+ "جود",
227
+ "کی",
228
+ "وز",
229
+ "▁سال",
230
+ "ارم",
231
+ "لم",
232
+ "یده",
233
+ "▁بده",
234
+ "▁راه",
235
+ "اند",
236
+ "بر",
237
+ "امه",
238
+ "الی",
239
+ "تون",
240
+ "هم",
241
+ "ارد",
242
+ "▁آم",
243
+ "▁چرا",
244
+ "▁مد",
245
+ "طور",
246
+ "▁های",
247
+ "▁میش",
248
+ "▁کند",
249
+ "▁تم",
250
+ "کار",
251
+ "اط",
252
+ "▁زند",
253
+ "▁میگ",
254
+ "▁اما",
255
+ "قه",
256
+ "▁روی",
257
+ "▁زی",
258
+ "▁مس",
259
+ "لو",
260
+ "زه",
261
+ "▁اح",
262
+ "▁رس",
263
+ "▁واق",
264
+ "▁قر",
265
+ "▁کسی",
266
+ "هر",
267
+ "فی",
268
+ "▁کنی",
269
+ "ادر",
270
+ "یاز",
271
+ "▁کی",
272
+ "▁پیش",
273
+ "ونی",
274
+ "▁حرف",
275
+ "▁خوش",
276
+ "▁نب",
277
+ "▁شر",
278
+ "قدر",
279
+ "جه",
280
+ "▁مید",
281
+ "عه",
282
+ "سته",
283
+ "شین",
284
+ "ونم",
285
+ "▁نم",
286
+ "ادی",
287
+ "وت",
288
+ "▁حس",
289
+ "عا",
290
+ "▁مم",
291
+ "ظر",
292
+ "▁نو",
293
+ "سه",
294
+ "▁بعد",
295
+ "فته",
296
+ "انید",
297
+ "▁بین",
298
+ "▁ار",
299
+ "▁سو",
300
+ "▁سی",
301
+ "یدم",
302
+ "▁عم",
303
+ "باره",
304
+ "اهد",
305
+ "▁بار",
306
+ "▁شن",
307
+ "اشته",
308
+ "یرم",
309
+ "▁تع",
310
+ "مین",
311
+ "▁تح",
312
+ "اهی",
313
+ "▁دن",
314
+ "▁پنج",
315
+ "بال",
316
+ "▁زن",
317
+ "▁دور",
318
+ "▁ولی",
319
+ "شتر",
320
+ "شون",
321
+ "رگ",
322
+ "زار",
323
+ "▁تن",
324
+ "▁یکی",
325
+ "صی",
326
+ "با",
327
+ "▁صد",
328
+ "▁شود",
329
+ "▁مث",
330
+ "قط",
331
+ "▁آب",
332
+ "وا",
333
+ "▁گذ",
334
+ "▁چیز",
335
+ "▁نز",
336
+ "▁اق",
337
+ "▁بسی",
338
+ "▁ری",
339
+ "▁وقت",
340
+ "سم",
341
+ "ترین",
342
+ "▁اد",
343
+ "زم",
344
+ "یدن",
345
+ "اف",
346
+ "▁اص",
347
+ "▁پای",
348
+ "▁قب",
349
+ "▁خر",
350
+ "▁دان",
351
+ "▁شک",
352
+ "رده",
353
+ "یری",
354
+ "ردم",
355
+ "▁مق",
356
+ "▁فقط",
357
+ "▁اند",
358
+ "▁نی",
359
+ "نامه",
360
+ "جام",
361
+ "دید",
362
+ "وب",
363
+ "کنم",
364
+ "▁کت",
365
+ "نه",
366
+ "▁میز",
367
+ "اک",
368
+ "▁اگه",
369
+ "▁بن",
370
+ "▁فی",
371
+ "▁صح",
372
+ "مد",
373
+ "▁مرا",
374
+ "▁بچه",
375
+ "شان",
376
+ "شی",
377
+ "اح",
378
+ "▁یاد",
379
+ "وس",
380
+ "▁جه",
381
+ "▁کمی",
382
+ "▁اول",
383
+ "وند",
384
+ "▁سه",
385
+ "▁نظر",
386
+ "نم",
387
+ "وق",
388
+ "اخت",
389
+ "▁بل",
390
+ "کان",
391
+ "▁خدا",
392
+ "ایه",
393
+ "هایی",
394
+ "تم",
395
+ "جی",
396
+ "▁بیا",
397
+ "▁لب",
398
+ "روز",
399
+ "▁اگر",
400
+ "▁برد",
401
+ "▁نق",
402
+ "▁هن",
403
+ "▁بیم",
404
+ "ستان",
405
+ "▁جم",
406
+ "▁زد",
407
+ "▁پول",
408
+ "▁موق",
409
+ "کس",
410
+ "خی",
411
+ "▁شم",
412
+ "▁پید",
413
+ "ضی",
414
+ "▁ساز",
415
+ "ینم",
416
+ "بان",
417
+ "▁تص",
418
+ "▁شب",
419
+ "▁افت",
420
+ "ویی",
421
+ "▁دل",
422
+ "▁جو",
423
+ "لام",
424
+ "▁تل",
425
+ "▁بیر",
426
+ "▁انت",
427
+ "ختر",
428
+ "هی",
429
+ "▁ایر",
430
+ "▁اه",
431
+ "▁تق",
432
+ "▁برو",
433
+ "▁خب",
434
+ "ایش",
435
+ "▁پرو",
436
+ "▁کو",
437
+ "▁اط",
438
+ "ورد",
439
+ "▁اخ",
440
+ "بار",
441
+ "▁کمک",
442
+ "▁برگ",
443
+ "اخ",
444
+ "▁انگ",
445
+ "▁کام",
446
+ "ینی",
447
+ "شید",
448
+ "▁مج",
449
+ "حال",
450
+ "ریک",
451
+ "▁شهر",
452
+ "▁فرو",
453
+ "▁سف",
454
+ "ذار",
455
+ "▁مثل",
456
+ "▁نام",
457
+ "▁کنه",
458
+ "▁مش",
459
+ "فاده",
460
+ "▁توی",
461
+ "ینه",
462
+ "غی",
463
+ "▁شدن",
464
+ "▁چون",
465
+ "ایت",
466
+ "▁پسر",
467
+ "▁میخ",
468
+ "مت",
469
+ "اشتم",
470
+ "یشه",
471
+ "دیک",
472
+ "▁گوش",
473
+ "▁اف",
474
+ "▁قط",
475
+ "▁خان",
476
+ "اسه",
477
+ "ارت",
478
+ "اطر",
479
+ "یره",
480
+ "ستم",
481
+ "▁لا",
482
+ "▁ض",
483
+ "▁تغی",
484
+ "ورت",
485
+ "▁جن",
486
+ "کنه",
487
+ "▁دقی",
488
+ "▁نک",
489
+ "▁گل",
490
+ "دام",
491
+ "▁احس",
492
+ "▁مشک",
493
+ "اهید",
494
+ "یدی",
495
+ "▁شو",
496
+ "تو",
497
+ "▁بله",
498
+ "کنی",
499
+ "اپ",
500
+ "جر",
501
+ "ندی",
502
+ "لات",
503
+ "مون",
504
+ "▁نس",
505
+ "▁مه",
506
+ "▁کل",
507
+ "لاق",
508
+ "▁میر",
509
+ "▁جای",
510
+ "▁شرو",
511
+ "▁تب",
512
+ "▁مهم",
513
+ "تان",
514
+ "انند",
515
+ "اسی",
516
+ "ازه",
517
+ "▁مخ",
518
+ "▁زیر",
519
+ "▁وی",
520
+ "▁آر",
521
+ "وهر",
522
+ "▁زود",
523
+ "▁تما",
524
+ "▁رنگ",
525
+ "▁صب",
526
+ "گان",
527
+ "▁اب",
528
+ "ارش",
529
+ "▁غذ",
530
+ "▁خط",
531
+ "عیت",
532
+ "▁بش",
533
+ "▁کوچ",
534
+ "شته",
535
+ "▁شه",
536
+ "ابر",
537
+ "▁طول",
538
+ "▁پار",
539
+ "▁بهش",
540
+ "▁درد",
541
+ "این",
542
+ "▁مط",
543
+ "▁همچ",
544
+ "خته",
545
+ "▁اشت",
546
+ "اسف",
547
+ "▁میب",
548
+ "▁صبح",
549
+ "▁ذ",
550
+ "مو",
551
+ "ورم",
552
+ "▁نگ",
553
+ "▁سرم",
554
+ "▁پشت",
555
+ "▁لی",
556
+ "▁ور",
557
+ "شگاه",
558
+ "گری",
559
+ "اص",
560
+ "▁چشم",
561
+ "فه",
562
+ "▁پرس",
563
+ "▁بپ",
564
+ "▁برم",
565
+ "وع",
566
+ "ظه",
567
+ "کنند",
568
+ "▁اعت",
569
+ "ابل",
570
+ "▁اع",
571
+ "یستم",
572
+ "▁شیر",
573
+ "▁شدم",
574
+ "▁خار",
575
+ "▁امت",
576
+ "▁فرد",
577
+ "▁خون",
578
+ "مل",
579
+ "▁آخ",
580
+ "فتم",
581
+ "▁سب",
582
+ "امی",
583
+ "شب",
584
+ "لت",
585
+ "طی",
586
+ "▁حق",
587
+ "ندان",
588
+ "▁منو",
589
+ "▁جل",
590
+ "سر",
591
+ "یات",
592
+ "بانی",
593
+ "▁جا",
594
+ "شو",
595
+ "دار",
596
+ "▁مدر",
597
+ "▁پرد",
598
+ "▁بلی",
599
+ "نین",
600
+ "هاش",
601
+ "▁اش",
602
+ "ابی",
603
+ "▁جان",
604
+ "▁اج",
605
+ "▁کا",
606
+ "▁خبر",
607
+ "▁قد",
608
+ "▁هفت",
609
+ "▁تیم",
610
+ "▁توس",
611
+ "▁مست",
612
+ "▁ماه",
613
+ "لیس",
614
+ "▁شام",
615
+ "ایید",
616
+ "گران",
617
+ "▁گو",
618
+ "▁چیه",
619
+ "▁ث",
620
+ "▁کس",
621
+ "▁کر",
622
+ "▁گم",
623
+ "▁آدم",
624
+ "▁آق",
625
+ "اسفم",
626
+ "▁بهم",
627
+ "▁منم",
628
+ "▁منت",
629
+ "اشق",
630
+ "کنیم",
631
+ "▁بشه",
632
+ "▁عمل",
633
+ "▁مال",
634
+ "سان",
635
+ "▁بگو",
636
+ "▁ارز",
637
+ "▁آره",
638
+ "تب",
639
+ "یس",
640
+ "مه",
641
+ "ذر",
642
+ "ستی",
643
+ "ایم",
644
+ "▁اجا",
645
+ "شار",
646
+ "یمی",
647
+ "▁حر",
648
+ "▁مص",
649
+ "ودی",
650
+ "▁مام",
651
+ "فتن",
652
+ "دهد",
653
+ "رسی",
654
+ "▁نفر",
655
+ "فاق",
656
+ "▁خد",
657
+ "▁هوا",
658
+ "▁طرف",
659
+ "▁نگه",
660
+ "تا",
661
+ "▁پدر",
662
+ "▁نف",
663
+ "▁ببخ",
664
+ "▁افر",
665
+ "گار",
666
+ "ژی",
667
+ "▁نکن",
668
+ "▁بزن",
669
+ "▁گرد",
670
+ "▁سنگ",
671
+ "▁بهت",
672
+ "خص",
673
+ "غل",
674
+ "▁جنگ",
675
+ "لوم",
676
+ "نا",
677
+ "▁عرو",
678
+ "▁پیر",
679
+ "کرد",
680
+ "▁قه",
681
+ "▁صند",
682
+ "▁هشت",
683
+ "ایل",
684
+ "▁عو",
685
+ "▁بک",
686
+ "▁اسم",
687
+ "▁کود",
688
+ "شد",
689
+ "کتر",
690
+ "زشک",
691
+ "▁بگم",
692
+ "نوان",
693
+ "لاع",
694
+ "▁علا",
695
+ "اهند",
696
+ "▁ترس",
697
+ "▁غیر",
698
+ "بون",
699
+ "ینده",
700
+ "صت",
701
+ "پی",
702
+ "▁ظ",
703
+ "ضا",
704
+ "حت",
705
+ "▁لح",
706
+ "نش",
707
+ "▁هز",
708
+ "▁جام",
709
+ "▁آز",
710
+ "▁حتی",
711
+ "▁شی",
712
+ "ویس",
713
+ "▁سپ",
714
+ "▁حم",
715
+ "▁حا",
716
+ "نت",
717
+ "عتی",
718
+ "▁ممن",
719
+ "میم",
720
+ "▁زنگ",
721
+ "▁تلف",
722
+ "▁نظ",
723
+ "داشت",
724
+ "▁موف",
725
+ "▁مدت",
726
+ "▁وا",
727
+ "▁دهم",
728
+ "زد",
729
+ "▁آه",
730
+ "▁قبل",
731
+ "▁گی",
732
+ "▁پا",
733
+ "▁بخو",
734
+ "▁کلی",
735
+ "▁بال",
736
+ "رس",
737
+ "▁خیا",
738
+ "قب",
739
+ "▁دیر",
740
+ "▁رود",
741
+ "▁نار",
742
+ "▁سخ",
743
+ "▁بخش",
744
+ "رین",
745
+ "یمت",
746
+ "خانه",
747
+ "▁منظ",
748
+ "ثر",
749
+ "تما",
750
+ "حان",
751
+ "قت",
752
+ "جان",
753
+ "شنه",
754
+ "▁حد",
755
+ "▁عکس",
756
+ "▁بدی",
757
+ "میر",
758
+ "▁قدر",
759
+ "ابه",
760
+ "▁ژ",
761
+ "▁جلو",
762
+ "▁مگه",
763
+ "یرید",
764
+ "بیه",
765
+ "▁سری",
766
+ "▁هو",
767
+ "▁سفر",
768
+ "مانی",
769
+ "ندم",
770
+ "صد",
771
+ "▁آش",
772
+ "▁غذا",
773
+ "▁جمع",
774
+ "▁درو",
775
+ "رات",
776
+ "▁بس",
777
+ "▁بید",
778
+ "▁کاش",
779
+ "▁ست",
780
+ "▁جور",
781
+ "▁شلو",
782
+ "▁آس",
783
+ "کسی",
784
+ "▁بح",
785
+ "استم",
786
+ "▁دلم",
787
+ "▁سخت",
788
+ "تونم",
789
+ "رخ",
790
+ "▁باب",
791
+ "میز",
792
+ "ثیر",
793
+ "▁میآ",
794
+ "عنی",
795
+ "باه",
796
+ "▁پاس",
797
+ "اله",
798
+ "▁موض",
799
+ "▁نا",
800
+ "لیل",
801
+ "اشتن",
802
+ "زارش",
803
+ "▁سمت",
804
+ "▁کف",
805
+ "▁عزی",
806
+ "▁تش",
807
+ "▁بست",
808
+ "▁صدا",
809
+ "▁تخ",
810
+ "▁نخ",
811
+ "▁ایج",
812
+ "▁زدن",
813
+ "یمان",
814
+ "▁اوه",
815
+ "▁فهم",
816
+ "▁اوم",
817
+ "▁دم",
818
+ "▁مسا",
819
+ "ارو",
820
+ "▁جون",
821
+ "وار",
822
+ "اهیم",
823
+ "داد",
824
+ "▁گرو",
825
+ "جوری",
826
+ "▁مسئ",
827
+ "ردی",
828
+ "▁عص",
829
+ "▁رق",
830
+ "▁عوض",
831
+ "حی",
832
+ "▁پوش",
833
+ "▁حل",
834
+ "▁کشت",
835
+ "اهر",
836
+ "▁رز",
837
+ "داری",
838
+ "شتری",
839
+ "صو",
840
+ "▁درس",
841
+ "▁زیب",
842
+ "ریبا",
843
+ "▁روش",
844
+ "وده",
845
+ "اتی",
846
+ "▁تان",
847
+ "کا",
848
+ "طه",
849
+ "خاب",
850
+ "▁پو",
851
+ "واده",
852
+ "واج",
853
+ "دن",
854
+ "▁دید",
855
+ "▁اقا",
856
+ "▁مرگ",
857
+ "▁وس",
858
+ "▁حو",
859
+ "سال",
860
+ "وشید",
861
+ "▁شش",
862
+ "نو",
863
+ "▁محی",
864
+ "راک",
865
+ "▁اسب",
866
+ "▁قسم",
867
+ "کز",
868
+ "▁تجر",
869
+ "▁بچ",
870
+ "▁بفر",
871
+ "تش",
872
+ "گونه",
873
+ "▁اید",
874
+ "نگی",
875
+ "▁دا",
876
+ "خره",
877
+ "▁دع",
878
+ "▁ازد",
879
+ "▁کوت",
880
+ "انت",
881
+ "▁حاض",
882
+ "چی",
883
+ "ادرش",
884
+ "▁سرو",
885
+ "▁آل",
886
+ "▁قل",
887
+ "▁وای",
888
+ "کاری",
889
+ "▁تول",
890
+ "▁نوع",
891
+ "یمه",
892
+ "بور",
893
+ "▁بف",
894
+ "ریکا",
895
+ "▁درآ",
896
+ "▁شور",
897
+ "ادش",
898
+ "اهای",
899
+ "▁سع",
900
+ "اموش",
901
+ "سا",
902
+ "▁چش",
903
+ "وبوس",
904
+ "گیری",
905
+ "▁مل",
906
+ "▁چک",
907
+ "شکرم",
908
+ "▁ضر",
909
+ "▁بان",
910
+ "▁بند",
911
+ "ذیر",
912
+ "خورد",
913
+ "▁شوم",
914
+ "▁پست",
915
+ "ژه",
916
+ "▁فرا",
917
+ "کو",
918
+ "ابت",
919
+ "گز",
920
+ "▁اقت",
921
+ "▁موش",
922
+ "جب",
923
+ "▁تاب",
924
+ "یه",
925
+ "ضای",
926
+ "ایهگ",
927
+ "▁بره",
928
+ "سل",
929
+ "اریخ",
930
+ "▁نیز",
931
+ "▁کلا",
932
+ "انیم",
933
+ "رمان",
934
+ "من",
935
+ "یند",
936
+ "یما",
937
+ "ردند",
938
+ "▁داخ",
939
+ "▁ببر",
940
+ "ئه",
941
+ "خصی",
942
+ "قا",
943
+ "▁محل",
944
+ "▁بقی",
945
+ "کش",
946
+ "▁سعی",
947
+ "▁پاک",
948
+ "تل",
949
+ "▁سرد",
950
+ "▁عر",
951
+ "▁هد",
952
+ "بته",
953
+ "▁راب",
954
+ "▁متو",
955
+ "▁تک",
956
+ "▁بیش",
957
+ "▁ره",
958
+ "عث",
959
+ "مئ",
960
+ "تمال",
961
+ "تونی",
962
+ "▁گشت",
963
+ "ادت",
964
+ "▁دهد",
965
+ "▁صبر",
966
+ "ران",
967
+ "▁جز",
968
+ "انس",
969
+ "▁تج",
970
+ "▁برق",
971
+ "جو",
972
+ "اریم",
973
+ "▁پن",
974
+ "▁قول",
975
+ "صل",
976
+ "▁برا",
977
+ "رمز",
978
+ "گرد",
979
+ "احت",
980
+ "▁وض",
981
+ "میت",
982
+ "▁رد",
983
+ "کنون",
984
+ "▁خیر",
985
+ "▁وال",
986
+ "▁چپ",
987
+ "▁مغ",
988
+ "ریف",
989
+ "بیل",
990
+ "قل",
991
+ "انهی",
992
+ "قد",
993
+ "▁",
994
+ "ا",
995
+ "ی",
996
+ "ر",
997
+ "م",
998
+ "ن",
999
+ "د",
1000
+ "ه",
1001
+ "و",
1002
+ "ت",
1003
+ "ب",
1004
+ "ک",
1005
+ "س",
1006
+ "ش",
1007
+ "ل",
1008
+ "ز",
1009
+ "خ",
1010
+ "ف",
1011
+ "گ",
1012
+ "ق",
1013
+ "ج",
1014
+ "ع",
1015
+ "چ",
1016
+ "پ",
1017
+ "ح",
1018
+ "آ",
1019
+ "ط",
1020
+ "ص",
1021
+ "غ",
1022
+ "ذ",
1023
+ "ض",
1024
+ "ظ",
1025
+ "ث",
1026
+ "ئ",
1027
+ "ژ",
1028
+ "ؤ",
1029
+ "<blank>"
1030
+ ]
1031
+ }