Implement batch processing, pause/resume, custom temp directory, and detailed pipeline logs
This commit is contained in:
+135
-2
@@ -235,6 +235,35 @@ class StartUpscaleRequest(BaseModel):
|
|||||||
ai_face_restoration: bool = False
|
ai_face_restoration: bool = False
|
||||||
ai_rife_interpolation: bool = False
|
ai_rife_interpolation: bool = False
|
||||||
ai_audio_denoise: bool = False
|
ai_audio_denoise: bool = False
|
||||||
|
temp_dir: str | None = None
|
||||||
|
|
||||||
|
class StartBatchUpscaleRequest(BaseModel):
|
||||||
|
file_ids: List[str]
|
||||||
|
model: str = "realesr-animevideov3"
|
||||||
|
scale: int = 4
|
||||||
|
tile_size: int = 256
|
||||||
|
preserve_audio: bool = True
|
||||||
|
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
|
||||||
|
webhook_url: str | None = None
|
||||||
|
transcode_format: str = "mp4"
|
||||||
|
is_preview: bool = False
|
||||||
|
ai_face_restoration: bool = False
|
||||||
|
ai_rife_interpolation: bool = False
|
||||||
|
ai_audio_denoise: bool = False
|
||||||
|
temp_dir: str | None = None
|
||||||
|
|
||||||
class PreviewRequest(BaseModel):
|
class PreviewRequest(BaseModel):
|
||||||
file_id: str
|
file_id: str
|
||||||
@@ -422,7 +451,8 @@ def start_upscale(req: StartUpscaleRequest):
|
|||||||
is_preview=req.is_preview,
|
is_preview=req.is_preview,
|
||||||
ai_face_restoration=req.ai_face_restoration,
|
ai_face_restoration=req.ai_face_restoration,
|
||||||
ai_rife_interpolation=req.ai_rife_interpolation,
|
ai_rife_interpolation=req.ai_rife_interpolation,
|
||||||
ai_audio_denoise=req.ai_audio_denoise
|
ai_audio_denoise=req.ai_audio_denoise,
|
||||||
|
temp_dir=req.temp_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
jobs_db[job_id] = job
|
jobs_db[job_id] = job
|
||||||
@@ -443,6 +473,80 @@ def start_upscale(req: StartUpscaleRequest):
|
|||||||
"status": "queued"
|
"status": "queued"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.post("/api/upscale/start/batch")
|
||||||
|
def start_upscale_batch(req: StartBatchUpscaleRequest):
|
||||||
|
"""Queue multiple upscaling tasks with one set of settings"""
|
||||||
|
queued_jobs = []
|
||||||
|
|
||||||
|
# 1. Validation loop
|
||||||
|
for file_id in req.file_ids:
|
||||||
|
file_path = None
|
||||||
|
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
|
||||||
|
for ext in exts:
|
||||||
|
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}")
|
||||||
|
if os.path.exists(test_path):
|
||||||
|
file_path = test_path
|
||||||
|
break
|
||||||
|
if not file_path:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Uploaded file {file_id} not found.")
|
||||||
|
|
||||||
|
# 2. Queueing loop
|
||||||
|
for file_id in req.file_ids:
|
||||||
|
file_path = None
|
||||||
|
for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
|
||||||
|
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}")
|
||||||
|
if os.path.exists(test_path):
|
||||||
|
file_path = test_path
|
||||||
|
break
|
||||||
|
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
job = upscaler.UpscaleJob(
|
||||||
|
job_id=job_id,
|
||||||
|
video_path=file_path,
|
||||||
|
model=req.model,
|
||||||
|
scale=req.scale,
|
||||||
|
tile_size=req.tile_size,
|
||||||
|
preserve_audio=req.preserve_audio,
|
||||||
|
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,
|
||||||
|
webhook_url=req.webhook_url,
|
||||||
|
transcode_format=req.transcode_format,
|
||||||
|
is_preview=req.is_preview,
|
||||||
|
ai_face_restoration=req.ai_face_restoration,
|
||||||
|
ai_rife_interpolation=req.ai_rife_interpolation,
|
||||||
|
ai_audio_denoise=req.ai_audio_denoise,
|
||||||
|
temp_dir=req.temp_dir
|
||||||
|
)
|
||||||
|
jobs_db[job_id] = job
|
||||||
|
job_queue.put(job_id)
|
||||||
|
queued_jobs.append({"job_id": job_id, "status": "queued"})
|
||||||
|
|
||||||
|
save_jobs_db()
|
||||||
|
|
||||||
|
# Broadcast initial queued progress for all queued jobs
|
||||||
|
for qj in queued_jobs:
|
||||||
|
broadcast_progress(qj["job_id"], {
|
||||||
|
"status": "queued",
|
||||||
|
"progress": 0.0,
|
||||||
|
"current_frame": 0,
|
||||||
|
"total_frames": 0,
|
||||||
|
"eta": "Calculating..."
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"jobs": queued_jobs}
|
||||||
|
|
||||||
@app.get("/api/upscale/status/{job_id}")
|
@app.get("/api/upscale/status/{job_id}")
|
||||||
def get_status(job_id: str):
|
def get_status(job_id: str):
|
||||||
"""Get status of upscale job"""
|
"""Get status of upscale job"""
|
||||||
@@ -699,7 +803,7 @@ def get_queue():
|
|||||||
|
|
||||||
@app.post("/api/upscale/resume/{job_id}")
|
@app.post("/api/upscale/resume/{job_id}")
|
||||||
def resume_job(job_id: str):
|
def resume_job(job_id: str):
|
||||||
"""Resume an interrupted/failed upscale job"""
|
"""Resume an interrupted/failed/paused upscale job"""
|
||||||
job = jobs_db.get(job_id)
|
job = jobs_db.get(job_id)
|
||||||
if not job:
|
if not job:
|
||||||
raise HTTPException(status_code=404, detail="Job not found.")
|
raise HTTPException(status_code=404, detail="Job not found.")
|
||||||
@@ -708,6 +812,8 @@ def resume_job(job_id: str):
|
|||||||
job.status = "queued"
|
job.status = "queued"
|
||||||
job.error = None
|
job.error = None
|
||||||
job.eta = "Queued for resume..."
|
job.eta = "Queued for resume..."
|
||||||
|
if hasattr(job, "_is_paused"):
|
||||||
|
job._is_paused = False
|
||||||
|
|
||||||
job_queue.put(job_id)
|
job_queue.put(job_id)
|
||||||
save_jobs_db()
|
save_jobs_db()
|
||||||
@@ -722,6 +828,33 @@ def resume_job(job_id: str):
|
|||||||
|
|
||||||
return {"job_id": job_id, "status": "queued"}
|
return {"job_id": job_id, "status": "queued"}
|
||||||
|
|
||||||
|
@app.post("/api/upscale/pause/{job_id}")
|
||||||
|
def pause_job(job_id: str):
|
||||||
|
"""Pause a running or queued job"""
|
||||||
|
job = jobs_db.get(job_id)
|
||||||
|
if not job:
|
||||||
|
raise HTTPException(status_code=404, detail="Job not found.")
|
||||||
|
|
||||||
|
if job.status not in ["queued", "pending", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"]:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Job in status {job.status} cannot be paused.")
|
||||||
|
|
||||||
|
# Remove from queue if it is in queue
|
||||||
|
job_queue.remove(job_id)
|
||||||
|
|
||||||
|
# Call pause logic on the job
|
||||||
|
job.pause()
|
||||||
|
save_jobs_db()
|
||||||
|
|
||||||
|
broadcast_progress(job_id, {
|
||||||
|
"status": "paused",
|
||||||
|
"progress": job.progress,
|
||||||
|
"current_frame": job.current_frame,
|
||||||
|
"total_frames": job.total_frames,
|
||||||
|
"eta": "Paused"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"job_id": job_id, "status": "paused"}
|
||||||
|
|
||||||
# Websocket endpoint for real-time progress updates
|
# Websocket endpoint for real-time progress updates
|
||||||
@app.websocket("/ws/progress/{job_id}")
|
@app.websocket("/ws/progress/{job_id}")
|
||||||
async def websocket_progress(websocket: WebSocket, job_id: str):
|
async def websocket_progress(websocket: WebSocket, job_id: str):
|
||||||
|
|||||||
+41
-5
@@ -26,8 +26,9 @@ class UpscaleJob:
|
|||||||
denoise: bool = False, sharpen: bool = False, interpolation: bool = False,
|
denoise: bool = False, sharpen: bool = False, interpolation: bool = False,
|
||||||
webhook_url: str = None, transcode_format: str = "mp4", is_preview: bool = False,
|
webhook_url: str = None, transcode_format: str = "mp4", is_preview: bool = False,
|
||||||
ai_face_restoration: bool = False, ai_rife_interpolation: bool = False,
|
ai_face_restoration: bool = False, ai_rife_interpolation: bool = False,
|
||||||
ai_audio_denoise: bool = False):
|
ai_audio_denoise: bool = False, temp_dir: str = None):
|
||||||
self.job_id = job_id
|
self.job_id = job_id
|
||||||
|
self.temp_dir = temp_dir
|
||||||
self.video_path = video_path
|
self.video_path = video_path
|
||||||
self.model = model
|
self.model = model
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
@@ -79,9 +80,10 @@ class UpscaleJob:
|
|||||||
self.start_time = None
|
self.start_time = None
|
||||||
self.output_file = None
|
self.output_file = None
|
||||||
|
|
||||||
# Track processes to allow cancellation
|
# Track processes to allow cancellation/pause
|
||||||
self._processes = []
|
self._processes = []
|
||||||
self._is_cancelled = False
|
self._is_cancelled = False
|
||||||
|
self._is_paused = False
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@@ -106,6 +108,7 @@ class UpscaleJob:
|
|||||||
setattr(job, k, v)
|
setattr(job, k, v)
|
||||||
job._processes = []
|
job._processes = []
|
||||||
job._is_cancelled = False
|
job._is_cancelled = False
|
||||||
|
job._is_paused = data.get('_is_paused', False) or (data.get('status') == 'paused')
|
||||||
job._lock = threading.Lock()
|
job._lock = threading.Lock()
|
||||||
return job
|
return job
|
||||||
|
|
||||||
@@ -137,10 +140,32 @@ class UpscaleJob:
|
|||||||
pass
|
pass
|
||||||
self._processes.clear()
|
self._processes.clear()
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
with self._lock:
|
||||||
|
if self.status in ["queued", "pending"]:
|
||||||
|
self.status = "paused"
|
||||||
|
self.eta = "Paused"
|
||||||
|
elif self.status in ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"]:
|
||||||
|
self._is_paused = True
|
||||||
|
self.status = "paused"
|
||||||
|
self.eta = "Paused"
|
||||||
|
for p in self._processes:
|
||||||
|
try:
|
||||||
|
p.terminate()
|
||||||
|
p.wait(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
p.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._processes.clear()
|
||||||
|
|
||||||
def run_command(self, cmd: list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) -> subprocess.Popen:
|
def run_command(self, cmd: list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) -> subprocess.Popen:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self._is_cancelled:
|
if self._is_cancelled:
|
||||||
raise InterruptedError("Job was cancelled")
|
raise InterruptedError("Job was cancelled")
|
||||||
|
if getattr(self, "_is_paused", False):
|
||||||
|
raise InterruptedError("Job was paused")
|
||||||
|
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
@@ -240,6 +265,7 @@ def upscale_image_file(input_path: str, output_path: str, model: str, scale: int
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dict[str, Any]], None]):
|
def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dict[str, Any]], None]):
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] starting pipeline. Video path: {job.video_path}, Model: {job.model}, Scale: {job.scale}")
|
||||||
job.start_time = time.time()
|
job.start_time = time.time()
|
||||||
job.update_status("analyzing", progress=5)
|
job.update_status("analyzing", progress=5)
|
||||||
|
|
||||||
@@ -254,7 +280,8 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
fps = info["fps"]
|
fps = info["fps"]
|
||||||
|
|
||||||
# Create job temp directories
|
# Create job temp directories
|
||||||
job_temp_dir = os.path.join(TEMP_DIR, job.job_id)
|
base_temp = job.temp_dir if getattr(job, "temp_dir", None) else TEMP_DIR
|
||||||
|
job_temp_dir = os.path.join(base_temp, job.job_id)
|
||||||
input_frames_dir = os.path.join(job_temp_dir, "input_frames")
|
input_frames_dir = os.path.join(job_temp_dir, "input_frames")
|
||||||
output_frames_dir = os.path.join(job_temp_dir, "output_frames")
|
output_frames_dir = os.path.join(job_temp_dir, "output_frames")
|
||||||
|
|
||||||
@@ -299,6 +326,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
if not skip_extraction:
|
if not skip_extraction:
|
||||||
job.update_status("extracting", progress=10)
|
job.update_status("extracting", progress=10)
|
||||||
on_progress_update(job.job_id, {"status": "extracting", "progress": 10})
|
on_progress_update(job.job_id, {"status": "extracting", "progress": 10})
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] extracting frames. Command: {' '.join(extract_cmd)}")
|
||||||
|
|
||||||
# High quality JPG frames to balance disk usage and speed
|
# High quality JPG frames to balance disk usage and speed
|
||||||
extract_cmd = ["ffmpeg", "-y"]
|
extract_cmd = ["ffmpeg", "-y"]
|
||||||
@@ -327,6 +355,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
# Count actual frames extracted
|
# Count actual frames extracted
|
||||||
extracted_files = sorted([f for f in os.listdir(input_frames_dir) if f.startswith("frame_")])
|
extracted_files = sorted([f for f in os.listdir(input_frames_dir) if f.startswith("frame_")])
|
||||||
actual_total = len(extracted_files)
|
actual_total = len(extracted_files)
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] extraction completed. Extracted {actual_total} frames.")
|
||||||
if actual_total == 0:
|
if actual_total == 0:
|
||||||
raise RuntimeError("No frames extracted from video")
|
raise RuntimeError("No frames extracted from video")
|
||||||
|
|
||||||
@@ -384,6 +413,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
upscale_stderr_path = os.path.join(job_temp_dir, "upscale_stderr.log")
|
upscale_stderr_path = os.path.join(job_temp_dir, "upscale_stderr.log")
|
||||||
|
|
||||||
with open(upscale_stdout_path, "w") as f_out, open(upscale_stderr_path, "w") as f_err:
|
with open(upscale_stdout_path, "w") as f_out, open(upscale_stderr_path, "w") as f_err:
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] starting realesrgan upscaling. Model: {job.model}, Tile size: {current_tile_size}. Command: {' '.join(upscale_cmd)}")
|
||||||
p_upscale = job.run_command(upscale_cmd, stdout=f_out, stderr=f_err)
|
p_upscale = job.run_command(upscale_cmd, stdout=f_out, stderr=f_err)
|
||||||
|
|
||||||
# Monitor thread for output files
|
# Monitor thread for output files
|
||||||
@@ -479,7 +509,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
gfpgan_installed = importlib.util.find_spec("gfpgan") is not None
|
gfpgan_installed = importlib.util.find_spec("gfpgan") is not None
|
||||||
|
|
||||||
if gfpgan_installed:
|
if gfpgan_installed:
|
||||||
print(f"Job {job.job_id}: GFPGAN detected. Running Face Restoration...")
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] starting GFPGAN face restoration.")
|
||||||
restored_dir = os.path.join(job_temp_dir, "restored_frames")
|
restored_dir = os.path.join(job_temp_dir, "restored_frames")
|
||||||
os.makedirs(restored_dir, exist_ok=True)
|
os.makedirs(restored_dir, exist_ok=True)
|
||||||
|
|
||||||
@@ -521,6 +551,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
if getattr(job, "ai_rife_interpolation", False):
|
if getattr(job, "ai_rife_interpolation", False):
|
||||||
rife_bin = os.path.join(BASE_DIR, "rife-bin", "rife-ncnn-vulkan")
|
rife_bin = os.path.join(BASE_DIR, "rife-bin", "rife-ncnn-vulkan")
|
||||||
if os.path.isfile(rife_bin):
|
if os.path.isfile(rife_bin):
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] starting RIFE frame interpolation.")
|
||||||
job.update_status("interpolating", progress=83.0)
|
job.update_status("interpolating", progress=83.0)
|
||||||
on_progress_update(job.job_id, {"status": "interpolating", "progress": 83.0})
|
on_progress_update(job.job_id, {"status": "interpolating", "progress": 83.0})
|
||||||
os.makedirs(rife_frames_dir, exist_ok=True)
|
os.makedirs(rife_frames_dir, exist_ok=True)
|
||||||
@@ -630,6 +661,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
out_filepath
|
out_filepath
|
||||||
])
|
])
|
||||||
|
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] starting video assembly. Command: {' '.join(assemble_cmd)}")
|
||||||
p_assemble = job.run_command(assemble_cmd)
|
p_assemble = job.run_command(assemble_cmd)
|
||||||
stdout, stderr = p_assemble.communicate()
|
stdout, stderr = p_assemble.communicate()
|
||||||
job.cleanup_process(p_assemble)
|
job.cleanup_process(p_assemble)
|
||||||
@@ -645,13 +677,17 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
|
|||||||
"eta": "Done",
|
"eta": "Done",
|
||||||
"output_file": out_filename
|
"output_file": out_filename
|
||||||
})
|
})
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] pipeline completed successfully. Output file: {job.output_file}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if not job._is_cancelled:
|
if not job._is_cancelled and not getattr(job, "_is_paused", False) and job.status != "paused":
|
||||||
job.update_status("failed", error=str(e))
|
job.update_status("failed", error=str(e))
|
||||||
on_progress_update(job.job_id, {"status": "failed", "error": str(e)})
|
on_progress_update(job.job_id, {"status": "failed", "error": str(e)})
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] pipeline failed. Error: {e}")
|
||||||
|
else:
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [Job {job.job_id}] pipeline halted. Status: {job.status}")
|
||||||
finally:
|
finally:
|
||||||
# Clean up temp frames to save space only if completed or cancelled
|
# Clean up temp frames to save space only if completed or cancelled
|
||||||
try:
|
try:
|
||||||
|
|||||||
+310
-62
@@ -241,13 +241,99 @@ if (uploadModelBtn) {
|
|||||||
dropZone.addEventListener('drop', (e) => {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
const dt = e.dataTransfer;
|
const dt = e.dataTransfer;
|
||||||
const files = dt.files;
|
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) => {
|
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) {
|
function formatBytes(bytes, decimals = 2) {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -718,53 +804,105 @@ startUpscaleBtn.addEventListener("click", async () => {
|
|||||||
const filterSharpen = document.getElementById("filter-sharpen");
|
const filterSharpen = document.getElementById("filter-sharpen");
|
||||||
const filterFps = document.getElementById("filter-fps");
|
const filterFps = document.getElementById("filter-fps");
|
||||||
const webhookUrl = document.getElementById("webhook-url");
|
const webhookUrl = document.getElementById("webhook-url");
|
||||||
|
const tempDirInput = document.getElementById("temp-dir-input");
|
||||||
|
|
||||||
const aiFaceRestoration = document.getElementById("ai-face-restoration");
|
const aiFaceRestoration = document.getElementById("ai-face-restoration");
|
||||||
const aiRifeInterpolation = document.getElementById("ai-rife-interpolation");
|
const aiRifeInterpolation = document.getElementById("ai-rife-interpolation");
|
||||||
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
|
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/upscale/start", {
|
if (window.isBatchMode && window.batchFileIds && window.batchFileIds.length > 0) {
|
||||||
method: "POST",
|
const res = await fetch("/api/upscale/start/batch", {
|
||||||
headers: { "Content-Type": "application/json" },
|
method: "POST",
|
||||||
body: JSON.stringify({
|
headers: { "Content-Type": "application/json" },
|
||||||
file_id: uploadFileId,
|
body: JSON.stringify({
|
||||||
model: modelSelect.value,
|
file_ids: window.batchFileIds,
|
||||||
scale: parseInt(scaleRange.value),
|
model: modelSelect.value,
|
||||||
tile_size: parseInt(tileRange.value),
|
scale: parseInt(scaleRange.value),
|
||||||
preserve_audio: audioToggle.checked,
|
tile_size: parseInt(tileRange.value),
|
||||||
|
preserve_audio: audioToggle.checked,
|
||||||
// Advanced fields
|
|
||||||
start_sec: parseFloat(trimStartRange.value),
|
// Advanced fields (disabled for batch)
|
||||||
end_sec: parseFloat(trimEndRange.value),
|
start_sec: null,
|
||||||
gpu_ids: gpuSelect.value === "auto" ? null : gpuSelect.value,
|
end_sec: null,
|
||||||
crf: parseInt(crfRange.value),
|
gpu_ids: gpuSelect.value === "auto" ? null : gpuSelect.value,
|
||||||
preset: presetSelect.value,
|
crf: parseInt(crfRange.value),
|
||||||
transcode_format: transcodeSelect.value,
|
preset: presetSelect.value,
|
||||||
denoise: filterDenoise.checked,
|
transcode_format: transcodeSelect.value,
|
||||||
sharpen: filterSharpen.checked,
|
denoise: filterDenoise.checked,
|
||||||
interpolation: filterFps.checked,
|
sharpen: filterSharpen.checked,
|
||||||
webhook_url: webhookUrl.value.trim() || null,
|
interpolation: filterFps.checked,
|
||||||
ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false,
|
webhook_url: webhookUrl.value.trim() || null,
|
||||||
ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false,
|
ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false,
|
||||||
ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.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."));
|
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) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
startUpscaleBtn.disabled = false;
|
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 btnLabel = job.status === "failed" ? "Retry" : "Resume";
|
||||||
const btnIcon = job.status === "failed" ? "fa-rotate-right" : "fa-play";
|
const btnIcon = job.status === "failed" ? "fa-rotate-right" : "fa-play";
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
@@ -1418,6 +1556,9 @@ async function loadQueue() {
|
|||||||
<button class="btn btn-primary btn-small btn-view-job" data-id="${job.job_id}">
|
<button class="btn btn-primary btn-small btn-view-job" data-id="${job.job_id}">
|
||||||
<i class="fa-solid fa-eye"></i> View
|
<i class="fa-solid fa-eye"></i> View
|
||||||
</button>
|
</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}">
|
<button class="btn btn-danger btn-small btn-abort-job" data-id="${job.job_id}">
|
||||||
<i class="fa-solid fa-circle-stop"></i> Abort
|
<i class="fa-solid fa-circle-stop"></i> Abort
|
||||||
</button>
|
</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;";
|
const progressStyle = showProgress ? "display: block;" : "display: none;";
|
||||||
|
|
||||||
item.innerHTML = `
|
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");
|
const deleteBtn = item.querySelector(".btn-delete-job");
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
deleteBtn.addEventListener("click", async (e) => {
|
deleteBtn.addEventListener("click", async (e) => {
|
||||||
@@ -1624,6 +1783,19 @@ async function loadModels() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useUploadedVideo(entry) {
|
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;
|
uploadFileId = entry.file_id;
|
||||||
uploadMetadata = entry.metadata;
|
uploadMetadata = entry.metadata;
|
||||||
|
|
||||||
@@ -1684,10 +1856,55 @@ function useUploadedVideo(entry) {
|
|||||||
stepConfig.classList.add("active");
|
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() {
|
async function loadUploadsBrowser() {
|
||||||
const browserList = document.getElementById("uploads-browser-list");
|
const browserList = document.getElementById("uploads-browser-list");
|
||||||
if (!browserList) return;
|
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 {
|
try {
|
||||||
const res = await fetch("/api/uploads");
|
const res = await fetch("/api/uploads");
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
@@ -1700,10 +1917,12 @@ async function loadUploadsBrowser() {
|
|||||||
<p>No uploaded files found. Upload a video above to get started!</p>
|
<p>No uploaded files found. Upload a video above to get started!</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (batchConfigBar) batchConfigBar.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
browserList.innerHTML = "";
|
browserList.innerHTML = "";
|
||||||
|
let selectedEntries = [];
|
||||||
|
|
||||||
uploads.forEach(entry => {
|
uploads.forEach(entry => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
@@ -1715,29 +1934,48 @@ async function loadUploadsBrowser() {
|
|||||||
const fpsStr = entry.metadata ? `${entry.metadata.fps} FPS` : "-";
|
const fpsStr = entry.metadata ? `${entry.metadata.fps} FPS` : "-";
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="queue-item-details">
|
<div style="display: flex; align-items: center; gap: 1rem; width: 100%;">
|
||||||
<div class="queue-item-title">
|
<input type="checkbox" class="batch-select-checkbox" data-id="${entry.file_id}" style="width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary);">
|
||||||
<i class="fa-solid fa-file-video" style="color: var(--primary);"></i> ${entry.original_filename}
|
<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>
|
||||||
<div class="queue-item-meta">
|
<div class="queue-item-status-bar" style="border: none; padding: 0;">
|
||||||
<span>Size: ${sizeStr}</span>
|
<div class="queue-item-actions">
|
||||||
<span>Resolution: ${resStr}</span>
|
<button class="btn btn-primary btn-small btn-use-upload" data-id="${entry.file_id}">
|
||||||
<span>Duration: ${durationStr}</span>
|
<i class="fa-solid fa-play"></i> Use
|
||||||
<span>FPS: ${fpsStr}</span>
|
</button>
|
||||||
</div>
|
<button class="btn btn-danger btn-small btn-delete-upload" data-id="${entry.file_id}">
|
||||||
</div>
|
<i class="fa-solid fa-trash"></i> Delete
|
||||||
<div class="queue-item-status-bar">
|
</button>
|
||||||
<div class="queue-item-actions">
|
</div>
|
||||||
<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>
|
</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", () => {
|
item.querySelector(".btn-use-upload").addEventListener("click", () => {
|
||||||
useUploadedVideo(entry);
|
useUploadedVideo(entry);
|
||||||
});
|
});
|
||||||
@@ -1757,6 +1995,16 @@ async function loadUploadsBrowser() {
|
|||||||
|
|
||||||
browserList.appendChild(item);
|
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) {
|
} catch (err) {
|
||||||
console.error("Error loading uploads browser:", err);
|
console.error("Error loading uploads browser:", err);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -42,10 +42,10 @@
|
|||||||
<!-- Step 1: Upload Video -->
|
<!-- Step 1: Upload Video -->
|
||||||
<div id="step-upload" class="step-container active">
|
<div id="step-upload" class="step-container active">
|
||||||
<div class="glass-card">
|
<div class="glass-card">
|
||||||
|
<input type="file" id="file-input" accept="video/*" multiple style="display: none;">
|
||||||
<div id="drop-zone" class="upload-area">
|
<div id="drop-zone" class="upload-area">
|
||||||
<input type="file" id="file-input" accept="video/*" style="display: none;">
|
|
||||||
<span class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
<span class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
||||||
<h3>Drag & Drop Video File</h3>
|
<h3>Drag & Drop Video File(s)</h3>
|
||||||
<p>Supports MP4, MKV, AVI, MOV, WEBM (Max 500MB recommended for local upscaling)</p>
|
<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()">
|
<button type="button" class="btn" onclick="document.getElementById('file-input').click()">
|
||||||
<i class="fa-solid fa-folder-open"></i> Browse Files
|
<i class="fa-solid fa-folder-open"></i> Browse Files
|
||||||
@@ -61,6 +61,15 @@
|
|||||||
<i class="fa-solid fa-arrows-rotate"></i> Refresh
|
<i class="fa-solid fa-arrows-rotate"></i> Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Configuration Bar -->
|
||||||
|
<div id="batch-config-bar" style="display: none; background: rgba(var(--primary-rgb), 0.1); border: 1px solid var(--primary); border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 1rem; justify-content: space-between; align-items: center; gap: 1rem;">
|
||||||
|
<span style="font-size: 0.9rem; color: var(--primary); font-weight: bold;"><i class="fa-solid fa-square-check"></i> <span id="batch-selected-count">0</span> video(s) selected</span>
|
||||||
|
<button type="button" id="batch-configure-btn" class="btn btn-primary btn-small">
|
||||||
|
<i class="fa-solid fa-sliders"></i> Configure Batch Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="uploads-browser-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
|
<div id="uploads-browser-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
<!-- Dynamically populated files -->
|
<!-- Dynamically populated files -->
|
||||||
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
||||||
@@ -267,6 +276,15 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Temporary Directory -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="temp-dir-input"><i class="fa-solid fa-folder-open" style="color: var(--primary); margin-right: 0.25rem;"></i> Custom Temporary Files Directory</label>
|
||||||
|
<input type="text" id="temp-dir-input" placeholder="e.g. /mnt/fast_ssd/temp (Leave empty for default temp/)">
|
||||||
|
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 2px;">
|
||||||
|
Specify a custom location for temporary image frames to prevent running out of primary drive space.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pre-filters selection checkboxes -->
|
<!-- Pre-filters selection checkboxes -->
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<span style="font-weight: 600; font-size: 0.85rem; display: block; color: var(--primary);">Pre-Filters & Enhancing</span>
|
<span style="font-weight: 600; font-size: 0.85rem; display: block; color: var(--primary);">Pre-Filters & Enhancing</span>
|
||||||
|
|||||||
@@ -659,6 +659,7 @@ input:checked + .slider:before {
|
|||||||
.status-badge.completed { background: rgba(0, 255, 135, 0.1); border: 1px solid var(--success); color: var(--success); }
|
.status-badge.completed { background: rgba(0, 255, 135, 0.1); border: 1px solid var(--success); color: var(--success); }
|
||||||
.status-badge.failed { background: rgba(255, 0, 85, 0.1); border: 1px solid var(--danger); color: var(--danger); }
|
.status-badge.failed { background: rgba(255, 0, 85, 0.1); border: 1px solid var(--danger); color: var(--danger); }
|
||||||
.status-badge.interrupted { background: rgba(255, 255, 255, 0.08); border: 1px solid var(--text-muted); color: var(--text-muted); }
|
.status-badge.interrupted { background: rgba(255, 255, 255, 0.08); border: 1px solid var(--text-muted); color: var(--text-muted); }
|
||||||
|
.status-badge.paused { background: rgba(255, 184, 0, 0.08); border: 1px solid var(--warning); color: var(--warning); }
|
||||||
|
|
||||||
|
|
||||||
.bar-container {
|
.bar-container {
|
||||||
|
|||||||
Reference in New Issue
Block a user