fix: robust browser TTS voice selection and Chrome cancel+speak workaround

_speakBrowser() was silently failing in two scenarios:

1. Chrome cancel+speak bug: calling speechSynthesis.speak() immediately
   after cancel() is silently dropped in Chrome. Fixed by adding a 50ms
   setTimeout between cancel() and speak().

2. No voice fallback: when tts_voice preference is empty (never saved)
   and the browser has no default 'it-IT' voice, speech fails silently.
   Now tries: preferred by name → first Italian voice → first any voice
   → lang-only as last resort.

3. Async voice loading: Chrome/Android load voices asynchronously.
   When getVoices() returns empty, now waits for onvoiceschanged (with
   a 500ms safety timeout) before speaking.
This commit is contained in:
dadaloop82
2026-05-25 17:55:04 +00:00
parent baed815a48
commit 1a6e0c87ce
+37 -2
View File
@@ -14100,19 +14100,54 @@ function _speakBrowser(text) {
// ── Web Speech API (desktop / mobile browser) ──────────────────────
if (!window.speechSynthesis) return;
const _doSpeak = () => {
window.speechSynthesis.cancel();
const utt = new SpeechSynthesisUtterance(text);
utt.rate = rate;
utt.pitch = pitch;
const voices = window.speechSynthesis.getVoices();
const preferred = voices.find(v => v.name === s.tts_voice);
// 1. User-selected voice by name
const preferred = s.tts_voice ? voices.find(v => v.name === s.tts_voice) : null;
if (preferred) {
utt.voice = preferred;
utt.lang = preferred.lang;
} else {
// 2. First Italian voice as fallback (avoids silent-failure on browsers with no 'it-IT' default)
const itVoice = voices.find(v => v.lang && v.lang.startsWith('it'));
if (itVoice) {
utt.voice = itVoice;
utt.lang = itVoice.lang;
} else if (voices.length > 0) {
// 3. Any available voice
utt.voice = voices[0];
utt.lang = voices[0].lang || 'it-IT';
} else {
// 4. No voices loaded yet — set lang and let the browser decide
utt.lang = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-US' : 'it-IT';
}
window.speechSynthesis.speak(utt);
}
// Chrome quirk: cancel() + immediate speak() is silently dropped — 50 ms gap fixes it
setTimeout(() => window.speechSynthesis.speak(utt), 50);
};
// If voices haven't loaded yet (async in Chrome/Android), wait once then speak
if (!window.speechSynthesis.getVoices().length) {
const _onReady = () => {
window.speechSynthesis.onvoiceschanged = null;
_doSpeak();
};
window.speechSynthesis.onvoiceschanged = _onReady;
// Safety timeout: fire anyway after 500 ms if onvoiceschanged never fires
setTimeout(() => {
if (window.speechSynthesis.onvoiceschanged === _onReady) {
window.speechSynthesis.onvoiceschanged = null;
_doSpeak();
}
}, 500);
} else {
_doSpeak();
}
}
async function testTTS() {