mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: wake word activation, mute toggle mic, silent auto-reload
This commit is contained in:
+111
-41
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
@@ -928,11 +934,13 @@ window.addEventListener("load", () => {
|
||||
loadVoices();
|
||||
|
||||
// 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) {
|
||||
sessionToken = saved;
|
||||
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';
|
||||
const app = document.getElementById('app');
|
||||
app.style.display = 'flex';
|
||||
|
||||
if (greeting) {
|
||||
addMessage('jarvis', greeting);
|
||||
speak(greeting);
|
||||
} else {
|
||||
const g = `Welcome back, ${name}. All systems online and standing by.`;
|
||||
addMessage('jarvis', g);
|
||||
speak(g);
|
||||
if (!silent) {
|
||||
if (greeting) {
|
||||
addMessage('jarvis', greeting);
|
||||
speak(greeting);
|
||||
} else {
|
||||
const g = `Welcome back, ${name}. All systems online and standing by.`;
|
||||
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;
|
||||
document.getElementById('textInput').value = transcript;
|
||||
stopListening();
|
||||
sendMessage();
|
||||
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;
|
||||
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 ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user