MogensR commited on
Commit
dc14288
·
1 Parent(s): b47d3c0

Update core/app.py

Browse files
Files changed (1) hide show
  1. core/app.py +146 -265
core/app.py CHANGED
@@ -4,27 +4,27 @@
4
  Refactored modular architecture - orchestrates specialized components
5
  """
6
 
7
- import early_env # <<< centralizes the OMP/torch thread fix; must be first
 
8
 
9
- import os
10
  import logging
11
  import threading
12
  from pathlib import Path
13
  from typing import Optional, Tuple, Dict, Any, Callable
14
 
15
- # Configure logging first
16
  logging.basicConfig(
17
  level=logging.INFO,
18
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
19
  )
20
- logger = logging.getLogger(__name__)
21
 
22
- # Apply Gradio schema patch early (before other imports)
23
  try:
24
  import gradio_client.utils as gc_utils
25
- original_get_type = gc_utils.get_type
26
 
27
- def patched_get_type(schema):
28
  if not isinstance(schema, dict):
29
  if isinstance(schema, bool):
30
  return "boolean"
@@ -33,18 +33,15 @@ def patched_get_type(schema):
33
  if isinstance(schema, (int, float)):
34
  return "number"
35
  return "string"
36
- return original_get_type(schema)
37
 
38
- gc_utils.get_type = patched_get_type
39
- logger.info("Gradio schema patch applied successfully")
40
  except Exception as e:
41
- logger.error(f"Gradio patch failed: {e}")
42
 
43
- # Import configuration from new location
44
- from processing.video.video_processor import ProcessorConfig
45
  from config.app_config import get_config
46
-
47
- # Import core components from new locations
48
  from core.exceptions import ModelLoadingError, VideoProcessingError
49
  from utils.hardware.device_manager import DeviceManager
50
  from utils.system.memory_manager import MemoryManager
@@ -53,179 +50,133 @@ def patched_get_type(schema):
53
  from processing.audio.audio_processor import AudioProcessor
54
  from utils.monitoring.progress_tracker import ProgressTracker
55
 
56
- # Import existing utilities (temporary during migration)
57
- from utilities import (
58
- segment_person_hq,
59
- refine_mask_hq,
60
- replace_background_hq,
61
- create_professional_background,
62
- PROFESSIONAL_BACKGROUNDS,
63
- validate_video_file
64
- )
65
-
66
- # Import two-stage processor if available
67
  try:
68
  from processing.two_stage.two_stage_processor import TwoStageProcessor, CHROMA_PRESETS
69
  TWO_STAGE_AVAILABLE = True
70
- except ImportError:
71
  TWO_STAGE_AVAILABLE = False
72
- CHROMA_PRESETS = {'standard': {}}
 
 
 
73
 
74
 
75
  class VideoProcessor:
76
  """
77
- Main video processing orchestrator - coordinates all specialized components
78
  """
79
-
80
  def __init__(self):
81
- """Initialize the video processor with all required components"""
82
- self.config = get_config() # Use singleton config
83
  self.device_manager = DeviceManager()
 
 
84
  self.memory_manager = MemoryManager(self.device_manager.get_optimal_device())
85
 
86
- # Initialize ModelLoader with DeviceManager and MemoryManager (as per actual implementation)
87
  self.model_loader = ModelLoader(self.device_manager, self.memory_manager)
88
 
89
  self.audio_processor = AudioProcessor()
90
- self.progress_tracker = None
 
91
 
92
- # Initialize core processor (will be set up after models load)
93
- self.core_processor = None
94
- self.two_stage_processor = None
95
-
96
- # State management
97
  self.models_loaded = False
98
  self.loading_lock = threading.Lock()
99
  self.cancel_event = threading.Event()
 
100
 
101
- logger.info(f"VideoProcessor initialized on device: {self.device_manager.get_optimal_device()}")
102
 
103
- def _initialize_progress_tracker(self, video_path: str, progress_callback: Optional[Callable] = None):
104
- """Initialize progress tracker with video frame count"""
105
  try:
106
  import cv2
107
  cap = cv2.VideoCapture(video_path)
108
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
109
  cap.release()
110
-
111
- if total_frames <= 0:
112
- total_frames = 100 # Fallback estimate
113
-
114
- self.progress_tracker = ProgressTracker(total_frames, progress_callback)
115
- logger.info(f"Progress tracker initialized for {total_frames} frames")
116
  except Exception as e:
117
- logger.warning(f"Could not initialize progress tracker: {e}")
118
- # Fallback to basic tracker
119
- self.progress_tracker = ProgressTracker(100, progress_callback)
120
 
 
121
  def load_models(self, progress_callback: Optional[Callable] = None) -> str:
122
- """Load and validate all AI models"""
123
  with self.loading_lock:
124
  if self.models_loaded:
125
  return "Models already loaded and validated"
126
 
127
  try:
128
  self.cancel_event.clear()
129
-
130
  if progress_callback:
131
- progress_callback(0.0, f"Starting model loading on {self.device_manager.get_optimal_device()}")
132
 
133
- # Add detailed debugging for the IndexError
134
  try:
135
- # Load models using load_all_models which returns tuple of (LoadedModel, LoadedModel)
136
- sam2_result, matanyone_result = self.model_loader.load_all_models(
137
  progress_callback=progress_callback,
138
  cancel_event=self.cancel_event
139
  )
140
-
141
  except IndexError as e:
142
  import traceback
143
- logger.error(f"IndexError in load_all_models: {e}")
144
- logger.error(f"Full traceback:\n{traceback.format_exc()}")
145
-
146
- # Get more context about where exactly the error happened
147
  tb = traceback.extract_tb(e.__traceback__)
148
- for frame in tb:
149
- logger.error(f" File: {frame.filename}, Line: {frame.lineno}, Function: {frame.name}")
150
- logger.error(f" Code: {frame.line}")
151
-
152
- # Re-raise with more context
153
- raise ModelLoadingError(f"Model loading failed with IndexError at line {tb[-1].lineno}: {e}")
154
-
155
  except Exception as e:
156
  import traceback
157
- logger.error(f"Unexpected error in load_all_models: {e}")
158
- logger.error(f"Error type: {type(e).__name__}")
159
- logger.error(f"Full traceback:\n{traceback.format_exc()}")
160
  raise
161
 
162
  if self.cancel_event.is_set():
163
  return "Model loading cancelled"
164
 
165
- # Extract actual models from LoadedModel wrappers for two-stage processor
166
- sam2_predictor = sam2_result.model if sam2_result else None
167
- matanyone_model = matanyone_result.model if matanyone_result else None
168
-
169
- # Check if at least one model loaded successfully
170
- success = sam2_predictor is not None or matanyone_model is not None
171
-
172
- if not success:
173
- return "Model loading failed - check logs for details"
174
 
175
- # Initialize core processor with the model loader (it expects a models object)
176
  self.core_processor = CoreVideoProcessor(
177
  config=self.config,
178
- models=self.model_loader # Pass the whole model_loader object
179
  )
180
 
181
- # Initialize two-stage processor if available and models loaded
182
- if TWO_STAGE_AVAILABLE:
183
- if sam2_predictor is not None or matanyone_model is not None:
184
- try:
185
- # Two-stage processor needs the actual models
186
- self.two_stage_processor = TwoStageProcessor(
187
- sam2_predictor=sam2_predictor,
188
- matanyone_model=matanyone_model
189
- )
190
- logger.info("Two-stage processor initialized with AI models")
191
- except Exception as e:
192
- logger.warning(f"Two-stage processor init failed: {e}")
193
- self.two_stage_processor = None
194
- else:
195
- logger.warning("Two-stage processor not initialized - models not available")
196
- if sam2_predictor is None:
197
- logger.warning(" - SAM2 predictor is None")
198
- if matanyone_model is None:
199
- logger.warning(" - MatAnyone model is None")
200
 
201
  self.models_loaded = True
202
- message = self.model_loader.get_load_summary()
203
-
204
- # Add two-stage status to message
205
- if self.two_stage_processor is not None:
206
- message += "\n✅ Two-stage processor ready with AI models"
207
  else:
208
- message += "\n⚠️ Two-stage processor not available"
209
-
210
- logger.info(message)
211
- return message
212
 
213
- except AttributeError as e:
214
- self.models_loaded = False
215
- error_msg = f"Model loading failed - method not found: {str(e)}"
216
- logger.error(error_msg)
217
- return error_msg
218
- except ModelLoadingError as e:
219
  self.models_loaded = False
220
- error_msg = f"Model loading failed: {str(e)}"
221
- logger.error(error_msg)
222
- return error_msg
223
  except Exception as e:
224
  self.models_loaded = False
225
- error_msg = f"Unexpected error during model loading: {str(e)}"
226
- logger.error(error_msg)
227
- return error_msg
228
 
 
229
  def process_video(
230
  self,
231
  video_path: str,
@@ -235,51 +186,37 @@ def process_video(
235
  use_two_stage: bool = False,
236
  chroma_preset: str = "standard",
237
  preview_mask: bool = False,
238
- preview_greenscreen: bool = False
239
  ) -> Tuple[Optional[str], str]:
240
- """Process video with the specified parameters"""
241
 
242
  if not self.models_loaded or not self.core_processor:
243
- return None, "Models not loaded. Please load models first."
244
 
245
  if self.cancel_event.is_set():
246
  return None, "Processing cancelled"
247
 
248
- # Initialize progress tracker with video frame count
249
- self._initialize_progress_tracker(video_path, progress_callback)
250
 
251
- # Validate input file
252
- is_valid, validation_msg = validate_video_file(video_path)
253
- if not is_valid:
254
- return None, f"Invalid video: {validation_msg}"
255
 
256
  try:
257
- # Route to appropriate processing method
258
  if use_two_stage:
259
  if not TWO_STAGE_AVAILABLE:
260
- return None, "Two-stage processing not available - module not found"
261
-
262
- if self.two_stage_processor is None:
263
- return None, "Two-stage processor not initialized - models may not be loaded properly"
264
-
265
- logger.info("Using two-stage processing pipeline with AI models")
266
- return self._process_two_stage(
267
- video_path, background_choice, custom_background_path,
268
- progress_callback, chroma_preset
269
- )
270
  else:
271
- logger.info("Using single-stage processing pipeline")
272
- return self._process_single_stage(
273
- video_path, background_choice, custom_background_path,
274
- progress_callback, preview_mask, preview_greenscreen
275
- )
276
 
277
  except VideoProcessingError as e:
278
- logger.error(f"Video processing failed: {e}")
279
- return None, f"Processing failed: {str(e)}"
280
  except Exception as e:
281
- logger.error(f"Unexpected error during video processing: {e}")
282
- return None, f"Unexpected error: {str(e)}"
283
 
284
  def _process_single_stage(
285
  self,
@@ -288,45 +225,36 @@ def _process_single_stage(
288
  custom_background_path: Optional[str],
289
  progress_callback: Optional[Callable],
290
  preview_mask: bool,
291
- preview_greenscreen: bool
292
  ) -> Tuple[Optional[str], str]:
293
- """Process video using single-stage pipeline"""
294
 
295
- # Generate output path
296
  import time
297
- timestamp = int(time.time())
298
- output_dir = Path(self.config.output_dir) / "single_stage"
299
- output_dir.mkdir(parents=True, exist_ok=True)
300
- output_path = str(output_dir / f"processed_{timestamp}.mp4")
301
 
302
- # Process video using core processor
303
  result = self.core_processor.process_video(
304
  input_path=video_path,
305
- output_path=output_path,
306
- bg_config={'background_choice': background_choice, 'custom_path': custom_background_path}
307
  )
308
-
309
  if not result:
310
  return None, "Video processing failed"
311
 
312
- # Add audio if not in preview mode
313
  if not (preview_mask or preview_greenscreen):
314
- final_video_path = self.audio_processor.add_audio_to_video(
315
- original_video=video_path,
316
- processed_video=output_path
317
- )
318
  else:
319
- final_video_path = output_path
320
 
321
- success_msg = (
322
- f"Processing completed successfully!\n"
323
- f"Frames processed: {result.get('frames', 'unknown')}\n"
324
  f"Background: {background_choice}\n"
325
  f"Mode: Single-stage\n"
326
  f"Device: {self.device_manager.get_optimal_device()}"
327
  )
328
-
329
- return final_video_path, success_msg
330
 
331
  def _process_two_stage(
332
  self,
@@ -334,117 +262,82 @@ def _process_two_stage(
334
  background_choice: str,
335
  custom_background_path: Optional[str],
336
  progress_callback: Optional[Callable],
337
- chroma_preset: str
338
  ) -> Tuple[Optional[str], str]:
339
- """Process video using two-stage pipeline"""
340
-
341
  if self.two_stage_processor is None:
342
  return None, "Two-stage processor not available"
343
 
344
- # Get video dimensions for background preparation
345
- import cv2
346
  cap = cv2.VideoCapture(video_path)
347
- frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
348
- frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
349
  cap.release()
350
 
351
- # Prepare background using core processor
352
- background = self.core_processor.prepare_background(
353
- background_choice, custom_background_path, frame_width, frame_height
354
- )
355
  if background is None:
356
  return None, "Failed to prepare background"
357
 
358
- # Process with two-stage pipeline
359
- import time
360
- timestamp = int(time.time())
361
- output_dir = Path(self.config.output_dir) / "two_stage"
362
- output_dir.mkdir(parents=True, exist_ok=True)
363
- final_output = str(output_dir / f"final_{timestamp}.mp4")
364
 
365
- chroma_settings = CHROMA_PRESETS.get(chroma_preset, CHROMA_PRESETS['standard'])
 
366
 
367
- logger.info(f"Starting two-stage processing with chroma preset: {chroma_preset}")
368
  result, message = self.two_stage_processor.process_full_pipeline(
369
- video_path,
370
- background,
371
- final_output,
372
- chroma_settings=chroma_settings,
373
- progress_callback=progress_callback
374
  )
375
-
376
  if result is None:
377
  return None, message
378
 
379
- success_msg = (
380
- f"Two-stage processing completed!\n"
381
  f"Background: {background_choice}\n"
382
  f"Chroma Preset: {chroma_preset}\n"
383
- f"Quality: Cinema-grade with AI models\n"
384
  f"Device: {self.device_manager.get_optimal_device()}"
385
  )
 
386
 
387
- return result, success_msg
388
-
389
  def get_status(self) -> Dict[str, Any]:
390
- """Get comprehensive status of all components"""
391
- base_status = {
392
- 'models_loaded': self.models_loaded,
393
- 'two_stage_available': TWO_STAGE_AVAILABLE and self.two_stage_processor is not None,
394
- 'device': str(self.device_manager.get_optimal_device()),
395
- 'memory_usage': self.memory_manager.get_memory_usage(),
396
- 'config': self.config.to_dict()
397
  }
 
 
 
 
 
 
398
 
399
- # Add model-specific status if available
400
- if self.model_loader:
401
- base_status['model_loader_available'] = True
402
- try:
403
- base_status['sam2_loaded'] = self.model_loader.get_sam2() is not None
404
- base_status['matanyone_loaded'] = self.model_loader.get_matanyone() is not None
405
- except AttributeError:
406
- base_status['sam2_loaded'] = False
407
- base_status['matanyone_loaded'] = False
408
-
409
- # Add processing status if available
410
- if self.core_processor:
411
- base_status['core_processor_loaded'] = True
412
-
413
- # Add two-stage processor status
414
- if self.two_stage_processor:
415
- base_status['two_stage_processor_ready'] = True
416
- else:
417
- base_status['two_stage_processor_ready'] = False
418
-
419
- # Add progress tracking if available
420
  if self.progress_tracker:
421
- base_status['progress'] = self.progress_tracker.get_all_progress()
422
-
423
- return base_status
424
 
425
  def cancel_processing(self):
426
- """Cancel any ongoing processing"""
427
  self.cancel_event.set()
428
- logger.info("Processing cancellation requested")
429
 
430
  def cleanup_resources(self):
431
- """Clean up all resources"""
432
  self.memory_manager.cleanup_aggressive()
433
  if self.model_loader:
434
  self.model_loader.cleanup()
435
  logger.info("Resources cleaned up")
436
 
437
 
438
- # Global processor instance for application
439
  processor = VideoProcessor()
440
 
441
-
442
- # Backward compatibility functions for existing UI
443
  def load_models_with_validation(progress_callback: Optional[Callable] = None) -> str:
444
- """Load models with validation - backward compatibility wrapper"""
445
  return processor.load_models(progress_callback)
446
 
447
-
448
  def process_video_fixed(
449
  video_path: str,
450
  background_choice: str,
@@ -453,55 +346,43 @@ def process_video_fixed(
453
  use_two_stage: bool = False,
454
  chroma_preset: str = "standard",
455
  preview_mask: bool = False,
456
- preview_greenscreen: bool = False
457
  ) -> Tuple[Optional[str], str]:
458
- """Process video - backward compatibility wrapper"""
459
  return processor.process_video(
460
  video_path, background_choice, custom_background_path,
461
- progress_callback, use_two_stage, chroma_preset,
462
- preview_mask, preview_greenscreen
463
  )
464
 
465
-
466
  def get_model_status() -> Dict[str, Any]:
467
- """Get model status - backward compatibility wrapper"""
468
  return processor.get_status()
469
 
470
-
471
  def get_cache_status() -> Dict[str, Any]:
472
- """Get cache status - backward compatibility wrapper"""
473
  return processor.get_status()
474
 
475
-
476
- # For backward compatibility
477
  PROCESS_CANCELLED = processor.cancel_event
478
 
479
 
480
  def main():
481
- """Main application entry point"""
482
  try:
483
- logger.info("Starting Video Background Replacement application")
484
  logger.info(f"Device: {processor.device_manager.get_optimal_device()}")
485
- logger.info(f"Two-stage module available: {TWO_STAGE_AVAILABLE}")
486
- logger.info("Modular architecture loaded successfully")
487
 
488
- # Import and create UI
489
- from ui_components import create_interface
490
  demo = create_interface()
491
 
492
- # Launch application (no share=True on Spaces)
493
  demo.queue().launch(
494
  server_name="0.0.0.0",
495
  server_port=7860,
496
  show_error=True,
497
  debug=False
498
  )
499
-
500
  except Exception as e:
501
- logger.error(f"Application startup failed: {e}")
502
  raise
503
  finally:
504
- # Cleanup on exit
505
  processor.cleanup_resources()
506
 
507
 
 
4
  Refactored modular architecture - orchestrates specialized components
5
  """
6
 
7
+ # 0) Early env/threading hygiene (must be first)
8
+ import early_env # sets OMP/MKL/OPENBLAS + torch threads safely
9
 
 
10
  import logging
11
  import threading
12
  from pathlib import Path
13
  from typing import Optional, Tuple, Dict, Any, Callable
14
 
15
+ # 1) Logging
16
  logging.basicConfig(
17
  level=logging.INFO,
18
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
19
  )
20
+ logger = logging.getLogger("core.app")
21
 
22
+ # 2) Patch Gradio schema early (HF Spaces quirk)
23
  try:
24
  import gradio_client.utils as gc_utils
25
+ _orig_get_type = gc_utils.get_type
26
 
27
+ def _patched_get_type(schema):
28
  if not isinstance(schema, dict):
29
  if isinstance(schema, bool):
30
  return "boolean"
 
33
  if isinstance(schema, (int, float)):
34
  return "number"
35
  return "string"
36
+ return _orig_get_type(schema)
37
 
38
+ gc_utils.get_type = _patched_get_type
39
+ logger.info("Gradio schema patch applied")
40
  except Exception as e:
41
+ logger.warning(f"Gradio patch failed: {e}")
42
 
43
+ # 3) Core config + components
 
44
  from config.app_config import get_config
 
 
45
  from core.exceptions import ModelLoadingError, VideoProcessingError
46
  from utils.hardware.device_manager import DeviceManager
47
  from utils.system.memory_manager import MemoryManager
 
50
  from processing.audio.audio_processor import AudioProcessor
51
  from utils.monitoring.progress_tracker import ProgressTracker
52
 
53
+ # Optional two-stage processor
 
 
 
 
 
 
 
 
 
 
54
  try:
55
  from processing.two_stage.two_stage_processor import TwoStageProcessor, CHROMA_PRESETS
56
  TWO_STAGE_AVAILABLE = True
57
+ except Exception:
58
  TWO_STAGE_AVAILABLE = False
59
+ CHROMA_PRESETS = {"standard": {}}
60
+
61
+ # Validation helper for inputs (lives with CV utils)
62
+ from utils.cv_processing import validate_video_file
63
 
64
 
65
  class VideoProcessor:
66
  """
67
+ Main video processing orchestrator - coordinates all specialized components.
68
  """
 
69
  def __init__(self):
70
+ self.config = get_config() # singleton-style app config
 
71
  self.device_manager = DeviceManager()
72
+
73
+ # Memory manager now requires a device object/string
74
  self.memory_manager = MemoryManager(self.device_manager.get_optimal_device())
75
 
76
+ # Model loader takes device + memory managers
77
  self.model_loader = ModelLoader(self.device_manager, self.memory_manager)
78
 
79
  self.audio_processor = AudioProcessor()
80
+ self.core_processor: CoreVideoProcessor | None = None
81
+ self.two_stage_processor: TwoStageProcessor | None = None
82
 
83
+ # State
 
 
 
 
84
  self.models_loaded = False
85
  self.loading_lock = threading.Lock()
86
  self.cancel_event = threading.Event()
87
+ self.progress_tracker: ProgressTracker | None = None
88
 
89
+ logger.info(f"VideoProcessor on device: {self.device_manager.get_optimal_device()}")
90
 
91
+ # ---------- Progress ----------
92
+ def _init_progress(self, video_path: str, cb: Optional[Callable] = None):
93
  try:
94
  import cv2
95
  cap = cv2.VideoCapture(video_path)
96
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
97
  cap.release()
98
+ if total <= 0:
99
+ total = 100
100
+ self.progress_tracker = ProgressTracker(total, cb)
 
 
 
101
  except Exception as e:
102
+ logger.warning(f"Progress init failed: {e}")
103
+ self.progress_tracker = ProgressTracker(100, cb)
 
104
 
105
+ # ---------- Load Models ----------
106
  def load_models(self, progress_callback: Optional[Callable] = None) -> str:
 
107
  with self.loading_lock:
108
  if self.models_loaded:
109
  return "Models already loaded and validated"
110
 
111
  try:
112
  self.cancel_event.clear()
 
113
  if progress_callback:
114
+ progress_callback(0.0, f"Loading on {self.device_manager.get_optimal_device()}")
115
 
 
116
  try:
117
+ sam2_loaded, mat_loaded = self.model_loader.load_all_models(
 
118
  progress_callback=progress_callback,
119
  cancel_event=self.cancel_event
120
  )
 
121
  except IndexError as e:
122
  import traceback
 
 
 
 
123
  tb = traceback.extract_tb(e.__traceback__)
124
+ where = f"{tb[-1].filename}:{tb[-1].lineno}" if tb else "unknown"
125
+ logger.error(f"IndexError in load_all_models at {where}: {e}")
126
+ raise ModelLoadingError(f"Model loading failed (IndexError @ {where}): {e}")
 
 
 
 
127
  except Exception as e:
128
  import traceback
129
+ logger.error(f"Unexpected error in load_all_models: {e}\n{traceback.format_exc()}")
 
 
130
  raise
131
 
132
  if self.cancel_event.is_set():
133
  return "Model loading cancelled"
134
 
135
+ # Unwrap actual model refs for two-stage
136
+ sam2_predictor = sam2_loaded.model if sam2_loaded else None
137
+ mat_model = mat_loaded.model if mat_loaded else None
138
+ if (sam2_predictor is None) and (mat_model is None):
139
+ return "Model loading failed - see logs"
 
 
 
 
140
 
141
+ # Core processor expects a "models" provider (we pass the loader itself)
142
  self.core_processor = CoreVideoProcessor(
143
  config=self.config,
144
+ models=self.model_loader
145
  )
146
 
147
+ # Optional 2-stage
148
+ if TWO_STAGE_AVAILABLE and (sam2_predictor or mat_model):
149
+ try:
150
+ self.two_stage_processor = TwoStageProcessor(
151
+ sam2_predictor=sam2_predictor,
152
+ matanyone_model=mat_model
153
+ )
154
+ logger.info("Two-stage processor initialized")
155
+ except Exception as e:
156
+ logger.warning(f"Two-stage init failed: {e}")
157
+ self.two_stage_processor = None
 
 
 
 
 
 
 
 
158
 
159
  self.models_loaded = True
160
+ msg = self.model_loader.get_load_summary()
161
+ if self.two_stage_processor:
162
+ msg += "\n✅ Two-stage processor ready"
 
 
163
  else:
164
+ msg += "\n⚠️ Two-stage processor not available"
165
+ logger.info(msg)
166
+ return msg
 
167
 
168
+ except (AttributeError, ModelLoadingError) as e:
 
 
 
 
 
169
  self.models_loaded = False
170
+ err = f"Model loading failed: {e}"
171
+ logger.error(err)
172
+ return err
173
  except Exception as e:
174
  self.models_loaded = False
175
+ err = f"Unexpected error during model loading: {e}"
176
+ logger.error(err)
177
+ return err
178
 
179
+ # ---------- Process ----------
180
  def process_video(
181
  self,
182
  video_path: str,
 
186
  use_two_stage: bool = False,
187
  chroma_preset: str = "standard",
188
  preview_mask: bool = False,
189
+ preview_greenscreen: bool = False,
190
  ) -> Tuple[Optional[str], str]:
 
191
 
192
  if not self.models_loaded or not self.core_processor:
193
+ return None, "Models not loaded. Please click “Load Models” first."
194
 
195
  if self.cancel_event.is_set():
196
  return None, "Processing cancelled"
197
 
198
+ self._init_progress(video_path, progress_callback)
 
199
 
200
+ ok, why = validate_video_file(video_path)
201
+ if not ok:
202
+ return None, f"Invalid video: {why}"
 
203
 
204
  try:
 
205
  if use_two_stage:
206
  if not TWO_STAGE_AVAILABLE:
207
+ return None, "Two-stage processing not available on this build"
208
+ if not self.two_stage_processor:
209
+ return None, "Two-stage processor not initialized (models not ready?)"
210
+ return self._process_two_stage(video_path, background_choice, custom_background_path, progress_callback, chroma_preset)
 
 
 
 
 
 
211
  else:
212
+ return self._process_single_stage(video_path, background_choice, custom_background_path, progress_callback, preview_mask, preview_greenscreen)
 
 
 
 
213
 
214
  except VideoProcessingError as e:
215
+ logger.error(f"Processing failed: {e}")
216
+ return None, f"Processing failed: {e}"
217
  except Exception as e:
218
+ logger.error(f"Unexpected processing error: {e}")
219
+ return None, f"Unexpected error: {e}"
220
 
221
  def _process_single_stage(
222
  self,
 
225
  custom_background_path: Optional[str],
226
  progress_callback: Optional[Callable],
227
  preview_mask: bool,
228
+ preview_greenscreen: bool,
229
  ) -> Tuple[Optional[str], str]:
 
230
 
 
231
  import time
232
+ ts = int(time.time())
233
+ out_dir = Path(self.config.output_dir) / "single_stage"
234
+ out_dir.mkdir(parents=True, exist_ok=True)
235
+ out_path = str(out_dir / f"processed_{ts}.mp4")
236
 
 
237
  result = self.core_processor.process_video(
238
  input_path=video_path,
239
+ output_path=out_path,
240
+ bg_config={"background_choice": background_choice, "custom_path": custom_background_path}
241
  )
 
242
  if not result:
243
  return None, "Video processing failed"
244
 
 
245
  if not (preview_mask or preview_greenscreen):
246
+ final_path = self.audio_processor.add_audio_to_video(original_video=video_path, processed_video=out_path)
 
 
 
247
  else:
248
+ final_path = out_path
249
 
250
+ msg = (
251
+ "Processing completed.\n"
252
+ f"Frames: {result.get('frames', 'unknown')}\n"
253
  f"Background: {background_choice}\n"
254
  f"Mode: Single-stage\n"
255
  f"Device: {self.device_manager.get_optimal_device()}"
256
  )
257
+ return final_path, msg
 
258
 
259
  def _process_two_stage(
260
  self,
 
262
  background_choice: str,
263
  custom_background_path: Optional[str],
264
  progress_callback: Optional[Callable],
265
+ chroma_preset: str,
266
  ) -> Tuple[Optional[str], str]:
 
 
267
  if self.two_stage_processor is None:
268
  return None, "Two-stage processor not available"
269
 
270
+ import cv2, time
 
271
  cap = cv2.VideoCapture(video_path)
272
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
273
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
274
  cap.release()
275
 
276
+ background = self.core_processor.prepare_background(background_choice, custom_background_path, w, h)
 
 
 
277
  if background is None:
278
  return None, "Failed to prepare background"
279
 
280
+ ts = int(time.time())
281
+ out_dir = Path(self.config.output_dir) / "two_stage"
282
+ out_dir.mkdir(parents=True, exist_ok=True)
283
+ final_out = str(out_dir / f"final_{ts}.mp4")
 
 
284
 
285
+ chroma = CHROMA_PRESETS.get(chroma_preset, CHROMA_PRESETS["standard"])
286
+ logger.info(f"Two-stage with preset: {chroma_preset}")
287
 
 
288
  result, message = self.two_stage_processor.process_full_pipeline(
289
+ video_path, background, final_out, chroma_settings=chroma, progress_callback=progress_callback
 
 
 
 
290
  )
 
291
  if result is None:
292
  return None, message
293
 
294
+ msg = (
295
+ "Two-stage processing completed.\n"
296
  f"Background: {background_choice}\n"
297
  f"Chroma Preset: {chroma_preset}\n"
 
298
  f"Device: {self.device_manager.get_optimal_device()}"
299
  )
300
+ return result, msg
301
 
302
+ # ---------- Status / Control ----------
 
303
  def get_status(self) -> Dict[str, Any]:
304
+ status = {
305
+ "models_loaded": self.models_loaded,
306
+ "two_stage_available": TWO_STAGE_AVAILABLE and (self.two_stage_processor is not None),
307
+ "device": str(self.device_manager.get_optimal_device()),
308
+ "memory_usage": self.memory_manager.get_memory_usage(),
309
+ "config": self.config.to_dict(),
310
+ "core_processor_loaded": self.core_processor is not None,
311
  }
312
+ try:
313
+ status["sam2_loaded"] = self.model_loader.get_sam2() is not None
314
+ status["matanyone_loaded"] = self.model_loader.get_matanyone() is not None
315
+ except Exception:
316
+ status["sam2_loaded"] = False
317
+ status["matanyone_loaded"] = False
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  if self.progress_tracker:
320
+ status["progress"] = self.progress_tracker.get_all_progress()
321
+ return status
 
322
 
323
  def cancel_processing(self):
 
324
  self.cancel_event.set()
325
+ logger.info("Cancellation requested")
326
 
327
  def cleanup_resources(self):
 
328
  self.memory_manager.cleanup_aggressive()
329
  if self.model_loader:
330
  self.model_loader.cleanup()
331
  logger.info("Resources cleaned up")
332
 
333
 
334
+ # Singleton for UI callbacks
335
  processor = VideoProcessor()
336
 
337
+ # Back-compat wrappers used by ui/callbacks.py
 
338
  def load_models_with_validation(progress_callback: Optional[Callable] = None) -> str:
 
339
  return processor.load_models(progress_callback)
340
 
 
341
  def process_video_fixed(
342
  video_path: str,
343
  background_choice: str,
 
346
  use_two_stage: bool = False,
347
  chroma_preset: str = "standard",
348
  preview_mask: bool = False,
349
+ preview_greenscreen: bool = False,
350
  ) -> Tuple[Optional[str], str]:
 
351
  return processor.process_video(
352
  video_path, background_choice, custom_background_path,
353
+ progress_callback, use_two_stage, chroma_preset, preview_mask, preview_greenscreen
 
354
  )
355
 
 
356
  def get_model_status() -> Dict[str, Any]:
 
357
  return processor.get_status()
358
 
 
359
  def get_cache_status() -> Dict[str, Any]:
360
+ # For now same as status; can add cache metrics later
361
  return processor.get_status()
362
 
 
 
363
  PROCESS_CANCELLED = processor.cancel_event
364
 
365
 
366
  def main():
 
367
  try:
368
+ logger.info("Starting BackgroundFX Pro")
369
  logger.info(f"Device: {processor.device_manager.get_optimal_device()}")
370
+ logger.info(f"Two-stage available: {TWO_STAGE_AVAILABLE}")
 
371
 
372
+ # NOTE: UI was split into ui/components.py
373
+ from ui.components import create_interface
374
  demo = create_interface()
375
 
 
376
  demo.queue().launch(
377
  server_name="0.0.0.0",
378
  server_port=7860,
379
  show_error=True,
380
  debug=False
381
  )
 
382
  except Exception as e:
383
+ logger.error(f"Startup failed: {e}")
384
  raise
385
  finally:
 
386
  processor.cleanup_resources()
387
 
388