Implement batch processing, pause/resume, custom temp directory, and detailed pipeline logs
This commit is contained in:
+310
-62
@@ -241,13 +241,99 @@ if (uploadModelBtn) {
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
if (files.length) handleVideoUpload(files[0]);
|
||||
if (files.length > 1) {
|
||||
handleBatchUpload(files);
|
||||
} else if (files.length === 1) {
|
||||
handleVideoUpload(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length) handleVideoUpload(e.target.files[0]);
|
||||
const files = e.target.files;
|
||||
if (files.length > 1) {
|
||||
handleBatchUpload(files);
|
||||
} else if (files.length === 1) {
|
||||
handleVideoUpload(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
function resetDropZone() {
|
||||
dropZone.innerHTML = `
|
||||
<span class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
||||
<h3>Drag & Drop Video File(s)</h3>
|
||||
<p>Supports MP4, MKV, AVI, MOV, WEBM (Max 500MB recommended for local upscaling)</p>
|
||||
<button type="button" class="btn" onclick="document.getElementById('file-input').click()">
|
||||
<i class="fa-solid fa-folder-open"></i> Browse Files
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
async function handleBatchUpload(files) {
|
||||
const totalFiles = files.length;
|
||||
let uploadedCount = 0;
|
||||
|
||||
dropZone.innerHTML = `
|
||||
<div class="spinner" style="margin: 0 auto 1.5rem auto;"></div>
|
||||
<h3>Uploading <span id="batch-upload-current">1</span> of ${totalFiles} Files...</h3>
|
||||
<p id="upload-progress-text">Preparing batch 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 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;
|
||||
@@ -718,53 +804,105 @@ startUpscaleBtn.addEventListener("click", async () => {
|
||||
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 {
|
||||
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."));
|
||||
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 = `<i class="fa-solid fa-play"></i> 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);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1398,7 +1536,7 @@ async function loadQueue() {
|
||||
`;
|
||||
}
|
||||
|
||||
if (job.status === "interrupted" || job.status === "failed") {
|
||||
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 = `
|
||||
@@ -1418,6 +1556,9 @@ async function loadQueue() {
|
||||
<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-secondary btn-small btn-pause-job" data-id="${job.job_id}">
|
||||
<i class="fa-solid fa-pause"></i> Pause
|
||||
</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>
|
||||
@@ -1433,7 +1574,7 @@ async function loadQueue() {
|
||||
`;
|
||||
}
|
||||
|
||||
const showProgress = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status);
|
||||
const showProgress = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling", "paused", "interrupted"].includes(job.status);
|
||||
const progressStyle = showProgress ? "display: block;" : "display: none;";
|
||||
|
||||
item.innerHTML = `
|
||||
@@ -1508,6 +1649,24 @@ async function loadQueue() {
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -1624,6 +1783,19 @@ async function loadModels() {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1684,10 +1856,55 @@ function useUploadedVideo(entry) {
|
||||
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;
|
||||
@@ -1700,10 +1917,12 @@ async function loadUploadsBrowser() {
|
||||
<p>No uploaded files found. Upload a video above to get started!</p>
|
||||
</div>
|
||||
`;
|
||||
if (batchConfigBar) batchConfigBar.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
browserList.innerHTML = "";
|
||||
let selectedEntries = [];
|
||||
|
||||
uploads.forEach(entry => {
|
||||
const item = document.createElement("div");
|
||||
@@ -1715,29 +1934,48 @@ async function loadUploadsBrowser() {
|
||||
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 style="display: flex; align-items: center; gap: 1rem; width: 100%;">
|
||||
<input type="checkbox" class="batch-select-checkbox" data-id="${entry.file_id}" style="width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary);">
|
||||
<div class="queue-item-details" style="flex: 1;">
|
||||
<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-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 class="queue-item-status-bar" style="border: none; padding: 0;">
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -1757,6 +1995,16 @@ async function loadUploadsBrowser() {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user