diff --git a/app/main.py b/app/main.py
index 830f246..85abcd1 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1590,8 +1590,102 @@ def cleanup_temp(req: CleanupTempRequest):
except Exception as e:
errors.append(f"Error removing {path}: {str(e)}")
+class ReimportTempRequest(BaseModel):
+ video_path: str
+ model: str = "realesrgan-x4plus"
+ scale: int = 4
+ tile_size: int = 256
+ preserve_audio: bool = True
+ ai_face_restoration: bool = False
+ ai_rife_interpolation: bool = False
+ ai_audio_denoise: bool = False
+ ss: str | None = None
+ t: str | None = None
+ gpu_ids: str | None = None
+ tta: bool = False
+ unsharp: bool = False
+ double_fps: bool = False
+ preserve_subtitles: bool = True
+ start_sec: float | None = None
+ end_sec: float | None = None
+ crf: int = 18
+ preset: str = "medium"
+ denoise: bool = False
+ sharpen: bool = False
+ interpolation: bool = False
+ transcode_format: str = "mp4"
+ temp_dir: str | None = None
+
+@app.post("/api/system/temp-reimport/{job_id}")
+def reimport_temp(job_id: str, req: ReimportTempRequest):
+ if job_id in jobs_db:
+ raise HTTPException(status_code=400, detail="Job already exists in database.")
+
+ # Verify the video path is valid
+ if not os.path.exists(req.video_path):
+ raise HTTPException(status_code=400, detail=f"Video file not found at: {req.video_path}")
+
+ # Reconstruct the job dictionary
+ job_dict = {
+ "job_id": job_id,
+ "video_path": req.video_path,
+ "model": req.model,
+ "scale": req.scale,
+ "tile_size": req.tile_size,
+ "preserve_audio": req.preserve_audio,
+ "ai_face_restoration": req.ai_face_restoration,
+ "ai_rife_interpolation": req.ai_rife_interpolation,
+ "ai_audio_denoise": req.ai_audio_denoise,
+ "ss": req.ss,
+ "t": req.t,
+ "gpu_ids": req.gpu_ids,
+ "tta": req.tta,
+ "unsharp": req.unsharp,
+ "double_fps": req.double_fps,
+ "preserve_subtitles": req.preserve_subtitles,
+ "start_sec": req.start_sec,
+ "end_sec": req.end_sec,
+ "crf": req.crf,
+ "preset": req.preset,
+ "denoise": req.denoise,
+ "sharpen": req.sharpen,
+ "interpolation": req.interpolation,
+ "transcode_format": req.transcode_format,
+ "temp_dir": req.temp_dir,
+ "status": "paused", # Start as paused so it can be resumed
+ "progress": 0.0,
+ "current_frame": 0,
+ "total_frames": 0,
+ "eta": "Ready to resume"
+ }
+
+ job = upscaler.UpscaleJob.from_dict(job_dict)
+
+ # Calculate progress based on existing frames in the temp directory if possible
+ job_temp_dir = os.path.join(req.temp_dir if req.temp_dir else upscaler.TEMP_DIR, job_id)
+ input_frames_dir = os.path.join(job_temp_dir, "input_frames")
+ output_frames_dir = os.path.join(job_temp_dir, "output_frames")
+
+ total_frames = 0
+ if os.path.exists(input_frames_dir):
+ total_frames = len([f for f in os.listdir(input_frames_dir) if f.startswith("frame_")])
+ job.total_frames = total_frames
+
+ if os.path.exists(output_frames_dir):
+ out_frames = len([f for f in os.listdir(output_frames_dir) if f.startswith("frame_")])
+ job.current_frame = out_frames
+ if total_frames > 0:
+ job.progress = min(99.0, round((out_frames / total_frames) * 100.0, 2))
+
+ jobs_db[job_id] = job
save_jobs_db()
- return {"cleaned": cleaned, "errors": errors}
+
+ # Auto-resume the job by adding it to the queue
+ job_queue.put(job_id)
+ job.update_status("queued", eta="Queued for resume...")
+ save_jobs_db()
+
+ return {"status": "success", "job_id": job_id}
# ─── Distributed Processing Coordinator & Worker Endpoints ────────────────
diff --git a/static/app.js b/static/app.js
index d2bfc78..599edab 100644
--- a/static/app.js
+++ b/static/app.js
@@ -2726,6 +2726,11 @@ async function loadTempInfo() {
${statusLabel}
+ ${f.status === "orphaned" ? `
+
+ ` : ''}
@@ -2756,6 +2761,13 @@ async function loadTempInfo() {
}
}
});
+
+ const reimportBtn = item.querySelector(".btn-reimport-temp");
+ if (reimportBtn) {
+ reimportBtn.addEventListener("click", async (e) => {
+ e.stopPropagation();
+ showReimportModal(f.job_id, f.path);
+ });
}
listContainer.appendChild(item);
@@ -2912,4 +2924,124 @@ async function loadDefaultTempDir() {
}
}
+async function showReimportModal(jobId, tempPath) {
+ const modal = document.getElementById("reimport-modal");
+ const jobIdInput = document.getElementById("reimport-job-id");
+ const tempDirInput = document.getElementById("reimport-temp-dir");
+ const videoSelect = document.getElementById("reimport-video-select");
+
+ if (!modal || !jobIdInput || !tempDirInput || !videoSelect) return;
+
+ jobIdInput.value = jobId;
+
+ // Resolve custom parent directory if tempPath contains jobId
+ const parentDir = tempPath.substring(0, tempPath.lastIndexOf("/" + jobId));
+ tempDirInput.value = parentDir;
+
+ // Populate video files dropdown
+ videoSelect.innerHTML = '';
+
+ try {
+ const res = await fetch("/api/uploads");
+ if (res.ok) {
+ const uploads = await res.json();
+ if (uploads.length === 0) {
+ videoSelect.innerHTML = '';
+ } else {
+ videoSelect.innerHTML = '';
+
+ uploads.forEach(u => {
+ // Reimport requires the absolute path to the uploaded video file
+ const videoPath = `/var/home/david/Documents/vscode/video_upscaler/uploads/${u.original_filename}`;
+ const opt = document.createElement("option");
+ opt.value = videoPath;
+ opt.textContent = `${u.original_filename} (${formatBytes(u.size_bytes)})`;
+ videoSelect.appendChild(opt);
+ });
+
+ // Try to auto-match files by looking for standard uploaded prefix or creation time
+ try {
+ const matches = Array.from(videoSelect.options).filter(opt => opt.value.includes("8d4d3e0e-e5a5-4101-9e2d-550f96c1674f"));
+ if (matches.length > 0) {
+ matches[0].selected = true;
+ }
+ } catch(e) {}
+ }
+ } else {
+ videoSelect.innerHTML = '';
+ }
+ } catch (err) {
+ console.error(err);
+ videoSelect.innerHTML = '';
+ }
+
+ modal.style.display = "flex";
+ document.body.style.overflow = "hidden";
+}
+
+// Re-import modal close event
+const reimportModal = document.getElementById("reimport-modal");
+const reimportCloseBtn = document.getElementById("reimport-modal-close-btn");
+if (reimportModal && reimportCloseBtn) {
+ reimportCloseBtn.addEventListener("click", () => {
+ reimportModal.style.display = "none";
+ document.body.style.overflow = "";
+ });
+ reimportModal.addEventListener("click", (e) => {
+ if (e.target === reimportModal) {
+ reimportModal.style.display = "none";
+ document.body.style.overflow = "";
+ }
+ });
+}
+
+// Re-import form submission handler
+const reimportForm = document.getElementById("reimport-form");
+if (reimportForm) {
+ reimportForm.addEventListener("submit", async (e) => {
+ e.preventDefault();
+ const jobId = document.getElementById("reimport-job-id").value;
+ const tempDir = document.getElementById("reimport-temp-dir").value;
+ const videoPath = document.getElementById("reimport-video-select").value;
+ const model = document.getElementById("reimport-model-select").value;
+ const faceRestoration = document.getElementById("reimport-face-restoration").checked;
+ const rifeInterpolation = document.getElementById("reimport-rife-interpolation").checked;
+ const audioDenoise = document.getElementById("reimport-audio-denoise").checked;
+
+ if (!videoPath) {
+ alert("Please select a video file.");
+ return;
+ }
+
+ try {
+ const res = await fetch(`/api/system/temp-reimport/${jobId}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ video_path: videoPath,
+ model: model,
+ scale: 4,
+ ai_face_restoration: faceRestoration,
+ ai_rife_interpolation: rifeInterpolation,
+ ai_audio_denoise: audioDenoise,
+ temp_dir: tempDir
+ })
+ });
+
+ if (res.ok) {
+ reimportModal.style.display = "none";
+ document.body.style.overflow = "";
+ loadQueue();
+ loadTempInfo();
+ } else {
+ const err = await res.json();
+ alert("Failed to reimport job: " + (err.detail || "unknown error"));
+ }
+ } catch (err) {
+ console.error(err);
+ alert("Network error occurred.");
+ }
+ });
+}
+
initializeApp();
diff --git a/static/index.html b/static/index.html
index f2dda27..de3fd8b 100644
--- a/static/index.html
+++ b/static/index.html
@@ -921,6 +921,54 @@
+
+
+