feat: wake word activation, mute toggle mic, silent auto-reload

This commit is contained in:
2026-05-31 16:24:38 +00:00
parent a0661bba14
commit 795411387c
+101 -31
View File
@@ -445,6 +445,7 @@ body::after{
animation:micPulse 0.8s ease-in-out infinite;
}
@keyframes micPulse{0%,100%{box-shadow:0 0 25px rgba(255,34,68,0.5)}50%{box-shadow:0 0 40px rgba(255,34,68,0.8),0 0 60px rgba(255,34,68,0.3)}}
#micBtn.muted{border-color:var(--text-dim);background:radial-gradient(circle,rgba(200,230,255,0.05),rgba(0,8,22,0.9));box-shadow:0 0 8px rgba(200,230,255,0.1);}
#micIcon{font-size:20px}
/* WAVEFORM ─────────────────────────────────────────────────────────── */
@@ -915,6 +916,11 @@ let autoMicCooldown = 0;
let faceApiReady = false;
let lastActivity = Date.now();
const IDLE_RELOAD_MS = 5 * 60 * 1000; // 5 min inactivity → full reload
let voiceMode = false; // true = JARVIS awake (listening for commands)
let voiceMuted = false; // true = awake but mic muted
let voiceLastCmd = 0;
const VOICE_SLEEP_MS = 30 * 60 * 1000; // 30 min voice inactivity → sleep
const WAKE_WORDS = ['jarvis', 'hey jarvis', "daddy's home", 'wake up'];
const FACE_MODEL_URL = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights';
// ── INIT ─────────────────────────────────────────────────────────────
@@ -929,10 +935,12 @@ window.addEventListener("load", () => {
// Check if already logged in
const saved = sessionStorage.getItem('jarvis_token');
const autoReload = sessionStorage.getItem('jarvis_autoreload') === '1';
sessionStorage.removeItem('jarvis_autoreload');
if (saved) {
sessionToken = saved;
sessionUser = sessionStorage.getItem('jarvis_user') || '';
showApp(sessionUser);
showApp(sessionUser, null, autoReload);
}
});
@@ -968,11 +976,12 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
}
});
function showApp(name, greeting) {
function showApp(name, greeting, silent = false) {
document.getElementById('loginScreen').style.display = 'none';
const app = document.getElementById('app');
app.style.display = 'flex';
if (!silent) {
if (greeting) {
addMessage('jarvis', greeting);
speak(greeting);
@@ -981,11 +990,23 @@ function showApp(name, greeting) {
addMessage('jarvis', g);
speak(g);
}
}
// Start data refresh
refreshAll();
refreshTimer = setInterval(refreshAll, 10000); // every 10s
setInterval(() => { if (Date.now() - lastActivity > IDLE_RELOAD_MS) location.reload(); }, 30000);
setInterval(() => {
if (Date.now() - lastActivity > IDLE_RELOAD_MS) {
sessionStorage.setItem('jarvis_autoreload', '1');
location.reload();
}
}, 30000);
setInterval(() => {
if (voiceMode && voiceLastCmd > 0 && Date.now() - voiceLastCmd > VOICE_SLEEP_MS) {
exitVoiceMode();
}
}, 60000);
startListening();
loadNetwork();
loadHA();
checkAgentStatus();
@@ -1090,15 +1111,15 @@ async function startCamera() {
const {width, height} = detection.box;
const ratio = (width * height) / (320 * 240);
// Trigger if face fills >3% of frame and mic not already on and cooldown passed
if (ratio > 0.03 && !isListening && now > autoMicCooldown) {
if (ratio > 0.03 && !voiceMode && now > autoMicCooldown) {
autoMicCooldown = now + 9000; // 9s between auto-triggers
document.getElementById('cameraBtn').classList.add('cam-sensing');
startListening();
enterVoiceMode();
}
} else {
// No face — stop if auto-triggered and face gone >3s
if (isListening && now - lastFaceSeen > 3000) {
stopListening();
// No face — exit voice mode if camera-triggered and face gone >3s
if (voiceMode && now - lastFaceSeen > 3000) {
exitVoiceMode();
}
document.getElementById('cameraBtn').classList.remove('cam-sensing');
}
@@ -1682,44 +1703,101 @@ async function sendMessage() {
function initVoice() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) {
// Chrome blocks speech API on untrusted HTTPS (self-signed certs)
if (window.isSecureContext === false) {
console.warn('Speech Recognition blocked: page is not a secure context (self-signed cert?)');
console.warn('Speech Recognition blocked: not a secure context');
} else {
console.warn('Speech Recognition not supported in this browser');
}
return;
}
recognition = new SR();
recognition.continuous = false;
recognition.continuous = true;
recognition.interimResults = false;
recognition.lang = 'en-US';
recognition.onresult = (e) => {
const transcript = e.results[0][0].transcript;
const result = e.results[e.results.length - 1];
if (!result.isFinal) return;
const transcript = result[0].transcript.trim();
if (!transcript) return;
if (!voiceMode) {
// Sleeping — check for wake word
const lc = transcript.toLowerCase();
if (WAKE_WORDS.some(w => lc.includes(w))) enterVoiceMode();
} else if (!voiceMuted) {
// Awake — process as command
voiceLastCmd = Date.now();
document.getElementById('textInput').value = transcript;
stopListening();
sendMessage();
}
};
recognition.onend = () => {
if (isListening) stopListening();
// Always restart for continuous wake word / command listening
if (isListening) setTimeout(() => { try { recognition.start(); } catch(_) {} }, 100);
};
recognition.onerror = (e) => {
stopListening();
if (e.error === 'not-allowed') {
isListening = false;
updateMicBtn();
addMessage('system', 'Microphone access denied. Please allow microphone permission in your browser, then reload.');
} else if (e.error === 'audio-capture') {
isListening = false;
updateMicBtn();
addMessage('system', 'No microphone detected. Please connect a microphone and try again.');
} else if (e.error !== 'no-speech') {
addMessage('system', 'Voice error: ' + e.error);
}
// no-speech and aborted are normal — onend will restart
};
}
function enterVoiceMode() {
voiceMode = true;
voiceMuted = false;
voiceLastCmd = Date.now();
updateMicBtn();
speak('Yes, ' + (sessionUser || 'Sir') + '?');
}
function exitVoiceMode() {
voiceMode = false;
voiceMuted = false;
updateMicBtn();
}
function updateMicBtn() {
const btn = document.getElementById('micBtn');
const icon = document.getElementById('micIcon');
const wave = document.getElementById('waveform');
if (!btn) return;
if (!voiceMode) {
btn.classList.remove('listening', 'muted');
btn.title = 'Click to activate or say "Hey JARVIS"';
icon.textContent = '🎤';
wave.classList.remove('active');
} else if (voiceMuted) {
btn.classList.remove('listening');
btn.classList.add('muted');
btn.title = 'Muted — click to unmute';
icon.textContent = '🔇';
wave.classList.remove('active');
} else {
btn.classList.add('listening');
btn.classList.remove('muted');
btn.title = 'Listening — click to mute';
icon.textContent = '🟢';
wave.classList.add('active');
}
}
function toggleVoice() {
if (isListening) stopListening(); else startListening();
if (!voiceMode) {
enterVoiceMode();
} else {
voiceMuted = !voiceMuted;
if (!voiceMuted) voiceLastCmd = Date.now();
updateMicBtn();
}
}
function startListening() {
@@ -1732,23 +1810,15 @@ function startListening() {
return;
}
isListening = true;
document.getElementById('micBtn').classList.add('listening');
document.getElementById('micIcon').textContent = '🔴';
document.getElementById('waveform').classList.add('active');
try {
recognition.start();
} catch(e) {
stopListening();
addMessage('system', 'Could not start microphone: ' + e.message);
}
try { recognition.start(); } catch(_) {}
}
function stopListening() {
isListening = false;
document.getElementById('micBtn').classList.remove('listening');
document.getElementById('micIcon').textContent = '🎤';
document.getElementById('waveform').classList.remove('active');
try { recognition.stop(); } catch(e) {}
voiceMode = false;
voiceMuted = false;
updateMicBtn();
try { recognition.abort(); } catch(_) {}
}
// ── SPEECH SYNTHESIS ──────────────────────────────────────────────────