From 2ddce52c9adbf7b6dfc551aced6981a39e314e7d Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 31 May 2026 19:34:00 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20voice=20engine=20rewrite=20=E2=80=94=20c?= =?UTF-8?q?ontinuous=3Dfalse=20restart-per-utterance,=20=5FscheduleRecStar?= =?UTF-8?q?t,=2012s=20heartbeat,=20clean=20TTS=20handoff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public_html/index.html | 66 +++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/public_html/index.html b/public_html/index.html index 61b717b..1645441 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -1012,13 +1012,21 @@ function showApp(name, greeting, silent = false) { exitVoiceMode(); } }, 60000); - // Watchdog: reset isSpeaking if stuck (TTS error left it true) + // Watchdog: reset isSpeaking if stuck; heartbeat keeps mic alive setInterval(() => { if (isSpeaking && !_ttsAudio && !window.speechSynthesis?.speaking) { isSpeaking = false; - if (isListening) try { recognition?.start(); } catch(_) {} + if (isListening) _scheduleRecStart(200); } - }, 5000); + }, 4000); + // Heartbeat: if mic should be on but recognition has gone quiet, nudge it + setInterval(() => { + if (isListening && !isSpeaking) { + try { + recognition.start(); // throws if already running — that's fine + } catch(_) {} + } + }, 12000); startListening(); loadNetwork(); loadHA(); @@ -1759,32 +1767,29 @@ function initVoice() { return; } recognition = new SR(); - recognition.continuous = true; + recognition.continuous = false; // restart-per-utterance — most reliable in Chrome recognition.interimResults = false; recognition.lang = 'en-US'; + recognition.maxAlternatives = 1; recognition.onresult = (e) => { - if (isSpeaking) return; // ignore mic during TTS playback - const result = e.results[e.results.length - 1]; - if (!result.isFinal) return; - const transcript = result[0].transcript.trim(); + if (isSpeaking) return; + const transcript = (e.results[0][0].transcript || '').trim(); if (!transcript) return; const lc = transcript.toLowerCase(); if (!voiceMode) { - // Sleeping — full wake phrase required if (WAKE_PHRASES.some(p => lc.includes(p))) enterVoiceMode(); } else if (!voiceMuted) { - // Awake — "Jarvis " triggers command; active window allows free speech - voiceLastCmd = Date.now(); // any detected speech resets 30-min sleep timer + voiceLastCmd = Date.now(); const inWindow = voiceActive > 0 && (Date.now() - voiceActive) < VOICE_ACTIVE_MS; let cmd = null; if (lc.startsWith(CMD_PREFIX)) { cmd = transcript.substring(CMD_PREFIX.length).trim(); } else if (inWindow) { - cmd = transcript; // active window: no prefix needed + cmd = transcript; } if (cmd) { - voiceActive = Date.now(); // reset 17s window + voiceActive = Date.now(); document.getElementById('textInput').value = cmd; sendMessage(); } @@ -1792,9 +1797,9 @@ function initVoice() { }; recognition.onend = () => { - // Only restart when not speaking — _resumeMic() handles restart after TTS + // Restart immediately unless TTS is playing or mic is off if (isListening && !isSpeaking) { - setTimeout(() => { try { recognition.start(); } catch(_) {} }, 150); + _scheduleRecStart(100); } }; @@ -1808,7 +1813,7 @@ function initVoice() { updateMicBtn(); addMessage('system', 'No microphone detected. Please connect a microphone and try again.'); } - // no-speech and aborted are normal — onend will restart + // no-speech / aborted / network: onend will fire and restart }; } @@ -1861,6 +1866,16 @@ function toggleVoice() { } } +let _recTimer = null; +function _scheduleRecStart(ms = 100) { + clearTimeout(_recTimer); + _recTimer = setTimeout(() => { + if (isListening && !isSpeaking) { + try { recognition.start(); } catch(_) {} + } + }, ms); +} + function startListening() { if (!recognition) { if (!window.isSecureContext) { @@ -1871,7 +1886,7 @@ function startListening() { return; } isListening = true; - try { recognition.start(); } catch(_) {} + _scheduleRecStart(50); } function stopListening() { @@ -1879,6 +1894,7 @@ function stopListening() { voiceMode = false; voiceMuted = false; updateMicBtn(); + clearTimeout(_recTimer); try { recognition.abort(); } catch(_) {} } @@ -1917,13 +1933,8 @@ async function speak(text) { const _resumeMic = () => { isSpeaking = false; reactor?.classList.remove('speaking'); - if (isListening) { - // Abort any stale session then restart cleanly - setTimeout(() => { - try { recognition?.abort(); } catch(_) {} - setTimeout(() => { try { recognition?.start(); } catch(_) {} }, 150); - }, 200); - } + // onend will fire from the abort we did before TTS, and restart cleanly + if (isListening) _scheduleRecStart(400); }; try { const res = await fetch('/api/tts', { @@ -1956,12 +1967,7 @@ function _speakFallback(text) { utter.onend = () => { reactor?.classList.remove('speaking'); isSpeaking = false; - if (isListening) { - setTimeout(() => { - try { recognition?.abort(); } catch(_) {} - setTimeout(() => { try { recognition?.start(); } catch(_) {} }, 150); - }, 200); - } + if (isListening) _scheduleRecStart(400); }; synth.speak(utter); }