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;