Update ui_components.py
Browse files- ui_components.py +81 -42
ui_components.py
CHANGED
|
@@ -8,14 +8,16 @@
|
|
| 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
|
|
@@ -23,9 +25,9 @@
|
|
| 23 |
import gradio as gr
|
| 24 |
from PIL import Image, ImageFilter, ImageOps
|
| 25 |
import numpy as np
|
|
|
|
| 26 |
|
| 27 |
-
# Import core wrappers (
|
| 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,
|
|
@@ -43,24 +45,42 @@
|
|
| 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
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)],
|
|
@@ -77,29 +97,25 @@ def _palette_from_prompt(prompt: str) -> List[tuple]:
|
|
| 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 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
acc +=
|
| 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]
|
|
@@ -120,17 +136,13 @@ def generate_ai_background(
|
|
| 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 |
-
|
| 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
|
|
@@ -144,12 +156,11 @@ def generate_ai_background(
|
|
| 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")
|
|
@@ -183,22 +194,29 @@ def create_interface() -> gr.Blocks:
|
|
| 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",
|
| 191 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -218,7 +236,10 @@ def create_interface() -> gr.Blocks:
|
|
| 218 |
with gr.Tab("🧠 AI Background (Lightweight)"):
|
| 219 |
with gr.Row():
|
| 220 |
with gr.Column(scale=1):
|
| 221 |
-
prompt = gr.Textbox(
|
|
|
|
|
|
|
|
|
|
| 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")
|
|
@@ -259,10 +280,10 @@ def _cb_process(
|
|
| 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,
|
|
@@ -292,7 +313,7 @@ def _cb_status() -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
| 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):
|
|
@@ -301,9 +322,27 @@ def _cb_generate_bg(prompt_text: str, w: int, h: int, b: float, v: float, c: flo
|
|
| 301 |
|
| 302 |
# Use AI gen as custom
|
| 303 |
def _cb_use_gen_bg(path_text: str):
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
# Wire events
|
| 309 |
btn_load.click(_cb_load_models, outputs=statusbox)
|
|
@@ -314,17 +353,17 @@ def _cb_use_gen_bg(path_text: str):
|
|
| 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 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
)
|
| 329 |
|
| 330 |
return demo
|
|
|
|
| 8 |
* Process video (single-stage / two-stage switch, previews, etc.)
|
| 9 |
* Status panel
|
| 10 |
- Adds lightweight "AI Background" generator (procedural, no heavy deps)
|
| 11 |
+
- NEW:
|
| 12 |
+
* Preview of uploaded custom background
|
| 13 |
+
* Preview of the video's first frame when a video is uploaded
|
| 14 |
+
* Background style keys aligned with utils.cv_processing.PROFESSIONAL_BACKGROUNDS
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
import os
|
|
|
|
| 20 |
import time
|
|
|
|
| 21 |
import random
|
| 22 |
from pathlib import Path
|
| 23 |
from typing import Optional, Tuple, Dict, Any, List
|
|
|
|
| 25 |
import gradio as gr
|
| 26 |
from PIL import Image, ImageFilter, ImageOps
|
| 27 |
import numpy as np
|
| 28 |
+
import cv2
|
| 29 |
|
| 30 |
+
# Import core wrappers (core/app.py only imports UI from inside main(), no circular import)
|
|
|
|
| 31 |
from core.app import (
|
| 32 |
load_models_with_validation,
|
| 33 |
process_video_fixed,
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def _save_pil(img: Image.Image, stem: str = "gen_bg", ext: str = "png") -> str:
|
|
|
|
| 48 |
ts = int(time.time() * 1000)
|
| 49 |
p = TMP_DIR / f"{stem}_{ts}.{ext}"
|
| 50 |
img.save(p)
|
| 51 |
return str(p)
|
| 52 |
|
| 53 |
|
| 54 |
+
def _pil_from_path(path: str) -> Optional[Image.Image]:
|
| 55 |
+
try:
|
| 56 |
+
return Image.open(path).convert("RGB")
|
| 57 |
+
except Exception:
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _first_frame(path: str, max_side: int = 960) -> Optional[Image.Image]:
|
| 62 |
+
"""Extract the first frame of a video for preview."""
|
| 63 |
+
try:
|
| 64 |
+
cap = cv2.VideoCapture(path)
|
| 65 |
+
ok, frame = cap.read()
|
| 66 |
+
cap.release()
|
| 67 |
+
if not ok or frame is None:
|
| 68 |
+
return None
|
| 69 |
+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 70 |
+
h, w = frame.shape[:2]
|
| 71 |
+
scale = min(1.0, max_side / max(h, w))
|
| 72 |
+
if scale < 1.0:
|
| 73 |
+
frame = cv2.resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
|
| 74 |
+
return Image.fromarray(frame)
|
| 75 |
+
except Exception:
|
| 76 |
+
return None
|
| 77 |
|
| 78 |
|
| 79 |
# --------------------------
|
| 80 |
# Lightweight "AI" background generator
|
|
|
|
| 81 |
# --------------------------
|
| 82 |
|
| 83 |
_PALETTES = {
|
|
|
|
| 84 |
"office": [(240, 245, 250), (210, 220, 230), (180, 190, 200)],
|
| 85 |
"studio": [(18, 18, 20), (32, 32, 36), (58, 60, 64)],
|
| 86 |
"sunset": [(255, 183, 77), (255, 138, 101), (244, 143, 177)],
|
|
|
|
| 97 |
for key, pal in _PALETTES.items():
|
| 98 |
if key in p:
|
| 99 |
return pal
|
|
|
|
| 100 |
random.seed(hash(p) % (2**32 - 1))
|
| 101 |
return [tuple(random.randint(90, 200) for _ in range(3)) for _ in range(3)]
|
| 102 |
|
| 103 |
|
| 104 |
def _perlin_like_noise(h: int, w: int, octaves: int = 4) -> np.ndarray:
|
|
|
|
| 105 |
acc = np.zeros((h, w), dtype=np.float32)
|
| 106 |
for o in range(octaves):
|
| 107 |
scale = 2 ** o
|
| 108 |
+
small = np.random.rand(h // scale + 1, w // scale + 1).astype(np.float32)
|
| 109 |
+
small = Image.fromarray((small * 255).astype(np.uint8)).resize((w, h), Image.BILINEAR)
|
| 110 |
+
arr = np.array(small).astype(np.float32) / 255.0
|
| 111 |
+
acc += arr / (o + 1)
|
| 112 |
+
acc = acc / max(1e-6, acc.max())
|
| 113 |
return acc
|
| 114 |
|
| 115 |
|
| 116 |
def _blend_palette(noise: np.ndarray, palette: List[tuple]) -> Image.Image:
|
|
|
|
| 117 |
h, w = noise.shape
|
| 118 |
img = np.zeros((h, w, 3), dtype=np.float32)
|
|
|
|
| 119 |
thresholds = [0.33, 0.66]
|
| 120 |
c0, c1, c2 = [np.array(c, dtype=np.float32) for c in palette]
|
| 121 |
mask0 = noise < thresholds[0]
|
|
|
|
| 136 |
vignette: float = 0.15,
|
| 137 |
contrast: float = 1.05,
|
| 138 |
) -> Tuple[Image.Image, str]:
|
|
|
|
| 139 |
palette = _palette_from_prompt(prompt)
|
| 140 |
noise = _perlin_like_noise(height, width, octaves=4)
|
| 141 |
img = _blend_palette(noise, palette)
|
| 142 |
|
|
|
|
| 143 |
if bokeh > 0:
|
| 144 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=max(0, min(50, bokeh))))
|
|
|
|
| 145 |
|
|
|
|
| 146 |
if vignette > 0:
|
| 147 |
y, x = np.ogrid[:height, :width]
|
| 148 |
cx, cy = width / 2, height / 2
|
|
|
|
| 156 |
out[..., c] = base[..., c] * mask
|
| 157 |
img = Image.fromarray(np.clip(out * 255, 0, 255).astype(np.uint8))
|
| 158 |
|
|
|
|
| 159 |
if contrast != 1.0:
|
| 160 |
img = ImageOps.autocontrast(img, cutoff=1)
|
| 161 |
arr = np.array(img).astype(np.float32)
|
| 162 |
mean = arr.mean(axis=(0, 1), keepdims=True)
|
| 163 |
+
arr = (arr - mean) * float(contrast) + mean
|
| 164 |
img = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
| 165 |
|
| 166 |
path = _save_pil(img, stem="ai_bg", ext="png")
|
|
|
|
| 194 |
with gr.Tab("🏁 Quick Start"):
|
| 195 |
with gr.Row():
|
| 196 |
with gr.Column(scale=1):
|
| 197 |
+
# Inputs
|
| 198 |
video = gr.Video(label="Upload Video")
|
| 199 |
+
video_preview = gr.Image(label="Video First Frame (Preview)", interactive=False)
|
| 200 |
+
|
| 201 |
+
# Align keys with utils.cv_processing.PROFESSIONAL_BACKGROUNDS
|
| 202 |
bg_style = gr.Dropdown(
|
| 203 |
label="Background Style",
|
| 204 |
choices=[
|
| 205 |
+
"minimalist",
|
| 206 |
+
"office_modern",
|
| 207 |
+
"studio_blue",
|
| 208 |
+
"studio_green",
|
| 209 |
+
"warm_gradient",
|
| 210 |
+
"tech_dark",
|
| 211 |
],
|
| 212 |
value="minimalist",
|
| 213 |
)
|
| 214 |
custom_bg = gr.File(label="Custom Background (Optional)", file_types=["image"])
|
| 215 |
+
custom_bg_preview = gr.Image(label="Custom Background Preview", interactive=False)
|
| 216 |
|
| 217 |
with gr.Accordion("Advanced", open=False):
|
| 218 |
use_two_stage = gr.Checkbox(label="Use Two-Stage Pipeline", value=False)
|
| 219 |
+
chroma_preset = gr.Dropdown(label="Chroma Preset", choices=["standard"], value="standard")
|
|
|
|
|
|
|
| 220 |
preview_mask = gr.Checkbox(label="Preview Mask (no audio remix)", value=False)
|
| 221 |
preview_greenscreen = gr.Checkbox(label="Preview Greenscreen (no audio remix)", value=False)
|
| 222 |
|
|
|
|
| 236 |
with gr.Tab("🧠 AI Background (Lightweight)"):
|
| 237 |
with gr.Row():
|
| 238 |
with gr.Column(scale=1):
|
| 239 |
+
prompt = gr.Textbox(
|
| 240 |
+
label="Describe the vibe (e.g., 'modern office', 'soft sunset studio')",
|
| 241 |
+
value="modern office"
|
| 242 |
+
)
|
| 243 |
with gr.Row():
|
| 244 |
gen_width = gr.Slider(640, 1920, value=1280, step=10, label="Width")
|
| 245 |
gen_height = gr.Slider(360, 1080, value=720, step=10, label="Height")
|
|
|
|
| 280 |
prev_green: bool,
|
| 281 |
):
|
| 282 |
if PROCESS_CANCELLED.is_set():
|
|
|
|
| 283 |
PROCESS_CANCELLED.clear()
|
| 284 |
custom_path = None
|
| 285 |
if isinstance(custom_file, dict) and custom_file.get("name"):
|
| 286 |
+
# Gradio passes {"name": "/tmp/...", "size": int, ...}
|
| 287 |
custom_path = custom_file["name"]
|
| 288 |
return process_video_fixed(
|
| 289 |
video_path=vid,
|
|
|
|
| 313 |
|
| 314 |
# Clear
|
| 315 |
def _cb_clear():
|
| 316 |
+
return None, "", None, "", None
|
| 317 |
|
| 318 |
# AI background generation
|
| 319 |
def _cb_generate_bg(prompt_text: str, w: int, h: int, b: float, v: float, c: float):
|
|
|
|
| 322 |
|
| 323 |
# Use AI gen as custom
|
| 324 |
def _cb_use_gen_bg(path_text: str):
|
| 325 |
+
return (
|
| 326 |
+
{"name": path_text, "size": os.path.getsize(path_text)}
|
| 327 |
+
if path_text and os.path.exists(path_text) else None
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# Video change -> extract first frame
|
| 331 |
+
def _cb_video_changed(vid_path: str):
|
| 332 |
+
if not vid_path:
|
| 333 |
+
return None
|
| 334 |
+
img = _first_frame(vid_path)
|
| 335 |
+
return img
|
| 336 |
+
|
| 337 |
+
# Custom background change -> preview image
|
| 338 |
+
def _cb_custom_bg_preview(file_obj: dict | None):
|
| 339 |
+
try:
|
| 340 |
+
if isinstance(file_obj, dict) and file_obj.get("name") and os.path.exists(file_obj["name"]):
|
| 341 |
+
pil = _pil_from_path(file_obj["name"])
|
| 342 |
+
return pil
|
| 343 |
+
except Exception:
|
| 344 |
+
pass
|
| 345 |
+
return None
|
| 346 |
|
| 347 |
# Wire events
|
| 348 |
btn_load.click(_cb_load_models, outputs=statusbox)
|
|
|
|
| 353 |
)
|
| 354 |
btn_cancel.click(_cb_cancel, outputs=statusbox)
|
| 355 |
btn_refresh.click(_cb_status, outputs=[model_status, cache_status])
|
| 356 |
+
btn_clear.click(_cb_clear, outputs=[out_video, statusbox, gen_preview, gen_path, custom_bg_preview])
|
| 357 |
|
| 358 |
btn_gen_bg.click(
|
| 359 |
_cb_generate_bg,
|
| 360 |
inputs=[prompt, gen_width, gen_height, bokeh, vignette, contrast],
|
| 361 |
outputs=[gen_preview, gen_path],
|
| 362 |
)
|
| 363 |
+
use_gen_as_custom.click(_cb_use_gen_bg, inputs=[gen_path], outputs=[custom_bg])
|
| 364 |
+
|
| 365 |
+
# Live previews
|
| 366 |
+
video.change(_cb_video_changed, inputs=[video], outputs=[video_preview])
|
| 367 |
+
custom_bg.change(_cb_custom_bg_preview, inputs=[custom_bg], outputs=[custom_bg_preview])
|
| 368 |
|
| 369 |
return demo
|