const API_ENDPOINT = "/api/detect"; const elements = { alertStatus: document.querySelector("#alertStatus"), clearButton: document.querySelector("#clearButton"), connectionStatus: document.querySelector("#connectionStatus"), detectionList: document.querySelector("#detectionList"), emptyState: document.querySelector("#emptyState"), fireCount: document.querySelector("#fireCount"), intervalSelect: document.querySelector("#intervalSelect"), lastUpdated: document.querySelector("#lastUpdated"), loadingState: document.querySelector("#loadingState"), maxConfidence: document.querySelector("#maxConfidence"), overlayCanvas: document.querySelector("#overlayCanvas"), riskStatus: document.querySelector("#riskStatus"), runDetectionButton: document.querySelector("#runDetectionButton"), smokeCount: document.querySelector("#smokeCount"), sourceLabel: document.querySelector("#sourceLabel"), videoInput: document.querySelector("#videoInput"), videoPreview: document.querySelector("#videoPreview"), }; const frameCanvas = document.createElement("canvas"); let videoUrl = null; let sessionId = null; let detectionActive = false; let requestInFlight = false; let lastDetectionAt = 0; function setConnectionStatus(label, state = "idle") { elements.connectionStatus.textContent = label; elements.connectionStatus.className = `status-pill status-${state}`; } function enabledChannelNames(channels = {}) { const names = []; if (channels.wechat) names.push("企业微信"); if (channels.feishu) names.push("飞书"); return names; } function clearResults() { elements.smokeCount.textContent = "0"; elements.fireCount.textContent = "0"; elements.maxConfidence.textContent = "--"; elements.riskStatus.textContent = "待机"; elements.detectionList.innerHTML = '
暂无检测结果
'; const context = elements.overlayCanvas.getContext("2d"); context.clearRect(0, 0, elements.overlayCanvas.width, elements.overlayCanvas.height); } function drawDetections(detections = []) { const video = elements.videoPreview; if (!video.videoWidth || !video.videoHeight) return; const canvas = elements.overlayCanvas; canvas.width = video.videoWidth; canvas.height = video.videoHeight; const context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); detections.forEach((detection) => { const [x1, y1, x2, y2] = detection.box || []; const color = detection.class === "fire" ? "#ff786b" : "#55d5c2"; context.strokeStyle = color; context.lineWidth = Math.max(2, canvas.width / 320); context.strokeRect(x1, y1, x2 - x1, y2 - y1); context.fillStyle = color; context.font = `${Math.max(13, canvas.width / 60)}px sans-serif`; context.fillText(`${detection.class} ${Math.round(detection.confidence * 100)}%`, x1 + 4, Math.max(18, y1 - 6)); }); } function updateResults(result) { const detections = result.detections || []; const smoke = detections.filter((item) => item.class === "smoke").length; const fire = detections.filter((item) => item.class === "fire").length; const max = detections.reduce((highest, item) => Math.max(highest, item.confidence || 0), 0); elements.smokeCount.textContent = String(smoke); elements.fireCount.textContent = String(fire); elements.maxConfidence.textContent = max ? `${Math.round(max * 100)}%` : "--"; elements.riskStatus.textContent = fire ? "火焰告警" : smoke ? "烟雾告警" : "正常"; elements.detectionList.innerHTML = detections.length ? detections.map((item) => `未发现目标
'; const alert = result.alert || {}; if (alert.triggered) { const labels = alert.classes.map((name) => name === "fire" ? "火焰" : "烟雾").join("、"); const channelNames = enabledChannelNames(alert.notification_channels); elements.alertStatus.textContent = channelNames.length ? `已触发 ${channelNames.join(" + ")} 告警:${labels}` : `已满足告警条件:${labels}(未配置机器人 Webhook)`; elements.alertStatus.className = "alert-status alert-triggered"; } else { const fireFrames = alert.consecutive?.fire || 0; const smokeFrames = alert.consecutive?.smoke || 0; const channelNames = enabledChannelNames(alert.notification_channels); elements.alertStatus.textContent = channelNames.length ? `${channelNames.join(" + ")} 告警已启用 · 连续帧 火焰 ${fireFrames} / 烟雾 ${smokeFrames}` : "机器人告警未配置,检测功能正常"; elements.alertStatus.className = "alert-status"; } drawDetections(detections); elements.lastUpdated.textContent = `视频 ${formatTime(elements.videoPreview.currentTime)} · 推理 ${result.inference_ms ?? "--"} ms`; } function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const remaining = Math.floor(seconds % 60); return `${String(minutes).padStart(2, "0")}:${String(remaining).padStart(2, "0")}`; } function captureFrame() { const video = elements.videoPreview; frameCanvas.width = video.videoWidth; frameCanvas.height = video.videoHeight; frameCanvas.getContext("2d").drawImage(video, 0, 0); return new Promise((resolve) => frameCanvas.toBlob(resolve, "image/jpeg", 0.88)); } async function detectCurrentFrame(timestamp) { if ( !detectionActive || requestInFlight || !sessionId || elements.videoPreview.paused || elements.videoPreview.ended ) return; const interval = Number(elements.intervalSelect.value); if (timestamp - lastDetectionAt < interval) return; lastDetectionAt = timestamp; requestInFlight = true; elements.loadingState.hidden = false; try { const frame = await captureFrame(); if (!frame) throw new Error("无法截取视频帧"); const form = new FormData(); form.append("file", frame, "video-frame.jpg"); const response = await fetch(`${API_ENDPOINT}?session_id=${encodeURIComponent(sessionId)}`, { method: "POST", body: form }); if (!response.ok) throw new Error(`API ${response.status}`); updateResults(await response.json()); setConnectionStatus("检测服务已连接", "ready"); } catch (error) { setConnectionStatus("检测服务异常", "alert"); elements.detectionList.innerHTML = `${error.message}
`; } finally { requestInFlight = false; elements.loadingState.hidden = true; } } function scheduleDetection(timestamp) { detectCurrentFrame(timestamp); if (detectionActive) requestAnimationFrame(scheduleDetection); } async function startDetection() { if (!elements.videoPreview.src) return; detectionActive = true; lastDetectionAt = -Infinity; elements.runDetectionButton.textContent = "停止检测"; elements.intervalSelect.disabled = true; try { await elements.videoPreview.play(); } catch { setConnectionStatus("请允许视频播放", "alert"); } requestAnimationFrame(scheduleDetection); } function stopDetection() { detectionActive = false; elements.runDetectionButton.textContent = "开始连续检测"; elements.intervalSelect.disabled = false; } async function resetSession() { const previousSession = sessionId; sessionId = crypto.randomUUID(); if (previousSession) { fetch(`/api/sessions/${encodeURIComponent(previousSession)}`, { method: "DELETE" }).catch(() => {}); } } async function checkHealth() { try { const response = await fetch("/api/health"); if (!response.ok) throw new Error(); const health = await response.json(); setConnectionStatus("检测服务已连接", "ready"); const channelNames = enabledChannelNames(health.alert_channels); elements.alertStatus.textContent = channelNames.length ? `${channelNames.join(" + ")} 告警已启用` : "机器人告警未配置,检测功能正常"; } catch { setConnectionStatus("检测服务未连接", "alert"); elements.alertStatus.textContent = "无法读取机器人告警状态"; } } elements.videoInput.addEventListener("change", async () => { const [file] = elements.videoInput.files; if (!file) return; stopDetection(); elements.videoPreview.pause(); if (videoUrl) URL.revokeObjectURL(videoUrl); videoUrl = URL.createObjectURL(file); elements.videoPreview.src = videoUrl; elements.videoPreview.hidden = false; elements.emptyState.hidden = true; elements.sourceLabel.textContent = file.name; elements.runDetectionButton.disabled = false; await resetSession(); clearResults(); }); elements.runDetectionButton.addEventListener("click", () => { if (detectionActive) stopDetection(); else startDetection(); }); elements.videoPreview.addEventListener("ended", stopDetection); elements.videoPreview.addEventListener("seeked", async () => { drawDetections([]); await resetSession(); }); elements.videoPreview.addEventListener("resize", () => drawDetections([])); elements.clearButton.addEventListener("click", async () => { stopDetection(); elements.videoPreview.pause(); elements.videoPreview.removeAttribute("src"); elements.videoPreview.load(); elements.videoPreview.hidden = true; if (videoUrl) URL.revokeObjectURL(videoUrl); videoUrl = null; elements.videoInput.value = ""; elements.sourceLabel.textContent = "等待选择视频"; elements.emptyState.hidden = false; elements.runDetectionButton.disabled = true; await resetSession(); clearResults(); }); resetSession(); checkHealth();