1811 lines
74 KiB
JavaScript
1811 lines
74 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");
|
|
loadUploadsBrowser();
|
|
} 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 aiFaceRestoration = document.getElementById("ai-face-restoration");
|
|
const aiRifeInterpolation = document.getElementById("ai-rife-interpolation");
|
|
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
|
|
|
|
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,
|
|
ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false,
|
|
ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false,
|
|
ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.checked : false
|
|
})
|
|
});
|
|
|
|
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");
|
|
|
|
const aiFaceRestoration = document.getElementById("ai-face-restoration");
|
|
const aiRifeInterpolation = document.getElementById("ai-rife-interpolation");
|
|
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
|
|
|
|
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,
|
|
ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false,
|
|
ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false,
|
|
ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.checked : false
|
|
})
|
|
});
|
|
|
|
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 === "restoring_faces") {
|
|
statusText = "GFPGAN: Running Face Restoration on upscaled frames...";
|
|
} else if (data.status === "interpolating") {
|
|
statusText = "RIFE: Interpolating frames for smooth motion...";
|
|
} 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 === "restoring_faces") {
|
|
appendLogLine("GFPGAN: Running Face Restoration on upscaled frames...", "info");
|
|
} else if (data.status === "interpolating") {
|
|
appendLogLine("RIFE: Interpolating frames for smooth motion...", "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", "restoring_faces", "interpolating", "assembling"].includes(job.status)
|
|
);
|
|
|
|
if (activeJob) {
|
|
currentJobId = activeJob.job_id;
|
|
}
|
|
} 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 moveQueueItem(jobId, direction) {
|
|
try {
|
|
const res = await fetch("/api/queue");
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const queue = data.queue;
|
|
const index = queue.indexOf(jobId);
|
|
if (index === -1) return;
|
|
|
|
const newIndex = index + direction;
|
|
if (newIndex < 0 || newIndex >= queue.length) return;
|
|
|
|
// Swap
|
|
const temp = queue[index];
|
|
queue[index] = queue[newIndex];
|
|
queue[newIndex] = temp;
|
|
|
|
const reorderRes = await fetch("/api/queue/reorder", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ job_ids: queue })
|
|
});
|
|
|
|
if (reorderRes.ok) {
|
|
loadQueue();
|
|
}
|
|
} catch (err) {
|
|
console.error("Error reordering queue:", 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", "restoring_faces", "interpolating", "assembling", "completed", "failed", "cancelled", "interrupted"].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" && job.status !== "completed" && job.status !== "interrupted") {
|
|
metaHtml += `<span><i class="fa-solid fa-clock"></i> ETA: ${job.eta || 'Calculating...'}</span>`;
|
|
}
|
|
|
|
let actionsHtml = "";
|
|
let reorderHtml = "";
|
|
|
|
if (job.status === "queued" && job.queue_position !== null && job.queue_position !== undefined) {
|
|
const queuedJobs = displayJobs.filter(j => j.status === "queued");
|
|
const isFirst = job.queue_position === 0;
|
|
const isLast = job.queue_position === queuedJobs.length - 1;
|
|
|
|
reorderHtml = `
|
|
<button class="btn btn-secondary btn-small btn-move-up" data-id="${job.job_id}" style="padding: 0.25rem 0.5rem; width: 30px; height: 30px; border-radius: 8px; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; ${isFirst ? 'visibility: hidden;' : ''}" title="Move Up">
|
|
<i class="fa-solid fa-arrow-up"></i>
|
|
</button>
|
|
<button class="btn btn-secondary btn-small btn-move-down" data-id="${job.job_id}" style="padding: 0.25rem 0.5rem; width: 30px; height: 30px; border-radius: 8px; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; ${isLast ? 'visibility: hidden;' : ''}" title="Move Down">
|
|
<i class="fa-solid fa-arrow-down"></i>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
if (job.status === "interrupted") {
|
|
actionsHtml = `
|
|
<button class="btn btn-primary btn-small btn-resume-job" data-id="${job.job_id}">
|
|
<i class="fa-solid fa-play"></i> Resume
|
|
</button>
|
|
<button class="btn btn-secondary btn-small btn-delete-job" data-id="${job.job_id}">
|
|
<i class="fa-solid fa-trash"></i> Clear
|
|
</button>
|
|
`;
|
|
} else if (["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status)) {
|
|
actionsHtml = `
|
|
${reorderHtml}
|
|
<button class="btn btn-primary btn-small btn-view-job" data-id="${job.job_id}">
|
|
<i class="fa-solid fa-eye"></i> View
|
|
</button>
|
|
<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-view-job" data-id="${job.job_id}">
|
|
<i class="fa-solid fa-eye"></i> View
|
|
</button>
|
|
<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", "restoring_faces", "interpolating", "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 upBtn = item.querySelector(".btn-move-up");
|
|
if (upBtn) {
|
|
upBtn.addEventListener("click", async (e) => {
|
|
e.stopPropagation();
|
|
await moveQueueItem(job.job_id, -1);
|
|
});
|
|
}
|
|
|
|
const downBtn = item.querySelector(".btn-move-down");
|
|
if (downBtn) {
|
|
downBtn.addEventListener("click", async (e) => {
|
|
e.stopPropagation();
|
|
await moveQueueItem(job.job_id, 1);
|
|
});
|
|
}
|
|
|
|
const resumeBtn = item.querySelector(".btn-resume-job");
|
|
if (resumeBtn) {
|
|
resumeBtn.addEventListener("click", async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
const resumeRes = await fetch(`/api/upscale/resume/${job.job_id}`, { method: "POST" });
|
|
if (resumeRes.ok) {
|
|
loadQueue();
|
|
} else {
|
|
const err = await resumeRes.json();
|
|
alert("Failed to resume job: " + formatFetchError(err, "unknown error"));
|
|
}
|
|
} catch (err) {
|
|
console.error("Error resuming job:", err);
|
|
}
|
|
});
|
|
}
|
|
|
|
const abortBtn = item.querySelector(".btn-abort-job");
|
|
if (abortBtn) {
|
|
abortBtn.addEventListener("click", async (e) => {
|
|
e.stopPropagation();
|
|
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 (e) => {
|
|
e.stopPropagation();
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const viewBtn = item.querySelector(".btn-view-job");
|
|
if (viewBtn) {
|
|
viewBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
currentJobId = job.job_id;
|
|
if (socket) {
|
|
try {
|
|
socket.close();
|
|
} catch (e) {}
|
|
}
|
|
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
|
if (job.status === "completed") {
|
|
document.getElementById("step-finished").classList.add("active");
|
|
document.getElementById("download-link").href = `/api/download/${job.output_file}`;
|
|
} else {
|
|
document.getElementById("step-progress").classList.add("active");
|
|
document.getElementById("job-id-display").textContent = currentJobId;
|
|
connectProgressWebSocket(currentJobId);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
function useUploadedVideo(entry) {
|
|
uploadFileId = entry.file_id;
|
|
uploadMetadata = entry.metadata;
|
|
|
|
// Populate Config Step Info
|
|
metaName.textContent = entry.original_filename;
|
|
metaSize.textContent = formatBytes(entry.size_bytes);
|
|
|
|
// Populate Accordions Stream details
|
|
const streams = entry.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 = `${entry.metadata.width} x ${entry.metadata.height}`;
|
|
document.getElementById("meta-fps").textContent = `${entry.metadata.fps} FPS`;
|
|
document.getElementById("meta-duration").textContent = `${entry.metadata.duration}s`;
|
|
document.getElementById("meta-total-frames").textContent = entry.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 = entry.metadata.duration - 0.5;
|
|
timelineSlider.value = (entry.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 = entry.metadata.duration;
|
|
trimStartRange.value = 0;
|
|
trimStartValue.textContent = "0.00s";
|
|
|
|
trimEndRange.min = 0;
|
|
trimEndRange.max = entry.metadata.duration;
|
|
trimEndRange.value = entry.metadata.duration;
|
|
trimEndValue.textContent = `${entry.metadata.duration.toFixed(2)}s`;
|
|
|
|
// Show config view
|
|
stepUpload.classList.remove("active");
|
|
stepConfig.classList.add("active");
|
|
}
|
|
|
|
async function loadUploadsBrowser() {
|
|
const browserList = document.getElementById("uploads-browser-list");
|
|
if (!browserList) return;
|
|
|
|
try {
|
|
const res = await fetch("/api/uploads");
|
|
if (!res.ok) return;
|
|
|
|
const uploads = await res.json();
|
|
if (uploads.length === 0) {
|
|
browserList.innerHTML = `
|
|
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
|
<i class="fa-solid fa-photo-film" style="font-size: 2rem; margin-bottom: 0.5rem; color: rgba(255,255,255,0.15);"></i>
|
|
<p>No uploaded files found. Upload a video above to get started!</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
browserList.innerHTML = "";
|
|
|
|
uploads.forEach(entry => {
|
|
const item = document.createElement("div");
|
|
item.className = "queue-item";
|
|
|
|
const sizeStr = formatBytes(entry.size_bytes || 0);
|
|
const durationStr = entry.metadata ? `${entry.metadata.duration}s` : "-";
|
|
const resStr = entry.metadata ? `${entry.metadata.width}x${entry.metadata.height}` : "-";
|
|
const fpsStr = entry.metadata ? `${entry.metadata.fps} FPS` : "-";
|
|
|
|
item.innerHTML = `
|
|
<div class="queue-item-details">
|
|
<div class="queue-item-title">
|
|
<i class="fa-solid fa-file-video" style="color: var(--primary);"></i> ${entry.original_filename}
|
|
</div>
|
|
<div class="queue-item-meta">
|
|
<span>Size: ${sizeStr}</span>
|
|
<span>Resolution: ${resStr}</span>
|
|
<span>Duration: ${durationStr}</span>
|
|
<span>FPS: ${fpsStr}</span>
|
|
</div>
|
|
</div>
|
|
<div class="queue-item-status-bar">
|
|
<div class="queue-item-actions">
|
|
<button class="btn btn-primary btn-small btn-use-upload" data-id="${entry.file_id}">
|
|
<i class="fa-solid fa-play"></i> Use
|
|
</button>
|
|
<button class="btn btn-danger btn-small btn-delete-upload" data-id="${entry.file_id}">
|
|
<i class="fa-solid fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
item.querySelector(".btn-use-upload").addEventListener("click", () => {
|
|
useUploadedVideo(entry);
|
|
});
|
|
|
|
item.querySelector(".btn-delete-upload").addEventListener("click", async () => {
|
|
if (confirm(`Are you sure you want to delete "${entry.original_filename}" from the workspace?`)) {
|
|
try {
|
|
const delRes = await fetch(`/api/uploads/${entry.file_id}`, { method: "DELETE" });
|
|
if (delRes.ok) {
|
|
loadUploadsBrowser();
|
|
}
|
|
} catch (err) {
|
|
console.error("Error deleting upload:", err);
|
|
}
|
|
}
|
|
});
|
|
|
|
browserList.appendChild(item);
|
|
});
|
|
} catch (err) {
|
|
console.error("Error loading uploads browser:", 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
|
|
await loadUploadsBrowser(); // Load previously uploaded videos browser
|
|
|
|
// Register btn-refresh-uploads click handler
|
|
const refreshUploadsBtn = document.getElementById("refresh-uploads-btn");
|
|
if (refreshUploadsBtn) {
|
|
refreshUploadsBtn.addEventListener("click", loadUploadsBrowser);
|
|
}
|
|
|
|
// Register back-to-workspace-btn click handler
|
|
const backToWorkspaceBtn = document.getElementById("back-to-workspace-btn");
|
|
if (backToWorkspaceBtn) {
|
|
backToWorkspaceBtn.addEventListener("click", () => {
|
|
if (socket) {
|
|
try {
|
|
socket.close();
|
|
} catch(e) {}
|
|
}
|
|
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
|
document.getElementById("step-upload").classList.add("active");
|
|
uploadFileId = null;
|
|
loadUploadsBrowser();
|
|
});
|
|
}
|
|
|
|
// Register documentation modal handlers
|
|
const docsBtn = document.getElementById("docs-btn");
|
|
const docsModal = document.getElementById("docs-modal");
|
|
const docsCloseBtn = document.getElementById("docs-modal-close-btn");
|
|
|
|
if (docsBtn && docsModal) {
|
|
docsBtn.addEventListener("click", () => {
|
|
docsModal.style.display = "flex";
|
|
document.body.style.overflow = "hidden";
|
|
});
|
|
}
|
|
|
|
if (docsCloseBtn && docsModal) {
|
|
docsCloseBtn.addEventListener("click", () => {
|
|
docsModal.style.display = "none";
|
|
document.body.style.overflow = "";
|
|
});
|
|
|
|
docsModal.addEventListener("click", (e) => {
|
|
if (e.target === docsModal) {
|
|
docsModal.style.display = "none";
|
|
document.body.style.overflow = "";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
initializeApp();
|