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
+111 -41
View File
@@ -445,6 +445,7 @@ body::after{
animation:micPulse 0.8s ease-in-out infinite; 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)}} @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} #micIcon{font-size:20px}
/* WAVEFORM ─────────────────────────────────────────────────────────── */ /* WAVEFORM ─────────────────────────────────────────────────────────── */
@@ -915,6 +916,11 @@ let autoMicCooldown = 0;
let faceApiReady = false; let faceApiReady = false;
let lastActivity = Date.now(); let lastActivity = Date.now();
const IDLE_RELOAD_MS = 5 * 60 * 1000; // 5 min inactivity → full reload 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'; const FACE_MODEL_URL = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights';
// ── INIT ───────────────────────────────────────────────────────────── // ── INIT ─────────────────────────────────────────────────────────────
@@ -928,11 +934,13 @@ window.addEventListener("load", () => {
loadVoices(); loadVoices();
// Check if already logged in // Check if already logged in
const saved = sessionStorage.getItem('jarvis_token'); const saved = sessionStorage.getItem('jarvis_token');
const autoReload = sessionStorage.getItem('jarvis_autoreload') === '1';
sessionStorage.removeItem('jarvis_autoreload');
if (saved) { if (saved) {
sessionToken = saved; sessionToken = saved;
sessionUser = sessionStorage.getItem('jarvis_user') || ''; sessionUser = sessionStorage.getItem('jarvis_user') || '';
showApp(sessionUser); showApp(sessionUser, null, autoReload);
} }
}); });
@@ -968,24 +976,37 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
} }
}); });
function showApp(name, greeting) { function showApp(name, greeting, silent = false) {
document.getElementById('loginScreen').style.display = 'none'; document.getElementById('loginScreen').style.display = 'none';
const app = document.getElementById('app'); const app = document.getElementById('app');
app.style.display = 'flex'; app.style.display = 'flex';
if (greeting) { if (!silent) {
addMessage('jarvis', greeting); if (greeting) {
speak(greeting); addMessage('jarvis', greeting);
} else { speak(greeting);
const g = `Welcome back, ${name}. All systems online and standing by.`; } else {
addMessage('jarvis', g); const g = `Welcome back, ${name}. All systems online and standing by.`;
speak(g); addMessage('jarvis', g);
speak(g);
}
} }
// Start data refresh // Start data refresh
refreshAll(); refreshAll();
refreshTimer = setInterval(refreshAll, 10000); // every 10s 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(); loadNetwork();
loadHA(); loadHA();
checkAgentStatus(); checkAgentStatus();
@@ -1090,15 +1111,15 @@ async function startCamera() {
const {width, height} = detection.box; const {width, height} = detection.box;
const ratio = (width * height) / (320 * 240); const ratio = (width * height) / (320 * 240);
// Trigger if face fills >3% of frame and mic not already on and cooldown passed // 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 autoMicCooldown = now + 9000; // 9s between auto-triggers
document.getElementById('cameraBtn').classList.add('cam-sensing'); document.getElementById('cameraBtn').classList.add('cam-sensing');
startListening(); enterVoiceMode();
} }
} else { } else {
// No face — stop if auto-triggered and face gone >3s // No face — exit voice mode if camera-triggered and face gone >3s
if (isListening && now - lastFaceSeen > 3000) { if (voiceMode && now - lastFaceSeen > 3000) {
stopListening(); exitVoiceMode();
} }
document.getElementById('cameraBtn').classList.remove('cam-sensing'); document.getElementById('cameraBtn').classList.remove('cam-sensing');
} }
@@ -1682,44 +1703,101 @@ async function sendMessage() {
function initVoice() { function initVoice() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition; const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) { if (!SR) {
// Chrome blocks speech API on untrusted HTTPS (self-signed certs)
if (window.isSecureContext === false) { 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 { } else {
console.warn('Speech Recognition not supported in this browser'); console.warn('Speech Recognition not supported in this browser');
} }
return; return;
} }
recognition = new SR(); recognition = new SR();
recognition.continuous = false; recognition.continuous = true;
recognition.interimResults = false; recognition.interimResults = false;
recognition.lang = 'en-US'; recognition.lang = 'en-US';
recognition.onresult = (e) => { recognition.onresult = (e) => {
const transcript = e.results[0][0].transcript; const result = e.results[e.results.length - 1];
document.getElementById('textInput').value = transcript; if (!result.isFinal) return;
stopListening(); const transcript = result[0].transcript.trim();
sendMessage(); 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;
sendMessage();
}
}; };
recognition.onend = () => { recognition.onend = () => {
if (isListening) stopListening(); // Always restart for continuous wake word / command listening
if (isListening) setTimeout(() => { try { recognition.start(); } catch(_) {} }, 100);
}; };
recognition.onerror = (e) => { recognition.onerror = (e) => {
stopListening();
if (e.error === 'not-allowed') { if (e.error === 'not-allowed') {
isListening = false;
updateMicBtn();
addMessage('system', 'Microphone access denied. Please allow microphone permission in your browser, then reload.'); addMessage('system', 'Microphone access denied. Please allow microphone permission in your browser, then reload.');
} else if (e.error === 'audio-capture') { } else if (e.error === 'audio-capture') {
isListening = false;
updateMicBtn();
addMessage('system', 'No microphone detected. Please connect a microphone and try again.'); 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() { function toggleVoice() {
if (isListening) stopListening(); else startListening(); if (!voiceMode) {
enterVoiceMode();
} else {
voiceMuted = !voiceMuted;
if (!voiceMuted) voiceLastCmd = Date.now();
updateMicBtn();
}
} }
function startListening() { function startListening() {
@@ -1732,23 +1810,15 @@ function startListening() {
return; return;
} }
isListening = true; isListening = true;
document.getElementById('micBtn').classList.add('listening'); try { recognition.start(); } catch(_) {}
document.getElementById('micIcon').textContent = '🔴';
document.getElementById('waveform').classList.add('active');
try {
recognition.start();
} catch(e) {
stopListening();
addMessage('system', 'Could not start microphone: ' + e.message);
}
} }
function stopListening() { function stopListening() {
isListening = false; isListening = false;
document.getElementById('micBtn').classList.remove('listening'); voiceMode = false;
document.getElementById('micIcon').textContent = '🎤'; voiceMuted = false;
document.getElementById('waveform').classList.remove('active'); updateMicBtn();
try { recognition.stop(); } catch(e) {} try { recognition.abort(); } catch(_) {}
} }
// ── SPEECH SYNTHESIS ────────────────────────────────────────────────── // ── SPEECH SYNTHESIS ──────────────────────────────────────────────────