594 lines
21 KiB
JavaScript
594 lines
21 KiB
JavaScript
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 progressBar = document.querySelector("#progressBar");
|
|
const resultVideoPreview = document.querySelector("#resultVideoPreview");
|
|
const resultVideoEmpty = document.querySelector("#resultVideoEmpty");
|
|
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 openCompareButton = document.querySelector("#openCompareButton");
|
|
const controlCompareButton = document.querySelector("#controlCompareButton");
|
|
const compareDialog = document.querySelector("#compareDialog");
|
|
const closeCompareDialog = document.querySelector("#closeCompareDialog");
|
|
const compareGrid = document.querySelector("#compareGrid");
|
|
const compareTitle = document.querySelector("#compareTitle");
|
|
|
|
const methodLabels = new Map();
|
|
const methodDescriptions = new Map();
|
|
let selectedFile = null;
|
|
let currentObjectUrl = null;
|
|
let lastFrames = [];
|
|
let lastResult = null;
|
|
let currentFrame = null;
|
|
let syncLock = false;
|
|
let progressTimers = [];
|
|
|
|
function setBusy(isBusy, text = "运行分割") {
|
|
const button = form.querySelector(".primary");
|
|
button.disabled = isBusy;
|
|
button.querySelector("span").textContent = isBusy ? "分割中" : text;
|
|
if (isBusy) {
|
|
progressWrap.hidden = false;
|
|
setProgress(8, "正在上传、抽帧并执行导丝分割");
|
|
}
|
|
}
|
|
|
|
function setProgress(percent, text) {
|
|
progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
|
progressText.textContent = text;
|
|
}
|
|
|
|
function scheduleProgress() {
|
|
progressTimers.forEach((timer) => clearTimeout(timer));
|
|
progressTimers = [
|
|
setTimeout(() => setProgress(28, "正在读取视频并抽取关键帧"), 400),
|
|
setTimeout(() => setProgress(58, "正在生成导丝掩膜与叠加视频"), 1400),
|
|
setTimeout(() => setProgress(82, "正在整理结果帧与指标"), 2800),
|
|
];
|
|
}
|
|
|
|
function finishProgress() {
|
|
progressTimers.forEach((timer) => clearTimeout(timer));
|
|
progressTimers = [];
|
|
setProgress(100, "分割完成");
|
|
setTimeout(() => {
|
|
progressWrap.hidden = true;
|
|
progressBar.style.width = "0%";
|
|
}, 650);
|
|
}
|
|
|
|
function failProgress(message) {
|
|
progressTimers.forEach((timer) => clearTimeout(timer));
|
|
progressTimers = [];
|
|
setProgress(100, message || "分割失败");
|
|
setTimeout(() => {
|
|
progressWrap.hidden = true;
|
|
progressBar.style.width = "0%";
|
|
}, 900);
|
|
}
|
|
|
|
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);
|
|
resetResultsForNewInput();
|
|
}
|
|
|
|
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]) => {
|
|
if (key === "compare") return;
|
|
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 = `<strong>${value.label}</strong><span>${value.description}</span>`;
|
|
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} 个结果`;
|
|
}
|
|
|
|
function resetResultsForNewInput() {
|
|
lastFrames = [];
|
|
lastResult = null;
|
|
currentFrame = null;
|
|
resultGrid.innerHTML = "";
|
|
resultVideoPreview.hidden = true;
|
|
resultVideoPreview.removeAttribute("src");
|
|
resultVideoEmpty.hidden = false;
|
|
emptyState.hidden = false;
|
|
emptyState.textContent = "已加载输入。点击左侧“运行分割”生成导丝掩膜和叠加结果。";
|
|
summaryStrip.hidden = true;
|
|
videoLink.hidden = true;
|
|
resultCount.textContent = "0 个结果";
|
|
setCompareEnabled(false);
|
|
}
|
|
|
|
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 = `
|
|
<div><dt>覆盖率</dt><dd>${(frame.metrics.coverage * 100).toFixed(3)}%</dd></div>
|
|
<div><dt>掩膜像素</dt><dd>${frame.metrics.mask_pixels}</dd></div>
|
|
<div><dt>骨架长度</dt><dd>${frame.metrics.skeleton_length}</dd></div>
|
|
<div><dt>连通域</dt><dd>${frame.metrics.components}</dd></div>
|
|
`;
|
|
detailDialog.showModal();
|
|
}
|
|
|
|
function renderResults(data) {
|
|
lastResult = data;
|
|
lastFrames = data.frames || [];
|
|
resultGrid.innerHTML = "";
|
|
emptyState.hidden = lastFrames.length > 0;
|
|
videoLink.hidden = !data.video_url;
|
|
if (data.video_url) {
|
|
videoLink.href = data.video_url;
|
|
videoLink.setAttribute("download", "");
|
|
resultVideoPreview.preload = "metadata";
|
|
resultVideoPreview.src = data.video_url;
|
|
resultVideoPreview.load();
|
|
resultVideoPreview.hidden = false;
|
|
resultVideoEmpty.hidden = true;
|
|
} else {
|
|
resultVideoPreview.hidden = true;
|
|
resultVideoPreview.removeAttribute("src");
|
|
resultVideoEmpty.hidden = false;
|
|
}
|
|
updateSummary(data);
|
|
|
|
lastFrames.forEach((frame, index) => {
|
|
const node = template.content.firstElementChild.cloneNode(true);
|
|
node.querySelector(".method").textContent = methodLabels.get(frame.method) || frame.method;
|
|
node.querySelector(".frame-index").textContent = `帧 ${frame.frame_index}`;
|
|
node.querySelector(".overlay").src = frame.overlay_url;
|
|
node.querySelector(".coverage").textContent = `${(frame.metrics.coverage * 100).toFixed(3)}%`;
|
|
node.querySelector(".skeleton").textContent = frame.metrics.skeleton_length;
|
|
node.querySelector(".components").textContent = frame.metrics.components;
|
|
node.dataset.frameIndex = frame.frame_index;
|
|
node.addEventListener("click", () => selectFrame(frame, index));
|
|
node.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") selectFrame(frame, index);
|
|
});
|
|
resultGrid.appendChild(node);
|
|
});
|
|
if (lastFrames.length) {
|
|
selectFrame(lastFrames[0], 0);
|
|
}
|
|
}
|
|
|
|
async function loadSample() {
|
|
sampleButton.disabled = true;
|
|
sampleButton.textContent = "加载中";
|
|
emptyState.hidden = false;
|
|
emptyState.textContent = "正在加载内置样例视频。";
|
|
try {
|
|
const response = await fetch("/api/samples");
|
|
if (!response.ok) throw new Error("样例清单读取失败");
|
|
const data = await response.json();
|
|
const sample = data.samples.find((item) => item.kind === "video") || data.samples[0];
|
|
if (!sample) throw new Error("未找到样例文件");
|
|
const separator = sample.url.includes("?") ? "&" : "?";
|
|
const sampleUrl = `${sample.url}${separator}t=${Date.now()}`;
|
|
const blobResponse = await fetch(sampleUrl, { cache: "reload" });
|
|
if (!blobResponse.ok) throw new Error("样例文件下载失败");
|
|
const blob = await blobResponse.blob();
|
|
const file = new File([blob], sample.name, { type: sample.kind === "video" ? "video/mp4" : "image/png" });
|
|
setFile(file);
|
|
emptyState.textContent = "样例已加载。点击左侧“运行分割”即可生成导丝掩膜和叠加结果。";
|
|
} catch (error) {
|
|
emptyState.textContent = error.message || "样例加载失败";
|
|
} finally {
|
|
sampleButton.disabled = false;
|
|
sampleButton.textContent = "加载样例";
|
|
}
|
|
}
|
|
|
|
function clearAll() {
|
|
selectedFile = null;
|
|
fileInput.value = "";
|
|
fileName.textContent = "支持 mp4、avi、png、jpg、tiff";
|
|
revokePreview();
|
|
videoPreview.removeAttribute("src");
|
|
imagePreview.removeAttribute("src");
|
|
videoPreview.hidden = true;
|
|
imagePreview.hidden = true;
|
|
sourceVideo.hidden = true;
|
|
sourceImage.hidden = true;
|
|
sourceVideo.removeAttribute("src");
|
|
sourceImage.removeAttribute("src");
|
|
resultVideoPreview.hidden = true;
|
|
resultVideoPreview.removeAttribute("src");
|
|
resultVideoEmpty.hidden = false;
|
|
openSourceButton.hidden = true;
|
|
setCompareEnabled(false);
|
|
sourcePaneTitle.textContent = "查看原始视频";
|
|
if (sourceDialog.open) sourceDialog.close();
|
|
previewEmpty.hidden = false;
|
|
resultGrid.innerHTML = "";
|
|
emptyState.hidden = false;
|
|
summaryStrip.hidden = true;
|
|
videoLink.hidden = true;
|
|
resultCount.textContent = "0 个结果";
|
|
jobMeta.textContent = "等待输入";
|
|
lastFrames = [];
|
|
lastResult = null;
|
|
currentFrame = null;
|
|
}
|
|
|
|
function setCompareEnabled(enabled) {
|
|
openCompareButton.disabled = !enabled;
|
|
controlCompareButton.disabled = !enabled;
|
|
}
|
|
|
|
function selectFrame(frame, index = 0) {
|
|
currentFrame = frame;
|
|
[...resultGrid.querySelectorAll(".result-card")].forEach((node) => {
|
|
node.classList.toggle("is-selected", Number(node.dataset.frameIndex) === Number(frame.frame_index));
|
|
});
|
|
syncLock = true;
|
|
seekMedia(videoPreview, frame.source_time);
|
|
seekMedia(resultVideoPreview, frame.result_time);
|
|
syncLock = false;
|
|
setCompareEnabled(Boolean(selectedFile && frame));
|
|
resultCount.textContent = `${lastFrames.length} 个结果 · 当前帧 ${frame.frame_index}`;
|
|
}
|
|
|
|
function seekMedia(media, time) {
|
|
if (!media || media.hidden || !Number.isFinite(time)) return;
|
|
if (Math.abs(media.currentTime - time) > 0.04) {
|
|
media.currentTime = Math.max(0, time);
|
|
}
|
|
}
|
|
|
|
function nearestFrameBy(source, time) {
|
|
if (!lastFrames.length) return null;
|
|
const key = source === "result" ? "result_time" : "source_time";
|
|
return lastFrames.reduce((best, frame) => {
|
|
const bestValue = Number.isFinite(best[key]) ? best[key] : 0;
|
|
const value = Number.isFinite(frame[key]) ? frame[key] : 0;
|
|
const bestDistance = Math.abs(bestValue - time);
|
|
const distance = Math.abs(value - time);
|
|
return distance < bestDistance ? frame : best;
|
|
}, lastFrames[0]);
|
|
}
|
|
|
|
function syncVideos(source, time) {
|
|
if (syncLock || !lastFrames.length) return;
|
|
const frame = nearestFrameBy(source, time);
|
|
if (!frame) return;
|
|
currentFrame = frame;
|
|
syncLock = true;
|
|
if (source === "source" && !resultVideoPreview.hidden) {
|
|
seekMedia(resultVideoPreview, time);
|
|
}
|
|
if (source === "result" && !videoPreview.hidden) {
|
|
seekMedia(videoPreview, time);
|
|
}
|
|
syncLock = false;
|
|
[...resultGrid.querySelectorAll(".result-card")].forEach((node) => {
|
|
node.classList.toggle("is-selected", Number(node.dataset.frameIndex) === Number(frame.frame_index));
|
|
});
|
|
setCompareEnabled(Boolean(selectedFile && frame));
|
|
resultCount.textContent = `${lastFrames.length} 个结果 · 当前帧 ${frame.frame_index}`;
|
|
}
|
|
|
|
function createMetricBlock(frame) {
|
|
const metrics = document.createElement("dl");
|
|
metrics.className = "metrics";
|
|
[
|
|
["覆盖率", `${(frame.metrics.coverage * 100).toFixed(3)}%`],
|
|
["骨架", frame.metrics.skeleton_length],
|
|
["连通域", frame.metrics.components],
|
|
].forEach(([label, value]) => {
|
|
const item = document.createElement("div");
|
|
const dt = document.createElement("dt");
|
|
const dd = document.createElement("dd");
|
|
dt.textContent = label;
|
|
dd.textContent = value;
|
|
item.append(dt, dd);
|
|
metrics.appendChild(item);
|
|
});
|
|
return metrics;
|
|
}
|
|
|
|
function renderCompareFrames(frames) {
|
|
compareGrid.innerHTML = "";
|
|
frames.forEach((frame) => {
|
|
const card = document.createElement("article");
|
|
card.className = "compare-card";
|
|
|
|
const top = document.createElement("div");
|
|
top.className = "card-top";
|
|
const method = document.createElement("span");
|
|
method.className = "method";
|
|
method.textContent = methodLabels.get(frame.method) || frame.method;
|
|
const index = document.createElement("span");
|
|
index.className = "frame-index";
|
|
index.textContent = `帧 ${frame.frame_index}`;
|
|
top.append(method, index);
|
|
|
|
const figure = document.createElement("figure");
|
|
const image = document.createElement("img");
|
|
image.src = frame.overlay_url;
|
|
image.alt = `${method.textContent} 叠加结果`;
|
|
const caption = document.createElement("figcaption");
|
|
caption.textContent = "当前帧叠加视图";
|
|
figure.append(image, caption);
|
|
|
|
card.append(top, figure, createMetricBlock(frame));
|
|
compareGrid.appendChild(card);
|
|
});
|
|
}
|
|
|
|
async function openCompareForCurrentFrame() {
|
|
if (!selectedFile || !currentFrame) return;
|
|
setCompareEnabled(false);
|
|
compareTitle.textContent = `当前帧 ${currentFrame.frame_index} 多方法对比`;
|
|
compareGrid.innerHTML = '<div class="compare-loading">正在生成当前帧多方法对比。</div>';
|
|
if (!compareDialog.open) compareDialog.showModal();
|
|
|
|
try {
|
|
const payload = new FormData();
|
|
payload.set("file", selectedFile);
|
|
payload.set("frame_index", currentFrame.frame_index);
|
|
payload.set("sensitivity", sensitivity.value);
|
|
const response = await fetch("/api/compare-frame", {
|
|
method: "POST",
|
|
body: payload,
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(data.detail || "多方法对比失败");
|
|
}
|
|
renderCompareFrames(data.frames || []);
|
|
} catch (error) {
|
|
compareGrid.innerHTML = "";
|
|
const message = document.createElement("div");
|
|
message.className = "compare-loading";
|
|
message.textContent = error.message || "多方法对比失败";
|
|
compareGrid.appendChild(message);
|
|
} finally {
|
|
setCompareEnabled(Boolean(selectedFile && currentFrame));
|
|
}
|
|
}
|
|
|
|
sensitivity.addEventListener("input", () => {
|
|
sensitivityValue.textContent = Number(sensitivity.value).toFixed(2);
|
|
});
|
|
|
|
fileInput.addEventListener("change", () => {
|
|
const file = fileInput.files[0];
|
|
if (file) setFile(file);
|
|
});
|
|
|
|
["dragenter", "dragover"].forEach((eventName) => {
|
|
dropZone.addEventListener(eventName, (event) => {
|
|
event.preventDefault();
|
|
dropZone.classList.add("is-dragging");
|
|
});
|
|
});
|
|
|
|
["dragleave", "drop"].forEach((eventName) => {
|
|
dropZone.addEventListener(eventName, (event) => {
|
|
event.preventDefault();
|
|
dropZone.classList.remove("is-dragging");
|
|
});
|
|
});
|
|
|
|
dropZone.addEventListener("drop", (event) => {
|
|
const file = event.dataTransfer.files[0];
|
|
if (file) setFile(file);
|
|
});
|
|
|
|
sampleButton.addEventListener("click", loadSample);
|
|
clearButton.addEventListener("click", clearAll);
|
|
closeDialog.addEventListener("click", () => detailDialog.close());
|
|
closeSourceDialog.addEventListener("click", () => sourceDialog.close());
|
|
closeCompareDialog.addEventListener("click", () => compareDialog.close());
|
|
openCompareButton.addEventListener("click", openCompareForCurrentFrame);
|
|
controlCompareButton.addEventListener("click", openCompareForCurrentFrame);
|
|
|
|
videoPreview.addEventListener("seeked", () => syncVideos("source", videoPreview.currentTime));
|
|
resultVideoPreview.addEventListener("seeked", () => syncVideos("result", resultVideoPreview.currentTime));
|
|
videoPreview.addEventListener("play", () => {
|
|
if (!syncLock && !resultVideoPreview.hidden) resultVideoPreview.play().catch(() => {});
|
|
});
|
|
resultVideoPreview.addEventListener("play", () => {
|
|
if (!syncLock && !videoPreview.hidden) videoPreview.play().catch(() => {});
|
|
});
|
|
videoPreview.addEventListener("pause", () => {
|
|
if (!syncLock && !resultVideoPreview.hidden) resultVideoPreview.pause();
|
|
});
|
|
resultVideoPreview.addEventListener("pause", () => {
|
|
if (!syncLock && !videoPreview.hidden) videoPreview.pause();
|
|
});
|
|
|
|
openSourceButton.addEventListener("click", () => {
|
|
if (!selectedFile || !currentObjectUrl) return;
|
|
sourceVideo.hidden = true;
|
|
sourceImage.hidden = true;
|
|
if (selectedFile.type.startsWith("video/")) {
|
|
sourceVideo.preload = "metadata";
|
|
sourceVideo.onloadedmetadata = () => {
|
|
if (Number.isFinite(sourceVideo.duration) && sourceVideo.duration > 0.2) {
|
|
sourceVideo.currentTime = 0.1;
|
|
}
|
|
};
|
|
sourceVideo.src = currentObjectUrl;
|
|
sourceVideo.load();
|
|
sourceVideo.hidden = false;
|
|
} else {
|
|
sourceImage.src = currentObjectUrl;
|
|
sourceImage.hidden = false;
|
|
}
|
|
sourceDialog.showModal();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const file = fileInput.files[0] || selectedFile;
|
|
if (!file) {
|
|
emptyState.hidden = false;
|
|
emptyState.textContent = "请先选择文件或加载样例。";
|
|
return;
|
|
}
|
|
|
|
setBusy(true);
|
|
scheduleProgress();
|
|
setCompareEnabled(false);
|
|
currentFrame = null;
|
|
emptyState.hidden = false;
|
|
emptyState.textContent = "正在抽帧和分割,请稍候。";
|
|
resultGrid.innerHTML = "";
|
|
resultVideoPreview.hidden = true;
|
|
resultVideoPreview.removeAttribute("src");
|
|
resultVideoEmpty.hidden = false;
|
|
videoLink.hidden = true;
|
|
|
|
try {
|
|
const payload = new FormData(form);
|
|
payload.set("file", file);
|
|
const response = await fetch("/api/segment", {
|
|
method: "POST",
|
|
body: payload,
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(data.detail || "分割失败");
|
|
}
|
|
renderResults(data);
|
|
finishProgress();
|
|
} catch (error) {
|
|
emptyState.hidden = false;
|
|
emptyState.textContent = error.message;
|
|
failProgress(error.message || "分割失败");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
});
|
|
|
|
loadHealth();
|
|
loadMethods();
|