fix: mute mic during ElevenLabs TTS to prevent feedback loop; wire isSpeaking

This commit is contained in:
2026-05-31 16:35:44 +00:00
parent aa622e97a5
commit 01c6cfe96b
+19 -4
View File
@@ -1734,6 +1734,7 @@ function initVoice() {
recognition.lang = 'en-US'; recognition.lang = 'en-US';
recognition.onresult = (e) => { recognition.onresult = (e) => {
if (isSpeaking) return; // ignore mic during TTS playback
const result = e.results[e.results.length - 1]; const result = e.results[e.results.length - 1];
if (!result.isFinal) return; if (!result.isFinal) return;
const transcript = result[0].transcript.trim(); const transcript = result[0].transcript.trim();
@@ -1866,8 +1867,17 @@ async function speak(text) {
if (!text) return; if (!text) return;
if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; } if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; }
synth?.cancel(); synth?.cancel();
isSpeaking = true;
// Pause recognition while JARVIS speaks to avoid mic feedback
try { recognition?.abort(); } catch(_) {}
const reactor = document.getElementById('arcReactor'); const reactor = document.getElementById('arcReactor');
reactor?.classList.add('speaking'); reactor?.classList.add('speaking');
const _resumeMic = () => {
isSpeaking = false;
reactor?.classList.remove('speaking');
// Let onend restart recognition automatically (300ms buffer after audio ends)
if (isListening) setTimeout(() => { try { recognition?.start(); } catch(_) {} }, 300);
};
try { try {
const res = await fetch('/api/tts', { const res = await fetch('/api/tts', {
method: 'POST', method: 'POST',
@@ -1878,11 +1888,11 @@ async function speak(text) {
const blob = await res.blob(); const blob = await res.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
_ttsAudio = new Audio(url); _ttsAudio = new Audio(url);
_ttsAudio.onended = () => { URL.revokeObjectURL(url); _ttsAudio = null; reactor?.classList.remove('speaking'); }; _ttsAudio.onended = () => { URL.revokeObjectURL(url); _ttsAudio = null; _resumeMic(); };
_ttsAudio.onerror = () => { reactor?.classList.remove('speaking'); _ttsAudio = null; }; _ttsAudio.onerror = () => { _ttsAudio = null; _resumeMic(); };
await _ttsAudio.play(); await _ttsAudio.play();
} catch(e) { } catch(e) {
reactor?.classList.remove('speaking'); _resumeMic();
_speakFallback(text); _speakFallback(text);
} }
} }
@@ -1890,12 +1900,17 @@ async function speak(text) {
function _speakFallback(text) { function _speakFallback(text) {
if (!synth || !text) return; if (!synth || !text) return;
synth.cancel(); synth.cancel();
isSpeaking = true;
const utter = new SpeechSynthesisUtterance(text); const utter = new SpeechSynthesisUtterance(text);
if (selectedVoice) utter.voice = selectedVoice; if (selectedVoice) utter.voice = selectedVoice;
utter.rate = 0.92; utter.pitch = 0.85; utter.volume = 1; utter.rate = 0.92; utter.pitch = 0.85; utter.volume = 1;
const reactor = document.getElementById('arcReactor'); const reactor = document.getElementById('arcReactor');
utter.onstart = () => reactor?.classList.add('speaking'); utter.onstart = () => reactor?.classList.add('speaking');
utter.onend = () => reactor?.classList.remove('speaking'); utter.onend = () => {
reactor?.classList.remove('speaking');
isSpeaking = false;
if (isListening) setTimeout(() => { try { recognition?.start(); } catch(_) {} }, 300);
};
synth.speak(utter); synth.speak(utter);
} }