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 = `[${time}] ${msg}`; 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 = '
Waiting for detection...
'; 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 = '
Camera is empty.
'; 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 = `
${confirmedPeople.size}
${name.replace(/_/g, ' ')}
Confirmed Identity
${time} `; 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 = '
Camera is empty.
'; 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 = `
🟢
${name.replace(/_/g, ' ')}
Present Now
${timeStr} `; 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();