Implement queued jobs workflow and uploaded source file browser

This commit is contained in:
2026-06-23 16:13:10 -04:00
parent 79d67ea6a5
commit 2ceb948b98
3 changed files with 323 additions and 13 deletions
+192 -8
View File
@@ -344,6 +344,7 @@ function handleVideoUpload(file) {
// 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();
@@ -1025,12 +1026,6 @@ async function checkActiveJobs() {
if (activeJob) {
currentJobId = activeJob.job_id;
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
stepProgress.classList.add("active");
jobIdDisplay.textContent = currentJobId;
connectProgressWebSocket(currentJobId);
}
} catch (err) {
console.error("Error checking active jobs:", err);
@@ -1292,7 +1287,7 @@ async function loadQueue() {
const jobs = await res.json();
const displayJobs = jobs.filter(job =>
["queued", "analyzing", "extracting", "upscaling", "assembling", "failed", "cancelled"].includes(job.status)
["queued", "analyzing", "extracting", "upscaling", "assembling", "completed", "failed", "cancelled"].includes(job.status)
);
if (displayJobs.length === 0) {
@@ -1323,19 +1318,25 @@ async function loadQueue() {
if (job.status === "failed" && job.error) {
metaHtml += `<span style="color: var(--danger);"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${job.error}</span>`;
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled") {
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed") {
metaHtml += `<span><i class="fa-solid fa-clock"></i> ETA: ${job.eta || 'Calculating...'}</span>`;
}
let actionsHtml = "";
if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) {
actionsHtml = `
<button class="btn btn-primary btn-small btn-view-job" data-id="${job.job_id}">
<i class="fa-solid fa-eye"></i> View
</button>
<button class="btn btn-danger btn-small btn-abort-job" data-id="${job.job_id}">
<i class="fa-solid fa-circle-stop"></i> Abort
</button>
`;
} else {
actionsHtml = `
<button class="btn btn-secondary btn-small btn-view-job" data-id="${job.job_id}">
<i class="fa-solid fa-eye"></i> View
</button>
<button class="btn btn-secondary btn-small btn-delete-job" data-id="${job.job_id}">
<i class="fa-solid fa-trash"></i> Clear
</button>
@@ -1398,6 +1399,27 @@ async function loadQueue() {
});
}
const viewBtn = item.querySelector(".btn-view-job");
if (viewBtn) {
viewBtn.addEventListener("click", () => {
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) {
@@ -1473,11 +1495,173 @@ async function loadModels() {
}
}
function useUploadedVideo(entry) {
uploadFileId = entry.file_id;
uploadMetadata = entry.metadata;
// Populate Config Step Info
metaName.textContent = entry.original_filename;
metaSize.textContent = formatBytes(entry.size_bytes);
// Populate Accordions Stream details
const streams = entry.metadata.streams || [];
const videoStream = streams.find(s => s.codec_type === "video") || {};
const audioStream = streams.find(s => s.codec_type === "audio") || {};
// Video stream metadata
document.getElementById("meta-res-codec").textContent = (videoStream.codec_name || "unknown").toUpperCase();
document.getElementById("meta-res").textContent = `${entry.metadata.width} x ${entry.metadata.height}`;
document.getElementById("meta-fps").textContent = `${entry.metadata.fps} FPS`;
document.getElementById("meta-duration").textContent = `${entry.metadata.duration}s`;
document.getElementById("meta-total-frames").textContent = entry.metadata.total_frames || "-";
document.getElementById("meta-aspect-ratio").textContent = videoStream.display_aspect_ratio || "N/A";
// Audio stream metadata
if (audioStream.codec_name) {
document.getElementById("meta-audio-codec").textContent = (audioStream.codec_name || "unknown").toUpperCase();
document.getElementById("meta-audio-channels").textContent = audioStream.channels || "N/A";
document.getElementById("meta-audio-samplerate").textContent = audioStream.sample_rate ? `${audioStream.sample_rate} Hz` : "N/A";
document.getElementById("meta-audio-bitrate").textContent = audioStream.bit_rate ? formatBytes(parseInt(audioStream.bit_rate)) + "/s" : "N/A";
} else {
document.getElementById("meta-audio-codec").textContent = "No Audio Stream";
document.getElementById("meta-audio-channels").textContent = "-";
document.getElementById("meta-audio-samplerate").textContent = "-";
document.getElementById("meta-audio-bitrate").textContent = "-";
}
// Configure timeline slider constraints
timelineSlider.min = 0.1;
timelineSlider.max = entry.metadata.duration - 0.5;
timelineSlider.value = (entry.metadata.duration * 0.15).toFixed(2);
timelineTime.textContent = `${timelineSlider.value}s`;
// Configure Trimming panel range values
const trimStartRange = document.getElementById("trim-start-range");
const trimEndRange = document.getElementById("trim-end-range");
const trimStartValue = document.getElementById("trim-start-value");
const trimEndValue = document.getElementById("trim-end-value");
trimStartRange.min = 0;
trimStartRange.max = entry.metadata.duration;
trimStartRange.value = 0;
trimStartValue.textContent = "0.00s";
trimEndRange.min = 0;
trimEndRange.max = entry.metadata.duration;
trimEndRange.value = entry.metadata.duration;
trimEndValue.textContent = `${entry.metadata.duration.toFixed(2)}s`;
// Show config view
stepUpload.classList.remove("active");
stepConfig.classList.add("active");
}
async function loadUploadsBrowser() {
const browserList = document.getElementById("uploads-browser-list");
if (!browserList) return;
try {
const res = await fetch("/api/uploads");
if (!res.ok) return;
const uploads = await res.json();
if (uploads.length === 0) {
browserList.innerHTML = `
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
<i class="fa-solid fa-photo-film" style="font-size: 2rem; margin-bottom: 0.5rem; color: rgba(255,255,255,0.15);"></i>
<p>No uploaded files found. Upload a video above to get started!</p>
</div>
`;
return;
}
browserList.innerHTML = "";
uploads.forEach(entry => {
const item = document.createElement("div");
item.className = "queue-item";
const sizeStr = formatBytes(entry.size_bytes || 0);
const durationStr = entry.metadata ? `${entry.metadata.duration}s` : "-";
const resStr = entry.metadata ? `${entry.metadata.width}x${entry.metadata.height}` : "-";
const fpsStr = entry.metadata ? `${entry.metadata.fps} FPS` : "-";
item.innerHTML = `
<div class="queue-item-details">
<div class="queue-item-title">
<i class="fa-solid fa-file-video" style="color: var(--primary);"></i> ${entry.original_filename}
</div>
<div class="queue-item-meta">
<span>Size: ${sizeStr}</span>
<span>Resolution: ${resStr}</span>
<span>Duration: ${durationStr}</span>
<span>FPS: ${fpsStr}</span>
</div>
</div>
<div class="queue-item-status-bar">
<div class="queue-item-actions">
<button class="btn btn-primary btn-small btn-use-upload" data-id="${entry.file_id}">
<i class="fa-solid fa-play"></i> Use
</button>
<button class="btn btn-danger btn-small btn-delete-upload" data-id="${entry.file_id}">
<i class="fa-solid fa-trash"></i> Delete
</button>
</div>
</div>
`;
item.querySelector(".btn-use-upload").addEventListener("click", () => {
useUploadedVideo(entry);
});
item.querySelector(".btn-delete-upload").addEventListener("click", async () => {
if (confirm(`Are you sure you want to delete "${entry.original_filename}" from the workspace?`)) {
try {
const delRes = await fetch(`/api/uploads/${entry.file_id}`, { method: "DELETE" });
if (delRes.ok) {
loadUploadsBrowser();
}
} catch (err) {
console.error("Error deleting upload:", err);
}
}
});
browserList.appendChild(item);
});
} catch (err) {
console.error("Error loading uploads browser:", err);
}
}
async function initializeApp() {
await loadModels();
await checkActiveJobs();
await loadGallery(); // Load enhancement gallery at page load
await loadQueue(); // Load active jobs queue at page load
await loadUploadsBrowser(); // Load previously uploaded videos browser
// Register btn-refresh-uploads click handler
const refreshUploadsBtn = document.getElementById("refresh-uploads-btn");
if (refreshUploadsBtn) {
refreshUploadsBtn.addEventListener("click", loadUploadsBrowser);
}
// Register back-to-workspace-btn click handler
const backToWorkspaceBtn = document.getElementById("back-to-workspace-btn");
if (backToWorkspaceBtn) {
backToWorkspaceBtn.addEventListener("click", () => {
if (socket) {
try {
socket.close();
} catch(e) {}
}
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
document.getElementById("step-upload").classList.add("active");
uploadFileId = null;
loadUploadsBrowser();
});
}
}
initializeApp();