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;
|
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 ──────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user