MogensR commited on
Commit
56754de
Β·
1 Parent(s): 7b01a2f

Update core/app.py

Browse files
Files changed (1) hide show
  1. core/app.py +1031 -586
core/app.py CHANGED
@@ -1,609 +1,1054 @@
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
- # Now import UI - this should work since callbacks no longer imports from core.app at module level
590
- from ui.ui_components import create_interface
591
-
592
- # Create and launch the interface
593
- demo = create_interface()
594
- demo.queue().launch(
595
- server_name="0.0.0.0",
596
- server_port=7860,
597
- show_error=True,
598
- debug=False,
599
- )
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  except Exception as e:
602
- logger.error(f"Fatal error in main: {e}")
603
- import traceback
604
- traceback.print_exc()
605
- finally:
606
- processor.cleanup_resources()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  if __name__ == "__main__":
609
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()