Skip to content

Commit c89596d

Browse files
committed
fix: eliminate all synth.cancel() from TTS startup path - root cause of no sound
Root cause: synth.cancel() called inside unlockTTS() was permanently breaking Chrome's speech engine. The cancel->speak pattern causes Chrome to silently drop all subsequent speak() calls. Changes: - Removed unlockTTS() function entirely - Inlined warmup directly in startSession (no cancel) - Removed redundant re-unlock after getDisplayMedia - Warmup uses 'test' text with no voice/lang to avoid mismatch - Added diagnostic logging (onstart, voice count, synth state) - Extended warmup wait to 3s with graceful fallthrough
1 parent 777492f commit c89596d

1 file changed

Lines changed: 30 additions & 29 deletions

File tree

interview/proctored-room.html

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2921,32 +2921,31 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
29212921
// ══════════════════════════════════════════════════════════════
29222922
// Track whether TTS has been unlocked by a user gesture
29232923
let _ttsUnlocked = false;
2924-
2925-
// Unlock TTS with a real utterance (empty strings are silently ignored by Chrome)
2926-
function unlockTTS() {
2924+
async function startSession() {
2925+
// STEP 1: UNLOCK TTS ENGINE ON USER GESTURE (synchronous)
2926+
// Chrome requires speak() during a click handler callback.
2927+
// CRITICAL: NO synth.cancel() before speak() - it kills the engine
29272928
try {
2928-
state.voice.synth.cancel(); // clear any stale queue
2929-
const warmup = new SpeechSynthesisUtterance('.');
2930-
warmup.volume = 0.01; // near-silent but non-zero (volume 0 may not init audio pipeline)
2931-
warmup.rate = 10; // fastest possible
2929+
const synth = state.voice.synth;
2930+
const voices = synth.getVoices();
2931+
console.log('[TTS-Init] Voices:', voices.length,
2932+
'preferred:', state.voice.preferredVoice?.name || 'NONE',
2933+
'synth:', synth.speaking, synth.pending, synth.paused);
2934+
2935+
const warmup = new SpeechSynthesisUtterance('test');
2936+
warmup.volume = 0.01;
2937+
warmup.rate = 10;
29322938
warmup.pitch = 0.01;
2933-
if (state.voice.preferredVoice) warmup.voice = state.voice.preferredVoice;
2934-
warmup.lang = state.interviewLanguage || 'en-IN';
2935-
warmup.onend = () => { _ttsUnlocked = true; console.log('[TTS] ✓ Engine unlocked via warmup'); };
2936-
warmup.onerror = () => { console.warn('[TTS] Warmup utterance error — will retry'); };
2937-
state.voice.synth.speak(warmup);
2938-
console.log('[TTS] Warmup utterance dispatched (text=".", vol=0.01)');
2939+
// Intentionally NOT setting voice/lang on warmup
2940+
warmup.onstart = () => { console.log('[TTS-Init] Warmup STARTED'); };
2941+
warmup.onend = () => { _ttsUnlocked = true; console.log('[TTS-Init] Warmup ENDED - unlocked'); };
2942+
warmup.onerror = (e) => { console.warn('[TTS-Init] Warmup error:', e.error); _ttsUnlocked = true; };
2943+
synth.speak(warmup);
2944+
console.log('[TTS-Init] Warmup dispatched');
29392945
} catch (e) {
2940-
console.warn('[TTS] Warmup failed:', e);
2946+
console.error('[TTS-Init] Exception:', e);
2947+
_ttsUnlocked = true;
29412948
}
2942-
}
2943-
2944-
async function startSession() {
2945-
// ── CRITICAL: Warm up TTS engine on user gesture ──
2946-
// Chrome's autoplay policy requires the FIRST speechSynthesis.speak()
2947-
// to happen during a user gesture callback. Must use NON-EMPTY text
2948-
// (empty strings are silently ignored) with non-zero volume.
2949-
unlockTTS();
29502949

29512950
// Request screen share
29522951
if (!state.media.screen) {
@@ -2973,11 +2972,6 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
29732972
}
29742973
}
29752974

2976-
// Re-unlock TTS after getDisplayMedia (the system dialog breaks gesture context)
2977-
if (!_ttsUnlocked) {
2978-
console.log('[TTS] Re-unlocking after screen share dialog...');
2979-
unlockTTS();
2980-
}
29812975

29822976
document.getElementById('setupOverlay').style.display = 'none';
29832977
document.getElementById('interviewApp').style.display = 'flex';
@@ -3006,10 +3000,17 @@ <h1><span class="brand-simp">Simpatico</span><span class="brand-hr">HR</span></h
30063000
// Wait for TTS warmup to actually complete (onend sets _ttsUnlocked)
30073001
// The '.' utterance at rate=10 finishes in ~50ms, but we wait up to 2s as safety
30083002
const ttsWaitStart = Date.now();
3009-
while (!_ttsUnlocked && (Date.now() - ttsWaitStart) < 2000) {
3003+
while (!_ttsUnlocked && (Date.now() - ttsWaitStart) < 3000) {
30103004
await sleep(50);
30113005
}
3012-
console.log('[TTS] Warmup wait done, unlocked=', _ttsUnlocked, 'waited=', Date.now() - ttsWaitStart, 'ms');
3006+
console.log('[TTS-Init] Ready to speak. unlocked=', _ttsUnlocked,
3007+
'waited=', Date.now() - ttsWaitStart, 'ms',
3008+
'synth.speaking=', state.voice.synth.speaking,
3009+
'voices=', state.voice.synth.getVoices().length);
3010+
3011+
if (!_ttsUnlocked) {
3012+
console.warn('[TTS-Init] WARNING: warmup never completed. Speaking anyway...');
3013+
}
30133014

30143015
await aiSpeak(introQuestion);
30153016
}

0 commit comments

Comments
 (0)