@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/client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { MLCEngineInterface } from "@mlc-ai/web-llm";
2
- import type { ModelPreset, ModelTask } from "./models.js";
3
- import { getModelPreset, MODEL_PRESETS, modelRepoLabel, modelSlots } from "./models.js";
2
+ import type { ModelPreset, ModelTask, ModelBackend } from "./models.js";
3
+ import { getModelPreset, MODEL_PRESETS, modelRepoLabel, modelSlots, supportsWasmFallback } from "./models.js";
4
4
  import type {
5
5
  ChatMessage,
6
6
  EngineHandles,
@@ -13,7 +13,6 @@ import type {
13
13
  import { EMPTY_RESPONSE, generate } from "./inference.js";
14
14
  import type { RuntimeParameters } from "./generation.js";
15
15
  import { clampNumber, runtimeParamsFromPreset } from "./generation.js";
16
- import type { MultimodalRuntime } from "./models.js";
17
16
  import type {
18
17
  MultimodalDevice,
19
18
  MultimodalHandle,
@@ -22,9 +21,11 @@ import type {
22
21
  SttResult,
23
22
  TtsTextSplitter,
24
23
  VlmResult,
24
+ ImageClassificationResult,
25
25
  } from "./multimodal.js";
26
26
  import {
27
27
  describeImage as describeImageWithHandle,
28
+ classifyImage as classifyImageWithHandle,
28
29
  disposeMultimodal,
29
30
  generateGemmaText,
30
31
  loadMultimodal,
@@ -39,7 +40,7 @@ import type { ModelSourceMode } from "./model-source.js";
39
40
  import { applyContextOverride, buildWebLLMAppConfig, TRANSFORMERS_CACHE_KEY } from "./model-source.js";
40
41
  import { verifyTransformersProxy, verifyWebLLMProxy } from "./proxy-verify.js";
41
42
  import type { HardwareSnapshot } from "./hardware.js";
42
- import { collectHardwareSnapshot } from "./hardware.js";
43
+ import { collectHardwareSnapshot, isChromiumBased } from "./hardware.js";
43
44
  import type { CacheCleanupResult, CachedModelStatus, IndividualModelCleanupResult, ModelCacheTarget, StorageEstimateSnapshot } from "./model-cache.js";
44
45
  import {
45
46
  deleteOneModelArtifactsFromBrowserStorage,
@@ -65,7 +66,7 @@ import { synthesizeClip } from "./audio/clip.js";
65
66
  export type LoadedModel = {
66
67
  modelId: string;
67
68
  slots: ModelTask[];
68
- backend: "webllm" | "transformers-js";
69
+ backend: ModelBackend;
69
70
  engine: MLCEngineInterface | null;
70
71
  worker: Worker | null;
71
72
  transformersGenerator: TransformersTextGenerationPipeline | null;
@@ -81,7 +82,6 @@ export type LoadProgress = { modelId: string; progress: number; status?: string;
81
82
  * Multimodal runtimes whose loaders honor a wasm device. The VLM/Gemma/audio-LLM recipes hardcode
82
83
  * WebGPU (their verified recipes only run there), so they cannot fall back.
83
84
  */
84
- const WASM_CAPABLE_RUNTIMES: ReadonlySet<MultimodalRuntime> = new Set(["stt", "tts-pipeline", "tts-kokoro"]);
85
85
 
86
86
  export type BrowserAIEvents = {
87
87
  /** Download/initialization progress for the model currently loading (progress 0–1). */
@@ -261,11 +261,16 @@ export class BrowserAI extends TypedEmitter<BrowserAIEvents> {
261
261
  try {
262
262
  this.#hardware = await collectHardwareSnapshot();
263
263
  let device: MultimodalDevice = "webgpu";
264
+ const wasmCapable = supportsWasmFallback(preset);
264
265
  if (!this.#hardware.webgpuSupported) {
265
266
  // Only the runtimes whose loaders honor a wasm device (ASR pipelines, TTS) can fall back;
266
267
  // VLM/Gemma/audio-LLM recipes hardcode WebGPU, and text engines require it outright.
267
- if (preset.mmRuntime && WASM_CAPABLE_RUNTIMES.has(preset.mmRuntime)) device = "wasm";
268
+ if (wasmCapable) device = "wasm";
268
269
  else throw new WebGPUUnavailableError(this.#hardware.webgpuReason);
270
+ } else if (wasmCapable && !isChromiumBased()) {
271
+ // Firefox and Safari now pass the WebGPU probe, but onnxruntime-web's WebGPU backend only
272
+ // reliably runs on Chromium (shader miscompiles / quantized-session failures elsewhere).
273
+ device = "wasm";
269
274
  }
270
275
 
271
276
  // Free any loaded models occupying the slots this one needs, waiting out their in-flight runs.
@@ -437,6 +442,18 @@ export class BrowserAI extends TypedEmitter<BrowserAIEvents> {
437
442
  }
438
443
 
439
444
  async #loadMultimodalModel(preset: ModelPreset, device: MultimodalDevice, progress: (info: Omit<LoadProgress, "modelId">) => void): Promise<LoadedModel> {
445
+ if (preset.mmRuntime === "stt-tdt") {
446
+ this.#status(`Loading ${preset.shortName} with ONNX Runtime…`);
447
+ const root = this.#modelSourceMode() === "same-origin-proxy"
448
+ ? `${this.#requireProxyOrigin()}/hf-transformers/` : "https://huggingface.co/";
449
+ if (this.#modelSourceMode() === "same-origin-proxy" && this.config.verifyProxy) {
450
+ await verifyTransformersProxy(this.#requireProxyOrigin(), preset.id);
451
+ }
452
+ const { loadTdtModel } = await import("./tdt/index.js");
453
+ const model = await loadTdtModel(preset.id, { ...this.config.tdt, device, baseUrl: `${root}${preset.id}/resolve/main/`, onProgress: this.#makeTransformersProgressForwarder(progress) });
454
+ progress({ progress: 1 });
455
+ return { ...newLoadedModel(preset), multimodal: { kind: "tdt", model } };
456
+ }
440
457
  this.#status(`Loading ${preset.shortName} with Transformers.js/WebGPU…`);
441
458
  progress({ progress: 0.05 });
442
459
 
@@ -542,12 +559,12 @@ export class BrowserAI extends TypedEmitter<BrowserAIEvents> {
542
559
  });
543
560
  }
544
561
 
545
- /** Transcribe 16 kHz mono PCM (or a URL) with the loaded Transcription-slot model. */
546
- async transcribe(audio: Float32Array | string, onDelta?: (text: string) => void): Promise<SttResult> {
562
+ /** Transcribe 16 kHz mono PCM. ASR pipelines also accept URLs; TDT and generative models require decoded PCM. The optional signal cooperatively cancels TDT decoding between inference calls. */
563
+ async transcribe(audio: Float32Array | string, onDelta?: (text: string) => void, signal?: AbortSignal): Promise<SttResult> {
547
564
  const model = this.sttModel();
548
565
  if (!model?.multimodal) throw new MissingModelError("stt");
549
566
  const handle = model.multimodal;
550
- return this.#runExclusive(model, () => transcribeWithHandle(handle, audio, onDelta));
567
+ return this.#runExclusive(model, () => transcribeWithHandle(handle, audio, onDelta, signal));
551
568
  }
552
569
 
553
570
  /** Synthesize speech with the loaded Speech-slot model. Returns the raw model audio. */
@@ -620,6 +637,14 @@ export class BrowserAI extends TypedEmitter<BrowserAIEvents> {
620
637
  };
621
638
  }
622
639
 
640
+ /** Return scored labels with the loaded Image-slot classifier. */
641
+ async classifyImage(imageSrc: string): Promise<ImageClassificationResult> {
642
+ const model = this.visionModel();
643
+ if (!model?.multimodal) throw new MissingModelError("vision");
644
+ const handle = model.multimodal;
645
+ return this.#runExclusive(model, () => classifyImageWithHandle(handle, imageSrc));
646
+ }
647
+
623
648
  /** Ask a question about an image with the loaded Image-slot model (VLM or Gemma). */
624
649
  async describeImage(imageSrc: string, prompt: string, onDelta?: (text: string) => void): Promise<VlmResult> {
625
650
  const model = this.visionModel();
package/src/config.ts CHANGED
@@ -26,6 +26,9 @@ export type KokoroOptions = {
26
26
  load?: () => Promise<KokoroModule>;
27
27
  };
28
28
 
29
+ /** Optional location of the matching ORT 1.31.1 asyncify MJS/WASM files. */
30
+ export type TdtOptions = { wasmPaths?: string | { mjs: string; wasm: string } };
31
+
29
32
  export type TtsDefaults = {
30
33
  /** Voice id for the loaded TTS model's roster (Kokoro: "af_heart" etc.; Supertonic: "F1"–"M5"). */
31
34
  voice: string;
@@ -69,6 +72,8 @@ export type BrowserAIConfig = {
69
72
  cacheBackend?: BrowserCacheBackend;
70
73
  webllm?: WebLLMOptions;
71
74
  kokoro?: KokoroOptions;
75
+ /** Override speech-runtime assets for self-hosted/offline applications. */
76
+ tdt?: TdtOptions;
72
77
  /** Default TTS voice/speed/pitch used by synthesize helpers and the voice pipeline. */
73
78
  tts?: Partial<TtsDefaults>;
74
79
  history?: HistoryOptions;
@@ -81,6 +86,7 @@ export type ResolvedConfig = {
81
86
  cacheBackend: BrowserCacheBackend;
82
87
  webllm: WebLLMOptions;
83
88
  kokoro: KokoroOptions;
89
+ tdt: TdtOptions;
84
90
  tts: TtsDefaults;
85
91
  history: HistoryOptions;
86
92
  };
@@ -98,6 +104,7 @@ export function resolveConfig(config: BrowserAIConfig = {}): ResolvedConfig {
98
104
  cacheBackend: config.cacheBackend ?? "indexeddb",
99
105
  webllm: config.webllm ?? {},
100
106
  kokoro: config.kokoro ?? {},
107
+ tdt: config.tdt ?? {},
101
108
  tts: { voice: "af_heart", speed: 1, pitch: 1, ...config.tts },
102
109
  history: config.history ?? {},
103
110
  };
package/src/hardware.ts CHANGED
@@ -19,6 +19,21 @@ export type HardwareSnapshot = {
19
19
  platform?: string;
20
20
  };
21
21
 
22
+ /**
23
+ * Whether this is a Chromium-based browser (Chrome, Edge, Opera, Brave, …), per the UA-Client-Hints
24
+ * brand list. onnxruntime-web's WebGPU backend is developed and tested against Chromium; Firefox's
25
+ * and Safari's newer WebGPU implementations still fail on some of its generated WGSL and its
26
+ * quantized-weight session transforms (errors that only surface at load/inference time), so
27
+ * wasm-capable runtimes prefer wasm off-Chromium even when the WebGPU probe passes.
28
+ * navigator.userAgentData is itself Chromium-only, and every Chromium new enough for WebGPU (113+)
29
+ * ships it, so the brand list is a reliable signal.
30
+ */
31
+ export function isChromiumBased(): boolean {
32
+ if (typeof navigator === "undefined") return false;
33
+ const nav = navigator as Navigator & { userAgentData?: { brands?: Array<{ brand: string }> } };
34
+ return nav.userAgentData?.brands?.some((entry) => entry.brand === "Chromium") ?? false;
35
+ }
36
+
22
37
  const LIMIT_NAMES = [
23
38
  "maxBufferSize",
24
39
  "maxStorageBufferBindingSize",
package/src/history.ts CHANGED
@@ -36,7 +36,7 @@ export type RunRecord = {
36
36
  /** Derived vendor bucket for filtering/labels. */
37
37
  vendor: ModelVendor;
38
38
  /** Backend used for the run. */
39
- backend: "webllm" | "transformers-js";
39
+ backend: "webllm" | "transformers-js" | "onnx-runtime";
40
40
  /** Task key ({@link RunCaseKey} suggested; any non-empty string accepted). */
41
41
  case: string;
42
42
  promptTokens: number;
@@ -111,7 +111,7 @@ function coerceRecord(value: unknown): RunRecord | null {
111
111
  ts: record.ts as number,
112
112
  model: record.model,
113
113
  vendor: VALID_VENDORS.has(record.vendor as ModelVendor) ? (record.vendor as ModelVendor) : "other",
114
- backend: record.backend === "transformers-js" ? "transformers-js" : "webllm",
114
+ backend: record.backend === "onnx-runtime" ? "onnx-runtime" : record.backend === "transformers-js" ? "transformers-js" : "webllm",
115
115
  case: record.case,
116
116
  promptTokens: record.promptTokens as number,
117
117
  completionTokens: record.completionTokens as number,
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ export type { BrowserAIEvents, GenerateTextOptions, LoadOptions, LoadProgress, L
12
12
 
13
13
  // Configuration
14
14
  export { DEFAULT_CONTEXT_LENGTH, resolveConfig } from "./config.js";
15
- export type { BrowserAIConfig, HistoryOptions, KokoroOptions, ResolvedConfig, StorageLike, TtsDefaults, WebLLMOptions } from "./config.js";
15
+ export type { BrowserAIConfig, HistoryOptions, KokoroOptions, ResolvedConfig, StorageLike, TdtOptions, TtsDefaults, WebLLMOptions } from "./config.js";
16
16
 
17
17
  // Errors
18
18
  export {
@@ -81,6 +81,7 @@ export type {
81
81
  // Multimodal runtimes (STT / TTS / VLM / Gemma)
82
82
  export {
83
83
  capSrc,
84
+ classifyImage,
84
85
  DEFAULT_KOKORO_CDN,
85
86
  describeImage,
86
87
  disposeMultimodal,
@@ -99,6 +100,7 @@ export {
99
100
  export type {
100
101
  AudioWindow,
101
102
  GenStats,
103
+ ImageClassificationResult,
102
104
  KokoroModule,
103
105
  LoadMultimodalOptions,
104
106
  MultimodalDevice,
package/src/inference.ts CHANGED
@@ -305,9 +305,10 @@ async function runTransformers(
305
305
  ): Promise<TransformersRunResult> {
306
306
  const generator = handles.transformersGenerator!;
307
307
  const tokenizer = handles.transformersTokenizer ?? generator.tokenizer ?? null;
308
+ const chatTemplateOptions = preset.disableThinkingSupported ? { enable_thinking: !runtime.disableThinking } : undefined;
308
309
 
309
310
  const telemetry: TransformersGenerationTelemetry = {
310
- promptTokens: countPromptTokens(tokenizer, messages),
311
+ promptTokens: countPromptTokens(tokenizer, messages, chatTemplateOptions),
311
312
  tokenCallbackIntervalSeconds: [],
312
313
  tokenCallbackCount: 0,
313
314
  tokenCountSource: "unavailable",
@@ -323,8 +324,8 @@ async function runTransformers(
323
324
  };
324
325
  if (runtime.temperature > 0) generationOptions.temperature = runtime.temperature;
325
326
  if (runtime.seed !== null) generationOptions.seed = runtime.seed;
326
- if (preset.disableThinkingSupported) {
327
- generationOptions.tokenizer_encode_kwargs = { enable_thinking: !runtime.disableThinking };
327
+ if (chatTemplateOptions) {
328
+ generationOptions.tokenizer_encode_kwargs = chatTemplateOptions;
328
329
  }
329
330
 
330
331
  const Streamer = handles.transformersModule?.TextStreamer;
@@ -438,7 +439,7 @@ export function collectGenericStats(model: string, backend: ModelBackend | "unkn
438
439
  /* ------------------------------------------------------------------ */
439
440
 
440
441
  export function buildRuntimeStatsText(stats: RuntimeStats): string {
441
- const backendLabel = stats.backend === "transformers-js" ? "Transformers.js" : stats.backend === "webllm" ? "WebLLM" : "unknown";
442
+ const backendLabel = stats.backend === "onnx-runtime" ? "ONNX Runtime" : stats.backend === "transformers-js" ? "Transformers.js" : stats.backend === "webllm" ? "WebLLM" : "unknown";
442
443
  const lines = [
443
444
  `model: ${stats.model}`,
444
445
  `backend: ${backendLabel}`,
@@ -514,11 +515,16 @@ function extractGeneratedText(output: unknown): string {
514
515
  return JSON.stringify(output, null, 2);
515
516
  }
516
517
 
517
- function countPromptTokens(tokenizer: TransformersTokenizer | null, messages: ChatMessage[]): number | undefined {
518
+ function countPromptTokens(
519
+ tokenizer: TransformersTokenizer | null,
520
+ messages: ChatMessage[],
521
+ chatTemplateOptions?: Record<string, unknown>,
522
+ ): number | undefined {
518
523
  if (!tokenizer) return undefined;
519
524
  try {
520
525
  if (typeof tokenizer.apply_chat_template === "function") {
521
- const tokenized = tokenizer.apply_chat_template(messages, { tokenize: true, add_generation_prompt: true });
526
+ // Count the same prompt the pipeline generates, including MiniCPM5's optional empty think block.
527
+ const tokenized = tokenizer.apply_chat_template(messages, { tokenize: true, add_generation_prompt: true, ...chatTemplateOptions });
522
528
  const count = extractTokenCount(tokenized);
523
529
  if (typeof count === "number") return count;
524
530
  }
package/src/metrics.ts CHANGED
@@ -14,7 +14,7 @@ export function buildMultimodalStats(gen: GenStats, modelId: string): RuntimeSta
14
14
  return {
15
15
  rawText: "",
16
16
  model: modelId,
17
- backend: "transformers-js",
17
+ backend: getModelPreset(modelId).backend,
18
18
  finishReason: "stop",
19
19
  measuredElapsedMs: elapsedMs,
20
20
  completionTokens,
@@ -41,7 +41,7 @@ export function runRecordFromStats(caseKey: string, stats: RuntimeStats): RunRec
41
41
  ts: Date.now(),
42
42
  model: preset.shortName,
43
43
  vendor: vendorForModel(preset),
44
- backend: stats.backend === "transformers-js" ? "transformers-js" : "webllm",
44
+ backend: stats.backend === "onnx-runtime" ? "onnx-runtime" : stats.backend === "transformers-js" ? "transformers-js" : "webllm",
45
45
  case: caseKey,
46
46
  promptTokens: stats.promptTokens ?? 0,
47
47
  completionTokens: stats.completionTokens ?? 0,
package/src/models.ts CHANGED
@@ -11,7 +11,7 @@ export type RuntimePreset = {
11
11
  disableThinking: boolean;
12
12
  };
13
13
 
14
- export type ModelBackend = "webllm" | "transformers-js";
14
+ export type ModelBackend = "webllm" | "transformers-js" | "onnx-runtime";
15
15
 
16
16
  /** Input/output modality of a model, used for the library modality-flow line + Playground gating. */
17
17
  export type Modality = "text" | "image" | "audio";
@@ -23,9 +23,11 @@ export type ModelTask = "text" | "vision" | "stt" | "tts";
23
23
  * inference recipe in llm/multimodal.ts.
24
24
  */
25
25
  export type MultimodalRuntime =
26
+ | "stt-tdt" // ONNX Runtime TDT speech recognition
26
27
  | "stt" // automatic-speech-recognition pipeline (Whisper / Moonshine)
27
28
  | "tts-kokoro" // kokoro-js KokoroTTS
28
29
  | "tts-pipeline" // native text-to-speech pipeline (Supertonic)
30
+ | "image-classification" // scored image labels (NSFW detection)
29
31
  | "vlm-vision2seq" // AutoModelForVision2Seq (SmolVLM)
30
32
  | "vlm-imagetext" // AutoModelForImageTextToText (Qwen3-VL, GLM-OCR)
31
33
  | "audio-text-to-text" // AutoModelForAudioTextToText with an <audio> token in the prompt (Granite Speech, Ultravox, non-realtime Voxtral)
@@ -60,6 +62,8 @@ export type ModelPreset = {
60
62
  modalityOut?: Modality[];
61
63
  /** How a multimodal (non-LLM-text) model loads + runs. Undefined for plain text LLMs. */
62
64
  mmRuntime?: MultimodalRuntime;
65
+ /** Per-model image processing and generation behavior. */
66
+ visionOptions?: { defaultPrompt?: string; maxNewTokens?: number; preserveSpecialTokens?: boolean; doImageSplitting?: boolean; outputFormat?: "doctags" };
63
67
  /** Optional display brand for the vendor tag (e.g. "Moonshine", "Kokoro", "Zhipu"). Falls back to the vendor label. */
64
68
  brand?: string;
65
69
  };
@@ -322,6 +326,80 @@ export const MODEL_PRESETS: ModelPreset[] = [
322
326
  officialRepo: "https://huggingface.co/google/gemma-4-E4B-it",
323
327
  artifactRepo: "https://huggingface.co/welcoma/gemma-4-E4B-it-q4f16_1-MLC",
324
328
  },
329
+ {
330
+ id: "enriqueag/phonon-2.5-onnx",
331
+ label: "Phonon 2.5 — compact speech recognition via WebGPU + WASM",
332
+ shortName: "Phonon 2.5",
333
+ description: "Fermion speech recognition with a compact quantized WebGPU encoder and a WASM token-duration decoder. Audio stays on device.",
334
+ approximateDownload: "~381 MB encoder, decoder and vocabulary",
335
+ vramRequiredMB: 1536,
336
+ vramSource: "estimated",
337
+ lowResourceRequired: true,
338
+ disableThinkingSupported: false,
339
+ defaultRuntime: extractionPreset(),
340
+ source: "transformers-onnx",
341
+ backend: "onnx-runtime",
342
+ stability: "experimental",
343
+ officialRepo: "https://huggingface.co/FermionResearch/Phonon-2.5",
344
+ artifactRepo: "https://huggingface.co/enriqueag/phonon-2.5-onnx",
345
+ tasks: ["stt"], modalityIn: ["audio"], modalityOut: ["text"],
346
+ mmRuntime: "stt-tdt",
347
+ brand: "Fermion",
348
+ notes: "Requires WebGPU for the compact encoder; the decoder runs on CPU/WASM. Long audio is split near quiet intervals into chunks of at most 30 seconds. Memory is estimated.",
349
+ },
350
+ {
351
+ id: "istupakov/parakeet-tdt-0.6b-v3-onnx",
352
+ label: "Parakeet TDT 0.6B v3 — multilingual speech recognition via WASM",
353
+ shortName: "Parakeet TDT v3",
354
+ description: "NVIDIA multilingual token-duration speech recognition, using the published int8 ONNX encoder and decoder on CPU/WASM.",
355
+ approximateDownload: "~671 MB int8 encoder, decoder, frontend and vocabulary",
356
+ vramRequiredMB: 0,
357
+ vramSource: "estimated",
358
+ lowResourceRequired: false,
359
+ disableThinkingSupported: false,
360
+ defaultRuntime: extractionPreset(),
361
+ source: "transformers-onnx",
362
+ backend: "onnx-runtime",
363
+ stability: "experimental",
364
+ officialRepo: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
365
+ artifactRepo: "https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx",
366
+ tasks: ["stt"], modalityIn: ["audio"], modalityOut: ["text"],
367
+ mmRuntime: "stt-tdt",
368
+ brand: "NVIDIA",
369
+ notes: "Runs on CPU/WASM and consumes system memory despite requiring no GPU memory. Long audio is split near quiet intervals into chunks of at most 30 seconds.",
370
+ },
371
+ {
372
+ id: "onnx-community/granite-docling-258M-ONNX",
373
+ label: "Granite Docling 258M — document conversion to DocTags",
374
+ shortName: "Granite Docling 258M",
375
+ description: "IBM Granite document vision model. Converts page images into structured DocTags, preserving layout and document markup.",
376
+ approximateDownload: "~1.27 GB FP32 ONNX modules plus tokenizer/config files",
377
+ vramRequiredMB: 3072, vramSource: "estimated", lowResourceRequired: false,
378
+ disableThinkingSupported: false, defaultRuntime: extractionPreset({ maxTokens: 4096 }),
379
+ source: "transformers-onnx", backend: "transformers-js", stability: "experimental",
380
+ officialRepo: "https://huggingface.co/ibm-granite/granite-docling-258M",
381
+ artifactRepo: "https://huggingface.co/onnx-community/granite-docling-258M-ONNX",
382
+ transformersDtype: "fp32", tasks: ["vision"], modalityIn: ["image", "text"], modalityOut: ["text"],
383
+ mmRuntime: "vlm-vision2seq", brand: "IBM",
384
+ visionOptions: { defaultPrompt: "Convert this page to docling.", maxNewTokens: 4096, preserveSpecialTokens: true, doImageSplitting: true, outputFormat: "doctags" },
385
+ notes: "FP32 is intentional: the FP16/q4f16 WebGPU paths failed accuracy smoke checks on Apple GPU. FP32 produced the expected result.",
386
+ },
387
+ {
388
+ id: "onnx-community/nsfw_image_detection-ONNX",
389
+ label: "NSFW image detection — scored normal / nsfw classification",
390
+ shortName: "NSFW image detection",
391
+ description: "ViT image classifier returning normal and nsfw scores. Scores are model predictions, not a guarantee of content safety.",
392
+ approximateDownload: "~343 MB FP32 ONNX weights plus image processor/config files",
393
+ vramRequiredMB: 768, vramSource: "estimated", lowResourceRequired: true,
394
+ disableThinkingSupported: false, defaultRuntime: extractionPreset({}),
395
+ source: "transformers-onnx", backend: "transformers-js", stability: "experimental",
396
+ officialRepo: "https://huggingface.co/Falconsai/nsfw_image_detection",
397
+ artifactRepo: "https://huggingface.co/onnx-community/nsfw_image_detection-ONNX",
398
+ transformersDtype: "fp32", tasks: ["vision"], modalityIn: ["image"], modalityOut: ["text"],
399
+ mmRuntime: "image-classification", brand: "Falconsai",
400
+ notes: "FP32 is intentional: the FP16/q4f16 WebGPU paths failed accuracy smoke checks on Apple GPU. FP32 produced the expected result.",
401
+ },
402
+
325
403
  {
326
404
  id: "onnx-community/whisper-base",
327
405
  label: "Whisper Base — multilingual speech-to-text via Transformers.js/WebGPU",
@@ -948,6 +1026,48 @@ export const MODEL_PRESETS: ModelPreset[] = [
948
1026
  artifactRepo: "https://huggingface.co/onnx-community/granite-4.0-h-micro-ONNX",
949
1027
  transformersDtype: "q4f16",
950
1028
  },
1029
+ {
1030
+ id: "RASMUS/MiniCPM5-2B-ONNX",
1031
+ label: "MiniCPM5 2B q4f16 — Transformers.js/WebGPU (~3 GB VRAM est.)",
1032
+ shortName: "MiniCPM5 2B",
1033
+ description:
1034
+ "OpenBMB's regular MiniCPM5 2B instruction model, quantized for browser GPU inference with streaming, optional thinking and measured token/timing metrics.",
1035
+ approximateDownload: "~1.83 GB ONNX weights plus tokenizer and browser cache overhead",
1036
+ vramRequiredMB: 3072,
1037
+ vramSource: "estimated",
1038
+ lowResourceRequired: false,
1039
+ disableThinkingSupported: true,
1040
+ defaultRuntime: extractionPreset({ maxTokens: 768, disableThinking: true }),
1041
+ source: "transformers-onnx",
1042
+ backend: "transformers-js",
1043
+ stability: "experimental",
1044
+ officialRepo: "https://huggingface.co/openbmb/MiniCPM5-2B",
1045
+ artifactRepo: "https://huggingface.co/RASMUS/MiniCPM5-2B-ONNX",
1046
+ transformersDtype: "q4f16",
1047
+ brand: "OpenBMB",
1048
+ notes: "Requires WebGPU shader-f16. JSON schema mode is prompt-only. Browser memory depends on context length; the VRAM figure is an estimate.",
1049
+ },
1050
+ {
1051
+ id: "j4ys0n/MiniCPM5-2B-heretic-abliterated-ONNX",
1052
+ label: "MiniCPM5 2B Heretic q4f16 — Transformers.js/WebGPU (~3 GB VRAM est.)",
1053
+ shortName: "MiniCPM5 2B Heretic",
1054
+ description:
1055
+ "Heretic-abliterated MiniCPM5 2B, converted from the original insraq safetensors checkpoint to browser-ready ONNX with streaming, optional thinking and measured token/timing metrics.",
1056
+ approximateDownload: "~1.83 GB ONNX weights plus tokenizer and browser cache overhead",
1057
+ vramRequiredMB: 3072,
1058
+ vramSource: "estimated",
1059
+ lowResourceRequired: false,
1060
+ disableThinkingSupported: true,
1061
+ defaultRuntime: extractionPreset({ maxTokens: 768, disableThinking: true }),
1062
+ source: "transformers-onnx",
1063
+ backend: "transformers-js",
1064
+ stability: "experimental",
1065
+ officialRepo: "https://huggingface.co/insraq/MiniCPM5-2B-heretic-abliterated",
1066
+ artifactRepo: "https://huggingface.co/j4ys0n/MiniCPM5-2B-heretic-abliterated-ONNX",
1067
+ transformersDtype: "q4f16",
1068
+ brand: "OpenBMB",
1069
+ notes: "Original Heretic checkpoint referenced by Abiray's model card. Requires WebGPU shader-f16. JSON schema mode is prompt-only; VRAM usage is estimated.",
1070
+ },
951
1071
  {
952
1072
  id: "Mike0021/MiniCPM5-1B-ONNX-Web",
953
1073
  label: "MiniCPM5 1B ONNX-web — community q4 Transformers.js/WebGPU export",
@@ -986,6 +1106,31 @@ export const MODEL_PRESETS: ModelPreset[] = [
986
1106
  artifactRepo: "https://huggingface.co/HuggingFaceTB/SmolLM3-3B-ONNX",
987
1107
  transformersDtype: "q4",
988
1108
  },
1109
+ {
1110
+ id: "j4ys0n/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX",
1111
+ label: "NVIDIA Nemotron 3.5 Bolt E4B ONNX — text, image and audio input; text output (~5.21 GB)",
1112
+ shortName: "Nemotron 3.5 Bolt E4B",
1113
+ description:
1114
+ "NVIDIA's post-trained Gemma 4 E4B, exported for Transformers.js/WebGPU with text, image, and audio input and text output.",
1115
+ approximateDownload: "~5.21 GB q4f16 ONNX artifacts plus tokenizer/config files",
1116
+ vramRequiredMB: 8192,
1117
+ vramSource: "estimated",
1118
+ lowResourceRequired: false,
1119
+ disableThinkingSupported: false,
1120
+ defaultRuntime: extractionPreset({ maxTokens: 768, topP: 0.9, repetitionPenalty: 1.05 }),
1121
+ source: "transformers-onnx",
1122
+ backend: "transformers-js",
1123
+ stability: "experimental",
1124
+ officialRepo: "https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Bolt-E4B-BF16",
1125
+ artifactRepo: "https://huggingface.co/j4ys0n/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX",
1126
+ notes: "Text, image, and audio browser inference smoke-tested on Apple WebGPU. Output quality and GPU memory have not been benchmarked; GPU memory is an estimate, not a measured minimum.",
1127
+ transformersDtype: "q4f16",
1128
+ tasks: ["text", "vision", "stt"],
1129
+ modalityIn: ["text", "image", "audio"],
1130
+ modalityOut: ["text"],
1131
+ mmRuntime: "gemma-mm",
1132
+ brand: "NVIDIA",
1133
+ },
989
1134
  {
990
1135
  id: "onnx-community/gemma-4-E2B-it-ONNX",
991
1136
  label: "Gemma 4 E2B ONNX — experimental Transformers.js/WebGPU alternative",
@@ -1169,3 +1314,9 @@ export function modelRepoLabel(preset: ModelPreset): string {
1169
1314
  return preset.id;
1170
1315
  }
1171
1316
  }
1317
+
1318
+ /** Whether this preset has an implemented CPU/WASM load path. */
1319
+ export function supportsWasmFallback(preset: ModelPreset): boolean {
1320
+ return preset.id === "istupakov/parakeet-tdt-0.6b-v3-onnx" ||
1321
+ (preset.mmRuntime !== undefined && ["stt", "tts-pipeline", "tts-kokoro", "image-classification"].includes(preset.mmRuntime));
1322
+ }