Implement orphaned temporary folder re-import and resume feature with web UI modal
This commit is contained in:
+95
-1
@@ -1590,8 +1590,102 @@ def cleanup_temp(req: CleanupTempRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"Error removing {path}: {str(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()
|
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 ────────────────
|
# ─── Distributed Processing Coordinator & Worker Endpoints ────────────────
|
||||||
|
|
||||||
|
|||||||
+132
@@ -2726,6 +2726,11 @@ async function loadTempInfo() {
|
|||||||
<div class="queue-item-status-bar" style="border: none; padding: 0;">
|
<div class="queue-item-status-bar" style="border: none; padding: 0;">
|
||||||
<span class="status-badge ${statusClass}" style="font-size: 0.7rem; padding: 0.15rem 0.5rem; margin-right: 0.5rem;">${statusLabel}</span>
|
<span class="status-badge ${statusClass}" style="font-size: 0.7rem; padding: 0.15rem 0.5rem; margin-right: 0.5rem;">${statusLabel}</span>
|
||||||
<div class="queue-item-actions">
|
<div class="queue-item-actions">
|
||||||
|
${f.status === "orphaned" ? `
|
||||||
|
<button class="btn btn-primary btn-small btn-reimport-temp" data-job-id="${f.job_id}" data-path="${f.path}">
|
||||||
|
<i class="fa-solid fa-cloud-arrow-down"></i> Re-import
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
<button class="btn btn-danger btn-small btn-delete-temp" data-path="${f.path}" ${isActive ? 'disabled style="opacity: 0.5; cursor: not-allowed;" title="Cannot delete active job frames"' : ''}>
|
<button class="btn btn-danger btn-small btn-delete-temp" data-path="${f.path}" ${isActive ? 'disabled style="opacity: 0.5; cursor: not-allowed;" title="Cannot delete active job frames"' : ''}>
|
||||||
<i class="fa-solid fa-trash"></i> Delete
|
<i class="fa-solid fa-trash"></i> Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -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);
|
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 = '<option value="">-- Loading Uploaded Videos --</option>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/uploads");
|
||||||
|
if (res.ok) {
|
||||||
|
const uploads = await res.json();
|
||||||
|
if (uploads.length === 0) {
|
||||||
|
videoSelect.innerHTML = '<option value="">No uploaded videos found. Upload one first.</option>';
|
||||||
|
} else {
|
||||||
|
videoSelect.innerHTML = '<option value="">-- Select a Video File --</option>';
|
||||||
|
|
||||||
|
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 = '<option value="">Error loading uploads.</option>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
videoSelect.innerHTML = '<option value="">Error loading uploads.</option>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
initializeApp();
|
||||||
|
|||||||
@@ -921,6 +921,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Re-import Modal -->
|
||||||
|
<div id="reimport-modal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-card glass-card" style="max-width: 480px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fa-solid fa-cloud-arrow-down"></i> Re-import & Resume Job</h3>
|
||||||
|
<button type="button" id="reimport-modal-close-btn" class="close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding: 1.5rem 0;">
|
||||||
|
<form id="reimport-form">
|
||||||
|
<input type="hidden" id="reimport-job-id">
|
||||||
|
<input type="hidden" id="reimport-temp-dir">
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 1.25rem;">
|
||||||
|
<label for="reimport-video-select" style="display: block; margin-bottom: 0.5rem; font-weight: 600; font-size: 0.85rem;">Select Video File:</label>
|
||||||
|
<select id="reimport-video-select" class="form-control" required style="width: 100%; background: var(--bg-card); color: var(--text-main); border: 1px solid var(--border-color); padding: 0.5rem; border-radius: 6px;">
|
||||||
|
<!-- Dynamically populated -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 1.25rem;">
|
||||||
|
<label for="reimport-model-select" style="display: block; margin-bottom: 0.5rem; font-weight: 600; font-size: 0.85rem;">Upscale Model:</label>
|
||||||
|
<select id="reimport-model-select" class="form-control" style="width: 100%; background: var(--bg-card); color: var(--text-main); border: 1px solid var(--border-color); padding: 0.5rem; border-radius: 6px;">
|
||||||
|
<option value="realesrgan-x4plus">Real-ESRGAN x4 Plus (Universal)</option>
|
||||||
|
<option value="realesr-animevideov3">Real-ESRGAN Anime Video v3</option>
|
||||||
|
<option value="realesrgan-x4plus-anime">Real-ESRGAN Anime x4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr; gap: 0.75rem; margin-bottom: 1.5rem;">
|
||||||
|
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.85rem;">
|
||||||
|
<input type="checkbox" id="reimport-face-restoration" style="width: 16px; height: 16px; accent-color: var(--primary);"> Enable Face Restoration (GFPGAN)
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.85rem;">
|
||||||
|
<input type="checkbox" id="reimport-rife-interpolation" style="width: 16px; height: 16px; accent-color: var(--primary);"> Enable Frame Interpolation (RIFE)
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.85rem;">
|
||||||
|
<input type="checkbox" id="reimport-audio-denoise" style="width: 16px; height: 16px; accent-color: var(--primary);"> Enable Audio Denoising
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center; padding: 0.75rem;">
|
||||||
|
<i class="fa-solid fa-play"></i> Import & Resume Job
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Javascript Orchestration -->
|
<!-- Javascript Orchestration -->
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user