Update ui_components.py
Browse files- ui_components.py +318 -219
ui_components.py
CHANGED
|
@@ -1,231 +1,330 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
UI Components for BackgroundFX Pro
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
import time
|
| 8 |
-
import
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
if
|
| 69 |
-
return
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
# Call the model loading function with progress callback
|
| 122 |
-
result = load_models_with_validation(progress)
|
| 123 |
-
return result
|
| 124 |
-
except Exception as e:
|
| 125 |
-
return f"Model loading failed: {str(e)}"
|
| 126 |
-
|
| 127 |
-
# --- UI setup ---
|
| 128 |
-
with gr.Blocks(
|
| 129 |
-
title="BackgroundFX Pro",
|
| 130 |
-
css="""
|
| 131 |
-
.main-header { text-align: center; margin-bottom: 20px; }
|
| 132 |
-
.status-box { background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0; }
|
| 133 |
-
.live-progress { font-size: 1.1em; font-weight: bold; color: #004085; }
|
| 134 |
-
"""
|
| 135 |
-
) as demo:
|
| 136 |
-
with gr.Row():
|
| 137 |
-
gr.Markdown("# BackgroundFX Pro - Video Background Replacement", elem_classes=["main-header"])
|
| 138 |
-
|
| 139 |
-
# --- Main UI fields ---
|
| 140 |
-
with gr.Row():
|
| 141 |
-
with gr.Column():
|
| 142 |
-
video_input = gr.Video(label="Upload Video", height=280)
|
| 143 |
-
background_method = gr.Radio(
|
| 144 |
-
["professional", "upload"], value="professional", label="Background Method"
|
| 145 |
-
)
|
| 146 |
-
professional_choice = gr.Dropdown(
|
| 147 |
-
choices=[(bg['name'], key) for key, bg in PROFESSIONAL_BACKGROUNDS.items()],
|
| 148 |
-
value=list(PROFESSIONAL_BACKGROUNDS.keys())[0],
|
| 149 |
-
label="Professional Background"
|
| 150 |
-
)
|
| 151 |
-
custom_background = gr.Image(label="Upload Background", type="filepath", visible=False)
|
| 152 |
-
quality_preset = gr.Dropdown(
|
| 153 |
-
choices=[
|
| 154 |
-
("Fast", "fast"),
|
| 155 |
-
("Balanced", "balanced"),
|
| 156 |
-
("High Quality", "high")
|
| 157 |
-
],
|
| 158 |
-
value="balanced",
|
| 159 |
-
label="Quality Preset"
|
| 160 |
-
)
|
| 161 |
-
use_two_stage = gr.Checkbox("Enable Two-Stage Mode", value=False)
|
| 162 |
-
chroma_preset = gr.Dropdown(
|
| 163 |
-
choices=[("Standard", "standard"), ("Studio", "studio"), ("Outdoor", "outdoor")],
|
| 164 |
-
value="standard",
|
| 165 |
-
label="Chroma Key Preset"
|
| 166 |
-
)
|
| 167 |
-
# Dynamic show/hide
|
| 168 |
-
def update_background_visibility(method):
|
| 169 |
-
return (
|
| 170 |
-
gr.update(visible=(method == "professional")),
|
| 171 |
-
gr.update(visible=(method == "upload"))
|
| 172 |
)
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
)
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
outputs=[
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
)
|
| 227 |
|
| 228 |
return demo
|
| 229 |
-
|
| 230 |
-
def create_ui():
|
| 231 |
-
return create_interface()
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
UI Components for BackgroundFX Pro (Hugging Face Spaces, CSP-safe)
|
| 4 |
+
|
| 5 |
+
- Clean, modern layout with tabs
|
| 6 |
+
- Keeps existing functionality:
|
| 7 |
+
* Load models
|
| 8 |
+
* Process video (single-stage / two-stage switch, previews, etc.)
|
| 9 |
+
* Status panel
|
| 10 |
+
- Adds lightweight "AI Background" generator (procedural, no heavy deps)
|
| 11 |
"""
|
| 12 |
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import io
|
| 17 |
import time
|
| 18 |
+
import math
|
| 19 |
+
import random
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Optional, Tuple, Dict, Any, List
|
| 22 |
+
|
| 23 |
+
import gradio as gr
|
| 24 |
+
from PIL import Image, ImageFilter, ImageOps
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
# Import core wrappers (these are defined in core/app.py)
|
| 28 |
+
# NOTE: core/app.py imports ui_components only *inside* main(), so this wonโt create a circular import.
|
| 29 |
+
from core.app import (
|
| 30 |
+
load_models_with_validation,
|
| 31 |
+
process_video_fixed,
|
| 32 |
+
get_model_status,
|
| 33 |
+
get_cache_status,
|
| 34 |
+
PROCESS_CANCELLED,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# --------------------------
|
| 38 |
+
# Helpers: file paths, io
|
| 39 |
+
# --------------------------
|
| 40 |
+
|
| 41 |
+
TMP_DIR = Path("/tmp/bgfx")
|
| 42 |
+
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _save_pil(img: Image.Image, stem: str = "gen_bg", ext: str = "png") -> str:
|
| 46 |
+
"""Save a PIL image into /tmp and return its path."""
|
| 47 |
+
ts = int(time.time() * 1000)
|
| 48 |
+
p = TMP_DIR / f"{stem}_{ts}.{ext}"
|
| 49 |
+
img.save(p)
|
| 50 |
+
return str(p)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _image_to_np(img: Image.Image) -> np.ndarray:
|
| 54 |
+
return np.array(img)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# --------------------------
|
| 58 |
+
# Lightweight "AI" background generator
|
| 59 |
+
# (procedural: palette from prompt + Perlin-ish noise + bokeh blur)
|
| 60 |
+
# --------------------------
|
| 61 |
+
|
| 62 |
+
_PALETTES = {
|
| 63 |
+
# very light keyword mapping; expand anytime
|
| 64 |
+
"office": [(240, 245, 250), (210, 220, 230), (180, 190, 200)],
|
| 65 |
+
"studio": [(18, 18, 20), (32, 32, 36), (58, 60, 64)],
|
| 66 |
+
"sunset": [(255, 183, 77), (255, 138, 101), (244, 143, 177)],
|
| 67 |
+
"forest": [(46, 125, 50), (102, 187, 106), (165, 214, 167)],
|
| 68 |
+
"ocean": [(33, 150, 243), (3, 169, 244), (0, 188, 212)],
|
| 69 |
+
"minimal": [(245, 246, 248), (230, 232, 236), (214, 218, 224)],
|
| 70 |
+
"warm": [(255, 224, 178), (255, 204, 128), (255, 171, 145)],
|
| 71 |
+
"cool": [(197, 202, 233), (179, 229, 252), (178, 235, 242)],
|
| 72 |
+
"royal": [(63, 81, 181), (121, 134, 203), (159, 168, 218)],
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def _palette_from_prompt(prompt: str) -> List[tuple]:
|
| 76 |
+
p = (prompt or "").lower()
|
| 77 |
+
for key, pal in _PALETTES.items():
|
| 78 |
+
if key in p:
|
| 79 |
+
return pal
|
| 80 |
+
# fallback: hash to palette
|
| 81 |
+
random.seed(hash(p) % (2**32 - 1))
|
| 82 |
+
return [tuple(random.randint(90, 200) for _ in range(3)) for _ in range(3)]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _perlin_like_noise(h: int, w: int, octaves: int = 4) -> np.ndarray:
|
| 86 |
+
"""Fast fake-perlin using summed blurred noise."""
|
| 87 |
+
acc = np.zeros((h, w), dtype=np.float32)
|
| 88 |
+
for o in range(octaves):
|
| 89 |
+
scale = 2 ** o
|
| 90 |
+
noise = np.random.rand(h // scale + 1, w // scale + 1).astype(np.float32)
|
| 91 |
+
noise = Image.fromarray((noise * 255).astype(np.uint8)).resize((w, h), Image.BILINEAR)
|
| 92 |
+
noise = np.array(noise).astype(np.float32) / 255.0
|
| 93 |
+
acc += noise / (o + 1)
|
| 94 |
+
acc = acc / acc.max()
|
| 95 |
+
return acc
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _blend_palette(noise: np.ndarray, palette: List[tuple]) -> Image.Image:
|
| 99 |
+
"""Map grayscale noise to a 3-color gradient."""
|
| 100 |
+
h, w = noise.shape
|
| 101 |
+
img = np.zeros((h, w, 3), dtype=np.float32)
|
| 102 |
+
# Tri-color mapping
|
| 103 |
+
thresholds = [0.33, 0.66]
|
| 104 |
+
c0, c1, c2 = [np.array(c, dtype=np.float32) for c in palette]
|
| 105 |
+
mask0 = noise < thresholds[0]
|
| 106 |
+
mask1 = (noise >= thresholds[0]) & (noise < thresholds[1])
|
| 107 |
+
mask2 = noise >= thresholds[1]
|
| 108 |
+
img[mask0] = c0
|
| 109 |
+
img[mask1] = c1
|
| 110 |
+
img[mask2] = c2
|
| 111 |
+
img = np.clip(img, 0, 255).astype(np.uint8)
|
| 112 |
+
return Image.fromarray(img)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def generate_ai_background(
|
| 116 |
+
prompt: str,
|
| 117 |
+
width: int = 1280,
|
| 118 |
+
height: int = 720,
|
| 119 |
+
bokeh: float = 0.0,
|
| 120 |
+
vignette: float = 0.15,
|
| 121 |
+
contrast: float = 1.05,
|
| 122 |
+
) -> Tuple[Image.Image, str]:
|
| 123 |
+
"""Procedural 'AI-ish' background for fast, dependency-free generation."""
|
| 124 |
+
palette = _palette_from_prompt(prompt)
|
| 125 |
+
noise = _perlin_like_noise(height, width, octaves=4)
|
| 126 |
+
img = _blend_palette(noise, palette)
|
| 127 |
+
|
| 128 |
+
# Subtle blur / bokeh
|
| 129 |
+
if bokeh > 0:
|
| 130 |
+
radius = max(0, min(50, bokeh))
|
| 131 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=radius))
|
| 132 |
+
|
| 133 |
+
# Vignette
|
| 134 |
+
if vignette > 0:
|
| 135 |
+
y, x = np.ogrid[:height, :width]
|
| 136 |
+
cx, cy = width / 2, height / 2
|
| 137 |
+
r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
|
| 138 |
+
mask = 1 - np.clip(r / (max(width, height) / 1.2), 0, 1)
|
| 139 |
+
mask = mask ** 2
|
| 140 |
+
mask = (mask * (1 - vignette) + (1 - (1 - vignette))).astype(np.float32)
|
| 141 |
+
base = np.array(img).astype(np.float32) / 255.0
|
| 142 |
+
out = np.empty_like(base)
|
| 143 |
+
for c in range(3):
|
| 144 |
+
out[..., c] = base[..., c] * mask
|
| 145 |
+
img = Image.fromarray(np.clip(out * 255, 0, 255).astype(np.uint8))
|
| 146 |
+
|
| 147 |
+
# Simple contrast
|
| 148 |
+
if contrast != 1.0:
|
| 149 |
+
img = ImageOps.autocontrast(img, cutoff=1)
|
| 150 |
+
arr = np.array(img).astype(np.float32)
|
| 151 |
+
mean = arr.mean(axis=(0, 1), keepdims=True)
|
| 152 |
+
arr = (arr - mean) * contrast + mean
|
| 153 |
+
img = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
| 154 |
+
|
| 155 |
+
path = _save_pil(img, stem="ai_bg", ext="png")
|
| 156 |
+
return img, path
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# --------------------------
|
| 160 |
+
# Gradio UI
|
| 161 |
+
# --------------------------
|
| 162 |
+
|
| 163 |
+
CSS = """
|
| 164 |
+
:root { --radius: 16px; }
|
| 165 |
+
.gradio-container { max-width: 1080px !important; margin: auto !important; }
|
| 166 |
+
#hero .prose { font-size: 15px; }
|
| 167 |
+
.card { border-radius: var(--radius); border: 1px solid rgba(0,0,0,.08); padding: 16px; background: linear-gradient(180deg, rgba(255,255,255,.9), rgba(248,250,252,.9)); box-shadow: 0 10px 30px rgba(0,0,0,.06); }
|
| 168 |
+
.footer-note { opacity: 0.7; font-size: 12px; }
|
| 169 |
+
.sm { font-size: 13px; opacity: 0.85; }
|
| 170 |
+
#statusbox { min-height: 120px; }
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
def create_interface() -> gr.Blocks:
|
| 174 |
+
with gr.Blocks(title="๐ฌ BackgroundFX Pro", css=CSS, analytics_enabled=False, theme=gr.themes.Soft()) as demo:
|
| 175 |
+
# ---------- HERO ----------
|
| 176 |
+
with gr.Row(elem_id="hero"):
|
| 177 |
+
gr.Markdown(
|
| 178 |
+
"## ๐ฌ BackgroundFX Pro\n"
|
| 179 |
+
"Polished matting & background replacement for video. Runs on Hugging Face Spaces.\n"
|
| 180 |
+
"Tip: **Load models** before processing for best results."
|
| 181 |
)
|
| 182 |
+
|
| 183 |
+
with gr.Tab("๐ Quick Start"):
|
| 184 |
+
with gr.Row():
|
| 185 |
+
with gr.Column(scale=1):
|
| 186 |
+
video = gr.Video(label="Upload Video")
|
| 187 |
+
bg_style = gr.Dropdown(
|
| 188 |
+
label="Background Style",
|
| 189 |
+
choices=[
|
| 190 |
+
"minimalist", "office", "studio", "ocean", "forest", "sunset",
|
| 191 |
+
"royal", "warm", "cool"
|
| 192 |
+
],
|
| 193 |
+
value="minimalist",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
)
|
| 195 |
+
custom_bg = gr.File(label="Custom Background (Optional)", file_types=["image"])
|
| 196 |
+
|
| 197 |
+
with gr.Accordion("Advanced", open=False):
|
| 198 |
+
use_two_stage = gr.Checkbox(label="Use Two-Stage Pipeline", value=False)
|
| 199 |
+
chroma_preset = gr.Dropdown(
|
| 200 |
+
label="Chroma Preset", choices=["standard"], value="standard"
|
| 201 |
+
)
|
| 202 |
+
preview_mask = gr.Checkbox(label="Preview Mask (no audio remix)", value=False)
|
| 203 |
+
preview_greenscreen = gr.Checkbox(label="Preview Greenscreen (no audio remix)", value=False)
|
| 204 |
+
|
| 205 |
+
with gr.Row():
|
| 206 |
+
btn_load = gr.Button("๐ Load Models", variant="secondary")
|
| 207 |
+
btn_run = gr.Button("๐ฌ Process Video", variant="primary")
|
| 208 |
+
btn_cancel = gr.Button("โน๏ธ Cancel", variant="secondary")
|
| 209 |
+
|
| 210 |
+
with gr.Column(scale=1):
|
| 211 |
+
out_video = gr.Video(label="Processed Output", interactive=False)
|
| 212 |
+
statusbox = gr.Textbox(label="Status", lines=8, elem_id="statusbox")
|
| 213 |
+
with gr.Row():
|
| 214 |
+
btn_refresh = gr.Button("๐ Refresh Status", variant="secondary")
|
| 215 |
+
btn_clear = gr.Button("๐งน Clear", variant="secondary")
|
| 216 |
+
|
| 217 |
+
# ---------- AI BACKGROUND ----------
|
| 218 |
+
with gr.Tab("๐ง AI Background (Lightweight)"):
|
| 219 |
+
with gr.Row():
|
| 220 |
+
with gr.Column(scale=1):
|
| 221 |
+
prompt = gr.Textbox(label="Describe the vibe (e.g., 'modern office', 'soft sunset studio')", value="modern office")
|
| 222 |
+
with gr.Row():
|
| 223 |
+
gen_width = gr.Slider(640, 1920, value=1280, step=10, label="Width")
|
| 224 |
+
gen_height = gr.Slider(360, 1080, value=720, step=10, label="Height")
|
| 225 |
+
with gr.Row():
|
| 226 |
+
bokeh = gr.Slider(0, 30, value=8, step=1, label="Bokeh Blur")
|
| 227 |
+
vignette = gr.Slider(0.0, 0.6, value=0.15, step=0.01, label="Vignette")
|
| 228 |
+
contrast = gr.Slider(0.8, 1.4, value=1.05, step=0.01, label="Contrast")
|
| 229 |
+
|
| 230 |
+
btn_gen_bg = gr.Button("โจ Generate Background", variant="primary")
|
| 231 |
+
|
| 232 |
+
with gr.Column(scale=1):
|
| 233 |
+
gen_preview = gr.Image(label="Generated Background", interactive=False)
|
| 234 |
+
gen_path = gr.Textbox(label="Saved Path", interactive=False)
|
| 235 |
+
use_gen_as_custom = gr.Button("๐ Use As Custom Background", variant="secondary")
|
| 236 |
+
|
| 237 |
+
# ---------- STATUS ----------
|
| 238 |
+
with gr.Tab("๐ Status & Settings"):
|
| 239 |
+
with gr.Row():
|
| 240 |
+
with gr.Column(scale=1, elem_classes=["card"]):
|
| 241 |
+
model_status = gr.JSON(label="Model Status")
|
| 242 |
+
with gr.Column(scale=1, elem_classes=["card"]):
|
| 243 |
+
cache_status = gr.JSON(label="Cache / System Status")
|
| 244 |
+
gr.Markdown("<div class='footer-note'>If models fail to load, fallbacks keep the UI responsive. Check logs for details.</div>")
|
| 245 |
+
|
| 246 |
+
# ---------- CALLBACKS ----------
|
| 247 |
+
# Load Models
|
| 248 |
+
def _cb_load_models() -> str:
|
| 249 |
+
return load_models_with_validation()
|
| 250 |
+
|
| 251 |
+
# Process
|
| 252 |
+
def _cb_process(
|
| 253 |
+
vid: str,
|
| 254 |
+
style: str,
|
| 255 |
+
custom_file: dict | None,
|
| 256 |
+
use_two: bool,
|
| 257 |
+
chroma: str,
|
| 258 |
+
prev_mask: bool,
|
| 259 |
+
prev_green: bool,
|
| 260 |
+
):
|
| 261 |
+
if PROCESS_CANCELLED.is_set():
|
| 262 |
+
# if user cancelled previously, reset it so a new run can proceed
|
| 263 |
+
PROCESS_CANCELLED.clear()
|
| 264 |
+
custom_path = None
|
| 265 |
+
if isinstance(custom_file, dict) and custom_file.get("name"):
|
| 266 |
+
custom_path = custom_file["name"]
|
| 267 |
+
return process_video_fixed(
|
| 268 |
+
video_path=vid,
|
| 269 |
+
background_choice=style,
|
| 270 |
+
custom_background_path=custom_path,
|
| 271 |
+
progress_callback=None,
|
| 272 |
+
use_two_stage=use_two,
|
| 273 |
+
chroma_preset=chroma,
|
| 274 |
+
preview_mask=prev_mask,
|
| 275 |
+
preview_greenscreen=prev_green,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# Cancel processing
|
| 279 |
+
def _cb_cancel() -> str:
|
| 280 |
+
try:
|
| 281 |
+
PROCESS_CANCELLED.set()
|
| 282 |
+
return "Cancellation requested."
|
| 283 |
+
except Exception as e:
|
| 284 |
+
return f"Cancel failed: {e}"
|
| 285 |
+
|
| 286 |
+
# Refresh status
|
| 287 |
+
def _cb_status() -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
| 288 |
+
try:
|
| 289 |
+
return get_model_status(), get_cache_status()
|
| 290 |
+
except Exception as e:
|
| 291 |
+
return {"error": str(e)}, {"error": str(e)}
|
| 292 |
+
|
| 293 |
+
# Clear
|
| 294 |
+
def _cb_clear():
|
| 295 |
+
return None, "", None, ""
|
| 296 |
+
|
| 297 |
+
# AI background generation
|
| 298 |
+
def _cb_generate_bg(prompt_text: str, w: int, h: int, b: float, v: float, c: float):
|
| 299 |
+
img, path = generate_ai_background(prompt_text, width=int(w), height=int(h), bokeh=b, vignette=v, contrast=c)
|
| 300 |
+
return img, path
|
| 301 |
+
|
| 302 |
+
# Use AI gen as custom
|
| 303 |
+
def _cb_use_gen_bg(path_text: str):
|
| 304 |
+
# The Quick Start tab expects a "file" object. We can simply echo the path
|
| 305 |
+
# and let the "Process" callback read it if provided.
|
| 306 |
+
return {"name": path_text, "size": os.path.getsize(path_text)} if path_text and os.path.exists(path_text) else None
|
| 307 |
+
|
| 308 |
+
# Wire events
|
| 309 |
+
btn_load.click(_cb_load_models, outputs=statusbox)
|
| 310 |
+
btn_run.click(
|
| 311 |
+
_cb_process,
|
| 312 |
+
inputs=[video, bg_style, custom_bg, use_two_stage, chroma_preset, preview_mask, preview_greenscreen],
|
| 313 |
+
outputs=[out_video, statusbox],
|
| 314 |
)
|
| 315 |
+
btn_cancel.click(_cb_cancel, outputs=statusbox)
|
| 316 |
+
btn_refresh.click(_cb_status, outputs=[model_status, cache_status])
|
| 317 |
+
btn_clear.click(_cb_clear, outputs=[out_video, statusbox, gen_preview, gen_path])
|
| 318 |
|
| 319 |
+
btn_gen_bg.click(
|
| 320 |
+
_cb_generate_bg,
|
| 321 |
+
inputs=[prompt, gen_width, gen_height, bokeh, vignette, contrast],
|
| 322 |
+
outputs=[gen_preview, gen_path],
|
| 323 |
+
)
|
| 324 |
+
use_gen_as_custom.click(
|
| 325 |
+
_cb_use_gen_bg,
|
| 326 |
+
inputs=[gen_path],
|
| 327 |
+
outputs=[custom_bg],
|
| 328 |
)
|
| 329 |
|
| 330 |
return demo
|
|
|
|
|
|
|
|
|