@@ -2701,14 +2701,13 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
27012701 const langBase = lang . split ( '-' ) [ 0 ] ; // e.g. 'hi', 'ta', 'ml', 'en'
27022702
27032703 console . log ( `[Voice] Looking for voice: lang=${ lang } , base=${ langBase } , available=${ voices . length } voices` ) ;
2704+ console . log ( `[Voice] All voices:` , voices . map ( v => `${ v . name } (${ v . lang } )` ) . join ( ', ' ) ) ;
27042705
27052706 // For non-English languages, find a matching voice first
27062707 if ( langBase !== 'en' ) {
2707- // Log all matching voices for debugging
27082708 const matchingVoices = voices . filter ( v => v . lang . startsWith ( langBase ) ) ;
27092709 console . log ( `[Voice] Found ${ matchingVoices . length } voices for '${ langBase } ':` , matchingVoices . map ( v => `${ v . name } (${ v . lang } )` ) ) ;
27102710
2711- // Prefer Google voices (usually higher quality for Indian languages)
27122711 const googleMatch = matchingVoices . find ( v => v . name . toLowerCase ( ) . includes ( 'google' ) ) ;
27132712 if ( googleMatch ) {
27142713 state . voice . preferredVoice = googleMatch ;
@@ -2727,20 +2726,64 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
27272726 console . log ( `[Voice] ✓ Selected base match: ${ baseMatch . name } (${ baseMatch . lang } )` ) ;
27282727 return ;
27292728 }
2730- console . warn ( `[Voice] ⚠️ No voice found for ${ lang } ! Malayalam/other Indic TTS requires Chrome. Falling back to English.` ) ;
2729+ console . warn ( `[Voice] ⚠️ No voice found for ${ lang } ! Falling back to neutral English.` ) ;
27312730 }
27322731
2733- // English preferred voices
2734- const preferred = [
2735- 'Google UK English Female' , 'Google US English' , 'Samantha' ,
2736- 'Microsoft Zira' , 'Karen' , 'Moira' , 'Fiona' ,
2737- 'Google UK English Male' , 'Microsoft David' , 'Daniel'
2732+ // ── NEUTRAL ENGLISH voice selection ──
2733+ // Priority: Clean US/UK/AU accents. Explicitly avoid Indian-accented voices
2734+ // which sound unprofessional in interviews.
2735+ const BLOCKED_VOICES = [
2736+ 'ravi' , 'hemant' , 'heera' , 'kalpana' , 'prabhat' , // Microsoft Indian English
2737+ 'google हिन्दी' , 'google മലയാളം' , 'google தமிழ்' , // Google Indic
27382738 ] ;
2739- for ( const name of preferred ) {
2739+
2740+ // Tier 1: Premium quality voices (natural sounding)
2741+ const tier1 = [
2742+ 'Google UK English Female' , 'Google US English' ,
2743+ 'Microsoft Aria' , 'Microsoft Jenny' , 'Microsoft Guy' ,
2744+ 'Samantha' , 'Karen' , 'Moira' , 'Tessa' ,
2745+ ] ;
2746+ // Tier 2: Good quality voices
2747+ const tier2 = [
2748+ 'Google UK English Male' , 'Microsoft Zira' , 'Microsoft David' ,
2749+ 'Daniel' , 'Fiona' , 'Alex' , 'Victoria' , 'Ava' ,
2750+ 'Microsoft Mark' , 'Microsoft Catherine' ,
2751+ ] ;
2752+
2753+ for ( const name of [ ...tier1 , ...tier2 ] ) {
27402754 const v = voices . find ( v => v . name . includes ( name ) ) ;
2741- if ( v ) { state . voice . preferredVoice = v ; return ; }
2755+ if ( v ) {
2756+ state . voice . preferredVoice = v ;
2757+ console . log ( `[Voice] ✓ Selected preferred: ${ v . name } (${ v . lang } )` ) ;
2758+ return ;
2759+ }
2760+ }
2761+
2762+ // Tier 3: Any en-US or en-GB voice that isn't blocked
2763+ const neutralEN = voices . find ( v =>
2764+ ( v . lang === 'en-US' || v . lang === 'en-GB' || v . lang === 'en-AU' ) &&
2765+ ! BLOCKED_VOICES . some ( b => v . name . toLowerCase ( ) . includes ( b ) )
2766+ ) ;
2767+ if ( neutralEN ) {
2768+ state . voice . preferredVoice = neutralEN ;
2769+ console . log ( `[Voice] ✓ Selected neutral EN: ${ neutralEN . name } (${ neutralEN . lang } )` ) ;
2770+ return ;
27422771 }
2772+
2773+ // Tier 4: Any English voice at all (except blocked)
2774+ const anyEN = voices . find ( v =>
2775+ v . lang . startsWith ( 'en' ) &&
2776+ ! BLOCKED_VOICES . some ( b => v . name . toLowerCase ( ) . includes ( b ) )
2777+ ) ;
2778+ if ( anyEN ) {
2779+ state . voice . preferredVoice = anyEN ;
2780+ console . log ( `[Voice] ✓ Selected fallback EN: ${ anyEN . name } (${ anyEN . lang } )` ) ;
2781+ return ;
2782+ }
2783+
2784+ // Last resort: any English voice
27432785 state . voice . preferredVoice = voices . find ( v => v . lang . startsWith ( 'en' ) ) || voices [ 0 ] || null ;
2786+ console . log ( `[Voice] ✓ Last resort: ${ state . voice . preferredVoice ?. name || 'NONE' } ` ) ;
27442787 }
27452788
27462789 // ══════════════════════════════════════════════════════════════
@@ -3039,6 +3082,7 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
30393082 // Uses Cloudflare Workers AI @cf /deepgram/aura-1 to generate audio
30403083 // Falls back to browser speechSynthesis if cloud TTS fails
30413084 let _cloudTTSAvailable = null ; // null = untested, true/false = tested
3085+ let _cloudTTSFailCount = 0 ; // Track consecutive failures (retry after 3 questions)
30423086
30433087 async function aiSpeakCloud ( text ) {
30443088 const cleanText = text . replace ( / \* \* / g, '' ) . replace ( / \n / g, ' ' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
@@ -3146,18 +3190,35 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
31463190 }
31473191 }
31483192
3149- // ── PRIMARY: Try Cloud TTS ──
3150- if ( _cloudTTSAvailable !== false ) {
3193+ // -- PRIMARY: Try Cloud TTS --
3194+ // Deepgram Aura is ENGLISH-ONLY. For Malayalam, Hindi, French, German, etc.
3195+ // skip cloud TTS and use browser speechSynthesis which has native voices.
3196+ const _ttsLangBase = ( state . interviewLanguage || 'en' ) . split ( '-' ) [ 0 ] ;
3197+ const _isEnglishInterview = ( _ttsLangBase === 'en' ) ;
3198+
3199+ if ( ! _isEnglishInterview ) {
3200+ console . log ( '[TTS] Non-English interview (' + state . interviewLanguage + '), skipping Cloud TTS -> using browser voices' ) ;
3201+ }
3202+
3203+ if ( _isEnglishInterview && _cloudTTSAvailable !== false ) {
31513204 try {
31523205 console . log ( '[TTS] Trying Cloud TTS...' ) ;
31533206 await aiSpeakCloud ( cleanText ) ;
31543207 _cloudTTSAvailable = true ;
3208+ _cloudTTSFailCount = 0 ;
31553209 console . log ( '[TTS] Cloud TTS succeeded' ) ;
31563210 finishSpeaking ( ) ;
31573211 return ;
31583212 } catch ( cloudErr ) {
31593213 console . warn ( '[TTS] Cloud TTS failed, falling back to browser:' , cloudErr . message ) ;
3160- _cloudTTSAvailable = false ;
3214+ _cloudTTSFailCount ++ ;
3215+ // Don't permanently disable — retry after 3 consecutive failures
3216+ if ( _cloudTTSFailCount >= 3 ) {
3217+ _cloudTTSAvailable = false ;
3218+ console . warn ( '[TTS] Cloud TTS disabled after 3 failures. Will retry periodically.' ) ;
3219+ // Re-enable after 60 seconds to try again
3220+ setTimeout ( ( ) => { _cloudTTSAvailable = null ; _cloudTTSFailCount = 0 ; } , 60000 ) ;
3221+ }
31613222 }
31623223 }
31633224
@@ -3195,7 +3256,12 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
31953256 const u = new SpeechSynthesisUtterance ( chunkText ) ;
31963257 state . voice . keepAlive . push ( u ) ;
31973258
3198- u . lang = state . interviewLanguage || 'en-IN' ;
3259+ // Use the correct language for speech synthesis:
3260+ // - Non-English interview: use interview language for proper accent
3261+ // - English interview: use the preferred voice's lang (en-US/en-GB) for clean accent
3262+ u . lang = _isEnglishInterview
3263+ ? ( state . voice . preferredVoice ?. lang || 'en-US' )
3264+ : ( state . interviewLanguage || 'en-US' ) ;
31993265 if ( state . voice . preferredVoice ) u . voice = state . voice . preferredVoice ;
32003266 u . rate = 0.92 ;
32013267 u . pitch = 1.0 ;
0 commit comments