Use single-thread ONNX runtime for mobile speech
Build and publish frontend / publish (push) Successful in 50s
Build and publish frontend / publish (push) Successful in 50s
This commit is contained in:
@@ -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.
|
||||||
@@ -172,6 +172,7 @@
|
|||||||
const preparing = message.startsWith('Förbereder svensk uppläsning.');
|
const preparing = message.startsWith('Förbereder svensk uppläsning.');
|
||||||
const phase = [
|
const phase = [
|
||||||
'Laddar WebAssembly-motorn för svensk uppläsning.',
|
'Laddar WebAssembly-motorn för svensk uppläsning.',
|
||||||
|
'Startar WebAssembly-motorn efter nedladdning av',
|
||||||
'Hämtar information om den svenska röstmodellen.',
|
'Hämtar information om den svenska röstmodellen.',
|
||||||
'Ansluter till servern för att hämta den svenska röstmodellen.',
|
'Ansluter till servern för att hämta den svenska röstmodellen.',
|
||||||
'Läser in den svenska röstmodellen.',
|
'Läser in den svenska röstmodellen.',
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export function trackDownloadProgress(response, {id, postMessage, asset = 'model
|
|||||||
try {
|
try {
|
||||||
const {done, value} = await reader.read();
|
const {done, value} = await reader.read();
|
||||||
if (done) {
|
if (done) {
|
||||||
|
if (asset === 'runtime') {
|
||||||
|
postMessage({id, type: 'progress', progress: {stage: 'initializing-runtime', file}});
|
||||||
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,10 @@ export function createOnnxWasmPaths(origin) {
|
|||||||
return absoluteAssetRoot;
|
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
@@ -1,7 +1,8 @@
|
|||||||
import {TtsSession} from '@mintplex-labs/piper-tts-web';
|
import {TtsSession} from '@mintplex-labs/piper-tts-web';
|
||||||
|
import * as onnxRuntime from 'onnxruntime-web';
|
||||||
import {trackDownloadProgress} from './piper-download-progress.mjs';
|
import {trackDownloadProgress} from './piper-download-progress.mjs';
|
||||||
import {localPiperModelUrl} from './piper-model-routing.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 VOICE_ID = 'sv_SE-nst-medium';
|
||||||
const ASSET_ROOT = '/static/piper-wasm-v1';
|
const ASSET_ROOT = '/static/piper-wasm-v1';
|
||||||
@@ -50,6 +51,7 @@ let activeRequestId = null;
|
|||||||
|
|
||||||
async function getSession() {
|
async function getSession() {
|
||||||
if (!sessionPromise) {
|
if (!sessionPromise) {
|
||||||
|
configureOnnxRuntime(onnxRuntime);
|
||||||
sessionPromise = TtsSession.create({
|
sessionPromise = TtsSession.create({
|
||||||
voiceId: VOICE_ID,
|
voiceId: VOICE_ID,
|
||||||
wasmPaths: {
|
wasmPaths: {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ grep -q 'localPiperModelUrl' piper-worker.js
|
|||||||
grep -q 'trackDownloadProgress' piper-worker.js
|
grep -q 'trackDownloadProgress' piper-worker.js
|
||||||
grep -q 'new ReadableStream' piper-download-progress.mjs
|
grep -q 'new ReadableStream' piper-download-progress.mjs
|
||||||
grep -q 'createOnnxWasmPaths(self.location.origin)' piper-worker.js
|
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 '/static/piper-wasm-v1/onnx/' piper-onnx-config.mjs
|
||||||
grep -q 'PIPER_MODEL_ORIGIN' piper-model-routing.mjs
|
grep -q 'PIPER_MODEL_ORIGIN' piper-model-routing.mjs
|
||||||
grep -q '/static/piper-wasm-v1/voices/' piper-model-routing.mjs
|
grep -q '/static/piper-wasm-v1/voices/' piper-model-routing.mjs
|
||||||
|
|||||||
@@ -34,5 +34,26 @@ test('model response progress works when content length is absent', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal((await tracked.arrayBuffer()).byteLength, 3);
|
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',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,3 +13,11 @@ test('ONNX Runtime base URL rejects an invalid worker origin', async () => {
|
|||||||
const {createOnnxWasmPaths} = await import('../piper-onnx-config.mjs');
|
const {createOnnxWasmPaths} = await import('../piper-onnx-config.mjs');
|
||||||
assert.throws(() => createOnnxWasmPaths('undefined'), TypeError);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -280,6 +280,7 @@
|
|||||||
'loading-runtime': 'Laddar WebAssembly-motorn för svensk uppläsning.',
|
'loading-runtime': 'Laddar WebAssembly-motorn för svensk uppläsning.',
|
||||||
'loading-runtime-assets': `Hämtar WebAssembly-filen ${event?.file || 'från servern'}.`,
|
'loading-runtime-assets': `Hämtar WebAssembly-filen ${event?.file || 'från servern'}.`,
|
||||||
'reading-runtime-assets': `Läser in WebAssembly-filen ${event?.file || ''}.`,
|
'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-info': 'Hämtar information om den svenska röstmodellen.',
|
||||||
'loading-model': 'Ansluter till servern för att hämta 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.',
|
'reading-model': 'Läser in den svenska röstmodellen.',
|
||||||
|
|||||||
Reference in New Issue
Block a user