| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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)) |
| } |
|
|
| |
| |
| 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)) |
| } |
| } |
|
|