MogensR commited on
Commit
d034be2
·
1 Parent(s): b8aa279

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -440
app.py CHANGED
@@ -1,20 +1,25 @@
1
  #!/usr/bin/env python3
2
  """
3
- Video Background Replacement - Main Application
4
- Refactored version with improved error handling, memory management, and configuration
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  import os
8
- import cv2
9
- import numpy as np
10
- import torch
11
- import time
12
  import logging
13
  import threading
14
- import subprocess
15
  from pathlib import Path
16
  from typing import Optional, Tuple, Dict, Any, Callable
17
- from dataclasses import dataclass
18
 
19
  # Configure logging
20
  logging.basicConfig(
@@ -23,7 +28,7 @@
23
  )
24
  logger = logging.getLogger(__name__)
25
 
26
- # Apply Gradio schema patch early
27
  try:
28
  import gradio_client.utils as gc_utils
29
  original_get_type = gc_utils.get_type
@@ -44,7 +49,17 @@ def patched_get_type(schema):
44
  except Exception as e:
45
  logger.error(f"Gradio patch failed: {e}")
46
 
47
- # Import core modules
 
 
 
 
 
 
 
 
 
 
48
  from utilities import (
49
  segment_person_hq,
50
  refine_mask_hq,
@@ -54,6 +69,7 @@ def patched_get_type(schema):
54
  validate_video_file
55
  )
56
 
 
57
  try:
58
  from two_stage_processor import TwoStageProcessor, CHROMA_PRESETS
59
  TWO_STAGE_AVAILABLE = True
@@ -61,273 +77,82 @@ def patched_get_type(schema):
61
  TWO_STAGE_AVAILABLE = False
62
  CHROMA_PRESETS = {'standard': {}}
63
 
64
- # Configuration
65
- @dataclass
66
- class ProcessingConfig:
67
- keyframe_interval: int = int(os.getenv('KEYFRAME_INTERVAL', '5'))
68
- frame_skip: int = int(os.getenv('FRAME_SKIP', '1'))
69
- memory_cleanup_interval: int = int(os.getenv('MEMORY_CLEANUP_INTERVAL', '30'))
70
- max_video_length: int = int(os.getenv('MAX_VIDEO_LENGTH', '300')) # seconds
71
- quality_preset: str = os.getenv('QUALITY_PRESET', 'balanced')
72
-
73
- class DeviceManager:
74
- """Manage device detection and switching"""
75
-
76
- @staticmethod
77
- def get_optimal_device():
78
- if torch.cuda.is_available():
79
- try:
80
- # Test CUDA functionality
81
- test_tensor = torch.tensor([1.0], device='cuda')
82
- del test_tensor
83
- torch.cuda.empty_cache()
84
- device = torch.device("cuda")
85
- logger.info(f"Using GPU: {torch.cuda.get_device_name(0)}")
86
- return device
87
- except Exception as e:
88
- logger.warning(f"CUDA test failed: {e}, falling back to CPU")
89
-
90
- logger.info("Using CPU device")
91
- return torch.device("cpu")
92
-
93
- class MemoryManager:
94
- """Enhanced memory management"""
95
-
96
- def __init__(self, device):
97
- self.device = device
98
- self.gpu_available = device.type == 'cuda'
99
-
100
- def cleanup_aggressive(self):
101
- import gc
102
- gc.collect()
103
- if self.gpu_available:
104
- torch.cuda.empty_cache()
105
- torch.cuda.synchronize()
106
-
107
- def get_memory_usage(self):
108
- usage = {}
109
- if self.gpu_available:
110
- gpu_memory = torch.cuda.get_device_properties(0).total_memory
111
- gpu_allocated = torch.cuda.memory_allocated(0)
112
- usage['gpu_percent'] = (gpu_allocated / gpu_memory) * 100
113
- usage['gpu_allocated_gb'] = gpu_allocated / (1024**3)
114
- return usage
115
-
116
- class ProgressTracker:
117
- """Enhanced progress tracking with detailed statistics"""
118
-
119
- def __init__(self, total_frames: int, callback: Optional[Callable] = None):
120
- self.total_frames = total_frames
121
- self.callback = callback
122
- self.start_time = time.time()
123
- self.processed_frames = 0
124
- self.frame_times = []
125
-
126
- def update(self, frame_number: int, stage: str = ""):
127
- current_time = time.time()
128
- self.processed_frames = frame_number
129
-
130
- elapsed_time = current_time - self.start_time
131
- current_fps = self.processed_frames / elapsed_time if elapsed_time > 0 else 0
132
-
133
- remaining_frames = self.total_frames - self.processed_frames
134
- eta_seconds = remaining_frames / current_fps if current_fps > 0 else 0
135
-
136
- progress_pct = self.processed_frames / self.total_frames if self.total_frames > 0 else 0
137
-
138
- message = (
139
- f"Frame {self.processed_frames}/{self.total_frames} | "
140
- f"Elapsed: {self._format_time(elapsed_time)} | "
141
- f"Speed: {current_fps:.1f} fps | "
142
- f"ETA: {self._format_time(eta_seconds)}"
143
- )
144
-
145
- if stage:
146
- message = f"{stage} | {message}"
147
-
148
- if self.callback:
149
- try:
150
- self.callback(progress_pct, message)
151
- except Exception as e:
152
- logger.warning(f"Progress callback failed: {e}")
153
-
154
- def _format_time(self, seconds: float) -> str:
155
- if seconds < 60:
156
- return f"{int(seconds)}s"
157
- elif seconds < 3600:
158
- return f"{int(seconds//60)}m {int(seconds%60)}s"
159
- else:
160
- hours = int(seconds // 3600)
161
- minutes = int((seconds % 3600) // 60)
162
- return f"{hours}h {minutes}m"
163
-
164
  class VideoProcessor:
165
- """Main video processing class with error recovery"""
 
 
166
 
167
  def __init__(self):
168
- self.device = DeviceManager.get_optimal_device()
169
- self.memory_manager = MemoryManager(self.device)
170
  self.config = ProcessingConfig()
171
- self.sam2_predictor = None
172
- self.matanyone_model = None
 
 
 
 
 
173
  self.two_stage_processor = None
 
 
174
  self.models_loaded = False
175
  self.loading_lock = threading.Lock()
176
  self.cancel_event = threading.Event()
177
 
 
 
178
  def load_models(self, progress_callback: Optional[Callable] = None) -> str:
179
- """Load AI models with comprehensive validation"""
180
  with self.loading_lock:
181
  if self.models_loaded:
182
  return "Models already loaded and validated"
183
 
184
  try:
185
  self.cancel_event.clear()
186
- start_time = time.time()
187
 
188
  if progress_callback:
189
- progress_callback(0.0, f"Starting model loading on {self.device}")
190
 
191
- # Load SAM2
192
- self.sam2_predictor = self._load_sam2(progress_callback)
193
- if self.cancel_event.is_set():
194
- return "Model loading cancelled"
 
195
 
196
- # Load MatAnyone
197
- self.matanyone_model = self._load_matanyone(progress_callback)
198
  if self.cancel_event.is_set():
199
  return "Model loading cancelled"
200
 
 
 
 
 
 
 
 
 
201
  # Initialize two-stage processor if available
202
- if TWO_STAGE_AVAILABLE:
203
  try:
204
- self.two_stage_processor = TwoStageProcessor(
205
- self.sam2_predictor, self.matanyone_model
206
- )
207
  logger.info("Two-stage processor initialized")
208
  except Exception as e:
209
  logger.warning(f"Two-stage processor init failed: {e}")
210
 
211
  self.models_loaded = True
212
- load_time = time.time() - start_time
213
-
214
- message = f"Models loaded successfully in {load_time:.1f}s on {self.device}"
215
- if TWO_STAGE_AVAILABLE:
216
- message += " (Two-stage mode available)"
217
-
218
  logger.info(message)
219
  return message
220
 
221
- except Exception as e:
222
  self.models_loaded = False
223
  error_msg = f"Model loading failed: {str(e)}"
224
  logger.error(error_msg)
225
  return error_msg
226
-
227
- def _load_sam2(self, progress_callback: Optional[Callable]) -> Any:
228
- """Load SAM2 predictor with validation"""
229
- if progress_callback:
230
- progress_callback(0.1, "Loading SAM2...")
231
-
232
- try:
233
- from huggingface_hub import hf_hub_download
234
- from sam2.build_sam import build_sam2
235
- from sam2.sam2_image_predictor import SAM2ImagePredictor
236
-
237
- # Download checkpoint
238
- checkpoint_path = hf_hub_download(
239
- repo_id="facebook/sam2-hiera-large",
240
- filename="sam2_hiera_large.pt",
241
- cache_dir=str(Path("/tmp/model_cache/sam2_checkpoint")),
242
- force_download=False
243
- )
244
-
245
- # Build model
246
- sam2_model = build_sam2("sam2_hiera_l.yaml", checkpoint_path)
247
- sam2_model.to(self.device)
248
- sam2_model.eval()
249
- predictor = SAM2ImagePredictor(sam2_model)
250
-
251
- # Validate with test
252
- test_image = np.zeros((256, 256, 3), dtype=np.uint8)
253
- predictor.set_image(test_image)
254
- test_points = np.array([[128.0, 128.0]], dtype=np.float32)
255
- test_labels = np.array([1], dtype=np.int32)
256
-
257
- with torch.no_grad():
258
- masks, scores, _ = predictor.predict(
259
- point_coords=test_points,
260
- point_labels=test_labels,
261
- multimask_output=False
262
- )
263
-
264
- if masks is None or len(masks) == 0:
265
- raise Exception("SAM2 validation failed")
266
-
267
- if progress_callback:
268
- progress_callback(0.5, "SAM2 loaded and validated")
269
-
270
- return predictor
271
-
272
- except Exception as e:
273
- logger.error(f"SAM2 loading failed: {e}")
274
- raise
275
-
276
- def _load_matanyone(self, progress_callback: Optional[Callable]) -> Any:
277
- """Load MatAnyone processor for Python 3.10"""
278
- if progress_callback:
279
- progress_callback(0.6, "Loading MatAnyone...")
280
-
281
- try:
282
- # Import MatAnyone - Python 3.10 compatible
283
- try:
284
- from matanyone import InferenceCore
285
- processor = InferenceCore("PeiqingYang/MatAnyone")
286
- logger.info("MatAnyone loaded via InferenceCore")
287
- except ImportError:
288
- try:
289
- # Alternative import path
290
- import matanyone
291
- processor = matanyone.load_model("PeiqingYang/MatAnyone")
292
- logger.info("MatAnyone loaded via direct import")
293
- except ImportError as e:
294
- logger.error(f"MatAnyone import failed: {e}")
295
- logger.error("Ensure all dependencies are installed: timm>=0.9.16, einops==0.8.0")
296
- return None
297
-
298
- # Test MatAnyone functionality
299
- test_image = np.zeros((256, 256, 3), dtype=np.uint8)
300
- test_mask = np.zeros((256, 256), dtype=np.uint8)
301
- test_mask[64:192, 64:192] = 255
302
-
303
- try:
304
- if hasattr(processor, 'infer'):
305
- test_result = processor.infer(test_image, test_mask)
306
- elif hasattr(processor, 'process'):
307
- test_result = processor.process(test_image, test_mask)
308
- elif callable(processor):
309
- test_result = processor(test_image, test_mask)
310
- else:
311
- logger.warning("MatAnyone processor has unknown interface")
312
- return processor # Return anyway, utilities will handle
313
-
314
- if test_result is not None:
315
- logger.info("MatAnyone test successful")
316
- else:
317
- logger.warning("MatAnyone test returned None")
318
-
319
- except Exception as test_error:
320
- logger.warning(f"MatAnyone test failed: {test_error}")
321
- # Still return processor - might work in actual use
322
-
323
- if progress_callback:
324
- progress_callback(0.9, "MatAnyone loaded successfully")
325
-
326
- return processor
327
-
328
- except Exception as e:
329
- logger.error(f"MatAnyone loading failed: {e}")
330
- return None
331
 
332
  def process_video(
333
  self,
@@ -340,20 +165,21 @@ def process_video(
340
  preview_mask: bool = False,
341
  preview_greenscreen: bool = False
342
  ) -> Tuple[Optional[str], str]:
343
- """Process video with comprehensive error handling"""
344
 
345
- if not self.models_loaded:
346
  return None, "Models not loaded. Please load models first."
347
 
348
  if self.cancel_event.is_set():
349
  return None, "Processing cancelled"
350
 
351
- # Validate input
352
  is_valid, validation_msg = validate_video_file(video_path)
353
  if not is_valid:
354
  return None, f"Invalid video: {validation_msg}"
355
 
356
  try:
 
357
  if use_two_stage and TWO_STAGE_AVAILABLE and self.two_stage_processor:
358
  return self._process_two_stage(
359
  video_path, background_choice, custom_background_path,
@@ -365,9 +191,12 @@ def process_video(
365
  progress_callback, preview_mask, preview_greenscreen
366
  )
367
 
368
- except Exception as e:
369
  logger.error(f"Video processing failed: {e}")
370
  return None, f"Processing failed: {str(e)}"
 
 
 
371
 
372
  def _process_single_stage(
373
  self,
@@ -378,115 +207,39 @@ def _process_single_stage(
378
  preview_mask: bool,
379
  preview_greenscreen: bool
380
  ) -> Tuple[Optional[str], str]:
381
- """Single-stage video processing"""
382
-
383
- cap = cv2.VideoCapture(video_path)
384
- if not cap.isOpened():
385
- return None, "Could not open video file"
386
-
387
- fps = cap.get(cv2.CAP_PROP_FPS)
388
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
389
- frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
390
- frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
391
-
392
- # Prepare background
393
- background = self._prepare_background(
394
- background_choice, custom_background_path, frame_width, frame_height
395
  )
396
- if background is None:
397
- cap.release()
398
- return None, "Failed to prepare background"
399
 
400
- # Setup output
401
- timestamp = int(time.time())
402
- output_path = f"/tmp/output_{timestamp}.mp4"
403
-
404
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
405
- out = cv2.VideoWriter(output_path, fourcc, fps, (frame_width, frame_height))
406
-
407
- if not out.isOpened():
408
- cap.release()
409
- return None, "Could not create output video"
410
-
411
- # Process frames
412
- progress_tracker = ProgressTracker(total_frames, progress_callback)
413
- frame_count = 0
414
- successful_frames = 0
415
- last_refined_mask = None
416
 
417
- try:
418
- while True:
419
- if self.cancel_event.is_set():
420
- break
421
-
422
- ret, frame = cap.read()
423
- if not ret:
424
- break
425
-
426
- try:
427
- progress_tracker.update(frame_count, "Processing")
428
-
429
- # Segmentation
430
- mask = segment_person_hq(frame, self.sam2_predictor)
431
-
432
- # Mask refinement (keyframe-based)
433
- if (frame_count % self.config.keyframe_interval == 0) or (last_refined_mask is None):
434
- refined_mask = refine_mask_hq(frame, mask, self.matanyone_model)
435
- last_refined_mask = refined_mask.copy()
436
- else:
437
- # Blend with previous refined mask for temporal consistency
438
- alpha = 0.7
439
- refined_mask = cv2.addWeighted(mask, alpha, last_refined_mask, 1-alpha, 0)
440
-
441
- # Generate output based on mode
442
- if preview_mask:
443
- result_frame = self._create_mask_preview(frame, refined_mask)
444
- elif preview_greenscreen:
445
- result_frame = self._create_greenscreen_preview(frame, refined_mask)
446
- else:
447
- result_frame = replace_background_hq(frame, refined_mask, background)
448
-
449
- out.write(result_frame)
450
- successful_frames += 1
451
-
452
- except Exception as frame_error:
453
- logger.warning(f"Frame {frame_count} processing failed: {frame_error}")
454
- out.write(frame) # Write original frame as fallback
455
-
456
- frame_count += 1
457
-
458
- # Memory cleanup
459
- if frame_count % self.config.memory_cleanup_interval == 0:
460
- self.memory_manager.cleanup_aggressive()
461
-
462
- finally:
463
- cap.release()
464
- out.release()
465
-
466
- if self.cancel_event.is_set():
467
- try:
468
- os.remove(output_path)
469
- except:
470
- pass
471
- return None, "Processing cancelled"
472
-
473
- if successful_frames == 0:
474
- return None, "No frames processed successfully"
475
-
476
- # Add audio if not preview mode
477
  if not (preview_mask or preview_greenscreen):
478
- final_output = self._add_audio(video_path, output_path)
 
 
 
479
  else:
480
- final_output = output_path
481
 
482
  success_msg = (
483
- f"Success! Processed {successful_frames}/{frame_count} frames\n"
484
  f"Background: {background_choice}\n"
485
  f"Mode: Single-stage\n"
486
- f"Device: {self.device}"
487
  )
488
 
489
- return final_output, success_msg
490
 
491
  def _process_two_stage(
492
  self,
@@ -496,21 +249,24 @@ def _process_two_stage(
496
  progress_callback: Optional[Callable],
497
  chroma_preset: str
498
  ) -> Tuple[Optional[str], str]:
499
- """Two-stage processing using green screen intermediate"""
500
 
 
 
501
  cap = cv2.VideoCapture(video_path)
502
  frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
503
  frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
504
  cap.release()
505
 
506
- # Prepare background
507
- background = self._prepare_background(
508
  background_choice, custom_background_path, frame_width, frame_height
509
  )
510
  if background is None:
511
  return None, "Failed to prepare background"
512
 
513
  # Process with two-stage pipeline
 
514
  timestamp = int(time.time())
515
  final_output = f"/tmp/twostage_final_{timestamp}.mp4"
516
 
@@ -532,117 +288,49 @@ def _process_two_stage(
532
  f"Background: {background_choice}\n"
533
  f"Preset: {chroma_preset}\n"
534
  f"Quality: Cinema-grade\n"
535
- f"Device: {self.device}"
536
  )
537
 
538
  return result, success_msg
539
 
540
- def _prepare_background(
541
- self,
542
- background_choice: str,
543
- custom_background_path: Optional[str],
544
- width: int,
545
- height: int
546
- ) -> Optional[np.ndarray]:
547
- """Prepare background image"""
548
-
549
- if background_choice == "custom" and custom_background_path:
550
- if not os.path.exists(custom_background_path):
551
- logger.error(f"Custom background not found: {custom_background_path}")
552
- return None
553
-
554
- background = cv2.imread(custom_background_path)
555
- if background is None:
556
- logger.error("Could not read custom background")
557
- return None
558
- else:
559
- if background_choice not in PROFESSIONAL_BACKGROUNDS:
560
- logger.error(f"Unknown background: {background_choice}")
561
- return None
562
-
563
- bg_config = PROFESSIONAL_BACKGROUNDS[background_choice]
564
- background = create_professional_background(bg_config, width, height)
565
-
566
- return cv2.resize(background, (width, height))
567
-
568
- def _create_mask_preview(self, frame: np.ndarray, mask: np.ndarray) -> np.ndarray:
569
- """Create mask preview visualization"""
570
- mask_vis = np.zeros_like(frame)
571
- mask_vis[..., 1] = mask # Green channel
572
- return mask_vis
573
-
574
- def _create_greenscreen_preview(self, frame: np.ndarray, mask: np.ndarray) -> np.ndarray:
575
- """Create green screen preview"""
576
- green_bg = np.zeros_like(frame)
577
- green_bg[:, :] = [0, 255, 0] # Pure green
578
-
579
- mask_3ch = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
580
- mask_norm = mask_3ch.astype(float) / 255
581
-
582
- return (frame * mask_norm + green_bg * (1 - mask_norm)).astype(np.uint8)
583
-
584
- def _add_audio(self, input_video: str, processed_video: str) -> str:
585
- """Add audio from original video to processed video"""
586
- timestamp = int(time.time())
587
- final_output = f"/tmp/final_with_audio_{timestamp}.mp4"
588
-
589
- try:
590
- # Check if input has audio
591
- result = subprocess.run([
592
- 'ffprobe', '-v', 'quiet', '-select_streams', 'a:0',
593
- '-show_entries', 'stream=codec_name', '-of', 'csv=p=0', input_video
594
- ], capture_output=True, text=True, timeout=30)
595
-
596
- if result.returncode != 0:
597
- logger.info("Input video has no audio")
598
- return processed_video
599
-
600
- # Add audio
601
- result = subprocess.run([
602
- 'ffmpeg', '-y', '-i', processed_video, '-i', input_video,
603
- '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k',
604
- '-map', '0:v:0', '-map', '1:a:0', '-shortest', final_output
605
- ], capture_output=True, text=True, timeout=300)
606
-
607
- if result.returncode == 0 and os.path.exists(final_output):
608
- try:
609
- os.remove(processed_video)
610
- except:
611
- pass
612
- return final_output
613
- else:
614
- logger.warning("Audio processing failed, using video without audio")
615
- return processed_video
616
-
617
- except Exception as e:
618
- logger.warning(f"Audio processing error: {e}")
619
- return processed_video
620
-
621
  def get_status(self) -> Dict[str, Any]:
622
- """Get current processor status"""
623
- return {
624
  'models_loaded': self.models_loaded,
625
- 'sam2_available': self.sam2_predictor is not None,
626
- 'matanyone_available': self.matanyone_model is not None,
627
  'two_stage_available': TWO_STAGE_AVAILABLE and self.two_stage_processor is not None,
628
- 'device': str(self.device),
629
  'memory_usage': self.memory_manager.get_memory_usage(),
630
- 'config': {
631
- 'keyframe_interval': self.config.keyframe_interval,
632
- 'quality_preset': self.config.quality_preset
633
- }
634
  }
 
 
 
 
 
 
 
 
 
 
635
 
636
  def cancel_processing(self):
637
- """Cancel current processing"""
638
  self.cancel_event.set()
639
  logger.info("Processing cancellation requested")
 
 
 
 
 
 
 
640
 
641
- # Global processor instance
642
  processor = VideoProcessor()
643
 
644
- # Compatibility functions for existing UI
645
  def load_models_with_validation(progress_callback: Optional[Callable] = None) -> str:
 
646
  return processor.load_models(progress_callback)
647
 
648
  def process_video_fixed(
@@ -655,6 +343,7 @@ def process_video_fixed(
655
  preview_mask: bool = False,
656
  preview_greenscreen: bool = False
657
  ) -> Tuple[Optional[str], str]:
 
658
  return processor.process_video(
659
  video_path, background_choice, custom_background_path,
660
  progress_callback, use_two_stage, chroma_preset,
@@ -662,9 +351,11 @@ def process_video_fixed(
662
  )
663
 
664
  def get_model_status() -> Dict[str, Any]:
 
665
  return processor.get_status()
666
 
667
  def get_cache_status() -> Dict[str, Any]:
 
668
  return processor.get_status()
669
 
670
  # For backward compatibility
@@ -674,8 +365,9 @@ def main():
674
  """Main application entry point"""
675
  try:
676
  logger.info("Starting Video Background Replacement application")
677
- logger.info(f"Device: {processor.device}")
678
  logger.info(f"Two-stage available: {TWO_STAGE_AVAILABLE}")
 
679
 
680
  # Import and create UI
681
  from ui_components import create_interface
@@ -693,6 +385,9 @@ def main():
693
  except Exception as e:
694
  logger.error(f"Application startup failed: {e}")
695
  raise
 
 
 
696
 
697
  if __name__ == "__main__":
698
  main()
 
1
  #!/usr/bin/env python3
2
  """
3
+ Video Background Replacement - Main Application Entry Point
4
+ Refactored modular architecture - orchestrates specialized components
5
+
6
+ This file has been refactored from a monolithic 600+ line structure into
7
+ a clean orchestration layer that coordinates specialized modules:
8
+ - config: Application configuration and environment variables
9
+ - device_manager: Hardware detection and optimization
10
+ - memory_manager: Memory and GPU resource management
11
+ - model_loader: AI model loading and validation
12
+ - video_processor: Core video processing pipeline
13
+ - audio_processor: Audio track handling and FFmpeg operations
14
+ - progress_tracker: Progress monitoring and ETA calculations
15
+ - exceptions: Custom exception classes for better error handling
16
  """
17
 
18
  import os
 
 
 
 
19
  import logging
20
  import threading
 
21
  from pathlib import Path
22
  from typing import Optional, Tuple, Dict, Any, Callable
 
23
 
24
  # Configure logging
25
  logging.basicConfig(
 
28
  )
29
  logger = logging.getLogger(__name__)
30
 
31
+ # Apply Gradio schema patch early (before other imports)
32
  try:
33
  import gradio_client.utils as gc_utils
34
  original_get_type = gc_utils.get_type
 
49
  except Exception as e:
50
  logger.error(f"Gradio patch failed: {e}")
51
 
52
+ # Import modular components
53
+ from config import ProcessingConfig
54
+ from device_manager import DeviceManager
55
+ from memory_manager import MemoryManager
56
+ from model_loader import ModelLoader
57
+ from video_processor import CoreVideoProcessor
58
+ from audio_processor import AudioProcessor
59
+ from progress_tracker import ProgressTracker
60
+ from exceptions import VideoProcessingError, ModelLoadingError, DeviceError
61
+
62
+ # Import utilities (existing)
63
  from utilities import (
64
  segment_person_hq,
65
  refine_mask_hq,
 
69
  validate_video_file
70
  )
71
 
72
+ # Import two-stage processor if available
73
  try:
74
  from two_stage_processor import TwoStageProcessor, CHROMA_PRESETS
75
  TWO_STAGE_AVAILABLE = True
 
77
  TWO_STAGE_AVAILABLE = False
78
  CHROMA_PRESETS = {'standard': {}}
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  class VideoProcessor:
81
+ """
82
+ Main video processing orchestrator - coordinates all specialized components
83
+ """
84
 
85
  def __init__(self):
86
+ """Initialize the video processor with all required components"""
 
87
  self.config = ProcessingConfig()
88
+ self.device_manager = DeviceManager()
89
+ self.memory_manager = MemoryManager(self.device_manager.get_optimal_device())
90
+ self.model_loader = ModelLoader(self.device_manager.get_optimal_device())
91
+ self.audio_processor = AudioProcessor()
92
+
93
+ # Initialize core processor (will be set up after models load)
94
+ self.core_processor = None
95
  self.two_stage_processor = None
96
+
97
+ # State management
98
  self.models_loaded = False
99
  self.loading_lock = threading.Lock()
100
  self.cancel_event = threading.Event()
101
 
102
+ logger.info(f"VideoProcessor initialized on device: {self.device_manager.get_optimal_device()}")
103
+
104
  def load_models(self, progress_callback: Optional[Callable] = None) -> str:
105
+ """Load and validate all AI models"""
106
  with self.loading_lock:
107
  if self.models_loaded:
108
  return "Models already loaded and validated"
109
 
110
  try:
111
  self.cancel_event.clear()
 
112
 
113
  if progress_callback:
114
+ progress_callback(0.0, f"Starting model loading on {self.device_manager.get_optimal_device()}")
115
 
116
+ # Load models using the specialized loader
117
+ sam2_predictor, matanyone_model = self.model_loader.load_all_models(
118
+ progress_callback=progress_callback,
119
+ cancel_event=self.cancel_event
120
+ )
121
 
 
 
122
  if self.cancel_event.is_set():
123
  return "Model loading cancelled"
124
 
125
+ # Initialize core processor with loaded models
126
+ self.core_processor = CoreVideoProcessor(
127
+ sam2_predictor=sam2_predictor,
128
+ matanyone_model=matanyone_model,
129
+ config=self.config,
130
+ memory_manager=self.memory_manager
131
+ )
132
+
133
  # Initialize two-stage processor if available
134
+ if TWO_STAGE_AVAILABLE and sam2_predictor and matanyone_model:
135
  try:
136
+ self.two_stage_processor = TwoStageProcessor(sam2_predictor, matanyone_model)
 
 
137
  logger.info("Two-stage processor initialized")
138
  except Exception as e:
139
  logger.warning(f"Two-stage processor init failed: {e}")
140
 
141
  self.models_loaded = True
142
+ message = self.model_loader.get_load_summary()
 
 
 
 
 
143
  logger.info(message)
144
  return message
145
 
146
+ except ModelLoadingError as e:
147
  self.models_loaded = False
148
  error_msg = f"Model loading failed: {str(e)}"
149
  logger.error(error_msg)
150
  return error_msg
151
+ except Exception as e:
152
+ self.models_loaded = False
153
+ error_msg = f"Unexpected error during model loading: {str(e)}"
154
+ logger.error(error_msg)
155
+ return error_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  def process_video(
158
  self,
 
165
  preview_mask: bool = False,
166
  preview_greenscreen: bool = False
167
  ) -> Tuple[Optional[str], str]:
168
+ """Process video with the specified parameters"""
169
 
170
+ if not self.models_loaded or not self.core_processor:
171
  return None, "Models not loaded. Please load models first."
172
 
173
  if self.cancel_event.is_set():
174
  return None, "Processing cancelled"
175
 
176
+ # Validate input file
177
  is_valid, validation_msg = validate_video_file(video_path)
178
  if not is_valid:
179
  return None, f"Invalid video: {validation_msg}"
180
 
181
  try:
182
+ # Route to appropriate processing method
183
  if use_two_stage and TWO_STAGE_AVAILABLE and self.two_stage_processor:
184
  return self._process_two_stage(
185
  video_path, background_choice, custom_background_path,
 
191
  progress_callback, preview_mask, preview_greenscreen
192
  )
193
 
194
+ except VideoProcessingError as e:
195
  logger.error(f"Video processing failed: {e}")
196
  return None, f"Processing failed: {str(e)}"
197
+ except Exception as e:
198
+ logger.error(f"Unexpected error during video processing: {e}")
199
+ return None, f"Unexpected error: {str(e)}"
200
 
201
  def _process_single_stage(
202
  self,
 
207
  preview_mask: bool,
208
  preview_greenscreen: bool
209
  ) -> Tuple[Optional[str], str]:
210
+ """Process video using single-stage pipeline"""
211
+
212
+ # Process video using core processor
213
+ processed_video_path, process_message = self.core_processor.process_video(
214
+ video_path=video_path,
215
+ background_choice=background_choice,
216
+ custom_background_path=custom_background_path,
217
+ progress_callback=progress_callback,
218
+ cancel_event=self.cancel_event,
219
+ preview_mask=preview_mask,
220
+ preview_greenscreen=preview_greenscreen
 
 
 
221
  )
 
 
 
222
 
223
+ if processed_video_path is None:
224
+ return None, process_message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ # Add audio if not in preview mode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  if not (preview_mask or preview_greenscreen):
228
+ final_video_path = self.audio_processor.add_audio_to_video(
229
+ original_video=video_path,
230
+ processed_video=processed_video_path
231
+ )
232
  else:
233
+ final_video_path = processed_video_path
234
 
235
  success_msg = (
236
+ f"{process_message}\n"
237
  f"Background: {background_choice}\n"
238
  f"Mode: Single-stage\n"
239
+ f"Device: {self.device_manager.get_optimal_device()}"
240
  )
241
 
242
+ return final_video_path, success_msg
243
 
244
  def _process_two_stage(
245
  self,
 
249
  progress_callback: Optional[Callable],
250
  chroma_preset: str
251
  ) -> Tuple[Optional[str], str]:
252
+ """Process video using two-stage pipeline"""
253
 
254
+ # Get video dimensions for background preparation
255
+ import cv2
256
  cap = cv2.VideoCapture(video_path)
257
  frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
258
  frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
259
  cap.release()
260
 
261
+ # Prepare background using core processor
262
+ background = self.core_processor.prepare_background(
263
  background_choice, custom_background_path, frame_width, frame_height
264
  )
265
  if background is None:
266
  return None, "Failed to prepare background"
267
 
268
  # Process with two-stage pipeline
269
+ import time
270
  timestamp = int(time.time())
271
  final_output = f"/tmp/twostage_final_{timestamp}.mp4"
272
 
 
288
  f"Background: {background_choice}\n"
289
  f"Preset: {chroma_preset}\n"
290
  f"Quality: Cinema-grade\n"
291
+ f"Device: {self.device_manager.get_optimal_device()}"
292
  )
293
 
294
  return result, success_msg
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  def get_status(self) -> Dict[str, Any]:
297
+ """Get comprehensive status of all components"""
298
+ base_status = {
299
  'models_loaded': self.models_loaded,
 
 
300
  'two_stage_available': TWO_STAGE_AVAILABLE and self.two_stage_processor is not None,
301
+ 'device': str(self.device_manager.get_optimal_device()),
302
  'memory_usage': self.memory_manager.get_memory_usage(),
303
+ 'config': self.config.to_dict()
 
 
 
304
  }
305
+
306
+ # Add model-specific status if available
307
+ if self.model_loader:
308
+ base_status.update(self.model_loader.get_status())
309
+
310
+ # Add processing status if available
311
+ if self.core_processor:
312
+ base_status.update(self.core_processor.get_status())
313
+
314
+ return base_status
315
 
316
  def cancel_processing(self):
317
+ """Cancel any ongoing processing"""
318
  self.cancel_event.set()
319
  logger.info("Processing cancellation requested")
320
+
321
+ def cleanup_resources(self):
322
+ """Clean up all resources"""
323
+ self.memory_manager.cleanup_aggressive()
324
+ if self.model_loader:
325
+ self.model_loader.cleanup()
326
+ logger.info("Resources cleaned up")
327
 
328
+ # Global processor instance for application
329
  processor = VideoProcessor()
330
 
331
+ # Backward compatibility functions for existing UI
332
  def load_models_with_validation(progress_callback: Optional[Callable] = None) -> str:
333
+ """Load models with validation - backward compatibility wrapper"""
334
  return processor.load_models(progress_callback)
335
 
336
  def process_video_fixed(
 
343
  preview_mask: bool = False,
344
  preview_greenscreen: bool = False
345
  ) -> Tuple[Optional[str], str]:
346
+ """Process video - backward compatibility wrapper"""
347
  return processor.process_video(
348
  video_path, background_choice, custom_background_path,
349
  progress_callback, use_two_stage, chroma_preset,
 
351
  )
352
 
353
  def get_model_status() -> Dict[str, Any]:
354
+ """Get model status - backward compatibility wrapper"""
355
  return processor.get_status()
356
 
357
  def get_cache_status() -> Dict[str, Any]:
358
+ """Get cache status - backward compatibility wrapper"""
359
  return processor.get_status()
360
 
361
  # For backward compatibility
 
365
  """Main application entry point"""
366
  try:
367
  logger.info("Starting Video Background Replacement application")
368
+ logger.info(f"Device: {processor.device_manager.get_optimal_device()}")
369
  logger.info(f"Two-stage available: {TWO_STAGE_AVAILABLE}")
370
+ logger.info("Modular architecture loaded successfully")
371
 
372
  # Import and create UI
373
  from ui_components import create_interface
 
385
  except Exception as e:
386
  logger.error(f"Application startup failed: {e}")
387
  raise
388
+ finally:
389
+ # Cleanup on exit
390
+ processor.cleanup_resources()
391
 
392
  if __name__ == "__main__":
393
  main()