MogensR commited on
Commit
66fefac
·
1 Parent(s): a70dcf0

Update ui_components.py

Browse files
Files changed (1) hide show
  1. 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 (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,
@@ -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 _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)],
@@ -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
- 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]
@@ -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
- 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
@@ -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", "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
 
@@ -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(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")
@@ -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
- # 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)
@@ -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
- _cb_use_gen_bg,
326
- inputs=[gen_path],
327
- outputs=[custom_bg],
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