@missionsquad/browserai 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +5 -0
  2. package/dist/client.d.ts +11 -5
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +33 -12
  5. package/dist/client.js.map +1 -1
  6. package/dist/config.d.ts +10 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +1 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/hardware.d.ts +10 -0
  11. package/dist/hardware.d.ts.map +1 -1
  12. package/dist/hardware.js +15 -0
  13. package/dist/hardware.js.map +1 -1
  14. package/dist/history.d.ts +1 -1
  15. package/dist/history.d.ts.map +1 -1
  16. package/dist/history.js +1 -1
  17. package/dist/history.js.map +1 -1
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/inference.d.ts.map +1 -1
  23. package/dist/inference.js +8 -6
  24. package/dist/inference.js.map +1 -1
  25. package/dist/metrics.js +2 -2
  26. package/dist/metrics.js.map +1 -1
  27. package/dist/models.d.ts +12 -2
  28. package/dist/models.d.ts.map +1 -1
  29. package/dist/models.js +142 -0
  30. package/dist/models.js.map +1 -1
  31. package/dist/multimodal.d.ts +21 -1
  32. package/dist/multimodal.d.ts.map +1 -1
  33. package/dist/multimodal.js +69 -11
  34. package/dist/multimodal.js.map +1 -1
  35. package/dist/tdt/frontend.d.ts +7 -0
  36. package/dist/tdt/frontend.d.ts.map +1 -0
  37. package/dist/tdt/frontend.js +73 -0
  38. package/dist/tdt/frontend.js.map +1 -0
  39. package/dist/tdt/index.d.ts +37 -0
  40. package/dist/tdt/index.d.ts.map +1 -0
  41. package/dist/tdt/index.js +318 -0
  42. package/dist/tdt/index.js.map +1 -0
  43. package/dist/tdt/nemo-frontend.d.ts +6 -0
  44. package/dist/tdt/nemo-frontend.d.ts.map +1 -0
  45. package/dist/tdt/nemo-frontend.js +53 -0
  46. package/dist/tdt/nemo-frontend.js.map +1 -0
  47. package/package.json +6 -3
  48. package/src/client.ts +35 -10
  49. package/src/config.ts +7 -0
  50. package/src/hardware.ts +15 -0
  51. package/src/history.ts +2 -2
  52. package/src/index.ts +3 -1
  53. package/src/inference.ts +12 -6
  54. package/src/metrics.ts +2 -2
  55. package/src/models.ts +152 -1
  56. package/src/multimodal.ts +64 -10
  57. package/src/tdt/frontend.ts +61 -0
  58. package/src/tdt/index.ts +230 -0
  59. package/src/tdt/nemo-frontend.ts +43 -0
  60. package/src/tdt/runtime-types.d.ts +5 -0
  61. package/worker-template/cloudflare-worker.ts +1 -1
package/src/multimodal.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TdtModel } from "./tdt/index.js";
1
2
  import { type ModelPreset, type MultimodalRuntime, SUPERTONIC_2_MODEL_ID } from "./models.js";
2
3
  import { AUDIO_SR } from "./audio/pcm.js";
3
4
 
@@ -73,6 +74,7 @@ export type ProgressCallback = (info: { status?: string; progress?: number; file
73
74
  export type GenStats = { completionTokens: number; firstTokenMs?: number; startedMs: number; endedMs: number };
74
75
  /** VLM / Gemma generation result: text plus telemetry. */
75
76
  export type VlmResult = { text: string } & GenStats;
77
+ export type ImageClassificationResult = { scores: Array<{ label: string; score: number }>; startedMs: number; endedMs: number };
76
78
  export type SttResult = { text: string; segments: Array<{ start: number; end: number; text: string }>; gen?: GenStats };
77
79
 
78
80
  /** Count tokens from a TextStreamer token_callback payload (array / typed array / tensor). */
@@ -93,10 +95,11 @@ function makeCountingStreamer(
93
95
  tokenizer: unknown,
94
96
  onDelta: ((text: string) => void) | undefined,
95
97
  sink: { text: string; completionTokens: number; firstTokenMs?: number },
98
+ preserveSpecialTokens = false,
96
99
  ): unknown {
97
100
  return new Streamer(tokenizer, {
98
101
  skip_prompt: true,
99
- skip_special_tokens: true,
102
+ skip_special_tokens: !preserveSpecialTokens,
100
103
  callback_function: (chunk: unknown) => {
101
104
  if (typeof chunk === "string") {
102
105
  if (sink.firstTokenMs === undefined) sink.firstTokenMs = performance.now();
@@ -112,6 +115,8 @@ function makeCountingStreamer(
112
115
 
113
116
  /** A loaded multimodal runtime, discriminated by kind. */
114
117
  export type MultimodalHandle =
118
+ | { kind: "tdt"; model: TdtModel }
119
+ | { kind: "image-classification"; classifier: ((src: string, opts: AnyRecord) => Promise<unknown>) & { dispose?: () => Promise<void> } }
115
120
  | { kind: "stt"; transcriber: (input: unknown, opts?: AnyRecord) => Promise<any> }
116
121
  | { kind: "tts-kokoro"; tts: KokoroInstance; TextSplitterStream: new () => TtsTextSplitter }
117
122
  | { kind: "tts-pipeline"; synth: (text: string, opts: AnyRecord) => Promise<RawAudio>; modelId: string }
@@ -126,6 +131,7 @@ export type MultimodalHandle =
126
131
  /** SmolVLM passes the image as [image] + a trailing {}; others pass a bare image. */
127
132
  imageArray: boolean;
128
133
  maxNewTokens: number;
134
+ visionOptions?: ModelPreset["visionOptions"];
129
135
  }
130
136
  | {
131
137
  kind: "gemma";
@@ -169,7 +175,10 @@ export type LoadMultimodalOptions = {
169
175
  };
170
176
 
171
177
  function vlmDtype(preset: ModelPreset, runtime: MultimodalRuntime): unknown {
172
- if (runtime === "vlm-vision2seq") return { embed_tokens: "fp16", vision_encoder: "q4", decoder_model_merged: "q4" };
178
+ if (runtime === "vlm-vision2seq") {
179
+ if (preset.visionOptions?.outputFormat === "doctags") return preset.transformersDtype ?? "fp32";
180
+ return { embed_tokens: "fp16", vision_encoder: "q4", decoder_model_merged: "q4" };
181
+ }
173
182
  // imagetext family: GLM-OCR ships a single q4f16 dtype; Qwen3-VL uses a per-module mixed map.
174
183
  if (preset.transformersDtype) return preset.transformersDtype;
175
184
  return { embed_tokens: "fp16", vision_encoder: "fp16", decoder_model_merged: "q4f16" };
@@ -204,6 +213,14 @@ export async function loadMultimodal(
204
213
  if (!runtime) throw new Error(`Model ${preset.shortName} has no multimodal runtime.`);
205
214
 
206
215
  switch (runtime) {
216
+ case "stt-tdt": {
217
+ const { loadTdtModel } = await import("./tdt/index.js");
218
+ return { kind: "tdt", model: await loadTdtModel(preset.id, { device, onProgress: progress_callback }) };
219
+ }
220
+ case "image-classification": {
221
+ const classifier = await transformers.pipeline("image-classification", preset.id, { device, dtype: preset.transformersDtype ?? "q4f16", progress_callback });
222
+ return { kind: "image-classification", classifier };
223
+ }
207
224
  case "stt": {
208
225
  // Whisper/Moonshine load an fp32 encoder + a quantized decoder (encoder quantization hurts
209
226
  // accuracy). Some ASR models document a single flat dtype instead — e.g. Cohere's cohere_asr,
@@ -214,7 +231,16 @@ export async function loadMultimodal(
214
231
  (device === "webgpu"
215
232
  ? { encoder_model: "fp32", decoder_model_merged: "q4" }
216
233
  : { encoder_model: "fp32", decoder_model_merged: "q8" });
217
- const transcriber = await transformers.pipeline("automatic-speech-recognition", preset.id, { device, dtype, progress_callback });
234
+ const transcriber = await transformers.pipeline("automatic-speech-recognition", preset.id, {
235
+ device,
236
+ dtype,
237
+ progress_callback,
238
+ // onnxruntime-web 1.26-dev's extended graph optimizations fail at session creation for the
239
+ // quantized ASR decoders on the wasm EP (qdq_actions.cc: "Missing required scale …
240
+ // TransposeDQWeightsForMatMulNBits"). Capping at "basic" skips the broken QDQ rewrites —
241
+ // verified to load and transcribe. The WebGPU path keeps the default level.
242
+ ...(device === "wasm" ? { session_options: { graphOptimizationLevel: "basic" as const } } : {}),
243
+ });
218
244
  return { kind: "stt", transcriber };
219
245
  }
220
246
  case "tts-pipeline": {
@@ -235,7 +261,14 @@ export async function loadMultimodal(
235
261
  case "vlm-imagetext": {
236
262
  const processor = await transformers.AutoProcessor.from_pretrained(preset.id, { progress_callback });
237
263
  const ModelClass = runtime === "vlm-vision2seq" ? transformers.AutoModelForVision2Seq : transformers.AutoModelForImageTextToText;
238
- const model = await ModelClass.from_pretrained(preset.id, { device: "webgpu", dtype: vlmDtype(preset, runtime), progress_callback });
264
+ let model;
265
+ try {
266
+ model = await ModelClass.from_pretrained(preset.id, { device: "webgpu", dtype: vlmDtype(preset, runtime), progress_callback });
267
+ } catch (error) {
268
+ // Release any processor-owned resources if the subsequent model load fails.
269
+ try { await processor.dispose?.(); } catch { /* preserve the loading error */ }
270
+ throw error;
271
+ }
239
272
  return {
240
273
  kind: "vlm",
241
274
  processor,
@@ -245,7 +278,8 @@ export async function loadMultimodal(
245
278
  maxPixels: runtime === "vlm-imagetext" ? MAX_PIXELS : 0,
246
279
  imageArray: runtime === "vlm-vision2seq",
247
280
  // Generous caps so descriptions/OCR are not truncated mid-output (any OCR model → 2048).
248
- maxNewTokens: preset.id.toLowerCase().includes("ocr") ? 2048 : 1024,
281
+ maxNewTokens: preset.visionOptions?.maxNewTokens ?? (preset.id.toLowerCase().includes("ocr") ? 2048 : 1024),
282
+ visionOptions: preset.visionOptions,
249
283
  };
250
284
  }
251
285
  case "gemma-mm": {
@@ -274,6 +308,10 @@ async function loadKokoroModule(options?: LoadMultimodalOptions["kokoro"]): Prom
274
308
 
275
309
  export async function disposeMultimodal(handle: MultimodalHandle | null): Promise<void> {
276
310
  if (!handle) return;
311
+ if (handle.kind === "image-classification") {
312
+ try { await handle.classifier.dispose?.(); } catch { /* best-effort cleanup */ }
313
+ return;
314
+ }
277
315
  const model = (handle as { model?: { dispose?: () => unknown } }).model;
278
316
  try {
279
317
  if (typeof model?.dispose === "function") await model.dispose();
@@ -320,10 +358,12 @@ export function planGemmaAudioWindows(totalSamples: number): AudioWindow[] {
320
358
  * the generative STT recipes (Gemma, Granite Speech, Voxtral) require decoded PCM.
321
359
  * @throws {Error} when a URL is passed to a model that cannot fetch audio itself.
322
360
  */
323
- export async function transcribe(handle: MultimodalHandle, audio: Float32Array | string, onDelta?: (text: string) => void): Promise<SttResult> {
361
+ export async function transcribe(handle: MultimodalHandle, audio: Float32Array | string, onDelta?: (text: string) => void, signal?: AbortSignal): Promise<SttResult> {
324
362
  if (typeof audio === "string" && handle.kind !== "stt") {
325
363
  throw new Error("URL audio input is only supported by ASR-pipeline models (Whisper/Moonshine). Decode to 16 kHz mono PCM first (decodeAudioTo16kMono).");
326
364
  }
365
+ signal?.throwIfAborted();
366
+ if (handle.kind === "tdt") return handle.model.transcribe(audio as Float32Array, onDelta, signal);
327
367
  if (handle.kind === "gemma") {
328
368
  // Callers supply decoded 16 kHz samples; chunk anything past Gemma's ~30s window.
329
369
  return transcribeGemmaChunked(handle, audio as Float32Array, onDelta);
@@ -566,22 +606,36 @@ export function synthesizeStream(
566
606
  return { splitter, chunks: chunks() };
567
607
  }
568
608
 
609
+ /** Classify one image, returning all labels and inference timing. */
610
+ export async function classifyImage(handle: MultimodalHandle, imageSrc: string): Promise<ImageClassificationResult> {
611
+ if (handle.kind !== "image-classification") throw new Error("Loaded model cannot classify images; select an image-classification model.");
612
+ if (!imageSrc.trim()) throw new Error("An image source is required.");
613
+ const startedMs = performance.now();
614
+ const output = await handle.classifier(imageSrc, { top_k: null });
615
+ if (!Array.isArray(output) || !output.every((item: unknown) => typeof item === "object" && item !== null && "label" in item && typeof item.label === "string" && "score" in item && typeof item.score === "number" && Number.isFinite(item.score))) {
616
+ throw new Error("Image classifier returned invalid label scores.");
617
+ }
618
+ return { scores: output.map((item: { label: string; score: number }) => ({ label: item.label, score: item.score })), startedMs, endedMs: performance.now() };
619
+ }
620
+
569
621
  /** Answer a question about an image (VLM). Streams partial text via onDelta; returns text + telemetry. */
570
622
  export async function describeImage(handle: MultimodalHandle, imageSrc: string, prompt: string, onDelta?: (text: string) => void): Promise<VlmResult> {
623
+ if (handle.kind === "image-classification") throw new Error("Loaded model classifies images; use classifyImage() to obtain label scores.");
571
624
  if (handle.kind === "gemma") return runGemma(handle, { imageSrc, prompt }, onDelta);
572
625
  if (handle.kind !== "vlm") throw new Error("Loaded model cannot analyze images.");
573
626
 
574
- const messages = [{ role: "user", content: [{ type: "image" }, { type: "text", text: prompt }] }];
627
+ const messages = [{ role: "user", content: [{ type: "image" }, { type: "text", text: prompt.trim() || handle.visionOptions?.defaultPrompt || prompt }] }];
575
628
  const text = handle.processor.apply_chat_template(messages, { add_generation_prompt: true });
576
629
  const src = handle.maxPixels ? await capSrc(imageSrc, handle.maxPixels) : imageSrc;
577
630
  const image = await handle.loadImage(src);
578
- const inputs = handle.imageArray ? await handle.processor(text, [image], {}) : await handle.processor(text, image);
631
+ const inputs = handle.imageArray ? await handle.processor(text, [image], handle.visionOptions?.doImageSplitting === undefined ? {} : { do_image_splitting: handle.visionOptions.doImageSplitting }) : await handle.processor(text, image);
579
632
 
580
633
  const sink = { text: "", completionTokens: 0, firstTokenMs: undefined as number | undefined };
581
634
  const startedMs = performance.now();
582
- const streamer = makeCountingStreamer(handle.Streamer, handle.processor.tokenizer, onDelta, sink);
635
+ const cleanOutput = (value: string): string => handle.visionOptions?.outputFormat === "doctags" ? value.replace(/<\|end_of_text\|>\s*$/, "").trimEnd() : value;
636
+ const streamer = makeCountingStreamer(handle.Streamer, handle.processor.tokenizer, onDelta ? (value) => onDelta(cleanOutput(value)) : undefined, sink, handle.visionOptions?.preserveSpecialTokens);
583
637
  await handle.model.generate({ ...inputs, max_new_tokens: handle.maxNewTokens, do_sample: false, streamer });
584
- return { text: sink.text.trim(), completionTokens: sink.completionTokens, firstTokenMs: sink.firstTokenMs, startedMs, endedMs: performance.now() };
638
+ return { text: cleanOutput(sink.text).trim(), completionTokens: sink.completionTokens, firstTokenMs: sink.firstTokenMs, startedMs, endedMs: performance.now() };
585
639
  }
586
640
 
587
641
  /** Gemma-4 text + image + audio. image/audio placeholders go before the text; audio is a 16 kHz Float32Array. */
@@ -0,0 +1,61 @@
1
+ /** Phonon reference front end: NeMo log-mel, without dither. */
2
+ export function phononFeatures(audio: Float32Array): { data: Float32Array; frames: number } {
3
+ const frames = Math.floor(audio.length / 160);
4
+ if (frames < 2) throw new Error('TDT audio must contain at least 320 samples (20 ms).');
5
+ const emphasized = new Float32Array(audio.length);
6
+ emphasized[0] = audio[0];
7
+ for (let i = 1; i < audio.length; i++) emphasized[i] = audio[i] - .97 * audio[i - 1];
8
+ const hzToMel = (hz: number): number => hz < 1000 ? hz / (200 / 3) : 15 + Math.log(hz / 1000) / (Math.log(6.4) / 27);
9
+ const melToHz = (mel: number): number => mel < 15 ? mel * (200 / 3) : 1000 * Math.exp((mel - 15) * Math.log(6.4) / 27);
10
+ const edges = Array.from({ length: 130 }, (_, i) => melToHz(hzToMel(8000) * i / 129));
11
+ const filters = Array.from({ length: 128 }, (_, m) => Float64Array.from({ length: 257 }, (_, k) => {
12
+ const hz = k * 16000 / 512;
13
+ return Math.max(0, Math.min((hz - edges[m]) / (edges[m + 1] - edges[m]), (edges[m + 2] - hz) / (edges[m + 2] - edges[m + 1]))) * 2 / (edges[m + 2] - edges[m]);
14
+ }));
15
+ const window = Float64Array.from({ length: 400 }, (_, i) => .5 - .5 * Math.cos(2 * Math.PI * i / 399));
16
+ const data = new Float32Array(frames * 128);
17
+ const real = new Float64Array(512), imag = new Float64Array(512);
18
+ for (let f = 0; f < frames; f++) {
19
+ real.fill(0); imag.fill(0);
20
+ for (let i = 0; i < 400; i++) {
21
+ const sample = f * 160 + i - 200;
22
+ real[i + 56] = (sample >= 0 && sample < audio.length ? emphasized[sample] : 0) * window[i];
23
+ }
24
+ fft(real, imag);
25
+ for (let m = 0; m < 128; m++) {
26
+ let power = 0;
27
+ for (let k = 0; k <= 256; k++) power += (real[k] ** 2 + imag[k] ** 2) * filters[m][k];
28
+ data[f * 128 + m] = Math.log(power + 2 ** -24);
29
+ }
30
+ }
31
+ for (let m = 0; m < 128; m++) {
32
+ let mean = 0;
33
+ for (let f = 0; f < frames; f++) mean += data[f * 128 + m];
34
+ mean /= frames;
35
+ let variance = 0;
36
+ for (let f = 0; f < frames; f++) variance += (data[f * 128 + m] - mean) ** 2;
37
+ const std = Math.sqrt(variance / (frames - 1)) + 1e-5;
38
+ for (let f = 0; f < frames; f++) data[f * 128 + m] = (data[f * 128 + m] - mean) / std;
39
+ }
40
+ return { data, frames };
41
+ }
42
+
43
+ export function fft(real: Float64Array, imag: Float64Array): void {
44
+ for (let i = 1, j = 0; i < 512; i++) {
45
+ let bit = 256;
46
+ for (; j & bit; bit >>= 1) j ^= bit;
47
+ j ^= bit;
48
+ if (i < j) { [real[i], real[j]] = [real[j], real[i]]; }
49
+ }
50
+ for (let size = 2; size <= 512; size *= 2) {
51
+ for (let start = 0; start < 512; start += size) {
52
+ for (let j = 0; j < size / 2; j++) {
53
+ const angle = -2 * Math.PI * j / size, c = Math.cos(angle), s = Math.sin(angle);
54
+ const a = start + j, b = a + size / 2;
55
+ const r = c * real[b] - s * imag[b], im = s * real[b] + c * imag[b];
56
+ real[b] = real[a] - r; imag[b] = imag[a] - im;
57
+ real[a] += r; imag[a] += im;
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,230 @@
1
+ /// <reference path="./runtime-types.d.ts" />
2
+ import * as ort from 'onnxruntime-tdt/webgpu';
3
+ import { phononFeatures } from './frontend.js';
4
+ import { nemoFeatures } from './nemo-frontend.js';
5
+
6
+ export interface TdtSegment { start: number; end: number; text: string }
7
+ export interface TdtModel {
8
+ transcribe(audio: Float32Array, onDelta?: (text: string) => void, signal?: AbortSignal): Promise<{ text: string; segments: TdtSegment[] }>;
9
+ dispose(): Promise<void>;
10
+ }
11
+ export interface TdtLoadOptions {
12
+ device: 'webgpu' | 'wasm'; baseUrl?: string; signal?: AbortSignal;
13
+ wasmPaths?: string | { mjs: string; wasm: string };
14
+ onProgress?: (info: { status?: string; progress?: number; file?: string }) => void;
15
+ }
16
+ export function checkAbort(signal?: AbortSignal): void { signal?.throwIfAborted(); }
17
+ /** Let queued input/cancellation events run even when WASM resolves only microtasks. */
18
+ async function yieldToBrowser(signal?: AbortSignal): Promise<void> {
19
+ checkAbort(signal);
20
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
21
+ checkAbort(signal);
22
+ }
23
+ export function parseVocabulary(text: string): string[] {
24
+ const vocab: string[] = [];
25
+ for (const line of text.split(/\r?\n/)) {
26
+ if (!line) continue;
27
+ const match = /^(.*) (\d+)$/.exec(line);
28
+ if (!match) throw new Error('Invalid TDT vocabulary line.');
29
+ const id = Number(match[2]);
30
+ if (vocab[id] !== undefined) throw new Error('Duplicate TDT vocabulary ID.');
31
+ vocab[id] = match[1];
32
+ }
33
+ if (!vocab.length || Array.from({ length: vocab.length }, (_, i) => vocab[i]).some(x => x === undefined)) throw new Error('Incomplete TDT vocabulary.');
34
+ return vocab;
35
+ }
36
+ export function decodePieces(ids: number[], vocab: string[]): string {
37
+ return ids.map(id => vocab[id] ?? '').filter(piece => piece !== '<unk>' && piece !== '<pad>' && piece !== '<blk>' && !/^<\|.*\|>$/.test(piece)).join('').replace(/▁/g, ' ').trim();
38
+ }
39
+ /** Split near the quietest 100 ms interval in the final five seconds, bounded at 30 seconds. */
40
+ export function splitAudio(audio: Float32Array): Array<{ start: number; end: number }> {
41
+ const chunks: Array<{ start: number; end: number }> = [];
42
+ let start = 0;
43
+ while (audio.length - start > 480000) {
44
+ let end = start + 480000, lowest = Infinity;
45
+ for (let p = start + 400000; p <= start + 478400; p += 1600) {
46
+ let energy = 0;
47
+ for (let i = p; i < p + 1600; i++) energy += audio[i] ** 2;
48
+ if (energy < lowest) { lowest = energy; end = p + 800; }
49
+ }
50
+ chunks.push({ start, end }); start = end;
51
+ }
52
+ if (start < audio.length) chunks.push({ start, end: audio.length });
53
+ return chunks;
54
+ }
55
+ async function artifact(url: string, options: TdtLoadOptions): Promise<Uint8Array> {
56
+ checkAbort(options.signal);
57
+ const cacheWarning = (): void => options.onProgress?.({ status: 'cache-warning', file: url });
58
+ let cache: Cache | undefined;
59
+ let response: Response | undefined;
60
+ try { cache = typeof caches !== 'undefined' ? await caches.open('transformers-cache') : undefined; }
61
+ catch { cacheWarning(); }
62
+ try { response = await cache?.match(url); }
63
+ catch { cacheWarning(); }
64
+ const cachePut = async (value: Response): Promise<void> => {
65
+ if (!cache) return;
66
+ try { await cache.put(url, value); }
67
+ catch { cacheWarning(); }
68
+ };
69
+ if (!response) {
70
+ response = await fetch(url, { signal: options.signal });
71
+ if (!response.ok) throw new Error(`TDT download failed (${response.status}): ${url}`);
72
+ // Consume incrementally for meaningful download progress, then cache only a complete artifact.
73
+ const total = Number(response.headers.get('content-length'));
74
+ const reader = response.body?.getReader();
75
+ if (reader) {
76
+ const chunks: Uint8Array[] = []; let received = 0;
77
+ try {
78
+ while (true) {
79
+ checkAbort(options.signal);
80
+ const next = await reader.read(); if (next.done) break;
81
+ chunks.push(next.value); received += next.value.length;
82
+ options.onProgress?.({ status: 'progress', file: url, ...(total > 0 ? { progress: Math.min(100, received / total * 100) } : {}) });
83
+ }
84
+ } finally { reader.releaseLock(); }
85
+ const bytes = new Uint8Array(received); let offset = 0;
86
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
87
+ checkAbort(options.signal);
88
+ await cachePut(new Response(bytes, { headers: response.headers }));
89
+ options.onProgress?.({ status: 'done', file: url, progress: 100 });
90
+ return bytes;
91
+ }
92
+ await cachePut(response.clone());
93
+ }
94
+ if (!response.ok) throw new Error(`Invalid cached TDT artifact (${response.status}): ${url}`);
95
+ const bytes = new Uint8Array(await response.arrayBuffer());
96
+ checkAbort(options.signal);
97
+ options.onProgress?.({ status: 'done', file: url, progress: 100 });
98
+ return bytes;
99
+ }
100
+ function floats(tensor: ort.Tensor | undefined, name: string): Float32Array {
101
+ if (!tensor || tensor.type !== 'float32' || !(tensor.data instanceof Float32Array)) throw new Error(`Invalid TDT ${name} tensor.`);
102
+ return tensor.data;
103
+ }
104
+ function releaseTensors(values: Record<string, ort.Tensor>): void { for (const tensor of new Set(Object.values(values))) tensor.dispose(); }
105
+ function argmax(values: Float32Array, start: number, end: number): number {
106
+ let best = start; for (let i = start + 1; i < end; i++) if (values[i] > values[best]) best = i;
107
+ return best - start;
108
+ }
109
+
110
+ /** Load the two explicitly supported ONNX TDT exports. Sessions are released on any partial load failure. */
111
+ export async function loadTdtModel(modelId: string, options: TdtLoadOptions): Promise<TdtModel> {
112
+ const phonon = modelId === 'enriqueag/phonon-2.5-onnx';
113
+ if (!phonon && modelId !== 'istupakov/parakeet-tdt-0.6b-v3-onnx') throw new Error(`Unsupported TDT model: ${modelId}`);
114
+ if (phonon && options.device !== 'webgpu') throw new Error('Phonon exact2x2 requires WebGPU.');
115
+ const base = options.baseUrl ?? `https://huggingface.co/${modelId}/resolve/main/`;
116
+ const url = (file: string): string => new URL(file, base.endsWith('/') ? base : `${base}/`).href;
117
+ ort.env.wasm.numThreads = 1;
118
+ ort.env.wasm.wasmPaths = options.wasmPaths ?? {
119
+ mjs: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.31.1/dist/ort-wasm-simd-threaded.asyncify.mjs',
120
+ wasm: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.31.1/dist/ort-wasm-simd-threaded.asyncify.wasm',
121
+ };
122
+ const sessions: ort.InferenceSession[] = [];
123
+ const create = async (file: string, device: 'webgpu' | 'wasm', decoder = false): Promise<ort.InferenceSession> => {
124
+ const bytes = await artifact(url(file), options); checkAbort(options.signal);
125
+ const session = await ort.InferenceSession.create(bytes, { executionProviders: [device], ...(decoder && phonon ? { extra: { session: { disable_quant_qdq: '1' } } } : {}) });
126
+ sessions.push(session); checkAbort(options.signal); return session;
127
+ };
128
+ try {
129
+ const vocab = parseVocabulary(new TextDecoder().decode(await artifact(url('vocab.txt'), options)));
130
+ const blank = phonon ? 8192 : vocab.indexOf('<blk>');
131
+ if (blank < 0 || (phonon && vocab.length !== 8192 && vocab.length !== 8193)) throw new Error('Invalid TDT blank/vocabulary configuration.');
132
+ const encoder = await create(phonon ? 'encoder.exact2x2.onnx' : 'encoder-model.int8.onnx', phonon ? 'webgpu' : 'wasm');
133
+ const decoder = await create(phonon ? 'decoder_joint.int8.onnx' : 'decoder_joint-model.int8.onnx', 'wasm', true);
134
+ let disposed = false, busy = false;
135
+ const stateShape = [2, 1, 640];
136
+ return {
137
+ async dispose() {
138
+ if (busy) throw new Error('Cannot dispose TDT model during transcription.');
139
+ if (!disposed) { disposed = true; await Promise.all(sessions.map(s => s.release())); }
140
+ },
141
+ async transcribe(audio, onDelta, signal) {
142
+ if (disposed) throw new Error('TDT model is disposed.');
143
+ if (busy) throw new Error('TDT model is already transcribing.');
144
+ if (!(audio instanceof Float32Array) || audio.some(x => !Number.isFinite(x))) throw new Error('TDT requires finite mono 16 kHz Float32 audio.');
145
+ checkAbort(signal); busy = true;
146
+ const segments: TdtSegment[] = [];
147
+ let runsSinceYield = 0, lastYield = performance.now();
148
+ try {
149
+ for (const chunk of splitAudio(audio)) {
150
+ checkAbort(signal);
151
+ const wave = audio.slice(chunk.start, chunk.end);
152
+ if (wave.length < 320) continue;
153
+ const resources = new Set<ort.Tensor>();
154
+ const own = <T extends ort.Tensor>(t: T): T => { resources.add(t); return t; };
155
+ const run = async (session: ort.InferenceSession, feeds: Record<string, ort.Tensor>): Promise<Record<string, ort.Tensor>> => {
156
+ checkAbort(signal);
157
+ if (++runsSinceYield >= 32 || performance.now() - lastYield >= 20) {
158
+ await yieldToBrowser(signal); runsSinceYield = 0; lastYield = performance.now();
159
+ }
160
+ const outputs = await session.run(feeds);
161
+ for (const tensor of Object.values(outputs)) own(tensor);
162
+ checkAbort(signal); return outputs;
163
+ };
164
+ try {
165
+ let encoded: Record<string, ort.Tensor>;
166
+ if (phonon) {
167
+ await yieldToBrowser(signal);
168
+ const features = phononFeatures(wave);
169
+ await yieldToBrowser(signal);
170
+ runsSinceYield = 0; lastYield = performance.now();
171
+ encoded = await run(encoder, { features: own(new ort.Tensor('float32', features.data, [1, features.frames, 128])) });
172
+ } else {
173
+ // The published nemo128 graph uses float64 STFT, unsupported by ORT Web.
174
+ // This front end reproduces that graph's reflect padding and statistics.
175
+ await yieldToBrowser(signal);
176
+ const features = nemoFeatures(wave);
177
+ await yieldToBrowser(signal);
178
+ runsSinceYield = 0; lastYield = performance.now();
179
+ encoded = await run(encoder, {
180
+ audio_signal: own(new ort.Tensor('float32', features.data, [1, 128, features.frames])),
181
+ length: own(new ort.Tensor('int64', BigInt64Array.of(BigInt(features.frames)), [1])),
182
+ });
183
+ }
184
+ // Large encoder runs are synchronous on WASM. Give input handlers a turn
185
+ // before entering the otherwise microtask-only decoder loop.
186
+ await yieldToBrowser(signal); runsSinceYield = 0; lastYield = performance.now();
187
+ const output = encoded[phonon ? 'encoded' : 'outputs'];
188
+ const data = floats(output, 'encoder');
189
+ const width = output.dims[phonon ? 2 : 1], paddedSteps = output.dims[phonon ? 1 : 2];
190
+ const lengths = encoded.encoded_lengths;
191
+ const steps = phonon ? paddedSteps : Number(lengths?.data[0]);
192
+ if (!Number.isInteger(steps) || steps < 0 || steps > paddedSteps) throw new Error('Invalid TDT encoder length.');
193
+ let h: ort.Tensor = own(new ort.Tensor('float32', new Float32Array(stateShape.reduce((a, b) => a * b, 1)), stateShape));
194
+ let c: ort.Tensor = own(new ort.Tensor('float32', new Float32Array(stateShape.reduce((a, b) => a * b, 1)), stateShape));
195
+ let token = blank, t = 0, emitted = 0;
196
+ const ids: number[] = [];
197
+ while (t < steps) {
198
+ const frame = new Float32Array(width);
199
+ for (let i = 0; i < width; i++) frame[i] = data[phonon ? t * width + i : i * paddedSteps + t];
200
+ const ephemeral: Record<string, ort.Tensor> = phonon ? {
201
+ encoded: new ort.Tensor('float32', frame, [1, width]), token: new ort.Tensor('int32', Int32Array.of(token), [1]),
202
+ } : {
203
+ encoder_outputs: new ort.Tensor('float32', frame, [1, width, 1]), targets: new ort.Tensor('int32', Int32Array.of(token), [1, 1]), target_length: new ort.Tensor('int32', Int32Array.of(1), [1]),
204
+ };
205
+ let result: Record<string, ort.Tensor>;
206
+ try { result = await run(decoder, { ...ephemeral, ...(phonon ? { h, c } : { input_states_1: h, input_states_2: c }) }); }
207
+ finally { releaseTensors(ephemeral); }
208
+ const logits = floats(result[phonon ? 'logits' : 'outputs'], 'decoder');
209
+ if (logits.length !== blank + 1 + 5) throw new Error('Invalid TDT token/duration logits.');
210
+ const next = argmax(logits, 0, blank + 1); let duration = argmax(logits, blank + 1, logits.length);
211
+ if (next !== blank) {
212
+ ids.push(next); token = next; emitted++;
213
+ resources.delete(h); h.dispose(); resources.delete(c); c.dispose();
214
+ h = result[phonon ? 'h_out' : 'output_states_1']; c = result[phonon ? 'c_out' : 'output_states_2'];
215
+ }
216
+ for (const tensor of Object.values(result)) if (tensor !== h && tensor !== c) { resources.delete(tensor); tensor.dispose(); }
217
+ if ((next === blank || emitted >= 10) && duration === 0) duration = 1;
218
+ if (duration > 0) emitted = 0;
219
+ t += duration;
220
+ }
221
+ const text = decodePieces(ids, vocab);
222
+ if (text) { segments.push({ start: chunk.start / 16000, end: chunk.end / 16000, text }); onDelta?.(segments.map(segment => segment.text).join(' ')); }
223
+ } finally { for (const tensor of resources) tensor.dispose(); }
224
+ }
225
+ return { text: segments.map(s => s.text).join(' '), segments };
226
+ } finally { busy = false; }
227
+ },
228
+ };
229
+ } catch (error) { await Promise.allSettled(sessions.map(s => s.release())); throw error; }
230
+ }
@@ -0,0 +1,43 @@
1
+ import { fft } from './frontend.js';
2
+
3
+ // Exact sparse mel matrix from istupakov/parakeet-tdt-0.6b-v3-onnx/nemo128.onnx.
4
+ // Source graph SHA-256: a9fde1486ebfcc08f328d75ad4610c67835fea58c73ba57e3209a6f6cf019e9f.
5
+ // Keeping these small constants avoids rounding differences in the exported filter edges.
6
+ const MEL: number[][] = [[1,0,0.02837754227221012],[1,1,0.014389011077582836],[2,1,0.01398853026330471],[2,2,0.028778022155165672],[3,3,0.04236607998609543],[3,4,0.0004004797956440598],[4,4,0.027977053076028824],[4,5,0.014789499342441559],[5,5,0.013588042929768562],[5,6,0.02917851135134697],[6,7,0.04196559265255928],[6,8,0.0008009598823264241],[7,8,0.027576573193073273],[7,9,0.01518999319523573],[8,9,0.013187538832426071],[8,10,0.029579007998108864],[9,11,0.041565097868442535],[9,12,0.0012014397652819753],[10,12,0.02717609517276287],[10,13,0.015590474009513855],[11,13,0.01278705894947052],[11,14,0.029979487881064415],[12,15,0.041164617985486984],[12,16,0.0016019197646528482],[13,16,0.02677561342716217],[13,17,0.015990953892469406],[14,17,0.012386579066514969],[14,18,0.03037998639047146],[15,19,0.040764085948467255],[15,20,0.002002452965825796],[16,20,0.026375113055109978],[16,21,0.016391444951295853],[17,21,0.011986115016043186],[17,22,0.030780402943491936],[18,23,0.04036368802189827],[18,24,0.0024028795305639505],[19,24,0.025974653661251068],[19,25,0.01679191365838051],[20,25,0.011585619300603867],[20,26,0.03118094801902771],[21,27,0.03996315225958824],[21,28,0.0028034152928739786],[22,28,0.02557411789894104],[22,29,0.017192449420690536],[23,29,0.011185083538293839],[23,30,0.031581442803144455],[24,31,0.03956267610192299],[24,32,0.0032038395293056965],[25,32,0.025173693895339966],[25,33,0.01759287342429161],[26,33,0.010784659534692764],[26,34,0.03198190778493881],[27,35,0.03916219249367714],[27,36,0.0036043752916157246],[28,36,0.024773158133029938],[28,37,0.01799340918660164],[29,37,0.010384122841060162],[29,38,0.03238244354724884],[30,39,0.03876161575317383],[30,40,0.004004895221441984],[31,40,0.024372702464461327],[31,41,0.018320715054869652],[32,41,0.010202586650848389],[32,42,0.03161151707172394],[33,43,0.03855384513735771],[33,44,0.001656117383390665],[34,44,0.028360538184642792],[34,45,0.010670339688658714],[35,45,0.01995559222996235],[35,46,0.01796126179397106],[36,46,0.013171199709177017],[36,47,0.023691896349191666],[37,47,0.007852383889257908],[37,48,0.0280131958425045],[38,48,0.0038550780154764652],[38,49,0.031065288931131363],[39,49,0.001046702265739441],[39,50,0.03297678381204605],[40,51,0.0325256809592247],[40,52,0.0006470278021879494],[41,52,0.03098229691386223],[41,53,0.0013818024890497327],[42,53,0.03026564046740532],[42,54,0.0013279326958581805],[43,54,0.03028314933180809],[44,54,0.000853460980579257],[43,55,0.0005752422730438411],[44,55,0.029303690418601036],[45,55,0.002906062640249729],[45,56,0.026582011952996254],[46,56,0.00546437269076705],[46,57,0.023383773863315582],[47,57,0.00845953170210123],[47,58,0.019775748252868652],[48,58,0.011828471906483173],[48,59,0.015819108113646507],[49,59,0.015513437800109386],[49,60,0.01156991720199585],[50,60,0.01946169137954712],[50,61,0.007079293020069599],[51,61,0.02362503670156002],[52,61,0.0026273371186107397],[51,62,0.002393868751823902],[52,62,0.022891240194439888],[53,62,0.007950658909976482],[53,63,0.017088213935494423],[54,63,0.013358916155993938],[54,64,0.011216753162443638],[55,64,0.01881822757422924],[56,64,0.0006491197855211794],[55,65,0.005309573374688625],[56,65,0.023045793175697327],[57,65,0.006984363775700331],[57,66,0.016297349706292152],[58,66,0.013272317126393318],[58,67,0.009608481079339981],[59,67,0.019490249454975128],[60,67,0.003768591908738017],[59,68,0.0030010610353201628],[60,68,0.018348077312111855],[61,68,0.010636676102876663],[61,69,0.011118832044303417],[62,69,0.01736198551952839],[63,69,0.00308591709472239],[62,70,0.004041498526930809],[63,70,0.017977513372898102],[64,70,0.010326532647013664],[64,71,0.010409270413219929],[65,71,0.017366580665111542],[66,71,0.00440334714949131],[65,72,0.0030487067997455597],[66,72,0.015703044831752777],[67,72,0.011844555847346783],[67,73,0.007962611503899097],[68,73,0.019040562212467194],[69,73,0.007269133348017931],[68,74,0.000472926622023806],[69,74,0.011963930912315845],[70,74,0.01476955134421587],[71,74,0.003552281530573964],[70,75,0.00418809661641717],[71,75,0.01513804029673338],[72,75,0.0113017363473773],[73,75,0.0006128569366410375],[72,76,0.007129882462322712],[73,76,0.01756405457854271],[74,76,0.00856065098196268],[74,77,0.009372946806252003],[75,77,0.01618116907775402],[76,77,0.00647500017657876],[75,78,0.0015116231516003609],[76,78,0.010986563749611378],[77,78,0.014228261075913906],[78,78,0.004979042336344719],[77,79,0.003005674807354808],[78,79,0.012034479528665543],[79,79,0.012825818732380867],[80,79,0.004012242890894413],[79,80,0.003972089849412441],[80,80,0.01257565151900053],[81,80,0.01191700343042612],[82,80,0.0035183706786483526],[81,81,0.00446630734950304],[82,81,0.012664892710745335],[83,81,0.011448914185166359],[84,81,0.0034456099383533],[83,82,0.004539744462817907],[84,82,0.012352367863059044],[85,82,0.011372808367013931],[86,82,0.003746368922293186],[85,83,0.004239588975906372],[86,83,0.011684300377964973],[87,83,0.011644029058516026],[88,83,0.004376695025712252],[87,84,0.0036092570517212152],[88,84,0.010703402571380138],[89,84,0.012221302837133408],[90,84,0.005296255461871624],[89,85,0.0026888747233897448],[90,85,0.009448954835534096],[91,85,0.013066906481981277],[92,85,0.006467771250754595],[91,86,0.0015151641564443707],[92,86,0.007957103662192822],[93,86,0.014145628549158573],[94,86,0.007857220247387886],[95,86,0.0015688107814639807],[93,87,0.0001221956918016076],[94,87,0.006260755006223917],[95,87,0.012399313971400261],[96,87,0.009433379396796227],[97,87,0.003441078355535865],[96,88,0.00439020711928606],[97,88,0.010239733383059502],[98,88,0.01116772647947073],[99,88,0.005457570310682058],[98,89,0.0023731838446110487],[99,89,0.007947309873998165],[100,89,0.013034172356128693],[101,89,0.007592816837131977],[102,89,0.0021514608524739742],[100,90,0.00023496054927818477],[101,90,0.005546679254621267],[102,90,0.010858396999537945],[103,90,0.009823615662753582],[104,90,0.004638473968952894],[103,91,0.0030602705664932728],[104,91,0.008121836930513382],[105,91,0.012128844857215881],[106,91,0.0071879224851727486],[107,91,0.002247000113129616],[105,92,0.0005085079465061426],[106,92,0.005331702530384064],[107,92,0.010154897347092628],[108,92,0.009781261906027794],[109,92,0.0050729489885270596],[110,92,0.0003646367113105953],[108,93,0.002505941316485405],[109,93,0.007102088537067175],[110,93,0.011698235757648945],[111,93,0.007915019057691097],[112,93,0.0034283846616744995],[111,94,0.004040570929646492],[112,94,0.008420334197580814],[113,94,0.010758612304925919],[114,94,0.00648314505815506],[115,94,0.0022076775785535574],[113,95,0.0009844168089330196],[114,95,0.005158022977411747],[115,95,0.009331629611551762],[116,95,0.009516296908259392],[117,95,0.005442172288894653],[118,95,0.0013680467382073402],[116,96,0.0019234465435147285],[117,96,0.005900462623685598],[118,96,0.009877478703856468],[119,96,0.008634302765130997],[120,96,0.004752081818878651],[121,96,0.0008698602323420346],[119,97,0.0025171886663883924],[120,97,0.006306931842118502],[121,97,0.010096673853695393],[122,97,0.00807521678507328],[123,97,0.004375695250928402],[124,97,0.0006761741824448109],[122,98,0.002802216447889805],[123,98,0.006413611583411694],[124,98,0.010025006718933582],[125,98,0.007804089691489935],[126,98,0.004278772044926882],[127,98,0.0007534541073255241],[125,99,0.002812325721606612],[126,99,0.006253630854189396],[127,99,0.009694935753941536],[128,99,0.007789100985974073],[129,99,0.004429795313626528],[130,99,0.0010704898741096258],[128,100,0.0025784021709114313],[129,100,0.005857671145349741],[130,100,0.009136940352618694],[131,100,0.008000868372619152],[132,100,0.004799720831215382],[133,100,0.0015985730569809675],[131,101,0.0021290841978043318],[132,101,0.0052539510652422905],[133,101,0.00837881863117218],[134,101,0.008412200026214123],[135,101,0.005361814517527819],[136,101,0.002311429474502802],[134,102,0.0014907211298123002],[135,102,0.004468441475182772],[136,102,0.007446161471307278],[137,102,0.008998350240290165],[138,102,0.006091524846851826],[139,102,0.00318469968624413],[140,102,0.0002778743510134518],[137,103,0.0006874035461805761],[138,103,0.003524982836097479],[139,103,0.006362562067806721],[140,103,0.009200140833854675],[141,103,0.006966239772737026],[142,103,0.004196294117718935],[143,103,0.0014263488119468093],[141,104,0.002445453079417348],[142,104,0.005149385891854763],[143,104,0.007853318005800247],[144,104,0.007965351454913616],[145,104,0.005325851496309042],[146,104,0.002686351304873824],[147,104,4.6851124352542683e-05],[144,105,0.0012498258147388697],[145,105,0.003826454281806946],[146,105,0.006403082516044378],[147,105,0.00897971075028181],[148,105,0.006554814055562019],[149,105,0.004039536230266094],[150,105,0.001524258521385491],[148,106,0.002411770634353161],[149,106,0.004867123905569315],[150,106,0.007322477176785469],[151,106,0.00786607339978218],[152,106,0.005469250492751598],[153,106,0.0030724273528903723],[154,106,0.0006756045040674508],[151,107,0.0009218427003361285],[152,107,0.003261544741690159],[153,107,0.00560124684125185],[154,107,0.007940948940813541],[155,107,0.006960286758840084],[156,107,0.004676329903304577],[157,107,0.0023923732805997133],[158,107,0.00010841657058335841],[155,108,0.001601085183210671],[156,108,0.003830620553344488],[157,108,0.006060156039893627],[158,108,0.008289691060781479],[159,108,0.006322428584098816],[160,108,0.004146021790802479],[161,108,0.0019696145318448544],[159,109,0.0020237017888575792],[160,109,0.004148253705352545],[161,109,0.006272805389016867],[162,109,0.007998434826731682],[163,109,0.005924496799707413],[164,109,0.0038505580741912127],[165,109,0.0017766198143363],[162,110,0.00019236317893955857],[163,110,0.0022169058211147785],[164,110,0.004241448361426592],[165,110,0.0062659913673996925],[166,110,0.007716970518231392],[167,110,0.005740635562688112],[168,110,0.003764300374314189],[169,110,0.0017879655351862311],[166,111,0.0002765751851256937],[167,111,0.002205819124355912],[168,111,0.0041350629180669785],[169,111,0.006064306478947401],[170,111,0.0076301731169223785],[171,111,0.005746927578002214],[172,111,0.003863682970404625],[173,111,0.001980437897145748],[174,111,9.719309309730306e-05],[170,112,0.0001752191747073084],[171,112,0.002013584366068244],[172,112,0.003851949702948332],[173,112,0.005690314806997776],[174,112,0.0075286803767085075],[175,112,0.005921597592532635],[176,112,0.004127021413296461],[177,112,0.002332444768399],[178,112,0.0005378681817092001],[175,113,0.0016614228952676058],[176,113,0.0034132569562643766],[177,113,0.005165090784430504],[178,113,0.006916924845427275],[179,113,0.006244422402232885],[180,113,0.004534309729933739],[181,113,0.0028241975232958794],[182,113,0.0011140849674120545],[179,114,0.0011690219398587942],[180,114,0.0028383824974298477],[181,114,0.004507742822170258],[182,114,0.006177103146910667],[183,114,0.00669668847694993],[184,114,0.005067131482064724],[185,114,0.0034375751856714487],[186,114,0.001808018540032208],[187,114,0.00017846190894488245],[183,115,0.0005544161540456116],[184,115,0.0021451376378536224],[185,115,0.0037358589470386505],[186,115,0.005326580721884966],[187,115,0.006917302496731281],[188,115,0.005708739627152681],[189,115,0.004155908711254597],[190,115,0.0026030780281871557],[191,115,0.0010502473451197147],[188,116,0.0013498314656317234],[189,116,0.002865670947358012],[190,116,0.004381510429084301],[191,116,0.0058973501436412334],[192,116,0.006443656049668789],[193,116,0.004963928833603859],[194,116,0.0034842020832002163],[195,116,0.00200447509996593],[196,116,0.0005247481749393046],[192,117,0.00046751255285926163],[193,117,0.0019119851058349013],[194,117,0.003356457455083728],[195,117,0.00480093015357852],[196,117,0.006245402619242668],[197,117,0.005847633816301823],[198,117,0.004437577910721302],[199,117,0.003027522237971425],[200,117,0.0016174663323909044],[201,117,0.00020741041225846857],[197,118,0.0008883357513695955],[198,118,0.0022648002486675978],[199,118,0.0036412647459656],[200,118,0.005017729010432959],[201,118,0.006394193507730961],[202,118,0.005450631491839886],[203,118,0.0041069528087973595],[204,118,0.0027632745914161205],[205,118,0.0014195962576195598],[206,118,7.591782923555002e-05],[202,119,0.0011187248164787889],[203,119,0.0024303868412971497],[204,119,0.0037420487497001886],[205,119,0.005053711123764515],[206,119,0.00636537279933691],[207,119,0.0052314260974526405],[208,119,0.003951027989387512],[209,119,0.002670630346983671],[210,119,0.001390232122503221],[211,119,0.00010983415268128738],[207,120,0.0011792670702561736],[208,120,0.002429152838885784],[209,120,0.003679038258269429],[210,120,0.004928923677653074],[211,120,0.00617880979552865],[212,120,0.005170582327991724],[213,120,0.0039504761807620525],[214,120,0.00273036933504045],[215,120,0.001510262954980135],[216,120,0.0002901565458159894],[212,121,0.001088875811547041],[213,121,0.002279920969158411],[214,121,0.003470966126769781],[215,121,0.004662011284381151],[216,121,0.005853056441992521],[217,121,0.005250118672847748],[218,121,0.004087434150278568],[219,121,0.002924750093370676],[220,121,0.001762065920047462],[221,121,0.0005993818631395698],[217,122,0.000865069217979908],[218,122,0.00200005155056715],[219,122,0.0031350336503237486],[220,122,0.004270015750080347],[221,122,0.005404998082667589],[222,122,0.005453326739370823],[223,122,0.004345402121543884],[224,122,0.0032374775037169456],[225,122,0.0021295526530593634],[226,122,0.001021627802401781],[222,123,0.0005239806487224996],[223,123,0.0016055030282586813],[224,123,0.0026870255824178457],[225,123,0.003768547670915723],[226,123,0.004850070457905531],[227,123,0.005765118170529604],[228,123,0.004709359724074602],[229,123,0.0036536010447889566],[230,123,0.002597842598333955],[231,123,0.0015420839190483093],[232,123,0.00048632529797032475],[227,124,8.027452713577077e-05],[228,124,0.0011108804028481245],[229,124,0.002141486154869199],[230,124,0.003172091906890273],[231,124,0.004202697891741991],[232,124,0.005233303643763065],[233,124,0.005165420472621918],[234,124,0.0041593704372644424],[235,124,0.003153321100398898],[236,124,0.00214727153070271],[237,124,0.0011412217281758785],[238,124,0.00013517220213543624],[233,125,0.000529693381395191],[234,125,0.0015117720467969775],[235,125,0.002493850886821747],[236,125,0.0034759296104311943],[237,125,0.004458008334040642],[238,125,0.005440087523311377],[239,125,0.004742166958749294],[240,125,0.0037834877148270607],[241,125,0.002824808470904827],[242,125,0.0018661292269825935],[243,125,0.0009074498666450381],[239,126,0.0008101033745333552],[240,126,0.00174594612326473],[241,126,0.002681788755580783],[242,126,0.0036176315043121576],[243,126,0.004553474020212889],[244,126,0.005390489473938942],[245,126,0.00447693420574069],[246,126,0.003563379170373082],[247,126,0.0026498239021748304],[248,126,0.0017362686339765787],[249,126,0.0008227135404013097],[244,127,4.7655015805503353e-05],[245,127,0.0009394447552040219],[246,127,0.0018312345491722226],[247,127,0.0027230242267251015],[248,127,0.003614814253523946],[249,127,0.004506603814661503],[250,127,0.005223152693361044],[251,127,0.004352618008852005],[252,127,0.0034820837900042534],[253,127,0.0026115491054952145],[254,127,0.0017410146538168192],[255,127,0.0008704800857231021]];
7
+ const WINDOW = Float64Array.from({ length: 400 }, (_, i) => .5 - .5 * Math.cos(2 * Math.PI * i / 399));
8
+
9
+ /** Matches nemo128.onnx: reflected centered STFT, includes its final frame. */
10
+ export function nemoFeatures(audio: Float32Array): { data: Float32Array; frames: number } {
11
+ if (audio.length < 320) throw new Error('TDT audio must contain at least 320 samples (20 ms).');
12
+ const frames = Math.floor(audio.length / 160) + 1;
13
+ const emphasized = new Float32Array(audio.length);
14
+ emphasized[0] = audio[0];
15
+ for (let i = 1; i < audio.length; i++) emphasized[i] = audio[i] - Math.fround(Math.fround(.97) * audio[i - 1]);
16
+ const real = new Float64Array(512), imag = new Float64Array(512);
17
+ const power = new Float32Array(257), data = new Float32Array(128 * frames);
18
+ for (let f = 0; f < frames; f++) {
19
+ real.fill(0); imag.fill(0);
20
+ for (let i = 0; i < 400; i++) {
21
+ let sample = f * 160 + i - 200;
22
+ if (sample < 0) sample = -sample;
23
+ if (sample >= audio.length) sample = 2 * audio.length - 2 - sample;
24
+ real[i + 56] = emphasized[sample] * WINDOW[i];
25
+ }
26
+ fft(real, imag);
27
+ for (let k = 0; k < 257; k++) power[k] = real[k] ** 2 + imag[k] ** 2;
28
+ const bands = new Float64Array(128);
29
+ for (const [k, m, weight] of MEL) bands[m] += power[k] * weight;
30
+ for (let m = 0; m < 128; m++) data[m * frames + f] = Math.log(Math.fround(Math.fround(bands[m]) + 2 ** -24));
31
+ }
32
+ for (let m = 0; m < 128; m++) {
33
+ let mean = 0;
34
+ for (let f = 0; f < frames; f++) mean += data[m * frames + f];
35
+ mean = Math.fround(Math.fround(mean) / frames);
36
+ let variance = 0;
37
+ for (let f = 0; f < frames; f++) variance += Math.fround(data[m * frames + f] - mean) ** 2;
38
+ const std = Math.fround(Math.sqrt(Math.fround(Math.fround(variance) / (frames - 1))));
39
+ const denominator = Math.fround(std + Math.fround(1e-5));
40
+ for (let f = 0; f < frames; f++) data[m * frames + f] = Math.fround(data[m * frames + f] - mean) / denominator;
41
+ }
42
+ return { data, frames };
43
+ }
@@ -0,0 +1,5 @@
1
+ // The npm alias declares only its original package name. These shared public
2
+ // session/tensor types also describe the API used by the isolated 1.31.1 runtime.
3
+ declare module 'onnxruntime-tdt/webgpu' {
4
+ export * from 'onnxruntime-common';
5
+ }
@@ -50,7 +50,7 @@ const ALLOWED_HF_REPOS_BY_OWNER = new Map<string, Set<string>>([
50
50
  // through /hf-transformers/*. Derive those allowlist entries from the SDK's model catalog so the
51
51
  // proxy can never drift out of sync with it — a missing entry 403s hosted model loads.
52
52
  for (const preset of MODEL_PRESETS) {
53
- if (preset.backend !== "transformers-js") continue;
53
+ if (preset.backend === "webllm") continue;
54
54
  const [owner, ...repoParts] = preset.id.split("/");
55
55
  if (!owner || repoParts.length === 0) continue;
56
56
  const repos = ALLOWED_HF_REPOS_BY_OWNER.get(owner) ?? new Set<string>();