Spaces:
Build error
Build error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SafetyMaster Pro - AI Safety Monitoring (Cloud)</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #2563eb; | |
| --primary-dark: #1d4ed8; | |
| --secondary-color: #64748b; | |
| --success-color: #10b981; | |
| --warning-color: #f59e0b; | |
| --danger-color: #ef4444; | |
| --bg-primary: #0f172a; | |
| --bg-secondary: #1e293b; | |
| --bg-tertiary: #334155; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #cbd5e1; | |
| --text-muted: #94a3b8; | |
| --border-color: #334155; | |
| --border-radius: 12px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .main-container { | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Fullscreen Video Container */ | |
| .video-main { | |
| flex: 1; | |
| position: relative; | |
| background: #000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #webcamVideo { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| #processedCanvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| z-index: 2; | |
| } | |
| .no-feed { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1.5rem; | |
| color: var(--text-muted); | |
| text-align: center; | |
| } | |
| .no-feed i { | |
| font-size: 4rem; | |
| color: var(--text-muted); | |
| opacity: 0.5; | |
| } | |
| .no-feed h3 { | |
| font-size: 1.5rem; | |
| font-weight: 500; | |
| } | |
| /* Floating Header */ | |
| .floating-header { | |
| position: absolute; | |
| top: 1.5rem; | |
| left: 1.5rem; | |
| right: 1.5rem; | |
| background: rgba(15, 23, 42, 0.9); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 16px; | |
| padding: 1rem 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| z-index: 100; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .logo { | |
| width: 36px; | |
| height: 36px; | |
| background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 1.25rem; | |
| } | |
| .header-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.5rem 1rem; | |
| border-radius: 50px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| .status-badge.connected { | |
| background: rgba(16, 185, 129, 0.15); | |
| color: var(--success-color); | |
| border: 1px solid rgba(16, 185, 129, 0.3); | |
| } | |
| .status-badge.disconnected { | |
| background: rgba(239, 68, 68, 0.15); | |
| color: var(--danger-color); | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .status-indicator { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| /* Floating Controls */ | |
| .floating-controls { | |
| position: absolute; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(15, 23, 42, 0.9); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| z-index: 100; | |
| } | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 12px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 0.875rem; | |
| } | |
| .btn-primary { | |
| background: var(--primary-color); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: var(--primary-dark); | |
| } | |
| .btn-danger { | |
| background: var(--danger-color); | |
| color: white; | |
| } | |
| .btn-danger:hover { | |
| background: #dc2626; | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* Floating Stats */ | |
| .floating-stats { | |
| position: absolute; | |
| top: 6rem; | |
| left: 1.5rem; | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1rem; | |
| z-index: 100; | |
| } | |
| .stat-card { | |
| background: rgba(15, 23, 42, 0.9); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| text-align: center; | |
| min-width: 120px; | |
| } | |
| .stat-value { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .stat-label { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| margin-top: 0.25rem; | |
| } | |
| /* Floating Violations */ | |
| .floating-violations { | |
| position: absolute; | |
| top: 6rem; | |
| right: 1.5rem; | |
| width: 300px; | |
| background: rgba(15, 23, 42, 0.9); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 16px; | |
| z-index: 100; | |
| max-height: 400px; | |
| overflow: hidden; | |
| } | |
| .violations-header { | |
| padding: 1rem 1.5rem; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .violations-title { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .violation-badge { | |
| background: var(--danger-color); | |
| color: white; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 50px; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| } | |
| .violations-list { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| } | |
| .violation-item { | |
| background: rgba(239, 68, 68, 0.1); | |
| border: 1px solid rgba(239, 68, 68, 0.2); | |
| border-radius: 8px; | |
| padding: 0.75rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| .violation-item:last-child { | |
| margin-bottom: 0; | |
| } | |
| .violation-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0.5rem; | |
| } | |
| .violation-time { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| .violation-severity { | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| padding: 0.125rem 0.5rem; | |
| border-radius: 4px; | |
| background: var(--danger-color); | |
| color: white; | |
| } | |
| .violation-description { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| } | |
| .no-violations { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-muted); | |
| } | |
| .no-violations i { | |
| font-size: 2rem; | |
| margin-bottom: 0.5rem; | |
| color: var(--success-color); | |
| } | |
| /* Loading animation */ | |
| .loading { | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid transparent; | |
| border-top: 2px solid currentColor; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .floating-stats { | |
| grid-template-columns: repeat(2, 1fr); | |
| top: 5rem; | |
| } | |
| .floating-violations { | |
| width: 250px; | |
| top: 5rem; | |
| } | |
| .floating-header { | |
| top: 1rem; | |
| left: 1rem; | |
| right: 1rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="main-container"> | |
| <!-- Floating Header --> | |
| <div class="floating-header"> | |
| <div class="header-left"> | |
| <div class="logo"> | |
| <i class="fas fa-shield-alt"></i> | |
| </div> | |
| <div class="header-title">SafetyMaster Pro</div> | |
| </div> | |
| <div class="header-right"> | |
| <div class="status-badge disconnected" id="statusBadge"> | |
| <div class="status-indicator"></div> | |
| <span id="statusText">Disconnected</span> | |
| </div> | |
| <div id="fpsCounter" style="font-size: 0.875rem; color: var(--text-muted);">FPS: 0</div> | |
| </div> | |
| </div> | |
| <!-- Floating Stats --> | |
| <div class="floating-stats"> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="totalPeople">0</div> | |
| <div class="stat-label">Total People</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="compliantPeople">0</div> | |
| <div class="stat-label">Compliant</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="violationCount">0</div> | |
| <div class="stat-label">Violations</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="complianceRate">0%</div> | |
| <div class="stat-label">Compliance</div> | |
| </div> | |
| </div> | |
| <!-- Floating Violations --> | |
| <div class="floating-violations"> | |
| <div class="violations-header"> | |
| <div class="violations-title">Safety Violations</div> | |
| <div class="violation-badge" id="violationBadge">0</div> | |
| </div> | |
| <div class="violations-list" id="violationsList"> | |
| <div class="no-violations"> | |
| <i class="fas fa-shield-check"></i> | |
| <div>All Clear</div> | |
| <small>No safety violations detected</small> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Video Area --> | |
| <div class="video-main"> | |
| <div class="video-container" id="videoContainer"> | |
| <div class="no-feed" id="noFeed"> | |
| <i class="fas fa-video"></i> | |
| <h3>Click "Start Webcam" to Begin</h3> | |
| <p>Allow camera access when prompted</p> | |
| </div> | |
| <video id="webcamVideo" autoplay muted style="display: none;"></video> | |
| <canvas id="processedCanvas" style="display: none;"></canvas> | |
| </div> | |
| </div> | |
| <!-- Floating Controls --> | |
| <div class="floating-controls"> | |
| <button class="btn btn-primary" id="startBtn"> | |
| <i class="fas fa-video"></i> Start Webcam | |
| </button> | |
| <button class="btn btn-danger" id="stopBtn" disabled> | |
| <i class="fas fa-stop"></i> Stop | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| // Global variables | |
| let socket; | |
| let isMonitoring = false; | |
| let webcamStream = null; | |
| let frameCount = 0; | |
| let lastFpsUpdate = Date.now(); | |
| let violationsData = []; | |
| let violationIds = new Set(); | |
| let processingInterval = null; | |
| // DOM elements | |
| const statusBadge = document.getElementById('statusBadge'); | |
| const statusText = document.getElementById('statusText'); | |
| const fpsCounter = document.getElementById('fpsCounter'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const webcamVideo = document.getElementById('webcamVideo'); | |
| const processedCanvas = document.getElementById('processedCanvas'); | |
| const noFeed = document.getElementById('noFeed'); | |
| const videoContainer = document.getElementById('videoContainer'); | |
| // Stats elements | |
| const totalPeople = document.getElementById('totalPeople'); | |
| const compliantPeople = document.getElementById('compliantPeople'); | |
| const violationCount = document.getElementById('violationCount'); | |
| const complianceRate = document.getElementById('complianceRate'); | |
| // Violations elements | |
| const violationsList = document.getElementById('violationsList'); | |
| const violationBadge = document.getElementById('violationBadge'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeSocket(); | |
| setupEventListeners(); | |
| }); | |
| function initializeSocket() { | |
| socket = io(); | |
| socket.on('connect', function() { | |
| console.log('Connected to server'); | |
| updateConnectionStatus(true); | |
| }); | |
| socket.on('disconnect', function() { | |
| console.log('Disconnected from server'); | |
| updateConnectionStatus(false); | |
| }); | |
| socket.on('detection_result', function(data) { | |
| updateStatistics(data); | |
| updateFPS(); | |
| drawDetections(data); | |
| }); | |
| } | |
| function setupEventListeners() { | |
| startBtn.addEventListener('click', startWebcam); | |
| stopBtn.addEventListener('click', stopWebcam); | |
| } | |
| async function startWebcam() { | |
| try { | |
| setLoadingState(startBtn, true); | |
| // Request webcam access | |
| webcamStream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 }, | |
| frameRate: { ideal: 30 } | |
| } | |
| }); | |
| // Set up video element | |
| webcamVideo.srcObject = webcamStream; | |
| webcamVideo.style.display = 'block'; | |
| processedCanvas.style.display = 'block'; | |
| noFeed.style.display = 'none'; | |
| // Wait for video to be ready | |
| await new Promise(resolve => { | |
| webcamVideo.onloadedmetadata = resolve; | |
| }); | |
| // Set up canvas | |
| const canvas = processedCanvas; | |
| canvas.width = webcamVideo.videoWidth; | |
| canvas.height = webcamVideo.videoHeight; | |
| // Start processing frames | |
| isMonitoring = true; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| // Process frames at 10 FPS (every 100ms) | |
| processingInterval = setInterval(processFrame, 100); | |
| console.log('Webcam started successfully'); | |
| } catch (error) { | |
| console.error('Error starting webcam:', error); | |
| alert('Failed to access webcam. Please ensure you have granted camera permissions.'); | |
| } finally { | |
| setLoadingState(startBtn, false); | |
| } | |
| } | |
| function stopWebcam() { | |
| try { | |
| setLoadingState(stopBtn, true); | |
| isMonitoring = false; | |
| // Stop processing | |
| if (processingInterval) { | |
| clearInterval(processingInterval); | |
| processingInterval = null; | |
| } | |
| // Stop webcam stream | |
| if (webcamStream) { | |
| webcamStream.getTracks().forEach(track => track.stop()); | |
| webcamStream = null; | |
| } | |
| // Hide video elements | |
| webcamVideo.style.display = 'none'; | |
| processedCanvas.style.display = 'none'; | |
| noFeed.style.display = 'flex'; | |
| // Reset UI | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| // Reset statistics | |
| totalPeople.textContent = '0'; | |
| compliantPeople.textContent = '0'; | |
| violationCount.textContent = '0'; | |
| complianceRate.textContent = '0%'; | |
| fpsCounter.textContent = 'FPS: 0'; | |
| // Clear violations | |
| violationsData = []; | |
| violationIds.clear(); | |
| renderViolations(); | |
| updateViolationBadge(); | |
| console.log('Webcam stopped'); | |
| } catch (error) { | |
| console.error('Error stopping webcam:', error); | |
| } finally { | |
| setLoadingState(stopBtn, false); | |
| } | |
| } | |
| function processFrame() { | |
| if (!isMonitoring || !webcamVideo.videoWidth) return; | |
| try { | |
| // Create a temporary canvas to capture the frame | |
| const tempCanvas = document.createElement('canvas'); | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCanvas.width = webcamVideo.videoWidth; | |
| tempCanvas.height = webcamVideo.videoHeight; | |
| // Draw current video frame | |
| tempCtx.drawImage(webcamVideo, 0, 0); | |
| // Convert to base64 | |
| const frameData = tempCanvas.toDataURL('image/jpeg', 0.8).split(',')[1]; | |
| // Send to server for AI processing | |
| socket.emit('process_frame', { frame: frameData }); | |
| } catch (error) { | |
| console.error('Error processing frame:', error); | |
| } | |
| } | |
| function drawDetections(data) { | |
| if (!data.processed_frame) return; | |
| try { | |
| const canvas = processedCanvas; | |
| const ctx = canvas.getContext('2d'); | |
| // Create image from processed frame | |
| const img = new Image(); | |
| img.onload = function() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw processed frame with detections | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| }; | |
| img.src = 'data:image/jpeg;base64,' + data.processed_frame; | |
| } catch (error) { | |
| console.error('Error drawing detections:', error); | |
| } | |
| } | |
| function updateConnectionStatus(connected) { | |
| if (connected) { | |
| statusBadge.classList.remove('disconnected'); | |
| statusBadge.classList.add('connected'); | |
| statusText.textContent = 'Connected'; | |
| } else { | |
| statusBadge.classList.remove('connected'); | |
| statusBadge.classList.add('disconnected'); | |
| statusText.textContent = 'Disconnected'; | |
| } | |
| } | |
| function updateStatistics(data) { | |
| totalPeople.textContent = data.people_count || 0; | |
| // Calculate compliant people | |
| const violationsLength = (data.violations || []).length; | |
| const compliantCount = Math.max(0, (data.people_count || 0) - violationsLength); | |
| compliantPeople.textContent = compliantCount; | |
| violationCount.textContent = violationsLength; | |
| // Calculate compliance rate | |
| const totalPeopleCount = data.people_count || 0; | |
| const compliancePercentage = totalPeopleCount > 0 ? | |
| (compliantCount / totalPeopleCount * 100) : 100; | |
| complianceRate.textContent = compliancePercentage.toFixed(0) + '%'; | |
| // Update violations | |
| if (data.violations && data.violations.length > 0) { | |
| data.violations.forEach(violation => { | |
| const violationId = `${violation.type}_${violation.description}_${Math.floor(Date.now() / 5000)}`; | |
| if (!violationIds.has(violationId)) { | |
| violationIds.add(violationId); | |
| addViolationAlert({ | |
| id: violationId, | |
| timestamp: new Date().toISOString(), | |
| type: violation.type, | |
| description: violation.description, | |
| severity: violation.severity || 'high' | |
| }); | |
| setTimeout(() => { | |
| violationIds.delete(violationId); | |
| }, 30000); | |
| } | |
| }); | |
| } | |
| } | |
| function updateFPS() { | |
| frameCount++; | |
| const now = Date.now(); | |
| if (now - lastFpsUpdate >= 1000) { | |
| const fps = Math.round(frameCount * 1000 / (now - lastFpsUpdate)); | |
| fpsCounter.textContent = `FPS: ${fps}`; | |
| frameCount = 0; | |
| lastFpsUpdate = now; | |
| } | |
| } | |
| function addViolationAlert(violation) { | |
| violationsData.unshift(violation); | |
| if (violationsData.length > 5) { | |
| violationsData = violationsData.slice(0, 5); | |
| } | |
| renderViolations(); | |
| updateViolationBadge(); | |
| } | |
| function renderViolations() { | |
| if (violationsData.length === 0) { | |
| violationsList.innerHTML = ` | |
| <div class="no-violations"> | |
| <i class="fas fa-shield-check"></i> | |
| <div>All Clear</div> | |
| <small>No safety violations detected</small> | |
| </div> | |
| `; | |
| return; | |
| } | |
| violationsList.innerHTML = violationsData.map((violation, index) => ` | |
| <div class="violation-item" style="animation-delay: ${index * 0.1}s"> | |
| <div class="violation-header"> | |
| <div class="violation-time">${formatTime(violation.timestamp)}</div> | |
| <div class="violation-severity ${violation.severity || 'high'}">${violation.severity || 'HIGH'}</div> | |
| </div> | |
| <div class="violation-description"> | |
| <strong>${violation.type || 'Safety Violation'}</strong><br> | |
| ${violation.description || 'Missing safety equipment detected'} | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function formatTime(timestamp) { | |
| return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); | |
| } | |
| function updateViolationBadge() { | |
| violationBadge.textContent = violationsData.length; | |
| } | |
| function setLoadingState(button, loading) { | |
| if (loading) { | |
| const icon = button.querySelector('i'); | |
| if (icon) { | |
| icon.className = 'loading'; | |
| } | |
| button.disabled = true; | |
| } else { | |
| button.disabled = false; | |
| // Restore original icons | |
| if (button === startBtn) { | |
| const icon = button.querySelector('i, .loading'); | |
| if (icon) { | |
| icon.className = 'fas fa-video'; | |
| } | |
| } else if (button === stopBtn) { | |
| const icon = button.querySelector('i, .loading'); | |
| if (icon) { | |
| icon.className = 'fas fa-stop'; | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |