MogensR commited on
Commit
69bef1e
·
1 Parent(s): 53a282c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +779 -149
app.py CHANGED
@@ -1,25 +1,36 @@
1
  #!/usr/bin/env python3
2
  """
3
- SAFE IMPORT VERSION - Handle missing dependencies gracefully
 
 
 
 
 
4
  """
5
- import sys
 
 
 
6
  import traceback
 
 
 
 
 
 
 
 
7
  import os
8
 
9
- print("=== APP STARTUP DEBUG ===")
10
- print(f"Python path: {sys.path}")
11
- print(f"Current directory: {os.getcwd()}")
12
- print("Files in current directory:")
13
- for f in os.listdir('.'):
14
- print(f" {f}")
15
-
16
- # CRITICAL: Apply Gradio patch first
17
- print("Applying Gradio schema patch...")
18
  try:
19
  import gradio_client.utils as gc_utils
20
  original_get_type = gc_utils.get_type
21
 
22
  def patched_get_type(schema):
 
23
  if not isinstance(schema, dict):
24
  if isinstance(schema, bool):
25
  return "boolean"
@@ -27,166 +38,785 @@ def patched_get_type(schema):
27
  return "string"
28
  if isinstance(schema, (int, float)):
29
  return "number"
30
- return "string"
 
 
31
  return original_get_type(schema)
32
 
33
  gc_utils.get_type = patched_get_type
34
- print("✅ Gradio schema patch applied successfully!")
35
 
36
- except Exception as e:
37
- print(f"❌ Gradio patch failed: {e}")
38
- traceback.print_exc()
39
 
40
- # Safe imports with error handling
41
- print("Testing basic imports...")
42
- try:
43
- import cv2
44
- import numpy as np
45
- import torch
46
- import gradio as gr
47
- from pathlib import Path
48
- print(f"✅ Basic imports successful - Gradio {gr.__version__}, PyTorch {torch.__version__}")
49
- except Exception as e:
50
- print(f"❌ Basic imports failed: {e}")
51
- traceback.print_exc()
52
- sys.exit(1)
53
-
54
- # Test utilities import
55
- print("Testing utilities import...")
56
- try:
57
- from utilities import (
58
- segment_person_hq,
59
- refine_mask_hq,
60
- enhance_mask_opencv,
61
- replace_background_hq,
62
- create_professional_background,
63
- PROFESSIONAL_BACKGROUNDS,
64
- validate_video_file
65
- )
66
- print("✅ Utilities imported successfully")
67
- UTILITIES_AVAILABLE = True
68
- except Exception as e:
69
- print(f"❌ Utilities import failed: {e}")
70
- traceback.print_exc()
71
- UTILITIES_AVAILABLE = False
72
-
73
- # Test two-stage processor
74
- print("Testing two-stage processor import...")
75
  try:
76
  from two_stage_processor import TwoStageProcessor, CHROMA_PRESETS
77
  TWO_STAGE_AVAILABLE = True
78
- print("✅ Two-stage processor available")
79
- except ImportError as e:
80
- print(f"⚠️ Two-stage processor not available: {e}")
81
  TWO_STAGE_AVAILABLE = False
82
- CHROMA_PRESETS = {'standard': {}}
83
 
84
- # Test UI components
85
- print("Testing UI components import...")
86
- try:
87
- from ui_components import create_interface
88
- print("✅ UI components imported successfully")
89
- UI_AVAILABLE = True
90
- except Exception as e:
91
- print(f"❌ UI components import failed: {e}")
92
- traceback.print_exc()
93
- UI_AVAILABLE = False
94
-
95
- # Device detection
96
- print("Testing device detection...")
97
- try:
98
- if torch.cuda.is_available():
99
- device_name = torch.cuda.get_device_name(0)
100
- print(f"✅ GPU detected: {device_name}")
101
- DEVICE = torch.device("cuda")
102
- else:
103
- print("✅ Using CPU")
104
- DEVICE = torch.device("cpu")
105
- except Exception as e:
106
- print(f"❌ Device detection failed: {e}")
107
- DEVICE = torch.device("cpu")
108
-
109
- # Create minimal interface if utilities failed
110
- def create_minimal_interface():
111
- """Create a minimal diagnostic interface"""
112
- with gr.Blocks(title="BackgroundFX Pro - Diagnostic Mode") as demo:
113
- gr.Markdown("# 🔧 BackgroundFX Pro - Diagnostic Mode")
114
- gr.Markdown("This is a minimal interface to test basic functionality.")
115
-
116
- gr.Markdown("## System Status")
117
- status_info = f"""
118
- - **Gradio Version**: {gr.__version__}
119
- - **PyTorch Version**: {torch.__version__}
120
- - **Device**: {DEVICE}
121
- - **CUDA Available**: {torch.cuda.is_available()}
122
- - **Utilities Available**: {UTILITIES_AVAILABLE}
123
- - **UI Components Available**: {UI_AVAILABLE}
124
- - **Two-Stage Available**: {TWO_STAGE_AVAILABLE}
125
- """
126
- gr.Markdown(status_info)
127
-
128
- # Simple test function
129
- def test_echo(text):
130
- return f"Echo: {text} (Device: {DEVICE})"
131
-
132
- gr.Markdown("## Connectivity Test")
133
- with gr.Row():
134
- test_input = gr.Textbox(label="Test Input", placeholder="Type something...")
135
- test_output = gr.Textbox(label="Test Output")
136
-
137
- test_btn = gr.Button("Test Connection", variant="primary")
138
- test_btn.click(fn=test_echo, inputs=test_input, outputs=test_output)
139
-
140
- if not UTILITIES_AVAILABLE:
141
- gr.Markdown("⚠️ **Warning**: Core utilities not available. Video processing disabled.")
142
-
143
- if not UI_AVAILABLE:
144
- gr.Markdown("⚠️ **Warning**: Full UI components not available. Using minimal interface.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- # Main execution
149
- def main():
150
  try:
151
- print("=== LAUNCHING APPLICATION ===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- if UI_AVAILABLE and UTILITIES_AVAILABLE:
154
- print("Creating full interface...")
155
- demo = create_interface()
156
- else:
157
- print("Creating minimal diagnostic interface...")
158
- demo = create_minimal_interface()
159
 
160
- print("Launching Gradio interface...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  demo.queue().launch(
162
  server_name="0.0.0.0",
163
  server_port=7860,
164
- share=True,
165
  show_error=True,
166
  debug=True
167
  )
168
-
169
  except Exception as e:
170
- print(f"=== LAUNCH FAILED ===")
171
- print(f"Error: {e}")
172
- traceback.print_exc()
173
-
174
- # Try absolute minimal interface
175
- try:
176
- print("Attempting emergency minimal interface...")
177
- with gr.Blocks() as emergency_demo:
178
- gr.Markdown("# Emergency Mode")
179
- gr.Markdown("App failed to start normally. This is a minimal test interface.")
180
- gr.Textbox("Emergency mode active", label="Status")
181
-
182
- emergency_demo.launch(
183
- server_name="0.0.0.0",
184
- server_port=7860,
185
- share=True
186
- )
187
- except Exception as emergency_e:
188
- print(f"Emergency launch also failed: {emergency_e}")
189
- sys.exit(1)
190
 
191
  if __name__ == "__main__":
192
  main()
 
1
  #!/usr/bin/env python3
2
  """
3
+ Final Fixed Video Background Replacement
4
+ Uses proper functions from utilities.py to avoid transparency issues
5
+ NEW: Added automatic device detection for Hugging Face Spaces compatibility,
6
+ improved error handling, and better resource management
7
+ FIXED: All issues identified by Grok4 - robust error handling, variable scope, codec fallbacks
8
+ FIXED: Added SSR mode disable for Gradio compatibility
9
  """
10
+ import cv2
11
+ import numpy as np
12
+ from pathlib import Path
13
+ import torch
14
  import traceback
15
+ import time
16
+ import shutil
17
+ import gc
18
+ import threading
19
+ import subprocess
20
+ from typing import Optional, Tuple, Dict, Any
21
+ import logging
22
+ from huggingface_hub import hf_hub_download
23
  import os
24
 
25
+ # ============================================================================ #
26
+ # CRITICAL: GRADIO SCHEMA VALIDATION FIX - MUST BE FIRST
27
+ # ============================================================================ #
 
 
 
 
 
 
28
  try:
29
  import gradio_client.utils as gc_utils
30
  original_get_type = gc_utils.get_type
31
 
32
  def patched_get_type(schema):
33
+ """Fixed get_type function that handles boolean schemas properly"""
34
  if not isinstance(schema, dict):
35
  if isinstance(schema, bool):
36
  return "boolean"
 
38
  return "string"
39
  if isinstance(schema, (int, float)):
40
  return "number"
41
+ return "string" # fallback
42
+
43
+ # If it's a dict, use original function
44
  return original_get_type(schema)
45
 
46
  gc_utils.get_type = patched_get_type
47
+ print("✅ CRITICAL: Gradio schema patch applied successfully!")
48
 
49
+ except (ImportError, AttributeError) as e:
50
+ print(f"❌ CRITICAL: Gradio patch failed: {e}")
 
51
 
52
+ # Import utilities - CRITICAL: Use these functions, don't duplicate!
53
+ from utilities import (
54
+ segment_person_hq,
55
+ refine_mask_hq,
56
+ enhance_mask_opencv,
57
+ replace_background_hq,
58
+ create_professional_background,
59
+ PROFESSIONAL_BACKGROUNDS,
60
+ validate_video_file
61
+ )
62
+
63
+ # Import two-stage processor if available
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  try:
65
  from two_stage_processor import TwoStageProcessor, CHROMA_PRESETS
66
  TWO_STAGE_AVAILABLE = True
67
+ except ImportError:
 
 
68
  TWO_STAGE_AVAILABLE = False
 
69
 
70
+ logging.basicConfig(level=logging.INFO)
71
+ logger = logging.getLogger(__name__)
72
+
73
+ # ============================================================================ #
74
+ # OPTIMIZATION SETTINGS
75
+ # ============================================================================ #
76
+ KEYFRAME_INTERVAL = 5 # Process MatAnyone every 5th frame
77
+ FRAME_SKIP = 1 # Process every frame (set to 2 for every other frame)
78
+ MEMORY_CLEANUP_INTERVAL = 30 # Clean memory every 30 frames
79
+
80
+ # ============================================================================ #
81
+ # MODEL CACHING SYSTEM
82
+ # ============================================================================ #
83
+ CACHE_DIR = Path("/tmp/model_cache")
84
+ CACHE_DIR.mkdir(exist_ok=True, parents=True)
85
+
86
+ # ============================================================================ #
87
+ # GLOBAL MODEL STATE
88
+ # ============================================================================ #
89
+ sam2_predictor = None
90
+ matanyone_model = None
91
+ models_loaded = False
92
+ loading_lock = threading.Lock()
93
+ two_stage_processor = None
94
+ PROCESS_CANCELLED = threading.Event()
95
+
96
+ # ============================================================================ #
97
+ # DEVICE DETECTION FOR HUGGING FACE SPACES - ROBUST
98
+ # ============================================================================ #
99
+ def get_device():
100
+ """Automatically detect the best available device (CPU or GPU) with robust error handling"""
101
+ try:
102
+ if torch.cuda.is_available():
103
+ # Try to get device name safely
104
+ try:
105
+ device_name = torch.cuda.get_device_name(0)
106
+ logger.info(f"Using GPU: {device_name}")
107
+ except Exception as e:
108
+ logger.warning(f"Could not get GPU name: {e}, but CUDA is available")
109
+ device_name = "CUDA GPU"
110
+
111
+ # Test CUDA functionality
112
+ try:
113
+ test_tensor = torch.tensor([1.0], device='cuda')
114
+ del test_tensor
115
+ torch.cuda.empty_cache()
116
+ return torch.device("cuda")
117
+ except Exception as e:
118
+ logger.error(f"CUDA test failed: {e}, falling back to CPU")
119
+ return torch.device("cpu")
120
+ else:
121
+ logger.info("Using CPU (no GPU available)")
122
+ return torch.device("cpu")
123
+ except Exception as e:
124
+ logger.error(f"Device detection failed: {e}, defaulting to CPU")
125
+ return torch.device("cpu")
126
+
127
+ # Set the device globally
128
+ DEVICE = get_device()
129
+
130
+ # ============================================================================ #
131
+ # ROBUST FFMPEG OPERATIONS
132
+ # ============================================================================ #
133
+ def run_ffmpeg_command(command_args: list, description: str = "FFmpeg operation") -> bool:
134
+ """Run ffmpeg command with proper error handling"""
135
+ try:
136
+ logger.info(f"Running {description}: {' '.join(command_args)}")
137
+ result = subprocess.run(
138
+ command_args,
139
+ check=True,
140
+ capture_output=True,
141
+ text=True,
142
+ timeout=300 # 5 minute timeout
143
+ )
144
+ logger.info(f"{description} completed successfully")
145
+ return True
146
+ except subprocess.CalledProcessError as e:
147
+ logger.error(f"{description} failed with exit code {e.returncode}")
148
+ logger.error(f"STDERR: {e.stderr}")
149
+ return False
150
+ except subprocess.TimeoutExpired:
151
+ logger.error(f"{description} timed out")
152
+ return False
153
+ except Exception as e:
154
+ logger.error(f"{description} failed: {e}")
155
+ return False
156
+
157
+ # ============================================================================ #
158
+ # ROBUST VIDEO WRITER WITH CODEC FALLBACK
159
+ # ============================================================================ #
160
+ def create_video_writer(output_path: str, fps: float, width: int, height: int) -> Tuple[Optional[cv2.VideoWriter], Optional[str]]:
161
+ """Create video writer with codec fallback"""
162
+ codecs_to_try = [
163
+ ('mp4v', '.mp4'), # Most compatible
164
+ ('avc1', '.mp4'), # H.264 if available
165
+ ('XVID', '.avi'), # Fallback
166
+ ]
167
 
168
+ for fourcc_str, ext in codecs_to_try:
169
+ try:
170
+ fourcc = cv2.VideoWriter_fourcc(*fourcc_str)
171
+ # Ensure output has correct extension
172
+ if not output_path.endswith(ext):
173
+ base = os.path.splitext(output_path)[0]
174
+ test_path = base + ext
175
+ else:
176
+ test_path = output_path
177
+
178
+ writer = cv2.VideoWriter(test_path, fourcc, fps, (width, height))
179
+ if writer.isOpened():
180
+ logger.info(f"Successfully created video writer with {fourcc_str} codec")
181
+ return writer, test_path
182
+ else:
183
+ writer.release()
184
+ except Exception as e:
185
+ logger.warning(f"Failed to create writer with {fourcc_str}: {e}")
186
+
187
+ logger.error("All video codecs failed")
188
+ return None, None
189
+
190
+ # ============================================================================ #
191
+ # SAM2 LOADER WITH VALIDATION - ROBUST
192
+ # ============================================================================ #
193
+ def load_sam2_predictor_fixed(device: torch.device = DEVICE, progress_callback: Optional[callable] = None) -> Any:
194
+ """Load SAM2 with proper error handling and validation"""
195
+ def _prog(pct: float, desc: str):
196
+ if progress_callback:
197
+ progress_callback(pct, desc)
198
+
199
+ # Format progress info for display in the UI
200
+ if "Frame" in desc and "|" in desc:
201
+ parts = desc.split("|")
202
+ frame_info = parts[0].strip() if len(parts) > 0 else ""
203
+ time_info = parts[1].strip() if len(parts) > 1 else ""
204
+ fps_info = parts[2].strip() if len(parts) > 2 else ""
205
+ eta_info = parts[3].strip() if len(parts) > 3 else ""
206
+ display_text = f"""📊 PROCESSING STATUS
207
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━
208
+ 🎬 {frame_info}
209
+ ⏱️ Elapsed: {time_info}
210
+ ⚡ Speed: {fps_info}
211
+ 🎯 {eta_info}
212
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━
213
+ 📈 Progress: {pct*100:.1f}%"""
214
+ try:
215
+ with open("/tmp/processing_info.txt", 'w') as f:
216
+ f.write(display_text)
217
+ except Exception as e:
218
+ logger.warning(f"Error writing processing info: {e}")
219
 
 
 
220
  try:
221
+ _prog(0.1, "Initializing SAM2...")
222
+
223
+ # Check HF token and environment
224
+ hf_token = os.getenv('HF_TOKEN')
225
+ if not hf_token:
226
+ logger.warning("No HF_TOKEN found, downloads may be rate limited")
227
+
228
+ # Download checkpoint with caching and robust error handling
229
+ try:
230
+ checkpoint_path = hf_hub_download(
231
+ repo_id="facebookresearch/sam2",
232
+ filename="sam2_hiera_large.pt",
233
+ cache_dir=str(CACHE_DIR / "sam2_checkpoint"),
234
+ force_download=False,
235
+ token=hf_token
236
+ )
237
+ except Exception as e:
238
+ logger.error(f"Failed to download SAM2 checkpoint: {e}")
239
+ raise Exception(f"SAM2 checkpoint download failed: {e}")
240
+
241
+ _prog(0.5, "SAM2 checkpoint downloaded, building model...")
242
+
243
+ # Import and build
244
+ try:
245
+ from sam2.build_sam import build_sam2
246
+ from sam2.sam2_image_predictor import SAM2ImagePredictor
247
+ except ImportError as e:
248
+ raise Exception(f"SAM2 import failed: {e}. Make sure SAM2 is properly installed.")
249
+
250
+ # Build model with explicit config
251
+ try:
252
+ sam2_model = build_sam2("sam2_hiera_l.yaml", checkpoint_path)
253
+ sam2_model.to(device)
254
+ sam2_model.eval() # Set to evaluation mode for inference
255
+ predictor = SAM2ImagePredictor(sam2_model)
256
+ except Exception as e:
257
+ raise Exception(f"SAM2 model creation failed: {e}")
258
+
259
+ # Test the predictor with dummy data - ROBUST TYPES
260
+ _prog(0.8, "Testing SAM2 functionality...")
261
+ test_image = np.zeros((256, 256, 3), dtype=np.uint8)
262
+ predictor.set_image(test_image)
263
 
264
+ # Ensure correct types and shapes for SAM2
265
+ test_points = np.array([[128.0, 128.0]], dtype=np.float32) # Explicit float32
266
+ test_labels = np.array([1], dtype=np.int32) # Explicit int32
 
 
 
267
 
268
+ try:
269
+ with torch.no_grad(): # Disable gradients for inference
270
+ masks, scores, _ = predictor.predict(
271
+ point_coords=test_points,
272
+ point_labels=test_labels,
273
+ multimask_output=False
274
+ )
275
+ except Exception as e:
276
+ raise Exception(f"SAM2 prediction test failed: {e}")
277
+
278
+ if masks is None or len(masks) == 0:
279
+ raise Exception("SAM2 predictor test failed - no masks generated")
280
+
281
+ _prog(1.0, "SAM2 loaded and validated successfully!")
282
+ logger.info(f"SAM2 predictor loaded and tested successfully on {device}")
283
+ return predictor
284
+
285
+ except Exception as e:
286
+ logger.error(f"SAM2 loading failed: {str(e)}")
287
+ logger.error(f"Full traceback: {traceback.format_exc()}")
288
+ raise Exception(f"SAM2 loading failed: {str(e)}")
289
+
290
+ # ============================================================================ #
291
+ # MATANYONE LOADER WITH VALIDATION - ROBUST
292
+ # ============================================================================ #
293
+ def load_matanyone_fixed(progress_callback: Optional[callable] = None) -> Any:
294
+ """Load MatAnyone with proper error handling and validation"""
295
+ def _prog(pct: float, desc: str):
296
+ if progress_callback:
297
+ progress_callback(pct, desc)
298
+
299
+ try:
300
+ _prog(0.2, "Loading MatAnyone...")
301
+
302
+ try:
303
+ from matanyone import InferenceCore
304
+ except ImportError as e:
305
+ raise Exception(f"MatAnyone import failed: {e}. Make sure MatAnyone is properly installed.")
306
+
307
+ try:
308
+ processor = InferenceCore("PeiqingYang/MatAnyone")
309
+ except Exception as e:
310
+ raise Exception(f"MatAnyone model loading failed: {e}")
311
+
312
+ # Test MatAnyone with dummy data
313
+ _prog(0.8, "Testing MatAnyone functionality...")
314
+ test_image = np.zeros((256, 256, 3), dtype=np.uint8)
315
+ test_mask = np.zeros((256, 256), dtype=np.uint8)
316
+ test_mask[64:192, 64:192] = 255
317
+
318
+ # Test the processor
319
+ try:
320
+ if hasattr(processor, 'process') or hasattr(processor, '__call__'):
321
+ logger.info("MatAnyone processor interface detected")
322
+ else:
323
+ logger.warning("MatAnyone interface unclear, will use fallback refinement")
324
+ except Exception as test_e:
325
+ logger.warning(f"MatAnyone test failed: {test_e}, will use enhanced OpenCV")
326
+
327
+ _prog(1.0, "MatAnyone loaded successfully!")
328
+ logger.info(f"MatAnyone processor loaded successfully on {DEVICE}")
329
+ return processor
330
+
331
+ except Exception as e:
332
+ logger.error(f"MatAnyone loading failed: {str(e)}")
333
+ logger.error(f"Full traceback: {traceback.format_exc()}")
334
+ raise Exception(f"MatAnyone loading failed: {str(e)}")
335
+
336
+ # ============================================================================ #
337
+ # MODEL MANAGEMENT FUNCTIONS
338
+ # ============================================================================ #
339
+ def get_model_status() -> Dict[str, str]:
340
+ """Return current model status for UI"""
341
+ global sam2_predictor, matanyone_model, models_loaded
342
+ return {
343
+ 'sam2': 'Ready' if sam2_predictor is not None else 'Not loaded',
344
+ 'matanyone': 'Ready' if matanyone_model is not None else 'Not loaded',
345
+ 'validated': models_loaded,
346
+ 'device': str(DEVICE)
347
+ }
348
+
349
+ def get_cache_status() -> Dict[str, Any]:
350
+ """Get current cache status"""
351
+ return {
352
+ "sam2_loaded": sam2_predictor is not None,
353
+ "matanyone_loaded": matanyone_model is not None,
354
+ "models_validated": models_loaded,
355
+ "two_stage_available": TWO_STAGE_AVAILABLE,
356
+ "device": str(DEVICE)
357
+ }
358
+
359
+ def load_models_with_validation(progress_callback: Optional[callable] = None) -> str:
360
+ """Load models with comprehensive validation"""
361
+ global sam2_predictor, matanyone_model, models_loaded, two_stage_processor, PROCESS_CANCELLED
362
+
363
+ with loading_lock:
364
+ if models_loaded and not PROCESS_CANCELLED.is_set():
365
+ return "Models already loaded and validated"
366
+
367
+ try:
368
+ PROCESS_CANCELLED.clear()
369
+ start_time = time.time()
370
+ logger.info(f"Starting model loading on {DEVICE}")
371
+
372
+ if progress_callback:
373
+ progress_callback(0.0, f"Starting model loading on {DEVICE}...")
374
+
375
+ # Load SAM2 with validation
376
+ sam2_predictor = load_sam2_predictor_fixed(device=DEVICE, progress_callback=progress_callback)
377
+
378
+ if PROCESS_CANCELLED.is_set():
379
+ return "Model loading cancelled by user"
380
+
381
+ # Load MatAnyone with validation
382
+ matanyone_model = load_matanyone_fixed(progress_callback=progress_callback)
383
+
384
+ if PROCESS_CANCELLED.is_set():
385
+ return "Model loading cancelled by user"
386
+
387
+ models_loaded = True
388
+
389
+ # Initialize two-stage processor if available
390
+ if TWO_STAGE_AVAILABLE:
391
+ two_stage_processor = TwoStageProcessor(sam2_predictor, matanyone_model)
392
+ logger.info("Two-stage processor initialized")
393
+
394
+ load_time = time.time() - start_time
395
+ message = f"SUCCESS: SAM2 + MatAnyone loaded and validated in {load_time:.1f}s on {DEVICE}"
396
+ if TWO_STAGE_AVAILABLE:
397
+ message += " (Two-stage mode available)"
398
+ logger.info(message)
399
+ return message
400
+
401
+ except Exception as e:
402
+ models_loaded = False
403
+ error_msg = f"Model loading failed: {str(e)}"
404
+ logger.error(error_msg)
405
+ return error_msg
406
+
407
+ # ============================================================================ #
408
+ # MAIN VIDEO PROCESSING - USING UTILITIES FUNCTIONS - ROBUST
409
+ # ============================================================================ #
410
+ def process_video_fixed(
411
+ video_path: str,
412
+ background_choice: str,
413
+ custom_background_path: Optional[str],
414
+ progress_callback: Optional[callable] = None,
415
+ use_two_stage: bool = False,
416
+ chroma_preset: str = "standard",
417
+ preview_mask: bool = False,
418
+ preview_greenscreen: bool = False
419
+ ) -> Tuple[Optional[str], str]:
420
+ """Optimized video processing using proper functions from utilities - ROBUST VERSION"""
421
+ global PROCESS_CANCELLED
422
+
423
+ if PROCESS_CANCELLED.is_set():
424
+ return None, "Processing cancelled by user"
425
+
426
+ if not models_loaded:
427
+ return None, "Models not loaded. Call load_models_with_validation() first."
428
+
429
+ if not video_path or not os.path.exists(video_path):
430
+ return None, f"Video file not found: {video_path}"
431
+
432
+ # Validate video file
433
+ is_valid, validation_msg = validate_video_file(video_path)
434
+ if not is_valid:
435
+ return None, f"Invalid video: {validation_msg}"
436
+
437
+ def _prog(pct: float, desc: str):
438
+ if PROCESS_CANCELLED.is_set():
439
+ raise Exception("Processing cancelled by user")
440
+
441
+ if progress_callback:
442
+ progress_callback(pct, desc)
443
+
444
+ # Update processing info file
445
+ if "Frame" in desc and "|" in desc:
446
+ parts = desc.split("|")
447
+ frame_info = parts[0].strip() if len(parts) > 0 else ""
448
+ time_info = parts[1].strip() if len(parts) > 1 else ""
449
+ fps_info = parts[2].strip() if len(parts) > 2 else ""
450
+ eta_info = parts[3].strip() if len(parts) > 3 else ""
451
+
452
+ display_text = f"""📊 PROCESSING STATUS
453
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━
454
+ 🎬 {frame_info}
455
+ ⏱️ Elapsed: {time_info}
456
+ ⚡ Speed: {fps_info}
457
+ 🎯 {eta_info}
458
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━
459
+ 📈 Progress: {pct*100:.1f}%"""
460
+ try:
461
+ with open("/tmp/processing_info.txt", 'w') as f:
462
+ f.write(display_text)
463
+ except Exception as e:
464
+ logger.warning(f"Error writing processing info: {e}")
465
+
466
+ try:
467
+ _prog(0.0, f"Starting {'TWO-STAGE' if use_two_stage else 'SINGLE-STAGE'} processing on {DEVICE}...")
468
+
469
+ # Check if two-stage mode is requested
470
+ if use_two_stage:
471
+ if not TWO_STAGE_AVAILABLE:
472
+ return None, "Two-stage mode not available. Please add two_stage_processor.py file."
473
+
474
+ if two_stage_processor is None:
475
+ return None, "Two-stage processor not initialized. Please reload models."
476
+
477
+ _prog(0.05, "Starting TWO-STAGE green screen processing...")
478
+
479
+ # Get video dimensions
480
+ cap = cv2.VideoCapture(video_path)
481
+ frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
482
+ frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
483
+ cap.release()
484
+
485
+ # Prepare background
486
+ if background_choice == "custom" and custom_background_path:
487
+ if not os.path.exists(custom_background_path):
488
+ return None, f"Custom background not found: {custom_background_path}"
489
+
490
+ background = cv2.imread(custom_background_path)
491
+ if background is None:
492
+ return None, "Could not read custom background image."
493
+ background_name = "Custom Image"
494
+ else:
495
+ if background_choice in PROFESSIONAL_BACKGROUNDS:
496
+ bg_config = PROFESSIONAL_BACKGROUNDS[background_choice]
497
+ background = create_professional_background(bg_config, frame_width, frame_height)
498
+ background_name = bg_config["name"]
499
+ else:
500
+ return None, f"Invalid background selection: {background_choice}"
501
+
502
+ # Get chroma settings
503
+ chroma_settings = CHROMA_PRESETS.get(chroma_preset, CHROMA_PRESETS['standard'])
504
+
505
+ # Run two-stage pipeline
506
+ timestamp = int(time.time())
507
+ final_output = f"/tmp/twostage_final_{timestamp}.mp4"
508
+
509
+ result, message = two_stage_processor.process_full_pipeline(
510
+ video_path,
511
+ background,
512
+ final_output,
513
+ chroma_settings=chroma_settings,
514
+ progress_callback=_prog
515
+ )
516
+
517
+ if PROCESS_CANCELLED.is_set():
518
+ return None, "Processing cancelled by user"
519
+
520
+ if result is None:
521
+ return None, message
522
+
523
+ # Add audio back - ROBUST VERSION
524
+ _prog(0.9, "Adding audio...")
525
+ final_with_audio = f"/tmp/twostage_audio_{timestamp}.mp4"
526
+
527
+ audio_success = run_ffmpeg_command([
528
+ 'ffmpeg', '-y', '-i', final_output, '-i', video_path,
529
+ '-c:v', 'libx264', '-crf', '18', '-preset', 'medium',
530
+ '-c:a', 'aac', '-b:a', '192k', '-ac', '2', '-ar', '48000',
531
+ '-map', '0:v:0', '-map', '1:a:0?', '-shortest', final_with_audio
532
+ ], "Two-stage audio processing")
533
+
534
+ if audio_success and os.path.exists(final_with_audio):
535
+ try:
536
+ os.remove(final_output)
537
+ except:
538
+ pass
539
+ final_output = final_with_audio
540
+ else:
541
+ logger.warning("Audio processing failed, using video without audio")
542
+
543
+ _prog(1.0, "TWO-STAGE processing complete!")
544
+
545
+ success_message = (
546
+ f"TWO-STAGE Success!\n"
547
+ f"Background: {background_name}\n"
548
+ f"Method: Green Screen Chroma Key\n"
549
+ f"Preset: {chroma_preset}\n"
550
+ f"Quality: Professional cinema-grade\n"
551
+ f"Device: {DEVICE}"
552
+ )
553
+
554
+ return final_output, success_message
555
+
556
+ # Single-stage processing
557
+ _prog(0.05, f"Starting SINGLE-STAGE processing on {DEVICE}...")
558
+
559
+ cap = cv2.VideoCapture(video_path)
560
+ if not cap.isOpened():
561
+ return None, "Could not open video file."
562
+
563
+ fps = cap.get(cv2.CAP_PROP_FPS)
564
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
565
+ frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
566
+ frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
567
+
568
+ if total_frames == 0:
569
+ cap.release()
570
+ return None, "Video appears to be empty."
571
+
572
+ # Log video info
573
+ logger.info(f"Video info: {frame_width}x{frame_height}, {fps}fps, {total_frames} frames, processing on {DEVICE}")
574
+
575
+ # Prepare background
576
+ background = None
577
+ background_name = ""
578
+
579
+ if background_choice == "custom" and custom_background_path:
580
+ if not os.path.exists(custom_background_path):
581
+ cap.release()
582
+ return None, f"Custom background not found: {custom_background_path}"
583
+
584
+ background = cv2.imread(custom_background_path)
585
+ if background is None:
586
+ cap.release()
587
+ return None, "Could not read custom background image."
588
+ background_name = "Custom Image"
589
+ else:
590
+ if background_choice in PROFESSIONAL_BACKGROUNDS:
591
+ bg_config = PROFESSIONAL_BACKGROUNDS[background_choice]
592
+ background = create_professional_background(bg_config, frame_width, frame_height)
593
+ background_name = bg_config["name"]
594
+ else:
595
+ cap.release()
596
+ return None, f"Invalid background selection: {background_choice}"
597
+
598
+ if background is None:
599
+ cap.release()
600
+ return None, "Failed to create background."
601
+
602
+ timestamp = int(time.time())
603
+
604
+ _prog(0.1, f"Processing {total_frames} frames with {'TWO-STAGE' if use_two_stage else 'SINGLE-STAGE'} processing on {DEVICE}...")
605
+
606
+ # FIXED: Ensure output_path is always defined
607
+ if preview_mask or preview_greenscreen:
608
+ output_path = f"/tmp/preview_{timestamp}.mp4"
609
+ else:
610
+ output_path = f"/tmp/output_{timestamp}.mp4"
611
+
612
+ # ROBUST: Create video writer with codec fallback
613
+ final_writer, actual_output_path = create_video_writer(output_path, fps, frame_width, frame_height)
614
+ if final_writer is None:
615
+ cap.release()
616
+ return None, "Could not create output video file with any codec."
617
+
618
+ # Update output_path to actual path (may have different extension)
619
+ output_path = actual_output_path
620
+
621
+ frame_count = 0
622
+ successful_frames = 0
623
+ last_refined_mask = None
624
+
625
+ # Processing stats
626
+ start_time = time.time()
627
+
628
+ try:
629
+ while True:
630
+ if PROCESS_CANCELLED.is_set():
631
+ break
632
+
633
+ ret, frame = cap.read()
634
+ if not ret:
635
+ break
636
+
637
+ # Skip frames if FRAME_SKIP > 1
638
+ if frame_count % FRAME_SKIP != 0:
639
+ frame_count += 1
640
+ continue
641
+
642
+ try:
643
+ # Update progress with detailed timing info and ETA
644
+ elapsed_time = time.time() - start_time
645
+ current_fps = frame_count / elapsed_time if elapsed_time > 0 else 0
646
+ remaining_frames = total_frames - frame_count
647
+ eta_seconds = remaining_frames / current_fps if current_fps > 0 else 0
648
+ eta_display = f"{int(eta_seconds//60)}m {int(eta_seconds%60)}s" if eta_seconds > 60 else f"{int(eta_seconds)}s"
649
+
650
+ progress_msg = f"Frame {frame_count + 1}/{total_frames} | {elapsed_time:.1f}s | {current_fps:.1f} fps | ETA: {eta_display} | Device: {DEVICE}"
651
+
652
+ # Log and display progress with clamped percentage
653
+ logger.info(progress_msg)
654
+ pct = min(1.0, 0.1 + (frame_count / max(1, total_frames)) * 0.8)
655
+ _prog(pct, progress_msg)
656
+
657
+ # CRITICAL: Use functions from utilities.py, not local implementations!
658
+ # SAM2 segmentation using utilities function
659
+ mask = segment_person_hq(frame, sam2_predictor)
660
+
661
+ if preview_mask:
662
+ # Save mask visualization - proper green color in BGR format
663
+ mask_vis = np.zeros_like(frame)
664
+ mask_vis[..., 1] = mask # Put mask in green channel (BGR format)
665
+ final_writer.write(mask_vis.astype(np.uint8))
666
+ frame_count += 1
667
+ continue
668
+
669
+ # MatAnyone refinement on keyframes using utilities function
670
+ if (frame_count % KEYFRAME_INTERVAL == 0) or (last_refined_mask is None):
671
+ refined_mask = refine_mask_hq(frame, mask, matanyone_model)
672
+ last_refined_mask = refined_mask.copy()
673
+ logger.info(f"Keyframe refinement at frame {frame_count} on {DEVICE}")
674
+ else:
675
+ # Blend SAM2 mask with last refined mask for temporal smoothness
676
+ alpha = 0.7
677
+ refined_mask = cv2.addWeighted(mask, alpha, last_refined_mask, 1-alpha, 0)
678
+
679
+ if preview_greenscreen:
680
+ # Create green screen preview
681
+ green_bg = np.zeros_like(frame)
682
+ green_bg[:, :] = [0, 255, 0] # Pure green
683
+ preview_frame = frame.copy()
684
+ mask_3ch = cv2.cvtColor(refined_mask, cv2.COLOR_GRAY2BGR)
685
+ mask_norm = mask_3ch.astype(float) / 255
686
+ preview_frame = preview_frame * mask_norm + green_bg * (1 - mask_norm)
687
+ final_writer.write(preview_frame.astype(np.uint8))
688
+ frame_count += 1
689
+ continue
690
+
691
+ # CRITICAL: Use replace_background_hq from utilities which has the transparency fix!
692
+ result_frame = replace_background_hq(frame, refined_mask, background)
693
+ final_writer.write(result_frame.astype(np.uint8))
694
+ successful_frames += 1
695
+
696
+ except Exception as frame_error:
697
+ logger.warning(f"Error processing frame {frame_count}: {frame_error}")
698
+ # Write original frame if processing fails
699
+ final_writer.write(frame)
700
+
701
+ frame_count += 1
702
+
703
+ # Memory management
704
+ if frame_count % MEMORY_CLEANUP_INTERVAL == 0:
705
+ gc.collect()
706
+ if DEVICE.type == 'cuda': # Use consistent device checking
707
+ torch.cuda.empty_cache()
708
+ elapsed = time.time() - start_time
709
+ fps_actual = frame_count / elapsed
710
+ eta = (total_frames - frame_count) / fps_actual if fps_actual > 0 else 0
711
+ logger.info(f"Progress: {frame_count}/{total_frames}, FPS: {fps_actual:.1f}, ETA: {eta:.0f}s, Device: {DEVICE}")
712
+
713
+ finally:
714
+ # ALWAYS cleanup resources
715
+ cap.release()
716
+ final_writer.release()
717
+
718
+ if PROCESS_CANCELLED.is_set():
719
+ _prog(0.95, "Cleaning up cancelled process...")
720
+ try:
721
+ if os.path.exists(output_path):
722
+ os.remove(output_path)
723
+ except:
724
+ pass
725
+ return None, "Processing cancelled by user"
726
+
727
+ if successful_frames == 0:
728
+ return None, "No frames were processed successfully with AI."
729
+
730
+ # Calculate processing stats
731
+ total_time = time.time() - start_time
732
+ avg_fps = frame_count / total_time if total_time > 0 else 0
733
+
734
+ _prog(0.9, "Finalizing output...")
735
+
736
+ if preview_mask or preview_greenscreen:
737
+ final_output = output_path
738
+ else:
739
+ # Add audio back for final output - ROBUST VERSION
740
+ _prog(0.9, "Adding audio...")
741
+ final_output = f"/tmp/final_{timestamp}.mp4"
742
+
743
+ audio_success = run_ffmpeg_command([
744
+ 'ffmpeg', '-y', '-i', output_path, '-i', video_path,
745
+ '-c:v', 'libx264', '-crf', '18', '-preset', 'medium',
746
+ '-c:a', 'aac', '-b:a', '192k', '-ac', '2', '-ar', '48000',
747
+ '-map', '0:v:0', '-map', '1:a:0?', '-shortest', final_output
748
+ ], "Audio processing")
749
+
750
+ if not audio_success or not os.path.exists(final_output):
751
+ logger.warning("Audio processing failed, using video without audio")
752
+ try:
753
+ shutil.copy2(output_path, final_output)
754
+ except Exception as e:
755
+ logger.error(f"Failed to copy video: {e}")
756
+ final_output = output_path
757
+
758
+ # Cleanup intermediate file
759
+ try:
760
+ if os.path.exists(output_path) and output_path != final_output:
761
+ os.remove(output_path)
762
+ except Exception as e:
763
+ logger.warning(f"Cleanup error: {e}")
764
+
765
+ _prog(1.0, "Processing complete!")
766
+
767
+ success_message = (
768
+ f"Success!\n"
769
+ f"Background: {background_name}\n"
770
+ f"Resolution: {frame_width}x{frame_height}\n"
771
+ f"Total frames: {frame_count}\n"
772
+ f"Successfully processed: {successful_frames}\n"
773
+ f"Processing time: {total_time:.1f}s\n"
774
+ f"Average FPS: {avg_fps:.1f}\n"
775
+ f"Keyframe interval: {KEYFRAME_INTERVAL}\n"
776
+ f"Mode: {'TWO-STAGE' if use_two_stage else 'SINGLE-STAGE'}\n"
777
+ f"Device: {DEVICE}"
778
+ )
779
+
780
+ return final_output, success_message
781
+
782
+ except Exception as e:
783
+ logger.error(f"Processing error: {traceback.format_exc()}")
784
+ return None, f"Processing Error: {str(e)}"
785
+
786
+ # ============================================================================ #
787
+ # MAIN - IMPORT UI COMPONENTS
788
+ # ============================================================================ #
789
+ def main():
790
+ try:
791
+ print("===== FINAL FIXED VIDEO BACKGROUND REPLACEMENT =====")
792
+ print(f"Keyframe interval: {KEYFRAME_INTERVAL} frames")
793
+ print(f"Frame skip: {FRAME_SKIP} (1=all frames, 2=every other)")
794
+ print(f"Two-stage mode: {'AVAILABLE' if TWO_STAGE_AVAILABLE else 'NOT AVAILABLE'}")
795
+ print(f"Device: {DEVICE}")
796
+ print("Loading UI components...")
797
+
798
+ # Import UI components
799
+ from ui_components import create_interface
800
+
801
+ os.makedirs("/tmp/MyAvatar/My_Videos/", exist_ok=True)
802
+ CACHE_DIR.mkdir(exist_ok=True, parents=True)
803
+
804
+ print("Creating interface...")
805
+ demo = create_interface()
806
+
807
+ print("Launching...")
808
+ # Fixed for HF Spaces - share=True is required when localhost not accessible
809
  demo.queue().launch(
810
  server_name="0.0.0.0",
811
  server_port=7860,
812
+ share=True, # Required for HF Spaces
813
  show_error=True,
814
  debug=True
815
  )
816
+
817
  except Exception as e:
818
+ logger.error(f"Startup failed: {e}")
819
+ print(f"Startup failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
820
 
821
  if __name__ == "__main__":
822
  main()