Implement advanced AI enhancements UI tracking, styling, requirements, and documentation modal

This commit is contained in:
2026-06-24 13:45:34 -04:00
parent 212291ea94
commit 7b7cb2866a
7 changed files with 236 additions and 16 deletions
+8 -2
View File
@@ -98,7 +98,7 @@ def load_jobs_db():
if job.status == "queued": if job.status == "queued":
job_queue.put(job_id) job_queue.put(job_id)
# Mark active items as interrupted so they can be resumed # Mark active items as interrupted so they can be resumed
elif job.status in ["analyzing", "extracting", "upscaling", "assembling"]: elif job.status in ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"]:
job.status = "interrupted" job.status = "interrupted"
job.eta = "Interrupted" job.eta = "Interrupted"
jobs_db[job_id] = job jobs_db[job_id] = job
@@ -232,6 +232,9 @@ class StartUpscaleRequest(BaseModel):
webhook_url: str | None = None webhook_url: str | None = None
transcode_format: str = "mp4" transcode_format: str = "mp4"
is_preview: bool = False is_preview: bool = False
ai_face_restoration: bool = False
ai_rife_interpolation: bool = False
ai_audio_denoise: bool = False
class PreviewRequest(BaseModel): class PreviewRequest(BaseModel):
file_id: str file_id: str
@@ -416,7 +419,10 @@ def start_upscale(req: StartUpscaleRequest):
interpolation=req.interpolation, interpolation=req.interpolation,
webhook_url=req.webhook_url, webhook_url=req.webhook_url,
transcode_format=req.transcode_format, transcode_format=req.transcode_format,
is_preview=req.is_preview 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
) )
jobs_db[job_id] = job jobs_db[job_id] = job
+102 -8
View File
@@ -23,13 +23,18 @@ class UpscaleJob:
unsharp: bool = False, double_fps: bool = False, preserve_subtitles: bool = True, unsharp: bool = False, double_fps: bool = False, preserve_subtitles: bool = True,
start_sec: float = None, end_sec: float = None, crf: int = 18, preset: str = "medium", start_sec: float = None, end_sec: float = None, crf: int = 18, preset: str = "medium",
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_audio_denoise: bool = False):
self.job_id = job_id self.job_id = job_id
self.video_path = video_path self.video_path = video_path
self.model = model self.model = model
self.scale = scale self.scale = scale
self.tile_size = tile_size self.tile_size = tile_size
self.preserve_audio = preserve_audio self.preserve_audio = preserve_audio
self.ai_face_restoration = ai_face_restoration
self.ai_rife_interpolation = ai_rife_interpolation
self.ai_audio_denoise = ai_audio_denoise
# Trim mapping # Trim mapping
if ss is not None: if ss is not None:
@@ -448,6 +453,87 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
processed_files = len(os.listdir(output_frames_dir)) processed_files = len(os.listdir(output_frames_dir))
job.update_status("upscaling", progress=80.0, current_frame=processed_files) job.update_status("upscaling", progress=80.0, current_frame=processed_files)
# Step 2.6: AI Face Restoration (GFPGAN)
if getattr(job, "ai_face_restoration", False):
job.update_status("restoring_faces", progress=81.0)
on_progress_update(job.job_id, {"status": "restoring_faces", "progress": 81.0})
import importlib.util
gfpgan_installed = importlib.util.find_spec("gfpgan") is not None
if gfpgan_installed:
print(f"Job {job.job_id}: GFPGAN detected. Running Face Restoration...")
restored_dir = os.path.join(job_temp_dir, "restored_frames")
os.makedirs(restored_dir, exist_ok=True)
gfpgan_cmd = [
sys.executable, "-m", "gfpgan.inference_gfpgan",
"-i", output_frames_dir,
"-o", restored_dir,
"-v", "1.4",
"-s", "1",
"--bg_upsampler", "None"
]
p_gfp = job.run_command(gfpgan_cmd)
stdout, stderr = p_gfp.communicate()
job.cleanup_process(p_gfp)
if p_gfp.returncode == 0:
gfp_output_path = os.path.join(restored_dir, "restored_imgs")
if os.path.exists(gfp_output_path) and len(os.listdir(gfp_output_path)) > 0:
for filename in os.listdir(gfp_output_path):
src_f = os.path.join(gfp_output_path, filename)
dst_f = os.path.join(output_frames_dir, filename)
try:
shutil.copy2(src_f, dst_f)
except Exception as e:
print(f"Error copying restored face frame: {e}")
print(f"Job {job.job_id}: Face Restoration completed successfully.")
else:
print(f"Job {job.job_id}: GFPGAN did not generate outputs in restored_imgs.")
else:
print(f"Job {job.job_id}: GFPGAN failed (exit code {p_gfp.returncode}). Continuing with normal upscale.")
else:
print(f"Job {job.job_id}: 'gfpgan' package is not installed in the virtual environment. Skipping face restoration. To enable, run: pip install gfpgan realesrgan")
# Step 2.7: AI Frame Interpolation (RIFE)
rife_frames_dir = os.path.join(job_temp_dir, "rife_frames")
use_rife = False
if getattr(job, "ai_rife_interpolation", False):
rife_bin = os.path.join(BASE_DIR, "rife-bin", "rife-ncnn-vulkan")
if os.path.isfile(rife_bin):
job.update_status("interpolating", progress=83.0)
on_progress_update(job.job_id, {"status": "interpolating", "progress": 83.0})
os.makedirs(rife_frames_dir, exist_ok=True)
try:
os.chmod(rife_bin, 0o755)
except Exception:
pass
rife_cmd = [
rife_bin,
"-i", output_frames_dir,
"-o", rife_frames_dir,
"-f", "jpg"
]
if getattr(job, "gpu_ids", None) is not None:
rife_cmd.extend(["-g", str(job.gpu_ids)])
p_rife = job.run_command(rife_cmd)
stdout, stderr = p_rife.communicate()
job.cleanup_process(p_rife)
if p_rife.returncode == 0:
use_rife = True
print(f"Job {job.job_id}: Successfully ran RIFE frame interpolation.")
else:
print(f"Job {job.job_id}: RIFE failed (exit code {p_rife.returncode}). Falling back to FFmpeg interpolation.")
else:
print(f"Job {job.job_id}: RIFE binary not found at {rife_bin}. Falling back to FFmpeg interpolation.")
# Step 3: Reassemble video # Step 3: Reassemble video
job.update_status("assembling", progress=85.0) job.update_status("assembling", progress=85.0)
on_progress_update(job.job_id, {"status": "assembling", "progress": 85.0}) on_progress_update(job.job_id, {"status": "assembling", "progress": 85.0})
@@ -463,12 +549,15 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
if transcode_fmt == "webm": if transcode_fmt == "webm":
vcodec = "libvpx-vp9" vcodec = "libvpx-vp9"
acodec = "libvorbis" acodec = "libvorbis"
assemble_frames_dir = rife_frames_dir if use_rife else output_frames_dir
assemble_fps = fps * 2 if (use_rife or getattr(job, "double_fps", False) or getattr(job, "interpolation", False)) else fps
# Construct ffmpeg reassembly command # Construct ffmpeg reassembly command
assemble_cmd = [ assemble_cmd = [
"ffmpeg", "-y", "ffmpeg", "-y",
"-framerate", str(fps), "-framerate", str(assemble_fps),
"-i", os.path.join(output_frames_dir, "frame_%08d.jpg") "-i", os.path.join(assemble_frames_dir, "frame_%08d.jpg")
] ]
# We need the original video as the second input (index 1) if we preserve audio or subtitles # We need the original video as the second input (index 1) if we preserve audio or subtitles
@@ -485,10 +574,15 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
assemble_cmd.extend(["-map", "0:v:0"]) assemble_cmd.extend(["-map", "0:v:0"])
if job.preserve_audio: if job.preserve_audio:
assemble_cmd.extend([ assemble_cmd.extend(["-map", "1:a:0?"])
"-map", "1:a:0?", if getattr(job, "ai_audio_denoise", False):
"-c:a", acodec acodec_denoise = "libvorbis" if transcode_fmt == "webm" else "aac"
]) assemble_cmd.extend([
"-af", "arnnoise",
"-c:a", acodec_denoise
])
else:
assemble_cmd.extend(["-c:a", acodec])
if getattr(job, "preserve_subtitles", True): if getattr(job, "preserve_subtitles", True):
assemble_cmd.extend([ assemble_cmd.extend([
@@ -502,7 +596,7 @@ def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dic
vf_filters.append("hqdn3d") vf_filters.append("hqdn3d")
if getattr(job, "sharpen", False) or getattr(job, "unsharp", False): if getattr(job, "sharpen", False) or getattr(job, "unsharp", False):
vf_filters.append("unsharp=3:3:0.5:3:3:0.5") vf_filters.append("unsharp=3:3:0.5:3:3:0.5")
if getattr(job, "interpolation", False) or getattr(job, "double_fps", False): if (getattr(job, "double_fps", False) or getattr(job, "interpolation", False) or getattr(job, "ai_rife_interpolation", False)) and not use_rife:
target_fps = fps * 2 if getattr(job, "double_fps", False) else 60 target_fps = fps * 2 if getattr(job, "double_fps", False) else 60
if target_fps < fps: if target_fps < fps:
target_fps = fps target_fps = fps
+6
View File
@@ -56,3 +56,9 @@ A typical upscale job follows these sequential steps:
- **Self-Sufficiency**: Running `python3 start.py` automatically checks for a local virtual environment (`venv/` or `.venv/`). - **Self-Sufficiency**: Running `python3 start.py` automatically checks for a local virtual environment (`venv/` or `.venv/`).
- **Auto-Provisioning**: If no virtual environment is found, `start.py` will initialize one in `venv/`, upgrade `pip`, install all dependencies listed in `requirements.txt`, mark the Real-ESRGAN binary as executable (`chmod +x`), and create necessary folders (`uploads/`, `outputs/`, `temp/`). - **Auto-Provisioning**: If no virtual environment is found, `start.py` will initialize one in `venv/`, upgrade `pip`, install all dependencies listed in `requirements.txt`, mark the Real-ESRGAN binary as executable (`chmod +x`), and create necessary folders (`uploads/`, `outputs/`, `temp/`).
- **Seamless Launch**: It then automatically launches the server process using the newly created environment interpreter. - **Seamless Launch**: It then automatically launches the server process using the newly created environment interpreter.
### 4. Advanced AI Enhancements
- **AI Face Restoration (GFPGAN)**: Runs as a python subprocess invoking `gfpgan.inference_gfpgan` to restore and clear up human faces in low-resolution video frames. Results are copied directly back into the frame output folder before motion interpolation and final video assembly.
- **AI Frame Interpolation (RIFE)**: Runs using the `rife-ncnn-vulkan` binary (expected in `rife-bin/`). Smooths motion by generating and inserting intermediate frames, doubling the framerate. Falls back to FFmpeg's `minterpolate` optical flow filter if the Vulkan binary is not present.
- **AI Audio Denoising (RNNoise)**: Transports and filters audio using the deep-learning-based `arnnoise` FFmpeg filter, eliminating background noise from output tracks during assembly.
+3
View File
@@ -7,3 +7,6 @@ python-multipart>=0.0.9
websockets>=13.0 websockets>=13.0
pydantic>=2.0 pydantic>=2.0
torch>=2.0 torch>=2.0
gfpgan>=1.3.8
realesrgan>=0.3.0
+54 -6
View File
@@ -600,6 +600,10 @@ generateVideoPreviewBtn.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 aiFaceRestoration = document.getElementById("ai-face-restoration");
const aiRifeInterpolation = document.getElementById("ai-rife-interpolation");
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
const transcodeFormat = transcodeSelect.value; const transcodeFormat = transcodeSelect.value;
try { try {
@@ -626,7 +630,10 @@ generateVideoPreviewBtn.addEventListener("click", async () => {
interpolation: filterFps.checked, interpolation: filterFps.checked,
webhook_url: null, webhook_url: null,
transcode_format: transcodeFormat, transcode_format: transcodeFormat,
is_preview: true is_preview: true,
ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false,
ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false,
ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.checked : false
}) })
}); });
@@ -711,6 +718,10 @@ 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 aiFaceRestoration = document.getElementById("ai-face-restoration");
const aiRifeInterpolation = document.getElementById("ai-rife-interpolation");
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
try { try {
const res = await fetch("/api/upscale/start", { const res = await fetch("/api/upscale/start", {
@@ -733,7 +744,10 @@ startUpscaleBtn.addEventListener("click", async () => {
denoise: filterDenoise.checked, denoise: filterDenoise.checked,
sharpen: filterSharpen.checked, sharpen: filterSharpen.checked,
interpolation: filterFps.checked, interpolation: filterFps.checked,
webhook_url: webhookUrl.value.trim() || null 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
}) })
}); });
@@ -816,6 +830,10 @@ function updateProgressUI(data) {
statusText = "Queued in pipeline. Waiting for GPU lock..."; statusText = "Queued in pipeline. Waiting for GPU lock...";
} else if (data.status === "extracting") { } else if (data.status === "extracting") {
statusText = "Extracting video frames..."; statusText = "Extracting video frames...";
} else if (data.status === "restoring_faces") {
statusText = "GFPGAN: Running Face Restoration on upscaled frames...";
} else if (data.status === "interpolating") {
statusText = "RIFE: Interpolating frames for smooth motion...";
} else if (data.status === "assembling") { } else if (data.status === "assembling") {
statusText = "Assembling video preview and audio tracks..."; statusText = "Assembling video preview and audio tracks...";
} }
@@ -891,6 +909,10 @@ function updateProgressUI(data) {
appendLogLine("FFmpeg: Splitting video stream to high-fidelity JPG frames...", "info"); appendLogLine("FFmpeg: Splitting video stream to high-fidelity JPG frames...", "info");
} else if (data.status === "upscaling") { } else if (data.status === "upscaling") {
appendLogLine(`Vulkan GPU: Processing frame ${data.current_frame} of ${data.total_frames} (ETA: ${data.eta})...`, "info"); appendLogLine(`Vulkan GPU: Processing frame ${data.current_frame} of ${data.total_frames} (ETA: ${data.eta})...`, "info");
} else if (data.status === "restoring_faces") {
appendLogLine("GFPGAN: Running Face Restoration on upscaled frames...", "info");
} else if (data.status === "interpolating") {
appendLogLine("RIFE: Interpolating frames for smooth motion...", "info");
} else if (data.status === "assembling") { } else if (data.status === "assembling") {
appendLogLine("FFmpeg: Reassembling enhanced frames & merging audio tracks...", "info"); appendLogLine("FFmpeg: Reassembling enhanced frames & merging audio tracks...", "info");
} else if (data.status === "completed") { } else if (data.status === "completed") {
@@ -1021,7 +1043,7 @@ async function checkActiveJobs() {
const jobs = await res.json(); const jobs = await res.json();
const activeJob = jobs.find(job => const activeJob = jobs.find(job =>
["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status) ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status)
); );
if (activeJob) { if (activeJob) {
@@ -1318,7 +1340,7 @@ async function loadQueue() {
const jobs = await res.json(); const jobs = await res.json();
const displayJobs = jobs.filter(job => const displayJobs = jobs.filter(job =>
["queued", "analyzing", "extracting", "upscaling", "assembling", "completed", "failed", "cancelled", "interrupted"].includes(job.status) ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling", "completed", "failed", "cancelled", "interrupted"].includes(job.status)
); );
if (displayJobs.length === 0) { if (displayJobs.length === 0) {
@@ -1380,7 +1402,7 @@ async function loadQueue() {
<i class="fa-solid fa-trash"></i> Clear <i class="fa-solid fa-trash"></i> Clear
</button> </button>
`; `;
} else if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) { } else if (["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status)) {
actionsHtml = ` actionsHtml = `
${reorderHtml} ${reorderHtml}
<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}">
@@ -1401,7 +1423,7 @@ async function loadQueue() {
`; `;
} }
const showProgress = ["analyzing", "extracting", "upscaling", "assembling"].includes(job.status); const showProgress = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status);
const progressStyle = showProgress ? "display: block;" : "display: none;"; const progressStyle = showProgress ? "display: block;" : "display: none;";
item.innerHTML = ` item.innerHTML = `
@@ -1757,6 +1779,32 @@ async function initializeApp() {
loadUploadsBrowser(); loadUploadsBrowser();
}); });
} }
// Register documentation modal handlers
const docsBtn = document.getElementById("docs-btn");
const docsModal = document.getElementById("docs-modal");
const docsCloseBtn = document.getElementById("docs-modal-close-btn");
if (docsBtn && docsModal) {
docsBtn.addEventListener("click", () => {
docsModal.style.display = "flex";
document.body.style.overflow = "hidden";
});
}
if (docsCloseBtn && docsModal) {
docsCloseBtn.addEventListener("click", () => {
docsModal.style.display = "none";
document.body.style.overflow = "";
});
docsModal.addEventListener("click", (e) => {
if (e.target === docsModal) {
docsModal.style.display = "none";
document.body.style.overflow = "";
}
});
}
} }
initializeApp(); initializeApp();
+60
View File
@@ -20,6 +20,9 @@
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button type="button" id="docs-btn" class="btn btn-secondary" style="height: 38px; border-radius: 50px; padding: 0 1.25rem; display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; box-shadow: none; border: 1px solid var(--border-color);">
<i class="fa-solid fa-book-open"></i> Docs
</button>
<div class="theme-selector"> <div class="theme-selector">
<label for="theme-select"><i class="fa-solid fa-palette"></i> Theme</label> <label for="theme-select"><i class="fa-solid fa-palette"></i> Theme</label>
<select id="theme-select"> <select id="theme-select">
@@ -280,6 +283,22 @@
</div> </div>
</div> </div>
<!-- AI Enhancements Section -->
<div class="filter-group" style="margin-top: 1rem;">
<span style="font-weight: 600; font-size: 0.85rem; display: block; color: var(--primary);">Advanced AI Enhancements</span>
<div class="filter-checkboxes">
<label class="checkbox-container">
<input type="checkbox" id="ai-face-restoration"> AI Face Restoration (GFPGAN)
</label>
<label class="checkbox-container">
<input type="checkbox" id="ai-rife-interpolation"> AI Frame Interpolation (RIFE)
</label>
<label class="checkbox-container">
<input type="checkbox" id="ai-audio-denoise"> AI Audio Denoising (RNNoise)
</label>
</div>
</div>
<!-- Webhook Notify Input --> <!-- Webhook Notify Input -->
<div class="form-group"> <div class="form-group">
<label for="webhook-url">Webhook Notification Callback</label> <label for="webhook-url">Webhook Notification Callback</label>
@@ -590,6 +609,47 @@
</div> </div>
</div> </div>
<!-- Documentation Modal -->
<div id="docs-modal" class="modal-overlay" style="display: none;">
<div class="modal-card" style="max-width: 800px; max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header">
<h3><i class="fa-solid fa-book-open"></i> Help & System Documentation</h3>
<button type="button" class="close-btn" id="docs-modal-close-btn">&times;</button>
</div>
<div class="modal-body" style="overflow-y: auto; padding: 2rem; font-family: var(--font-body); line-height: 1.6; color: var(--text-main);">
<div class="docs-markdown-body">
<h2>🎬 AI Video Upscaler Guide</h2>
<p>Welcome to the AI Video Upscaler! This application leverages neural network models running locally to upscale and enhance video footage.</p>
<h3>💡 Quick Start</h3>
<ol>
<li><strong>Upload:</strong> Drag & drop a video file onto the upload zone, or browse to select one.</li>
<li><strong>Configure:</strong> Choose your AI model, scale factor, and adjust parameters.</li>
<li><strong>Preview:</strong> Select a frame using the timeline and render a single-frame preview or a 5-second video preview to test your options.</li>
<li><strong>Run:</strong> Click "Start Full Upscale" to add the job to the GPU queue.</li>
</ol>
<h3>⚙️ AI Enhancement Features</h3>
<ul>
<li><strong>AI Face Restoration (GFPGAN):</strong> Restores human faces in blurry or low-res footage. Highly recommended for family home videos or film restoration. Requires python <code>gfpgan</code> package.</li>
<li><strong>AI Frame Interpolation (RIFE):</strong> Smooths motion by inserting high-quality AI generated frames (e.g. converting 24/30 FPS to 60 FPS). Requires <code>rife-ncnn-vulkan</code> binary.</li>
<li><strong>AI Audio Denoising (RNNoise):</strong> Eliminates steady background noise, wind, and camera hiss directly from audio tracks using a deep learning RNN model.</li>
</ul>
<h3>📊 Advanced Settings</h3>
<ul>
<li><strong>CRF (Constant Rate Factor):</strong> Controls output quality/compression. Lower values (like 18) are near-lossless, higher values compress more.</li>
<li><strong>Tile Size:</strong> Decreasing this (e.g., to 128 or 64) helps run upscaling on low-end GPUs with limited VRAM.</li>
<li><strong>Video Trimming:</strong> Trim start and end times to only upscale the parts you need.</li>
</ul>
<h3>🔄 Job Resumption</h3>
<p>If the application stops or the server crashes while upscaling, the job will be marked as <strong>interrupted</strong> in the queue. Simply click <strong>Resume</strong> to pick up exactly where you left off. Already upscaled frames will be skipped, saving time and energy.</p>
</div>
</div>
</div>
</div>
<!-- Javascript Orchestration --> <!-- Javascript Orchestration -->
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
+3
View File
@@ -653,11 +653,14 @@ input:checked + .slider:before {
.status-badge.queued { background: rgba(255, 184, 0, 0.1); border: 1px solid var(--warning); color: var(--warning); } .status-badge.queued { background: rgba(255, 184, 0, 0.1); border: 1px solid var(--warning); color: var(--warning); }
.status-badge.extracting { background: rgba(189, 0, 255, 0.15); border: 1px solid var(--secondary); color: #e499ff; } .status-badge.extracting { background: rgba(189, 0, 255, 0.15); border: 1px solid var(--secondary); color: #e499ff; }
.status-badge.upscaling { background: rgba(0, 240, 255, 0.1); border: 1px solid var(--primary); color: var(--primary); } .status-badge.upscaling { background: rgba(0, 240, 255, 0.1); border: 1px solid var(--primary); color: var(--primary); }
.status-badge.restoring_faces { background: rgba(255, 0, 255, 0.1); border: 1px solid var(--secondary); color: var(--secondary); }
.status-badge.interpolating { background: rgba(0, 255, 184, 0.1); border: 1px solid var(--primary); color: var(--primary); }
.status-badge.assembling { background: rgba(0, 240, 255, 0.1); border: 1px solid var(--primary); color: var(--primary); } .status-badge.assembling { background: rgba(0, 240, 255, 0.1); border: 1px solid var(--primary); color: var(--primary); }
.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); }
.bar-container { .bar-container {
width: 100%; width: 100%;
height: 16px; height: 16px;