MogensR commited on
Commit
8ca115b
·
1 Parent(s): cfb2174

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -72
app.py CHANGED
@@ -1,47 +1,43 @@
1
  #!/usr/bin/env python3
2
  """
3
  BackgroundFX Pro - CSP-Safe Application Entry Point
4
- Built for Hugging Face Spaces (strict Content Security Policy)
5
- No inline JavaScript, CSP-compliant Gradio, safe environment vars, fallback AI models
6
  """
7
 
8
- import early_env # <<< must be FIRST to sanitize threading/env before anything else
9
 
10
- import os
11
  from pathlib import Path
12
- from typing import Optional, Dict, Any, Callable
13
 
14
- # 1️⃣ Set CSP-safe environment variables BEFORE any imports (Gradio will see these)
15
  os.environ['GRADIO_ALLOW_FLAGGING'] = 'never'
16
  os.environ['GRADIO_ANALYTICS_ENABLED'] = 'False'
17
  os.environ['GRADIO_SERVER_NAME'] = '0.0.0.0'
18
  os.environ['GRADIO_SERVER_PORT'] = '7860'
19
 
20
- # 2️⃣ Patch Gradio schema early for Hugging Face bug compatibility
21
  try:
22
  import gradio_client.utils as gc_utils
23
  orig_get_type = gc_utils.get_type
24
  def patched_get_type(schema):
25
  if not isinstance(schema, dict):
26
- if isinstance(schema, bool):
27
- return "boolean"
28
- if isinstance(schema, str):
29
- return "string"
30
- if isinstance(schema, (int, float)):
31
- return "number"
32
  return "string"
33
  return orig_get_type(schema)
34
  gc_utils.get_type = patched_get_type
35
  except Exception:
36
- pass # No fatal error if Gradio patch fails
37
 
38
- # 3️⃣ Initialize logging EARLY (before importing modules that emit logs)
39
  from utils.logging_setup import setup_logging, make_logger
40
- setup_logging(app_name="backgroundfx") # LOG_LEVEL env var respected; defaults to DEBUG in dev
41
  logger = make_logger("entrypoint")
42
  logger.info("Entrypoint starting…")
43
 
44
- # 4️⃣ Import your modular code (assuming your project structure)
45
  from core.exceptions import ModelLoadingError, VideoProcessingError
46
  from config.app_config import get_config
47
  from utils.hardware.device_manager import DeviceManager
@@ -50,10 +46,39 @@ def patched_get_type(schema):
50
  from processing.video.video_processor import CoreVideoProcessor, ProcessorConfig
51
  from processing.audio.audio_processor import AudioProcessor
52
 
53
- # NOTE: We import background presets (for the UI) and validator from utils
54
- from utils import PROFESSIONAL_BACKGROUNDS, validate_video_file
55
-
56
- # 5️⃣ CSP-safe fallback model stubs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  class CSPSafeSAM2:
58
  def set_image(self, image):
59
  self.shape = getattr(image, 'shape', (512, 512, 3))
@@ -66,7 +91,6 @@ def predict(self, point_coords=None, point_labels=None, box=None, multimask_outp
66
  class CSPSafeMatAnyone:
67
  def step(self, image_tensor, mask_tensor=None, objects=None, first_frame_pred=False, **kwargs):
68
  import torch
69
- # image_tensor can be CHW or NCHW; our model guard normalizes it upstream
70
  if hasattr(image_tensor, "shape"):
71
  if len(image_tensor.shape) == 3:
72
  _, H, W = image_tensor.shape
@@ -82,7 +106,25 @@ def output_prob_to_mask(self, output_prob):
82
  def process(self, image, mask, **kwargs):
83
  return mask
84
 
85
- # 6️⃣ Application main processor object
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  class VideoBackgroundApp:
87
  def __init__(self):
88
  self.config = get_config()
@@ -105,21 +147,18 @@ def load_models(self, progress_callback: Optional[Callable]=None) -> str:
105
  sam2_model = getattr(sam2, "model", sam2) if sam2 else CSPSafeSAM2()
106
  matanyone_model = getattr(matanyone, "model", matanyone) if matanyone else CSPSafeMatAnyone()
107
 
108
- # ⬇️ NEW: fast-but-safe defaults (NVENC + model-only downscale)
109
  cfg = ProcessorConfig(
110
- background_preset="office", # valid preset key
111
- write_fps=None, # keep source FPS
112
- max_model_size=1280, # model-only downscale; output remains full-res
113
- use_nvenc=True, # try GPU encoder if available
114
- nvenc_codec="h264", # browser-safe preview
115
- nvenc_preset="p5", # HQ preset
116
- nvenc_cq=18, # lower = higher quality
117
- nvenc_tune_hq=True, # high-quality tuning
118
- nvenc_pix_fmt="yuv420p", # web-compatible pixel format
119
  )
120
  self.core_processor = CoreVideoProcessor(config=cfg, models=None)
121
-
122
- # Minimal adapter the processor expects
123
  self.core_processor.models = type('FakeModelManager', (), {
124
  'get_sam2': lambda self_: sam2_model,
125
  'get_matanyone': lambda self_: matanyone_model
@@ -130,31 +169,103 @@ def load_models(self, progress_callback: Optional[Callable]=None) -> str:
130
  type(sam2_model).__name__, type(matanyone_model).__name__)
131
  return "Models loaded (CSP-safe; fallbacks in use if actual AI models failed)."
132
 
133
- def process_video(self, video, bg_style, custom_bg_file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  if not self.models_loaded:
135
  return None, "Models not loaded yet"
136
 
137
- logger.info("process_video called (video=%s, bg_style=%s, custom_bg=%s)",
138
- video, bg_style, getattr(custom_bg_file, "name", None) if custom_bg_file else None)
 
 
139
 
140
- import time
141
  output_path = f"/tmp/output_{int(time.time())}.mp4"
142
 
143
- # Background config passed to the processor:
144
- # - custom image via {"custom_path": "..."}
145
- # - preset via {"background_choice": "<key>"}
146
- if custom_bg_file:
147
- bg_cfg = {"custom_path": custom_bg_file.name}
148
- else:
149
- style = bg_style if (bg_style in PROFESSIONAL_BACKGROUNDS) else "office"
150
- bg_cfg = {"background_choice": style}
151
-
152
- # Validate input video (utils.validate_video_file returns bool)
153
  ok = validate_video_file(video)
154
  if not ok:
155
  logger.warning("Invalid/unreadable video: %s", video)
156
  return None, "Invalid or unreadable video file"
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  try:
159
  result = self.core_processor.process_video(
160
  input_path=video,
@@ -167,13 +278,13 @@ def process_video(self, video, bg_style, custom_bg_file):
167
  logger.info("Audio merged → %s", output_with_audio)
168
 
169
  frames = (result.get('frames') if isinstance(result, dict) else None) or "n/a"
170
- return output_with_audio, f"Processing complete ({frames} frames, style={bg_style})"
171
 
172
  except Exception as e:
173
  logger.exception("Processing failed")
174
  return None, f"Processing failed: {e}"
175
 
176
- # 7️⃣ Gradio interface CSP-safe
177
  def create_csp_safe_gradio():
178
  import gradio as gr
179
  app = VideoBackgroundApp()
@@ -182,46 +293,116 @@ def create_csp_safe_gradio():
182
  title="BackgroundFX Pro - CSP Safe",
183
  analytics_enabled=False,
184
  css="""
185
- .gradio-container { max-width: 1000px; margin: auto; }
186
  """
187
  ) as demo:
188
  gr.Markdown("# 🎬 BackgroundFX Pro (CSP-Safe)")
189
- gr.Markdown("Replace your video background with cinema-quality AI matting. Built for Hugging Face Spaces CSP.")
190
 
191
  with gr.Row():
192
- with gr.Column():
193
  video = gr.Video(label="Upload Video")
194
- # Ensure default choice exists in PROFESSIONAL_BACKGROUNDS (use 'office')
195
- choices = list(PROFESSIONAL_BACKGROUNDS.keys())
196
- default_choice = "office" if "office" in choices else (choices[0] if choices else "office")
197
- bg_style = gr.Dropdown(
198
- choices=choices,
199
- value=default_choice,
200
- label="Background Style"
201
  )
202
- custom_bg = gr.File(label="Custom Background (Optional)", file_types=["image"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  btn_load = gr.Button("🔄 Load Models", variant="secondary")
204
  btn_run = gr.Button("🎬 Process Video", variant="primary")
205
 
206
- with gr.Column():
207
  status = gr.Textbox(label="Status", lines=4)
 
208
  out_video = gr.Video(label="Processed Video")
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  def safe_load():
211
  msg = app.load_models()
212
  logger.info("UI: models loaded")
213
- return msg
214
-
215
- def safe_process(vid, style, custom_bg_file):
216
- logger.info("UI: starting processing (style=%s)", style)
217
- return app.process_video(vid, style, custom_bg_file)
218
-
219
- btn_load.click(fn=safe_load, outputs=[status])
220
- btn_run.click(fn=safe_process, inputs=[video, bg_style, custom_bg], outputs=[out_video, status])
 
 
 
221
 
222
  return demo
223
 
224
- # 8️⃣ Main entry point (for Hugging Face Spaces)
225
  if __name__ == "__main__":
226
  logger.info("Launching CSP-safe Gradio interface for Hugging Face Spaces")
227
  demo = create_csp_safe_gradio()
 
1
  #!/usr/bin/env python3
2
  """
3
  BackgroundFX Pro - CSP-Safe Application Entry Point
4
+ Now with: live background preview + sources: Preset / Upload / Gradient / AI Generate
 
5
  """
6
 
7
+ import early_env # <<< must be FIRST
8
 
9
+ import os, time, tempfile
10
  from pathlib import Path
11
+ from typing import Optional, Dict, Any, Callable, Tuple
12
 
13
+ # 1) CSP-safe Gradio env
14
  os.environ['GRADIO_ALLOW_FLAGGING'] = 'never'
15
  os.environ['GRADIO_ANALYTICS_ENABLED'] = 'False'
16
  os.environ['GRADIO_SERVER_NAME'] = '0.0.0.0'
17
  os.environ['GRADIO_SERVER_PORT'] = '7860'
18
 
19
+ # 2) Gradio schema patch
20
  try:
21
  import gradio_client.utils as gc_utils
22
  orig_get_type = gc_utils.get_type
23
  def patched_get_type(schema):
24
  if not isinstance(schema, dict):
25
+ if isinstance(schema, bool): return "boolean"
26
+ if isinstance(schema, str): return "string"
27
+ if isinstance(schema, (int, float)): return "number"
 
 
 
28
  return "string"
29
  return orig_get_type(schema)
30
  gc_utils.get_type = patched_get_type
31
  except Exception:
32
+ pass
33
 
34
+ # 3) Logging early
35
  from utils.logging_setup import setup_logging, make_logger
36
+ setup_logging(app_name="backgroundfx")
37
  logger = make_logger("entrypoint")
38
  logger.info("Entrypoint starting…")
39
 
40
+ # 4) Imports
41
  from core.exceptions import ModelLoadingError, VideoProcessingError
42
  from config.app_config import get_config
43
  from utils.hardware.device_manager import DeviceManager
 
46
  from processing.video.video_processor import CoreVideoProcessor, ProcessorConfig
47
  from processing.audio.audio_processor import AudioProcessor
48
 
49
+ # Background helpers
50
+ from utils import PROFESSIONAL_BACKGROUNDS, validate_video_file, create_professional_background
51
+ # Gradient helper (add this to utils; fallback here for preview only if missing)
52
+ try:
53
+ from utils import create_gradient_background
54
+ except Exception:
55
+ def create_gradient_background(spec: Dict[str, Any], width: int, height: int):
56
+ # Lightweight fallback preview (linear only)
57
+ import numpy as np
58
+ import cv2
59
+ def _to_rgb(c):
60
+ if isinstance(c, (list, tuple)) and len(c) == 3:
61
+ return tuple(int(x) for x in c)
62
+ if isinstance(c, str) and c.startswith("#") and len(c) == 7:
63
+ return tuple(int(c[i:i+2], 16) for i in (1,3,5))
64
+ return (255,255,255)
65
+ start = _to_rgb(spec.get("start", "#222222"))
66
+ end = _to_rgb(spec.get("end", "#888888"))
67
+ angle = float(spec.get("angle_deg", 0))
68
+ # build vertical then rotate
69
+ bg = np.zeros((height, width, 3), np.uint8)
70
+ for y in range(height):
71
+ t = y / max(1, height-1)
72
+ r = int(start[0]*(1-t) + end[0]*t)
73
+ g = int(start[1]*(1-t) + end[1]*t)
74
+ b = int(start[2]*(1-t) + end[2]*t)
75
+ bg[y,:] = (r,g,b)
76
+ # rotate to angle
77
+ center = (width/2, height/2)
78
+ rot = cv2.getRotationMatrix2D(center, angle, 1.0)
79
+ return cv2.warpAffine(bg, rot, (width, height), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
80
+
81
+ # 5) CSP-safe fallbacks for models
82
  class CSPSafeSAM2:
83
  def set_image(self, image):
84
  self.shape = getattr(image, 'shape', (512, 512, 3))
 
91
  class CSPSafeMatAnyone:
92
  def step(self, image_tensor, mask_tensor=None, objects=None, first_frame_pred=False, **kwargs):
93
  import torch
 
94
  if hasattr(image_tensor, "shape"):
95
  if len(image_tensor.shape) == 3:
96
  _, H, W = image_tensor.shape
 
106
  def process(self, image, mask, **kwargs):
107
  return mask
108
 
109
+ # ---------- helpers for UI ----------
110
+ import numpy as np
111
+ import cv2
112
+ from PIL import Image
113
+
114
+ PREVIEW_W, PREVIEW_H = 640, 360 # 16:9
115
+
116
+ def _hex_to_rgb(x: str) -> Tuple[int,int,int]:
117
+ x = x.strip()
118
+ if x.startswith("#") and len(x) == 7:
119
+ return tuple(int(x[i:i+2], 16) for i in (1,3,5))
120
+ return (255,255,255)
121
+
122
+ def _np_to_pil(arr: np.ndarray) -> Image.Image:
123
+ if arr.dtype != np.uint8:
124
+ arr = arr.clip(0,255).astype(np.uint8)
125
+ return Image.fromarray(arr)
126
+
127
+ # ---------- main app ----------
128
  class VideoBackgroundApp:
129
  def __init__(self):
130
  self.config = get_config()
 
147
  sam2_model = getattr(sam2, "model", sam2) if sam2 else CSPSafeSAM2()
148
  matanyone_model = getattr(matanyone, "model", matanyone) if matanyone else CSPSafeMatAnyone()
149
 
 
150
  cfg = ProcessorConfig(
151
+ background_preset="office",
152
+ write_fps=None,
153
+ max_model_size=1280,
154
+ use_nvenc=True,
155
+ nvenc_codec="h264",
156
+ nvenc_preset="p5",
157
+ nvenc_cq=18,
158
+ nvenc_tune_hq=True,
159
+ nvenc_pix_fmt="yuv420p",
160
  )
161
  self.core_processor = CoreVideoProcessor(config=cfg, models=None)
 
 
162
  self.core_processor.models = type('FakeModelManager', (), {
163
  'get_sam2': lambda self_: sam2_model,
164
  'get_matanyone': lambda self_: matanyone_model
 
169
  type(sam2_model).__name__, type(matanyone_model).__name__)
170
  return "Models loaded (CSP-safe; fallbacks in use if actual AI models failed)."
171
 
172
+ # ---- PREVIEWS ----
173
+ def preview_preset(self, preset_key: str) -> Image.Image:
174
+ key = preset_key if preset_key in PROFESSIONAL_BACKGROUNDS else "office"
175
+ bg = create_professional_background(key, PREVIEW_W, PREVIEW_H) # RGB
176
+ return _np_to_pil(bg)
177
+
178
+ def preview_upload(self, file) -> Optional[Image.Image]:
179
+ if file is None: return None
180
+ try:
181
+ img = Image.open(file.name).convert("RGB")
182
+ img = img.resize((PREVIEW_W, PREVIEW_H), Image.LANCZOS)
183
+ return img
184
+ except Exception as e:
185
+ logger.warning("Upload preview failed: %s", e)
186
+ return None
187
+
188
+ def preview_gradient(self, gtype: str, color1: str, color2: str, angle: int) -> Image.Image:
189
+ spec = {
190
+ "type": gtype.lower(), # "linear" or "radial" (linear in fallback)
191
+ "start": _hex_to_rgb(color1),
192
+ "end": _hex_to_rgb(color2),
193
+ "angle_deg": float(angle),
194
+ }
195
+ bg = create_gradient_background(spec, PREVIEW_W, PREVIEW_H)
196
+ return _np_to_pil(bg)
197
+
198
+ def ai_generate_background(self, prompt: str, seed: int, width: int, height: int) -> Tuple[Optional[Image.Image], Optional[str], str]:
199
+ """
200
+ Try generating a background with diffusers; save to /tmp and return (img, path, status).
201
+ """
202
+ try:
203
+ from diffusers import StableDiffusionPipeline
204
+ import torch
205
+ model_id = os.environ.get("BGFX_T2I_MODEL", "stabilityai/stable-diffusion-2-1")
206
+ dtype = torch.float16 if torch.cuda.is_available() else torch.float32
207
+ device = "cuda" if torch.cuda.is_available() else "cpu"
208
+ pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=dtype)
209
+ pipe = pipe.to(device)
210
+ g = torch.Generator(device=device).manual_seed(int(seed)) if seed is not None else None
211
+ with torch.autocast(device if device=="cuda" else "cpu"):
212
+ img = pipe(prompt, height=height, width=width, guidance_scale=7.0, num_inference_steps=25, generator=g).images[0]
213
+ tmp_path = f"/tmp/ai_bg_{int(time.time())}.png"
214
+ img.save(tmp_path)
215
+ return img.resize((PREVIEW_W, PREVIEW_H), Image.LANCZOS), tmp_path, f"AI background generated ✓ ({os.path.basename(tmp_path)})"
216
+ except Exception as e:
217
+ logger.warning("AI generation unavailable: %s", e)
218
+ return None, None, f"AI generation unavailable: {e}"
219
+
220
+ # ---- PROCESS VIDEO ----
221
+ def process_video(
222
+ self,
223
+ video: str,
224
+ bg_source: str,
225
+ preset_key: str,
226
+ custom_bg_file,
227
+ grad_type: str,
228
+ grad_color1: str,
229
+ grad_color2: str,
230
+ grad_angle: int,
231
+ ai_bg_path: Optional[str],
232
+ ):
233
  if not self.models_loaded:
234
  return None, "Models not loaded yet"
235
 
236
+ logger.info("process_video called (video=%s, source=%s, preset=%s, file=%s, grad=%s, ai=%s)",
237
+ video, bg_source, preset_key, getattr(custom_bg_file, "name", None) if custom_bg_file else None,
238
+ {"type": grad_type, "c1": grad_color1, "c2": grad_color2, "angle": grad_angle},
239
+ ai_bg_path)
240
 
 
241
  output_path = f"/tmp/output_{int(time.time())}.mp4"
242
 
243
+ # Validate input video
 
 
 
 
 
 
 
 
 
244
  ok = validate_video_file(video)
245
  if not ok:
246
  logger.warning("Invalid/unreadable video: %s", video)
247
  return None, "Invalid or unreadable video file"
248
 
249
+ # Build bg_config based on source
250
+ bg_cfg: Dict[str, Any]
251
+ src = (bg_source or "Preset").lower()
252
+ if src == "upload" and custom_bg_file is not None:
253
+ bg_cfg = {"custom_path": custom_bg_file.name}
254
+ elif src == "gradient":
255
+ bg_cfg = {
256
+ "gradient": {
257
+ "type": (grad_type or "linear").lower(),
258
+ "start": _hex_to_rgb(grad_color1 or "#222222"),
259
+ "end": _hex_to_rgb(grad_color2 or "#888888"),
260
+ "angle_deg": float(grad_angle or 0),
261
+ }
262
+ }
263
+ elif src == "ai generate" and ai_bg_path:
264
+ bg_cfg = {"custom_path": ai_bg_path}
265
+ else:
266
+ key = preset_key if preset_key in PROFESSIONAL_BACKGROUNDS else "office"
267
+ bg_cfg = {"background_choice": key}
268
+
269
  try:
270
  result = self.core_processor.process_video(
271
  input_path=video,
 
278
  logger.info("Audio merged → %s", output_with_audio)
279
 
280
  frames = (result.get('frames') if isinstance(result, dict) else None) or "n/a"
281
+ return output_with_audio, f"Processing complete ({frames} frames, background={bg_source})"
282
 
283
  except Exception as e:
284
  logger.exception("Processing failed")
285
  return None, f"Processing failed: {e}"
286
 
287
+ # 7) Gradio UI
288
  def create_csp_safe_gradio():
289
  import gradio as gr
290
  app = VideoBackgroundApp()
 
293
  title="BackgroundFX Pro - CSP Safe",
294
  analytics_enabled=False,
295
  css="""
296
+ .gradio-container { max-width: 1100px; margin: auto; }
297
  """
298
  ) as demo:
299
  gr.Markdown("# 🎬 BackgroundFX Pro (CSP-Safe)")
300
+ gr.Markdown("Replace your video background with cinema-quality AI matting. Now with live background preview.")
301
 
302
  with gr.Row():
303
+ with gr.Column(scale=1):
304
  video = gr.Video(label="Upload Video")
305
+ bg_source = gr.Radio(
306
+ ["Preset", "Upload", "Gradient", "AI Generate"],
307
+ value="Preset",
308
+ label="Background Source",
309
+ interactive=True,
 
 
310
  )
311
+
312
+ # PRESET
313
+ preset_choices = list(PROFESSIONAL_BACKGROUNDS.keys())
314
+ preset_key = gr.Dropdown(choices=preset_choices, value=("office" if "office" in preset_choices else preset_choices[0]), label="Preset")
315
+ # UPLOAD
316
+ custom_bg = gr.File(label="Custom Background (Image)", file_types=["image"], visible=False)
317
+ # GRADIENT
318
+ grad_type = gr.Dropdown(choices=["Linear", "Radial"], value="Linear", label="Gradient Type", visible=False)
319
+ grad_color1 = gr.ColorPicker(value="#222222", label="Start Color", visible=False)
320
+ grad_color2 = gr.ColorPicker(value="#888888", label="End Color", visible=False)
321
+ grad_angle = gr.Slider(0, 360, value=0, step=1, label="Angle (degrees)", visible=False)
322
+
323
+ # AI
324
+ ai_prompt = gr.Textbox(label="AI Prompt", placeholder="e.g., sunlit modern office, soft bokeh, neutral palette", visible=False)
325
+ ai_seed = gr.Slider(0, 2**31-1, step=1, value=42, label="Seed", visible=False)
326
+ ai_size = gr.Dropdown(choices=["640x360","960x540","1280x720"], value="640x360", label="AI Image Size", visible=False)
327
+ ai_go = gr.Button("✨ Generate Background", visible=False, variant="secondary")
328
+ ai_status = gr.Markdown(visible=False)
329
+ ai_bg_path_state = gr.State(value=None) # store /tmp path
330
+
331
  btn_load = gr.Button("🔄 Load Models", variant="secondary")
332
  btn_run = gr.Button("🎬 Process Video", variant="primary")
333
 
334
+ with gr.Column(scale=1):
335
  status = gr.Textbox(label="Status", lines=4)
336
+ bg_preview = gr.Image(label="Background Preview", width=PREVIEW_W, height=PREVIEW_H, interactive=False)
337
  out_video = gr.Video(label="Processed Video")
338
 
339
+ # ---------- UI wiring ----------
340
+
341
+ # background source → show/hide controls
342
+ def on_source_change(src):
343
+ src = (src or "Preset").lower()
344
+ return (
345
+ gr.update(visible=(src=="preset")),
346
+ gr.update(visible=(src=="upload")),
347
+ gr.update(visible=(src=="gradient")),
348
+ gr.update(visible=(src=="gradient")),
349
+ gr.update(visible=(src=="gradient")),
350
+ gr.update(visible=(src=="gradient")),
351
+ gr.update(visible=(src=="ai generate")),
352
+ gr.update(visible=(src=="ai generate")),
353
+ gr.update(visible=(src=="ai generate")),
354
+ gr.update(visible=(src=="ai generate")),
355
+ gr.update(visible=(src=="ai generate")),
356
+ )
357
+ bg_source.change(
358
+ fn=on_source_change,
359
+ inputs=[bg_source],
360
+ outputs=[preset_key, custom_bg, grad_type, grad_color1, grad_color2, grad_angle, ai_prompt, ai_seed, ai_size, ai_go, ai_status],
361
+ )
362
+
363
+ # live previews
364
+ def preview_from_preset(key):
365
+ return app.preview_preset(key)
366
+ preset_key.change(fn=preview_from_preset, inputs=[preset_key], outputs=[bg_preview])
367
+
368
+ def preview_from_upload(file):
369
+ return app.preview_upload(file)
370
+ custom_bg.change(fn=preview_from_upload, inputs=[custom_bg], outputs=[bg_preview])
371
+
372
+ def preview_from_gradient(gt, c1, c2, ang):
373
+ return app.preview_gradient(gt, c1, c2, ang)
374
+ for comp in (grad_type, grad_color1, grad_color2, grad_angle):
375
+ comp.change(fn=preview_from_gradient, inputs=[grad_type, grad_color1, grad_color2, grad_angle], outputs=[bg_preview])
376
+
377
+ # AI generate
378
+ def ai_generate(prompt, seed, size):
379
+ try:
380
+ w,h = map(int, size.split("x"))
381
+ except Exception:
382
+ w,h = PREVIEW_W, PREVIEW_H
383
+ img, path, msg = app.ai_generate_background(prompt or "professional modern office background, neutral colors, depth of field", int(seed), w, h)
384
+ return img, (path or None), msg
385
+ ai_go.click(fn=ai_generate, inputs=[ai_prompt, ai_seed, ai_size], outputs=[bg_preview, ai_bg_path_state, ai_status])
386
+
387
+ # model load / run
388
  def safe_load():
389
  msg = app.load_models()
390
  logger.info("UI: models loaded")
391
+ # set initial preview (preset default)
392
+ return msg, app.preview_preset(preset_key.value if hasattr(preset_key, "value") else "office")
393
+ btn_load.click(fn=safe_load, outputs=[status, bg_preview])
394
+
395
+ def safe_process(vid, src, pkey, file, gtype, c1, c2, ang, ai_path):
396
+ return app.process_video(vid, src, pkey, file, gtype, c1, c2, ang, ai_path)
397
+ btn_run.click(
398
+ fn=safe_process,
399
+ inputs=[video, bg_source, preset_key, custom_bg, grad_type, grad_color1, grad_color2, grad_angle, ai_bg_path_state],
400
+ outputs=[out_video, status]
401
+ )
402
 
403
  return demo
404
 
405
+ # 8) Launch
406
  if __name__ == "__main__":
407
  logger.info("Launching CSP-safe Gradio interface for Hugging Face Spaces")
408
  demo = create_csp_safe_gradio()