Compare commits

...
2 Commits
Author SHA1 Message Date
Gitea Actions fa11314b23 Release 0.1.61 [auto-version-bump] 2026-10-17 16:38:43 +02:00
Ein Anderssono c9b14e0726 Fix Piper memory pressure on iOS
Build and publish frontend / publish (push) Successful in 49s
2026-10-17 16:38:16 +02:00
8 changed files with 67 additions and 9 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.60 0.1.61
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: ai-spel-frontend name: ai-spel-frontend
description: Static web frontend for the Swedish life simulator description: Static web frontend for the Swedish life simulator
type: application type: application
version: 0.1.60 version: 0.1.61
appVersion: "0.1.60" appVersion: "0.1.61"
@@ -0,0 +1,13 @@
# ADR 0003: Limit Piper WASM to one thread and reset failed workers
## Decision
The Piper worker reports one available processor to Piper and configures ONNX Runtime for one thread. If a synthesis request fails, the browser client terminates that worker; the next player retry creates a fresh worker and ONNX module.
## Context
Piper TTS Web 1.0.3 sets `env.wasm.numThreads` from `navigator.hardwareConcurrency` during `TtsSession.init()`, after the application had configured ONNX Runtime. On iPad this could undo the single-thread setting and request a larger WASM worker pool. The library also retains a singleton session if ONNX initialization fails, so retrying in the same worker can reuse a failed initialization.
## Consequences
The one-thread limit reduces parallel inference but lowers memory pressure on mobile Safari. Failed initialization no longer poisons future attempts in the same worker. This cannot guarantee that every device has enough memory for the 63 MB medium voice model; the CI release and Kubernetes checks validate packaging and deployment, while actual iPad memory behavior still requires device verification.
+17
View File
@@ -7,6 +7,23 @@ export function createOnnxWasmPaths(origin) {
export function configureOnnxRuntime(onnxRuntime) { export function configureOnnxRuntime(onnxRuntime) {
if (!onnxRuntime?.env?.wasm) throw new TypeError('ONNX Runtime WebAssembly settings are unavailable.'); if (!onnxRuntime?.env?.wasm) throw new TypeError('ONNX Runtime WebAssembly settings are unavailable.');
// Piper 1.0.3 overwrites this value in TtsSession.init() with
// navigator.hardwareConcurrency. Override that source as well, otherwise an
// iPad may start a large WASM worker pool despite this setting.
const navigatorObject = globalThis.navigator;
if (navigatorObject && navigatorObject.hardwareConcurrency !== 1) {
try {
Object.defineProperty(navigatorObject, 'hardwareConcurrency', {
configurable: true,
value: 1,
});
} catch (_) {
// The check below converts an unsupported override into a clear error.
}
}
if (navigatorObject && navigatorObject.hardwareConcurrency !== 1) {
throw new Error('Piper could not be restricted to one processor thread.');
}
// Piper already runs in a worker. Avoid a second worker pool on mobile Safari. // Piper already runs in a worker. Avoid a second worker pool on mobile Safari.
onnxRuntime.env.wasm.numThreads = 1; onnxRuntime.env.wasm.numThreads = 1;
onnxRuntime.env.wasm.initTimeout = 60000; onnxRuntime.env.wasm.initTimeout = 60000;
+13 -2
View File
@@ -17,8 +17,19 @@ function getWorker() {
request.onProgress?.(message.progress); request.onProgress?.(message.progress);
} else { } else {
pending.delete(message.id); pending.delete(message.id);
if (message.type === 'audio') request.resolve(new Blob([message.audio], {type: 'audio/x-wav'})); if (message.type === 'audio') {
else request.reject(new Error(message.error || 'Piper-arbetaren misslyckades.')); request.resolve(new Blob([message.audio], {type: 'audio/x-wav'}));
} else {
const error = new Error(message.error || 'Piper-arbetaren misslyckades.');
request.reject(error);
// TtsSession keeps a singleton even when ONNX initialization fails.
// Discard the whole worker so a player retry starts with a clean ONNX
// module and does not reuse a poisoned WASM initialization.
for (const queued of pending.values()) queued.reject(error);
pending.clear();
worker?.terminate();
worker = null;
}
} }
}; };
worker.onerror = event => { worker.onerror = event => {
+2
View File
@@ -59,6 +59,8 @@ 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 'configureOnnxRuntime(onnxRuntime)' piper-worker.js
grep -q 'numThreads = 1' piper-onnx-config.mjs grep -q 'numThreads = 1' piper-onnx-config.mjs
grep -q "hardwareConcurrency'" piper-onnx-config.mjs
grep -q 'Piper could not be restricted to one processor thread' 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
+13 -4
View File
@@ -14,10 +14,19 @@ test('ONNX Runtime base URL rejects an invalid worker origin', async () => {
assert.throws(() => createOnnxWasmPaths('undefined'), TypeError); assert.throws(() => createOnnxWasmPaths('undefined'), TypeError);
}); });
test('ONNX Runtime uses one thread and a bounded startup timeout', async () => { test('Piper and ONNX Runtime stay on one thread even though Piper overwrites ONNX settings', async () => {
const {configureOnnxRuntime} = await import('../piper-onnx-config.mjs'); const {configureOnnxRuntime} = await import('../piper-onnx-config.mjs');
const navigatorObject = {hardwareConcurrency: 8};
const previousNavigator = globalThis.navigator;
Object.defineProperty(globalThis, 'navigator', {configurable: true, value: navigatorObject});
const runtime = {env: {wasm: {numThreads: 0, initTimeout: 0}}}; const runtime = {env: {wasm: {numThreads: 0, initTimeout: 0}}};
configureOnnxRuntime(runtime); try {
assert.equal(runtime.env.wasm.numThreads, 1); configureOnnxRuntime(runtime);
assert.equal(runtime.env.wasm.initTimeout, 60000); assert.equal(navigatorObject.hardwareConcurrency, 1);
assert.equal(runtime.env.wasm.numThreads, 1);
assert.equal(runtime.env.wasm.initTimeout, 60000);
} finally {
if (previousNavigator === undefined) delete globalThis.navigator;
else Object.defineProperty(globalThis, 'navigator', {configurable: true, value: previousNavigator});
}
}); });
+6
View File
@@ -11,3 +11,9 @@ test('Piper worker is built and started as an ES module', async () => {
assert.match(buildScript, /outfile: join\(workerRoot, 'piper-worker\.js'\),\s*bundle: true,\s*format: 'esm'/); assert.match(buildScript, /outfile: join\(workerRoot, 'piper-worker\.js'\),\s*bundle: true,\s*format: 'esm'/);
assert.match(runtime, /new Worker\(workerUrl, \{type: 'module'\}\)/); assert.match(runtime, /new Worker\(workerUrl, \{type: 'module'\}\)/);
}); });
test('Piper runtime discards a worker after a failed initialization for a clean retry', async () => {
const runtime = await readFile(join(__dirname, '..', 'piper-runtime.js'), 'utf8');
assert.match(runtime, /TtsSession keeps a singleton even when ONNX initialization fails/);
assert.match(runtime, /worker\?\.terminate\(\);\s*worker = null;/);
});