Summary
Technical investigation into running Pocket TTS (Kyutai CALM, zero-shot voice cloning) on sokuji's local-inference stack. The same int8 ONNX models were measured across three runtimes:
- Browser WASM (onnxruntime-web): ~0.6× realtime — not viable for real-time.
- Native CPU (Python sherpa-onnx ~4×, Node onnxruntime-node ~2–2.4×): real-time with headroom.
- The model is English-only.
PoC branches: feat/pocket-tts-playground-poc (browser WASM), feat/pocket-tts-electron-native-poc (native Node in Electron). Both dev-gated, not wired into the production pipeline.
What is Pocket TTS
Kyutai CALM (Continuous Audio LM), zero-shot voice cloning: a reference clip → voice embedding → conditioned autoregressive generation. Pipeline = mimi_encoder (reference → latents) → flow_lm_main (~100M AR backbone, per-frame) + flow_lm_flow (consistency/flow head) → mimi_decoder (latents → 24 kHz PCM), plus text_conditioner. int8 ONNX bundle ≈ 140 MB (5 onnx + tokenizer + bundle.json + BOS npy). BOS-prepend is required for faithful timbre.
What was tested
Same int8 graphs, three runtimes: (1) onnxruntime-web WASM in a dev playground; (2) sherpa-onnx native via Python; (3) onnxruntime-node native inside Electron (our TS pocketInferenceCore).
Performance
| runtime |
host |
RTF (cached) |
| onnxruntime-web WASM (1 thread) |
browser |
~0.6× |
| sherpa-onnx WASM (4 threads) |
browser |
~0.65× |
| onnxruntime-node |
plain Node |
~3.2× |
| onnxruntime-node |
Electron child_process |
~2.0–2.4× headless · ~1.75–2.35× full app |
| sherpa-onnx |
plain Python |
~4× (1–8 threads) |
Per-frame, per-stage (same int8 graphs):
| stage |
WASM |
native node |
ratio |
flow_lm_main (AR backbone) |
~56 ms |
~22 ms |
~2.5× |
mimi_decoder (vocoder) |
~38 ms |
~6 ms |
~6× |
Why WASM ≪ native
int8 kernel quality dominates:
- SIMD / VNNI — native MLAS uses AVX2 / AVX-512 / AVX-VNNI (single-instruction int8 dot-products); WASM SIMD is fixed 128-bit, no VNNI.
- Kernels — hand-tuned assembly (native) vs generic Emscripten C++ (WASM).
- Threading — native multi-threads the encoder/decoder; WASM is forced single-thread (threaded WASM OOMs on Pocket's KV-caches).
- WASM overhead — linear-memory bounds-checks, growable memory, JS boundary.
The conv-heavy mimi_decoder is hit hardest (~6×). This is an intrinsic WASM↔native gap for int8 NN inference — not fixable in WASM; the only browser path to native-class speed is WebGPU (no Pocket/Mimi WebGPU port exists today).
Native host options (for future use)
A. Python — sherpa-onnx. Fastest (~4×), simplest API (OfflineTts + GenerationConfig), ships its own C++ engine + threading + model packaging. Would run as a sidecar (Python runtime to bundle).
B. Node — onnxruntime-node (this PoC). ~2–2.4×, reuses our TS pocketInferenceCore (one core; web + node hosts via Tensor dependency injection). Integrates into Electron with no extra runtime via child_process.fork + ELECTRON_RUN_AS_NODE=1 (Electron's own Node). Hard-won learnings:
- Use
child_process.fork, not utilityProcess — the latter intermittently SIGTRAP-crashed while hosting the native addon.
serialization: 'advanced' is required to pass Float32Array over IPC.
- Cap
intraOpNumThreads — the default (= all logical cores) oversubscribes the tiny per-frame matmuls and ~halves throughput.
- onnxruntime-node ships napi-v6 → ABI-stable across Node & Electron;
@electron/rebuild is not needed.
Platform / runtime viability
- Electron desktop: ✅ native (Python or Node), real-time.
- Browser extension: ❌ WASM ceiling ~0.6×; no WebGPU port.
- Constraint: English-only model.
Not covered by this investigation
Production packaging (Forge/asar native .node unpack, ~140 MB model distribution), LocalInferenceClient pipeline wiring, reference-voice UX, GPU execution providers, non-English, streaming, and model licensing (Kyutai/CALM weights + the KevinAHM ONNX port — to verify before any distribution).
References
- Spec:
docs/superpowers/specs/2026-06-18-pocket-tts-electron-native-poc-design.md
- Plan:
docs/superpowers/plans/2026-06-18-pocket-tts-electron-native-poc.md
- Branches:
feat/pocket-tts-playground-poc (browser WASM), feat/pocket-tts-electron-native-poc (native Node)
- Key commits:
cfa07803 (Tensor DI), f46ad7e9 (node bench), 4adc1a0d (Electron host), 6008dc02 (child_process fix)
Summary
Technical investigation into running Pocket TTS (Kyutai CALM, zero-shot voice cloning) on sokuji's local-inference stack. The same int8 ONNX models were measured across three runtimes:
PoC branches:
feat/pocket-tts-playground-poc(browser WASM),feat/pocket-tts-electron-native-poc(native Node in Electron). Both dev-gated, not wired into the production pipeline.What is Pocket TTS
Kyutai CALM (Continuous Audio LM), zero-shot voice cloning: a reference clip → voice embedding → conditioned autoregressive generation. Pipeline =
mimi_encoder(reference → latents) →flow_lm_main(~100M AR backbone, per-frame) +flow_lm_flow(consistency/flow head) →mimi_decoder(latents → 24 kHz PCM), plustext_conditioner. int8 ONNX bundle ≈ 140 MB (5 onnx + tokenizer + bundle.json + BOS npy). BOS-prepend is required for faithful timbre.What was tested
Same int8 graphs, three runtimes: (1) onnxruntime-web WASM in a dev playground; (2) sherpa-onnx native via Python; (3) onnxruntime-node native inside Electron (our TS
pocketInferenceCore).Performance
child_processPer-frame, per-stage (same int8 graphs):
flow_lm_main(AR backbone)mimi_decoder(vocoder)Why WASM ≪ native
int8 kernel quality dominates:
The conv-heavy
mimi_decoderis hit hardest (~6×). This is an intrinsic WASM↔native gap for int8 NN inference — not fixable in WASM; the only browser path to native-class speed is WebGPU (no Pocket/Mimi WebGPU port exists today).Native host options (for future use)
A. Python — sherpa-onnx. Fastest (~4×), simplest API (
OfflineTts+GenerationConfig), ships its own C++ engine + threading + model packaging. Would run as a sidecar (Python runtime to bundle).B. Node — onnxruntime-node (this PoC). ~2–2.4×, reuses our TS
pocketInferenceCore(one core; web + node hosts viaTensordependency injection). Integrates into Electron with no extra runtime viachild_process.fork+ELECTRON_RUN_AS_NODE=1(Electron's own Node). Hard-won learnings:child_process.fork, notutilityProcess— the latter intermittently SIGTRAP-crashed while hosting the native addon.serialization: 'advanced'is required to passFloat32Arrayover IPC.intraOpNumThreads— the default (= all logical cores) oversubscribes the tiny per-frame matmuls and ~halves throughput.@electron/rebuildis not needed.Platform / runtime viability
Not covered by this investigation
Production packaging (Forge/asar native
.nodeunpack, ~140 MB model distribution),LocalInferenceClientpipeline wiring, reference-voice UX, GPU execution providers, non-English, streaming, and model licensing (Kyutai/CALM weights + the KevinAHM ONNX port — to verify before any distribution).References
docs/superpowers/specs/2026-06-18-pocket-tts-electron-native-poc-design.mddocs/superpowers/plans/2026-06-18-pocket-tts-electron-native-poc.mdfeat/pocket-tts-playground-poc(browser WASM),feat/pocket-tts-electron-native-poc(native Node)cfa07803(Tensor DI),f46ad7e9(node bench),4adc1a0d(Electron host),6008dc02(child_process fix)