const form = document.querySelector("#segmentForm"); const methodSelect = document.querySelector("#method"); const methodGrid = document.querySelector("#methodGrid"); const sensitivity = document.querySelector("#sensitivity"); const sensitivityValue = document.querySelector("#sensitivityValue"); const fileInput = document.querySelector("#file"); const fileName = document.querySelector("#fileName"); const dropZone = document.querySelector("#dropZone"); const sampleButton = document.querySelector("#sampleButton"); const clearButton = document.querySelector("#clearButton"); const resultGrid = document.querySelector("#resultGrid"); const emptyState = document.querySelector("#emptyState"); const videoLink = document.querySelector("#videoLink"); const health = document.querySelector("#health"); const template = document.querySelector("#resultCardTemplate"); const previewEmpty = document.querySelector("#previewEmpty"); const videoPreview = document.querySelector("#videoPreview"); const imagePreview = document.querySelector("#imagePreview"); const progressWrap = document.querySelector("#progressWrap"); const progressText = document.querySelector("#progressText"); const summaryStrip = document.querySelector("#summaryStrip"); const summaryJob = document.querySelector("#summaryJob"); const summaryFrames = document.querySelector("#summaryFrames"); const summaryCoverage = document.querySelector("#summaryCoverage"); const summarySkeleton = document.querySelector("#summarySkeleton"); const resultCount = document.querySelector("#resultCount"); const jobMeta = document.querySelector("#jobMeta"); const detailDialog = document.querySelector("#detailDialog"); const closeDialog = document.querySelector("#closeDialog"); const openSourceButton = document.querySelector("#openSourceButton"); const sourcePaneTitle = document.querySelector("#sourcePaneTitle"); const sourceDialog = document.querySelector("#sourceDialog"); const closeSourceDialog = document.querySelector("#closeSourceDialog"); const sourceTitle = document.querySelector("#sourceTitle"); const sourceVideo = document.querySelector("#sourceVideo"); const sourceImage = document.querySelector("#sourceImage"); const methodLabels = new Map(); const methodDescriptions = new Map(); let selectedFile = null; let currentObjectUrl = null; let lastFrames = []; function setBusy(isBusy, text = "运行分割") { const button = form.querySelector(".primary"); button.disabled = isBusy; button.querySelector("span").textContent = isBusy ? "分割中" : text; progressWrap.hidden = !isBusy; if (isBusy) { progressText.textContent = "正在上传、抽帧并执行导丝分割"; } } function setFile(file) { selectedFile = file; if (typeof DataTransfer !== "undefined") { const transfer = new DataTransfer(); transfer.items.add(file); fileInput.files = transfer.files; } fileName.textContent = `${file.name} · ${(file.size / 1024 / 1024).toFixed(2)} MB`; renderPreview(file); } function revokePreview() { if (currentObjectUrl) { URL.revokeObjectURL(currentObjectUrl); currentObjectUrl = null; } } function renderPreview(file) { revokePreview(); currentObjectUrl = URL.createObjectURL(file); previewEmpty.hidden = true; videoPreview.hidden = true; imagePreview.hidden = true; openSourceButton.hidden = false; if (file.type.startsWith("video/")) { videoPreview.preload = "metadata"; videoPreview.onloadedmetadata = () => { if (Number.isFinite(videoPreview.duration) && videoPreview.duration > 0.2) { videoPreview.currentTime = 0.1; } }; videoPreview.src = currentObjectUrl; videoPreview.load(); videoPreview.hidden = false; openSourceButton.textContent = "放大查看"; sourcePaneTitle.textContent = "查看原始视频"; sourceTitle.textContent = "原始视频"; } else { imagePreview.src = currentObjectUrl; imagePreview.hidden = false; openSourceButton.textContent = "放大查看"; sourcePaneTitle.textContent = "查看原始图像"; sourceTitle.textContent = "原始图像"; } jobMeta.textContent = `已选择 ${file.name}`; } async function loadHealth() { try { const response = await fetch("/api/health"); if (!response.ok) throw new Error("bad health"); const data = await response.json(); health.textContent = `${data.service} ${data.version}`; health.classList.add("ok"); } catch { health.textContent = "服务不可用"; health.classList.add("bad"); } } function activateMethod(key) { methodSelect.value = key; [...methodGrid.querySelectorAll(".method-option")].forEach((node) => { node.classList.toggle("is-active", node.dataset.method === key); }); } async function loadMethods() { const response = await fetch("/api/methods"); const data = await response.json(); methodSelect.innerHTML = ""; methodGrid.innerHTML = ""; Object.entries(data.methods).forEach(([key, value]) => { methodLabels.set(key, value.label); methodDescriptions.set(key, value.description); const option = document.createElement("option"); option.value = key; option.textContent = value.label; methodSelect.appendChild(option); const card = document.createElement("button"); card.type = "button"; card.className = "method-option"; card.dataset.method = key; card.innerHTML = `${value.label}${value.description}`; card.addEventListener("click", () => activateMethod(key)); methodGrid.appendChild(card); }); activateMethod("fusion"); } function updateSummary(data) { const frames = data.frames || []; const coverage = frames.reduce((sum, frame) => sum + frame.metrics.coverage, 0) / Math.max(1, frames.length); const skeleton = frames.reduce((sum, frame) => sum + frame.metrics.skeleton_length, 0) / Math.max(1, frames.length); summaryJob.textContent = data.job_id; summaryFrames.textContent = frames.length; summaryCoverage.textContent = `${(coverage * 100).toFixed(3)}%`; summarySkeleton.textContent = Math.round(skeleton); summaryStrip.hidden = false; resultCount.textContent = `${frames.length} 个结果`; jobMeta.textContent = `${data.kind === "video" ? "视频" : "图像"} · ${methodLabels.get(data.method) || data.method}`; } function openDetail(frame) { document.querySelector("#detailMethod").textContent = methodLabels.get(frame.method) || frame.method; document.querySelector("#detailTitle").textContent = `帧 ${frame.frame_index} 分割详情`; document.querySelector("#detailOriginal").src = frame.original_url; document.querySelector("#detailOverlay").src = frame.overlay_url; document.querySelector("#detailMask").src = frame.mask_url; const metrics = document.querySelector("#detailMetrics"); metrics.innerHTML = `