Files
Dehaze/web_dehaze/static/app.js
2026-06-10 17:57:54 +08:00

344 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `
<span>${image.name}</span>
<span class="image-meta">${image.width || "?"}x${image.height || "?"} · ${formatBytes(image.size)}</span>
`;
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 = `
<span>
<strong>${info.label}</strong>
<span class="method-meta">${info.available ? "ready" : `${info.module_ok ? "模型" : info.module}`}</span>
</span>
<input type="checkbox" value="${key}" ${info.available || checked ? "" : "disabled"} ${checked ? "checked" : ""} />
`;
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 = `
<span><strong>${info.label}</strong></span>
<input type="checkbox" value="${key}" ${key === "auto_sv" ? "checked" : ""} />
`;
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 ? '<span class="badge ok">ready</span>' : '<span class="badge pending">未生成</span>';
const body = exists
? `<div class="image-frame"><img src="${assetUrl(path)}" alt="${title}" loading="lazy" /></div>${summary ? `<div class="sv-strip"><span>${summary}</span><small>点击载入</small></div>` : ""}`
: '<div class="image-frame"><span class="missing">暂无结果</span></div>';
card.innerHTML = `<header><h3>${title}</h3>${badge}</header>${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;
});