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 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) { const card = document.createElement("article"); card.className = "result-card"; const badge = exists ? 'ready' : '未生成'; const body = exists ? `
${title}
` : '
暂无结果
'; card.innerHTML = `

${title}

${badge}
${body}`; 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)); }); 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", () => { $("sGainValue").textContent = `${Math.round(Number($("sGain").value) * 100)}%`; }); $("vGain").addEventListener("input", () => { $("vGainValue").textContent = `${Math.round(Number($("vGain").value) * 100)}%`; }); } 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; });