Fix camera/mic interference: detection runs during TTS, 12s exit threshold, require no recent commands

- Remove isSpeaking outer guard — detection loop now runs always so lastFaceSeen stays current
- While isSpeaking + no face detected: refresh lastFaceSeen to prevent exit timer ticking during TTS
- Raise no-face exit threshold from 3s to 12s — sporadic missed frames no longer kill the session
- Add noCommandMs guard: camera never exits voice mode within 12s of last spoken command
- Voice auto-trigger still respects isSpeaking (wont reactivate mid-response)
This commit is contained in:
2026-06-02 00:15:47 +00:00
parent c91e5b8be7
commit c34d497e9d
+16 -10
View File
@@ -1539,7 +1539,8 @@ async function startCamera() {
addMessage('system', 'Face detection active — reactor tracking engaged.'); addMessage('system', 'Face detection active — reactor tracking engaged.');
faceLoopId = setInterval(async () => { faceLoopId = setInterval(async () => {
if (!cameraActive || isSpeaking) return; if (!cameraActive) return;
// Run detection even while speaking — needed for tracking + prevents lastFaceSeen staling out
try { try {
const detection = await faceapi.detectSingleFace( const detection = await faceapi.detectSingleFace(
document.getElementById('faceVideo'), document.getElementById('faceVideo'),
@@ -1548,25 +1549,30 @@ async function startCamera() {
const now = Date.now(); const now = Date.now();
if (detection) { if (detection) {
lastFaceSeen = now; lastFaceSeen = now;
const {x, y, width, height} = detection.box; const ratio = (detection.box.width * detection.box.height) / (320 * 240);
const ratio = (width * height) / (320 * 240);
// Drive the reactor to follow the face // Always drive the reactor
updateFaceTarget(detection.box, 320, 240); updateFaceTarget(detection.box, 320, 240);
// Trigger voice if face fills >3% of frame // Only auto-trigger voice when not already speaking/active, cooldown passed
if (ratio > 0.03 && !voiceMode && now > autoMicCooldown) { if (ratio > 0.03 && !voiceMode && !isSpeaking && now > autoMicCooldown) {
autoMicCooldown = now + 9000; autoMicCooldown = now + 9000;
document.getElementById('cameraBtn').classList.add('cam-sensing'); document.getElementById('cameraBtn').classList.add('cam-sensing');
enterVoiceMode(); enterVoiceMode();
} }
} else { } else {
clearFaceTarget(); // While JARVIS is speaking, keep lastFaceSeen fresh so the exit timer doesn't tick down
// No face — exit voice mode if face gone >3s if (isSpeaking) { lastFaceSeen = now; }
if (voiceMode && now - lastFaceSeen > 3000) { else { clearFaceTarget(); }
document.getElementById('cameraBtn').classList.remove('cam-sensing');
// Exit voice mode only if: face gone >12s AND no command in that same window AND not speaking
const noFaceMs = now - lastFaceSeen;
const noCommandMs = now - (voiceLastCmd || 0);
if (voiceMode && !isSpeaking && noFaceMs > 12000 && noCommandMs > 12000) {
exitVoiceMode(); exitVoiceMode();
} }
document.getElementById('cameraBtn').classList.remove('cam-sensing');
} }
} catch(_) {} } catch(_) {}
}, 600); }, 600);