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:
+37
-2
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user