@@ -120,33 +120,93 @@ export function startSpeechRecognition(language: Language = 'fr'): Promise<strin
120120}
121121
122122/**
123- * Speak text using Web Speech Synthesis
123+ * Get available voices, waiting for them to load if necessary
124124 */
125- export function speak ( text : string , language : Language = 'fr' ) : Promise < void > {
126- return new Promise ( ( resolve , reject ) => {
127- console . log ( '[TTS] speak() called with text length:' , text . length ) ;
128-
129- if ( ! isSpeechSynthesisSupported ( ) ) {
130- console . error ( '[TTS] Speech Synthesis not supported' ) ;
131- reject ( new Error ( 'Speech Synthesis not supported' ) ) ;
125+ function getVoices ( ) : Promise < SpeechSynthesisVoice [ ] > {
126+ return new Promise ( ( resolve ) => {
127+ const voices = window . speechSynthesis . getVoices ( ) ;
128+ if ( voices . length > 0 ) {
129+ resolve ( voices ) ;
132130 return ;
133131 }
134132
135- // Cancel any ongoing speech
136- console . log ( '[TTS] Cancelling any ongoing speech' ) ;
137- window . speechSynthesis . cancel ( ) ;
133+ // Voices not loaded yet, wait for them
134+ const handleVoicesChanged = ( ) => {
135+ const loadedVoices = window . speechSynthesis . getVoices ( ) ;
136+ console . log ( '[TTS] Voices loaded:' , loadedVoices . length ) ;
137+ resolve ( loadedVoices ) ;
138+ } ;
139+
140+ window . speechSynthesis . addEventListener ( 'voiceschanged' , handleVoicesChanged , { once : true } ) ;
141+
142+ // Fallback timeout in case voiceschanged never fires
143+ setTimeout ( ( ) => {
144+ const fallbackVoices = window . speechSynthesis . getVoices ( ) ;
145+ console . log ( '[TTS] Voices fallback:' , fallbackVoices . length ) ;
146+ resolve ( fallbackVoices ) ;
147+ } , 100 ) ;
148+ } ) ;
149+ }
150+
151+ /**
152+ * Find the best voice for a language
153+ */
154+ function findVoiceForLanguage ( voices : SpeechSynthesisVoice [ ] , lang : string ) : SpeechSynthesisVoice | null {
155+ // First try to find a voice that exactly matches the language
156+ let voice = voices . find ( v => v . lang === lang ) ;
157+ if ( voice ) return voice ;
158+
159+ // Try to find a voice that starts with the language code (e.g., 'fr' for 'fr-FR')
160+ const langPrefix = lang . split ( '-' ) [ 0 ] ;
161+ voice = voices . find ( v => v . lang . startsWith ( langPrefix ) ) ;
162+ if ( voice ) return voice ;
163+
164+ // Fall back to any available voice
165+ return voices [ 0 ] || null ;
166+ }
167+
168+ /**
169+ * Speak text using Web Speech Synthesis
170+ */
171+ export async function speak ( text : string , language : Language = 'fr' ) : Promise < void > {
172+ console . log ( '[TTS] speak() called with text length:' , text . length ) ;
173+
174+ if ( ! isSpeechSynthesisSupported ( ) ) {
175+ console . error ( '[TTS] Speech Synthesis not supported' ) ;
176+ throw new Error ( 'Speech Synthesis not supported' ) ;
177+ }
138178
139- // Remove markdown formatting that would be read aloud
140- const cleanedText = text . replace ( / \* \* / g , ' ') ;
141- console . log ( '[TTS] Cleaned text:' , cleanedText . substring ( 0 , 100 ) + '...' ) ;
179+ // Cancel any ongoing speech
180+ console . log ( '[TTS] Cancelling any ongoing speech ') ;
181+ window . speechSynthesis . cancel ( ) ;
142182
183+ // Get available voices
184+ const voices = await getVoices ( ) ;
185+ console . log ( '[TTS] Available voices:' , voices . map ( v => `${ v . name } (${ v . lang } )` ) . join ( ', ' ) ) ;
186+
187+ // Find a voice for the language
188+ const targetLang = SPEECH_LANGUAGE_MAP [ language ] ;
189+ const voice = findVoiceForLanguage ( voices , targetLang ) ;
190+ console . log ( '[TTS] Selected voice:' , voice ? `${ voice . name } (${ voice . lang } )` : 'none' ) ;
191+
192+ if ( ! voice ) {
193+ console . error ( '[TTS] No voice available for language:' , targetLang ) ;
194+ throw new Error ( `No voice available for language: ${ targetLang } ` ) ;
195+ }
196+
197+ // Remove markdown formatting that would be read aloud
198+ const cleanedText = text . replace ( / \* \* / g, '' ) ;
199+ console . log ( '[TTS] Cleaned text:' , cleanedText . substring ( 0 , 100 ) + '...' ) ;
200+
201+ return new Promise ( ( resolve , reject ) => {
143202 const utterance = new SpeechSynthesisUtterance ( cleanedText ) ;
144- utterance . lang = SPEECH_LANGUAGE_MAP [ language ] ;
203+ utterance . voice = voice ;
204+ utterance . lang = voice . lang ;
145205 utterance . rate = 1.0 ;
146206 utterance . pitch = 1.0 ;
147207 utterance . volume = 1.0 ;
148208
149- console . log ( '[TTS] Created utterance with lang:' , utterance . lang ) ;
209+ console . log ( '[TTS] Created utterance with voice:' , voice . name , ' lang:', utterance . lang ) ;
150210
151211 utterance . onstart = ( ) => {
152212 console . log ( '[TTS] Speech started' ) ;
@@ -168,7 +228,6 @@ export function speak(text: string, language: Language = 'fr'): Promise<void> {
168228 } ;
169229
170230 // Chrome bug workaround: small delay after cancel before speaking
171- // and resume in case synthesis is paused
172231 setTimeout ( ( ) => {
173232 console . log ( '[TTS] After delay - calling speechSynthesis.speak()' ) ;
174233 console . log ( '[TTS] speechSynthesis.speaking:' , window . speechSynthesis . speaking ) ;
0 commit comments