vrfefavr's picture
Update static/app.js
17313e4 verified
const video = document.getElementById('videoElement');
const canvas = document.getElementById('canvasElement');
const ctx = canvas.getContext('2d');
const overlayCanvas = document.getElementById('overlayCanvas');
const overlayCtx = overlayCanvas.getContext('2d');
const statusBadge = document.getElementById('status-badge');
const attendanceList = document.getElementById('attendance-list');
const confirmedList = document.getElementById('confirmed-list');
const terminalLogs = document.getElementById('terminal-logs');
const debugCrops = document.getElementById('debug-crops');
const cropCount = document.getElementById('crop-count');
let ws;
let isProcessing = false;
let frameCount = 0;
let lastFpsTime = Date.now();
let reconnectAttempts = 0;
let pingInterval;
let currentlyVisible = {};
let confirmedPeople = new Set();
const VISIBILITY_TIMEOUT_MS = 8000;
// Calculate dynamic WebSocket URL for Hugging Face Spaces
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
function logToTerminal(msg) {
const div = document.createElement('div');
const time = new Date().toISOString().split('T')[1].slice(0, 12);
div.innerHTML = `<span class="text-slate-500">[${time}]</span> <span class="${msg.includes('ERROR') ? 'text-red-400' : 'text-slate-300'}">${msg}</span>`;
terminalLogs.appendChild(div);
if (terminalLogs.children.length > 20) terminalLogs.removeChild(terminalLogs.firstChild);
terminalLogs.scrollTop = terminalLogs.scrollHeight;
}
function drawFaceBoxes(faces) {
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
if (!faces) return;
faces.forEach(face => {
if (face.box) {
let { x, y, w, h } = face.box;
// Mirror math for canvas
const mirroredX = overlayCanvas.width - x - w;
const color = face.status === 'match' ? '#10b981' : (face.status === 'fail' ? '#f59e0b' : '#ef4444');
overlayCtx.strokeStyle = color;
overlayCtx.lineWidth = 3;
overlayCtx.strokeRect(mirroredX, y, w, h);
overlayCtx.fillStyle = color;
overlayCtx.font = 'bold 12px monospace';
const label = `${face.name} (${face.score}%)`;
overlayCtx.fillText(label, mirroredX, y > 20 ? y - 10 : y + 20);
}
});
}
function updateDebugCrops(faces) {
// Always clear previous crops
debugCrops.innerHTML = '';
if (!faces || faces.length === 0) {
debugCrops.innerHTML = '<div class="text-xs text-slate-600 italic">Waiting for detection...</div>';
cropCount.textContent = '0 faces';
return;
}
cropCount.textContent = `${faces.length} faces`;
faces.forEach(face => {
if (face.crop) {
const container = document.createElement('div');
container.className = 'relative flex-shrink-0';
const img = document.createElement('img');
img.src = face.crop;
img.className = `w-16 h-16 rounded border-2 ${face.status === 'match' ? 'border-emerald-500' : (face.status === 'fail' ? 'border-amber-500' : 'border-slate-700')} object-cover`;
const badge = document.createElement('div');
badge.className = 'absolute bottom-0 left-0 right-0 bg-black/70 text-[8px] text-center py-0.5 truncate';
badge.textContent = face.status === 'match' ? 'MATCH' : (face.score > 0 ? `${face.score}%` : '???');
container.appendChild(img);
container.appendChild(badge);
debugCrops.appendChild(container);
}
});
}
function connectWebSocket() {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
reconnectAttempts = 0;
statusBadge.textContent = '🟢 System Active';
statusBadge.className = 'px-4 py-2 rounded-full text-sm font-semibold bg-emerald-500/20 text-emerald-400 border border-emerald-500/50';
document.getElementById('ws-status').textContent = 'Connected';
logToTerminal("WebSocket Connected. Running Visual Debugger...");
attendanceList.innerHTML = '<div class="text-center text-slate-500 mt-10 text-sm">Camera is empty.</div>';
currentlyVisible = {};
pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'heartbeat' })); }, 10000);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'attendance') {
currentlyVisible[data.name] = Date.now();
renderPresenceList();
addConfirmedEntry(data.name, data.time);
} else if (data.type === 'ready') {
isProcessing = false;
if(data.debug) logToTerminal(data.debug);
if(data.faces) {
drawFaceBoxes(data.faces);
updateDebugCrops(data.faces);
}
}
};
ws.onclose = () => {
isProcessing = false;
clearInterval(pingInterval);
statusBadge.textContent = '🔴 Disconnected';
statusBadge.className = 'px-4 py-2 rounded-full text-sm font-semibold bg-red-500/20 text-red-400 border border-red-500/50';
document.getElementById('ws-status').textContent = 'Offline';
const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
logToTerminal(`ERROR: Lost connection. Reconnect in ${timeout/1000}s...`);
reconnectAttempts++;
setTimeout(connectWebSocket, timeout);
};
}
function addConfirmedEntry(name, time) {
if (confirmedPeople.has(name)) return;
if (confirmedPeople.size === 0) confirmedList.innerHTML = '';
confirmedPeople.add(name);
const div = document.createElement('div');
div.className = 'bg-emerald-500/10 p-3 rounded-xl border border-emerald-500/20 flex justify-between items-center animate-[slideIn_0.3s_ease-out]';
div.innerHTML = `
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center text-xs font-bold">${confirmedPeople.size}</div>
<div>
<div class="font-semibold text-slate-200 capitalize tracking-wide">${name.replace(/_/g, ' ')}</div>
<div class="text-[9px] text-emerald-500 uppercase font-bold tracking-wider">Confirmed Identity</div>
</div>
</div>
<span class="text-[10px] text-emerald-400 font-mono bg-emerald-400/5 px-2 py-1 rounded border border-emerald-400/10">${time}</span>
`;
confirmedList.insertBefore(div, confirmedList.firstChild);
}
setInterval(() => {
const now = Date.now();
let hasChanges = false;
for (const name in currentlyVisible) {
if (now - currentlyVisible[name] > VISIBILITY_TIMEOUT_MS) {
delete currentlyVisible[name];
hasChanges = true;
}
}
if (hasChanges) renderPresenceList();
}, 1000);
function renderPresenceList() {
const names = Object.keys(currentlyVisible);
if (names.length === 0) {
attendanceList.innerHTML = '<div class="text-center text-slate-500 mt-10 text-sm animate-pulse">Camera is empty.</div>';
return;
}
attendanceList.innerHTML = '';
const timeStr = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
names.forEach(name => {
const div = document.createElement('div');
div.className = 'bg-slate-800/80 p-3 rounded-xl border border-emerald-500/50 flex justify-between items-center animate-[slideIn_0.3s_ease-out] shadow-[0_0_10px_rgba(16,185,129,0.1)]';
div.innerHTML = `
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center text-lg shadow-inner">🟢</div>
<div>
<div class="font-semibold text-slate-200 capitalize tracking-wide">${name.replace(/_/g, ' ')}</div>
<div class="text-[10px] text-emerald-400 uppercase font-bold tracking-wider">Present Now</div>
</div>
</div>
<span class="text-xs text-emerald-400 font-mono bg-emerald-400/10 px-2 py-1 rounded border border-emerald-400/20">${timeStr}</span>
`;
attendanceList.appendChild(div);
});
}
async function startCamera() {
try {
// Lowered to 640x480 (VGA) for ultra-fast AI processing without losing accuracy
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480, facingMode: "user" }
});
video.srcObject = stream;
video.onloadedmetadata = () => {
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
overlayCanvas.width = video.videoWidth; overlayCanvas.height = video.videoHeight;
requestAnimationFrame(processFrame);
};
} catch (err) {
statusBadge.textContent = '⚠️ Camera Denied';
logToTerminal("ERROR: Camera access denied.");
}
}
function processFrame() {
frameCount++;
if (Date.now() - lastFpsTime >= 1000) { document.getElementById('fps-counter').textContent = frameCount; frameCount = 0; lastFpsTime = Date.now(); }
if (ws && ws.readyState === WebSocket.OPEN && !isProcessing) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
isProcessing = true;
// Lowered to 0.7 quality to minimize network latency on Hugging Face proxy
ws.send(JSON.stringify({ type: 'frame', image: canvas.toDataURL('image/jpeg', 0.7) }));
}
requestAnimationFrame(processFrame);
}
connectWebSocket();
startCamera();