Skip to content

Commit 4569e74

Browse files
committed
fix: TTS accent quality - skip cloud TTS for non-English, prefer neutral EN voices, restrict BYOK save to admin roles
1 parent 1d6dede commit 4569e74

2 files changed

Lines changed: 81 additions & 15 deletions

File tree

backend/simpatico-ats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7343,7 +7343,7 @@ async function handleBYOKCacheClear(request, env, ctx) {
73437343
* Body: { ai_provider, ai_api_key, ai_base_url, ai_model }
73447344
*/
73457345
async function handleBYOKConfigSave(request, env, ctx) {
7346-
requireAuth(ctx);
7346+
requireRole(ctx, "admin", "superadmin", "company_admin", "hr_manager", "employer");
73477347
const tenantId = ctx.tenantId;
73487348
if (!tenantId || tenantId === "default") {
73497349
throw new ValidationError("Tenant ID required to save AI config");

interview/proctored-room.html

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

Comments
 (0)