const state = { images: [], capabilities: null, selectedImage: null, currentJob: null, pollTimer: null, }; const $ = (id) => document.getElementById(id); function assetUrl(path) { return `/asset?path=${encodeURIComponent(path)}&t=${Date.now()}`; } async function api(path, options = {}) { const response = await fetch(path, options); const payload = await response.json(); if (!response.ok) { throw new Error(payload.error || response.statusText); } return payload; } function formatBytes(bytes) { if (!Number.isFinite(bytes)) return ""; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } function formatGain(value) { const number = Number(value); return Number.isFinite(number) ? number.toFixed(3) : ""; } function svSummary(meta = {}) { const s = formatGain(meta.s_gain); const v = formatGain(meta.v_gain); if (!s || !v) return ""; return `S ${s} · V ${v}`; } function updateGainLabels() { $("sGainValue").textContent = formatGain($("sGain").value); $("vGainValue").textContent = formatGain($("vGain").value); } function setOnlyPostprocessor(processor) { [...$("postList").querySelectorAll("input[type=checkbox]")].forEach((input) => { input.checked = input.value === processor; }); } function setSelectIfOption(selectId, value) { const select = $(selectId); if ([...select.options].some((option) => option.value === value)) { select.value = value; } } function applySvFromResult(item) { const meta = item.meta || {}; const sGain = Number(meta.s_gain); const vGain = Number(meta.v_gain); if (!Number.isFinite(sGain) || !Number.isFinite(vGain)) return; $("sGain").value = Math.min(2.5, Math.max(0, sGain)); $("vGain").value = Math.min(2.5, Math.max(0, vGain)); updateGainLabels(); setOnlyPostprocessor("manual_sv"); if (item.source) { setSelectIfOption("postSource", item.source); } if (item.reference) { setSelectIfOption("referenceImage", item.reference); } $("logBox").textContent = `已载入 ${item.name} 的参数:S=${formatGain(sGain)},V=${formatGain(vGain)}\n可在左侧手动微调后点击“生成后处理”。`; setJobState("SV Ready", "done"); } function renderImages() { const box = $("imageList"); box.innerHTML = ""; state.images.forEach((image) => { const item = document.createElement("button"); item.className = `image-item ${state.selectedImage === image.name ? "active" : ""}`; item.type = "button"; item.innerHTML = ` ${image.name} ${image.width || "?"}x${image.height || "?"} · ${formatBytes(image.size)} `; item.addEventListener("click", () => selectImage(image.name)); box.appendChild(item); }); } function renderMethods() { const box = $("methodList"); box.innerHTML = ""; const methods = state.capabilities?.methods || {}; Object.entries(methods).forEach(([key, info]) => { const checked = key === "Baidu_API" || key === "DCP"; const label = document.createElement("label"); label.className = `check-item ${info.available ? "" : "disabled"}`; label.innerHTML = ` ${info.label} ${info.available ? "ready" : `缺 ${info.module_ok ? "模型" : info.module}`} `; box.appendChild(label); }); } function renderPostprocessors() { const box = $("postList"); box.innerHTML = ""; const processors = state.capabilities?.postprocessors || {}; Object.entries(processors).forEach(([key, info]) => { const label = document.createElement("label"); label.className = "check-item"; label.innerHTML = ` ${info.label} `; box.appendChild(label); }); } function renderReferenceOptions() { const select = $("referenceImage"); select.innerHTML = ""; state.images.forEach((image) => { const option = document.createElement("option"); option.value = image.name; option.textContent = image.name; option.selected = image.name === state.selectedImage; select.appendChild(option); }); } function setJobState(text, status = "") { const box = $("jobState"); box.textContent = text; box.className = `job-state ${status}`; } async function selectImage(name) { state.selectedImage = name; $("currentTitle").textContent = name; renderImages(); renderReferenceOptions(); await refreshResults(); } function resultCard(title, path, exists = true, item = null) { const card = document.createElement("article"); const summary = item ? svSummary(item.meta) : ""; card.className = `result-card ${summary ? "clickable" : ""}`; const badge = exists ? 'ready' : '未生成'; const body = exists ? `
${title}
${summary ? `
${summary}点击载入
` : ""}` : '
暂无结果
'; card.innerHTML = `

${title}

${badge}
${body}`; if (summary && item) { card.tabIndex = 0; card.title = "点击将这组 S/V 载入左侧手动调整"; card.addEventListener("click", () => applySvFromResult(item)); card.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); applySvFromResult(item); } }); } return card; } function updatePostSourceOptions(results) { const select = $("postSource"); const previous = select.value; select.innerHTML = ""; const original = document.createElement("option"); original.value = "original"; original.textContent = "原图"; select.appendChild(original); results.dehaze.filter((item) => item.exists).forEach((item) => { const option = document.createElement("option"); option.value = item.method; option.textContent = item.label; select.appendChild(option); }); if ([...select.options].some((option) => option.value === previous)) { select.value = previous; } else if (results.dehaze.some((item) => item.method === "Baidu_API" && item.exists)) { select.value = "Baidu_API"; } else if (results.dehaze.some((item) => item.method === "DCP" && item.exists)) { select.value = "DCP"; } } async function refreshResults() { if (!state.selectedImage) return; const results = await api(`/api/results?image=${encodeURIComponent(state.selectedImage)}`); const grid = $("resultGrid"); grid.innerHTML = ""; grid.appendChild(resultCard(results.original.label, results.original.path, true)); results.dehaze.forEach((item) => { grid.appendChild(resultCard(item.label, item.path, item.exists)); }); results.postprocess.forEach((item) => { grid.appendChild(resultCard(item.name, item.path, true, item)); }); updatePostSourceOptions(results); } function selectedValues(containerId) { return [...$(containerId).querySelectorAll("input[type=checkbox]:checked")].map((input) => input.value); } function postParams() { return { manual_sv: { s_gain: Number($("sGain").value), v_gain: Number($("vGain").value), }, hsv_hist: { match_hue: $("matchHue").checked, }, hist_auto_sv: { match_hue: $("matchHue").checked, }, }; } async function startJob(endpoint, payload) { const response = await api(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); state.currentJob = response.job.id; $("logBox").textContent = ""; setJobState("Running", "running"); pollJob(); } async function pollJob() { if (!state.currentJob) return; clearTimeout(state.pollTimer); try { const job = await api(`/api/job?id=${encodeURIComponent(state.currentJob)}`); $("logBox").textContent = (job.logs || []).join("\n"); $("logBox").scrollTop = $("logBox").scrollHeight; if (job.status === "running") { setJobState("Running", "running"); state.pollTimer = setTimeout(pollJob, 1000); } else { setJobState(job.status === "done" ? "Done" : "Error", job.status); await refreshResults(); } } catch (error) { setJobState("Error", "error"); $("logBox").textContent = error.message; } } async function runSelectedModels() { if (!state.selectedImage) return; const methods = selectedValues("methodList"); const payload = { image: state.selectedImage, methods, options: { DCP: { sz: Number($("dcpSz").value), tx: Number($("dcpTx").value), }, }, }; await startJob("/api/run", payload); } async function runDefaultBatch() { await startJob("/api/run-default", { skip_existing: true }); } async function runPostprocess() { if (!state.selectedImage) return; const processors = selectedValues("postList"); const payload = { image: state.selectedImage, source: $("postSource").value, reference: $("referenceImage").value || state.selectedImage, processors, params: postParams(), }; await startJob("/api/postprocess", payload); } function downloadCurrent() { if (!state.selectedImage) return; window.location.href = `/api/download?scope=current&image=${encodeURIComponent(state.selectedImage)}`; } function downloadAll() { window.location.href = "/api/download?scope=all"; } function bindControls() { $("runBtn").addEventListener("click", runSelectedModels); $("runDefaultBatchBtn").addEventListener("click", runDefaultBatch); $("postBtn").addEventListener("click", runPostprocess); $("downloadCurrentBtn").addEventListener("click", downloadCurrent); $("downloadAllBtn").addEventListener("click", downloadAll); $("refreshBtn").addEventListener("click", refreshResults); $("sGain").addEventListener("input", updateGainLabels); $("vGain").addEventListener("input", updateGainLabels); updateGainLabels(); } async function init() { bindControls(); const [capabilities, images] = await Promise.all([api("/api/capabilities"), api("/api/images")]); state.capabilities = capabilities; state.images = images.images || []; $("envLine").textContent = capabilities.results_dir; renderMethods(); renderPostprocessors(); renderImages(); if (state.images.length) { await selectImage(state.images[0].name); } else { $("currentTitle").textContent = "待去雾图片为空"; } setJobState("Idle"); } init().catch((error) => { setJobState("Error", "error"); $("logBox").textContent = error.stack || error.message; });