Files
video_upscaler/static/app.js
T

1484 lines
58 KiB
JavaScript

// App State
let uploadFileId = null;
let uploadMetadata = null;
let currentJobId = null;
let socket = null;
let sliderInitialized = false;
// Error formatting helper
function formatFetchError(err, defaultMsg) {
if (!err) return defaultMsg;
if (typeof err === "string") return err;
if (err.detail) {
if (typeof err.detail === "string") {
return err.detail;
}
if (Array.isArray(err.detail)) {
return err.detail.map(d => {
const locStr = (d.loc && Array.isArray(d.loc)) ? d.loc.join(".") : "";
return locStr ? `${locStr}: ${d.msg}` : (d.msg || JSON.stringify(d));
}).join("\n");
}
if (typeof err.detail === "object") {
return err.detail.message || err.detail.error || JSON.stringify(err.detail);
}
}
if (err.message) return err.message;
return JSON.stringify(err) || defaultMsg;
}
// DOM Elements
const stepUpload = document.getElementById("step-upload");
const stepConfig = document.getElementById("step-config");
const stepProgress = document.getElementById("step-progress");
const stepFinished = document.getElementById("step-finished");
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
// Meta fields
const metaName = document.getElementById("meta-name");
const metaSize = document.getElementById("meta-size");
const metaRes = document.getElementById("meta-res");
const metaDuration = document.getElementById("meta-duration");
const metaFps = document.getElementById("meta-fps");
// Config inputs
const modelSelect = document.getElementById("model-select");
const scaleRange = document.getElementById("scale-range");
const scaleValue = document.getElementById("scale-value");
const tileRange = document.getElementById("tile-range");
const tileValue = document.getElementById("tile-value");
const audioToggle = document.getElementById("audio-toggle");
// Timeline and Preview elements
const timelineSlider = document.getElementById("timeline-slider");
const timelineTime = document.getElementById("timeline-time");
const generatePreviewBtn = document.getElementById("generate-preview-btn");
const generateVideoPreviewBtn = document.getElementById("generate-video-preview-btn");
const previewPlaceholderBox = document.getElementById("preview-placeholder-box");
const comparisonSliderContainer = document.getElementById("comparison-slider-container");
const previewLoader = document.getElementById("preview-loader");
const imgOriginal = document.getElementById("img-original");
const imgUpscaled = document.getElementById("img-upscaled");
const sliderSplit = document.getElementById("slider-split");
// Progress elements
const jobIdDisplay = document.getElementById("job-id-display");
const statusBadge = document.getElementById("status-badge");
const progressPercent = document.getElementById("progress-percent");
const progressBarFill = document.getElementById("progress-bar-fill");
const statStatus = document.getElementById("stat-status");
const statFrames = document.getElementById("stat-frames");
const statEta = document.getElementById("stat-eta");
const logTerminal = document.getElementById("log-terminal");
const cancelUpscaleBtn = document.getElementById("cancel-upscale-btn");
const startUpscaleBtn = document.getElementById("start-upscale-btn");
// Finished elements
const downloadLink = document.getElementById("download-link");
const finishedPlayComparisonBtn = document.getElementById("finished-play-comparison-btn");
// Synced Playback Modal Elements
const videoModal = document.getElementById("video-modal");
const modalVideoOriginal = document.getElementById("modal-video-original");
const modalVideoUpscaled = document.getElementById("modal-video-upscaled");
const modalPlayBtn = document.getElementById("modal-play-btn");
const modalTimeSlider = document.getElementById("modal-time-slider");
const modalTimeCurrent = document.getElementById("modal-time-current");
const modalTimeDuration = document.getElementById("modal-time-duration");
const modalMuteBtn = document.getElementById("modal-mute-btn");
const modalCloseBtn = document.getElementById("modal-close-btn");
/* ----------------------------------------------------
1. Theme Selector
---------------------------------------------------- */
const themeSelect = document.getElementById("theme-select");
function applyTheme(theme) {
document.body.className = "";
if (theme && theme !== "cyberpunk") {
document.body.classList.add(`theme-${theme}`);
} else {
document.body.classList.add("theme-cyberpunk");
}
localStorage.setItem("selected-theme", theme);
if (themeSelect) themeSelect.value = theme;
}
if (themeSelect) {
themeSelect.addEventListener("change", (e) => {
applyTheme(e.target.value);
});
}
applyTheme(localStorage.getItem("selected-theme") || "cyberpunk");
/* ----------------------------------------------------
2. Dynamic Sliders & Parameter Listeners
---------------------------------------------------- */
scaleRange.addEventListener("input", (e) => {
scaleValue.textContent = `${e.target.value}x`;
// Lock model scaling if needed: Real-ESRGAN x4 models are scale 4. Real-ESRGAN-animevideov3 supports 2,3,4.
const model = modelSelect.value;
if (model.includes("x4plus") && e.target.value !== "4") {
scaleRange.value = 4;
scaleValue.textContent = "4x (Locked)";
}
});
modelSelect.addEventListener("change", (e) => {
const val = e.target.value;
if (val.includes("x4plus")) {
scaleRange.value = 4;
scaleRange.disabled = true;
scaleValue.textContent = "4x (Fixed)";
} else {
scaleRange.disabled = false;
scaleValue.textContent = `${scaleRange.value}x`;
}
});
tileRange.addEventListener("input", (e) => {
tileValue.textContent = e.target.value === "0" ? "Auto" : e.target.value;
});
const crfRange = document.getElementById("crf-range");
const crfValue = document.getElementById("crf-value");
if (crfRange && crfValue) {
crfRange.addEventListener("input", (e) => {
let label = e.target.value;
if (e.target.value === "0") label += " (Lossless)";
else if (e.target.value === "18") label += " (Pristine)";
else if (e.target.value > "28") label += " (Compress)";
crfValue.textContent = label;
});
}
/* ----------------------------------------------------
3. Custom Model Upload Action
---------------------------------------------------- */
const customModelInput = document.getElementById("custom-model-input");
const modelUploadFilename = document.getElementById("model-upload-filename");
const uploadModelBtnWrapper = document.getElementById("upload-model-btn-wrapper");
const uploadModelBtn = document.getElementById("upload-model-btn");
if (customModelInput) {
customModelInput.addEventListener("change", (e) => {
const files = e.target.files;
if (files.length > 0) {
const names = Array.from(files).map(f => f.name).join(", ");
modelUploadFilename.textContent = names;
uploadModelBtnWrapper.style.display = "block";
} else {
modelUploadFilename.textContent = "No files selected";
uploadModelBtnWrapper.style.display = "none";
}
});
}
if (uploadModelBtn) {
uploadModelBtn.addEventListener("click", async () => {
const files = customModelInput.files;
if (files.length === 0) return;
uploadModelBtn.disabled = true;
uploadModelBtn.innerHTML = `<div class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;margin-right:5px;"></div> Uploading...`;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
const ext = files[i].name.split('.').pop().toLowerCase();
if (ext === 'param') {
formData.append("param_file", files[i]);
} else if (ext === 'bin') {
formData.append("bin_file", files[i]);
} else {
formData.append("files", files[i]);
}
}
try {
const res = await fetch("/api/models/upload", {
method: "POST",
body: formData
});
if (!res.ok) {
const err = await res.json();
throw new Error(formatFetchError(err, "Model upload failed."));
}
alert("Custom model uploaded successfully!");
modelUploadFilename.textContent = "Uploaded successfully!";
uploadModelBtnWrapper.style.display = "none";
await loadModels();
} catch (err) {
alert(err.message);
} finally {
uploadModelBtn.disabled = false;
uploadModelBtn.innerHTML = `<i class="fa-solid fa-cloud-arrow-up"></i> Upload Model Files`;
}
});
}
/* ----------------------------------------------------
4. Video File Upload Routing
---------------------------------------------------- */
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
}, false);
});
dropZone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) handleVideoUpload(files[0]);
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) handleVideoUpload(e.target.files[0]);
});
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function handleVideoUpload(file) {
dropZone.innerHTML = `
<div class="spinner" style="margin: 0 auto 1.5rem auto;"></div>
<h3>Uploading Video File...</h3>
<p id="upload-progress-text">Preparing upload...</p>
<div class="bar-container" style="max-width: 400px; margin: 1rem auto; height: 10px;">
<div id="upload-bar" class="bar-fill" style="width: 0%;"></div>
</div>
`;
const formData = new FormData();
formData.append("file", file);
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/upload", true);
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
const progressText = document.getElementById("upload-progress-text");
const progressBar = document.getElementById("upload-bar");
if (progressText) progressText.textContent = `Uploaded ${formatBytes(e.loaded)} / ${formatBytes(e.total)} (${Math.round(percentComplete)}%)`;
if (progressBar) progressBar.style.width = `${percentComplete}%`;
}
});
xhr.onload = function() {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
uploadFileId = res.file_id;
uploadMetadata = res.metadata;
// Populate Config Step Info
metaName.textContent = res.filename;
metaSize.textContent = formatBytes(file.size);
// Populate Accordions Stream details
const streams = res.metadata.streams || [];
const videoStream = streams.find(s => s.codec_type === "video") || {};
const audioStream = streams.find(s => s.codec_type === "audio") || {};
// Video stream metadata
document.getElementById("meta-res-codec").textContent = (videoStream.codec_name || "unknown").toUpperCase();
document.getElementById("meta-res").textContent = `${res.metadata.width} x ${res.metadata.height}`;
document.getElementById("meta-fps").textContent = `${res.metadata.fps} FPS`;
document.getElementById("meta-duration").textContent = `${res.metadata.duration}s`;
document.getElementById("meta-total-frames").textContent = res.metadata.total_frames || "-";
document.getElementById("meta-aspect-ratio").textContent = videoStream.display_aspect_ratio || "N/A";
// Audio stream metadata
if (audioStream.codec_name) {
document.getElementById("meta-audio-codec").textContent = (audioStream.codec_name || "unknown").toUpperCase();
document.getElementById("meta-audio-channels").textContent = audioStream.channels || "N/A";
document.getElementById("meta-audio-samplerate").textContent = audioStream.sample_rate ? `${audioStream.sample_rate} Hz` : "N/A";
document.getElementById("meta-audio-bitrate").textContent = audioStream.bit_rate ? formatBytes(parseInt(audioStream.bit_rate)) + "/s" : "N/A";
} else {
document.getElementById("meta-audio-codec").textContent = "No Audio Stream";
document.getElementById("meta-audio-channels").textContent = "-";
document.getElementById("meta-audio-samplerate").textContent = "-";
document.getElementById("meta-audio-bitrate").textContent = "-";
}
// Configure timeline slider constraints
timelineSlider.min = 0.1;
timelineSlider.max = res.metadata.duration - 0.5;
timelineSlider.value = (res.metadata.duration * 0.15).toFixed(2);
timelineTime.textContent = `${timelineSlider.value}s`;
// Configure Trimming panel range values
const trimStartRange = document.getElementById("trim-start-range");
const trimEndRange = document.getElementById("trim-end-range");
const trimStartValue = document.getElementById("trim-start-value");
const trimEndValue = document.getElementById("trim-end-value");
trimStartRange.min = 0;
trimStartRange.max = res.metadata.duration;
trimStartRange.value = 0;
trimStartValue.textContent = "0.00s";
trimEndRange.min = 0;
trimEndRange.max = res.metadata.duration;
trimEndRange.value = res.metadata.duration;
trimEndValue.textContent = `${res.metadata.duration.toFixed(2)}s`;
// Show config view
stepUpload.classList.remove("active");
stepConfig.classList.add("active");
} else {
alert("Upload failed: " + (JSON.parse(xhr.responseText).detail || xhr.statusText));
window.location.reload();
}
};
xhr.onerror = function() {
alert("Upload error occurred.");
window.location.reload();
};
xhr.send(formData);
}
// Timeline slider change
timelineSlider.addEventListener("input", (e) => {
timelineTime.textContent = `${parseFloat(e.target.value).toFixed(2)}s`;
});
// Trimming sliders inputs listeners
const trimStartRange = document.getElementById("trim-start-range");
const trimEndRange = document.getElementById("trim-end-range");
const trimStartValue = document.getElementById("trim-start-value");
const trimEndValue = document.getElementById("trim-end-value");
trimStartRange.addEventListener("input", (e) => {
let val = parseFloat(e.target.value);
const endVal = parseFloat(trimEndRange.value);
if (val >= endVal) {
val = endVal - 0.05;
trimStartRange.value = val;
}
trimStartValue.textContent = `${val.toFixed(2)}s`;
});
trimEndRange.addEventListener("input", (e) => {
let val = parseFloat(e.target.value);
const startVal = parseFloat(trimStartRange.value);
if (val <= startVal) {
val = startVal + 0.05;
trimEndRange.value = val;
}
trimEndValue.textContent = `${val.toFixed(2)}s`;
});
/* ----------------------------------------------------
5. Single Frame Preview Comparisons & Synced Zoom/Pan
---------------------------------------------------- */
const zoomSlider = document.getElementById("zoom-slider");
const zoomVal = document.getElementById("zoom-val");
const panXSlider = document.getElementById("pan-x-slider");
const panXVal = document.getElementById("pan-x-val");
const panYSlider = document.getElementById("pan-y-slider");
const panYVal = document.getElementById("pan-y-val");
const resetZoomPanBtn = document.getElementById("reset-zoom-pan-btn");
let previewZoom = 1.0;
let previewPanX = 0;
let previewPanY = 0;
function updatePreviewTransform() {
const transformStr = `translate(${previewPanX}px, ${previewPanY}px) scale(${previewZoom})`;
imgOriginal.style.transform = transformStr;
imgOriginal.style.transformOrigin = "center center";
imgOriginal.style.transition = "transform 0.1s ease-out";
imgUpscaled.style.transform = transformStr;
imgUpscaled.style.transformOrigin = "center center";
imgUpscaled.style.transition = "transform 0.1s ease-out";
const vidOriginal = document.getElementById("video-preview-original");
const vidUpscaled = document.getElementById("video-preview-upscaled");
if (vidOriginal) {
vidOriginal.style.transform = transformStr;
vidOriginal.style.transformOrigin = "center center";
vidOriginal.style.transition = "transform 0.1s ease-out";
}
if (vidUpscaled) {
vidUpscaled.style.transform = transformStr;
vidUpscaled.style.transformOrigin = "center center";
vidUpscaled.style.transition = "transform 0.1s ease-out";
}
zoomSlider.value = previewZoom;
zoomVal.textContent = `${previewZoom.toFixed(1)}x`;
panXSlider.value = previewPanX;
panXVal.textContent = `${previewPanX}px`;
panYSlider.value = previewPanY;
panYVal.textContent = `${previewPanY}px`;
}
zoomSlider.addEventListener("input", (e) => {
previewZoom = parseFloat(e.target.value);
updatePreviewTransform();
});
panXSlider.addEventListener("input", (e) => {
previewPanX = parseInt(e.target.value);
updatePreviewTransform();
});
panYSlider.addEventListener("input", (e) => {
previewPanY = parseInt(e.target.value);
updatePreviewTransform();
});
resetZoomPanBtn.addEventListener("click", () => {
previewZoom = 1.0;
previewPanX = 0;
previewPanY = 0;
updatePreviewTransform();
});
// Dragging to pan comparison image
let isDraggingPreview = false;
let startDragX = 0;
let startDragY = 0;
comparisonSliderContainer.addEventListener("mousedown", (e) => {
if (e.target.id === "slider-split" || e.target.classList.contains("slider-handle")) {
return;
}
isDraggingPreview = true;
startDragX = e.clientX - previewPanX;
startDragY = e.clientY - previewPanY;
comparisonSliderContainer.style.cursor = "grabbing";
});
window.addEventListener("mouseup", () => {
if (isDraggingPreview) {
isDraggingPreview = false;
comparisonSliderContainer.style.cursor = "default";
}
});
window.addEventListener("mousemove", (e) => {
if (!isDraggingPreview) return;
previewPanX = e.clientX - startDragX;
previewPanY = e.clientY - startDragY;
if (previewPanX < -400) previewPanX = -400;
if (previewPanX > 400) previewPanX = 400;
if (previewPanY < -400) previewPanY = -400;
if (previewPanY > 400) previewPanY = 400;
updatePreviewTransform();
});
// Generate single frame preview trigger
generatePreviewBtn.addEventListener("click", async () => {
if (!uploadFileId) return;
const loaderTitle = document.getElementById("preview-loader-title");
const loaderContainer = document.getElementById("preview-loader-progress-container");
const loaderText = document.getElementById("preview-loader-text");
previewLoader.style.display = "flex";
previewPlaceholderBox.style.display = "none";
comparisonSliderContainer.style.display = "none";
document.getElementById("zoom-pan-controls").style.display = "none";
document.getElementById("video-preview-controls").style.display = "none";
if (loaderTitle) loaderTitle.textContent = "Orchestrating Vulkan GPU...";
if (loaderContainer) loaderContainer.style.display = "none";
if (loaderText) loaderText.textContent = "Extracting and upscaling frame. Please wait...";
const gpuSelect = document.getElementById("gpu-select");
try {
const res = await fetch("/api/preview/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_id: uploadFileId,
timestamp_sec: parseFloat(timelineSlider.value),
model: modelSelect.value,
scale: parseInt(scaleRange.value),
tile_size: parseInt(tileRange.value),
gpu_ids: gpuSelect.value === 'auto' ? null : gpuSelect.value
})
});
if (!res.ok) {
const err = await res.json();
throw new Error(formatFetchError(err, "Frame preview rendering failed."));
}
const data = await res.json();
const previewId = data.preview_id;
imgOriginal.src = `/api/preview/original/${previewId}?t=${Date.now()}`;
imgUpscaled.src = `/api/preview/upscaled/${previewId}?t=${Date.now()}`;
await Promise.all([
new Promise(resolve => imgOriginal.onload = resolve),
new Promise(resolve => imgUpscaled.onload = resolve)
]);
// Hide videos, show images
imgOriginal.style.display = "block";
document.getElementById("img-upscaled-wrapper").style.display = "block";
document.getElementById("video-preview-original").style.display = "none";
document.getElementById("video-preview-upscaled-wrapper").style.display = "none";
previewPlaceholderBox.style.display = "none";
comparisonSliderContainer.style.display = "block";
document.getElementById("zoom-pan-controls").style.display = "flex";
initComparisonSlider();
// Reset transforms
previewZoom = 1.0;
previewPanX = 0;
previewPanY = 0;
updatePreviewTransform();
} catch (err) {
alert(err.message);
} finally {
previewLoader.style.display = "none";
}
});
generateVideoPreviewBtn.addEventListener("click", async () => {
if (!uploadFileId) return;
const loaderTitle = document.getElementById("preview-loader-title");
const loaderContainer = document.getElementById("preview-loader-progress-container");
const loaderBar = document.getElementById("preview-loader-progress-bar");
const loaderText = document.getElementById("preview-loader-text");
previewLoader.style.display = "flex";
previewPlaceholderBox.style.display = "none";
comparisonSliderContainer.style.display = "none";
document.getElementById("zoom-pan-controls").style.display = "none";
document.getElementById("video-preview-controls").style.display = "none";
if (loaderTitle) loaderTitle.textContent = "Generating Video Preview";
if (loaderContainer) loaderContainer.style.display = "block";
if (loaderBar) loaderBar.style.width = "0%";
if (loaderText) loaderText.textContent = "Queuing preview job in pipeline...";
const gpuSelect = document.getElementById("gpu-select");
const crfRange = document.getElementById("crf-range");
const presetSelect = document.getElementById("preset-select");
const transcodeSelect = document.getElementById("transcode-select");
const filterDenoise = document.getElementById("filter-denoise");
const filterSharpen = document.getElementById("filter-sharpen");
const filterFps = document.getElementById("filter-fps");
const transcodeFormat = transcodeSelect.value;
try {
const res = await fetch("/api/upscale/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_id: uploadFileId,
model: modelSelect.value,
scale: parseInt(scaleRange.value),
tile_size: parseInt(tileRange.value),
preserve_audio: audioToggle.checked,
start_sec: parseFloat(timelineSlider.value),
end_sec: parseFloat(timelineSlider.value) + 5.0,
gpu_ids: gpuSelect.value === 'auto' ? null : gpuSelect.value,
tta: false,
unsharp: false,
double_fps: false,
preserve_subtitles: false,
crf: parseInt(crfRange.value),
preset: presetSelect.value,
denoise: filterDenoise.checked,
sharpen: filterSharpen.checked,
interpolation: filterFps.checked,
webhook_url: null,
transcode_format: transcodeFormat,
is_preview: true
})
});
if (!res.ok) {
const err = await res.json();
throw new Error(formatFetchError(err, "Failed to start video preview."));
}
const data = await res.json();
currentJobId = data.job_id;
connectProgressWebSocket(currentJobId);
} catch (err) {
alert(err.message);
previewLoader.style.display = "none";
previewPlaceholderBox.style.display = "flex";
}
});
function initComparisonSlider() {
const upscaledWrapper = document.getElementById("img-upscaled-wrapper");
const videoUpscaledWrapper = document.getElementById("video-preview-upscaled-wrapper");
// Reset split positions to 50%
if (upscaledWrapper) upscaledWrapper.style.clipPath = `inset(0 0 0 50%)`;
if (videoUpscaledWrapper) videoUpscaledWrapper.style.clipPath = `inset(0 0 0 50%)`;
sliderSplit.style.left = `50%`;
if (sliderInitialized) return;
sliderInitialized = true;
let active = false;
function setSliderPosition(x) {
const rect = comparisonSliderContainer.getBoundingClientRect();
let posX = x - rect.left;
if (posX < 0) posX = 0;
if (posX > rect.width) posX = rect.width;
const pct = (posX / rect.width) * 100;
if (upscaledWrapper) upscaledWrapper.style.clipPath = `inset(0 0 0 ${pct}%)`;
if (videoUpscaledWrapper) videoUpscaledWrapper.style.clipPath = `inset(0 0 0 ${pct}%)`;
sliderSplit.style.left = `${pct}%`;
}
sliderSplit.addEventListener('mousedown', (e) => { e.stopPropagation(); active = true; });
window.addEventListener('mouseup', () => { active = false; });
window.addEventListener('mousemove', (e) => {
if (!active) return;
setSliderPosition(e.pageX);
});
sliderSplit.addEventListener('touchstart', (e) => { e.stopPropagation(); active = true; });
window.addEventListener('touchend', () => { active = false; });
window.addEventListener('touchmove', (e) => {
if (!active) return;
setSliderPosition(e.touches[0].pageX);
});
comparisonSliderContainer.addEventListener('dblclick', () => {
upscaledWrapper.style.clipPath = `inset(0 0 0 50%)`;
sliderSplit.style.left = `50%`;
});
}
/* ----------------------------------------------------
6. Video Pipeline Jobs Orchestration
---------------------------------------------------- */
startUpscaleBtn.addEventListener("click", async () => {
if (!uploadFileId) return;
startUpscaleBtn.disabled = true;
startUpscaleBtn.innerHTML = `<div class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;"></div> Starting Job...`;
const gpuSelect = document.getElementById("gpu-select");
const crfRange = document.getElementById("crf-range");
const presetSelect = document.getElementById("preset-select");
const transcodeSelect = document.getElementById("transcode-select");
const filterDenoise = document.getElementById("filter-denoise");
const filterSharpen = document.getElementById("filter-sharpen");
const filterFps = document.getElementById("filter-fps");
const webhookUrl = document.getElementById("webhook-url");
try {
const res = await fetch("/api/upscale/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_id: uploadFileId,
model: modelSelect.value,
scale: parseInt(scaleRange.value),
tile_size: parseInt(tileRange.value),
preserve_audio: audioToggle.checked,
// Advanced fields
start_sec: parseFloat(trimStartRange.value),
end_sec: parseFloat(trimEndRange.value),
gpu_ids: gpuSelect.value === "auto" ? null : gpuSelect.value,
crf: parseInt(crfRange.value),
preset: presetSelect.value,
transcode_format: transcodeSelect.value,
denoise: filterDenoise.checked,
sharpen: filterSharpen.checked,
interpolation: filterFps.checked,
webhook_url: webhookUrl.value.trim() || null
})
});
if (!res.ok) {
const err = await res.json();
throw new Error(formatFetchError(err, "Orchestrator failed to launch job."));
}
const data = await res.json();
currentJobId = data.job_id;
stepConfig.classList.remove("active");
stepProgress.classList.add("active");
jobIdDisplay.textContent = currentJobId;
connectProgressWebSocket(currentJobId);
} catch (err) {
alert(err.message);
startUpscaleBtn.disabled = false;
startUpscaleBtn.innerHTML = `<i class="fa-solid fa-play"></i> Start Full Upscale`;
}
});
function connectProgressWebSocket(jobId) {
const loc = window.location;
const protocol = loc.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${loc.host}/ws/progress/${jobId}`;
socket = new WebSocket(wsUrl);
logTerminal.innerHTML = `<div class="log-line">Connected to backend job orchestrator...</div>`;
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
updateProgressUI(data);
};
socket.onclose = function() {
console.log("WebSocket progress connection closed");
};
socket.onerror = function(err) {
console.error("WebSocket error:", err);
startFallbackPolling(jobId);
};
}
function appendLogLine(text, type = "info") {
const line = document.createElement("div");
line.className = `log-line ${type}`;
line.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
logTerminal.appendChild(line);
logTerminal.scrollTop = logTerminal.scrollHeight;
}
function updateProgressUI(data) {
if (data.is_preview) {
// Update inline loader inside the preview panel
const loaderTitle = document.getElementById("preview-loader-title");
const loaderContainer = document.getElementById("preview-loader-progress-container");
const loaderBar = document.getElementById("preview-loader-progress-bar");
const loaderText = document.getElementById("preview-loader-text");
if (previewLoader.style.display !== "flex") {
previewLoader.style.display = "flex";
previewPlaceholderBox.style.display = "none";
comparisonSliderContainer.style.display = "none";
document.getElementById("zoom-pan-controls").style.display = "none";
document.getElementById("video-preview-controls").style.display = "none";
}
if (loaderTitle) loaderTitle.textContent = "Generating Video Preview";
if (loaderContainer) loaderContainer.style.display = "block";
if (loaderBar) loaderBar.style.width = `${Math.round(data.progress || 0)}%`;
let statusText = `Status: ${data.status}`;
if (data.status === "upscaling" && data.total_frames > 0) {
statusText = `Upscaling frame ${data.current_frame} of ${data.total_frames} (ETA: ${data.eta})`;
} else if (data.status === "queued") {
statusText = "Queued in pipeline. Waiting for GPU lock...";
} else if (data.status === "extracting") {
statusText = "Extracting video frames...";
} else if (data.status === "assembling") {
statusText = "Assembling video preview and audio tracks...";
}
if (loaderText) loaderText.textContent = statusText;
if (data.status === "completed") {
if (socket) socket.close();
// Hide loader
previewLoader.style.display = "none";
// Show comparison elements
previewPlaceholderBox.style.display = "none";
comparisonSliderContainer.style.display = "block";
document.getElementById("zoom-pan-controls").style.display = "flex";
document.getElementById("video-preview-controls").style.display = "flex";
// Toggle elements visibility: hide images, show videos
imgOriginal.style.display = "none";
document.getElementById("img-upscaled-wrapper").style.display = "none";
const vidOriginal = document.getElementById("video-preview-original");
const vidUpscaledWrapper = document.getElementById("video-preview-upscaled-wrapper");
const vidUpscaled = document.getElementById("video-preview-upscaled");
vidOriginal.style.display = "block";
vidUpscaledWrapper.style.display = "block";
vidOriginal.src = `/api/download/${data.original_preview_file}`;
vidUpscaled.src = `/api/download/${data.output_file}`;
vidOriginal.load();
vidUpscaled.load();
initComparisonSlider();
initVideoPreviewControls(vidOriginal, vidUpscaled);
// Reset zoom/pan
previewZoom = 1.0;
previewPanX = 0;
previewPanY = 0;
updatePreviewTransform();
} else if (data.status === "failed") {
alert(`Preview generation failed: ${data.error}`);
previewLoader.style.display = "none";
previewPlaceholderBox.style.display = "flex";
}
loadQueue();
return;
}
statusBadge.className = `status-badge ${data.status}`;
statusBadge.textContent = data.status;
statStatus.textContent = data.status.toUpperCase();
if (data.total_frames > 0) {
statFrames.textContent = `${data.current_frame} / ${data.total_frames}`;
} else {
statFrames.textContent = "-";
}
statEta.textContent = data.eta || "Calculating...";
const progressVal = Math.round(data.progress || 0);
progressPercent.textContent = `${progressVal}%`;
progressBarFill.style.width = `${progressVal}%`;
if (data.status === "queued") {
appendLogLine("Job added to FIFO queue. Waiting for GPU lock...", "info");
} else if (data.status === "analyzing") {
appendLogLine("GPU locked. Analyzing video parameters & framerates...", "info");
} else if (data.status === "extracting") {
appendLogLine("FFmpeg: Splitting video stream to high-fidelity JPG frames...", "info");
} else if (data.status === "upscaling") {
appendLogLine(`Vulkan GPU: Processing frame ${data.current_frame} of ${data.total_frames} (ETA: ${data.eta})...`, "info");
} else if (data.status === "assembling") {
appendLogLine("FFmpeg: Reassembling enhanced frames & merging audio tracks...", "info");
} else if (data.status === "completed") {
appendLogLine("Success! Upscaling process finished cleanly. Temp files deleted.", "success");
setTimeout(() => {
if (socket) socket.close();
stepProgress.classList.remove("active");
stepFinished.classList.add("active");
downloadLink.href = `/api/download/${data.output_file}`;
loadGallery();
}, 1500);
} else if (data.status === "failed") {
appendLogLine(`Critical Error: ${data.error}`, "error");
alert(`Upscaling job failed: ${data.error}`);
} else if (data.status === "cancelled") {
appendLogLine("Process aborted by user.", "error");
alert("Upscaling job cancelled.");
window.location.reload();
}
loadQueue();
}
let vprepScrubbing = false;
function initVideoPreviewControls(vidOriginal, vidUpscaled) {
const playBtn = document.getElementById("vprep-play-btn");
const scrubber = document.getElementById("vprep-scrubber");
const timeCurrent = document.getElementById("vprep-time-current");
const timeDuration = document.getElementById("vprep-time-duration");
// Play/Pause button click
playBtn.onclick = () => {
if (vidOriginal.paused) {
vidOriginal.play();
vidUpscaled.play();
playBtn.innerHTML = `<i class="fa-solid fa-pause"></i>`;
} else {
vidOriginal.pause();
vidUpscaled.pause();
playBtn.innerHTML = `<i class="fa-solid fa-play"></i>`;
}
};
// Time Sync logic
vidOriginal.onplay = () => { vidUpscaled.play(); playBtn.innerHTML = `<i class="fa-solid fa-pause"></i>`; };
vidOriginal.onpause = () => { vidUpscaled.pause(); playBtn.innerHTML = `<i class="fa-solid fa-play"></i>`; };
vidOriginal.onloadedmetadata = () => {
scrubber.max = vidOriginal.duration || 5;
timeDuration.textContent = formatTime(vidOriginal.duration || 5);
};
vidOriginal.ontimeupdate = () => {
if (!vprepScrubbing) {
scrubber.value = vidOriginal.currentTime;
timeCurrent.textContent = formatTime(vidOriginal.currentTime);
}
// Sync check
const diff = Math.abs(vidOriginal.currentTime - vidUpscaled.currentTime);
if (diff > 0.15 && !vidOriginal.seeking && !vidUpscaled.seeking) {
vidUpscaled.currentTime = vidOriginal.currentTime;
}
};
// Scrubber drag seeking
scrubber.onmousedown = () => { vprepScrubbing = true; };
scrubber.onmouseup = () => {
vprepScrubbing = false;
vidOriginal.currentTime = parseFloat(scrubber.value);
vidUpscaled.currentTime = parseFloat(scrubber.value);
};
scrubber.oninput = () => {
timeCurrent.textContent = formatTime(parseFloat(scrubber.value));
};
// Mobile touch scrubber events
scrubber.ontouchstart = () => { vprepScrubbing = true; };
scrubber.ontouchend = () => {
vprepScrubbing = false;
vidOriginal.currentTime = parseFloat(scrubber.value);
vidUpscaled.currentTime = parseFloat(scrubber.value);
};
}
let pollingInterval = null;
function startFallbackPolling(jobId) {
if (pollingInterval) clearInterval(pollingInterval);
appendLogLine("Fallback: WebSockets offline. Switched to REST polling...", "warning");
pollingInterval = setInterval(async () => {
try {
const res = await fetch(`/api/upscale/status/${jobId}`);
if (res.ok) {
const data = await res.json();
updateProgressUI(data);
if (data.status === "completed" || data.status === "failed" || data.status === "cancelled") {
clearInterval(pollingInterval);
}
}
} catch (err) {
console.error("Polling error:", err);
}
}, 1000);
}
cancelUpscaleBtn.addEventListener("click", async () => {
if (!currentJobId) return;
if (confirm("Are you sure you want to abort the upscaling process? All progress will be lost.")) {
try {
const res = await fetch(`/api/upscale/cancel/${currentJobId}`, { method: "POST" });
if (res.ok) {
appendLogLine("Sending abort command to worker...", "error");
}
} catch (err) {
console.error("Error cancelling job:", err);
}
}
});
async function checkActiveJobs() {
try {
const res = await fetch("/api/jobs");
if (!res.ok) return;
const jobs = await res.json();
const activeJob = jobs.find(job =>
["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)
);
if (activeJob) {
currentJobId = activeJob.job_id;
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
stepProgress.classList.add("active");
jobIdDisplay.textContent = currentJobId;
connectProgressWebSocket(currentJobId);
}
} catch (err) {
console.error("Error checking active jobs:", err);
}
}
/* ----------------------------------------------------
7. Synced Playback Modal & Control Handlers
---------------------------------------------------- */
let isScrubbing = false;
let syncInterval = null;
function formatTime(seconds) {
if (isNaN(seconds)) return "00:00";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
function openVideoComparisonModal(originalSrc, upscaledSrc) {
modalVideoOriginal.src = originalSrc;
modalVideoUpscaled.src = upscaledSrc;
modalVideoOriginal.load();
modalVideoUpscaled.load();
videoModal.style.display = "flex";
document.body.style.overflow = "hidden";
modalPlayBtn.innerHTML = `<i class="fa-solid fa-play"></i>`;
modalTimeSlider.value = 0;
modalTimeCurrent.textContent = "00:00";
modalTimeDuration.textContent = "00:00";
modalVideoOriginal.onloadedmetadata = () => {
modalTimeSlider.max = modalVideoOriginal.duration;
modalTimeDuration.textContent = formatTime(modalVideoOriginal.duration);
};
// Strict periodic sync check
syncInterval = setInterval(() => {
if (!modalVideoOriginal.paused && !modalVideoOriginal.seeking && !modalVideoUpscaled.seeking) {
const diff = Math.abs(modalVideoOriginal.currentTime - modalVideoUpscaled.currentTime);
if (diff > 0.15) {
console.log(`Syncing video channels: diff was ${diff}s`);
modalVideoUpscaled.currentTime = modalVideoOriginal.currentTime;
}
}
if (!isScrubbing && !modalVideoOriginal.paused) {
modalTimeSlider.value = modalVideoOriginal.currentTime;
modalTimeCurrent.textContent = formatTime(modalVideoOriginal.currentTime);
}
}, 100);
}
function closeVideoModal() {
if (syncInterval) clearInterval(syncInterval);
modalVideoOriginal.pause();
modalVideoUpscaled.pause();
modalVideoOriginal.removeAttribute("src");
modalVideoUpscaled.removeAttribute("src");
modalVideoOriginal.load();
modalVideoUpscaled.load();
videoModal.style.display = "none";
document.body.style.overflow = "";
}
modalCloseBtn.addEventListener("click", closeVideoModal);
videoModal.addEventListener("click", (e) => {
if (e.target === videoModal) closeVideoModal();
});
modalPlayBtn.addEventListener("click", () => {
if (modalVideoOriginal.paused) {
modalVideoOriginal.play();
modalVideoUpscaled.play();
modalPlayBtn.innerHTML = `<i class="fa-solid fa-pause"></i>`;
} else {
modalVideoOriginal.pause();
modalVideoUpscaled.pause();
modalPlayBtn.innerHTML = `<i class="fa-solid fa-play"></i>`;
}
});
modalTimeSlider.addEventListener("mousedown", () => { isScrubbing = true; });
modalTimeSlider.addEventListener("mouseup", () => { isScrubbing = false; });
modalTimeSlider.addEventListener("input", (e) => {
const val = parseFloat(e.target.value);
modalVideoOriginal.currentTime = val;
modalVideoUpscaled.currentTime = val;
modalTimeCurrent.textContent = formatTime(val);
});
modalMuteBtn.addEventListener("click", () => {
const isMuted = modalVideoOriginal.muted;
modalVideoOriginal.muted = !isMuted;
modalVideoUpscaled.muted = !isMuted;
modalMuteBtn.innerHTML = isMuted ? `<i class="fa-solid fa-volume-high"></i>` : `<i class="fa-solid fa-volume-xmark"></i>`;
});
/* ----------------------------------------------------
8. Finished/Completed Runs Direct Comparison Launch
---------------------------------------------------- */
if (finishedPlayComparisonBtn) {
finishedPlayComparisonBtn.addEventListener("click", async () => {
if (!currentJobId) return;
try {
const res = await fetch("/api/jobs");
if (!res.ok) return;
const jobs = await res.json();
const job = jobs.find(j => j.job_id === currentJobId);
if (job) {
const originalFilename = job.video_path.split("/").pop();
const originalUrl = `/api/uploads/${originalFilename}`;
const upscaledUrl = `/api/download/${job.output_file.split("/").pop()}`;
openVideoComparisonModal(originalUrl, upscaledUrl);
}
} catch (err) {
console.error("Comparison playback trigger error:", err);
}
});
}
/* ----------------------------------------------------
9. Visual Outputs Gallery & Delete Action
---------------------------------------------------- */
async function loadGallery() {
const galleryGrid = document.getElementById("gallery-grid");
if (!galleryGrid) return;
try {
const [outputsRes, jobsRes] = await Promise.all([
fetch("/api/outputs"),
fetch("/api/jobs")
]);
if (!outputsRes.ok || !jobsRes.ok) return;
const outputs = await outputsRes.json();
const jobs = await jobsRes.json();
if (outputs.length === 0) {
galleryGrid.innerHTML = `
<div class="gallery-empty-state">
<i class="fa-solid fa-photo-film"></i>
<p>No completed jobs found. Start upscaling a video above to see it in the gallery!</p>
</div>
`;
return;
}
galleryGrid.innerHTML = "";
outputs.forEach(file => {
const basename = file.filename.replace("upscaled_", "");
const jobId = basename.split(".")[0];
const matchingJob = jobs.find(j => j.job_id === jobId);
const card = document.createElement("div");
card.className = "gallery-card";
let modelInfo = "AI Upscaled";
let originalFilename = null;
let originalUrl = null;
let upscaledUrl = file.url;
if (matchingJob) {
modelInfo = `${matchingJob.model} (${matchingJob.scale}x)`;
originalFilename = matchingJob.video_path.split("/").pop();
originalUrl = `/api/uploads/${originalFilename}`;
}
card.innerHTML = `
<div class="gallery-thumbnail">
<i class="fa-solid fa-file-video"></i>
<div class="play-overlay" title="Play comparison playback">
<i class="fa-solid fa-circle-play"></i>
</div>
</div>
<div class="gallery-info">
<h4 title="${file.filename}">${file.filename}</h4>
<p>Size: ${formatBytes(file.size)}</p>
<p>Model: ${modelInfo}</p>
</div>
<div class="gallery-actions">
<button type="button" class="btn btn-play-compare">
<i class="fa-solid fa-film"></i> Compare
</button>
<a href="${file.url}" class="btn" download>
<i class="fa-solid fa-download"></i>
</a>
<button type="button" class="btn btn-danger btn-delete" title="Delete completed file">
<i class="fa-solid fa-trash"></i>
</button>
</div>
`;
const playOverlay = card.querySelector(".play-overlay");
const playCompareBtn = card.querySelector(".btn-play-compare");
const playAction = () => {
if (originalUrl) {
openVideoComparisonModal(originalUrl, upscaledUrl);
} else {
alert("Original video source file not found on disk. Performing upscaled output playback comparison.");
openVideoComparisonModal(upscaledUrl, upscaledUrl);
}
};
playOverlay.addEventListener("click", playAction);
playCompareBtn.addEventListener("click", playAction);
const deleteBtn = card.querySelector(".btn-delete");
deleteBtn.addEventListener("click", async () => {
if (confirm(`Are you sure you want to delete ${file.filename}? This will remove all related uploads, outputs, and logs permanently.`)) {
try {
let delSuccess = false;
if (matchingJob) {
const delJobRes = await fetch(`/api/jobs/${matchingJob.job_id}`, {
method: "DELETE"
});
delSuccess = delJobRes.ok;
} else {
const delOutputRes = await fetch(`/api/outputs/delete/${file.filename}`, {
method: "DELETE"
});
delSuccess = delOutputRes.ok;
}
if (delSuccess) {
loadGallery();
} else {
alert("Failed to delete video file and related resources.");
}
} catch (err) {
console.error("Delete error:", err);
}
}
});
galleryGrid.appendChild(card);
});
} catch (err) {
console.error("Error loading gallery:", err);
}
}
async function loadQueue() {
const queueList = document.getElementById("queue-list");
if (!queueList) return;
try {
const res = await fetch("/api/jobs");
if (!res.ok) return;
const jobs = await res.json();
const displayJobs = jobs.filter(job =>
["queued", "analyzing", "extracting", "upscaling", "assembling", "failed", "cancelled"].includes(job.status)
);
if (displayJobs.length === 0) {
queueList.innerHTML = `
<div class="queue-empty-state" style="text-align: center; padding: 2rem; border: 1px dashed var(--border-color); border-radius: 12px; color: var(--text-muted);">
<i class="fa-solid fa-list-ul" style="font-size: 2rem; margin-bottom: 0.5rem; color: var(--primary);"></i>
<p>No active or queued jobs at the moment.</p>
</div>
`;
return;
}
queueList.innerHTML = "";
displayJobs.forEach(job => {
const item = document.createElement("div");
item.className = "queue-item";
const titleLabel = job.is_preview ? `Preview (5s) - ${job.job_id.substring(0, 8)}...` : `Job - ${job.job_id.substring(0, 8)}...`;
const modelLabel = job.model;
const scaleLabel = `${job.scale}x`;
const progress = Math.round(job.progress || 0);
let metaHtml = `
<span><i class="fa-solid fa-microchip"></i> Model: ${modelLabel} (${scaleLabel})</span>
<span><i class="fa-solid fa-file-video"></i> Source: ${job.video_path.split('/').pop()}</span>
`;
if (job.status === "failed" && job.error) {
metaHtml += `<span style="color: var(--danger);"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${job.error}</span>`;
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled") {
metaHtml += `<span><i class="fa-solid fa-clock"></i> ETA: ${job.eta || 'Calculating...'}</span>`;
}
let actionsHtml = "";
if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) {
actionsHtml = `
<button class="btn btn-danger btn-small btn-abort-job" data-id="${job.job_id}">
<i class="fa-solid fa-circle-stop"></i> Abort
</button>
`;
} else {
actionsHtml = `
<button class="btn btn-secondary btn-small btn-delete-job" data-id="${job.job_id}">
<i class="fa-solid fa-trash"></i> Clear
</button>
`;
}
const showProgress = ["analyzing", "extracting", "upscaling", "assembling"].includes(job.status);
const progressStyle = showProgress ? "display: block;" : "display: none;";
item.innerHTML = `
<div class="queue-item-details">
<div class="queue-item-title">
${titleLabel}
<span class="status-badge ${job.status}" style="font-size: 0.7rem; padding: 0.15rem 0.5rem; margin-left: 0.5rem;">${job.status}</span>
</div>
<div class="queue-item-meta">
${metaHtml}
</div>
</div>
<div class="queue-item-status-bar">
<div class="queue-item-progress-container" style="${progressStyle}">
<div class="queue-item-progress-fill" style="width: ${progress}%;"></div>
</div>
<div class="queue-item-actions">
${actionsHtml}
</div>
</div>
`;
const abortBtn = item.querySelector(".btn-abort-job");
if (abortBtn) {
abortBtn.addEventListener("click", async () => {
if (confirm("Are you sure you want to abort this upscaling job?")) {
try {
const abortRes = await fetch(`/api/upscale/cancel/${job.job_id}`, { method: "POST" });
if (abortRes.ok) {
loadQueue();
}
} catch (err) {
console.error("Error aborting job:", err);
}
}
});
}
const deleteBtn = item.querySelector(".btn-delete-job");
if (deleteBtn) {
deleteBtn.addEventListener("click", async () => {
if (confirm("Are you sure you want to clear this job from history?")) {
try {
const delRes = await fetch(`/api/jobs/${job.job_id}`, { method: "DELETE" });
if (delRes.ok) {
loadQueue();
loadGallery();
}
} catch (err) {
console.error("Error clearing job:", err);
}
}
});
}
queueList.appendChild(item);
});
} catch (err) {
console.error("Error loading queue:", err);
}
}
const refreshGalleryBtn = document.getElementById("refresh-gallery-btn");
if (refreshGalleryBtn) {
refreshGalleryBtn.addEventListener("click", loadGallery);
}
const refreshQueueBtn = document.getElementById("refresh-queue-btn");
if (refreshQueueBtn) {
refreshQueueBtn.addEventListener("click", loadQueue);
}
const purgeAllBtn = document.getElementById("purge-all-btn");
if (purgeAllBtn) {
purgeAllBtn.addEventListener("click", async () => {
if (confirm("⚠️ WARNING: This will permanently delete ALL uploaded source videos, completed outputs, temp frames, and history logs. Are you sure you want to proceed?")) {
try {
const res = await fetch("/api/jobs/purge-all", {
method: "POST"
});
if (res.ok) {
alert("System files purged successfully!");
loadGallery();
} else {
alert("Failed to purge system files.");
}
} catch (err) {
console.error("Purge all error:", err);
}
}
});
}
/* ----------------------------------------------------
10. Core Setup & Models Loading
---------------------------------------------------- */
const standardModelNames = {
"realesr-animevideov3": "Real-ESRGAN Anime Video V3 (Fast / Anime & Video)",
"realesrgan-x4plus": "Real-ESRGAN X4 Plus (Ultra Quality / Real World)",
"realesrgan-x4plus-anime": "Real-ESRGAN X4 Plus Anime (Ultra Quality / Illustration)",
"realesrnet-x4plus": "Real-ESRNet X4 Plus (Alternate / Real World)"
};
async function loadModels() {
try {
const res = await fetch("/api/models");
if (!res.ok) return;
const models = await res.json();
if (models && models.length > 0) {
modelSelect.innerHTML = "";
models.forEach(model => {
const opt = document.createElement("option");
opt.value = model;
opt.textContent = standardModelNames[model] || `Custom Model: ${model}`;
if (model === "realesr-animevideov3") {
opt.selected = true;
}
modelSelect.appendChild(opt);
});
modelSelect.dispatchEvent(new Event("change"));
}
} catch (err) {
console.error("Error loading models list:", err);
}
}
async function initializeApp() {
await loadModels();
await checkActiveJobs();
await loadGallery(); // Load enhancement gallery at page load
await loadQueue(); // Load active jobs queue at page load
}
initializeApp();