Spaces:
Sleeping
Sleeping
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <title>FaceRecognition - Live Detection</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 15px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { font-size: 2.5em; margin-bottom: 10px; } | |
| .content { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 30px; | |
| padding: 30px; | |
| } | |
| .video-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| background: #000; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| aspect-ratio: 4/3; | |
| } | |
| video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| flex: 1; | |
| min-width: 120px; | |
| padding: 12px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 1em; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.2); } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .btn-detect { background: #4CAF50; color: white; } | |
| .btn-start { background: #2196F3; color: white; } | |
| .btn-stop { background: #f44336; color: white; } | |
| .btn-embed { background: #FF9800; color: white; } | |
| .output-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .output-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .status-badge { | |
| padding: 8px 15px; | |
| border-radius: 20px; | |
| font-weight: 600; | |
| font-size: 0.9em; | |
| } | |
| .status-connected { background: #4CAF50; color: white; } | |
| .status-disconnected { background: #999; color: white; } | |
| .status-idle { background: #2196F3; color: white; } | |
| #output { | |
| flex: 1; | |
| background: #f5f5f5; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| padding: 15px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.9em; | |
| overflow: auto; | |
| max-height: 400px; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| padding: 10px; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| font-size: 1em; | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .stat-box { | |
| background: #f0f0f0; | |
| padding: 10px; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .stat-label { font-size: 0.8em; color: #666; } | |
| .stat-value { font-size: 1.5em; font-weight: bold; color: #333; } | |
| @media (max-width: 768px) { | |
| .content { grid-template-columns: 1fr; } | |
| .header h1 { font-size: 1.8em; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>π FaceRecognition - Live Detection</h1> | |
| <p>Real-time face detection and recognition via WebSocket</p> | |
| </div> | |
| <div class="content"> | |
| <div class="video-section"> | |
| <div class="video-container"> | |
| <video id="video" autoplay muted playsinline></video> | |
| </div> | |
| <div class="controls"> | |
| <button id="snap" class="btn-detect">πΈ Single Frame</button> | |
| <button id="startStream" class="btn-start">βΆοΈ Start Stream</button> | |
| <button id="stopStream" class="btn-stop" disabled>βΉοΈ Stop Stream</button> | |
| </div> | |
| <div class="input-group"> | |
| <input type="text" id="userId" placeholder="Enter user ID (optional)" /> | |
| <button id="embedBtn" class="btn-embed">β Embed</button> | |
| </div> | |
| </div> | |
| <div class="output-section"> | |
| <div class="output-header"> | |
| <h3>π Results</h3> | |
| <span id="status" class="status-badge status-idle">βͺ Idle</span> | |
| </div> | |
| <div id="output">Waiting for input...</div> | |
| <div class="stats"> | |
| <div class="stat-box"> | |
| <div class="stat-label">Frames Sent</div> | |
| <div class="stat-value" id="frameCount">0</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Faces Detected</div> | |
| <div class="stat-value" id="faceCount">0</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Connection</div> | |
| <div class="stat-value" id="connStatus">β</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">FPS</div> | |
| <div class="stat-value" id="fpsCount">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const pathParts = location.pathname.split('/').filter(p => p.length > 0); | |
| let variant = 'v1'; | |
| if (pathParts.length >= 2 && pathParts[0].toLowerCase().includes('autoproctor')) { | |
| variant = pathParts[1] || variant; | |
| } | |
| const video = document.getElementById('video'); | |
| const snapBtn = document.getElementById('snap'); | |
| const startBtn = document.getElementById('startStream'); | |
| const stopBtn = document.getElementById('stopStream'); | |
| const embedBtn = document.getElementById('embedBtn'); | |
| const userIdInput = document.getElementById('userId'); | |
| const output = document.getElementById('output'); | |
| const statusBadge = document.getElementById('status'); | |
| const frameCountEl = document.getElementById('frameCount'); | |
| const faceCountEl = document.getElementById('faceCount'); | |
| const connStatusEl = document.getElementById('connStatus'); | |
| const fpsCountEl = document.getElementById('fpsCount'); | |
| let ws = null; | |
| let streamRunning = false; | |
| let frameCount = 0; | |
| let totalFaceCount = 0; | |
| let lastFrameTime = 0; | |
| function updateStatus(text, cssClass) { | |
| statusBadge.textContent = text; | |
| statusBadge.className = `status-badge ${cssClass}`; | |
| } | |
| function logOutput(msg) { | |
| output.textContent = typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2); | |
| output.scrollTop = output.scrollHeight; | |
| } | |
| navigator.mediaDevices.getUserMedia({ video: true, audio: false }) | |
| .then(s => { video.srcObject = s; updateStatus('β Camera Ready', 'status-idle'); }) | |
| .catch(e => { logOutput('β Camera error: ' + e); updateStatus('β Camera Failed', 'status-disconnected'); }); | |
| function canvasToBase64() { | |
| const c = document.createElement('canvas'); | |
| c.width = video.videoWidth || 640; | |
| c.height = video.videoHeight || 480; | |
| const ctx = c.getContext('2d'); | |
| ctx.drawImage(video, 0, 0, c.width, c.height); | |
| return c.toDataURL('image/jpeg', 0.85).split(',')[1]; // Return only base64 part | |
| } | |
| snapBtn.onclick = async () => { | |
| try { | |
| updateStatus('Processing...', 'status-idle'); | |
| const base64 = canvasToBase64(); | |
| const blob = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); | |
| const fd = new FormData(); | |
| fd.append('file', new Blob([blob], { type: 'image/jpeg' }), 'frame.jpg'); | |
| const res = await fetch(`/AutoProctor/${variant}/data/detect/frame`, { method: 'POST', body: fd }); | |
| const j = await res.json(); | |
| logOutput(j); | |
| const facesDetected = j.results ? j.results.ids?.length || 0 : 0; | |
| totalFaceCount += facesDetected; | |
| faceCountEl.textContent = totalFaceCount; | |
| updateStatus('β Result Ready', 'status-connected'); | |
| } catch (e) { | |
| logOutput('β Error: ' + e.message); | |
| updateStatus('β Error', 'status-disconnected'); | |
| } | |
| }; | |
| startBtn.onclick = async () => { | |
| if (ws) { ws.close(); ws = null; } | |
| const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; | |
| const url = `${protocol}://${location.host}/AutoProctor/${variant}/data/detect/stream`; | |
| console.log('Connecting to:', url); | |
| ws = new WebSocket(url); | |
| ws.onopen = () => { | |
| logOutput('π’ WebSocket Connected'); | |
| updateStatus('π’ Connected', 'status-connected'); | |
| connStatusEl.textContent = 'Online'; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| frameCount = 0; | |
| lastFrameTime = Date.now(); | |
| streamRunning = true; | |
| sendFrames(); | |
| }; | |
| ws.onmessage = (ev) => { | |
| try { | |
| const d = JSON.parse(ev.data); | |
| logOutput(d); | |
| if (d.faces_detected) { | |
| totalFaceCount += d.faces_detected; | |
| faceCountEl.textContent = totalFaceCount; | |
| } | |
| // Update FPS | |
| const now = Date.now(); | |
| if (lastFrameTime > 0) { | |
| const fps = Math.round(1000 / (now - lastFrameTime)); | |
| fpsCountEl.textContent = fps; | |
| } | |
| lastFrameTime = now; | |
| } catch (e) { | |
| logOutput('β οΈ ' + ev.data); | |
| } | |
| }; | |
| ws.onerror = (err) => { | |
| logOutput('β WebSocket error: ' + err); | |
| updateStatus('β Error', 'status-disconnected'); | |
| }; | |
| ws.onclose = () => { | |
| logOutput('π΄ WebSocket Closed'); | |
| updateStatus('βͺ Disconnected', 'status-disconnected'); | |
| connStatusEl.textContent = 'Offline'; | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| streamRunning = false; | |
| }; | |
| }; | |
| async function sendFrames() { | |
| while (streamRunning && ws && ws.readyState === 1) { | |
| const base64 = canvasToBase64(); | |
| frameCount++; | |
| frameCountEl.textContent = frameCount; | |
| try { | |
| ws.send(base64); | |
| } catch (e) { | |
| console.error('Send error:', e); | |
| break; | |
| } | |
| await new Promise(r => setTimeout(r, 2000)); // F P 2S | |
| } | |
| } | |
| stopBtn.onclick = () => { | |
| streamRunning = false; | |
| if (ws) { ws.close(); } | |
| }; | |
| embedBtn.onclick = async () => { | |
| const userId = userIdInput.value.trim(); | |
| if (!userId) { | |
| logOutput('β Please enter a user ID'); | |
| return; | |
| } | |
| try { | |
| updateStatus('Embedding...', 'status-idle'); | |
| const base64 = canvasToBase64(); | |
| const blob = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); | |
| const fd = new FormData(); | |
| fd.append('file', new Blob([blob], { type: 'image/jpeg' }), 'frame.jpg'); | |
| const res = await fetch(`/AutoProctor/${variant}/data/embed/${userId}`, { method: 'POST', body: fd }); | |
| const j = await res.json(); | |
| logOutput(j); | |
| updateStatus('β Embedded', 'status-connected'); | |
| } catch (e) { | |
| logOutput('β Embedding error: ' + e.message); | |
| updateStatus('β Error', 'status-disconnected'); | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |