sam12555 commited on
Commit
18c136e
·
verified ·
1 Parent(s): 418cd06

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +241 -195
app.py CHANGED
@@ -1,4 +1,6 @@
1
- import gradio as gr
 
 
2
  import torch
3
  import torch.nn as nn
4
  import torch.nn.functional as F
@@ -14,14 +16,45 @@ import librosa
14
  from PIL import Image
15
  import tempfile
16
  import os
17
- from typing import Tuple, Dict, Any
 
18
  import json
19
  import warnings
 
 
 
20
 
21
- print(f"PyTorch version: {torch.__version__}")
22
- print(f"CUDA available: {torch.cuda.is_available()}")
 
23
 
24
- # Actual MirrorMindModel architecture (copied from mirrormind.py)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  class GradientReverseFn(torch.autograd.Function):
26
  """Gradient reversal function for adversarial training"""
27
  @staticmethod
@@ -93,13 +126,13 @@ class MirrorMindModel(nn.Module):
93
  if any(x in name for x in ['encoder.layers.10', 'encoder.layers.11']):
94
  param.requires_grad = True
95
  self.audio_pool = nn.AdaptiveAvgPool1d(1)
96
- print("Using Wav2Vec2 audio encoder")
97
  except Exception as e:
98
- print(f"Warning: Could not load Wav2Vec2, using CNN: {e}")
99
  self._create_improved_audio_encoder()
100
  else:
101
  self._create_improved_audio_encoder()
102
- print("Using CNN audio encoder")
103
 
104
  self.audio_proj = nn.Sequential(
105
  nn.Linear(self.audio_feat_dim, hidden_dim),
@@ -231,7 +264,7 @@ class MirrorMindModel(nn.Module):
231
  vid_feat = self._process_video_temporal_attention(vid_feat_bt, B, T)
232
  vid_feat = self.video_proj(vid_feat)
233
  except Exception as e:
234
- print(f"Video processing error: {e}")
235
  vid_feat = torch.zeros((B, self.hidden_dim), device=device)
236
  try:
237
  if audio is None or torch.all(audio == 0):
@@ -252,7 +285,7 @@ class MirrorMindModel(nn.Module):
252
  aud_feat = x
253
  aud_feat = self.audio_proj(aud_feat)
254
  except Exception as e:
255
- print(f"Audio processing error: {e}")
256
  aud_feat = torch.zeros((B, self.hidden_dim), device=device)
257
  fused = torch.cat([vid_feat, aud_feat], dim=1)
258
  fused_final = self.fusion_proj(fused)
@@ -265,17 +298,16 @@ class MirrorMindModel(nn.Module):
265
  domain_logits = self.domain_head(rev)
266
  return neuroticism_pred, emotion_logits, domain_logits
267
 
268
- # Inference wrapper (renamed to avoid conflict)
269
  class MirrorMindInference:
270
  def __init__(self):
271
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
272
- print(f"Using device: {self.device}")
273
 
274
  model_path = "mirror_model.pth"
275
- print(f"Loading model from {model_path}...")
276
 
277
  if not os.path.exists(model_path):
278
- print(f"Model file {model_path} not found. Using fallback mode.")
279
  self.model = None
280
  return
281
 
@@ -283,132 +315,97 @@ class MirrorMindInference:
283
  pytorch_version = torch.__version__
284
 
285
  if pytorch_version.startswith(("2.8", "2.9")):
286
- print(f"Detected PyTorch {pytorch_version} - using version-specific loading...")
287
  try:
288
- print("Loading with weights_only=False...")
289
  with warnings.catch_warnings():
290
  warnings.simplefilter("ignore")
291
  checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
292
- print("✓ Successfully loaded complete model")
293
  except Exception as e1:
294
- print(f"✗ Failed: {e1}")
295
  try:
296
- print("Attempting state_dict loading with weights_only=True...")
297
  checkpoint = torch.load(model_path, map_location=self.device, weights_only=True)
298
- print("✓ Loaded as state_dict")
299
  except Exception as e2:
300
- print(f"✗ Failed: {e2}")
301
  checkpoint = None
302
  else:
303
  try:
304
- print(f"Using standard loading for PyTorch {pytorch_version}...")
305
  checkpoint = torch.load(model_path, map_location=self.device)
306
- print("✓ Loaded with standard method")
307
  except Exception as e:
308
- print(f"✗ Failed: {e}")
309
  checkpoint = None
310
 
311
  if checkpoint is None:
312
- print("All loading methods failed. Using fallback mode.")
313
  self.model = None
314
  return
315
 
316
  if isinstance(checkpoint, dict):
317
- print(f"Checkpoint keys: {list(checkpoint.keys())}")
318
 
319
  if 'model' in checkpoint and 'state_dict' in checkpoint:
320
  self.model = checkpoint['model']
321
  self.model.load_state_dict(checkpoint['state_dict'])
322
- print("✓ Loaded model architecture + state dict")
323
 
324
  elif 'state_dict' in checkpoint:
325
- print("Found 'state_dict' - attempting to reconstruct model...")
326
  if 'model_config' in checkpoint:
327
  self.model = MirrorMindModel(**checkpoint['model_config'])
328
  self.model.load_state_dict(checkpoint['state_dict'])
329
- print("✓ Loaded using model_config + state_dict")
330
  else:
331
- print("⚠️ No model_config. Using fallback.")
332
  self.model = None
333
  return
334
 
335
  elif 'model_state_dict' in checkpoint:
336
- print("Found 'model_state_dict' - checking for model class info...")
337
  state_dict = checkpoint['model_state_dict']
338
 
339
  if 'model_config' in checkpoint:
340
  self.model = MirrorMindModel(**checkpoint['model_config'])
341
  self.model.load_state_dict(state_dict)
342
- print("✓ Loaded using model_config + model_state_dict")
343
  else:
344
- model_info = self.analyze_state_dict(state_dict)
345
- print(f"State dict analysis: {model_info}")
346
- print("⚠️ No model_config. Using fallback.")
347
  self.model = None
348
  return
349
 
350
  elif len(checkpoint.keys()) > 0 and all(isinstance(v, torch.Tensor) for v in checkpoint.values()):
351
- print("Checkpoint appears to be a direct state dict")
352
- model_info = self.analyze_state_dict(checkpoint)
353
- print(f"Direct state dict analysis: {model_info}")
354
- print("⚠️ Cannot reconstruct without model_config. Using fallback.")
355
  self.model = None
356
  return
357
 
358
  else:
359
  if hasattr(checkpoint, 'eval') and callable(checkpoint.eval):
360
  self.model = checkpoint
361
- print("✓ Using checkpoint as complete model")
362
  else:
363
- print("⚠️ Unrecognized format. Using fallback.")
364
  self.model = None
365
  return
366
  else:
367
  if hasattr(checkpoint, 'eval') and callable(checkpoint.eval):
368
  self.model = checkpoint
369
- print("✓ Loaded complete model object")
370
  else:
371
- print("⚠️ Not a model object. Using fallback.")
372
  self.model = None
373
  return
374
 
375
  if self.model is not None:
376
  self.model.to(self.device)
377
  self.model.eval()
378
- print("Model loaded and ready for inference!")
379
  else:
380
- print("Model is None after loading. Using fallback.")
381
-
382
- def analyze_state_dict(self, state_dict):
383
- info = {
384
- 'total_params': len(state_dict),
385
- 'layer_types': set(),
386
- 'input_features': None,
387
- 'output_features': None,
388
- 'has_conv': False,
389
- 'has_lstm': False,
390
- 'has_attention': False
391
- }
392
-
393
- for key, tensor in state_dict.items():
394
- if 'conv' in key.lower():
395
- info['has_conv'] = True
396
- info['layer_types'].add('conv')
397
- elif 'lstm' in key.lower() or 'rnn' in key.lower():
398
- info['has_lstm'] = True
399
- info['layer_types'].add('lstm')
400
- elif 'attention' in key.lower() or 'attn' in key.lower():
401
- info['has_attention'] = True
402
- info['layer_types'].add('attention')
403
- elif 'linear' in key.lower() or 'fc' in key.lower():
404
- info['layer_types'].add('linear')
405
- if key.endswith('.weight'):
406
- if info['input_features'] is None:
407
- info['input_features'] = tensor.shape[-1]
408
- info['output_features'] = tensor.shape[0]
409
-
410
- info['layer_types'] = list(info['layer_types'])
411
- return info
412
 
413
  def extract_video_frames(self, video_path: str, num_frames: int = 8) -> torch.Tensor:
414
  try:
@@ -442,7 +439,7 @@ class MirrorMindInference:
442
  return video_tensor
443
 
444
  except Exception as e:
445
- print(f"Video extraction failed: {e}")
446
  dummy_frames = np.random.rand(num_frames, 3, 224, 224).astype(np.float32)
447
  return torch.from_numpy(dummy_frames).to(self.device)
448
 
@@ -465,7 +462,7 @@ class MirrorMindInference:
465
 
466
  return audio_tensor
467
  except Exception as e:
468
- print(f"Audio extraction failed: {e}")
469
  return torch.zeros(14).to(self.device)
470
 
471
  def predict(self, video_path: str) -> Dict[str, Any]:
@@ -485,7 +482,7 @@ class MirrorMindInference:
485
  emotion_labels = ['Anger', 'Disgust', 'Fear', 'Happy', 'Neutral', 'Sad']
486
  emotion_scores = dict(zip(emotion_labels, emotion_probs))
487
  else:
488
- print("Using fallback predictions")
489
  neuroticism_score = np.random.uniform(0.2, 0.8)
490
  emotion_scores = {
491
  'Happy': np.random.uniform(0.1, 0.4),
@@ -507,7 +504,7 @@ class MirrorMindInference:
507
  }
508
 
509
  except Exception as e:
510
- print(f"Prediction error: {e}")
511
  return {
512
  'error': str(e),
513
  'neuroticism': 0.0,
@@ -517,17 +514,90 @@ class MirrorMindInference:
517
  'model_used': 'error'
518
  }
519
 
520
- def analyze_video(video_file) -> Tuple[float, str, str]:
521
- if video_file is None:
522
- return 0.0, "No video uploaded", "Please upload a video file"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
  try:
525
- video_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
526
- results = model.predict(video_path)
 
 
 
 
527
 
528
  if 'error' in results:
529
- return 0.0, f"Analysis Error: {results['error']}", str(results)
530
 
 
531
  neuroticism_score = results['neuroticism']
532
 
533
  if neuroticism_score <= 0.3:
@@ -540,130 +610,106 @@ def analyze_video(video_file) -> Tuple[float, str, str]:
540
  emotions = results['emotions']
541
  dominant_emotion = max(emotions.keys(), key=lambda k: emotions[k])
542
 
543
- emotion_text = f"**Dominant Emotion:** {dominant_emotion} ({emotions[dominant_emotion]:.1%})\n\n"
544
- emotion_text += "**All Emotions:**\n"
545
- for emotion, score in sorted(emotions.items(), key=lambda x: x[1], reverse=True):
546
- emotion_text += f"- {emotion}: {score:.1%}\n"
547
 
548
- model_status = " Real Model" if results['model_used'] == 'real' else "⚠️ Fallback Mode"
549
- detailed_results = f"""
550
- **Analysis Summary:**
551
- - Neuroticism Score: {neuroticism_score:.3f}
552
- - Neuroticism Level: {neuroticism_level}
553
- - Frames Processed: {results['frames_processed']}
554
- - Audio Features: {'✓' if results['audio_features_extracted'] else '✗'}
555
-
556
- **Technical Details:**
557
- - Model: {model_status}
558
- - Processing: Multimodal (Video + Audio)
559
- - Device: {'GPU' if torch.cuda.is_available() else 'CPU'}
560
- - Confidence: {'High' if results['model_used'] == 'real' else 'Demo Mode'}
561
- """.strip()
562
-
563
- return neuroticism_score, emotion_text, detailed_results
564
 
 
 
565
  except Exception as e:
566
- error_msg = f"Processing error: {str(e)}"
567
- return 0.0, error_msg, error_msg
 
 
 
 
 
 
 
568
 
569
- def create_interface():
570
- css = """
571
- .gradio-container {
572
- font-family: 'Helvetica Neue', Arial, sans-serif;
573
- }
574
- .output-class {
575
- font-size: 16px;
576
- }
577
  """
 
578
 
579
- with gr.Blocks(css=css, title="🧠 MirrorMind Analysis") as demo:
580
-
581
- model_status_text = " Real Model Loaded" if model.model is not None else "⚠️ Demo Mode - Using fallback predictions"
582
-
583
- gr.Markdown(f"""
584
- # 🧠 MirrorMind: AI Personality & Emotion Analysis
 
 
 
 
 
 
 
 
 
 
 
585
 
586
- Upload a video to analyze personality traits and emotions using your trained MirrorMind model.
 
 
587
 
588
- **Model Status:** {model_status_text}
589
- **PyTorch Version:** {torch.__version__}
590
- **CUDA Available:** {'Yes' if torch.cuda.is_available() else 'No'}
591
- """)
592
 
593
- with gr.Row():
594
- with gr.Column(scale=1):
595
- video_input = gr.Video(
596
- label="Upload Video",
597
- sources=["upload"],
598
- )
599
-
600
- analyze_btn = gr.Button(
601
- "🔍 Analyze Video",
602
- variant="primary",
603
- scale=1
604
- )
605
-
606
- gr.Markdown("""
607
- **Supported formats:** MP4, AVI, MOV, WebM
608
- **Optimal duration:** 4-10 seconds
609
- **Requirements:** Clear face, good lighting, audio included
610
- """)
611
-
612
- with gr.Column(scale=2):
613
- neuroticism_output = gr.Number(
614
- label="🎭 Neuroticism Score (0.0 - 1.0)",
615
- precision=3
616
- )
617
-
618
- emotion_output = gr.Markdown(
619
- label="😊 Emotion Analysis"
620
- )
621
-
622
- details_output = gr.Markdown(
623
- label="📊 Detailed Results"
624
- )
625
 
626
- analyze_btn.click(
627
- fn=analyze_video,
628
- inputs=[video_input],
629
- outputs=[neuroticism_output, emotion_output, details_output]
630
- )
631
 
632
- video_input.change(
633
- fn=analyze_video,
634
- inputs=[video_input],
635
- outputs=[neuroticism_output, emotion_output, details_output]
636
- )
 
637
 
638
- gr.Markdown("""
639
- ---
640
- ### 📋 Understanding Your Results
641
 
642
- **Neuroticism Scale:**
643
- - **0.0-0.3:** Low - Emotionally stable, calm under pressure
644
- - **0.3-0.7:** Medium - Moderate emotional reactivity
645
- - **0.7-1.0:** High - More emotionally sensitive, reactive
646
 
647
- **Emotions Detected:** Anger, Disgust, Fear, Happy, Neutral, Sad
 
 
 
 
 
 
 
 
 
648
 
649
- **Model Information:**
650
- - Uses your trained `mirror_model.pth` for real AI predictions
651
- - Processes both video frames and audio features
652
- - Automatically falls back to demo mode if model loading fails
653
- """)
 
 
654
 
655
- return demo
656
-
657
- print("Initializing MirrorMind model...")
658
- model = MirrorMindInference()
 
 
659
 
660
  if __name__ == "__main__":
661
- demo = create_interface()
662
- demo.launch(
663
- server_name="0.0.0.0",
664
- server_port=7860,
665
- share=False,
666
- debug=False,
667
- show_error=True,
668
- quiet=False
669
- )
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
  import torch
5
  import torch.nn as nn
6
  import torch.nn.functional as F
 
16
  from PIL import Image
17
  import tempfile
18
  import os
19
+ import shutil
20
+ from typing import Dict, Any, Optional
21
  import json
22
  import warnings
23
+ import logging
24
+ import asyncio
25
+ from contextlib import asynccontextmanager
26
 
27
+ # Configure logging
28
+ logging.basicConfig(level=logging.INFO)
29
+ logger = logging.getLogger(__name__)
30
 
31
+ # Global model instance
32
+ model_instance = None
33
+
34
+ # Response models
35
+ class EmotionScores(BaseModel):
36
+ Anger: float
37
+ Disgust: float
38
+ Fear: float
39
+ Happy: float
40
+ Neutral: float
41
+ Sad: float
42
+
43
+ class AnalysisResult(BaseModel):
44
+ neuroticism: float
45
+ neuroticism_level: str
46
+ emotions: EmotionScores
47
+ dominant_emotion: str
48
+ frames_processed: int
49
+ audio_features_extracted: bool
50
+ model_used: str
51
+ confidence: str
52
+
53
+ class ErrorResponse(BaseModel):
54
+ error: str
55
+ message: str
56
+
57
+ # MirrorMind Model Architecture (same as your original)
58
  class GradientReverseFn(torch.autograd.Function):
59
  """Gradient reversal function for adversarial training"""
60
  @staticmethod
 
126
  if any(x in name for x in ['encoder.layers.10', 'encoder.layers.11']):
127
  param.requires_grad = True
128
  self.audio_pool = nn.AdaptiveAvgPool1d(1)
129
+ logger.info("Using Wav2Vec2 audio encoder")
130
  except Exception as e:
131
+ logger.warning(f"Could not load Wav2Vec2, using CNN: {e}")
132
  self._create_improved_audio_encoder()
133
  else:
134
  self._create_improved_audio_encoder()
135
+ logger.info("Using CNN audio encoder")
136
 
137
  self.audio_proj = nn.Sequential(
138
  nn.Linear(self.audio_feat_dim, hidden_dim),
 
264
  vid_feat = self._process_video_temporal_attention(vid_feat_bt, B, T)
265
  vid_feat = self.video_proj(vid_feat)
266
  except Exception as e:
267
+ logger.error(f"Video processing error: {e}")
268
  vid_feat = torch.zeros((B, self.hidden_dim), device=device)
269
  try:
270
  if audio is None or torch.all(audio == 0):
 
285
  aud_feat = x
286
  aud_feat = self.audio_proj(aud_feat)
287
  except Exception as e:
288
+ logger.error(f"Audio processing error: {e}")
289
  aud_feat = torch.zeros((B, self.hidden_dim), device=device)
290
  fused = torch.cat([vid_feat, aud_feat], dim=1)
291
  fused_final = self.fusion_proj(fused)
 
298
  domain_logits = self.domain_head(rev)
299
  return neuroticism_pred, emotion_logits, domain_logits
300
 
 
301
  class MirrorMindInference:
302
  def __init__(self):
303
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
304
+ logger.info(f"Using device: {self.device}")
305
 
306
  model_path = "mirror_model.pth"
307
+ logger.info(f"Loading model from {model_path}...")
308
 
309
  if not os.path.exists(model_path):
310
+ logger.warning(f"Model file {model_path} not found. Using fallback mode.")
311
  self.model = None
312
  return
313
 
 
315
  pytorch_version = torch.__version__
316
 
317
  if pytorch_version.startswith(("2.8", "2.9")):
318
+ logger.info(f"Detected PyTorch {pytorch_version} - using version-specific loading...")
319
  try:
320
+ logger.info("Loading with weights_only=False...")
321
  with warnings.catch_warnings():
322
  warnings.simplefilter("ignore")
323
  checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
324
+ logger.info("✓ Successfully loaded complete model")
325
  except Exception as e1:
326
+ logger.error(f"✗ Failed: {e1}")
327
  try:
328
+ logger.info("Attempting state_dict loading with weights_only=True...")
329
  checkpoint = torch.load(model_path, map_location=self.device, weights_only=True)
330
+ logger.info("✓ Loaded as state_dict")
331
  except Exception as e2:
332
+ logger.error(f"✗ Failed: {e2}")
333
  checkpoint = None
334
  else:
335
  try:
336
+ logger.info(f"Using standard loading for PyTorch {pytorch_version}...")
337
  checkpoint = torch.load(model_path, map_location=self.device)
338
+ logger.info("✓ Loaded with standard method")
339
  except Exception as e:
340
+ logger.error(f"✗ Failed: {e}")
341
  checkpoint = None
342
 
343
  if checkpoint is None:
344
+ logger.warning("All loading methods failed. Using fallback mode.")
345
  self.model = None
346
  return
347
 
348
  if isinstance(checkpoint, dict):
349
+ logger.info(f"Checkpoint keys: {list(checkpoint.keys())}")
350
 
351
  if 'model' in checkpoint and 'state_dict' in checkpoint:
352
  self.model = checkpoint['model']
353
  self.model.load_state_dict(checkpoint['state_dict'])
354
+ logger.info("✓ Loaded model architecture + state dict")
355
 
356
  elif 'state_dict' in checkpoint:
357
+ logger.info("Found 'state_dict' - attempting to reconstruct model...")
358
  if 'model_config' in checkpoint:
359
  self.model = MirrorMindModel(**checkpoint['model_config'])
360
  self.model.load_state_dict(checkpoint['state_dict'])
361
+ logger.info("✓ Loaded using model_config + state_dict")
362
  else:
363
+ logger.warning("⚠️ No model_config. Using fallback.")
364
  self.model = None
365
  return
366
 
367
  elif 'model_state_dict' in checkpoint:
368
+ logger.info("Found 'model_state_dict' - checking for model class info...")
369
  state_dict = checkpoint['model_state_dict']
370
 
371
  if 'model_config' in checkpoint:
372
  self.model = MirrorMindModel(**checkpoint['model_config'])
373
  self.model.load_state_dict(state_dict)
374
+ logger.info("✓ Loaded using model_config + model_state_dict")
375
  else:
376
+ logger.warning("⚠️ No model_config. Using fallback.")
 
 
377
  self.model = None
378
  return
379
 
380
  elif len(checkpoint.keys()) > 0 and all(isinstance(v, torch.Tensor) for v in checkpoint.values()):
381
+ logger.info("Checkpoint appears to be a direct state dict")
382
+ logger.warning("⚠️ Cannot reconstruct without model_config. Using fallback.")
 
 
383
  self.model = None
384
  return
385
 
386
  else:
387
  if hasattr(checkpoint, 'eval') and callable(checkpoint.eval):
388
  self.model = checkpoint
389
+ logger.info("✓ Using checkpoint as complete model")
390
  else:
391
+ logger.warning("⚠️ Unrecognized format. Using fallback.")
392
  self.model = None
393
  return
394
  else:
395
  if hasattr(checkpoint, 'eval') and callable(checkpoint.eval):
396
  self.model = checkpoint
397
+ logger.info("✓ Loaded complete model object")
398
  else:
399
+ logger.warning("⚠️ Not a model object. Using fallback.")
400
  self.model = None
401
  return
402
 
403
  if self.model is not None:
404
  self.model.to(self.device)
405
  self.model.eval()
406
+ logger.info("Model loaded and ready for inference!")
407
  else:
408
+ logger.warning("Model is None after loading. Using fallback.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  def extract_video_frames(self, video_path: str, num_frames: int = 8) -> torch.Tensor:
411
  try:
 
439
  return video_tensor
440
 
441
  except Exception as e:
442
+ logger.error(f"Video extraction failed: {e}")
443
  dummy_frames = np.random.rand(num_frames, 3, 224, 224).astype(np.float32)
444
  return torch.from_numpy(dummy_frames).to(self.device)
445
 
 
462
 
463
  return audio_tensor
464
  except Exception as e:
465
+ logger.error(f"Audio extraction failed: {e}")
466
  return torch.zeros(14).to(self.device)
467
 
468
  def predict(self, video_path: str) -> Dict[str, Any]:
 
482
  emotion_labels = ['Anger', 'Disgust', 'Fear', 'Happy', 'Neutral', 'Sad']
483
  emotion_scores = dict(zip(emotion_labels, emotion_probs))
484
  else:
485
+ logger.info("Using fallback predictions")
486
  neuroticism_score = np.random.uniform(0.2, 0.8)
487
  emotion_scores = {
488
  'Happy': np.random.uniform(0.1, 0.4),
 
504
  }
505
 
506
  except Exception as e:
507
+ logger.error(f"Prediction error: {e}")
508
  return {
509
  'error': str(e),
510
  'neuroticism': 0.0,
 
514
  'model_used': 'error'
515
  }
516
 
517
+ # Initialize model on startup
518
+ @asynccontextmanager
519
+ async def lifespan(app: FastAPI):
520
+ global model_instance
521
+ logger.info("Starting MirrorMind API service...")
522
+ model_instance = MirrorMindInference()
523
+ logger.info(f"PyTorch version: {torch.__version__}")
524
+ logger.info(f"CUDA available: {torch.cuda.is_available()}")
525
+ yield
526
+ logger.info("Shutting down MirrorMind API service...")
527
+
528
+ # Initialize FastAPI app
529
+ app = FastAPI(
530
+ title="MirrorMind API",
531
+ description="AI Personality & Emotion Analysis API",
532
+ version="1.0.0",
533
+ lifespan=lifespan
534
+ )
535
+
536
+ # Add CORS middleware
537
+ app.add_middleware(
538
+ CORSMiddleware,
539
+ allow_origins=["*"], # Configure this for production
540
+ allow_credentials=True,
541
+ allow_methods=["*"],
542
+ allow_headers=["*"],
543
+ )
544
+
545
+ @app.get("/")
546
+ async def root():
547
+ return {
548
+ "message": "MirrorMind API is running",
549
+ "version": "1.0.0",
550
+ "pytorch_version": torch.__version__,
551
+ "cuda_available": torch.cuda.is_available(),
552
+ "model_loaded": model_instance.model is not None if model_instance else False
553
+ }
554
+
555
+ @app.get("/health")
556
+ async def health_check():
557
+ return {
558
+ "status": "healthy",
559
+ "model_status": "loaded" if model_instance and model_instance.model is not None else "fallback",
560
+ "device": str(model_instance.device) if model_instance else "unknown"
561
+ }
562
+
563
+ @app.post("/analyze", response_model=AnalysisResult)
564
+ async def analyze_video(file: UploadFile = File(...)):
565
+ """
566
+ Analyze a video file for personality traits and emotions.
567
+
568
+ - **file**: Video file (MP4, AVI, MOV, WebM)
569
+ - Returns neuroticism score and emotion analysis
570
+ """
571
+
572
+ if not model_instance:
573
+ raise HTTPException(status_code=503, detail="Model not initialized")
574
+
575
+ # Validate file type
576
+ allowed_extensions = {'.mp4', '.avi', '.mov', '.webm', '.mkv'}
577
+ file_extension = os.path.splitext(file.filename.lower())[1]
578
+
579
+ if file_extension not in allowed_extensions:
580
+ raise HTTPException(
581
+ status_code=400,
582
+ detail=f"Unsupported file format. Allowed formats: {', '.join(allowed_extensions)}"
583
+ )
584
+
585
+ # Create temporary file
586
+ temp_dir = tempfile.mkdtemp()
587
+ temp_file_path = os.path.join(temp_dir, f"uploaded_video{file_extension}")
588
 
589
  try:
590
+ # Save uploaded file
591
+ with open(temp_file_path, "wb") as buffer:
592
+ shutil.copyfileobj(file.file, buffer)
593
+
594
+ # Analyze video
595
+ results = model_instance.predict(temp_file_path)
596
 
597
  if 'error' in results:
598
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {results['error']}")
599
 
600
+ # Process results
601
  neuroticism_score = results['neuroticism']
602
 
603
  if neuroticism_score <= 0.3:
 
610
  emotions = results['emotions']
611
  dominant_emotion = max(emotions.keys(), key=lambda k: emotions[k])
612
 
613
+ confidence = "High" if results['model_used'] == 'real' else "Demo Mode"
 
 
 
614
 
615
+ return AnalysisResult(
616
+ neuroticism=neuroticism_score,
617
+ neuroticism_level=neuroticism_level,
618
+ emotions=EmotionScores(**emotions),
619
+ dominant_emotion=dominant_emotion,
620
+ frames_processed=results['frames_processed'],
621
+ audio_features_extracted=results['audio_features_extracted'],
622
+ model_used=results['model_used'],
623
+ confidence=confidence
624
+ )
 
 
 
 
 
 
625
 
626
+ except HTTPException:
627
+ raise
628
  except Exception as e:
629
+ logger.error(f"Analysis error: {e}")
630
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
631
+
632
+ finally:
633
+ # Clean up temporary files
634
+ try:
635
+ shutil.rmtree(temp_dir)
636
+ except Exception as e:
637
+ logger.warning(f"Failed to clean up temp directory: {e}")
638
 
639
+ @app.post("/analyze-from-url")
640
+ async def analyze_video_from_url(video_url: str):
 
 
 
 
 
 
641
  """
642
+ Analyze a video from a URL (Firebase/Supabase storage).
643
 
644
+ - **video_url**: Direct URL to video file
645
+ - Returns neuroticism score and emotion analysis
646
+ """
647
+
648
+ if not model_instance:
649
+ raise HTTPException(status_code=503, detail="Model not initialized")
650
+
651
+ import requests
652
+
653
+ # Create temporary file
654
+ temp_dir = tempfile.mkdtemp()
655
+ temp_file_path = os.path.join(temp_dir, "downloaded_video.mp4")
656
+
657
+ try:
658
+ # Download video from URL
659
+ response = requests.get(video_url, stream=True, timeout=30)
660
+ response.raise_for_status()
661
 
662
+ with open(temp_file_path, "wb") as f:
663
+ for chunk in response.iter_content(chunk_size=8192):
664
+ f.write(chunk)
665
 
666
+ # Analyze video
667
+ results = model_instance.predict(temp_file_path)
 
 
668
 
669
+ if 'error' in results:
670
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {results['error']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
 
672
+ # Process results (same as above)
673
+ neuroticism_score = results['neuroticism']
 
 
 
674
 
675
+ if neuroticism_score <= 0.3:
676
+ neuroticism_level = "Low (Emotionally Stable)"
677
+ elif neuroticism_score <= 0.7:
678
+ neuroticism_level = "Medium (Moderate Reactivity)"
679
+ else:
680
+ neuroticism_level = "High (Emotionally Sensitive)"
681
 
682
+ emotions = results['emotions']
683
+ dominant_emotion = max(emotions.keys(), key=lambda k: emotions[k])
 
684
 
685
+ confidence = "High" if results['model_used'] == 'real' else "Demo Mode"
 
 
 
686
 
687
+ return AnalysisResult(
688
+ neuroticism=neuroticism_score,
689
+ neuroticism_level=neuroticism_level,
690
+ emotions=EmotionScores(**emotions),
691
+ dominant_emotion=dominant_emotion,
692
+ frames_processed=results['frames_processed'],
693
+ audio_features_extracted=results['audio_features_extracted'],
694
+ model_used=results['model_used'],
695
+ confidence=confidence
696
+ )
697
 
698
+ except requests.RequestException as e:
699
+ raise HTTPException(status_code=400, detail=f"Failed to download video: {str(e)}")
700
+ except HTTPException:
701
+ raise
702
+ except Exception as e:
703
+ logger.error(f"Analysis error: {e}")
704
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
705
 
706
+ finally:
707
+ # Clean up temporary files
708
+ try:
709
+ shutil.rmtree(temp_dir)
710
+ except Exception as e:
711
+ logger.warning(f"Failed to clean up temp directory: {e}")
712
 
713
  if __name__ == "__main__":
714
+ import uvicorn
715
+ uvicorn.run(app, host="0.0.0.0", port=8000)