// 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 = `
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 = ` 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 > 1) { handleBatchUpload(files); } else if (files.length === 1) { handleVideoUpload(files[0]); } }); fileInput.addEventListener('change', (e) => { const files = e.target.files; if (files.length > 1) { handleBatchUpload(files); } else if (files.length === 1) { handleVideoUpload(files[0]); } }); function resetDropZone() { dropZone.innerHTML = `

Drag & Drop Video File(s)

Supports MP4, MKV, AVI, MOV, WEBM (Max 500MB recommended for local upscaling)

`; } async function handleBatchUpload(files) { const totalFiles = files.length; let uploadedCount = 0; dropZone.innerHTML = `

Uploading 1 of ${totalFiles} Files...

Preparing batch upload...

`; const progressText = document.getElementById("upload-progress-text"); const progressBar = document.getElementById("upload-bar"); const currentText = document.getElementById("batch-upload-current"); for (let i = 0; i < totalFiles; i++) { const file = files[i]; if (currentText) currentText.textContent = i + 1; if (progressText) progressText.textContent = `Uploading "${file.name}"...`; if (progressBar) progressBar.style.width = "0%"; try { await new Promise((resolve, reject) => { 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; if (progressText) { progressText.textContent = `Uploading "${file.name}": ${formatBytes(e.loaded)} / ${formatBytes(e.total)} (${Math.round(percentComplete)}%)`; } if (progressBar) progressBar.style.width = `${percentComplete}%`; } }); xhr.onload = function() { if (xhr.status === 200) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error(`Upload failed for ${file.name}`)); } }; xhr.onerror = function() { reject(new Error(`Network error uploading ${file.name}`)); }; xhr.send(formData); }); uploadedCount++; } catch (err) { console.error(err); alert(`Error uploading "${file.name}": ${err.message}`); } } await loadUploadsBrowser(); resetDropZone(); } 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 = `

Uploading Video File...

Preparing upload...

`; 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 = `
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 tempDirInput = document.getElementById("temp-dir-input"); const aiFaceRestoration = document.getElementById("ai-face-restoration"); const aiRifeInterpolation = document.getElementById("ai-rife-interpolation"); const aiAudioDenoise = document.getElementById("ai-audio-denoise"); try { if (window.isBatchMode && window.batchFileIds && window.batchFileIds.length > 0) { const res = await fetch("/api/upscale/start/batch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_ids: window.batchFileIds, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), preserve_audio: audioToggle.checked, // Advanced fields (disabled for batch) start_sec: null, end_sec: null, 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, temp_dir: (tempDirInput && tempDirInput.value.trim()) || null }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Orchestrator failed to launch batch jobs.")); } window.isBatchMode = false; window.batchFileIds = []; document.querySelectorAll(".batch-select-checkbox").forEach(cb => cb.checked = false); const batchConfigBar = document.getElementById("batch-config-bar"); if (batchConfigBar) batchConfigBar.style.display = "none"; document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active")); stepUpload.classList.add("active"); alert("Batch jobs successfully queued!"); loadQueue(); startUpscaleBtn.disabled = false; startUpscaleBtn.innerHTML = ` Start Full Upscale`; } else { 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, temp_dir: (tempDirInput && tempDirInput.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 = ` 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 = `
Connected to backend job orchestrator...
`; 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 = ``; } else { vidOriginal.pause(); vidUpscaled.pause(); playBtn.innerHTML = ``; } }; // Time Sync logic vidOriginal.onplay = () => { vidUpscaled.play(); playBtn.innerHTML = ``; }; vidOriginal.onpause = () => { vidUpscaled.pause(); playBtn.innerHTML = ``; }; 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 = ``; 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 = ``; } else { modalVideoOriginal.pause(); modalVideoUpscaled.pause(); modalPlayBtn.innerHTML = ``; } }); 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 ? `` : ``; }); /* ---------------------------------------------------- 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 = ` `; 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 = ` `; 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 = `

No active or queued jobs at the moment.

`; 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 = ` Model: ${modelLabel} (${scaleLabel}) Source: ${job.video_path.split('/').pop()} `; let errorHtml = ""; if (job.status === "failed" && job.error) { errorHtml = `
Failure Cause: ${job.error}
`; } else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed" && job.status !== "interrupted") { metaHtml += ` ETA: ${job.eta || 'Calculating...'}`; } 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 = ` `; } if (job.status === "interrupted" || job.status === "failed" || job.status === "paused") { const btnLabel = job.status === "failed" ? "Retry" : "Resume"; const btnIcon = job.status === "failed" ? "fa-rotate-right" : "fa-play"; actionsHtml = ` `; } else if (["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status)) { actionsHtml = ` ${reorderHtml} `; } else { actionsHtml = ` `; } const showProgress = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling", "paused", "interrupted"].includes(job.status); const progressStyle = showProgress ? "display: block;" : "display: none;"; item.innerHTML = `
${titleLabel} ${job.status}
${metaHtml}
${errorHtml}
${actionsHtml}
`; 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 pauseBtn = item.querySelector(".btn-pause-job"); if (pauseBtn) { pauseBtn.addEventListener("click", async (e) => { e.stopPropagation(); try { const pauseRes = await fetch(`/api/upscale/pause/${job.job_id}`, { method: "POST" }); if (pauseRes.ok) { loadQueue(); } else { const err = await pauseRes.json(); alert("Failed to pause job: " + formatFetchError(err, "unknown error")); } } catch (err) { console.error("Error pausing 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) { window.isBatchMode = false; window.batchFileIds = []; // Enable inputs timelineSlider.disabled = false; generatePreviewBtn.disabled = false; generateVideoPreviewBtn.disabled = false; document.getElementById("trim-start-range").disabled = false; document.getElementById("trim-end-range").disabled = false; previewPlaceholderBox.style.display = "flex"; previewPlaceholderBox.querySelector("p").textContent = "Move timeline slider and generate frame preview to calibrate parameters."; comparisonSliderContainer.style.display = "none"; 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"); } function useBatchUploadedVideos(entries) { window.isBatchMode = true; window.batchFileIds = entries.map(e => e.file_id); uploadFileId = entries[0].file_id; // Set fallback file ID for safety // Set configuration name metaName.textContent = `Batch Processing - ${entries.length} video(s)`; metaSize.textContent = `Total size: ${formatBytes(entries.reduce((acc, e) => acc + (e.size_bytes || 0), 0))}`; // Set video/audio stream specs to default/mixed document.getElementById("meta-res-codec").textContent = "MIXED"; document.getElementById("meta-res").textContent = "Varies"; document.getElementById("meta-fps").textContent = "Varies"; document.getElementById("meta-duration").textContent = "Varies"; document.getElementById("meta-total-frames").textContent = "Varies"; document.getElementById("meta-aspect-ratio").textContent = "Varies"; document.getElementById("meta-audio-codec").textContent = "Varies"; document.getElementById("meta-audio-channels").textContent = "Varies"; document.getElementById("meta-audio-samplerate").textContent = "Varies"; document.getElementById("meta-audio-bitrate").textContent = "Varies"; // Disable timeline and previews timelineSlider.disabled = true; generatePreviewBtn.disabled = true; generateVideoPreviewBtn.disabled = true; // Hide trimming panel or disable inputs document.getElementById("trim-start-range").disabled = true; document.getElementById("trim-end-range").disabled = true; // Hide preview placeholders previewPlaceholderBox.style.display = "flex"; previewPlaceholderBox.querySelector("p").textContent = "Preview and trimming are disabled in Batch Mode."; comparisonSliderContainer.style.display = "none"; // Show config view stepUpload.classList.remove("active"); stepConfig.classList.add("active"); } async function loadUploadsBrowser() { const browserList = document.getElementById("uploads-browser-list"); if (!browserList) return; const batchConfigBar = document.getElementById("batch-config-bar"); const batchSelectedCount = document.getElementById("batch-selected-count"); const batchConfigureBtn = document.getElementById("batch-configure-btn"); try { const res = await fetch("/api/uploads"); if (!res.ok) return; const uploads = await res.json(); if (uploads.length === 0) { browserList.innerHTML = `

No uploaded files found. Upload a video above to get started!

`; if (batchConfigBar) batchConfigBar.style.display = "none"; return; } browserList.innerHTML = ""; let selectedEntries = []; 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 = `
${entry.original_filename}
Size: ${sizeStr} Resolution: ${resStr} Duration: ${durationStr} FPS: ${fpsStr}
`; const checkbox = item.querySelector(".batch-select-checkbox"); checkbox.addEventListener("change", () => { if (checkbox.checked) { selectedEntries.push(entry); } else { selectedEntries = selectedEntries.filter(se => se.file_id !== entry.file_id); } if (selectedEntries.length > 0) { if (batchSelectedCount) batchSelectedCount.textContent = selectedEntries.length; if (batchConfigBar) batchConfigBar.style.display = "flex"; } else { if (batchConfigBar) batchConfigBar.style.display = "none"; } }); 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); }); if (batchConfigureBtn) { const newBtn = batchConfigureBtn.cloneNode(true); batchConfigureBtn.parentNode.replaceChild(newBtn, batchConfigureBtn); newBtn.addEventListener("click", () => { if (selectedEntries.length > 0) { useBatchUploadedVideos(selectedEntries); } }); } } 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 // Load GPU Diagnostics try { const res = await fetch("/api/diagnostics"); if (res.ok) { const diag = await res.json(); const gpuInfo = document.getElementById("gpu-info"); if (gpuInfo) { if (diag.gpu && diag.gpu.available && diag.gpu.gpus.length > 0) { const gpuName = diag.gpu.gpus[0].name; gpuInfo.textContent = `GPU Active: ${gpuName} (Vulkan)`; } else { gpuInfo.textContent = "GPU Active: Vulkan Device"; } } } } catch (err) { console.error("Error loading GPU diagnostics:", err); } // 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();