Spaces:
Sleeping
Sleeping
File size: 9,803 Bytes
3a74216 ac10861 3a74216 04f722e 3a74216 ac10861 3a74216 ac10861 04f722e 1996a84 3a74216 ac10861 3a74216 ac10861 4e658cb ac10861 4e658cb ac10861 3a74216 ac10861 3a74216 ac10861 04f722e 3a74216 ac10861 3a74216 8b5d569 04f722e 3a74216 ac10861 3a74216 8b5d569 3a74216 ac10861 3a74216 04f722e ac10861 04f722e ac10861 04f722e 8b5d569 3a74216 8b5d569 3a74216 ac10861 8b5d569 3a74216 8b5d569 3a74216 ac10861 3a74216 17313e4 a586513 17313e4 a586513 3a74216 ac10861 3a74216 ac10861 3a74216 ac10861 3a74216 8b5d569 17313e4 3a74216 ac10861 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | 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();
|