In-browser inference for FluidAudio's core models via WebGPU + WebAssembly — no server, no upload, everything runs client-side. This is the browser sibling of the Swift/CoreML FluidAudio framework and the Rust/WASM FluidVad.
Why a separate repo? CoreML (
.mlmodelc) cannot run in a browser — it is an Apple-only runtime. The browser runs the ONNX source of the same models throughonnxruntime-web(WebGPU EP, WASM fallback),transformers.js, andkokoro-js. So this repo tracks the ONNX exports, not the CoreML bundles.
Every engine is verified on real data — no scaffolds. Parakeet v3 = 2.15% WER
on full LibriSpeech test-clean (matches native FluidAudio ~2.14%). Measured
in-browser on WebGPU (Chrome/macOS, warm/steady-state): VAD 139×, Sortformer
128×, EOU 91×, Parakeet v3 47×, Whisper 33×, Kokoro ~10×,
Nemotron 4.1× — all 8 engines correct + real-time-plus, on WebGPU/WASM via
ONNX. (Nemotron uses the soniqo fp16 export — built to run on ORT-WebGPU, since
the int4 export can't.) First (cold) run is several× slower — WebGPU compiles
shaders. See docs/BENCHMARKS.md.
| Engine | Model | Runtime | Backend | Status |
|---|---|---|---|---|
asr-parakeet |
Parakeet TDT 0.6B v3 | onnxruntime-web |
fp16 enc WebGPU + WASM dec | ✅ 2.15% WER, 46× (warm); fp16 encoder 1.24 GB (fp32 exceeded the 2 GB buffer cap) |
asr-whisper |
Whisper (99 langs) | transformers.js | WebGPU / WASM | ✅ 24× |
tts-kokoro |
Kokoro 82M (en + zh g2pW) | kokoro-js |
WebGPU / WASM | ✅ 10× en / 10× zh (warm) |
diarization-sortformer |
NVIDIA Sortformer 4-spk | onnxruntime-web |
WebGPU / WASM | ✅ 82× short-audio; long-audio needs streaming loop |
asr-nemotron |
Nemotron 3.5 streaming (multilingual) | onnxruntime-web |
fp16 enc WebGPU + WASM dec | ✅ 4.1×, correct — soniqo fp16 export (built for ORT-WebGPU; int4 can't run there) |
vad-silero |
Silero VAD v5 | onnxruntime-web |
WASM | ✅ 132× (direct ORT, no vad-web) |
eou-parakeet |
Parakeet EOU 120M | onnxruntime-web |
WebGPU / WASM | ✅ 86× (transcript + <EOU>/<EOB>) |
✅ = correctness checked (WER / RTFx / output) on real data. Numbers in
docs/BENCHMARKS.md.
Per-engine deep dives: ARCHITECTURE.md (integration +
WebGPU-vs-WASM tradeoffs), NEMOTRON.md (cache-aware streaming
RNNT), EOU.md (end-of-utterance detection + the NA-mel gotcha),
KOKORO_ZH.md (Chinese G2P frontend).
npm install
npm run dev # http://localhost:5173 (cross-origin isolated for threaded WASM)
npm run buildOpen /bench.html — it runs each engine on a bundled 12s sample and downloads
a results JSON (load/run ms, RTFx, per-stage timings, output, per-engine errors).
This is where you get the real WebGPU numbers. First run downloads model weights
(cached after). Params: ?full=1 (add the heavy engines — Kokoro-zh, Nemotron,
Parakeet EOU), ?engines=a,b (pick), ?noauto=1 (don't auto-download).
Live demo: https://fluidinference.github.io/fluidaudio-web/ (and
/bench.html?full=1 for the auto-benchmark). Auto-deploys from main via
.github/workflows/deploy.yml (one-time: repo Settings → Pages → Source: GitHub
Actions).
Static site — npm run build outputs dist/. Model weights stream from Hugging
Face and cache client-side — no backend.
GitHub Pages can't send headers, so WASM runs single-threaded there (WebGPU engines
are unaffected). For multi-threaded WASM (SharedArrayBuffer — speeds up the
WASM-bound work like Nemotron's decode), deploy to Cloudflare Pages, which serves the
public/_headers file that sets COOP/COEP. One-time dashboard setup:
- Workers & Pages → Create → Pages → Connect to Git → pick
FluidInference/fluidaudio-web. - Build settings: Framework preset = None (or Vite), Build command =
npm run build, Build output directory =dist. - Environment variables → add
NPM_FLAGS = --ignore-scripts(skips the unusedsharp/native postinstalls so the install doesn't fail). - Deploy. CF sets
CF_PAGES=1, so Vite uses base/(root of the*.pages.devdomain), andpublic/_headersturns on cross-origin isolation.
COOP/COEP note: _headers uses require-corp. The HF model fetches are CORS
(fetch() with access-control-allow-origin: *), which satisfies COEP — but if a
download is ever blocked, switch the header to Cross-Origin-Embedder-Policy: credentialless. Netlify uses the same _headers; Vercel needs vercel.json; nginx
add_header.
First-load weight: the default
asr-parakeetuses a ~1.24 GB fp16 encoder — fine once cached, heavy on first visit. For a light demo use?engines=tts-kokoro-en, diarization-sortformer,vad-silero(≈ tens of MB).
WebGPU (Chrome/Edge 121+, Safari 26+) is optional — it accelerates the heavy encoders (Parakeet, Kokoro). Everything else, and every engine's fallback path, runs on WASM: Silero VAD is WASM-only by design (tiny model), and engines downgrade automatically where WebGPU op coverage is incomplete. A WASM-only browser still runs the full matrix, just slower on the big encoders.
src/
core/ shared runtime: ORT session factory, model cache, audio I/O, registry
engines/ one folder per model, all implement a common interface (core/types.ts)
main.ts demo UI wiring
docs/ architecture + per-engine notes
- WebGPU has no int8/int4 kernels → use fp16 for the browser. ORT-web's WebGPU EP can't run quantized weights: Parakeet's int8 encoder collapses to all-blank and Nemotron's int4 returns empty. The fix for both was the same — an fp16 encoder (Parakeet 1.24 GB; Nemotron via soniqo's fp16 export, purpose-built for ORT-WebGPU), which runs correctly and fast on the GPU. (Check an encoder's output std before blaming the decoder.)
- Task inputs matter more than precision. Nemotron returned empty also because
lang_id=0= Bulgarian; en-US is ordinal 24. Always verify language/prompt ids. - Models vary wildly in size. Parakeet fp16 encoder ≈ 1.24 GB, Nemotron fp16 ≈ 1.24 GB, EOU fp32 ≈ 480 MB, Kokoro ≈ 90 MB, Silero VAD ≈ 2 MB. Fetched from Hugging Face once, persisted via the Cache API.
- G2P is the TTS long pole. The acoustic model is easy; Chinese uses a
precomputed misaki-exact pinyin→IPA table +
pinyin-pro(docs/KOKORO_ZH.md). - onnxruntime-web + Vite: keep ORT out of
optimizeDeps(Vite rewrites itsjsep.mjsimport and breaks it), and don't pinwasmPathsto a mismatched version — let ORT self-resolve. - Don't fight
@ricky0123/vad-web. It's CJS and does a dynamicrequire("onnxruntime-web/wasm")that Vite can't resolve once ORT is excluded fromoptimizeDeps. Silero's ONNX interface is trivial (512-sample windows + a 2×1×128 state), sovad-silerodrivessilero_vad.onnxdirectly throughcore/ortand drops the dependency. - EOU wants a different mel.
parakeet-realtime-eouis a NeMo streaming RNNT and expects NA (un-normalized) log-mel — the Nemotron frontend, not Parakeet's per-feature CMVN. Feed it the wrong normalization and the encoder emits content-free frames (flat per-frame RMS) while the joint predicts blank on every step — looks like a decode bug, is actually the frontend.
MIT (code). Model licenses follow their upstream repos (see registry).