| |
|
|
| let selectedFile = null; |
| let availableModels = {}; |
| let selectedModels = []; |
| let currentAnalysisId = null; |
| let currentJobId = null; |
| let pollTimer = null; |
| let seenHighRiskCount = 0; |
| let bannerDismissTimer = null; |
|
|
| |
|
|
| document.addEventListener("DOMContentLoaded", async () => { |
| await loadAvailableModels(); |
| await loadSelectedModels(); |
| wireDropzone(); |
| }); |
|
|
| |
|
|
| 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) { |
| |
| } |
| } |
|
|
| function renderModelSelection() { |
| const grid = document.getElementById("models-grid"); |
| if (!grid) return; |
| |
| const allByDefault = selectedModels.length === 0; |
| const cards = Object.entries(availableModels).map( |
| ([key, info]) => ` |
| <div class="model-card"> |
| <label class="model-checkbox-label"> |
| <input type="checkbox" class="model-checkbox" data-model-id="${key}" |
| ${allByDefault || selectedModels.includes(key) ? "checked" : ""}> |
| <div class="checkbox-content"> |
| <strong>${info.name}</strong> |
| <p>${info.description}</p> |
| <span class="model-badge">${key}</span> |
| </div> |
| </label> |
| </div>`, |
| ); |
| grid.innerHTML = cards.length ? cards.join("") : "<p>No models available</p>"; |
| } |
|
|
| function updateModelCheckboxes() { |
| |
| 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"); |
| } |
| } |
|
|
| |
|
|
| 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]}`; |
| } |
|
|
| |
|
|
| async function uploadAndAnalyze() { |
| if (!selectedFile) { |
| showToast("Select a video first", "error"); |
| return; |
| } |
|
|
| |
| 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 }), |
| }); |
|
|
| |
| 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); |
| }); |
| } |
|
|
| |
|
|
| 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); |
|
|
| |
| const newHighRisk = (data.high_risk_alerts || []).slice(seenHighRiskCount); |
| newHighRisk.forEach((a) => { |
| triggerAlertBanner(a); |
| addFeedItem(a, true); |
| }); |
| seenHighRiskCount = (data.high_risk_alerts || []).length; |
|
|
| |
| const weaponAlerts = (data.alerts || []).filter(a => |
| a.type && a.type.toLowerCase().includes('weapon') |
| ); |
| weaponAlerts.forEach((a) => { |
| |
| 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); |
| } |
| }); |
|
|
| |
| const allAlerts = data.alerts || []; |
| const alreadyShown = seenHighRiskCount; |
| allAlerts |
| .filter((a) => !["HIGH", "CRITICAL"].includes(a.severity)) |
| .slice(-5) |
| .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); |
| } |
| } |
|
|
| |
|
|
| 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 = |
| '<p style="color:var(--slate-500);font-size:0.85rem;padding:0.3rem;">No alerts yet β all clear</p>'; |
|
|
| |
| 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; |
|
|
| |
| document.getElementById("rt-progress-fill").style.width = pct + "%"; |
|
|
| |
| document.getElementById("rt-frame-info").textContent = |
| tot > 0 |
| ? `Frame ${cur.toLocaleString()} of ${tot.toLocaleString()} (${pct}%)` |
| : `Processing⦠${pct}%`; |
|
|
| |
| 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(); |
|
|
| |
| 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 = ` |
| <span class="feed-badge ${sev}">${alert.severity}</span> |
| <span><strong>Frame ${alert.frame}</strong> β ${alert.message || alert.type}</span>`; |
| feed.insertBefore(div, feed.firstChild); |
|
|
| |
| while (feed.children.length > 20) feed.removeChild(feed.lastChild); |
| } |
|
|
| |
|
|
| 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"; |
|
|
| |
| 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"; |
| } |
|
|
| |
|
|
| function displayResults(results) { |
| |
| 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"; |
|
|
| |
| 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(); |
|
|
| |
| const mediaEl = document.getElementById("media-results"); |
| const cards = []; |
| if (results.preview_url) { |
| cards.push(` |
| <div class="detection-item"> |
| <strong>Preview Frame</strong> |
| <div style="margin-top:0.8rem;"> |
| <img src="${results.preview_url}" alt="Preview" style="max-width:100%;border-radius:10px;"> |
| </div> |
| </div>`); |
| } |
| if (results.output_url) { |
| |
| const url = results.output_url; |
| let mimeType = "video/mp4"; |
| if (url.endsWith(".avi")) { |
| |
| 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(` |
| <div class="detection-item"> |
| <strong>Processed Video with Detection Overlays</strong> |
| <div style="margin-top:0.8rem;"> |
| <video controls style="width:100%;border-radius:10px;background:#000;"> |
| <source src="${results.output_url}" type="${mimeType}"> |
| Your browser does not support video playback. |
| </video> |
| </div> |
| <p style="margin-top:0.5rem;font-size:0.9rem;"> |
| <a href="${results.output_url}" download>π₯ Download processed video</a> |
| </p> |
| </div>`); |
| } |
| mediaEl.innerHTML = |
| cards.join("") || |
| "<p style='color:var(--slate-600);'>No processed video available.</p>"; |
|
|
| |
| 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 ` |
| <div style="position:relative;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:pointer;" |
| onclick="viewEmergencyFrame('${f.filename}','${f.alert_type}')"> |
| <img src="/processed/emergency_frames/${f.filename}" alt="Emergency" |
| style="width:100%;height:140px;object-fit:cover;display:block;" |
| onerror="this.style.display='none'"> |
| <div style="position:absolute;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(0,0,0,0.8),transparent);padding:0.5rem;color:white;font-size:0.75rem;"> |
| <div style="font-weight:700;">π¨ ${f.alert_type}</div> |
| <div style="opacity:0.8;">Frame ${f.frame_number}</div> |
| </div> |
| <div style="position:absolute;top:4px;right:4px;background:${color};color:white;padding:2px 7px;border-radius:4px;font-size:0.7rem;font-weight:700;">EMERGENCY</div> |
| </div>`; |
| }) |
| .join(""); |
| } else { |
| efSection.style.display = "none"; |
| } |
|
|
| |
| populateInsightsPanel(results); |
|
|
| |
| document |
| .getElementById("results-section") |
| .scrollIntoView({ behavior: "smooth" }); |
| } |
|
|
| |
|
|
| function populateInsightsPanel(results) { |
| const insightsPanel = document.getElementById("insights-panel"); |
| const insights = []; |
|
|
| |
| 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", |
| }); |
|
|
| |
| 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", |
| }); |
|
|
| |
| const dets = results.detections || []; |
| const weaponCount = dets.filter((d) => |
| (d.class || "").toLowerCase().includes("weapon"), |
| ).length; |
| |
| |
| if (weaponCount > 0) { |
| const weaponAlerts = (results.alerts || []).filter(a => |
| a.type && a.type.toLowerCase().includes('weapon') |
| ); |
| if (weaponAlerts.length > 0) { |
| |
| 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 { |
| |
| 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", |
| }); |
|
|
| |
| 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", |
| }); |
|
|
| |
| 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", |
| }); |
|
|
| |
| 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", |
| }); |
|
|
| |
| insightsPanel.innerHTML = insights |
| .map( |
| (insight) => ` |
| <div class="insight-card ${insight.class}"> |
| <div class="insight-icon">${insight.icon}</div> |
| <div class="insight-label">${insight.label}</div> |
| <div class="insight-value">${insight.value}</div> |
| <div class="insight-desc">${insight.desc}</div> |
| </div> |
| `, |
| ) |
| .join(""); |
| } |
|
|
| |
|
|
| function viewEmergencyFrame(filename, alertType) { |
| const url = `/processed/emergency_frames/${filename}`; |
| new Modal( |
| "π¨ Emergency Frame", |
| ` |
| <div style="text-align:center;"> |
| <img src="${url}" alt="Emergency" style="max-width:100%;max-height:480px;border-radius:8px;margin-bottom:1rem;"> |
| <div style="background:#fff5f5;border:2px solid var(--danger);padding:1rem;border-radius:8px;"> |
| <p style="color:var(--danger);font-weight:700;">π¨ ${alertType} β High Priority Detection</p> |
| </div> |
| <div style="margin-top:1rem;"> |
| <a href="${url}" target="_blank" class="btn btn-primary">π Full Size</a> |
| </div> |
| </div>`, |
| { footer: false, width: "640px" }, |
| ).show(); |
| } |
|
|
| |
|
|
| 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 = |
| '<p style="color:var(--slate-600);">No detection records saved yet.</p>'; |
| 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 ` |
| <div class="det-record"> |
| <img class="det-thumb" src="/api/detection-image/${r.image_filename}" |
| alt="Detection" onerror="this.style.display='none'"> |
| <div class="det-info"> |
| <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;"> |
| <strong>${r.detection_type.toUpperCase()}</strong> |
| <span class="badge-sev ${r.alert_level}">${r.alert_level}</span> |
| <span style="font-size:0.75rem;color:var(--slate-500);">${src}</span> |
| </div> |
| <div style="color:var(--slate-700);">Confidence: ${(r.confidence * 100).toFixed(1)}%</div> |
| <div style="color:var(--slate-500);font-size:0.8rem;">${ts}</div> |
| ${r.details?.message ? `<div style="color:var(--slate-700);margin-top:0.2rem;font-size:0.8rem;">${r.details.message}</div>` : ""} |
| </div> |
| <a href="/api/detection-image/${r.image_filename}" download |
| class="btn btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.8rem;flex-shrink:0;">β¬οΈ</a> |
| </div>`; |
| }) |
| .join(""); |
| } catch (e) { |
| console.error("loadDetectionRecords error:", e); |
| } |
| } |
|
|
| |
|
|
| 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 = "<p>No previous analyses.</p>"; |
| return; |
| } |
| el.innerHTML = analyses |
| .map((a) => { |
| const isCur = a.id === currentId; |
| const ts = new Date(a.created_at).toLocaleString(); |
| return ` |
| <div class="history-item" style="${isCur ? "border-color:var(--blue-600);background:var(--blue-50);" : ""}"> |
| ${ |
| a.preview_image_url |
| ? `<img class="history-thumb" src="${a.preview_image_url}" onerror="this.style.display='none'">` |
| : `<div class="history-thumb" style="display:flex;align-items:center;justify-content:center;font-size:1.8rem;">π¬</div>` |
| } |
| <div class="history-meta" style="flex:1;min-width:0;"> |
| <div style="font-weight:600;word-break:break-all;">${a.original_filename || "video"}${isCur ? " (current)" : ""}</div> |
| <div style="font-size:0.8rem;color:var(--slate-600);margin-top:0.2rem;">${ts}</div> |
| <div class="history-badges"> |
| <span class="hbadge">π¬ ${a.total_frames} frames</span> |
| <span class="hbadge">π ${a.detection_count} detections</span> |
| <span class="hbadge ${a.alert_count > 0 ? "red" : ""}">π¨ ${a.alert_count} alerts</span> |
| ${a.emergency_frames_count > 0 ? `<span class="hbadge red">β‘ ${a.emergency_frames_count} emergency</span>` : ""} |
| </div> |
| </div> |
| <button class="btn btn-secondary" onclick="loadAnalysisDetail(${a.id})" |
| style="white-space:nowrap;flex-shrink:0;padding:0.4rem 0.8rem;font-size:0.85rem;"> |
| View |
| </button> |
| </div>`; |
| }) |
| .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"); |
| } |
| } |
|
|
| |
|
|
| 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"); |
| } |
| } |
|
|
| |
|
|
| 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); |
| } |
|
|