// Shared browser text-to-speech helper (French voice). // // `getVoices()` is unreliable on first call (especially Chrome): the voice // list often loads asynchronously, so an early call returns `[]`. We cache // the list, refresh it on the `voiceschanged` event, and pick a French voice // once it's available — `lang` is always set to 'fr-FR' regardless, so the // browser still defaults sanely if no matching voice is found. // // Beginners need speech slowed down, and we prefer a female voice (common // names across Chrome/macOS/Edge French voice packs). const SPEECH_RATE = 0.85 const FEMALE_VOICE_NAMES = [ 'amélie', 'amelie', 'audrey', 'marie', 'julie', 'hortense', 'céline', 'celine', 'virginie', 'chantal', 'aurélie', 'aurelie', 'google français', 'female', ] let cachedVoices = [] function refreshVoices() { if (!window.speechSynthesis) return const voices = window.speechSynthesis.getVoices() if (voices.length) cachedVoices = voices } if (window.speechSynthesis) { refreshVoices() window.speechSynthesis.addEventListener('voiceschanged', refreshVoices) } function isFemaleVoice(voice) { const name = voice.name.toLowerCase() return FEMALE_VOICE_NAMES.some((n) => name.includes(n)) } function frenchVoice() { refreshVoices() const frFR = cachedVoices.filter((v) => v.lang === 'fr-FR') const frAny = cachedVoices.filter((v) => v.lang && v.lang.startsWith('fr')) return ( frFR.find(isFemaleVoice) || frAny.find(isFemaleVoice) || frFR[0] || frAny[0] || null ) } function makeUtterance(text) { const u = new SpeechSynthesisUtterance(text) u.lang = 'fr-FR' u.rate = SPEECH_RATE const voice = frenchVoice() if (voice) u.voice = voice return u } export function speak(text) { if (!text || !window.speechSynthesis) return window.speechSynthesis.cancel() window.speechSynthesis.speak(makeUtterance(text)) } // Speak several lines back-to-back (e.g. consecutive dialogue turns from the // same speaker) without one cancelling the next. export function speakAll(texts) { const list = (texts || []).filter(Boolean) if (!list.length || !window.speechSynthesis) return window.speechSynthesis.cancel() for (const text of list) { window.speechSynthesis.speak(makeUtterance(text)) } }