Prevent duplicate voice-mode rearm after watchdog recovery

This commit is contained in:
Rod Boev committed 2026-06-11 18:58:12 -04:00
1 parent f4a6544121
commit 09f19d1233
2 files changed
+14

No files matched your search

+9
View File
@@ -855,6 +855,7 @@ window._micPendingSend=window._micPendingSend||false;
let _voiceModeThinkingSid=null;
let _browserTtsKeepAlive=null;
let _browserTtsWatchdog=null;
let _browserTtsSuppressNextErrorRearm=false;
const SILENCE_MS=1800; // auto-send after 1.8s silence
function _clearBrowserTtsRecovery(){
@@ -870,11 +871,13 @@ window._micPendingSend=window._micPendingSend||false;
function _armBrowserTtsRecovery(clean, rate){
_clearBrowserTtsRecovery();
_browserTtsSuppressNextErrorRearm=false;
const safeRate=(Number.isFinite(rate)&&rate>0)?rate:1;
// Chromium can drop utter.onend on later turns, so force a recovery path.
const watchdogMs=Math.max(4000,Math.round((String(clean||'').length/(12*safeRate))*1000)+10000);
_browserTtsWatchdog=setTimeout(()=>{
if(!_voiceModeActive||_voiceModeState!=='speaking') return;
_browserTtsSuppressNextErrorRearm=true;
try{ speechSynthesis.cancel(); }catch(_){}
_clearBrowserTtsRecovery();
_startListening();
@@ -1095,12 +1098,17 @@ window._micPendingSend=window._micPendingSend||false;
if(!isNaN(savedPitch)) utter.pitch=Math.min(2,Math.max(0,savedPitch));
utter.onend=()=>{
_browserTtsSuppressNextErrorRearm=false;
_clearBrowserTtsRecovery();
// After speaking, go back to listening
if(_voiceModeActive) setTimeout(()=>_startListening(),500);
};
utter.onerror=()=>{
_clearBrowserTtsRecovery();
if(_browserTtsSuppressNextErrorRearm){
_browserTtsSuppressNextErrorRearm=false;
return;
}
if(_voiceModeActive) setTimeout(()=>_startListening(),1000);
};
@@ -1167,6 +1175,7 @@ window._micPendingSend=window._micPendingSend||false;
_voiceModeActive=false;
_voiceModeState='idle';
_voiceModeThinkingSid=null;
_browserTtsSuppressNextErrorRearm=false;
modeBtn.classList.remove('active');
_setButtonTooltip(modeBtn, t('voice_mode_toggle'));
bar.style.display='none';
@@ -9,6 +9,7 @@ def test_boot_js_declares_browser_tts_recovery_helpers():
src = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
assert "let _browserTtsKeepAlive=null;" in src
assert "let _browserTtsWatchdog=null;" in src
assert "let _browserTtsSuppressNextErrorRearm=false;" in src
assert "function _clearBrowserTtsRecovery()" in src
assert "function _armBrowserTtsRecovery(clean, rate)" in src
@@ -20,6 +21,7 @@ def test_browser_tts_watchdog_rearms_listening_if_onend_drops():
arm_body = src[arm_idx : arm_idx + 1400]
assert "_browserTtsWatchdog=setTimeout" in arm_body
assert "_voiceModeState!=='speaking'" in arm_body
assert "_browserTtsSuppressNextErrorRearm=true;" in arm_body
assert "speechSynthesis.cancel()" in arm_body
assert "_startListening();" in arm_body
assert "_browserTtsKeepAlive=setInterval" in arm_body
@@ -37,6 +39,8 @@ def test_browser_tts_callbacks_and_deactivate_clear_recovery_handles():
assert speak_body.count("_clearBrowserTtsRecovery();") >= 2, (
"Both browser TTS completion callbacks must clear watchdog/keep-alive handles."
)
assert "_browserTtsSuppressNextErrorRearm=false;" in speak_body
assert "if(_browserTtsSuppressNextErrorRearm){" in speak_body
assert "_armBrowserTtsRecovery(clean, utter.rate);" in speak_body
deactivate_match = re.search(
@@ -49,6 +53,7 @@ def test_browser_tts_callbacks_and_deactivate_clear_recovery_handles():
assert "_clearBrowserTtsRecovery();" in deactivate_body, (
"_deactivate() must clear browser TTS watchdog/keep-alive handles."
)
assert "_browserTtsSuppressNextErrorRearm=false;" in deactivate_body
def test_edge_audio_branch_stays_separate():