Use single-thread ONNX runtime for mobile speech
Build and publish frontend / publish (push) Successful in 50s

This commit is contained in:
Ein Anderssono
2026-10-16 08:51:08 +02:00
parent aa832fd7cc
commit a4f89f2b82
9 changed files with 60 additions and 2 deletions
@@ -0,0 +1,13 @@
# ADR 0002: Run ONNX with one thread inside the Piper worker
## Decision
Configure ONNX Runtime Web to use one thread and stop backend initialization after 60 seconds. Piper already runs in a dedicated worker, so ONNX does not need to create another worker pool.
## Context
On iPad, the threaded ONNX WASM file downloaded fully, but the runtime did not report that initialization had completed. Nested worker startup and shared-memory setup are a likely compatibility boundary on mobile Safari. The available production assets currently include the threaded ONNX build.
## Consequences
This removes ONNX's additional worker pool and bounds initialization failures with a visible error. It may reduce inference throughput on desktop devices. CI and Kubernetes release verification must confirm that Piper still initializes and speaks on supported browsers; iPad acceptance remains a manual device check.
+1
View File
@@ -172,6 +172,7 @@
const preparing = message.startsWith('Förbereder svensk uppläsning.');
const phase = [
'Laddar WebAssembly-motorn för svensk uppläsning.',
'Startar WebAssembly-motorn efter nedladdning av',
'Hämtar information om den svenska röstmodellen.',
'Ansluter till servern för att hämta den svenska röstmodellen.',
'Läser in den svenska röstmodellen.',
+3
View File
@@ -9,6 +9,9 @@ export function trackDownloadProgress(response, {id, postMessage, asset = 'model
try {
const {done, value} = await reader.read();
if (done) {
if (asset === 'runtime') {
postMessage({id, type: 'progress', progress: {stage: 'initializing-runtime', file}});
}
controller.close();
return;
}
+7
View File
@@ -5,3 +5,10 @@ export function createOnnxWasmPaths(origin) {
return absoluteAssetRoot;
}
export function configureOnnxRuntime(onnxRuntime) {
if (!onnxRuntime?.env?.wasm) throw new TypeError('ONNX Runtime WebAssembly settings are unavailable.');
// Piper already runs in a worker. Avoid a second worker pool on mobile Safari.
onnxRuntime.env.wasm.numThreads = 1;
onnxRuntime.env.wasm.initTimeout = 60000;
}
+3 -1
View File
@@ -1,7 +1,8 @@
import {TtsSession} from '@mintplex-labs/piper-tts-web';
import * as onnxRuntime from 'onnxruntime-web';
import {trackDownloadProgress} from './piper-download-progress.mjs';
import {localPiperModelUrl} from './piper-model-routing.mjs';
import {createOnnxWasmPaths} from './piper-onnx-config.mjs';
import {configureOnnxRuntime, createOnnxWasmPaths} from './piper-onnx-config.mjs';
const VOICE_ID = 'sv_SE-nst-medium';
const ASSET_ROOT = '/static/piper-wasm-v1';
@@ -50,6 +51,7 @@ let activeRequestId = null;
async function getSession() {
if (!sessionPromise) {
configureOnnxRuntime(onnxRuntime);
sessionPromise = TtsSession.create({
voiceId: VOICE_ID,
wasmPaths: {
+2
View File
@@ -57,6 +57,8 @@ grep -q 'localPiperModelUrl' piper-worker.js
grep -q 'trackDownloadProgress' piper-worker.js
grep -q 'new ReadableStream' piper-download-progress.mjs
grep -q 'createOnnxWasmPaths(self.location.origin)' piper-worker.js
grep -q 'configureOnnxRuntime(onnxRuntime)' piper-worker.js
grep -q 'numThreads = 1' piper-onnx-config.mjs
grep -q '/static/piper-wasm-v1/onnx/' piper-onnx-config.mjs
grep -q 'PIPER_MODEL_ORIGIN' piper-model-routing.mjs
grep -q '/static/piper-wasm-v1/voices/' piper-model-routing.mjs
+22 -1
View File
@@ -34,5 +34,26 @@ test('model response progress works when content length is absent', async () =>
});
assert.equal((await tracked.arrayBuffer()).byteLength, 3);
assert.deepEqual(messages.map(message => message.progress), [{asset: 'runtime', file: 'runtime.wasm', loaded: 3, total: 0}]);
assert.deepEqual(messages
.map(message => message.progress)
.filter(progress => Number.isFinite(progress.loaded)),
[{asset: 'runtime', file: 'runtime.wasm', loaded: 3, total: 0}]);
});
test('runtime asset completion reports that initialization follows the download', async () => {
const {trackDownloadProgress} = await import('../piper-download-progress.mjs');
const response = new Response(new Uint8Array([1, 2]), {headers: {'content-length': '2'}});
const messages = [];
const tracked = trackDownloadProgress(response, {
id: 10,
asset: 'runtime',
file: 'ort-wasm-simd-threaded.wasm',
postMessage: message => messages.push(message),
});
await tracked.arrayBuffer();
assert.deepEqual(messages.at(-1).progress, {
stage: 'initializing-runtime',
file: 'ort-wasm-simd-threaded.wasm',
});
});
+8
View File
@@ -13,3 +13,11 @@ test('ONNX Runtime base URL rejects an invalid worker origin', async () => {
const {createOnnxWasmPaths} = await import('../piper-onnx-config.mjs');
assert.throws(() => createOnnxWasmPaths('undefined'), TypeError);
});
test('ONNX Runtime uses one thread and a bounded startup timeout', async () => {
const {configureOnnxRuntime} = await import('../piper-onnx-config.mjs');
const runtime = {env: {wasm: {numThreads: 0, initTimeout: 0}}};
configureOnnxRuntime(runtime);
assert.equal(runtime.env.wasm.numThreads, 1);
assert.equal(runtime.env.wasm.initTimeout, 60000);
});
+1
View File
@@ -280,6 +280,7 @@
'loading-runtime': 'Laddar WebAssembly-motorn för svensk uppläsning.',
'loading-runtime-assets': `Hämtar WebAssembly-filen ${event?.file || 'från servern'}.`,
'reading-runtime-assets': `Läser in WebAssembly-filen ${event?.file || ''}.`,
'initializing-runtime': `Startar WebAssembly-motorn efter nedladdning av ${event?.file || 'filen'}.`,
'loading-model-info': 'Hämtar information om den svenska röstmodellen.',
'loading-model': 'Ansluter till servern för att hämta den svenska röstmodellen.',
'reading-model': 'Läser in den svenska röstmodellen.',