MogensR commited on
Commit
dd49b64
·
1 Parent(s): d6e686b

Update ui_components.py

Browse files
Files changed (1) hide show
  1. ui_components.py +38 -277
ui_components.py CHANGED
@@ -1,188 +1,40 @@
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
- - 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
24
-
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,
34
- get_model_status,
35
- get_cache_status,
36
- PROCESS_CANCELLED,
37
  )
38
-
39
- # --------------------------
40
- # Helpers: file paths, io
41
- # --------------------------
42
-
43
- TMP_DIR = Path("/tmp/bgfx")
44
- TMP_DIR.mkdir(parents=True, exist_ok=True)
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)],
87
- "forest": [(46, 125, 50), (102, 187, 106), (165, 214, 167)],
88
- "ocean": [(33, 150, 243), (3, 169, 244), (0, 188, 212)],
89
- "minimal": [(245, 246, 248), (230, 232, 236), (214, 218, 224)],
90
- "warm": [(255, 224, 178), (255, 204, 128), (255, 171, 145)],
91
- "cool": [(197, 202, 233), (179, 229, 252), (178, 235, 242)],
92
- "royal": [(63, 81, 181), (121, 134, 203), (159, 168, 218)],
93
- }
94
-
95
- def _palette_from_prompt(prompt: str) -> List[tuple]:
96
- p = (prompt or "").lower()
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]
122
- mask1 = (noise >= thresholds[0]) & (noise < thresholds[1])
123
- mask2 = noise >= thresholds[1]
124
- img[mask0] = c0
125
- img[mask1] = c1
126
- img[mask2] = c2
127
- img = np.clip(img, 0, 255).astype(np.uint8)
128
- return Image.fromarray(img)
129
-
130
-
131
- def generate_ai_background(
132
- prompt: str,
133
- width: int = 1280,
134
- height: int = 720,
135
- bokeh: float = 0.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
149
- r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
150
- mask = 1 - np.clip(r / (max(width, height) / 1.2), 0, 1)
151
- mask = mask ** 2
152
- mask = (mask * (1 - vignette) + (1 - (1 - vignette))).astype(np.float32)
153
- base = np.array(img).astype(np.float32) / 255.0
154
- out = np.empty_like(base)
155
- for c in range(3):
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")
167
- return img, path
168
-
169
-
170
- # --------------------------
171
- # Gradio UI
172
- # --------------------------
173
 
174
  CSS = """
175
  :root { --radius: 16px; }
176
  .gradio-container { max-width: 1080px !important; margin: auto !important; }
177
  #hero .prose { font-size: 15px; }
178
- .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); }
 
 
179
  .footer-note { opacity: 0.7; font-size: 12px; }
180
  .sm { font-size: 13px; opacity: 0.85; }
181
  #statusbox { min-height: 120px; }
182
  """
183
 
184
  def create_interface() -> gr.Blocks:
185
- with gr.Blocks(title="🎬 BackgroundFX Pro", css=CSS, analytics_enabled=False, theme=gr.themes.Soft()) as demo:
 
 
186
  # ---------- HERO ----------
187
  with gr.Row(elem_id="hero"):
188
  gr.Markdown(
@@ -191,32 +43,24 @@ def create_interface() -> gr.Blocks:
191
  "Tip: **Load models** before processing for best results."
192
  )
193
 
 
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,10 +80,7 @@ def create_interface() -> gr.Blocks:
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")
@@ -264,106 +105,26 @@ def create_interface() -> gr.Blocks:
264
  cache_status = gr.JSON(label="Cache / System Status")
265
  gr.Markdown("<div class='footer-note'>If models fail to load, fallbacks keep the UI responsive. Check logs for details.</div>")
266
 
267
- # ---------- CALLBACKS ----------
268
- # Load Models
269
- def _cb_load_models() -> str:
270
- return load_models_with_validation()
271
-
272
- # Process
273
- def _cb_process(
274
- vid: str,
275
- style: str,
276
- custom_file: dict | None,
277
- use_two: bool,
278
- chroma: str,
279
- prev_mask: bool,
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,
290
- background_choice=style,
291
- custom_background_path=custom_path,
292
- progress_callback=None,
293
- use_two_stage=use_two,
294
- chroma_preset=chroma,
295
- preview_mask=prev_mask,
296
- preview_greenscreen=prev_green,
297
- )
298
-
299
- # Cancel processing
300
- def _cb_cancel() -> str:
301
- try:
302
- PROCESS_CANCELLED.set()
303
- return "Cancellation requested."
304
- except Exception as e:
305
- return f"Cancel failed: {e}"
306
-
307
- # Refresh status
308
- def _cb_status() -> Tuple[Dict[str, Any], Dict[str, Any]]:
309
- try:
310
- return get_model_status(), get_cache_status()
311
- except Exception as e:
312
- return {"error": str(e)}, {"error": str(e)}
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):
320
- img, path = generate_ai_background(prompt_text, width=int(w), height=int(h), bokeh=b, vignette=v, contrast=c)
321
- return img, path
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)
349
  btn_run.click(
350
- _cb_process,
351
  inputs=[video, bg_style, custom_bg, use_two_stage, chroma_preset, preview_mask, preview_greenscreen],
352
  outputs=[out_video, 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
 
1
  #!/usr/bin/env python3
2
  """
3
+ UI Components for BackgroundFX Pro
4
+ - Layout only (no heavy logic here)
5
+ - Delegates callbacks to ui/callbacks.py
 
 
 
 
 
 
 
 
 
6
  """
7
 
 
 
 
 
 
 
 
 
8
  import gradio as gr
9
+ from ui.callbacks import (
10
+ cb_load_models,
11
+ cb_process_video,
12
+ cb_cancel,
13
+ cb_status,
14
+ cb_clear,
15
+ cb_generate_bg,
16
+ cb_use_gen_bg,
17
+ cb_video_changed,
18
+ cb_custom_bg_preview,
 
19
  )
20
+ from utils.bg_generator import generate_ai_background
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  CSS = """
23
  :root { --radius: 16px; }
24
  .gradio-container { max-width: 1080px !important; margin: auto !important; }
25
  #hero .prose { font-size: 15px; }
26
+ .card { border-radius: var(--radius); border: 1px solid rgba(0,0,0,.08); padding: 16px;
27
+ background: linear-gradient(180deg, rgba(255,255,255,.9), rgba(248,250,252,.9));
28
+ box-shadow: 0 10px 30px rgba(0,0,0,.06); }
29
  .footer-note { opacity: 0.7; font-size: 12px; }
30
  .sm { font-size: 13px; opacity: 0.85; }
31
  #statusbox { min-height: 120px; }
32
  """
33
 
34
  def create_interface() -> gr.Blocks:
35
+ with gr.Blocks(title="🎬 BackgroundFX Pro", css=CSS, analytics_enabled=False,
36
+ theme=gr.themes.Soft()) as demo:
37
+
38
  # ---------- HERO ----------
39
  with gr.Row(elem_id="hero"):
40
  gr.Markdown(
 
43
  "Tip: **Load models** before processing for best results."
44
  )
45
 
46
+ # ---------- QUICK START ----------
47
  with gr.Tab("🏁 Quick Start"):
48
  with gr.Row():
49
  with gr.Column(scale=1):
 
50
  video = gr.Video(label="Upload Video")
 
 
 
51
  bg_style = gr.Dropdown(
52
  label="Background Style",
53
+ choices=["minimalist", "office", "studio", "ocean", "forest",
54
+ "sunset", "royal", "warm", "cool"],
 
 
 
 
 
 
55
  value="minimalist",
56
  )
57
  custom_bg = gr.File(label="Custom Background (Optional)", file_types=["image"])
 
58
 
59
  with gr.Accordion("Advanced", open=False):
60
  use_two_stage = gr.Checkbox(label="Use Two-Stage Pipeline", value=False)
61
+ chroma_preset = gr.Dropdown(
62
+ label="Chroma Preset", choices=["standard"], value="standard"
63
+ )
64
  preview_mask = gr.Checkbox(label="Preview Mask (no audio remix)", value=False)
65
  preview_greenscreen = gr.Checkbox(label="Preview Greenscreen (no audio remix)", value=False)
66
 
 
80
  with gr.Tab("🧠 AI Background (Lightweight)"):
81
  with gr.Row():
82
  with gr.Column(scale=1):
83
+ prompt = gr.Textbox(label="Describe vibe", value="modern office")
 
 
 
84
  with gr.Row():
85
  gen_width = gr.Slider(640, 1920, value=1280, step=10, label="Width")
86
  gen_height = gr.Slider(360, 1080, value=720, step=10, label="Height")
 
105
  cache_status = gr.JSON(label="Cache / System Status")
106
  gr.Markdown("<div class='footer-note'>If models fail to load, fallbacks keep the UI responsive. Check logs for details.</div>")
107
 
108
+ # ---------- CALLBACKS WIRING ----------
109
+ btn_load.click(cb_load_models, outputs=statusbox)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  btn_run.click(
111
+ cb_process_video,
112
  inputs=[video, bg_style, custom_bg, use_two_stage, chroma_preset, preview_mask, preview_greenscreen],
113
  outputs=[out_video, statusbox],
114
  )
115
+ btn_cancel.click(cb_cancel, outputs=statusbox)
116
+ btn_refresh.click(cb_status, outputs=[model_status, cache_status])
117
+ btn_clear.click(cb_clear, outputs=[out_video, statusbox, gen_preview, gen_path])
118
 
119
  btn_gen_bg.click(
120
+ cb_generate_bg,
121
  inputs=[prompt, gen_width, gen_height, bokeh, vignette, contrast],
122
  outputs=[gen_preview, gen_path],
123
  )
124
+ use_gen_as_custom.click(cb_use_gen_bg, inputs=[gen_path], outputs=[custom_bg])
125
 
126
+ # Previews
127
+ video.change(cb_video_changed, inputs=[video], outputs=[]) # can later output a preview img
128
+ custom_bg.change(cb_custom_bg_preview, inputs=[custom_bg], outputs=[])
129
 
130
  return demo