final_eye / static /script.js
sudo-raj-1's picture
chore: clean up comments and format code to look natural
95b7795
Raw
History Blame Contribute Delete
6.37 kB
const video = document.getElementById("webcam");
const canvas = document.getElementById("canvas");
const startOverlay = document.getElementById("startOverlay");
const ctx = canvas.getContext("2d");
let lastSpokenText = "";
let isSpeaking = false;
let silenceUntil = 0;
let isPageVisible = true;
let isRequestInFlight = false; // prevents double-taps from flooding the server
function speak(message) {
if (!message) return;
if (Date.now() < silenceUntil) return;
speechSynthesis.cancel();
isSpeaking = true;
const utterance = new SpeechSynthesisUtterance(message);
utterance.rate = 1.0;
utterance.pitch = 1.0;
utterance.onend = () => {
isSpeaking = false;
lastSpokenText = message;
};
utterance.onerror = (e) => {
console.warn("Speech error:", e.error);
isSpeaking = false;
};
speechSynthesis.speak(utterance);
}
function captureFrame() {
return new Promise((resolve) => {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => resolve(blob), "image/jpeg", 0.8);
});
}
async function apiTell() {
if (isRequestInFlight) return;
isRequestInFlight = true;
try {
const blob = await captureFrame();
if (!blob) {
isRequestInFlight = false;
return;
}
const formData = new FormData();
formData.append("file", blob, "frame.jpg");
const res = await fetch("/api/tell", { method: "POST", body: formData });
const data = await res.json();
if (data.summary) {
speak(data.summary);
} else if (data.error) {
speak(data.error);
}
} catch (e) {
console.error("apiTell error:", e);
speak("Connection failed.");
} finally {
isRequestInFlight = false;
}
}
async function apiMore() {
if (isRequestInFlight) return;
isRequestInFlight = true;
speak("Looking closer...");
try {
const blob = await captureFrame();
if (!blob) {
isRequestInFlight = false;
return;
}
const formData = new FormData();
formData.append("file", blob, "frame.jpg");
const res = await fetch("/api/more", { method: "POST", body: formData });
const data = await res.json();
if (data.caption) {
speak(data.caption);
} else if (data.error) {
speak(data.error);
}
} catch (e) {
console.error("apiMore error:", e);
speak("Connection failed.");
} finally {
isRequestInFlight = false;
}
}
// gesture setup
let tapTimer = null;
let longPressTimer = null;
let isLongPress = false;
const DOUBLE_TAP_DELAY = 300;
function setupGestures(target) {
// prevent default context menus and text selection
target.addEventListener("contextmenu", (e) => e.preventDefault());
// mobile touch gestures
target.addEventListener("touchstart", (e) => {
if (e.touches.length >= 2) {
e.preventDefault();
clearTimeout(tapTimer);
clearTimeout(longPressTimer);
cmdShut();
return;
}
isLongPress = false;
longPressTimer = setTimeout(() => {
isLongPress = true;
cmdRepeat();
}, 600);
}, { passive: false });
target.addEventListener("touchend", (e) => {
clearTimeout(longPressTimer);
if (isLongPress || e.changedTouches.length > 1) return;
e.preventDefault();
if (tapTimer) {
clearTimeout(tapTimer);
tapTimer = null;
cmdMore();
} else {
tapTimer = setTimeout(() => {
tapTimer = null;
cmdTell();
}, DOUBLE_TAP_DELAY);
}
});
target.addEventListener("touchmove", () => {
clearTimeout(longPressTimer);
});
// desktop mouse gestures fallback
target.addEventListener("click", (e) => {
if (tapTimer) {
clearTimeout(tapTimer);
tapTimer = null;
cmdMore();
} else {
tapTimer = setTimeout(() => {
tapTimer = null;
cmdTell();
}, DOUBLE_TAP_DELAY);
}
});
target.addEventListener("contextmenu", (e) => {
e.preventDefault();
cmdShut();
});
target.addEventListener("mousedown", (e) => {
if (e.button === 1) {
e.preventDefault();
cmdRepeat();
}
});
}
function cmdTell() {
console.log("CMD: tell (single tap)");
apiTell();
}
function cmdMore() {
console.log("CMD: more (double tap)");
apiMore();
}
function cmdShut() {
console.log("CMD: shut (two-finger / right-click)");
speechSynthesis.cancel();
isSpeaking = false;
silenceUntil = Date.now() + 5000;
}
function cmdRepeat() {
console.log("CMD: repeat (long press / middle click)");
if (lastSpokenText) {
speak(lastSpokenText);
} else {
speak("Nothing to repeat yet.");
}
}
document.addEventListener("visibilitychange", () => {
isPageVisible = !document.hidden;
if (!isPageVisible) {
console.log("Page hidden — silencing speech");
speechSynthesis.cancel();
isSpeaking = false;
}
});
function initCamera() {
// try rear camera first (mobile), fall back to any camera (laptop)
navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: "environment" } }
})
.then(onCameraReady)
.catch(() => {
console.log("Rear camera not available, trying any camera...");
return navigator.mediaDevices.getUserMedia({ video: true });
})
.then(onCameraReady)
.catch((err) => {
console.error("Camera error:", err);
speak("Camera access failed. Please allow camera permissions.");
});
}
function onCameraReady(stream) {
if (!stream || video.srcObject) return;
video.srcObject = stream;
video.addEventListener("loadedmetadata", () => {
canvas.width = 640;
canvas.height = 480;
speak("System online. Tap the screen to hear what I see.");
});
}
startOverlay.addEventListener("click", () => {
startOverlay.classList.add("hidden");
initCamera();
setupGestures(document.body);
});