Skip to content

Commit 132b6a0

Browse files
authored
Merge pull request #17 from sysdevrun/claude/tts-fix-01FCHndHG55trrh1Tv9jwsba
fix: Explicitly select voice for TTS to fix Chrome silent speech bug
2 parents ea24bfe + 321ac90 commit 132b6a0

1 file changed

Lines changed: 76 additions & 17 deletions

File tree

src/services/speech.ts

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)