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; worker = new Worker(workerUrl); 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 request.reject(new Error(message.error || 'Piper-arbetaren misslyckades.')); } }; 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); } }); }, };