From c5c0fb6b03e93794af086a5cc6f47d855cf18234 Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Fri, 16 Oct 2026 00:40:52 +0200 Subject: [PATCH] Offer speech retry after iOS playback errors --- index.html | 2 +- tests/voice.test.cjs | 10 ++++++++++ voice.js | 8 ++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 7106953..ab29c59 100644 --- a/index.html +++ b/index.html @@ -219,7 +219,7 @@ microphone.textContent = listening ? 'Avsluta mikrofon' : 'Starta mikrofon'; refreshMicrophone(); if (message) setVoiceStatus(message); - if (/Uppläsningen startade inte/.test(message)) showSpeechRecovery(message); + if (/Läs senaste repliken/.test(message)) showSpeechRecovery(message); playVoiceSignal(state === 'IDLE' && /Mikrofonen är avstängd/.test(message) ? 'STOPPED' : state); }, onTranscript(text) { diff --git a/tests/voice.test.cjs b/tests/voice.test.cjs index 2d1d370..7d1134e 100644 --- a/tests/voice.test.cjs +++ b/tests/voice.test.cjs @@ -165,6 +165,16 @@ test('speech start timeout tells the player how to recover when mobile playback assert.equal(h.synthesis.cancelled, cancelledBeforeRetry); }); +test('iOS not-allowed speech error offers an immediate gesture-based retry', () => { + const h = makeHarness(); + h.controller.speak('En replik'); + h.utterances[0].onerror({error: 'not-allowed'}); + assert.match(h.states.at(-1).message, /Safari blockerade uppläsningen/); + assert.match(h.states.at(-1).message, /Läs senaste repliken/); + assert.equal(h.controller.speak('Försök igen'), true); + assert.equal(h.synthesis.spoken.at(-1).text, 'Försök igen'); +}); + test('character voice identity gives stable distinct masculine, feminine, and neutral timbres', () => { const profile = GameVoice.characterVoiceProfile; const masculine = profile('masculine', 'character-1'); diff --git a/voice.js b/voice.js index 4f5915a..4edf212 100644 --- a/voice.js +++ b/voice.js @@ -240,7 +240,7 @@ currentUtterance = null; setState('IDLE'); }; - utterance.onerror = () => { + utterance.onerror = event => { if (token !== generation) return; if (speechTimer !== null) win.clearTimeout(speechTimer); if (speechStartTimer !== null) win.clearTimeout(speechStartTimer); @@ -248,7 +248,11 @@ speechStartTimer = null; currentUtterance = null; generation += 1; - setState('IDLE', 'Uppläsningen avbröts. Dialogen finns kvar som text.'); + const blocked = event?.error === 'not-allowed' || event?.error === 'service-not-allowed'; + const message = blocked + ? 'Safari blockerade uppläsningen. Tryck på Läs senaste repliken för att starta ljudet.' + : 'Uppläsningen avbröts. Tryck på Läs senaste repliken för att försöka igen. Dialogen finns kvar som text.'; + setState('IDLE', message); }; setState('SPEAKING', 'Läser upp dialog.'); currentUtterance = utterance;