Spaces:
Runtime error
Runtime error
| const video = document.getElementById('videoFeed'); | |
| const statusDot = document.getElementById('statusDot'); | |
| const statusText = document.getElementById('statusText'); | |
| const modal = document.getElementById('successModal'); | |
| const modalTitle = document.getElementById('modalTitle'); | |
| const modalName = document.getElementById('modalName'); | |
| const modalTime = document.getElementById('modalTime'); | |
| const cameraContainer = document.getElementById('cameraContainer'); | |
| const statusHint = document.getElementById('statusHint'); | |
| // Spoof toast | |
| const spoofToast = document.getElementById('spoofToast'); | |
| const spoofToastMessage = document.getElementById('spoofToastMessage'); | |
| // Frame buffer for sequence liveness (motion + blink); need enough frames to catch a blink | |
| const FRAME_BUFFER_SIZE = 14; | |
| const CAPTURE_INTERVAL_MS = 280; | |
| let frameBuffer = []; | |
| let isScanning = true; | |
| let stream = null; | |
| let toastDismissTimer = null; | |
| let lastFaceAlertAt = 0; | |
| const FACE_ALERT_COOLDOWN_MS = 4000; | |
| // βββ Camera βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function startCamera() { | |
| try { | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { facingMode: 'user', width: 640, height: 480 } | |
| }); | |
| video.srcObject = stream; | |
| startCaptureLoop(); | |
| } catch (err) { | |
| console.error("Camera error:", err); | |
| statusText.textContent = "Camera access denied or unavailable"; | |
| statusText.style.color = "red"; | |
| } | |
| } | |
| function stopCamera() { | |
| if (stream) { | |
| stream.getTracks().forEach(track => track.stop()); | |
| } | |
| } | |
| function captureFrame() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(video, 0, 0); | |
| return canvas.toDataURL('image/jpeg', 0.8); | |
| } | |
| // βββ Capture loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function startCaptureLoop() { | |
| while (isScanning) { | |
| if (video.readyState === video.HAVE_ENOUGH_DATA) { | |
| const frame = captureFrame(); | |
| frameBuffer.push(frame); | |
| if (frameBuffer.length > FRAME_BUFFER_SIZE) frameBuffer.shift(); | |
| // Send sequence only when we have enough frames for blink detection (backend needs ~4+ frames) | |
| const payload = frameBuffer.length >= 6 | |
| ? { frames: frameBuffer.slice() } | |
| : { frame: frame }; | |
| try { | |
| const response = await fetch('/api/recognize', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const result = await response.json(); | |
| handleResult(result); | |
| } catch (e) { | |
| console.log("Network error", e); | |
| } | |
| } | |
| await new Promise(r => setTimeout(r, CAPTURE_INTERVAL_MS)); | |
| } | |
| } | |
| // βββ Result handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function handleResult(result) { | |
| if (result.status === 'success') { | |
| showSuccess(result); | |
| var action = (result.punch_type === 'out') ? 'Punched out' : 'Punched in'; | |
| if (typeof showSnackbar === 'function') { | |
| showSnackbar(action + ' at ' + (result.timestamp || ''), 'success'); | |
| } | |
| } else if (result.status === 'cooldown') { | |
| statusText.textContent = 'Please wait 1 min'; | |
| statusText.style.color = "#FFD700"; | |
| if (typeof showSnackbar === 'function') showSnackbar(result.message || 'Please wait 1 minute before punching again.', 'info'); | |
| } else if (result.status === 'spoof') { | |
| showSpoofToast(result); | |
| } else if (result.status === 'unknown') { | |
| statusText.textContent = "Face not recognized"; | |
| statusText.style.color = "#A5A5A5"; | |
| if (typeof showSnackbar === 'function' && Date.now() - lastFaceAlertAt > FACE_ALERT_COOLDOWN_MS) { | |
| lastFaceAlertAt = Date.now(); | |
| showSnackbar('Face not recognized β ensure your face is clearly visible.', 'info'); | |
| } | |
| } else if (result.status === 'no_face') { | |
| statusText.textContent = "Position your face in the frame"; | |
| statusText.style.color = "#A5A5A5"; | |
| if (typeof showSnackbar === 'function' && Date.now() - lastFaceAlertAt > FACE_ALERT_COOLDOWN_MS) { | |
| lastFaceAlertAt = Date.now(); | |
| showSnackbar('Adjust position β keep your face clearly visible in the frame.', 'info'); | |
| } | |
| } else if (result.status === 'spoof' && result.reason && result.reason.toLowerCase().includes('blink')) { | |
| statusText.textContent = "Please blink to verify"; | |
| statusText.style.color = "#FFD700"; | |
| } | |
| } | |
| // βββ Spoof toast (banner, auto-dismiss) βββββββββββββββββββββββββββββββββββββ | |
| function showSpoofToast(data) { | |
| const msg = data.reason || data.message || "Use a live face, not a photo or screen."; | |
| spoofToastMessage.textContent = msg; | |
| spoofToast.classList.add('show'); | |
| statusText.textContent = "β Spoofing detected"; | |
| statusText.style.color = "#FF3B30"; | |
| if (toastDismissTimer) clearTimeout(toastDismissTimer); | |
| toastDismissTimer = setTimeout(() => { | |
| spoofToast.classList.remove('show'); | |
| statusText.textContent = "Position your face in the frame"; | |
| statusText.style.color = "#A5A5A5"; | |
| toastDismissTimer = null; | |
| }, 4500); | |
| } | |
| // βββ Success modal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showSuccess(data) { | |
| isScanning = false; | |
| statusDot.classList.add('active'); | |
| if (statusHint) statusHint.style.visibility = 'hidden'; | |
| if (modalTitle) modalTitle.textContent = (data.punch_type === 'out') ? 'Punched out' : 'Punched in'; | |
| if (modalName) modalName.textContent = data.name; | |
| if (modalTime) modalTime.textContent = data.timestamp || ''; | |
| modal.classList.add('show'); | |
| // Auto dismiss after 4s | |
| setTimeout(dismissModal, 4000); | |
| } | |
| function dismissModal() { | |
| modal.classList.remove('show'); | |
| statusDot.classList.remove('active'); | |
| statusText.textContent = "Position your face in the frame"; | |
| statusText.style.color = "#A5A5A5"; | |
| if (statusHint) statusHint.style.visibility = ""; | |
| isScanning = true; | |
| startCaptureLoop(); | |
| } | |
| // βββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| startCamera(); | |
| window.addEventListener('beforeunload', stopCamera); | |