// 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) 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 = `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 transcodeFormat = transcodeSelect.value; try { const res = await fetch("/api/upscale/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_id: uploadFileId, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), preserve_audio: audioToggle.checked, start_sec: parseFloat(timelineSlider.value), end_sec: parseFloat(timelineSlider.value) + 5.0, gpu_ids: gpuSelect.value === 'auto' ? null : gpuSelect.value, tta: false, unsharp: false, double_fps: false, preserve_subtitles: false, crf: parseInt(crfRange.value), preset: presetSelect.value, denoise: filterDenoise.checked, sharpen: filterSharpen.checked, interpolation: filterFps.checked, webhook_url: null, transcode_format: transcodeFormat, is_preview: true }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Failed to start video preview.")); } const data = await res.json(); currentJobId = data.job_id; connectProgressWebSocket(currentJobId); } catch (err) { alert(err.message); previewLoader.style.display = "none"; previewPlaceholderBox.style.display = "flex"; } }); function initComparisonSlider() { const upscaledWrapper = document.getElementById("img-upscaled-wrapper"); const videoUpscaledWrapper = document.getElementById("video-preview-upscaled-wrapper"); // Reset split positions to 50% if (upscaledWrapper) upscaledWrapper.style.clipPath = `inset(0 0 0 50%)`; if (videoUpscaledWrapper) videoUpscaledWrapper.style.clipPath = `inset(0 0 0 50%)`; sliderSplit.style.left = `50%`; if (sliderInitialized) return; sliderInitialized = true; let active = false; function setSliderPosition(x) { const rect = comparisonSliderContainer.getBoundingClientRect(); let posX = x - rect.left; if (posX < 0) posX = 0; if (posX > rect.width) posX = rect.width; const pct = (posX / rect.width) * 100; if (upscaledWrapper) upscaledWrapper.style.clipPath = `inset(0 0 0 ${pct}%)`; if (videoUpscaledWrapper) videoUpscaledWrapper.style.clipPath = `inset(0 0 0 ${pct}%)`; sliderSplit.style.left = `${pct}%`; } sliderSplit.addEventListener('mousedown', (e) => { e.stopPropagation(); active = true; }); window.addEventListener('mouseup', () => { active = false; }); window.addEventListener('mousemove', (e) => { if (!active) return; setSliderPosition(e.pageX); }); sliderSplit.addEventListener('touchstart', (e) => { e.stopPropagation(); active = true; }); window.addEventListener('touchend', () => { active = false; }); window.addEventListener('touchmove', (e) => { if (!active) return; setSliderPosition(e.touches[0].pageX); }); comparisonSliderContainer.addEventListener('dblclick', () => { upscaledWrapper.style.clipPath = `inset(0 0 0 50%)`; sliderSplit.style.left = `50%`; }); } /* ---------------------------------------------------- 6. Video Pipeline Jobs Orchestration ---------------------------------------------------- */ startUpscaleBtn.addEventListener("click", async () => { if (!uploadFileId) return; startUpscaleBtn.disabled = true; startUpscaleBtn.innerHTML = ` Starting Job...`; const gpuSelect = document.getElementById("gpu-select"); const crfRange = document.getElementById("crf-range"); const presetSelect = document.getElementById("preset-select"); const transcodeSelect = document.getElementById("transcode-select"); const filterDenoise = document.getElementById("filter-denoise"); const filterSharpen = document.getElementById("filter-sharpen"); const filterFps = document.getElementById("filter-fps"); const webhookUrl = document.getElementById("webhook-url"); try { const res = await fetch("/api/upscale/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_id: uploadFileId, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), preserve_audio: audioToggle.checked, // Advanced fields start_sec: parseFloat(trimStartRange.value), end_sec: parseFloat(trimEndRange.value), gpu_ids: gpuSelect.value === "auto" ? null : gpuSelect.value, crf: parseInt(crfRange.value), preset: presetSelect.value, transcode_format: transcodeSelect.value, denoise: filterDenoise.checked, sharpen: filterSharpen.checked, interpolation: filterFps.checked, webhook_url: webhookUrl.value.trim() || null }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Orchestrator failed to launch job.")); } const data = await res.json(); currentJobId = data.job_id; stepConfig.classList.remove("active"); stepProgress.classList.add("active"); jobIdDisplay.textContent = currentJobId; connectProgressWebSocket(currentJobId); } catch (err) { alert(err.message); startUpscaleBtn.disabled = false; startUpscaleBtn.innerHTML = ` 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 = `No completed jobs found. Start upscaling a video above to see it in the gallery!
Size: ${formatBytes(file.size)}
Model: ${modelInfo}
No active or queued jobs at the moment.
No uploaded files found. Upload a video above to get started!