MogensR commited on
Commit
63f9af1
Β·
1 Parent(s): 7249b9c

Update core/app.py

Browse files
Files changed (1) hide show
  1. core/app.py +82 -59
core/app.py CHANGED
@@ -3,12 +3,11 @@
3
  BackgroundFX Pro – Main Application Entry Point
4
  Refactored modular architecture – orchestrates specialised components
5
 
6
- Changes in this version (2025-08-27):
7
- - Robust two-stage import with clear diagnostics (tries multiple locations, logs traceback)
8
- - Properly reports "Two-stage available" based on actual import + init
9
- - Defensive config patching: adds missing attributes (e.g., use_nvenc) at runtime to avoid AttributeError
10
- - Cleaner progress handling and error messages
11
- - Minor hardening around background preparation and status reporting
12
  """
13
 
14
  from __future__ import annotations
@@ -23,6 +22,8 @@
23
  import traceback
24
  import sys
25
  import os
 
 
26
  from pathlib import Path
27
  from typing import Optional, Tuple, Dict, Any, Callable
28
 
@@ -35,9 +36,7 @@
35
  )
36
  logger = logging.getLogger("core.app")
37
 
38
- # ─────────────────────────────────────────────────────────────────────────────
39
- # 1.1 Ensure project root is importable (helps HF Spaces and local runs)
40
- # ─────────────────────────────────────────────────────────────────────────────
41
  PROJECT_FILE = Path(__file__).resolve()
42
  CORE_DIR = PROJECT_FILE.parent
43
  ROOT = CORE_DIR.parent
@@ -82,33 +81,63 @@ def _patched_get_type(schema):
82
  from utils.cv_processing import validate_video_file
83
 
84
  # ─────────────────────────────────────────────────────────────────────────────
85
- # 3.1) Optional: Two-stage import helper with diagnostics
86
  # ─────────────────────────────────────────────────────────────────────────────
87
- def _import_two_stage() -> tuple[bool, Any, Dict[str, Any], str]:
88
  """
89
- Try multiple import paths for the TwoStageProcessor module.
90
- Returns (available, TwoStageProcessor or None, CHROMA_PRESETS or {}, import_path_string)
 
 
 
91
  """
92
- paths_to_try = [
93
  "processing.two_stage.two_stage_processor",
94
  "two_stage_processor",
95
  "processing.two_stage_processor",
96
  ]
97
- last_err = None
98
- for mod_path in paths_to_try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  try:
100
- mod = __import__(mod_path, fromlist=["TwoStageProcessor", "CHROMA_PRESETS"])
101
- TwoStageProcessor = getattr(mod, "TwoStageProcessor")
102
- CHROMA_PRESETS = getattr(mod, "CHROMA_PRESETS", {"standard": {}})
103
- logger.info(f"Two-stage import OK from '{mod_path}'")
104
- return True, TwoStageProcessor, CHROMA_PRESETS, mod_path
 
 
 
 
 
 
 
105
  except Exception as e:
106
- last_err = e
107
- logger.debug("Two-stage import failed from '%s': %r\n%s", mod_path, e, traceback.format_exc())
108
- logger.warning("Two-stage import failed from all known paths: %r\n%s", last_err, traceback.format_exc())
109
- return False, None, {"standard": {}}, ""
110
 
111
- TWO_STAGE_AVAILABLE, _TwoStageProcessor, CHROMA_PRESETS, TWO_STAGE_IMPORT_PATH = _import_two_stage()
 
 
 
112
 
113
  # ╔══════════════════════════════════════════════════════════════════════════╗
114
  # β•‘ VideoProcessor class β•‘
@@ -118,9 +147,6 @@ class VideoProcessor:
118
  Main orchestrator – coordinates all specialised components.
119
  """
120
 
121
- # ─────────────────────────────────────────────────────────────────────
122
- # Init
123
- # ─────────────────────────────────────────────────────────────────────
124
  def __init__(self):
125
  self.config = get_config()
126
  self._patch_config_defaults(self.config) # ensure missing attrs exist
@@ -146,14 +172,20 @@ def __init__(self):
146
  @staticmethod
147
  def _patch_config_defaults(cfg: Any) -> None:
148
  """
149
- Some downstream modules (FFmpeg/OpenCV writers) may read fields that
150
- older configs don't define (e.g., use_nvenc). Add safe defaults here.
151
  """
152
  defaults = {
153
- "use_nvenc": False, # guards "'ProcessingConfig' has no attribute 'use_nvenc'"
 
154
  "prefer_mp4": True,
155
- "video_codec": "mp4v", # OpenCV fallback codec
156
- "audio_copy": True, # for ffmpeg muxing, if used downstream
 
 
 
 
 
157
  "output_dir": str(ROOT / "outputs"),
158
  }
159
  for k, v in defaults.items():
@@ -164,7 +196,6 @@ def _patch_config_defaults(cfg: Any) -> None:
164
  try:
165
  Path(cfg.output_dir).mkdir(parents=True, exist_ok=True)
166
  except Exception:
167
- # Last-resort fallback to a temp-like folder under ROOT
168
  out = ROOT / "outputs"
169
  out.mkdir(parents=True, exist_ok=True)
170
  cfg.output_dir = str(out)
@@ -223,10 +254,7 @@ def load_models(self, progress_callback: Optional[Callable] = None) -> str:
223
  self.two_stage_processor = _TwoStageProcessor(
224
  sam2_predictor=sam2_predictor, matanyone_model=mat_model
225
  )
226
- logger.info(
227
- "Two-stage processor initialised (imported from '%s')",
228
- TWO_STAGE_IMPORT_PATH or "unknown",
229
- )
230
  except Exception as e:
231
  logger.warning("Two-stage init failed: %r\n%s", e, traceback.format_exc())
232
  self.two_stage_processor = None
@@ -264,13 +292,10 @@ def process_video(
264
  progress_callback: Optional[Callable] = None,
265
  use_two_stage: bool = False,
266
  chroma_preset: str = "standard",
267
- key_color_mode: str = "auto", # NEW
268
  preview_mask: bool = False,
269
  preview_greenscreen: bool = False,
270
  ) -> Tuple[Optional[str], str]:
271
- """
272
- Dispatch to single-stage or two-stage pipeline.
273
- """
274
  if not self.models_loaded or not self.core_processor:
275
  return None, "Models not loaded. Please click β€œLoad Models” first."
276
  if self.cancel_event.is_set():
@@ -292,7 +317,7 @@ def process_video(
292
  custom_background_path,
293
  progress_callback,
294
  chroma_preset,
295
- key_color_mode, # NEW
296
  )
297
  else:
298
  return self._process_single_stage(
@@ -373,7 +398,7 @@ def _process_two_stage(
373
  custom_background_path: Optional[str],
374
  progress_callback: Optional[Callable],
375
  chroma_preset: str,
376
- key_color_mode: str, # NEW
377
  ) -> Tuple[Optional[str], str]:
378
  if self.two_stage_processor is None:
379
  return None, "Two-stage processor not available"
@@ -407,15 +432,15 @@ def _process_two_stage(
407
 
408
  chroma_cfg = CHROMA_PRESETS.get(chroma_preset, CHROMA_PRESETS.get("standard", {}))
409
  logger.info(
410
- "Two-stage with preset: %s | key_color_mode=%s | import=%s",
411
- chroma_preset, key_color_mode, TWO_STAGE_IMPORT_PATH or "unknown"
412
  )
413
 
414
  result, message = self.two_stage_processor.process_full_pipeline(
415
  video_path,
416
  background,
417
  final_out,
418
- key_color_mode=key_color_mode, # NEW
419
  chroma_settings=chroma_cfg,
420
  progress_callback=progress_callback,
421
  )
@@ -428,7 +453,7 @@ def _process_two_stage(
428
  original_video=video_path, processed_video=result
429
  )
430
  except Exception as e:
431
- logger.warning("Audio mux failed for two-stage, returning video without audio: %r", e)
432
  final_path = result
433
 
434
  msg = (
@@ -446,7 +471,8 @@ def get_status(self) -> Dict[str, Any]:
446
  status = {
447
  "models_loaded": self.models_loaded,
448
  "two_stage_available": bool(TWO_STAGE_AVAILABLE and (self.two_stage_processor is not None)),
449
- "two_stage_import_path": TWO_STAGE_IMPORT_PATH or "",
 
450
  "device": str(self.device_manager.get_optimal_device()),
451
  "core_processor_loaded": self.core_processor is not None,
452
  "config": self._safe_config_dict(),
@@ -467,8 +493,8 @@ def _safe_config_dict(self) -> Dict[str, Any]:
467
  try:
468
  return self.config.to_dict()
469
  except Exception:
470
- # Build a minimal dict view with known fields
471
- keys = ["use_nvenc", "prefer_mp4", "video_codec", "audio_copy", "output_dir"]
472
  return {k: getattr(self.config, k, None) for k in keys}
473
 
474
  def _safe_memory_usage(self) -> Dict[str, Any]:
@@ -508,7 +534,7 @@ def process_video_fixed(
508
  progress_callback: Optional[Callable] = None,
509
  use_two_stage: bool = False,
510
  chroma_preset: str = "standard",
511
- key_color_mode: str = "auto", # NEW
512
  preview_mask: bool = False,
513
  preview_greenscreen: bool = False,
514
  ) -> Tuple[Optional[str], str]:
@@ -519,7 +545,7 @@ def process_video_fixed(
519
  progress_callback,
520
  use_two_stage,
521
  chroma_preset,
522
- key_color_mode, # NEW
523
  preview_mask,
524
  preview_greenscreen,
525
  )
@@ -528,7 +554,6 @@ def get_model_status() -> Dict[str, Any]:
528
  return processor.get_status()
529
 
530
  def get_cache_status() -> Dict[str, Any]:
531
- # Placeholder – could expose FS cache size, etc.
532
  return processor.get_status()
533
 
534
  PROCESS_CANCELLED = processor.cancel_event
@@ -542,13 +567,11 @@ def main():
542
  logger.info("Starting BackgroundFX Pro")
543
  logger.info(f"Device: {processor.device_manager.get_optimal_device()}")
544
  logger.info(
545
- "Two-stage available at import-time: %s (path='%s')",
546
- TWO_STAGE_AVAILABLE, TWO_STAGE_IMPORT_PATH or "unknown"
547
  )
548
 
549
- # UI lives in ui/ui_components.py (recent rename from ui/components.py)
550
  from ui.ui_components import create_interface
551
-
552
  demo = create_interface()
553
  demo.queue().launch(
554
  server_name="0.0.0.0",
 
3
  BackgroundFX Pro – Main Application Entry Point
4
  Refactored modular architecture – orchestrates specialised components
5
 
6
+ 2025-08-27 update:
7
+ - Robust Two-Stage importer: tries package import, then direct file import (bypasses
8
+ any side-effect errors in processing/__init__.py). Clear logging of where it loaded.
9
+ - Config hardening: adds safe defaults for fields like max_model_size/use_nvenc/etc.
10
+ - Defensive error logs with tracebacks for easier diagnosis.
 
11
  """
12
 
13
  from __future__ import annotations
 
22
  import traceback
23
  import sys
24
  import os
25
+ import importlib
26
+ import importlib.util
27
  from pathlib import Path
28
  from typing import Optional, Tuple, Dict, Any, Callable
29
 
 
36
  )
37
  logger = logging.getLogger("core.app")
38
 
39
+ # Ensure project root is importable (helps HF Spaces and local runs)
 
 
40
  PROJECT_FILE = Path(__file__).resolve()
41
  CORE_DIR = PROJECT_FILE.parent
42
  ROOT = CORE_DIR.parent
 
81
  from utils.cv_processing import validate_video_file
82
 
83
  # ─────────────────────────────────────────────────────────────────────────────
84
+ # 3.1) Optional: Two-stage import with package and file fallbacks
85
  # ─────────────────────────────────────────────────────────────────────────────
86
+ def _import_two_stage() -> tuple[bool, Any, Dict[str, Any], str, str]:
87
  """
88
+ Returns (available, TwoStageProcessor|None, CHROMA_PRESETS|{}, import_origin, error_text)
89
+
90
+ Tries:
91
+ 1) package imports (preferred)
92
+ 2) direct file imports (bypass processing/__init__.py side-effects)
93
  """
94
+ pkg_paths = [
95
  "processing.two_stage.two_stage_processor",
96
  "two_stage_processor",
97
  "processing.two_stage_processor",
98
  ]
99
+ fs_paths = [
100
+ ROOT / "processing" / "two_stage" / "two_stage_processor.py",
101
+ ROOT / "processing" / "two_stage_processor.py",
102
+ ROOT / "two_stage_processor.py",
103
+ ]
104
+
105
+ # Package imports
106
+ last_err_text = ""
107
+ for mod_path in pkg_paths:
108
+ try:
109
+ mod = importlib.import_module(mod_path)
110
+ TSP = getattr(mod, "TwoStageProcessor")
111
+ PRESETS = getattr(mod, "CHROMA_PRESETS", {"standard": {}})
112
+ logger.info(f"Two-stage import OK from package '{mod_path}'")
113
+ return True, TSP, PRESETS, f"pkg:{mod_path}", ""
114
+ except Exception as e:
115
+ tb = traceback.format_exc()
116
+ last_err_text += f"[pkg:{mod_path}] {repr(e)}\n{tb}\n"
117
+
118
+ # Direct file imports (bypass __init__.py)
119
+ for path in fs_paths:
120
  try:
121
+ if not path.exists():
122
+ continue
123
+ spec = importlib.util.spec_from_file_location("bx_two_stage", str(path))
124
+ if not spec or not spec.loader:
125
+ continue
126
+ mod = importlib.util.module_from_spec(spec)
127
+ sys.modules["bx_two_stage"] = mod
128
+ spec.loader.exec_module(mod) # type: ignore[attr-defined]
129
+ TSP = getattr(mod, "TwoStageProcessor")
130
+ PRESETS = getattr(mod, "CHROMA_PRESETS", {"standard": {}})
131
+ logger.info(f"Two-stage import OK from file '{path}'")
132
+ return True, TSP, PRESETS, f"file:{path}", ""
133
  except Exception as e:
134
+ tb = traceback.format_exc()
135
+ last_err_text += f"[file:{path}] {repr(e)}\n{tb}\n"
 
 
136
 
137
+ logger.error("Two-stage import failed from all paths:\n%s", last_err_text or "(no traceback)")
138
+ return False, None, {"standard": {}}, "", last_err_text or "(no traceback)"
139
+
140
+ TWO_STAGE_AVAILABLE, _TwoStageProcessor, CHROMA_PRESETS, TWO_STAGE_IMPORT_ORIGIN, TWO_STAGE_IMPORT_ERROR = _import_two_stage()
141
 
142
  # ╔══════════════════════════════════════════════════════════════════════════╗
143
  # β•‘ VideoProcessor class β•‘
 
147
  Main orchestrator – coordinates all specialised components.
148
  """
149
 
 
 
 
150
  def __init__(self):
151
  self.config = get_config()
152
  self._patch_config_defaults(self.config) # ensure missing attrs exist
 
172
  @staticmethod
173
  def _patch_config_defaults(cfg: Any) -> None:
174
  """
175
+ Some downstream modules may read fields that older configs don't define.
176
+ Add safe defaults here so AttributeErrors never occur.
177
  """
178
  defaults = {
179
+ # video i/o & writer
180
+ "use_nvenc": False,
181
  "prefer_mp4": True,
182
+ "video_codec": "mp4v",
183
+ "audio_copy": True,
184
+ "ffmpeg_path": "ffmpeg",
185
+ # model/resource guards
186
+ "max_model_size": 0, # bytes or MB; downstream should treat 0/None as "no limit"
187
+ "max_model_size_bytes": 0,
188
+ # housekeeping
189
  "output_dir": str(ROOT / "outputs"),
190
  }
191
  for k, v in defaults.items():
 
196
  try:
197
  Path(cfg.output_dir).mkdir(parents=True, exist_ok=True)
198
  except Exception:
 
199
  out = ROOT / "outputs"
200
  out.mkdir(parents=True, exist_ok=True)
201
  cfg.output_dir = str(out)
 
254
  self.two_stage_processor = _TwoStageProcessor(
255
  sam2_predictor=sam2_predictor, matanyone_model=mat_model
256
  )
257
+ logger.info("Two-stage processor initialised (%s)", TWO_STAGE_IMPORT_ORIGIN or "unknown")
 
 
 
258
  except Exception as e:
259
  logger.warning("Two-stage init failed: %r\n%s", e, traceback.format_exc())
260
  self.two_stage_processor = None
 
292
  progress_callback: Optional[Callable] = None,
293
  use_two_stage: bool = False,
294
  chroma_preset: str = "standard",
295
+ key_color_mode: str = "auto",
296
  preview_mask: bool = False,
297
  preview_greenscreen: bool = False,
298
  ) -> Tuple[Optional[str], str]:
 
 
 
299
  if not self.models_loaded or not self.core_processor:
300
  return None, "Models not loaded. Please click β€œLoad Models” first."
301
  if self.cancel_event.is_set():
 
317
  custom_background_path,
318
  progress_callback,
319
  chroma_preset,
320
+ key_color_mode,
321
  )
322
  else:
323
  return self._process_single_stage(
 
398
  custom_background_path: Optional[str],
399
  progress_callback: Optional[Callable],
400
  chroma_preset: str,
401
+ key_color_mode: str,
402
  ) -> Tuple[Optional[str], str]:
403
  if self.two_stage_processor is None:
404
  return None, "Two-stage processor not available"
 
432
 
433
  chroma_cfg = CHROMA_PRESETS.get(chroma_preset, CHROMA_PRESETS.get("standard", {}))
434
  logger.info(
435
+ "Two-stage with preset: %s | key_color_mode=%s | origin=%s",
436
+ chroma_preset, key_color_mode, TWO_STAGE_IMPORT_ORIGIN or "unknown"
437
  )
438
 
439
  result, message = self.two_stage_processor.process_full_pipeline(
440
  video_path,
441
  background,
442
  final_out,
443
+ key_color_mode=key_color_mode,
444
  chroma_settings=chroma_cfg,
445
  progress_callback=progress_callback,
446
  )
 
453
  original_video=video_path, processed_video=result
454
  )
455
  except Exception as e:
456
+ logger.warning("Audio mux failed for two-stage; returning video without audio: %r", e)
457
  final_path = result
458
 
459
  msg = (
 
471
  status = {
472
  "models_loaded": self.models_loaded,
473
  "two_stage_available": bool(TWO_STAGE_AVAILABLE and (self.two_stage_processor is not None)),
474
+ "two_stage_origin": TWO_STAGE_IMPORT_ORIGIN or "",
475
+ "two_stage_error": TWO_STAGE_IMPORT_ERROR[:5000] if TWO_STAGE_IMPORT_ERROR else "",
476
  "device": str(self.device_manager.get_optimal_device()),
477
  "core_processor_loaded": self.core_processor is not None,
478
  "config": self._safe_config_dict(),
 
493
  try:
494
  return self.config.to_dict()
495
  except Exception:
496
+ keys = ["use_nvenc", "prefer_mp4", "video_codec", "audio_copy",
497
+ "ffmpeg_path", "max_model_size", "max_model_size_bytes", "output_dir"]
498
  return {k: getattr(self.config, k, None) for k in keys}
499
 
500
  def _safe_memory_usage(self) -> Dict[str, Any]:
 
534
  progress_callback: Optional[Callable] = None,
535
  use_two_stage: bool = False,
536
  chroma_preset: str = "standard",
537
+ key_color_mode: str = "auto",
538
  preview_mask: bool = False,
539
  preview_greenscreen: bool = False,
540
  ) -> Tuple[Optional[str], str]:
 
545
  progress_callback,
546
  use_two_stage,
547
  chroma_preset,
548
+ key_color_mode,
549
  preview_mask,
550
  preview_greenscreen,
551
  )
 
554
  return processor.get_status()
555
 
556
  def get_cache_status() -> Dict[str, Any]:
 
557
  return processor.get_status()
558
 
559
  PROCESS_CANCELLED = processor.cancel_event
 
567
  logger.info("Starting BackgroundFX Pro")
568
  logger.info(f"Device: {processor.device_manager.get_optimal_device()}")
569
  logger.info(
570
+ "Two-stage available at import-time: %s (origin='%s')",
571
+ TWO_STAGE_AVAILABLE, TWO_STAGE_IMPORT_ORIGIN or "unknown"
572
  )
573
 
 
574
  from ui.ui_components import create_interface
 
575
  demo = create_interface()
576
  demo.queue().launch(
577
  server_name="0.0.0.0",