344 lines
11 KiB
JavaScript
344 lines
11 KiB
JavaScript
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;
|
||
});
|