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
? `
})
${summary ? `${summary}点击载入
` : ""}`
: '暂无结果
';
card.innerHTML = `${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;
});