const version = document.querySelector('#frontend-version')?.textContent || 'dev'; const workerUrl = `/tts/piper-wasm-v1/piper-worker.js?v=${encodeURIComponent(version)}`; let worker; let requestId = 0; const pending = new Map(); function getWorker() { if (worker) return worker; // ONNX Runtime resolves its WASM loader relative to the worker module URL. // Keep the worker as ESM so import.meta.url remains available at runtime. worker = new Worker(workerUrl, {type: 'module'}); worker.onmessage = event => { const message = event.data || {}; const request = pending.get(message.id); if (!request) return; if (message.type === 'progress') { request.onProgress?.(message.progress); } else { pending.delete(message.id); if (message.type === 'audio') { 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 => { const details = [event.message, event.filename && `${event.filename}:${event.lineno || 0}:${event.colno || 0}`] .filter(Boolean) .join(' — '); const error = new Error(details ? `Piper-arbetaren kunde inte startas: ${details}` : 'Piper-arbetaren kunde inte startas.'); for (const request of pending.values()) request.reject(error); pending.clear(); worker?.terminate(); worker = null; }; worker.onmessageerror = () => { const error = new Error('Piper-arbetaren skickade ett oläsbart svar.'); for (const request of pending.values()) request.reject(error); pending.clear(); worker?.terminate(); worker = null; }; return worker; } window.GamePiper = { voiceId: 'sv_SE-nst-medium', synthesize(text, onProgress) { return new Promise((resolve, reject) => { const id = ++requestId; pending.set(id, {resolve, reject, onProgress}); try { getWorker().postMessage({id, text: String(text)}); } catch (error) { pending.delete(id); reject(error); } }); }, };