| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Video Analysis - NETRA</title> |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/dark-theme.css') }}"> |
| <style> |
| |
| .model-checkbox-label { |
| display: flex; |
| align-items: center; |
| gap: 0.8rem; |
| padding: 0.8rem; |
| border-radius: 10px; |
| cursor: pointer; |
| transition: var(--transition); |
| background: var(--white); |
| border: 1px solid var(--line); |
| } |
| .model-checkbox-label:hover { background: var(--blue-100); border-color: var(--blue-600); } |
| .model-checkbox-label input { cursor: pointer; width: 18px; height: 18px; } |
| .panel-header { cursor: pointer; user-select: none; transition: var(--transition); } |
| .panel-header:hover { background: rgba(28,110,242,0.05); } |
| .panel-actions { display: flex; gap: 0.8rem; margin-top: 1rem; flex-wrap: wrap; } |
| .panel-actions .btn { flex: 1; min-width: 120px; } |
| |
| |
| #alert-banner { |
| display: none; |
| position: fixed; |
| top: 70px; |
| left: 50%; |
| transform: translateX(-50%); |
| z-index: 9999; |
| background: var(--danger); |
| color: white; |
| padding: 0.9rem 2rem; |
| border-radius: 12px; |
| font-weight: 700; |
| font-size: 1.05rem; |
| box-shadow: 0 8px 30px rgba(229,57,53,0.45); |
| animation: pulseBanner 1s ease-in-out infinite; |
| text-align: center; |
| min-width: 340px; |
| max-width: 90vw; |
| } |
| @keyframes pulseBanner { |
| 0%,100% { box-shadow: 0 8px 30px rgba(229,57,53,0.45); } |
| 50% { box-shadow: 0 8px 50px rgba(229,57,53,0.85); transform: translateX(-50%) scale(1.02); } |
| } |
| |
| |
| #realtime-panel { |
| display: none; |
| background: var(--white); |
| border: 2px solid var(--blue-200); |
| border-radius: var(--radius-md); |
| padding: 1.5rem; |
| margin-bottom: 1.5rem; |
| box-shadow: var(--shadow-sm); |
| } |
| .rt-header { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1rem; } |
| .rt-spinner { width: 20px; height: 20px; border: 3px solid var(--blue-200); border-top-color: var(--blue-600); border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .rt-frame-info { color: var(--slate-700); font-size: 0.9rem; margin-bottom: 0.5rem; } |
| |
| |
| .rt-progress-wrap { background: var(--blue-100); border-radius: 99px; height: 10px; overflow: hidden; margin-bottom: 1rem; } |
| .rt-progress-fill { height: 100%; background: linear-gradient(90deg, var(--blue-600), var(--orange-600)); border-radius: 99px; transition: width 0.5s ease; width: 0%; } |
| |
| |
| .rt-stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; } |
| .rt-stat { flex: 1; min-width: 80px; background: var(--blue-50); border: 1px solid var(--blue-200); border-radius: 10px; padding: 0.7rem 1rem; text-align: center; } |
| .rt-stat-val { font-size: 1.6rem; font-weight: 700; color: var(--blue-700); } |
| .rt-stat-lbl { font-size: 0.75rem; color: var(--slate-700); margin-top: 0.2rem; } |
| |
| |
| #live-alerts-feed { |
| max-height: 200px; |
| overflow-y: auto; |
| border: 1px solid var(--line); |
| border-radius: 10px; |
| padding: 0.5rem; |
| background: var(--white); |
| } |
| .feed-item { |
| display: flex; |
| align-items: flex-start; |
| gap: 0.6rem; |
| padding: 0.5rem 0.6rem; |
| border-radius: 8px; |
| margin-bottom: 0.4rem; |
| font-size: 0.85rem; |
| animation: feedSlide 0.3s ease; |
| } |
| @keyframes feedSlide { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; } } |
| .feed-item.critical { background: #ffeef0; border-left: 3px solid var(--danger); } |
| .feed-item.high { background: #fff3e0; border-left: 3px solid var(--warn); } |
| .feed-item.medium { background: var(--blue-50); border-left: 3px solid var(--info); } |
| .feed-item.low { background: #f1f8f4; border-left: 3px solid var(--ok); } |
| .feed-badge { font-weight: 700; font-size: 0.75rem; padding: 2px 6px; border-radius: 4px; flex-shrink: 0; margin-top: 1px; } |
| .feed-badge.critical { background: var(--danger); color: white; } |
| .feed-badge.high { background: var(--warn); color: white; } |
| .feed-badge.medium { background: var(--info); color: white; } |
| .feed-badge.low { background: var(--ok); color: white; } |
| |
| |
| .processing-video-panel { |
| background: var(--white); |
| border: 2px solid var(--blue-200); |
| border-radius: var(--radius-md); |
| overflow: hidden; |
| box-shadow: var(--shadow-sm); |
| position: relative; |
| } |
| .processing-video-container { |
| position: relative; |
| background: #000; |
| width: 100%; |
| padding-bottom: 56.25%; |
| overflow: hidden; |
| border-bottom: 1px solid var(--line); |
| } |
| .processing-video-content { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| .processing-overlay { |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: rgba(0, 0, 0, 0.6); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| gap: 1rem; |
| z-index: 10; |
| } |
| .processing-spinner { |
| width: 50px; |
| height: 50px; |
| border: 4px solid rgba(28, 110, 242, 0.2); |
| border-top-color: var(--blue-600); |
| border-radius: 50%; |
| animation: spin 0.8s linear infinite; |
| } |
| .processing-info { |
| color: white; |
| text-align: center; |
| font-size: 0.95rem; |
| } |
| .processing-info strong { display: block; margin-bottom: 0.3rem; font-size: 1.1rem; } |
| .processing-progress { |
| width: 200px; |
| height: 6px; |
| background: rgba(255, 255, 255, 0.2); |
| border-radius: 3px; |
| overflow: hidden; |
| margin-top: 0.5rem; |
| } |
| .processing-bar { |
| height: 100%; |
| background: linear-gradient(90deg, var(--blue-600), var(--orange-600)); |
| width: 0%; |
| border-radius: 3px; |
| transition: width 0.4s ease; |
| } |
| .processing-video-info { |
| padding: 1rem; |
| background: var(--blue-50); |
| border-top: 1px solid var(--blue-200); |
| font-size: 0.9rem; |
| color: var(--slate-700); |
| } |
| .processing-video-info strong { color: var(--blue-900); } |
| |
| |
| .results-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; } |
| .res-card { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-md); padding: 1.1rem; text-align: center; box-shadow: var(--shadow-sm); } |
| .res-card.danger { border-color: #ffcdd2; background: #fff5f5; } |
| .res-card.warn { border-color: #ffe0b2; background: #fffbf0; } |
| .res-val { font-size: 2rem; font-weight: 700; color: var(--blue-700); } |
| .res-card.danger .res-val { color: var(--danger); } |
| .res-card.warn .res-val { color: var(--warn); } |
| .res-lbl { font-size: 0.8rem; color: var(--slate-700); margin-top: 0.3rem; } |
| |
| |
| .history-item { display: flex; gap: 1rem; align-items: flex-start; padding: 1rem; border: 1px solid var(--line); border-radius: 10px; background: var(--white); transition: var(--transition); } |
| .history-item:hover { border-color: var(--blue-600); box-shadow: var(--shadow-sm); } |
| .history-item + .history-item { margin-top: 0.6rem; } |
| .history-thumb { width: 90px; height: 60px; object-fit: cover; border-radius: 6px; flex-shrink: 0; background: var(--blue-50); } |
| .history-meta { flex: 1; min-width: 0; } |
| .history-badges { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; } |
| .hbadge { font-size: 0.75rem; padding: 2px 7px; border-radius: 10px; background: var(--blue-100); color: var(--blue-700); font-weight: 600; } |
| .hbadge.red { background: #ffeef0; color: var(--danger); } |
| |
| |
| .det-record { display: flex; gap: 0.8rem; align-items: flex-start; padding: 0.8rem; border: 1px solid var(--line); border-radius: 10px; background: var(--white); } |
| .det-record + .det-record { margin-top: 0.5rem; } |
| .det-thumb { width: 70px; height: 55px; object-fit: cover; border-radius: 6px; flex-shrink: 0; } |
| .det-info { flex: 1; min-width: 0; font-size: 0.85rem; } |
| .badge-sev { display: inline-block; padding: 2px 8px; border-radius: 6px; font-weight: 700; font-size: 0.75rem; color: white; } |
| .badge-sev.CRITICAL,.badge-sev.HIGH { background: var(--danger); } |
| .badge-sev.MEDIUM { background: var(--warn); } |
| .badge-sev.LOW { background: var(--ok); } |
| |
| @media (max-width: 640px) { |
| .panel-actions { flex-direction: column; } |
| .rt-stats { gap: 0.5rem; } |
| } |
| |
| |
| #insights-panel { |
| animation: fadeIn 0.5s ease; |
| } |
| .insight-card { |
| background: linear-gradient(135deg, var(--blue-50) 0%, var(--blue-100) 100%); |
| border: 2px solid var(--blue-200); |
| border-radius: 12px; |
| padding: 1.2rem; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| position: relative; |
| overflow: hidden; |
| } |
| .insight-card:hover { |
| border-color: var(--blue-600); |
| box-shadow: 0 8px 20px rgba(28, 110, 242, 0.25); |
| transform: translateY(-2px); |
| } |
| .insight-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: linear-gradient(135deg, rgba(28, 110, 242, 0.05) 0%, transparent 100%); |
| pointer-events: none; |
| } |
| .insight-card.alert { |
| background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%); |
| border-color: #ffb3b3; |
| } |
| .insight-card.alert:hover { |
| border-color: var(--danger); |
| box-shadow: 0 8px 20px rgba(229, 57, 53, 0.25); |
| } |
| .insight-card.warning { |
| background: linear-gradient(135deg, #fffbf0 0%, #ffe0b2 100%); |
| border-color: #ffb74d; |
| } |
| .insight-card.warning:hover { |
| border-color: var(--warn); |
| box-shadow: 0 8px 20px rgba(255, 152, 0, 0.25); |
| } |
| .insight-icon { |
| font-size: 2rem; |
| margin-bottom: 0.5rem; |
| } |
| .insight-label { |
| font-size: 0.85rem; |
| color: var(--slate-700); |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| margin-bottom: 0.3rem; |
| } |
| .insight-value { |
| font-size: 1.8rem; |
| font-weight: 700; |
| color: var(--blue-700); |
| margin-bottom: 0.2rem; |
| } |
| .insight-card.alert .insight-value { |
| color: var(--danger); |
| } |
| .insight-card.warning .insight-value { |
| color: var(--warn); |
| } |
| .insight-desc { |
| font-size: 0.75rem; |
| color: var(--slate-600); |
| margin-top: 0.4rem; |
| line-height: 1.4; |
| } |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div id="alert-banner"> |
| <span id="alert-banner-text">π¨ HIGH RISK DETECTED</span> |
| <button onclick="dismissBanner()" style="margin-left:1.2rem;background:rgba(255,255,255,0.25);border:none;color:white;cursor:pointer;border-radius:6px;padding:3px 8px;font-size:0.85rem;">β</button> |
| </div> |
|
|
| |
| <div id="toast-container"></div> |
|
|
| |
| <nav class="navbar"> |
| <div class="nav-container"> |
| <div class="nav-brand"> |
| <span class="brand-icon">ποΈ</span> |
| <span>NETRA</span> |
| </div> |
| <button class="hamburger-menu" id="hamburger-toggle"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </button> |
| <div class="nav-menu" id="nav-menu"> |
| <a href="{{ url_for('dashboard') }}" class="nav-link">Dashboard</a> |
| <div class="dropdown" id="analysis-dropdown"> |
| <button class="nav-link dropdown-btn" onclick="toggleDropdown(event)">π Analysis βΌ</button> |
| <div class="dropdown-content"> |
| <a href="{{ url_for('live_camera') }}">πΉ Live Camera</a> |
| <a href="{{ url_for('video_analysis') }}" style="opacity:0.6;">π Video Analysis</a> |
| </div> |
| </div> |
| <a href="{{ url_for('logout') }}" class="btn btn-outline">Logout</a> |
| </div> |
| </div> |
| </nav> |
|
|
| <div class="content-container"> |
| |
| <div class="page-header reveal"> |
| <h1>πΉ Video Analysis</h1> |
| <p>Upload a video and watch AI detect threats in real-time</p> |
| </div> |
|
|
| |
| <div class="upload-section reveal"> |
| <div class="model-selection-panel"> |
| <div class="panel-header" onclick="toggleModelPanel()" style="padding:1rem;border-bottom:1px solid var(--line);"> |
| <div style="display:flex;align-items:center;justify-content:space-between;"> |
| <span class="panel-title">π€ Select Detection Models</span> |
| <span id="model-toggle-icon">βΌ</span> |
| </div> |
| </div> |
| <div id="model-panel-content" class="panel-content"> |
| <div class="models-grid" id="models-grid"> |
| <p style="text-align:center;color:var(--slate-700);"> |
| <span class="spinner" style="width:14px;height:14px;margin-right:0.5rem;"></span> |
| Loading available models... |
| </p> |
| </div> |
| <div class="panel-actions"> |
| <button class="btn btn-secondary" onclick="selectAllModels()">β Select All</button> |
| <button class="btn btn-secondary" onclick="deselectAllModels()">β Deselect All</button> |
| <button class="btn btn-primary" onclick="applyModelSelection()">βοΈ Apply</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="upload-section reveal" id="upload-section"> |
| <div class="upload-box" id="upload-box" onclick="document.getElementById('video-file').click()" title="Click or drag & drop a video file"> |
| <div class="upload-icon" style="animation:bounce 1.5s ease-in-out infinite;">π</div> |
| <h3>Upload Video File</h3> |
| <p>Click to select or drag and drop</p> |
| <small style="color:var(--slate-500);">Supported: MP4, AVI, MOV, MKV (Max 500 MB)</small> |
| <input type="file" id="video-file" accept="video/*" style="display:none;" onchange="handleFileSelect(event)"> |
| </div> |
|
|
| |
| <div id="file-info" style="display:none;align-items:center;gap:1rem;padding:1rem;background:var(--blue-50);border:1px solid var(--blue-200);border-radius:var(--radius-md);"> |
| <span style="font-size:2rem;">π¬</span> |
| <div style="flex:1;min-width:0;"> |
| <strong id="file-name" style="word-break:break-all;"></strong> |
| <p id="file-size" style="color:var(--slate-700);margin-top:0.2rem;font-size:0.9rem;"></p> |
| </div> |
| <div style="display:flex;gap:0.6rem;flex-shrink:0;"> |
| <button class="btn btn-secondary" onclick="cancelFile()">β Cancel</button> |
| <button class="btn btn-primary" onclick="uploadAndAnalyze()">π Analyze</button> |
| </div> |
| </div> |
|
|
| |
| <div id="upload-progress" style="display:none;margin-top:1rem;"> |
| <div class="progress-bar"><div class="progress-fill" id="upload-fill"></div></div> |
| <p id="upload-text" style="color:var(--slate-700);margin-top:0.5rem;font-size:0.9rem;text-align:center;">Uploading...</p> |
| </div> |
| </div> |
|
|
| |
| <div id="realtime-panel"> |
| <div class="rt-header"> |
| <div class="rt-spinner"></div> |
| <div> |
| <strong style="color:var(--blue-900);">Analysing video in real-timeβ¦</strong> |
| <p class="rt-frame-info" id="rt-frame-info">Preparingβ¦</p> |
| </div> |
| </div> |
|
|
| <div class="rt-progress-wrap"> |
| <div class="rt-progress-fill" id="rt-progress-fill"></div> |
| </div> |
|
|
| |
| <div class="processing-video-panel" style="margin-bottom:1rem;"> |
| <div class="processing-video-container"> |
| <div class="processing-video-content"> |
| <div id="processing-video-frame" style="width:100%;height:100%;background:var(--slate-900);"></div> |
| </div> |
| <div id="processing-overlay" class="processing-overlay"> |
| <div class="processing-spinner"></div> |
| <div class="processing-info"> |
| <strong id="processing-status">Processing Videoβ¦</strong> |
| <div id="processing-frame-count">Frame 0 of 0</div> |
| <div class="processing-progress"> |
| <div class="processing-bar" id="processing-bar-fill"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="processing-video-info"> |
| <div><strong>π Live Stats:</strong></div> |
| <div>Detections: <span id="proc-detections">0</span> | Alerts: <span id="proc-alerts">0</span> | High Risk: <span id="proc-high-risk" style="color:var(--danger);">0</span></div> |
| </div> |
| </div> |
|
|
| <div class="rt-stats"> |
| <div class="rt-stat"> |
| <div class="rt-stat-val" id="rt-detections">0</div> |
| <div class="rt-stat-lbl">Detections</div> |
| </div> |
| <div class="rt-stat"> |
| <div class="rt-stat-val" id="rt-alerts" style="color:var(--danger);">0</div> |
| <div class="rt-stat-lbl">Alerts</div> |
| </div> |
| <div class="rt-stat"> |
| <div class="rt-stat-val" id="rt-high-risk" style="color:var(--danger);">0</div> |
| <div class="rt-stat-lbl">High Risk</div> |
| </div> |
| <div class="rt-stat" style="flex:2;"> |
| <div style="font-size:0.8rem;color:var(--slate-700);text-align:left;">Live Threat Feed</div> |
| <div id="live-alerts-feed" style="margin-top:0.3rem;"> |
| <p style="color:var(--slate-500);font-size:0.85rem;padding:0.3rem;">No alerts yet β all clear</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="results-section" style="display:none;"> |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;flex-wrap:wrap;gap:1rem;"> |
| <h2 style="color:var(--blue-900);">π Analysis Results</h2> |
| <div style="display:flex;gap:0.8rem;flex-wrap:wrap;"> |
| <button class="btn btn-primary" onclick="reanalyzeVideo()" id="reanalyze-btn" style="display:none;">π Re-analyze</button> |
| <button class="btn btn-secondary" onclick="startNewAnalysis()">π New Analysis</button> |
| </div> |
| </div> |
|
|
| |
| <div class="results-grid" id="summary-cards"> |
| <div class="res-card"><div class="res-val" id="res-total-frames">0</div><div class="res-lbl">Total Frames</div></div> |
| <div class="res-card"><div class="res-val" id="res-analyzed">0</div><div class="res-lbl">Analyzed</div></div> |
| <div class="res-card"><div class="res-val" id="res-detections">0</div><div class="res-lbl">Detections</div></div> |
| <div class="res-card warn"><div class="res-val" id="res-alerts">0</div><div class="res-lbl">Alerts</div></div> |
| <div class="res-card danger"><div class="res-val" id="res-emergency">0</div><div class="res-lbl">Emergency Frames</div></div> |
| </div> |
|
|
| |
| <div class="detections-table" style="margin-bottom:1.5rem;"> |
| <h3>π¬ Processed Video</h3> |
| <div id="media-results" class="detections-list"></div> |
| </div> |
|
|
| |
| <div id="emergency-section" class="detections-table" style="display:none;margin-bottom:1.5rem;"> |
| <h3>π¨ Emergency Frames</h3> |
| <div id="emergency-gallery" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;"></div> |
| </div> |
|
|
| |
| <div class="detections-table" style="margin-bottom:1.5rem;"> |
| <h3>π Quick Insights</h3> |
| <div id="insights-panel" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script src="{{ url_for('static', filename='js/ui-utils.js') }}"></script> |
| <script src="{{ url_for('static', filename='js/video_analysis.js') }}"></script> |
| |
| <script> |
| |
| const hamburger = document.getElementById('hamburger-toggle'); |
| const navMenu = document.getElementById('nav-menu'); |
| |
| if (hamburger) { |
| hamburger.addEventListener('click', function(e) { |
| e.stopPropagation(); |
| hamburger.classList.toggle('active'); |
| navMenu.classList.toggle('active'); |
| }); |
| } |
| |
| |
| document.querySelectorAll('.nav-link, .nav-menu a').forEach(link => { |
| link.addEventListener('click', function() { |
| hamburger?.classList.remove('active'); |
| navMenu?.classList.remove('active'); |
| }); |
| }); |
| |
| |
| document.addEventListener('click', function(e) { |
| if (!e.target.closest('.nav-container')) { |
| hamburger?.classList.remove('active'); |
| navMenu?.classList.remove('active'); |
| } |
| }); |
| |
| |
| function toggleDropdown(e) { |
| e.preventDefault(); |
| const dropdown = e.target.closest('.dropdown'); |
| dropdown.classList.toggle('active'); |
| } |
| </script> |
| </body> |
| </html> |
|
|