MogensR commited on
Commit
d4b7ec0
·
1 Parent(s): 3d9c3c0

Update ui/callbacks.py

Browse files
Files changed (1) hide show
  1. ui/callbacks.py +96 -59
ui/callbacks.py CHANGED
@@ -1,8 +1,14 @@
1
  #!/usr/bin/env python3
2
  """
3
  Callbacks for BackgroundFX Pro UI
4
- - Pure functions wired to UI events
5
- - Safe imports & graceful fallbacks so the UI keeps working even if optional utils are missing
 
 
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
@@ -25,127 +31,154 @@
25
  except Exception:
26
  pass
27
 
28
- def _generate_ai_background(prompt_text: str, width: int, height: int, bokeh: float, vignette: float, contrast: float):
 
 
 
 
 
 
 
 
 
 
 
29
  """
30
- Lightweight inline fallback if utils.bg_generator is not present yet.
31
- Generates a simple procedural background.
32
  """
33
  if _try_bg_gen is not None:
34
- return _try_bg_gen(prompt_text, width=width, height=height, bokeh=bokeh, vignette=vignette, contrast=contrast)
35
-
36
- # Fallback (minimal dependency, PIL-only)
 
 
 
 
 
 
 
37
  from pathlib import Path
38
- import time
39
- import random
40
- import numpy as np
41
  from PIL import Image, ImageFilter, ImageOps
42
 
43
  TMP_DIR = Path("/tmp/bgfx")
44
  TMP_DIR.mkdir(parents=True, exist_ok=True)
45
 
46
  palettes = {
47
- "office": [(240, 245, 250), (210, 220, 230), (180, 190, 200)],
48
- "studio": [(18, 18, 20), (32, 32, 36), (58, 60, 64)],
49
- "sunset": [(255, 183, 77), (255, 138, 101), (244, 143, 177)],
50
- "forest": [(46, 125, 50), (102, 187, 106), (165, 214, 167)],
51
- "ocean": [(33, 150, 243), (3, 169, 244), (0, 188, 212)],
52
- "minimal": [(245, 246, 248), (230, 232, 236), (214, 218, 224)],
53
- "warm": [(255, 224, 178), (255, 204, 128), (255, 171, 145)],
54
- "cool": [(197, 202, 233), (179, 229, 252), (178, 235, 242)],
55
- "royal": [(63, 81, 181), (121, 134, 203), (159, 168, 218)],
56
  }
57
  p = (prompt_text or "").lower()
58
- for k, pal in palettes.items():
59
- if k in p:
60
- palette = pal
61
- break
62
- else:
63
- random.seed(hash(p) % (2**32 - 1))
64
  palette = [tuple(random.randint(90, 200) for _ in range(3)) for _ in range(3)]
65
 
66
- # Fake Perlin-ish noise
67
  def _noise(h, w, octaves=4):
68
- acc = np.zeros((h, w), dtype=np.float32)
69
  for o in range(octaves):
70
- scale = 2 ** o
71
- small = np.random.rand(h // scale + 1, w // scale + 1).astype(np.float32)
72
- img = Image.fromarray((small * 255).astype(np.uint8)).resize((w, h), Image.BILINEAR)
73
- acc += np.array(img, dtype=np.float32) / 255.0 / (o + 1)
74
- acc = acc / max(1e-6, acc.max())
75
  return acc
76
 
77
- def _blend(noise, pal):
78
- h, w = noise.shape
79
- img = np.zeros((h, w, 3), dtype=np.float32)
80
  thr = [0.33, 0.66]
81
- c0, c1, c2 = [np.array(c, dtype=np.float32) for c in pal]
82
- img[noise < thr[0]] = c0
83
- mid = (noise >= thr[0]) & (noise < thr[1])
 
84
  img[mid] = c1
85
- img[noise >= thr[1]] = c2
86
  return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
87
 
88
- n = _noise(height, width, octaves=4)
89
  out = _blend(n, palette)
90
 
91
  if bokeh > 0:
92
- from PIL import ImageFilter
93
  out = out.filter(ImageFilter.GaussianBlur(radius=min(50, max(0, bokeh))))
94
-
95
  if vignette > 0:
96
- import numpy as np
97
  y, x = np.ogrid[:height, :width]
98
  cx, cy = width / 2, height / 2
99
  r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
100
  mask = 1 - np.clip(r / (max(width, height) / 1.2), 0, 1)
101
- mask = (mask ** 2).astype(np.float32)
102
  base = np.array(out).astype(np.float32) / 255.0
103
- out_arr = base * (mask[..., None] * (1 - vignette) + vignette)
104
- out = Image.fromarray(np.clip(out_arr * 255, 0, 255).astype(np.uint8))
105
-
106
  if contrast != 1.0:
107
- from PIL import ImageOps
108
  out = ImageOps.autocontrast(out, cutoff=1)
109
- arr = (np.array(out).astype(np.float32))
110
  mean = arr.mean(axis=(0, 1), keepdims=True)
111
  arr = (arr - mean) * float(contrast) + mean
112
  out = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
113
 
114
- ts = int(time.time() * 1000)
115
  path = str((TMP_DIR / f"ai_bg_{ts}.png").resolve())
116
  out.save(path)
117
  return out, path
118
 
119
 
120
- # -------- MODEL MANAGEMENT --------
 
 
121
  def cb_load_models() -> str:
 
122
  return load_models_with_validation()
123
 
 
 
 
 
124
  def cb_process_video(
125
  vid: str,
126
  style: str,
127
  custom_file: dict | None,
128
  use_two: bool,
129
  chroma: str,
 
130
  prev_mask: bool,
131
  prev_green: bool,
132
  ):
 
 
 
 
 
133
  if PROCESS_CANCELLED.is_set():
134
  PROCESS_CANCELLED.clear()
 
 
135
  custom_path = None
136
  if isinstance(custom_file, dict) and custom_file.get("name"):
137
  custom_path = custom_file["name"]
 
 
138
  return process_video_fixed(
139
  video_path=vid,
140
  background_choice=style,
141
  custom_background_path=custom_path,
142
- progress_callback=None,
143
  use_two_stage=use_two,
144
  chroma_preset=chroma,
 
145
  preview_mask=prev_mask,
146
  preview_greenscreen=prev_green,
147
  )
148
 
 
 
 
 
149
  def cb_cancel() -> str:
150
  try:
151
  PROCESS_CANCELLED.set()
@@ -160,24 +193,28 @@ def cb_status() -> Tuple[Dict[str, Any], Dict[str, Any]]:
160
  return {"error": str(e)}, {"error": str(e)}
161
 
162
  def cb_clear():
163
- # out_video, status, gen_preview, gen_path
164
  return None, "", None, ""
165
 
166
 
167
- # -------- AI BACKGROUND --------
 
 
168
  def cb_generate_bg(prompt_text: str, w: int, h: int, b: float, v: float, c: float):
169
  img, path = _generate_ai_background(prompt_text, int(w), int(h), b, v, c)
170
  return img, path
171
 
172
  def cb_use_gen_bg(path_text: str):
173
- return {"name": path_text, "size": os.path.getsize(path_text)} if path_text and os.path.exists(path_text) else None
 
 
174
 
175
 
176
- # -------- PREVIEWS (no-op safe defaults) --------
 
 
177
  def cb_video_changed(vid_path: str):
178
- # Reserved hook for future preview image extraction
179
  return None
180
 
181
  def cb_custom_bg_preview(file_obj: dict | None):
182
- # Reserved hook to show uploaded custom background
183
  return None
 
1
  #!/usr/bin/env python3
2
  """
3
  Callbacks for BackgroundFX Pro UI
4
+ ---------------------------------
5
+ All functions here are *thin* wrappers wired to the Gradio interface.
6
+
7
+ Changes in this revision
8
+ ------------------------
9
+ • Added **key_color_mode** positional arg to `cb_process_video`
10
+ (matches the new dropdown in ui_components.py)
11
+ • Passes that value straight through to `process_video_fixed(...)`
12
  """
13
 
14
  from __future__ import annotations
 
31
  except Exception:
32
  pass
33
 
34
+
35
+ # ------------------------------------------------------------------
36
+ # LIGHTWEIGHT BG GENERATOR (inline fallback)
37
+ # ------------------------------------------------------------------
38
+ def _generate_ai_background(
39
+ prompt_text: str,
40
+ width: int,
41
+ height: int,
42
+ bokeh: float,
43
+ vignette: float,
44
+ contrast: float,
45
+ ):
46
  """
47
+ If utils.bg_generator.generate_ai_background exists, use it.
48
+ Otherwise fall back to a tiny procedural background made with PIL & NumPy.
49
  """
50
  if _try_bg_gen is not None:
51
+ return _try_bg_gen(
52
+ prompt_text,
53
+ width=width,
54
+ height=height,
55
+ bokeh=bokeh,
56
+ vignette=vignette,
57
+ contrast=contrast,
58
+ )
59
+
60
+ # -------- Tiny fallback (PIL only) --------
61
  from pathlib import Path
62
+ import time, random, numpy as np
 
 
63
  from PIL import Image, ImageFilter, ImageOps
64
 
65
  TMP_DIR = Path("/tmp/bgfx")
66
  TMP_DIR.mkdir(parents=True, exist_ok=True)
67
 
68
  palettes = {
69
+ "office": [(240, 245, 250), (210, 220, 230), (180, 190, 200)],
70
+ "studio": [(18, 18, 20), (32, 32, 36), (58, 60, 64)],
71
+ "sunset": [(255,183,77), (255,138,101), (244,143,177)],
72
+ "forest": [(46,125,50), (102,187,106), (165,214,167)],
73
+ "ocean": [(33,150,243), (3,169,244), (0,188,212)],
74
+ "minimal": [(245,246,248), (230,232,236), (214,218,224)],
75
+ "warm": [(255,224,178), (255,204,128), (255,171,145)],
76
+ "cool": [(197,202,233), (179,229,252), (178,235,242)],
77
+ "royal": [(63,81,181), (121,134,203), (159,168,218)],
78
  }
79
  p = (prompt_text or "").lower()
80
+ palette = next((pal for k, pal in palettes.items() if k in p), None)
81
+ if palette is None:
82
+ random.seed(hash(p) & 0xFFFFFFFF)
 
 
 
83
  palette = [tuple(random.randint(90, 200) for _ in range(3)) for _ in range(3)]
84
 
 
85
  def _noise(h, w, octaves=4):
86
+ acc = np.zeros((h, w), np.float32)
87
  for o in range(octaves):
88
+ s = 2**o
89
+ small = np.random.rand(h // s + 1, w // s + 1).astype(np.float32)
90
+ acc += cv2.resize(small, (w, h), interpolation=cv2.INTER_LINEAR) / (o + 1)
91
+ acc /= max(1e-6, acc.max())
 
92
  return acc
93
 
94
+ def _blend(n, pal):
95
+ h, w = n.shape
 
96
  thr = [0.33, 0.66]
97
+ img = np.zeros((h, w, 3), np.float32)
98
+ c0, c1, c2 = [np.array(c, np.float32) for c in pal]
99
+ img[n < thr[0]] = c0
100
+ mid = (n >= thr[0]) & (n < thr[1])
101
  img[mid] = c1
102
+ img[n >= thr[1]] = c2
103
  return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
104
 
105
+ n = _noise(height, width, 4)
106
  out = _blend(n, palette)
107
 
108
  if bokeh > 0:
 
109
  out = out.filter(ImageFilter.GaussianBlur(radius=min(50, max(0, bokeh))))
 
110
  if vignette > 0:
 
111
  y, x = np.ogrid[:height, :width]
112
  cx, cy = width / 2, height / 2
113
  r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
114
  mask = 1 - np.clip(r / (max(width, height) / 1.2), 0, 1)
115
+ mask = (mask**2).astype(np.float32)
116
  base = np.array(out).astype(np.float32) / 255.0
117
+ out = Image.fromarray(np.clip(base * (mask[..., None] * (1 - vignette) + vignette) * 255, 0, 255).astype(np.uint8))
 
 
118
  if contrast != 1.0:
 
119
  out = ImageOps.autocontrast(out, cutoff=1)
120
+ arr = np.array(out).astype(np.float32)
121
  mean = arr.mean(axis=(0, 1), keepdims=True)
122
  arr = (arr - mean) * float(contrast) + mean
123
  out = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
124
 
125
+ ts = int(time.time() * 1000)
126
  path = str((TMP_DIR / f"ai_bg_{ts}.png").resolve())
127
  out.save(path)
128
  return out, path
129
 
130
 
131
+ # ------------------------------------------------------------------
132
+ # MODEL MANAGEMENT
133
+ # ------------------------------------------------------------------
134
  def cb_load_models() -> str:
135
+ """Load SAM2 + MatAnyOne and return human-readable status."""
136
  return load_models_with_validation()
137
 
138
+
139
+ # ------------------------------------------------------------------
140
+ # MAIN video-processing callback
141
+ # ------------------------------------------------------------------
142
  def cb_process_video(
143
  vid: str,
144
  style: str,
145
  custom_file: dict | None,
146
  use_two: bool,
147
  chroma: str,
148
+ key_color_mode: str, # <-- NEW ARG from dropdown
149
  prev_mask: bool,
150
  prev_green: bool,
151
  ):
152
+ """
153
+ Runs the two-stage (or single-stage) pipeline and returns:
154
+ (processed_video_path | None, status_message:str)
155
+ """
156
+ # Reset any prior cancel flag when user clicks Run
157
  if PROCESS_CANCELLED.is_set():
158
  PROCESS_CANCELLED.clear()
159
+
160
+ # Resolve custom background path (if provided)
161
  custom_path = None
162
  if isinstance(custom_file, dict) and custom_file.get("name"):
163
  custom_path = custom_file["name"]
164
+
165
+ # Fire the core function
166
  return process_video_fixed(
167
  video_path=vid,
168
  background_choice=style,
169
  custom_background_path=custom_path,
170
+ progress_callback=None, # UI-level progress handled inside
171
  use_two_stage=use_two,
172
  chroma_preset=chroma,
173
+ key_color_mode=key_color_mode, # <-- pass straight through
174
  preview_mask=prev_mask,
175
  preview_greenscreen=prev_green,
176
  )
177
 
178
+
179
+ # ------------------------------------------------------------------
180
+ # CANCEL / STATUS / CLEAR
181
+ # ------------------------------------------------------------------
182
  def cb_cancel() -> str:
183
  try:
184
  PROCESS_CANCELLED.set()
 
193
  return {"error": str(e)}, {"error": str(e)}
194
 
195
  def cb_clear():
196
+ # Return blanks for (out_video, status, gen_preview, gen_path)
197
  return None, "", None, ""
198
 
199
 
200
+ # ------------------------------------------------------------------
201
+ # AI BACKGROUND
202
+ # ------------------------------------------------------------------
203
  def cb_generate_bg(prompt_text: str, w: int, h: int, b: float, v: float, c: float):
204
  img, path = _generate_ai_background(prompt_text, int(w), int(h), b, v, c)
205
  return img, path
206
 
207
  def cb_use_gen_bg(path_text: str):
208
+ if path_text and os.path.exists(path_text):
209
+ return {"name": path_text, "size": os.path.getsize(path_text)}
210
+ return None
211
 
212
 
213
+ # ------------------------------------------------------------------
214
+ # PREVIEWS (safe no-ops — extend later)
215
+ # ------------------------------------------------------------------
216
  def cb_video_changed(vid_path: str):
 
217
  return None
218
 
219
  def cb_custom_bg_preview(file_obj: dict | None):
 
220
  return None