MogensR commited on
Commit
412ec39
·
1 Parent(s): 56754de

Update core/app.py

Browse files
Files changed (1) hide show
  1. core/app.py +618 -1030
core/app.py CHANGED
@@ -1,1054 +1,642 @@
 
1
  """
2
- MyAvatar - Complete AI Avatar Video Generation Platform
3
- ========================================================
4
- Modular version with enhanced organization - REFACTORED ROUTES + PREMIUM FEATURES + BACKGROUNDFX + VIDEO PROCESSING
5
  """
6
- from fastapi import FastAPI, Request, HTTPException, Depends
7
- from fastapi.middleware.cors import CORSMiddleware
8
- from fastapi.staticfiles import StaticFiles
9
- from fastapi.templating import Jinja2Templates
10
- import uvicorn
11
- from app.database.database import init_database, create_admin_user, update_database_schema
12
- from app.utils.logging_config import logger, log_info, log_error, log_compatibility_status
13
- import gradio as gr
14
- from app.tools.huggingface_app import huggingface_gradio_app
15
- from dotenv import load_dotenv
16
  import os
 
 
 
 
 
 
17
  import logging
18
- import traceback
19
- import uuid
20
  import threading
21
- from datetime import datetime
22
- from typing import Optional
23
- from pathlib import Path
24
  import sys
25
-
26
- # Load environment variables
27
- load_dotenv()
28
-
29
- # Define the base directory of the project
30
- BASE_DIR = Path(__file__).resolve().parent
31
-
32
- # Setup logging
33
  logging.basicConfig(
34
- level=logging.INFO,
35
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
36
- )
37
- logger = logging.getLogger("MyAvatar")
38
-
39
- # SAFE IMPORTS - wrap in try/catch to prevent startup crashes
40
- try:
41
- from app.services.notifications import send_alert, notify_service_status
42
- except ImportError as e:
43
- logger.warning(f"Notifications service not available: {e}")
44
- def notify_service_status(*args, **kwargs): pass
45
-
46
- try:
47
- # Import compatibility mode checking
48
- from app.compatibility import ENABLE_SAFE_MODE, ENABLE_BACKGROUND_REPLACEMENT, log_compatibility_status
49
- except ImportError as e:
50
- logger.warning(f"Compatibility module not available: {e}")
51
- ENABLE_SAFE_MODE = True
52
- ENABLE_BACKGROUND_REPLACEMENT = False
53
- def log_compatibility_status(): return {"safe_mode": True}
54
-
55
- try:
56
- # Import modular components
57
- from app.logger.log_handler import log_handler, log_info, log_error, log_warning
58
- except ImportError as e:
59
- logger.warning(f"Log handler not available: {e}")
60
- def log_info(msg, context): logger.info(f"[{context}] {msg}")
61
- def log_error(msg, context, exc=None): logger.error(f"[{context}] {msg}")
62
- def log_warning(msg, context): logger.warning(f"[{context}] {msg}")
63
-
64
- try:
65
- from app.db.database import init_database, update_database_schema, get_db_connection
66
- from app.db.admin import create_admin_user
67
- except ImportError as e:
68
- logger.error(f"Database modules not available: {e}")
69
- def init_database(): pass
70
- def update_database_schema(): pass
71
- def create_admin_user(): pass
72
-
73
- # Import HeyGen API for debug endpoints
74
- try:
75
- from app.api.heygen import get_available_avatars
76
- except ImportError as e:
77
- logger.warning(f"HeyGen API module not available: {e}")
78
- def get_available_avatars(*args, **kwargs):
79
- return {"error": "HeyGen API not available"}
80
-
81
- # IMPORT VIDEO URL REFRESHER
82
- try:
83
- from video_url_refresher import VideoURLRefresher
84
- video_refresher_available = True
85
- logger.info("Video URL refresher imported successfully")
86
- except ImportError as e:
87
- logger.warning(f"Video URL refresher not available: {e}")
88
- video_refresher_available = False
89
-
90
- # FASTAPI APP INITIALIZATION
91
- app = FastAPI(title="MyAvatar", description="AI Avatar Video Generation Platform - Premium Edition with BackgroundFX + Advanced Video Processing")
92
-
93
- # AUTO-RUN DATABASE MIGRATIONS ON STARTUP
94
- try:
95
- logger.info("🔄 Starting database migration process...")
96
- logger.info(f"🔍 Current working directory: {os.getcwd()}")
97
- logger.info(f"🔍 Python path: {sys.path[:3]}...") # Show first 3 paths
98
-
99
- # Try to import migration runner
100
- logger.info("📦 Importing migration runner...")
101
- from run_migrations import run_migrations
102
- logger.info("✅ Migration runner imported successfully")
103
-
104
- # Run migrations
105
- logger.info("🚀 Executing migrations...")
106
- migration_success = run_migrations()
107
-
108
- if migration_success:
109
- logger.info("🎉 Database migrations completed successfully")
110
- else:
111
- logger.error("❌ Database migrations failed - this will cause upload errors")
112
-
113
- except ImportError as e:
114
- logger.error(f"❌ Could not import migration runner: {e}")
115
- logger.error("📁 This means video processing will fail - missing database tables")
116
- except Exception as e:
117
- logger.error(f"❌ Migration error: {e}")
118
- logger.error("📁 This will cause video upload failures")
119
- import traceback
120
- logger.error(f"🔍 Full traceback: {traceback.format_exc()}")
121
-
122
- # CORS middleware
123
- app.add_middleware(
124
- CORSMiddleware,
125
- allow_origins=["*"],
126
- allow_credentials=True,
127
- allow_methods=["*"],
128
- allow_headers=["*"],
129
  )
130
-
131
- # Exception handler
132
- @app.exception_handler(Exception)
133
- async def global_exception_handler(request: Request, exc: Exception):
134
- log_error(f"Uncaught exception: {str(exc)}", "Server", exc)
135
- return JSONResponse(
136
- status_code=500,
137
- content={"detail": "Internal server error", "message": str(exc)}
138
- )
139
-
140
- # Mount static files safely
141
- try:
142
- app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
143
- except Exception as e:
144
- logger.warning(f"Could not mount static files: {e}")
145
-
146
-
147
- # =============================================================================
148
- # VIDEO URL REFRESHER BACKGROUND SERVICE
149
- # =============================================================================
150
-
151
- def start_video_url_refresher():
152
- """Start the video URL refresher in a background thread"""
153
- try:
154
- if not video_refresher_available:
155
- logger.warning("Video URL refresher not available - skipping background service")
156
- return
157
-
158
- logger.info("Starting video URL refresher background service...")
159
- refresher = VideoURLRefresher()
160
-
161
- # Configure intervals from environment variables
162
- interval_hours = int(os.getenv('REFRESH_INTERVAL_HOURS', '4'))
163
- threshold_hours = int(os.getenv('EXPIRY_THRESHOLD_HOURS', '6'))
164
-
165
- logger.info(f"Video URL refresher configured:")
166
- logger.info(f" • Refresh interval: {interval_hours} hours")
167
- logger.info(f" • Expiry threshold: {threshold_hours} hours")
168
-
169
- # Run the refresher continuously
170
- refresher.run_continuous(interval_hours=interval_hours, hours_threshold=threshold_hours)
171
-
172
- except Exception as e:
173
- logger.error(f"Error starting video URL refresher: {e}")
174
- logger.error(f"Video URL refresher traceback: {traceback.format_exc()}")
175
-
176
- # Start the background refresher service
177
- if video_refresher_available:
178
- refresher_thread = threading.Thread(target=start_video_url_refresher, daemon=True)
179
- refresher_thread.start()
180
- logger.info("✅ Video URL refresher background service started")
181
- else:
182
- logger.warning("⚠️ Video URL refresher background service not started - module not available")
183
-
184
- # ============================================================================
185
- # GRADIO MONKEY PATCH (BUG FIX for gradio>=4.44.0)
186
- # ============================================================================
187
- # Addresses a schema validation issue where booleans are not handled correctly.
188
- # See memory: 6d1e1af9-ea10-4f3d-b9d5-45c2657ebcf6
189
-
190
  try:
191
  import gradio_client.utils as gc_utils
192
- original_get_type = gc_utils.get_type
193
-
194
- def patched_get_type(schema):
195
- # The original function fails if schema is not a dict, handle this gracefully
196
  if not isinstance(schema, dict):
197
- # if the schema is a boolean, return "boolean" to avoid an iteration error
198
- if isinstance(schema, bool):
199
- return "boolean"
200
- # Fallback for other unexpected schema formats
201
  return "string"
202
- return original_get_type(schema)
203
-
204
- gc_utils.get_type = patched_get_type
205
- logging.info("✅ Applied Gradio schema validation monkey patch.")
206
- except (ImportError, AttributeError) as e:
207
- logging.warning(f"⚠️ Could not apply Gradio monkey patch: {e}")
208
-
209
- # ============================================================================
210
- # APPLICATION STARTUP
211
- # ============================================================================
212
-
213
- # Perform initial setup
214
- def ensure_directories():
215
- # Ensure necessary directories exist
216
- directories = ['static', 'templates', 'app/database']
217
- for directory in directories:
218
- dir_path = BASE_DIR / directory
219
- if not dir_path.exists():
220
- os.makedirs(dir_path)
221
-
222
- def install_system_dependencies():
223
- # Install system dependencies
224
- # Add your system dependencies installation code here
225
- pass
226
-
227
- def install_python_requirements():
228
- # Install Python requirements
229
- # Add your Python requirements installation code here
230
- pass
231
-
232
- ensure_directories()
233
- install_system_dependencies()
234
- install_python_requirements()
235
-
236
- # ============================================================================
237
- # FASTAPI APPLICATION
238
- # =============================================================================
239
-
240
- # Initialize file change tracker
241
- try:
242
- from app.startup.file_tracker_startup import initialize_file_tracker
243
- initialize_file_tracker()
244
- except ImportError as e:
245
- logger.warning(f"File change tracker not available: {e}")
246
  except Exception as e:
247
- logger.error(f"Error starting file change tracker: {e}")
248
-
249
- # =============================================================================
250
- # MODULAR ROUTE IMPORTS - REFACTORED STRUCTURE + PREMIUM FEATURES + BACKGROUNDFX + VIDEO PROCESSING
251
- # =============================================================================
252
-
253
- routers_loaded = []
254
- router_errors = []
255
-
256
- # 🔄 NEW MODULAR ROUTES (split from old web_routes.py)
257
- modular_route_imports = [
258
- # Core authentication and user routes (no prefix - root level)
259
- ("app.routes.auth_routes", "router", None),
260
-
261
- # Admin routes with /admin prefix
262
- ("app.routes.admin_routes", "router", "/admin"),
263
-
264
- # API routes with /api prefix - UNCOMMENTED to enable video creation endpoints
265
- ("app.routes.api_routes", "router", "/api"),
266
-
267
- # 🎯 PREMIUM ROUTES - NEW COMPLETE PREMIUM SYSTEM - FIXED IMPORT PATH
268
- ("app.routes.premium_routes", "router", None), # ✅ FIXED - Now uses app.routes.premium_routes
269
-
270
- # 🎯 BACKGROUNDFX ROUTES - NEW HeyGen WebM + Transparent Video System
271
- ("app.routes.backgroundfx_routes", "router", None), # No prefix since it has its own /api/backgrounds
272
-
273
- # 🎬 VIDEO PROCESSING ROUTES - NEW Advanced Background Replacement API
274
- ("app.routes.video_processing_routes", "router", "/video-processing"),
275
-
276
- # Video routes with NO prefix - FIXED for template routes
277
- ("app.routes.video_routes", "router", None),
278
-
279
- # Emergency routes with /emergency prefix
280
- ("app.routes.emergency_routes", "router", "/emergency"),
281
-
282
- # File tracker routes with /admin/file-tracker prefix
283
- ("app.routes.file_tracker_routes", "router", "/admin/file-tracker"),
284
-
285
- # Dashboard and main app routes (if you create web_routes.py for remaining routes)
286
- ("app.routes.web_routes", "router", None),
287
- ]
288
-
289
- # Load modular routes with prefixes
290
- for module_name, router_name, prefix in modular_route_imports:
291
- try:
292
- logger.info(f"Attempting to import {module_name}...")
293
- module = __import__(module_name, fromlist=[router_name])
294
- logger.info(f"Successfully imported {module_name}, getting router...")
295
- router = getattr(module, router_name)
296
- logger.info(f"Got router from {module_name}, including in app with prefix: {prefix}")
297
-
298
- if prefix:
299
- app.include_router(router, prefix=prefix)
300
- logger.info(f"✅ Successfully loaded router: {module_name} (prefix: {prefix})")
301
- else:
302
- app.include_router(router)
303
- logger.info(f"✅ Successfully loaded router: {module_name} (no prefix)")
304
-
305
- routers_loaded.append(f"{module_name}{' -> ' + prefix if prefix else ''}")
306
-
307
- except Exception as e:
308
- error_details = {
309
- "module": module_name,
310
- "error": str(e),
311
- "traceback": traceback.format_exc()
312
- }
313
- router_errors.append(error_details)
314
- logger.error(f"❌ Could not load router {module_name}: {e}")
315
- logger.error(f"Full traceback: {traceback.format_exc()}")
316
-
317
- # 🔧 LEGACY/REMAINING ROUTES (keep your existing working routes)
318
- legacy_route_imports = [
319
- # Keep these existing routes that still work
320
- ("app.routes.health_routes", "router"),
321
- ("app.routes.debug_routes", "router"),
322
- ("app.routes.voice_routes", "router"),
323
- ("app.routes.avatar_rebuild_route", "router"),
324
- ("app.routes.migration_routes", "router"),
325
- ("app.routes.finance_routes", "router"),
326
  ]
327
-
328
- # Load legacy routes (no prefixes)
329
- for module_name, router_name in legacy_route_imports:
330
- try:
331
- logger.info(f"Attempting to import legacy route {module_name}...")
332
- module = __import__(module_name, fromlist=[router_name])
333
- logger.info(f"Successfully imported {module_name}, getting router...")
334
- router = getattr(module, router_name)
335
- logger.info(f"Got router from {module_name}, including in app...")
336
- app.include_router(router)
337
- routers_loaded.append(f"{module_name} (legacy)")
338
- logger.info(f"✅ Successfully loaded legacy router: {module_name}")
339
- except Exception as e:
340
- error_details = {
341
- "module": module_name,
342
- "error": str(e),
343
- "traceback": traceback.format_exc()
344
- }
345
- router_errors.append(error_details)
346
- logger.error(f"❌ Could not load legacy router {module_name}: {e}")
347
- logger.error(f"Full traceback: {traceback.format_exc()}")
348
-
349
- # Background routes - conditional and safe
350
- if ENABLE_BACKGROUND_REPLACEMENT:
351
- try:
352
- from app.database.background_schema import initialize_backgrounds_schema, add_default_backgrounds
353
- from app.routes.background_routes import router as background_router
354
- app.include_router(background_router, prefix="/background", tags=["background"])
355
- routers_loaded.append("background_routes -> /background")
356
- logger.info("Background replacement routes loaded")
357
- except Exception as e:
358
- logger.warning(f"Could not load background routes: {e}")
359
-
360
- # =============================================================================
361
- # BACKGROUNDFX PAGE ROUTE - Serve the HTML interface
362
- # =============================================================================
363
-
364
- @app.get("/backgroundfx")
365
- async def backgroundfx_page(request: Request):
366
- """BackgroundFX premium feature page - HeyGen WebM + Transparent Videos"""
367
  try:
368
- logger.info("BackgroundFX page accessed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- # First try the enhanced template
371
- enhanced_html_file = BASE_DIR / "templates" / "backgroundfx_enhanced.html"
372
- if enhanced_html_file.exists():
373
- templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
374
- return templates.TemplateResponse("backgroundfx_enhanced.html", {"request": request})
375
 
376
- # Fall back to original template
377
- html_file = BASE_DIR / "templates" / "backgroundfx.html"
378
- if html_file.exists():
379
- with open(html_file, 'r', encoding='utf-8') as f:
380
- content = f.read()
381
- logger.info("✅ BackgroundFX HTML template loaded successfully")
382
- return HTMLResponse(content=content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  else:
384
- # Fallback to basic page with link to dashboard
385
- logger.warning("⚠️ BackgroundFX HTML template not found, using fallback")
386
- return HTMLResponse(content="""
387
- <!DOCTYPE html>
388
- <html>
389
- <head>
390
- <title>BackgroundFX - Enhanced Video Processing</title>
391
- <style>
392
- body { font-family: Arial, sans-serif; margin: 40px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; color: white; }
393
- .container { max-width: 600px; margin: 0 auto; padding: 40px; background: rgba(255,255,255,0.1); border-radius: 20px; }
394
- h1 { font-size: 3em; margin-bottom: 20px; }
395
- .btn { padding: 15px 30px; background: #fff; color: #667eea; text-decoration: none; border-radius: 10px; font-weight: bold; margin: 10px; display: inline-block; }
396
- .status { background: rgba(16, 185, 129, 0.2); padding: 15px; border-radius: 10px; margin: 20px 0; }
397
- </style>
398
- </head>
399
- <body>
400
- <div class="container">
401
- <h1>BackgroundFX - Enhanced Video Processing</h1>
402
- <div class="status">
403
- <h3>⚠️ Enhanced UI Not Found</h3>
404
- <p>The advanced user interface file is missing.</p>
405
- </div>
406
- <p><strong>Save the Enhanced UI as:</strong> <code>templates/backgroundfx_enhanced.html</code></p>
407
- <a href="/dashboard-direct" class="btn">← Back to Dashboard</a>
408
- <a href="/video-processing/status" class="btn">🔧 Test API</a>
409
- </div>
410
- </body>
411
- </html>
412
- """)
413
-
414
- except Exception as e:
415
- logger.error(f"Error serving BackgroundFX page: {e}")
416
- return HTMLResponse(content=f"""
417
- <div style="font-family: Arial, sans-serif; margin: 40px; text-align: center;">
418
- <h1>BackgroundFX Error</h1>
419
- <p>Error: {str(e)}</p>
420
- <p><a href="/dashboard-direct">Back to Dashboard</a></p>
421
- </div>
422
- """, status_code=500)
423
-
424
- # =============================================================================
425
- # ADMIN PREMIUM MANAGEMENT PAGE
426
- # =============================================================================
427
-
428
- @app.get("/admin/premium")
429
- async def admin_premium_page(request: Request):
430
- """Admin premium management interface"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  try:
432
- logger.info("Admin premium page accessed")
 
 
433
 
434
- # Check if admin premium template exists
435
- admin_premium_file = BASE_DIR / "templates" / "admin_premium.html"
436
- if admin_premium_file.exists():
437
- templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
438
- return templates.TemplateResponse("admin_premium.html", {"request": request})
439
- else:
440
- # Fallback to basic admin premium page
441
- logger.warning("⚠️ Admin premium template not found, using fallback")
442
- return HTMLResponse(content="""
443
- <!DOCTYPE html>
444
- <html>
445
- <head>
446
- <title>Premium Management - MyAvatar Admin</title>
447
- <style>
448
- body { font-family: Arial, sans-serif; margin: 40px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; color: white; }
449
- .container { max-width: 800px; margin: 0 auto; padding: 40px; background: rgba(255,255,255,0.1); border-radius: 20px; }
450
- h1 { font-size: 3em; margin-bottom: 20px; }
451
- .btn { padding: 15px 30px; background: #fff; color: #667eea; text-decoration: none; border-radius: 10px; font-weight: bold; margin: 10px; display: inline-block; }
452
- .status { background: rgba(16, 185, 129, 0.2); padding: 15px; border-radius: 10px; margin: 20px 0; }
453
- </style>
454
- </head>
455
- <body>
456
- <div class="container">
457
- <h1>🎯 Premium Management</h1>
458
- <p>Manage premium subscriptions and user access</p>
459
-
460
- <div class="status">
461
- <h3> Premium System Status: OPERATIONAL</h3>
462
- <p>• Premium routes loaded<br>
463
- # Initialize core processor with loaded models
464
- self.core_processor = CoreVideoProcessor(
465
- sam2_predictor=sam2_predictor,
466
- matanyone_model=matanyone_model,
467
- config=self.config,
468
- memory_mgr=self.memory_manager
469
- ) <p><strong>Save the Admin Interface as:</strong> <code>templates/admin_premium.html</code></p>
470
- <a href="/admin" class="btn"> Back to Admin</a>
471
- <a href="/admin/premium/users" class="btn"> Test API</a>
472
- </div>
473
- </body>
474
- </html>
475
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  except Exception as e:
478
- logger.error(f"Error serving admin premium page: {e}")
479
- return HTMLResponse(content=f"""
480
- <div style="font-family: Arial, sans-serif; margin: 40px; text-align: center;">
481
- <h1>Admin Premium Error</h1>
482
- <p>Error: {str(e)}</p>
483
- <p><a href="/admin">Back to Admin</a></p>
484
- </div>
485
- """, status_code=500)
486
-
487
- # Create necessary directories
488
- directories = [
489
- "static/uploads/audio",
490
- "static/uploads/images",
491
- "output",
492
- "processed",
493
- "uploads",
494
- "temp_audio",
495
- "static/backgrounds", # BackgroundFX storage
496
- "temp/background_processing",
497
- "temp/video_processing"
498
- ]
499
-
500
- for directory in directories:
501
- try:
502
- os.makedirs(BASE_DIR / directory, exist_ok=True)
503
- except Exception as e:
504
- logger.warning(f"Could not create directory {directory}: {e}")
505
-
506
- # BULLETPROOF Health check endpoint
507
- @app.get("/health")
508
- async def health_check():
509
- """Bulletproof health check that never fails"""
510
- try:
511
- log_info("Health check endpoint accessed", "Health")
512
- return {"status": "ok", "timestamp": datetime.now().isoformat()}
513
- except Exception:
514
- # Even if logging fails, return basic response
515
- return {"status": "ok"}
516
-
517
- @app.get("/simple-health")
518
- async def simple_health_check():
519
- """Ultra-simple health check for deployment platforms"""
520
- return {"status": "ok"}
521
-
522
- # DEBUG ROUTE - TO DIAGNOSE ROUTE LOADING ISSUES
523
- @app.get("/debug-routes")
524
- async def debug_routes():
525
- """Debug which routes are loaded and why others failed"""
526
- routes = []
527
- for route in app.routes:
528
- routes.append({
529
- "path": getattr(route, 'path', 'unknown'),
530
- "methods": getattr(route, 'methods', []),
531
- "name": getattr(route, 'name', 'unknown')
532
- })
533
-
534
- # Check for specific route types
535
- backgroundfx_routes = [r for r in routes if "/api/backgrounds" in r.get("path", "")]
536
- video_processing_routes = [r for r in routes if "/video-processing" in r.get("path", "")]
537
- premium_routes = [r for r in routes if "/api/premium" in r.get("path", "") or "/admin/premium" in r.get("path", "")]
538
-
539
- return {
540
- "total_routes": len(app.routes),
541
- "routes_loaded_successfully": routers_loaded,
542
- "router_import_errors": router_errors,
543
- "all_routes": routes,
544
- "premium_system_status": {
545
- "premium_routes_loaded": len(premium_routes),
546
- "premium_routes": premium_routes,
547
- "premium_router_in_loaded": any("premium_routes" in r for r in routers_loaded),
548
- "admin_premium_page_available": "/admin/premium" in [r.get("path") for r in routes]
549
- },
550
- "backgroundfx_status": {
551
- "backgroundfx_routes_loaded": len(backgroundfx_routes),
552
- "backgroundfx_routes": backgroundfx_routes,
553
- "backgroundfx_page_available": "/backgroundfx" in [r.get("path") for r in routes],
554
- "heygen_api_configured": bool(os.getenv("HEYGEN_API_KEY") and os.getenv("HEYGEN_API_KEY") != "your-heygen-api-key"),
555
- "unsplash_api_configured": bool(os.getenv("UNSPLASH_ACCESS_KEY") and os.getenv("UNSPLASH_ACCESS_KEY") != "your-unsplash-key"),
556
- "openai_api_configured": bool(os.getenv("OPENAI_API_KEY") and os.getenv("OPENAI_API_KEY") != "your-openai-api-key")
557
- },
558
- "video_processing_status": {
559
- "video_processing_routes_loaded": len(video_processing_routes),
560
- "video_processing_routes": video_processing_routes,
561
- "api_prefix": "/video-processing",
562
- "endpoints_available": [r.get("path") for r in video_processing_routes]
563
- },
564
- "refactoring_status": {
565
- "modular_routes_loaded": len([r for r in routers_loaded if not "legacy" in r]),
566
- "legacy_routes_loaded": len([r for r in routers_loaded if "legacy" in r]),
567
- "total_errors": len(router_errors)
568
- },
569
- "video_url_refresher_status": {
570
- "available": video_refresher_available,
571
- "running": video_refresher_available,
572
- "refresh_interval": os.getenv('REFRESH_INTERVAL_HOURS', '4') + " hours",
573
- "expiry_threshold": os.getenv('EXPIRY_THRESHOLD_HOURS', '6') + " hours"
574
- }
575
- }
576
-
577
- # TEST ROUTE - Simple route to confirm basic functionality
578
- @app.get("/test")
579
- async def test_route():
580
- """Test route to confirm FastAPI is working"""
581
- return {
582
- "message": "FastAPI is working with Premium Features + BackgroundFX + Video Processing!",
583
- "routers_loaded": routers_loaded,
584
- "timestamp": datetime.now().isoformat(),
585
- "refactoring_complete": True,
586
- "premium_features_enabled": True,
587
- "backgroundfx_enabled": "app.routes.backgroundfx_routes" in str(routers_loaded),
588
- "video_processing_enabled": "app.routes.video_processing_routes" in str(routers_loaded),
589
- "premium_system_enabled": "app.routes.premium_routes" in str(routers_loaded),
590
- "modular_structure": {
591
- "auth_routes": "/login, /register, /logout, /dashboard-direct",
592
- "admin_routes": "/admin/*",
593
- "api_routes": "/api/* (ENABLED)",
594
- "premium_routes": "/api/premium/*, /admin/premium/* (NEW - Complete Premium System)",
595
- "backgroundfx_routes": "/api/backgrounds/* (HeyGen WebM + Transparent Videos)",
596
- "video_processing_routes": "/video-processing/* (Advanced Background Replacement)",
597
- "video_routes": "/voice-recording, /text-to-video (ENABLED)",
598
- "emergency_routes": "/emergency/*"
599
- },
600
- "premium_endpoints": {
601
- "admin_users": "/admin/premium/users",
602
- "admin_set_premium": "/admin/premium/set-user-premium",
603
- "admin_remove_premium": "/admin/premium/remove-user-premium",
604
- "user_status": "/api/premium/status",
605
- "user_start_trial": "/api/premium/start-trial",
606
- "user_upgrade": "/api/premium/upgrade",
607
- "check_feature_access": "/api/premium/check-feature-access/{feature}",
608
- "admin_premium_page": "/admin/premium"
609
- },
610
- "backgroundfx_endpoints": {
611
- "page": "/backgroundfx",
612
- "status": "/api/backgrounds/status",
613
- "get_videos": "/api/videos",
614
- "transparent_video": "/api/backgrounds/get-transparent-video",
615
- "green_screen": "/api/backgrounds/create-green-screen",
616
- "backgrounds": "/api/backgrounds",
617
- "upload_background": "/api/backgrounds/upload",
618
- "search_images": "/api/backgrounds/search-images",
619
- "ai_generate": "/api/backgrounds/generate-ai-image",
620
- "add_from_url": "/api/backgrounds/add-from-url"
621
- },
622
- "video_processing_endpoints": {
623
- "status": "/video-processing/status",
624
- "upload_video": "/video-processing/upload-video",
625
- "upload_background": "/video-processing/upload-background",
626
- "replace_background": "/video-processing/replace-background",
627
- "job_status": "/video-processing/job/{job_id}/status",
628
- "download": "/video-processing/job/{job_id}/download",
629
- "list_jobs": "/video-processing/jobs"
630
- },
631
- "video_url_refresher": {
632
- "status": "running" if video_refresher_available else "not_available",
633
- "interval": os.getenv('REFRESH_INTERVAL_HOURS', '4') + " hours"
634
- }
635
- }
636
-
637
- # =============================================================================
638
- # BACKGROUNDFX INTEGRATION STATUS
639
- # =============================================================================
640
-
641
- @app.get("/admin/backgroundfx-status")
642
- async def backgroundfx_system_status():
643
- """Check BackgroundFX system status for admin"""
644
- try:
645
- # Check environment variables
646
- heygen_configured = heygen_api_configured
647
- unsplash_configured = unsplash_api_configured
648
- openai_configured = openai_api_configured
649
-
650
- # Check if routers are loaded
651
- backgroundfx_router_loaded = "app.routes.backgroundfx_routes" in str(routers_loaded)
652
- video_processing_router_loaded = "app.routes.video_processing_routes" in str(routers_loaded)
653
- premium_router_loaded = "app.routes.premium_routes" in str(routers_loaded)
654
-
655
- # Check static directory
656
- backgrounds_dir = Path("static/backgrounds")
657
- backgrounds_dir_exists = backgrounds_dir.exists()
658
-
659
- return {
660
- "backgroundfx_system_status": "operational" if backgroundfx_router_loaded else "error",
661
- "premium_system_status": "operational" if premium_router_loaded else "error",
662
- "router_loaded": backgroundfx_router_loaded,
663
- "video_processing_router_loaded": video_processing_router_loaded,
664
- "premium_router_loaded": premium_router_loaded,
665
- "api_integrations": {
666
- "heygen_webm_api": heygen_configured,
667
- "unsplash_search": unsplash_configured,
668
- "openai_dalle": openai_configured
669
- },
670
- "features_available": {
671
- "transparent_videos": heygen_configured and premium_router_loaded,
672
- "green_screen_videos": heygen_configured and premium_router_loaded,
673
- "image_search": unsplash_configured and premium_router_loaded,
674
- "ai_image_generation": openai_configured and premium_router_loaded,
675
- "background_library": premium_router_loaded,
676
- "file_upload": backgrounds_dir_exists and premium_router_loaded,
677
- "advanced_video_processing": video_processing_router_loaded and premium_router_loaded,
678
- "premium_user_management": premium_router_loaded,
679
- "trial_system": premium_router_loaded
680
- },
681
- "storage": {
682
- "backgrounds_directory": str(backgrounds_dir),
683
- "directory_exists": backgrounds_dir_exists,
684
- "writable": backgrounds_dir.exists() and os.access(backgrounds_dir, os.W_OK)
685
- },
686
- "database_tables": {
687
- "background_videos": "auto-initialized",
688
- "user_backgrounds": "auto-initialized",
689
- "video_processing_jobs": "created",
690
- "uploaded_videos": "created",
691
- "background_images": "created",
692
- "premium_subscriptions": "created" if premium_router_loaded else "missing",
693
- "premium_features": "created" if premium_router_loaded else "missing"
694
- },
695
- "endpoints": {
696
- "frontend_page": "/backgroundfx",
697
- "admin_premium_page": "/admin/premium",
698
- "api_base": "/api/backgrounds",
699
- "video_processing_base": "/video-processing",
700
- "premium_api_base": "/api/premium",
701
- "status_check": "/api/backgrounds/status",
702
- "video_processing_status": "/video-processing/status",
703
- "premium_status": "/api/premium/status"
704
- },
705
- "timestamp": datetime.now().isoformat()
706
- }
707
- except Exception as e:
708
- return {
709
- "backgroundfx_system_status": "error",
710
- "premium_system_status": "error",
711
- "error": str(e),
712
- "timestamp": datetime.now().isoformat()
713
- }
714
-
715
- # =============================================================================
716
- # MANUAL VIDEO REFRESH ENDPOINTS
717
- # =============================================================================
718
-
719
- @app.get("/admin/refresh-videos")
720
- async def manual_refresh_videos():
721
- """Manually trigger video URL refresh"""
722
- try:
723
- if not video_refresher_available:
724
- return {"error": "Video URL refresher not available"}
725
-
726
- logger.info("Manual video refresh triggered")
727
- refresher = VideoURLRefresher()
728
- refresher.run_refresh_cycle()
729
-
730
- return {
731
- "status": "success",
732
- "message": "Video URL refresh completed",
733
- "timestamp": datetime.now().isoformat()
734
- }
735
- except Exception as e:
736
- logger.error(f"Manual refresh failed: {e}")
737
- return {
738
- "status": "error",
739
- "message": str(e),
740
- "timestamp": datetime.now().isoformat()
741
- }
742
-
743
- @app.get("/admin/refresh-status")
744
- async def refresh_status():
745
- """Check video URL refresh service status"""
746
- return {
747
- "video_url_refresher": {
748
- "available": video_refresher_available,
749
- "running": video_refresher_available,
750
- "refresh_interval": os.getenv('REFRESH_INTERVAL_HOURS', '4') + " hours",
751
- "expiry_threshold": os.getenv('EXPIRY_THRESHOLD_HOURS', '6') + " hours"
752
- },
753
- "environment": {
754
- "REFRESH_INTERVAL_HOURS": os.getenv('REFRESH_INTERVAL_HOURS', '4'),
755
- "EXPIRY_THRESHOLD_HOURS": os.getenv('EXPIRY_THRESHOLD_HOURS', '6'),
756
- "REFRESHER_MODE": os.getenv('REFRESHER_MODE', 'continuous')
757
- },
758
- "timestamp": datetime.now().isoformat()
759
- }
760
-
761
- # =============================================================================
762
- # HEYGEN AVATAR DEBUG ENDPOINTS
763
- # =============================================================================
764
-
765
- @app.get("/debug-avatars")
766
- async def debug_avatars():
767
- """
768
- Temporary debug endpoint to inspect HeyGen API response structure.
769
- Access this at: https://app.myavatar.dk/debug-avatars
770
- """
771
- try:
772
- log_info("Debug avatars endpoint accessed", "Debug")
773
-
774
- api_key = os.getenv("HEYGEN_API_KEY")
775
- if not api_key:
776
- logger.error("HEYGEN_API_KEY not found in environment")
777
- raise HTTPException(status_code=500, detail="HEYGEN_API_KEY not configured")
778
-
779
- logger.info("Fetching avatars from HeyGen API...")
780
- result = get_available_avatars(api_key)
781
-
782
- if not result:
783
- raise HTTPException(status_code=500, detail="No response from HeyGen API")
784
-
785
- # Analyze the response structure for better debugging
786
- if isinstance(result, dict) and 'data' in result:
787
- avatars = result['data']
788
- logger.info(f"Successfully fetched {len(avatars)} avatars from HeyGen")
789
-
790
- # Create analysis for easier debugging
791
- analysis = {
792
- "success": True,
793
- "total_avatars": len(avatars),
794
- "api_response_keys": list(result.keys()),
795
- "sample_avatar_keys": list(avatars[0].keys()) if avatars else [],
796
- "sample_avatars": avatars[:3] if len(avatars) > 3 else avatars,
797
- "timestamp": datetime.now().isoformat()
798
- }
799
-
800
- # Extract all unique fields across all avatars
801
- all_fields = set()
802
- field_samples = {}
803
-
804
- for avatar in avatars:
805
- for key, value in avatar.items():
806
- all_fields.add(key)
807
- if key not in field_samples and value:
808
- field_samples[key] = str(value)[:100] # First 100 chars as sample
809
-
810
- analysis["all_available_fields"] = sorted(list(all_fields))
811
- analysis["field_samples"] = field_samples
812
-
813
- # Look for potential naming fields
814
- naming_fields = []
815
- for field in all_fields:
816
- field_lower = field.lower()
817
- if any(keyword in field_lower for keyword in ['name', 'title', 'display', 'label', 'desc']):
818
- naming_fields.append(field)
819
-
820
- analysis["potential_naming_fields"] = naming_fields
821
-
822
- log_info(f"Avatar debug analysis completed: {len(avatars)} avatars, {len(all_fields)} unique fields", "Debug")
823
- return analysis
824
- else:
825
- logger.warning(f"Unexpected HeyGen API response structure: {type(result)}")
826
- return {
827
- "success": False,
828
- "raw_response": result,
829
- "message": "Unexpected response structure from HeyGen API"
830
- }
831
-
832
- except HTTPException:
833
- raise # Re-raise HTTP exceptions
834
- except Exception as e:
835
- log_error(f"Error in debug-avatars endpoint: {str(e)}", "Debug", e)
836
- logger.error(f"Debug avatars error traceback: {traceback.format_exc()}")
837
- raise HTTPException(status_code=500, detail=f"Debug endpoint error: {str(e)}")
838
-
839
- @app.get("/debug-env")
840
- async def debug_environment():
841
- """
842
- Debug endpoint to check environment variables (safely).
843
- Shows which keys exist without exposing values.
844
- """
845
- try:
846
- env_status = {
847
- "HEYGEN_API_KEY": "✓ Set" if os.getenv("HEYGEN_API_KEY") else "✗ Missing",
848
- "DATABASE_URL": "✓ Set" if os.getenv("DATABASE_URL") else "✗ Missing",
849
- "CLOUDINARY_CLOUD_NAME": "✓ Set" if os.getenv("CLOUDINARY_CLOUD_NAME") else "✗ Missing",
850
- "CLOUDINARY_API_KEY": "✓ Set" if os.getenv("CLOUDINARY_API_KEY") else "✗ Missing",
851
- "CLOUDINARY_API_SECRET": "✓ Set" if os.getenv("CLOUDINARY_API_SECRET") else "✗ Missing",
852
- "SECRET_KEY": "✓ Set" if os.getenv("SECRET_KEY") else "✗ Missing",
853
- "UNSPLASH_ACCESS_KEY": "✓ Set" if os.getenv("UNSPLASH_ACCESS_KEY") else "✗ Missing",
854
- "OPENAI_API_KEY": "✓ Set" if os.getenv("OPENAI_API_KEY") else "✗ Missing",
855
- "RAILWAY_ENVIRONMENT": os.getenv("RAILWAY_ENVIRONMENT", "not_railway"),
856
- "PORT": os.getenv("PORT", "not_set"),
857
- "REFRESH_INTERVAL_HOURS": os.getenv("REFRESH_INTERVAL_HOURS", "4"),
858
- "EXPIRY_THRESHOLD_HOURS": os.getenv("EXPIRY_THRESHOLD_HOURS", "6"),
859
- "REFRESHER_MODE": os.getenv("REFRESHER_MODE", "continuous"),
860
- "timestamp": datetime.now().isoformat()
861
- }
862
-
863
- # Check if we can import modules
864
- try:
865
- from app.api.heygen import get_available_avatars
866
- env_status["heygen_module"] = "✓ Available"
867
- except ImportError as e:
868
- env_status["heygen_module"] = f"✗ Import Error: {str(e)}"
869
-
870
- # Check video refresher status
871
- env_status["video_refresher_module"] = "✓ Available" if video_refresher_available else "✗ Not Available"
872
-
873
- # Check premium system
874
- try:
875
- from app.routes.premium_routes import check_premium_access
876
- env_status["premium_system"] = "✓ Available"
877
- except ImportError as e:
878
- env_status["premium_system"] = f"✗ Import Error: {str(e)}"
879
-
880
- # Check BackgroundFX system
881
- try:
882
- from app.routes.backgroundfx_routes import router
883
- env_status["backgroundfx_system"] = "✓ Available"
884
- except ImportError as e:
885
- env_status["backgroundfx_system"] = f"✗ Import Error: {str(e)}"
886
-
887
- # Check Video Processing system
888
- try:
889
- from app.routes.video_processing_routes import router
890
- env_status["video_processing_system"] = "✓ Available"
891
- except ImportError as e:
892
- env_status["video_processing_system"] = f"✗ Import Error: {str(e)}"
893
-
894
- return env_status
895
-
896
- except Exception as e:
897
- return {"error": str(e), "timestamp": datetime.now().isoformat()}
898
-
899
- # Startup event with safe initialization
900
- @app.on_event("startup")
901
- async def startup_event():
902
- """Safe startup with comprehensive error handling"""
903
-
904
- # Initialize the notification system safely
905
- try:
906
- notify_service_status("MyAvatar", "up", "Application started successfully")
907
- logger.info("Notification system initialized successfully")
908
- except Exception as e:
909
- logger.warning(f"Notification system unavailable: {str(e)}")
910
-
911
- # Log compatibility status safely
912
- try:
913
- status = log_compatibility_status()
914
- log_info(f"Starting MyAvatar application (Safe Mode: {status.get('safe_mode', 'unknown')})", "Server")
915
- except Exception as e:
916
- logger.warning(f"Could not determine compatibility status: {e}")
917
- log_info("Starting MyAvatar application", "Server")
918
-
919
- # Database initialization with error handling
920
- try:
921
- init_database()
922
- update_database_schema()
923
- create_admin_user()
924
- logger.info("Database initialization completed")
925
- except Exception as e:
926
- log_error(f"Database initialization failed: {str(e)}", "Server", e)
927
- logger.warning("Application may have limited functionality due to database issues")
928
-
929
- # GDPR schema initialization
930
- try:
931
- from app.database.gdpr_schema import initialize_gdpr_schema
932
- initialize_gdpr_schema()
933
- logger.info("GDPR schema initialized")
934
- except Exception as gdpr_error:
935
- logger.warning(f"GDPR schema initialization failed: {str(gdpr_error)}")
936
-
937
- # Background replacement initialization
938
- if ENABLE_BACKGROUND_REPLACEMENT:
939
- try:
940
- logger.info("Background replacement functionality enabled via BackgroundFX microservice")
941
- except Exception as e:
942
- logger.warning(f"Background replacement initialization warning: {e}")
943
-
944
- # Premium system initialization
945
- try:
946
- if "app.routes.premium_routes" in str(routers_loaded):
947
- logger.info("✅ Premium system loaded successfully")
948
- logger.info("🎯 Premium user management ready")
949
- logger.info("🎁 14-day trial system ready")
950
- logger.info("⭐ Premium feature access control ready")
951
- else:
952
- logger.warning("⚠️ Premium system not loaded")
953
- except Exception as e:
954
- logger.warning(f"Premium system initialization warning: {e}")
955
-
956
- # BackgroundFX system initialization
957
- try:
958
- if "app.routes.backgroundfx_routes" in str(routers_loaded):
959
- logger.info("✅ BackgroundFX system loaded successfully")
960
- logger.info("🎬 HeyGen WebM API integration ready")
961
- logger.info("🖼️ Unsplash image search ready")
962
- logger.info("🤖 OpenAI DALL-E integration ready")
963
- else:
964
- logger.warning("⚠️ BackgroundFX system not loaded")
965
- except Exception as e:
966
- logger.warning(f"BackgroundFX system check warning: {e}")
967
-
968
- # Video Processing system initialization
969
- try:
970
- if "app.routes.video_processing_routes" in str(routers_loaded):
971
- logger.info("✅ Video Processing system loaded successfully")
972
- logger.info("🎥 Advanced background replacement API ready")
973
- logger.info("📊 Real-time job tracking ready")
974
- logger.info("🔄 File upload and processing ready")
975
- else:
976
- logger.warning("⚠️ Video Processing system not loaded")
977
- except Exception as e:
978
- logger.warning(f"Video Processing system check warning: {e}")
979
-
980
- # Report successful startup
981
- edition_name = "Premium Edition with BackgroundFX + Video Processing + Complete Premium System"
982
- log_info(f"MyAvatar {edition_name} is running with MODULAR ROUTE STRUCTURE", "Server")
983
- logger.info(f"✅ Successfully loaded {len(routers_loaded)} route modules: {', '.join(routers_loaded)}")
984
-
985
- if router_errors:
986
- logger.error(f"❌ Failed to load {len(router_errors)} route modules")
987
- for error in router_errors:
988
- logger.error(f" - {error['module']}: {error['error']}")
989
-
990
- # Log system status
991
- modular_count = len([r for r in routers_loaded if not "legacy" in r])
992
- legacy_count = len([r for r in routers_loaded if "legacy" in r])
993
- logger.info(f"🏗️ REFACTORING COMPLETE: {modular_count} modular routes, {legacy_count} legacy routes")
994
-
995
- # Mount the Gradio app for the background removal tool
996
- # This is done at the end of startup to ensure all other initializations are complete
997
- try:
998
- logger.info("🔧 Mounting Gradio app for background removal tool...")
999
- gr.mount_gradio_app(app, huggingface_gradio_app, path="/tools/background_remover")
1000
- logger.info("✅ Gradio app mounted successfully at /tools/background_remover")
1001
- except Exception as e:
1002
- logger.error(f"❌ Failed to mount Gradio app: {e}")
1003
- logger.error(f"Full traceback: {traceback.format_exc()}")
1004
-
1005
- # Check feature status
1006
- premium_loaded = any("premium_routes" in r for r in routers_loaded)
1007
- backgroundfx_loaded = any("backgroundfx_routes" in r for r in routers_loaded)
1008
- video_processing_loaded = any("video_processing_routes" in r for r in routers_loaded)
1009
-
1010
- if premium_loaded:
1011
- logger.info("🎯 PREMIUM SYSTEM ACTIVE: Complete user management and access control")
1012
- else:
1013
- logger.warning("⚠️ Premium system not loaded - BackgroundFX and Video Processing will fail")
1014
-
1015
- if backgroundfx_loaded:
1016
- logger.info("🎬 BACKGROUNDFX FEATURES ACTIVE: HeyGen WebM + Transparent Videos ready")
1017
- else:
1018
- logger.warning("⚠️ BackgroundFX features not loaded")
1019
-
1020
- if video_processing_loaded:
1021
- logger.info("🎥 VIDEO PROCESSING FEATURES ACTIVE: Advanced Background Replacement ready")
1022
- else:
1023
- logger.warning("⚠️ Video Processing features not loaded")
1024
-
1025
- # Log video refresher status
1026
- if video_refresher_available:
1027
- logger.info("🔄 Video URL refresher background service is running")
1028
- else:
1029
- logger.warning("⚠️ Video URL refresher background service is not available")
1030
-
1031
- # Final system status
1032
- if premium_loaded and backgroundfx_loaded and video_processing_loaded:
1033
- logger.info("🎉 ALL SYSTEMS OPERATIONAL: Premium + BackgroundFX + Video Processing")
1034
- else:
1035
- logger.warning("⚠️ Some systems not operational - check router loading errors")
1036
-
1037
- # Entry point
1038
- # Main application entry point
1039
- if __name__ == "__main__":
1040
- try:
1041
- logger.info("Starting application server...")
1042
- # Note: Database initialization is now handled by the startup event.
1043
- # Start the FastAPI application using uvicorn
1044
- uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)), reload=True)
1045
-
1046
- except ImportError as e:
1047
- print(f"FATAL: Missing essential dependency: {e}")
1048
- print("Please run 'pip install -r requirements.txt' to install required packages.")
1049
  import traceback
1050
  traceback.print_exc()
1051
- except Exception as e:
1052
- print(f"FATAL: Application failed to start: {e}")
1053
- import traceback
1054
- traceback.print_exc()
 
 
1
+ #!/usr/bin/env python3
2
  """
3
+ BackgroundFX Pro Main Application Entry Point
4
+ Refactored modular architecture – orchestrates specialised components
 
5
  """
6
+ from __future__ import annotations
7
+ # ── Early env/threading hygiene (safe default to silence libgomp) ────────────
 
 
 
 
 
 
 
 
8
  import os
9
+ os.environ["OMP_NUM_THREADS"] = "2" # Force valid value early
10
+ # If you use early_env in your project, keep this import (harmless if absent)
11
+ try:
12
+ import early_env # sets OMP/MKL/OPENBLAS + torch threads safely
13
+ except Exception:
14
+ pass
15
  import logging
 
 
16
  import threading
17
+ import traceback
 
 
18
  import sys
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Optional, Tuple, Dict, Any, Callable
22
+ # Mitigate CUDA fragmentation (must be set before importing torch)
23
+ if "PYTORCH_CUDA_ALLOC_CONF" not in os.environ:
24
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True,max_split_size_mb:128"
25
+ # ── Logging ──────────────────────────────────────────────────────────────────
 
26
  logging.basicConfig(
27
+ level=logging.INFO,
28
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  )
30
+ logger = logging.getLogger("core.app")
31
+ # ── Ensure project root importable ───────────────────────────────────────────
32
+ PROJECT_FILE = Path(__file__).resolve()
33
+ CORE_DIR = PROJECT_FILE.parent
34
+ ROOT = CORE_DIR.parent
35
+ if str(ROOT) not in sys.path:
36
+ sys.path.insert(0, str(ROOT))
37
+ # Create loader directories if they don't exist
38
+ loaders_dir = ROOT / "models" / "loaders"
39
+ loaders_dir.mkdir(parents=True, exist_ok=True)
40
+ # ── Gradio schema patch (HF quirk) ───────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  try:
42
  import gradio_client.utils as gc_utils
43
+ _orig_get_type = gc_utils.get_type
44
+ def _patched_get_type(schema):
 
 
45
  if not isinstance(schema, dict):
46
+ if isinstance(schema, bool): return "boolean"
47
+ if isinstance(schema, str): return "string"
48
+ if isinstance(schema, (int, float)): return "number"
 
49
  return "string"
50
+ return _orig_get_type(schema)
51
+ gc_utils.get_type = _patched_get_type
52
+ logger.info("Gradio schema patch applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  except Exception as e:
54
+ logger.warning(f"Gradio patch failed: {e}")
55
+ # ── Core config + components ─────────────────────────────────────────────────
56
+ try:
57
+ from config.app_config import get_config
58
+ except ImportError:
59
+ # Dummy if missing
60
+ class DummyConfig:
61
+ def to_dict(self):
62
+ return {}
63
+ get_config = lambda: DummyConfig()
64
+ from utils.hardware.device_manager import DeviceManager
65
+ from utils.system.memory_manager import MemoryManager
66
+ # Try to import the new split loaders first, fall back to old if needed
67
+ try:
68
+ from models.loaders.model_loader import ModelLoader
69
+ logger.info("Using split loader architecture")
70
+ except ImportError:
71
+ logger.warning("Split loaders not found, using legacy loader")
72
+ # Fall back to old loader if split architecture isn't available yet
73
+ from models.model_loader import ModelLoader # type: ignore
74
+ from processing.video.video_processor import CoreVideoProcessor
75
+ from processing.audio.audio_processor import AudioProcessor
76
+ from utils.monitoring.progress_tracker import ProgressTracker
77
+ from utils.cv_processing import validate_video_file
78
+ # ── Optional Two-Stage import ────────────────────────────────────────────────
79
+ TWO_STAGE_AVAILABLE = False
80
+ TWO_STAGE_IMPORT_ORIGIN = ""
81
+ TWO_STAGE_IMPORT_ERROR = ""
82
+ CHROMA_PRESETS: Dict[str, Dict[str, Any]] = {"standard": {}}
83
+ TwoStageProcessor = None # type: ignore
84
+ # Try multiple import paths for two-stage processor
85
+ two_stage_paths = [
86
+ "processors.two_stage", # Your fixed version
87
+ "processing.two_stage.two_stage_processor",
88
+ "processing.two_stage",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  ]
90
+ for import_path in two_stage_paths:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  try:
92
+ exec(f"from {import_path} import TwoStageProcessor, CHROMA_PRESETS")
93
+ TWO_STAGE_AVAILABLE = True
94
+ TWO_STAGE_IMPORT_ORIGIN = import_path
95
+ logger.info(f"Two-stage import OK ({import_path})")
96
+ break
97
+ except Exception as e:
98
+ TWO_STAGE_IMPORT_ERROR = str(e)
99
+ continue
100
+ if not TWO_STAGE_AVAILABLE:
101
+ logger.warning(f"Two-stage import FAILED from all paths: {TWO_STAGE_IMPORT_ERROR}")
102
+ # ── Quiet startup self-check (async by default) ──────────────────────────────
103
+ # Place the helper in tools/startup_selfcheck.py (with tools/__init__.py present)
104
+ try:
105
+ from tools.startup_selfcheck import schedule_startup_selfcheck
106
+ except Exception:
107
+ schedule_startup_selfcheck = None # graceful if the helper isn't shipped
108
+ # Dummy exceptions if core.exceptions not available
109
+ class ModelLoadingError(Exception):
110
+ pass
111
+ class VideoProcessingError(Exception):
112
+ pass
113
+ # ╔══════════════════════════════════════════════════════════════════════════╗
114
+ # ║ VideoProcessor class ║
115
+ # ╚══════════════════════════════════════════════════════════════════════════╝
116
+ class VideoProcessor:
117
+ """
118
+ Main orchestrator – coordinates all specialised components.
119
+ """
120
+ def __init__(self):
121
+ self.config = get_config()
122
+ self._patch_config_defaults(self.config) # avoid AttributeError on older configs
123
+ self.device_manager = DeviceManager()
124
+ self.memory_manager = MemoryManager(self.device_manager.get_optimal_device())
125
+ self.model_loader = ModelLoader(self.device_manager, self.memory_manager)
126
+ self.audio_processor = AudioProcessor()
127
+ self.core_processor: Optional[CoreVideoProcessor] = None
128
+ self.two_stage_processor: Optional[Any] = None
129
+ self.models_loaded = False
130
+ self.loading_lock = threading.Lock()
131
+ self.cancel_event = threading.Event()
132
+ self.progress_tracker: Optional[ProgressTracker] = None
133
+ logger.info(f"VideoProcessor on device: {self.device_manager.get_optimal_device()}")
134
+ # ── Config hardening: add missing fields safely ───────────────────────────
135
+ @staticmethod
136
+ def _patch_config_defaults(cfg: Any) -> None:
137
+ defaults = {
138
+ # video / i/o
139
+ "use_nvenc": False,
140
+ "prefer_mp4": True,
141
+ "video_codec": "mp4v",
142
+ "audio_copy": True,
143
+ "ffmpeg_path": "ffmpeg",
144
+ # model/resource guards
145
+ "max_model_size": 0,
146
+ "max_model_size_bytes": 0,
147
+ # housekeeping
148
+ "output_dir": str((Path(__file__).resolve().parent.parent) / "outputs"),
149
+ # MatAnyone settings to ensure it's enabled
150
+ "matanyone_enabled": True,
151
+ "use_matanyone": True,
152
+ }
153
+ for k, v in defaults.items():
154
+ if not hasattr(cfg, k):
155
+ setattr(cfg, k, v)
156
+ Path(cfg.output_dir).mkdir(parents=True, exist_ok=True)
157
+ # ── Progress helper ───────────────────────────────────────────────────────
158
+ def _init_progress(self, video_path: str, cb: Optional[Callable] = None):
159
+ try:
160
+ import cv2
161
+ cap = cv2.VideoCapture(video_path)
162
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
163
+ cap.release()
164
+ if total <= 0:
165
+ total = 100
166
+ self.progress_tracker = ProgressTracker(total, cb)
167
+ except Exception as e:
168
+ logger.warning(f"Progress init failed: {e}")
169
+ self.progress_tracker = ProgressTracker(100, cb)
170
+ # ── Model loading ─────────────────────────────────────────────────────────
171
+ def load_models(self, progress_callback: Optional[Callable] = None) -> str:
172
+ with self.loading_lock:
173
+ if self.models_loaded:
174
+ return "Models already loaded and validated"
175
+ try:
176
+ self.cancel_event.clear()
177
+ if progress_callback:
178
+ progress_callback(0.0, f"Loading on {self.device_manager.get_optimal_device()}")
179
+ sam2_loaded, mat_loaded = self.model_loader.load_all_models(
180
+ progress_callback=progress_callback, cancel_event=self.cancel_event
181
+ )
182
+ if self.cancel_event.is_set():
183
+ return "Model loading cancelled"
184
+ # Get the actual models
185
+ sam2_predictor = sam2_loaded.model if sam2_loaded else None
186
+ mat_model = mat_loaded.model if mat_loaded else None # NOTE: in our MatAnyone loader this is a stateful adapter (callable)
187
+ # Initialize core processor
188
+ self.core_processor = CoreVideoProcessor(config=self.config, models=self.model_loader)
189
+ # Initialize two-stage processor if available
190
+ self.two_stage_processor = None
191
+ if TWO_STAGE_AVAILABLE and TwoStageProcessor and (sam2_predictor or mat_model):
192
+ try:
193
+ self.two_stage_processor = TwoStageProcessor(
194
+ sam2_predictor=sam2_predictor, matanyone_model=mat_model
195
+ )
196
+ logger.info("Two-stage processor initialised")
197
+ except Exception as e:
198
+ logger.warning(f"Two-stage init failed: {e}")
199
+ self.two_stage_processor = None
200
+ self.models_loaded = True
201
+ msg = self.model_loader.get_load_summary()
202
+
203
+ # Add status about processors
204
+ if self.two_stage_processor:
205
+ msg += "\n✅ Two-stage processor ready"
206
+ else:
207
+ msg += "\n⚠️ Two-stage processor not available"
208
+
209
+ if mat_model:
210
+ msg += "\n✅ MatAnyone refinement active"
211
+ else:
212
+ msg += "\n⚠️ MatAnyone not loaded (edges may be rough)"
213
+
214
+ logger.info(msg)
215
+ return msg
216
+ except (AttributeError, ModelLoadingError) as e:
217
+ self.models_loaded = False
218
+ err = f"Model loading failed: {e}"
219
+ logger.error(err)
220
+ return err
221
+ except Exception as e:
222
+ self.models_loaded = False
223
+ err = f"Unexpected error during model loading: {e}"
224
+ logger.error(f"{err}\n{traceback.format_exc()}")
225
+ return err
226
+ # ── Public entry – process video ─────────────────────────────────────────
227
+ def process_video(
228
+ self,
229
+ video_path: str,
230
+ background_choice: str,
231
+ custom_background_path: Optional[str] = None,
232
+ progress_callback: Optional[Callable] = None,
233
+ use_two_stage: bool = False,
234
+ chroma_preset: str = "standard",
235
+ key_color_mode: str = "auto",
236
+ preview_mask: bool = False,
237
+ preview_greenscreen: bool = False,
238
+ ) -> Tuple[Optional[str], Optional[str], str]:
239
+
240
+ # ===== BACKGROUND PATH DEBUG & FIX =====
241
+ logger.info("=" * 60)
242
+ logger.info("BACKGROUND PATH DEBUGGING")
243
+ logger.info(f"background_choice: {background_choice}")
244
+ logger.info(f"custom_background_path type: {type(custom_background_path)}")
245
+ logger.info(f"custom_background_path value: {custom_background_path}")
246
+
247
+ # Fix 1: Handle if Gradio sends a dict
248
+ if isinstance(custom_background_path, dict):
249
+ original = custom_background_path
250
+ custom_background_path = custom_background_path.get('name') or custom_background_path.get('path')
251
+ logger.info(f"Extracted path from dict: {original} -> {custom_background_path}")
252
+
253
+ # Fix 2: Handle PIL Image objects
254
+ try:
255
+ from PIL import Image
256
+ if isinstance(custom_background_path, Image.Image):
257
+ import tempfile
258
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
259
+ custom_background_path.save(tmp.name)
260
+ custom_background_path = tmp.name
261
+ logger.info(f"Saved PIL Image to: {custom_background_path}")
262
+ except ImportError:
263
+ pass
264
+
265
+ # Fix 3: Verify file exists when using custom background
266
+ if background_choice == "custom" or custom_background_path:
267
+ if custom_background_path:
268
+ if Path(custom_background_path).exists():
269
+ logger.info(f"✅ Background file exists: {custom_background_path}")
270
+ else:
271
+ logger.warning(f"⚠️ Background file does not exist: {custom_background_path}")
272
+ # Try to find it in Gradio temp directories
273
+ import glob
274
+ patterns = [
275
+ "/tmp/gradio*/**/*.jpg",
276
+ "/tmp/gradio*/**/*.jpeg",
277
+ "/tmp/gradio*/**/*.png",
278
+ "/tmp/**/*.jpg",
279
+ "/tmp/**/*.jpeg",
280
+ "/tmp/**/*.png",
281
+ ]
282
+ for pattern in patterns:
283
+ files = glob.glob(pattern, recursive=True)
284
+ if files:
285
+ # Get the most recent file
286
+ newest = max(files, key=os.path.getmtime)
287
+ logger.info(f"Found potential background: {newest}")
288
+ # Only use it if it was created in the last 5 minutes
289
+ if (time.time() - os.path.getmtime(newest)) < 300:
290
+ custom_background_path = newest
291
+ logger.info(f"✅ Using recent temp file: {custom_background_path}")
292
+ break
293
+ else:
294
+ logger.error("❌ Custom background mode but path is None!")
295
 
296
+ logger.info(f"Final custom_background_path: {custom_background_path}")
297
+ logger.info("=" * 60)
 
 
 
298
 
299
+ if not self.models_loaded or not self.core_processor:
300
+ return None, None, "Models not loaded. Please click 'Load Models' first."
301
+ if self.cancel_event.is_set():
302
+ return None, None, "Processing cancelled"
303
+ self._init_progress(video_path, progress_callback)
304
+ ok, why = validate_video_file(video_path)
305
+ if not ok:
306
+ return None, None, f"Invalid video: {why}"
307
+ try:
308
+ # Log which mode we're using
309
+ mode = "two-stage" if use_two_stage else "single-stage"
310
+ matanyone_status = "enabled" if self.model_loader.get_matanyone() else "disabled"
311
+ logger.info(f"Processing video in {mode} mode, MatAnyone: {matanyone_status}")
312
+ # IMPORTANT: start each video with a clean MatAnyone memory
313
+ self._reset_matanyone_session()
314
+ if use_two_stage:
315
+ if not TWO_STAGE_AVAILABLE or self.two_stage_processor is None:
316
+ return None, None, "Two-stage processing not available"
317
+ final, green, msg = self._process_two_stage(
318
+ video_path,
319
+ background_choice,
320
+ custom_background_path,
321
+ progress_callback,
322
+ chroma_preset,
323
+ key_color_mode,
324
+ )
325
+ return final, green, msg
326
+ else:
327
+ final, green, msg = self._process_single_stage(
328
+ video_path,
329
+ background_choice,
330
+ custom_background_path,
331
+ progress_callback,
332
+ preview_mask,
333
+ preview_greenscreen,
334
+ )
335
+ return final, green, msg
336
+ except VideoProcessingError as e:
337
+ logger.error(f"Processing failed: {e}")
338
+ return None, None, f"Processing failed: {e}"
339
+ except Exception as e:
340
+ logger.error(f"Unexpected processing error: {e}\n{traceback.format_exc()}")
341
+ return None, None, f"Unexpected error: {e}"
342
+ # ── Private – per-video MatAnyone reset ──────────────────────────────────
343
+ def _reset_matanyone_session(self):
344
+ """
345
+ Ensure a fresh MatAnyone memory per video. The MatAnyone loader we use returns a
346
+ callable *stateful adapter*. If present, reset() clears its InferenceCore memory.
347
+ """
348
+ try:
349
+ mat = self.model_loader.get_matanyone()
350
+ except Exception:
351
+ mat = None
352
+ if mat is not None and hasattr(mat, "reset") and callable(mat.reset):
353
+ try:
354
+ mat.reset()
355
+ logger.info("MatAnyone session reset for new video")
356
+ except Exception as e:
357
+ logger.warning(f"MatAnyone session reset failed (continuing): {e}")
358
+ # ── Private – single-stage ───────────────────────────────────────────────
359
+ def _process_single_stage(
360
+ self,
361
+ video_path: str,
362
+ background_choice: str,
363
+ custom_background_path: Optional[str],
364
+ progress_callback: Optional[Callable],
365
+ preview_mask: bool,
366
+ preview_greenscreen: bool,
367
+ ) -> Tuple[Optional[str], Optional[str], str]:
368
+
369
+ # Additional debug logging for single-stage
370
+ logger.info(f"[Single-stage] background_choice: {background_choice}")
371
+ logger.info(f"[Single-stage] custom_background_path: {custom_background_path}")
372
+
373
+ ts = int(time.time())
374
+ out_dir = Path(self.config.output_dir) / "single_stage"
375
+ out_dir.mkdir(parents=True, exist_ok=True)
376
+ out_path = str(out_dir / f"processed_{ts}.mp4")
377
+ # Process video via your CoreVideoProcessor
378
+ result = self.core_processor.process_video(
379
+ input_path=video_path,
380
+ output_path=out_path,
381
+ bg_config={
382
+ "background_choice": background_choice,
383
+ "custom_path": custom_background_path,
384
+ },
385
+ progress_callback=progress_callback,
386
+ )
387
+
388
+ if not result:
389
+ return None, None, "Video processing failed"
390
+ # Mux audio unless preview-only
391
+ if not (preview_mask or preview_greenscreen):
392
+ try:
393
+ final_path = self.audio_processor.add_audio_to_video(
394
+ original_video=video_path, processed_video=out_path
395
+ )
396
+ except Exception as e:
397
+ logger.warning(f"Audio mux failed, returning video without audio: {e}")
398
+ final_path = out_path
399
  else:
400
+ final_path = out_path
401
+ # Build status message
402
+ try:
403
+ mat_loaded = bool(self.model_loader.get_matanyone())
404
+ except Exception:
405
+ mat_loaded = False
406
+ matanyone_status = "✓" if mat_loaded else "✗"
407
+ msg = (
408
+ "Processing completed.\n"
409
+ f"Frames: {result.get('frames', 'unknown')}\n"
410
+ f"Background: {background_choice}\n"
411
+ f"Mode: Single-stage\n"
412
+ f"MatAnyone: {matanyone_status}\n"
413
+ f"Device: {self.device_manager.get_optimal_device()}"
414
+ )
415
+ return final_path, None, msg # No green in single-stage
416
+ # ── Private – two-stage ─────────────────────────────────────────────────
417
+ def _process_two_stage(
418
+ self,
419
+ video_path: str,
420
+ background_choice: str,
421
+ custom_background_path: Optional[str],
422
+ progress_callback: Optional[Callable],
423
+ chroma_preset: str,
424
+ key_color_mode: str,
425
+ ) -> Tuple[Optional[str], Optional[str], str]:
426
+ if self.two_stage_processor is None:
427
+ return None, None, "Two-stage processor not available"
428
+
429
+ # Additional debug logging for two-stage
430
+ logger.info(f"[Two-stage] background_choice: {background_choice}")
431
+ logger.info(f"[Two-stage] custom_background_path: {custom_background_path}")
432
+
433
+ import cv2
434
+ cap = cv2.VideoCapture(video_path)
435
+ if not cap.isOpened():
436
+ return None, None, "Could not open input video"
437
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) or 1280
438
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) or 720
439
+ cap.release()
440
+ # Prepare background
441
+ try:
442
+ background = self.core_processor.prepare_background(
443
+ background_choice, custom_background_path, w, h
444
+ )
445
+ except Exception as e:
446
+ logger.error(f"Background preparation failed: {e}")
447
+ return None, None, f"Failed to prepare background: {e}"
448
+ if background is None:
449
+ return None, None, "Failed to prepare background"
450
+ ts = int(time.time())
451
+ out_dir = Path(self.config.output_dir) / "two_stage"
452
+ out_dir.mkdir(parents=True, exist_ok=True)
453
+ final_out = str(out_dir / f"final_{ts}.mp4")
454
+ chroma_cfg = CHROMA_PRESETS.get(chroma_preset, CHROMA_PRESETS.get("standard", {}))
455
+ logger.info(f"Two-stage with preset: {chroma_preset} | key_color: {key_color_mode}")
456
+ # (Per-video reset already called in process_video)
457
+ final_path, green_path, stage2_msg = self.two_stage_processor.process_full_pipeline(
458
+ video_path,
459
+ background,
460
+ final_out,
461
+ key_color_mode=key_color_mode,
462
+ chroma_settings=chroma_cfg,
463
+ progress_callback=progress_callback,
464
+ )
465
+ if final_path is None:
466
+ return None, None, stage2_msg
467
+ # Mux audio
468
+ try:
469
+ final_with_audio = self.audio_processor.add_audio_to_video(
470
+ original_video=video_path, processed_video=final_path
471
+ )
472
+ except Exception as e:
473
+ logger.warning(f"Audio mux failed: {e}")
474
+ final_with_audio = final_path
475
+ try:
476
+ mat_loaded = bool(self.model_loader.get_matanyone())
477
+ except Exception:
478
+ mat_loaded = False
479
+ matanyone_status = "✓" if mat_loaded else "✗"
480
+ msg = (
481
+ "Two-stage processing completed.\n"
482
+ f"Background: {background_choice}\n"
483
+ f"Chroma Preset: {chroma_preset}\n"
484
+ f"MatAnyone: {matanyone_status}\n"
485
+ f"Device: {self.device_manager.get_optimal_device()}"
486
+ )
487
+ return final_with_audio, green_path, msg
488
+ # ── Status helpers ───────────────────────────────────────────────────────
489
+ def get_status(self) -> Dict[str, Any]:
490
+ status = {
491
+ "models_loaded": self.models_loaded,
492
+ "two_stage_available": bool(TWO_STAGE_AVAILABLE and self.two_stage_processor),
493
+ "two_stage_origin": TWO_STAGE_IMPORT_ORIGIN or "",
494
+ "device": str(self.device_manager.get_optimal_device()),
495
+ "core_processor_loaded": self.core_processor is not None,
496
+ "config": self._safe_config_dict(),
497
+ "memory_usage": self._safe_memory_usage(),
498
+ }
499
+
500
+ try:
501
+ status["sam2_loaded"] = self.model_loader.get_sam2() is not None
502
+ status["matanyone_loaded"] = self.model_loader.get_matanyone() is not None
503
+ status["model_info"] = self.model_loader.get_model_info()
504
+ except Exception:
505
+ status["sam2_loaded"] = False
506
+ status["matanyone_loaded"] = False
507
+ if self.progress_tracker:
508
+ status["progress"] = self.progress_tracker.get_all_progress()
509
+
510
+ return status
511
+ def _safe_config_dict(self) -> Dict[str, Any]:
512
+ try:
513
+ return self.config.to_dict()
514
+ except Exception:
515
+ keys = ["use_nvenc", "prefer_mp4", "video_codec", "audio_copy",
516
+ "ffmpeg_path", "max_model_size", "max_model_size_bytes",
517
+ "output_dir", "matanyone_enabled"]
518
+ return {k: getattr(self.config, k, None) for k in keys}
519
+ def _safe_memory_usage(self) -> Dict[str, Any]:
520
+ try:
521
+ return self.memory_manager.get_memory_usage()
522
+ except Exception:
523
+ return {}
524
+ def cancel_processing(self):
525
+ self.cancel_event.set()
526
+ logger.info("Cancellation requested")
527
+ def cleanup_resources(self):
528
+ try:
529
+ self.memory_manager.cleanup_aggressive()
530
+ except Exception:
531
+ pass
532
+ try:
533
+ self.model_loader.cleanup()
534
+ except Exception:
535
+ pass
536
+ logger.info("Resources cleaned up")
537
+ # ── Singleton + thin wrappers (used by UI callbacks) ────────────────────────
538
+ processor = VideoProcessor()
539
+ def load_models_with_validation(progress_callback: Optional[Callable] = None) -> str:
540
+ return processor.load_models(progress_callback)
541
+ def process_video_fixed(
542
+ video_path: str,
543
+ background_choice: str,
544
+ custom_background_path: Optional[str],
545
+ progress_callback: Optional[Callable] = None,
546
+ use_two_stage: bool = False,
547
+ chroma_preset: str = "standard",
548
+ key_color_mode: str = "auto",
549
+ preview_mask: bool = False,
550
+ preview_greenscreen: bool = False,
551
+ ) -> Tuple[Optional[str], Optional[str], str]:
552
+ return processor.process_video(
553
+ video_path,
554
+ background_choice,
555
+ custom_background_path,
556
+ progress_callback,
557
+ use_two_stage,
558
+ chroma_preset,
559
+ key_color_mode,
560
+ preview_mask,
561
+ preview_greenscreen,
562
+ )
563
+ def get_model_status() -> Dict[str, Any]:
564
+ return processor.get_status()
565
+ def get_cache_status() -> Dict[str, Any]:
566
+ return processor.get_status()
567
+ PROCESS_CANCELLED = processor.cancel_event
568
+ # ── CLI entrypoint (must exist; app.py imports main) ─────────────────────────
569
+ def main():
570
  try:
571
+ logger.info("Starting BackgroundFX Pro")
572
+ logger.info(f"Device: {processor.device_manager.get_optimal_device()}")
573
+ logger.info(f"Two-stage available: {TWO_STAGE_AVAILABLE}")
574
 
575
+ # 🔹 Quiet model self-check (defaults to async; set SELF_CHECK_MODE=sync to block)
576
+ if schedule_startup_selfcheck is not None:
577
+ try:
578
+ schedule_startup_selfcheck(mode=os.getenv("SELF_CHECK_MODE", "async"))
579
+ except Exception as e:
580
+ logger.error(f"Startup self-check skipped: {e}", exc_info=True)
581
+
582
+ # Log model loader type
583
+ try:
584
+ from models.loaders.model_loader import ModelLoader
585
+ logger.info("Using split loader architecture")
586
+ except Exception:
587
+ logger.info("Using legacy loader")
588
+
589
+ # FIXED: Move UI import inside main() to avoid circular dependency
590
+ # and add better error handling
591
+ try:
592
+ # Import here to break circular dependency
593
+ from ui import ui_components
594
+
595
+ # Now get the create_interface function
596
+ if hasattr(ui_components, 'create_interface'):
597
+ create_interface = ui_components.create_interface
598
+ else:
599
+ logger.error("create_interface not found in ui_components")
600
+ logger.error(f"Available attributes: {dir(ui_components)}")
601
+ raise ImportError("create_interface function not found")
602
+
603
+ except ImportError as e:
604
+ logger.error(f"Failed to import UI components: {e}")
605
+ import traceback
606
+ traceback.print_exc()
607
+
608
+ # Try alternate import method
609
+ try:
610
+ logger.info("Trying alternate import method...")
611
+ import importlib
612
+ ui_components = importlib.import_module('ui.ui_components')
613
+ create_interface = getattr(ui_components, 'create_interface')
614
+ logger.info("Alternate import successful")
615
+ except Exception as e2:
616
+ logger.error(f"Alternate import also failed: {e2}")
617
+ logger.info("System initialized but UI unavailable. Exiting.")
618
+ return
619
+
620
+ # Create and launch the interface
621
+ try:
622
+ demo = create_interface()
623
+ demo.queue().launch(
624
+ server_name="0.0.0.0",
625
+ server_port=7860,
626
+ show_error=True,
627
+ debug=False,
628
+ )
629
+ except Exception as e:
630
+ logger.error(f"Failed to launch Gradio interface: {e}")
631
+ import traceback
632
+ traceback.print_exc()
633
 
634
  except Exception as e:
635
+ logger.error(f"Fatal error in main: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  import traceback
637
  traceback.print_exc()
638
+ finally:
639
+ processor.cleanup_resources()
640
+
641
+ if __name__ == "__main__":
642
+ main()