// ─── Video Analysis – NETRA ───────────────────────────────────────────────── let selectedFile = null; let availableModels = {}; let selectedModels = []; let currentAnalysisId = null; let currentJobId = null; let pollTimer = null; let seenHighRiskCount = 0; // track alerts already shown in banner let bannerDismissTimer = null; // ─── Init ──────────────────────────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", async () => { await loadAvailableModels(); await loadSelectedModels(); wireDropzone(); }); // ─── Model selection ───────────────────────────────────────────────────────── async function loadAvailableModels() { try { const data = await (await fetch("/api/available-models")).json(); availableModels = data.models || {}; renderModelSelection(); } catch (e) { showToast("Failed to load models", "error"); } } async function loadSelectedModels() { try { const data = await (await fetch("/api/get-selected-models")).json(); selectedModels = data.selected_models || []; updateModelCheckboxes(); } catch (e) { /* silent */ } } function renderModelSelection() { const grid = document.getElementById("models-grid"); if (!grid) return; // Empty selectedModels means no saved preference → default all checked const allByDefault = selectedModels.length === 0; const cards = Object.entries(availableModels).map( ([key, info]) => `
`, ); grid.innerHTML = cards.length ? cards.join("") : "

No models available

"; } function updateModelCheckboxes() { // Empty selectedModels = no saved preference → keep all checked if (selectedModels.length === 0) return; document.querySelectorAll(".model-checkbox").forEach((cb) => { cb.checked = selectedModels.includes(cb.dataset.modelId); }); } function toggleModelPanel() { const c = document.getElementById("model-panel-content"); const i = document.getElementById("model-toggle-icon"); if (!c) return; c.classList.toggle("open"); i.textContent = c.classList.contains("open") ? "▲" : "▼"; } function selectAllModels() { document .querySelectorAll(".model-checkbox") .forEach((cb) => (cb.checked = true)); } function deselectAllModels() { document .querySelectorAll(".model-checkbox") .forEach((cb) => (cb.checked = false)); } async function applyModelSelection() { const selected = Array.from( document.querySelectorAll(".model-checkbox:checked"), ).map((cb) => cb.dataset.modelId); try { const data = await ( await fetch("/api/set-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ models: selected }), }) ).json(); if (data.success) { selectedModels = selected; showToast(`Applied ${selected.length || "all"} model(s)`, "success"); } else { showToast("Failed to apply models", "error"); } } catch (e) { showToast("Error applying models", "error"); } } // ─── File handling ─────────────────────────────────────────────────────────── function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; const validExts = /\.(mp4|avi|mov|mkv)$/i; if (!validExts.test(file.name)) { showToast("Please select a valid video file (MP4, AVI, MOV, MKV)", "error"); return; } if (file.size > 500 * 1024 * 1024) { showToast("File size must be under 500 MB", "error"); return; } selectedFile = file; document.getElementById("file-name").textContent = file.name; document.getElementById("file-size").textContent = fmtSize(file.size); document.getElementById("upload-box").style.display = "none"; document.getElementById("file-info").style.display = "flex"; } function cancelFile() { selectedFile = null; document.getElementById("video-file").value = ""; document.getElementById("file-info").style.display = "none"; document.getElementById("upload-box").style.display = "flex"; } function wireDropzone() { const box = document.getElementById("upload-box"); if (!box) return; box.addEventListener("dragover", (e) => { e.preventDefault(); box.style.borderColor = "var(--blue-600)"; }); box.addEventListener("dragleave", () => { box.style.borderColor = ""; }); box.addEventListener("drop", (e) => { e.preventDefault(); box.style.borderColor = ""; if (e.dataTransfer.files.length) { const input = document.getElementById("video-file"); input.files = e.dataTransfer.files; handleFileSelect({ target: input }); } }); } function fmtSize(bytes) { const units = ["B", "KB", "MB", "GB"]; let i = 0; while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(1)} ${units[i]}`; } // ─── Upload & start analysis ────────────────────────────────────────────────── async function uploadAndAnalyze() { if (!selectedFile) { showToast("Select a video first", "error"); return; } // Apply currently checked models — fall back to all available if none checked let checked = Array.from( document.querySelectorAll(".model-checkbox:checked"), ).map((cb) => cb.dataset.modelId); if (checked.length === 0) { checked = Object.keys(availableModels); if (checked.length === 0) { showToast("No detection models available", "error"); return; } showToast("Using all available detection models", "info"); } await fetch("/api/set-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ models: checked }), }); // Show upload progress UI document.getElementById("file-info").style.display = "none"; document.getElementById("upload-progress").style.display = "block"; document.getElementById("results-section").style.display = "none"; resetLivePanel(); const formData = new FormData(); formData.append("video", selectedFile); return new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); document.getElementById("upload-fill").style.width = pct + "%"; document.getElementById("upload-text").textContent = `Uploading… ${pct}%`; } }); xhr.upload.addEventListener("load", () => { document.getElementById("upload-text").textContent = "Upload complete — queuing analysis…"; }); xhr.addEventListener("load", () => { document.getElementById("upload-progress").style.display = "none"; if (xhr.status === 200) { const resp = JSON.parse(xhr.responseText); if (resp.success) { currentJobId = resp.job_id; seenHighRiskCount = 0; showRealtimePanel(); startPolling(resp.job_id); showToast("Video uploaded — analysis started", "success"); } else { showToast("Error: " + (resp.message || "Unknown error"), "error"); showUploadBox(); } } else { showToast("Upload failed — please try again", "error"); showUploadBox(); } resolve(); }); xhr.addEventListener("error", () => { document.getElementById("upload-progress").style.display = "none"; showToast("Network error during upload", "error"); showUploadBox(); resolve(); }); xhr.open("POST", "/api/start-video-analysis"); xhr.send(formData); }); } // ─── Polling ───────────────────────────────────────────────────────────────── function startPolling(jobId) { stopPolling(); pollTimer = setInterval(() => pollJob(jobId), 1500); } function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } async function pollJob(jobId) { try { const data = await (await fetch(`/api/analysis-progress/${jobId}`)).json(); if (!data.success) return; updateRealtimePanel(data); // Show new high-risk alerts in banner + feed const newHighRisk = (data.high_risk_alerts || []).slice(seenHighRiskCount); newHighRisk.forEach((a) => { triggerAlertBanner(a); addFeedItem(a, true); }); seenHighRiskCount = (data.high_risk_alerts || []).length; // Check for weapon detection alerts and trigger them prominently const weaponAlerts = (data.alerts || []).filter(a => a.type && a.type.toLowerCase().includes('weapon') ); weaponAlerts.forEach((a) => { // Upgrade weapon alerts to trigger banner if they aren't already if (!newHighRisk.some(hr => hr.type === a.type)) { triggerAlertBanner({ severity: a.severity || 'HIGH', frame: a.frame || 0, message: a.message || `🔫 WEAPON DETECTED`, type: a.type || 'weapon_detected' }); addFeedItem(a, true); } }); // Add any new alerts to feed const allAlerts = data.alerts || []; const alreadyShown = seenHighRiskCount; // high-risk already handled allAlerts .filter((a) => !["HIGH", "CRITICAL"].includes(a.severity)) .slice(-5) // only show last 5 non-critical to avoid flooding .forEach((a) => addFeedItem(a, false)); if (data.status === "done") { stopPolling(); hideRealtimePanel(); currentAnalysisId = data.analysis_id; showToast("Analysis complete!", "success"); displayResults(data.results); if (data.analysis_id) { setTimeout(() => { loadAnalysisHistory(data.analysis_id); loadDetectionRecords(); document.getElementById("reanalyze-btn").style.display = "inline-block"; }, 400); } } else if (data.status === "error") { stopPolling(); hideRealtimePanel(); showToast("Analysis error: " + (data.error || "Unknown"), "error"); showUploadBox(); } } catch (e) { console.error("Poll error:", e); } } // ─── Real-time panel helpers ────────────────────────────────────────────────── function showRealtimePanel() { document.getElementById("realtime-panel").style.display = "block"; document.getElementById("upload-box").style.display = "none"; } function hideRealtimePanel() { document.getElementById("realtime-panel").style.display = "none"; } function resetLivePanel() { document.getElementById("rt-progress-fill").style.width = "0%"; document.getElementById("rt-frame-info").textContent = "Preparing…"; document.getElementById("rt-detections").textContent = "0"; document.getElementById("rt-alerts").textContent = "0"; document.getElementById("rt-high-risk").textContent = "0"; document.getElementById("live-alerts-feed").innerHTML = '

No alerts yet — all clear

'; // Reset processing video panel document.getElementById("processing-bar-fill").style.width = "0%"; document.getElementById("processing-frame-count").textContent = "Frame 0 of 0"; document.getElementById("proc-detections").textContent = "0"; document.getElementById("proc-alerts").textContent = "0"; document.getElementById("proc-high-risk").textContent = "0"; } function updateRealtimePanel(data) { const pct = data.progress || 0; const cur = data.current_frame || 0; const tot = data.total_frames || 0; // Update main progress bar document.getElementById("rt-progress-fill").style.width = pct + "%"; // Update frame info document.getElementById("rt-frame-info").textContent = tot > 0 ? `Frame ${cur.toLocaleString()} of ${tot.toLocaleString()} (${pct}%)` : `Processing… ${pct}%`; // Update main stats document.getElementById("rt-detections").textContent = ( data.detection_count || 0 ).toLocaleString(); document.getElementById("rt-alerts").textContent = ( data.alert_count || 0 ).toLocaleString(); document.getElementById("rt-high-risk").textContent = ( data.high_risk_alerts || [] ).length.toString(); // Update processing video panel with live stats document.getElementById("processing-bar-fill").style.width = pct + "%"; document.getElementById("processing-frame-count").textContent = tot > 0 ? `Frame ${cur.toLocaleString()} of ${tot.toLocaleString()}` : "Frame 0 of 0"; document.getElementById("proc-detections").textContent = ( data.detection_count || 0 ).toLocaleString(); document.getElementById("proc-alerts").textContent = ( data.alert_count || 0 ).toLocaleString(); document.getElementById("proc-high-risk").textContent = ( data.high_risk_alerts || [] ).length.toString(); } const feedShown = new Set(); function addFeedItem(alert, isHighRisk) { const key = `${alert.frame}-${alert.type}-${alert.severity}`; if (feedShown.has(key)) return; feedShown.add(key); const feed = document.getElementById("live-alerts-feed"); const placeholder = feed.querySelector("p"); if (placeholder) placeholder.remove(); const sev = (alert.severity || "low").toLowerCase(); const div = document.createElement("div"); div.className = `feed-item ${sev}`; div.innerHTML = ` ${alert.severity} Frame ${alert.frame} — ${alert.message || alert.type}`; feed.insertBefore(div, feed.firstChild); // Keep feed manageable while (feed.children.length > 20) feed.removeChild(feed.lastChild); } // ─── Alert banner ───────────────────────────────────────────────────────────── function triggerAlertBanner(alert) { const banner = document.getElementById("alert-banner"); const text = document.getElementById("alert-banner-text"); text.textContent = `🚨 ${alert.severity} ALERT — Frame ${alert.frame}: ${alert.message || alert.type}`; banner.style.display = "block"; // Play browser notification sound if available try { new Audio("data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAA==") .play() .catch(() => {}); } catch (e) {} clearTimeout(bannerDismissTimer); bannerDismissTimer = setTimeout(dismissBanner, 6000); } function dismissBanner() { document.getElementById("alert-banner").style.display = "none"; } // ─── Display results ───────────────────────────────────────────────────────── function displayResults(results) { // Hide the processing overlay when results are ready const processingOverlay = document.getElementById("processing-overlay"); if (processingOverlay) { processingOverlay.style.opacity = "0"; processingOverlay.style.visibility = "hidden"; processingOverlay.style.transition = "opacity 0.5s ease, visibility 0.5s ease"; } document.getElementById("results-section").style.display = "block"; // Summary cards document.getElementById("res-total-frames").textContent = ( results.total_frames || 0 ).toLocaleString(); document.getElementById("res-analyzed").textContent = ( results.processed_frames || 0 ).toLocaleString(); document.getElementById("res-detections").textContent = ( results.summary?.total_detections || 0 ).toLocaleString(); document.getElementById("res-alerts").textContent = ( results.summary?.total_alerts || 0 ).toLocaleString(); document.getElementById("res-emergency").textContent = ( results.summary?.emergency_frames_count ?? results.emergency_frames?.length ?? 0 ).toString(); // Processed video const mediaEl = document.getElementById("media-results"); const cards = []; if (results.preview_url) { cards.push(`
Preview Frame
Preview
`); } if (results.output_url) { // Determine MIME type based on file extension const url = results.output_url; let mimeType = "video/mp4"; if (url.endsWith(".avi")) { // Include codec info for better browser compatibility mimeType = 'video/x-msvideo; codecs="xvid"'; } else if (url.endsWith(".mkv")) { mimeType = "video/x-matroska"; } else if (url.endsWith(".mov")) { mimeType = "video/quicktime"; } cards.push(`
Processed Video with Detection Overlays

📥 Download processed video

`); } mediaEl.innerHTML = cards.join("") || "

No processed video available.

"; // Emergency frames const ef = results.emergency_frames || []; const efSection = document.getElementById("emergency-section"); if (ef.length > 0) { efSection.style.display = "block"; document.getElementById("emergency-gallery").innerHTML = ef .map((f) => { const color = f.alert_type === "CRITICAL" ? "var(--danger)" : f.alert_type === "WEAPON" ? "var(--warn)" : "var(--info)"; return `
Emergency
🚨 ${f.alert_type}
Frame ${f.frame_number}
EMERGENCY
`; }) .join(""); } else { efSection.style.display = "none"; } // Populate insights panel with key insights populateInsightsPanel(results); // Scroll to results document .getElementById("results-section") .scrollIntoView({ behavior: "smooth" }); } // ─── Populate Insights Panel ────────────────────────────────────────────────── function populateInsightsPanel(results) { const insightsPanel = document.getElementById("insights-panel"); const insights = []; // Total detections insight const totalDets = results.summary?.total_detections || 0; insights.push({ icon: "🔍", label: "Total Detections", value: totalDets, class: totalDets > 0 ? "" : "info", desc: totalDets > 0 ? `Found ${totalDets} objects` : "No objects detected", }); // Alerts insight const totalAlerts = results.summary?.total_alerts || 0; insights.push({ icon: "🚨", label: "Alerts", value: totalAlerts, class: totalAlerts > 0 ? "alert" : "", desc: totalAlerts > 0 ? `${totalAlerts} alert(s) generated` : "No alerts", }); // Weapons detected const dets = results.detections || []; const weaponCount = dets.filter((d) => (d.class || "").toLowerCase().includes("weapon"), ).length; // TRIGGER WEAPON ALERT if weapons detected if (weaponCount > 0) { const weaponAlerts = (results.alerts || []).filter(a => a.type && a.type.toLowerCase().includes('weapon') ); if (weaponAlerts.length > 0) { // Trigger the first weapon alert as banner triggerAlertBanner({ severity: weaponAlerts[0].severity || 'HIGH', frame: weaponAlerts[0].frame || 0, message: weaponAlerts[0].message || `${weaponCount} weapon(s) detected in video`, type: weaponAlerts[0].type || 'weapon_detected' }); } else { // Even without alert details, trigger a warning for weapons triggerAlertBanner({ severity: 'HIGH', frame: 0, message: `🔫 WEAPON DETECTED — ${weaponCount} weapon(s) found in video`, type: 'weapon_detected' }); } } insights.push({ icon: "🔫", label: "Weapons", value: weaponCount, class: weaponCount > 0 ? "alert" : "", desc: weaponCount > 0 ? `${weaponCount} weapon(s) detected — ⚠️ ALERT TRIGGERED` : "No weapons detected", }); // People detected const personCount = dets.filter((d) => (d.class || "").toLowerCase().includes("person"), ).length; insights.push({ icon: "👤", label: "People", value: personCount, class: "", desc: personCount > 0 ? `${personCount} person(ies) detected` : "No people detected", }); // High-risk frames const summaries = results.frame_summaries || []; const highRiskFrames = summaries.filter( (f) => f.alert_state === "CRITICAL" || f.alert_state === "HIGH", ).length; insights.push({ icon: "⚠️", label: "High-Risk Frames", value: highRiskFrames, class: highRiskFrames > 0 ? "warning" : "", desc: highRiskFrames > 0 ? `${highRiskFrames} frame(s) flagged` : "No high-risk frames", }); // Emergency frames const emergencyFrames = results.emergency_frames?.length || 0; insights.push({ icon: "⚡", label: "Emergency Frames", value: emergencyFrames, class: emergencyFrames > 0 ? "alert" : "", desc: emergencyFrames > 0 ? `${emergencyFrames} critical frame(s)` : "No emergencies", }); // Generate HTML insightsPanel.innerHTML = insights .map( (insight) => `
${insight.icon}
${insight.label}
${insight.value}
${insight.desc}
`, ) .join(""); } // ─── Emergency frame viewer ─────────────────────────────────────────────────── function viewEmergencyFrame(filename, alertType) { const url = `/processed/emergency_frames/${filename}`; new Modal( "🚨 Emergency Frame", `
Emergency

🚨 ${alertType} — High Priority Detection

🔍 Full Size
`, { footer: false, width: "640px" }, ).show(); } // ─── Detection records from DB ──────────────────────────────────────────────── async function loadDetectionRecords() { try { const data = await (await fetch("/api/detection-history?limit=20")).json(); if (!data.success) return; const el = document.getElementById("detection-records"); const records = (data.data || []).filter((d) => d.type === "detection"); if (records.length === 0) { el.innerHTML = '

No detection records saved yet.

'; return; } el.innerHTML = records .map((r) => { const ts = new Date(r.detected_at).toLocaleString(); const src = r.details?.source === "video_analysis" ? "Video" : "Live Camera"; return `
Detection
${r.detection_type.toUpperCase()} ${r.alert_level} ${src}
Confidence: ${(r.confidence * 100).toFixed(1)}%
${ts}
${r.details?.message ? `
${r.details.message}
` : ""}
⬇️
`; }) .join(""); } catch (e) { console.error("loadDetectionRecords error:", e); } } // ─── Analysis history ───────────────────────────────────────────────────────── async function loadAnalysisHistory(currentId) { try { const data = await ( await fetch("/api/video-analysis-list?limit=10") ).json(); if (!data.success) return; const el = document.getElementById("analysis-history"); const analyses = data.data || []; if (analyses.length === 0) { el.innerHTML = "

No previous analyses.

"; return; } el.innerHTML = analyses .map((a) => { const isCur = a.id === currentId; const ts = new Date(a.created_at).toLocaleString(); return `
${ a.preview_image_url ? `` : `
🎬
` }
${a.original_filename || "video"}${isCur ? " (current)" : ""}
${ts}
🎬 ${a.total_frames} frames 🔍 ${a.detection_count} detections 🚨 ${a.alert_count} alerts ${a.emergency_frames_count > 0 ? `⚡ ${a.emergency_frames_count} emergency` : ""}
`; }) .join(""); } catch (e) { console.error("loadAnalysisHistory error:", e); } } async function loadAnalysisDetail(id) { try { const data = await ( await fetch(`/api/video-analysis-history/${id}`) ).json(); if (!data.success) { showToast("Failed to load details", "error"); return; } const a = data.data; currentAnalysisId = a.id; displayResults({ total_frames: a.total_frames, processed_frames: a.processed_frames, output_url: a.processed_video_url, output_mime: "video/mp4", preview_url: a.preview_image_url, detections: a.detections, alerts: [], frame_summaries: a.frame_summaries, emergency_frames: a.emergency_frames, summary: { total_detections: a.detection_count, total_alerts: a.alert_count, emergency_frames_count: (a.emergency_frames || []).length, }, }); document.getElementById("reanalyze-btn").style.display = "inline-block"; loadAnalysisHistory(id); showToast("Analysis details loaded", "success"); } catch (e) { showToast("Error loading details", "error"); } } // ─── Re-analyze ─────────────────────────────────────────────────────────────── async function reanalyzeVideo() { if (!currentAnalysisId) { showToast("No analysis to re-run. Upload a video first.", "info"); return; } if (!confirm("Re-analyze this video with the currently selected models?")) return; document.getElementById("results-section").style.display = "none"; resetLivePanel(); showRealtimePanel(); seenHighRiskCount = 0; try { const data = await ( await fetch(`/api/reanalyze-video/${currentAnalysisId}`, { method: "POST", headers: { "Content-Type": "application/json" }, }) ).json(); if (data.success) { currentAnalysisId = data.analysis_id; hideRealtimePanel(); displayResults(data.results); loadAnalysisHistory(currentAnalysisId); loadDetectionRecords(); showToast("Re-analysis complete", "success"); } else { hideRealtimePanel(); showToast("Re-analysis failed: " + (data.error || "Unknown"), "error"); } } catch (e) { hideRealtimePanel(); showToast("Re-analysis error", "error"); } } // ─── UI helpers ─────────────────────────────────────────────────────────────── function startNewAnalysis() { stopPolling(); selectedFile = null; currentJobId = null; currentAnalysisId = null; seenHighRiskCount = 0; feedShown.clear(); dismissBanner(); document.getElementById("video-file").value = ""; document.getElementById("file-info").style.display = "none"; document.getElementById("upload-progress").style.display = "none"; document.getElementById("results-section").style.display = "none"; hideRealtimePanel(); showUploadBox(); showToast("Ready for new analysis", "info"); } function showUploadBox() { document.getElementById("upload-box").style.display = "flex"; document.getElementById("file-info").style.display = "none"; } function showToast(message, type = "info") { const c = document.getElementById("toast-container"); if (!c) return; const t = document.createElement("div"); t.className = `toast ${type}`; t.textContent = message; c.appendChild(t); setTimeout(() => t.remove(), 3000); }