Spaces:
Runtime error
Runtime 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</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; | |
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); | |
--border-radius: 12px; | |
--border-radius-lg: 16px; | |
} | |
* { | |
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; | |
line-height: 1.6; | |
} | |
.main-container { | |
height: 100vh; | |
display: flex; | |
flex-direction: column; | |
position: relative; | |
} | |
/* Fullscreen Video Container */ | |
.video-main { | |
flex: 1; | |
position: relative; | |
background: #000; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
overflow: hidden; | |
} | |
.video-feed { | |
width: 100%; | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.video-feed img { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
} | |
.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; | |
} | |
.no-feed p { | |
opacity: 0.7; | |
} | |
/* Floating Header */ | |
.floating-header { | |
position: absolute; | |
top: 1.5rem; | |
left: 1.5rem; | |
right: 1.5rem; | |
background: rgba(15, 23, 42, 0.8); | |
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; | |
transition: all 0.3s ease; | |
} | |
.floating-header:hover { | |
background: rgba(15, 23, 42, 0.9); | |
border-color: rgba(51, 65, 85, 0.5); | |
} | |
.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; | |
transition: all 0.3s ease; | |
} | |
.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 Stats */ | |
.floating-stats { | |
position: absolute; | |
top: 6rem; | |
left: 1.5rem; | |
display: grid; | |
grid-template-columns: repeat(4, 1fr); | |
gap: 1rem; | |
z-index: 90; | |
} | |
.mini-stat { | |
background: rgba(15, 23, 42, 0.8); | |
backdrop-filter: blur(20px); | |
border: 1px solid rgba(51, 65, 85, 0.3); | |
border-radius: 12px; | |
padding: 1rem; | |
min-width: 120px; | |
text-align: center; | |
transition: all 0.3s ease; | |
} | |
.mini-stat:hover { | |
background: rgba(15, 23, 42, 0.9); | |
transform: translateY(-2px); | |
} | |
.mini-stat-value { | |
font-size: 1.75rem; | |
font-weight: 700; | |
margin-bottom: 0.25rem; | |
} | |
.mini-stat-label { | |
font-size: 0.75rem; | |
color: var(--text-muted); | |
text-transform: uppercase; | |
letter-spacing: 0.05em; | |
} | |
.mini-stat.success .mini-stat-value { | |
color: var(--success-color); | |
} | |
.mini-stat.warning .mini-stat-value { | |
color: var(--warning-color); | |
} | |
.mini-stat.danger .mini-stat-value { | |
color: var(--danger-color); | |
} | |
/* Floating Controls */ | |
.floating-controls { | |
position: absolute; | |
bottom: 1.5rem; | |
left: 1.5rem; | |
background: rgba(15, 23, 42, 0.8); | |
backdrop-filter: blur(20px); | |
border: 1px solid rgba(51, 65, 85, 0.3); | |
border-radius: 16px; | |
padding: 1.5rem; | |
z-index: 100; | |
transition: all 0.3s ease; | |
transform: translateY(0); | |
} | |
.floating-controls.collapsed { | |
transform: translateY(calc(100% - 60px)); | |
} | |
.controls-toggle { | |
position: absolute; | |
top: -15px; | |
right: 20px; | |
background: rgba(37, 99, 235, 0.9); | |
border: none; | |
border-radius: 50px; | |
width: 30px; | |
height: 30px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
cursor: pointer; | |
font-size: 0.875rem; | |
transition: all 0.3s ease; | |
} | |
.controls-toggle:hover { | |
background: var(--primary-dark); | |
transform: scale(1.1); | |
} | |
.controls-content { | |
display: flex; | |
align-items: center; | |
gap: 1.5rem; | |
} | |
.control-group { | |
display: flex; | |
flex-direction: column; | |
gap: 0.5rem; | |
} | |
.control-label { | |
font-size: 0.75rem; | |
color: var(--text-muted); | |
text-transform: uppercase; | |
letter-spacing: 0.05em; | |
} | |
.control-input { | |
padding: 0.5rem 0.75rem; | |
background: rgba(51, 65, 85, 0.5); | |
border: 1px solid rgba(51, 65, 85, 0.5); | |
border-radius: 8px; | |
color: var(--text-primary); | |
font-size: 0.875rem; | |
width: 120px; | |
} | |
.control-input:focus { | |
outline: none; | |
border-color: var(--primary-color); | |
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); | |
} | |
.range-input { | |
-webkit-appearance: none; | |
height: 4px; | |
background: var(--bg-tertiary); | |
border-radius: 2px; | |
outline: none; | |
width: 120px; | |
} | |
.range-input::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
width: 16px; | |
height: 16px; | |
background: var(--primary-color); | |
border-radius: 50%; | |
cursor: pointer; | |
} | |
.btn { | |
padding: 0.75rem 1.5rem; | |
border: none; | |
border-radius: 10px; | |
font-size: 0.875rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
text-transform: uppercase; | |
letter-spacing: 0.05em; | |
} | |
.btn-primary { | |
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
color: white; | |
} | |
.btn-primary:hover:not(:disabled) { | |
transform: translateY(-2px); | |
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.3); | |
} | |
.btn-danger { | |
background: linear-gradient(135deg, var(--danger-color), #dc2626); | |
color: white; | |
} | |
.btn-danger:hover:not(:disabled) { | |
transform: translateY(-2px); | |
box-shadow: 0 10px 20px rgba(239, 68, 68, 0.3); | |
} | |
.btn:disabled { | |
opacity: 0.5; | |
cursor: not-allowed; | |
transform: none ; | |
} | |
/* Floating FPS */ | |
.floating-fps { | |
position: absolute; | |
top: 1.5rem; | |
right: 1.5rem; | |
background: rgba(15, 23, 42, 0.8); | |
backdrop-filter: blur(20px); | |
border: 1px solid rgba(51, 65, 85, 0.3); | |
border-radius: 12px; | |
padding: 0.75rem 1rem; | |
font-size: 0.875rem; | |
font-weight: 600; | |
color: var(--primary-color); | |
z-index: 100; | |
} | |
/* Floating Violations */ | |
.floating-violations { | |
position: absolute; | |
bottom: 1.5rem; | |
right: 1.5rem; | |
width: 300px; | |
max-height: 400px; | |
background: rgba(15, 23, 42, 0.8); | |
backdrop-filter: blur(20px); | |
border: 1px solid rgba(51, 65, 85, 0.3); | |
border-radius: 16px; | |
overflow: hidden; | |
z-index: 100; | |
transition: all 0.3s ease; | |
transform: translateX(0); | |
} | |
.floating-violations.collapsed { | |
transform: translateX(calc(100% - 60px)); | |
} | |
.violations-header { | |
padding: 1rem 1.5rem; | |
border-bottom: 1px solid rgba(51, 65, 85, 0.3); | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
} | |
.violations-title { | |
display: flex; | |
align-items: center; | |
gap: 0.75rem; | |
font-size: 1rem; | |
font-weight: 600; | |
} | |
.violations-badge { | |
background: rgba(239, 68, 68, 0.2); | |
color: var(--danger-color); | |
padding: 0.25rem 0.5rem; | |
border-radius: 6px; | |
font-size: 0.75rem; | |
font-weight: 600; | |
} | |
.violations-toggle { | |
background: none; | |
border: none; | |
color: var(--text-muted); | |
cursor: pointer; | |
padding: 0.25rem; | |
border-radius: 4px; | |
transition: all 0.3s ease; | |
} | |
.violations-toggle:hover { | |
background: rgba(51, 65, 85, 0.5); | |
color: var(--text-primary); | |
} | |
.violations-content { | |
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: 10px; | |
padding: 1rem; | |
margin-bottom: 0.75rem; | |
opacity: 0; | |
animation: violationSlideIn 0.5s ease-out forwards; | |
transition: all 0.3s ease; | |
} | |
.violation-item:hover { | |
background: rgba(239, 68, 68, 0.15); | |
transform: translateX(-4px); | |
} | |
.violation-item:last-child { | |
margin-bottom: 0; | |
} | |
@keyframes violationSlideIn { | |
from { | |
opacity: 0; | |
transform: translateY(20px) scale(0.95); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0) scale(1); | |
} | |
} | |
.violation-header { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
margin-bottom: 0.5rem; | |
} | |
.violation-time { | |
font-size: 0.75rem; | |
color: var(--text-muted); | |
font-weight: 500; | |
} | |
.violation-severity { | |
padding: 0.25rem 0.5rem; | |
border-radius: 4px; | |
font-size: 0.75rem; | |
font-weight: 600; | |
text-transform: uppercase; | |
letter-spacing: 0.05em; | |
} | |
.violation-severity.high { | |
background: rgba(239, 68, 68, 0.2); | |
color: var(--danger-color); | |
} | |
.violation-description { | |
font-size: 0.875rem; | |
color: var(--text-secondary); | |
line-height: 1.4; | |
} | |
.no-violations { | |
text-align: center; | |
color: var(--text-muted); | |
padding: 2rem 1rem; | |
} | |
.no-violations i { | |
font-size: 2rem; | |
margin-bottom: 1rem; | |
color: var(--success-color); | |
opacity: 0.5; | |
} | |
/* Loading Animation */ | |
.loading { | |
display: inline-block; | |
width: 16px; | |
height: 16px; | |
border: 2px solid rgba(255, 255, 255, 0.3); | |
border-radius: 50%; | |
border-top-color: currentColor; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
to { | |
transform: rotate(360deg); | |
} | |
} | |
/* Scrollbar Styling */ | |
::-webkit-scrollbar { | |
width: 4px; | |
} | |
::-webkit-scrollbar-track { | |
background: transparent; | |
} | |
::-webkit-scrollbar-thumb { | |
background: rgba(100, 116, 139, 0.5); | |
border-radius: 2px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: rgba(100, 116, 139, 0.7); | |
} | |
/* Responsive Design */ | |
@media (max-width: 1024px) { | |
.floating-stats { | |
grid-template-columns: repeat(2, 1fr); | |
top: 5rem; | |
} | |
.floating-violations { | |
width: 280px; | |
} | |
} | |
@media (max-width: 768px) { | |
.floating-header { | |
top: 1rem; | |
left: 1rem; | |
right: 1rem; | |
padding: 0.75rem 1rem; | |
} | |
.floating-stats { | |
top: 4.5rem; | |
left: 1rem; | |
grid-template-columns: repeat(2, 1fr); | |
gap: 0.75rem; | |
} | |
.mini-stat { | |
padding: 0.75rem; | |
min-width: 100px; | |
} | |
.floating-controls { | |
bottom: 1rem; | |
left: 1rem; | |
right: 1rem; | |
padding: 1rem; | |
} | |
.controls-content { | |
flex-wrap: wrap; | |
gap: 1rem; | |
} | |
.floating-violations { | |
bottom: 1rem; | |
right: 1rem; | |
width: 260px; | |
} | |
} | |
/* Fade in animation for page load */ | |
.main-container { | |
animation: fadeIn 0.5s ease-out; | |
} | |
@keyframes fadeIn { | |
from { | |
opacity: 0; | |
} | |
to { | |
opacity: 1; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="main-container"> | |
<!-- Fullscreen Video --> | |
<div class="video-main"> | |
<div class="video-feed" id="videoFeed"> | |
<div class="no-feed"> | |
<i class="fas fa-video-slash"></i> | |
<h3>SafetyMaster Pro</h3> | |
<p>Click "Start" to begin AI safety monitoring</p> | |
</div> | |
</div> | |
</div> | |
<!-- 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" id="statusBadge"> | |
<div class="status-indicator"></div> | |
<span id="statusText">Disconnected</span> | |
</div> | |
</div> | |
</div> | |
<!-- Floating FPS Counter --> | |
<div class="floating-fps" id="fpsCounter">FPS: 0</div> | |
<!-- Floating Statistics --> | |
<div class="floating-stats"> | |
<div class="mini-stat"> | |
<div class="mini-stat-value" id="totalPeople">0</div> | |
<div class="mini-stat-label">People</div> | |
</div> | |
<div class="mini-stat success"> | |
<div class="mini-stat-value" id="compliantPeople">0</div> | |
<div class="mini-stat-label">Compliant</div> | |
</div> | |
<div class="mini-stat danger"> | |
<div class="mini-stat-value" id="violationCount">0</div> | |
<div class="mini-stat-label">Violations</div> | |
</div> | |
<div class="mini-stat warning"> | |
<div class="mini-stat-value" id="complianceRate">0%</div> | |
<div class="mini-stat-label">Compliance</div> | |
</div> | |
</div> | |
<!-- Floating Controls --> | |
<div class="floating-controls" id="floatingControls"> | |
<button class="controls-toggle" id="controlsToggle"> | |
<i class="fas fa-chevron-down"></i> | |
</button> | |
<div class="controls-content"> | |
<div class="control-group"> | |
<label class="control-label">Camera</label> | |
<input type="number" class="control-input" id="cameraSource" value="0" min="0"> | |
</div> | |
<div class="control-group"> | |
<label class="control-label">Confidence: <span id="confidenceValue">0.5</span></label> | |
<input type="range" class="control-input range-input" id="confidenceSlider" min="0.1" max="1" step="0.1" value="0.5"> | |
</div> | |
<div class="control-group"> | |
<button class="btn btn-primary" id="startBtn"> | |
<i class="fas fa-play"></i> | |
Start | |
</button> | |
</div> | |
<div class="control-group"> | |
<button class="btn btn-danger" id="stopBtn" disabled> | |
<i class="fas fa-stop"></i> | |
Stop | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Floating Violations --> | |
<div class="floating-violations" id="floatingViolations"> | |
<div class="violations-header"> | |
<div class="violations-title"> | |
<i class="fas fa-exclamation-triangle"></i> | |
Violations | |
<span class="violations-badge" id="violationBadge">0</span> | |
</div> | |
<button class="violations-toggle" id="violationsToggle"> | |
<i class="fas fa-chevron-right"></i> | |
</button> | |
</div> | |
<div class="violations-content" 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> | |
</div> | |
<script> | |
// Initialize Socket.IO connection | |
const socket = io(); | |
// DOM elements | |
const statusBadge = document.getElementById('statusBadge'); | |
const statusText = document.getElementById('statusText'); | |
const videoFeed = document.getElementById('videoFeed'); | |
const fpsCounter = document.getElementById('fpsCounter'); | |
const startBtn = document.getElementById('startBtn'); | |
const stopBtn = document.getElementById('stopBtn'); | |
const cameraSource = document.getElementById('cameraSource'); | |
const confidenceSlider = document.getElementById('confidenceSlider'); | |
const confidenceValue = document.getElementById('confidenceValue'); | |
const violationsList = document.getElementById('violationsList'); | |
const violationBadge = document.getElementById('violationBadge'); | |
// Floating panel elements | |
const controlsToggle = document.getElementById('controlsToggle'); | |
const floatingControls = document.getElementById('floatingControls'); | |
const violationsToggle = document.getElementById('violationsToggle'); | |
const floatingViolations = document.getElementById('floatingViolations'); | |
// Statistics elements | |
const totalPeople = document.getElementById('totalPeople'); | |
const compliantPeople = document.getElementById('compliantPeople'); | |
const violationCount = document.getElementById('violationCount'); | |
const complianceRate = document.getElementById('complianceRate'); | |
// State variables | |
let isMonitoring = false; | |
let frameCount = 0; | |
let lastFpsUpdate = Date.now(); | |
let violationsData = []; | |
let violationIds = new Set(); // Track violation IDs to prevent duplicates | |
// Socket event handlers | |
socket.on('connect', function() { | |
console.log('Connected to server'); | |
updateConnectionStatus(true); | |
}); | |
socket.on('disconnect', function() { | |
console.log('Disconnected from server'); | |
updateConnectionStatus(false); | |
}); | |
socket.on('video_frame', function(data) { | |
updateVideoFeed(data); | |
updateStatistics(data); | |
updateFPS(); | |
}); | |
socket.on('violation_alert', function(data) { | |
addViolationAlert(data); | |
}); | |
socket.on('status', function(data) { | |
console.log('Status update:', data); | |
}); | |
// UI event handlers | |
startBtn.addEventListener('click', startMonitoring); | |
stopBtn.addEventListener('click', stopMonitoring); | |
confidenceSlider.addEventListener('input', function() { | |
confidenceValue.textContent = this.value; | |
}); | |
// Floating panel toggles | |
controlsToggle.addEventListener('click', function() { | |
floatingControls.classList.toggle('collapsed'); | |
const icon = this.querySelector('i'); | |
if (floatingControls.classList.contains('collapsed')) { | |
icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); | |
} else { | |
icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); | |
} | |
}); | |
violationsToggle.addEventListener('click', function() { | |
floatingViolations.classList.toggle('collapsed'); | |
const icon = this.querySelector('i'); | |
if (floatingViolations.classList.contains('collapsed')) { | |
icon.classList.replace('fa-chevron-right', 'fa-chevron-left'); | |
} else { | |
icon.classList.replace('fa-chevron-left', 'fa-chevron-right'); | |
} | |
}); | |
// Functions | |
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 updateVideoFeed(data) { | |
const img = new Image(); | |
img.onload = function() { | |
videoFeed.innerHTML = ''; | |
videoFeed.appendChild(img); | |
}; | |
img.onerror = function() { | |
showNoFeed(); | |
}; | |
img.src = 'data:image/jpeg;base64,' + data.frame; | |
} | |
function showNoFeed() { | |
videoFeed.innerHTML = ` | |
<div class="no-feed"> | |
<i class="fas fa-video-slash"></i> | |
<h3>Camera Disconnected</h3> | |
<p>Check camera connection and try again</p> | |
</div> | |
`; | |
} | |
function updateStatistics(data) { | |
totalPeople.textContent = data.people_count || 0; | |
// Calculate compliant people (people - violations) | |
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 present (with duplicate prevention) | |
if (data.violations && data.violations.length > 0) { | |
data.violations.forEach(violation => { | |
const violationId = `${violation.type}_${violation.description}_${Math.floor(Date.now() / 5000)}`; // Group by 5-second intervals | |
if (!violationIds.has(violationId)) { | |
violationIds.add(violationId); | |
addViolationAlert({ | |
id: violationId, | |
timestamp: new Date().toISOString(), | |
type: violation.type, | |
description: violation.description, | |
severity: violation.severity || 'high' | |
}); | |
// Clean up old IDs after 30 seconds | |
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) { | |
button.innerHTML = button.innerHTML.replace(/<i[^>]*><\/i>/, '<div class="loading"></div>'); | |
button.disabled = true; | |
} else { | |
// Restore original icon based on button | |
if (button === startBtn) { | |
button.innerHTML = '<i class="fas fa-play"></i> Start'; | |
} else if (button === stopBtn) { | |
button.innerHTML = '<i class="fas fa-stop"></i> Stop'; | |
} | |
} | |
} | |
function startMonitoring() { | |
const source = parseInt(cameraSource.value) || 0; | |
const confidence = parseFloat(confidenceSlider.value); | |
setLoadingState(startBtn, true); | |
fetch('/api/start_monitoring', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
camera_source: source, | |
confidence: confidence | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
isMonitoring = true; | |
updateUI(); | |
console.log('Monitoring started:', data); | |
} else { | |
alert('Failed to start monitoring: ' + data.message); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('Failed to start monitoring'); | |
}) | |
.finally(() => { | |
setLoadingState(startBtn, false); | |
updateUI(); | |
}); | |
} | |
function stopMonitoring() { | |
setLoadingState(stopBtn, true); | |
fetch('/api/stop_monitoring', { | |
method: 'POST' | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
isMonitoring = false; | |
updateUI(); | |
showNoFeed(); | |
fpsCounter.textContent = 'FPS: 0'; | |
// Reset statistics | |
totalPeople.textContent = '0'; | |
compliantPeople.textContent = '0'; | |
violationCount.textContent = '0'; | |
complianceRate.textContent = '0%'; | |
// Clear violations | |
violationsData = []; | |
violationIds.clear(); | |
renderViolations(); | |
updateViolationBadge(); | |
console.log('Monitoring stopped:', data); | |
} else { | |
alert('Failed to stop monitoring: ' + data.message); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('Failed to stop monitoring'); | |
}) | |
.finally(() => { | |
setLoadingState(stopBtn, false); | |
updateUI(); | |
}); | |
} | |
function updateUI() { | |
startBtn.disabled = isMonitoring; | |
stopBtn.disabled = !isMonitoring; | |
} | |
// Load initial violations | |
fetch('/api/violations') | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
violationsData = data.violations || []; | |
renderViolations(); | |
updateViolationBadge(); | |
} | |
}) | |
.catch(error => console.error('Error loading violations:', error)); | |
// Initial UI update | |
updateUI(); | |
</script> | |
</body> | |
</html> |