Files changed (1) hide show
  1. app.py +597 -175
app.py CHANGED
@@ -5,7 +5,12 @@ import numpy as np
5
  import librosa
6
  import cv2
7
  import re
8
- from transformers import Wav2Vec2Processor, Wav2Vec2Model, AutoTokenizer, AutoModel
 
 
 
 
 
9
  from torchvision import models
10
  import tempfile
11
  import os
@@ -13,187 +18,392 @@ from huggingface_hub import hf_hub_download
13
  import whisper
14
  import subprocess
15
 
16
- # Configuration
 
 
 
17
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
  SAMPLE_RATE = 16000
19
  TEXT_MAX_LEN = 64
20
  LABELS = ["angry", "happy", "neutral", "sad"]
21
 
22
- # Load processors
23
- processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base-960h")
24
- tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- # Model Architecture (same as training)
27
  class ResNetVideoEncoder(nn.Module):
28
  def __init__(self, out_dim=768):
29
  super().__init__()
 
30
  base = models.resnet18(pretrained=False)
31
- self.backbone = nn.Sequential(*list(base.children())[:-1])
 
 
 
 
32
  self.proj = nn.Linear(512, out_dim)
33
 
34
  def forward(self, x):
 
35
  B, C, T, H, W = x.shape
 
36
  feats = []
 
37
  for t in range(T):
 
38
  ft = self.backbone(x[:, :, t])
39
- feats.append(ft.squeeze(-1).squeeze(-1))
 
 
 
 
40
  feats = torch.stack(feats, dim=1).mean(1)
 
41
  return self.proj(feats)
42
 
 
 
43
  def mean_pool(x, mask):
 
44
  mask = mask[:, :x.size(1)]
 
45
  mask = mask.unsqueeze(-1).float()
46
- return (x * mask).sum(1) / mask.sum(1).clamp(min=1e-6)
 
 
 
 
 
 
47
 
48
  class HBF(nn.Module):
 
49
  def __init__(self, d=768, n_layers=6):
 
50
  super().__init__()
51
- self.proj_a = nn.ModuleList([nn.Linear(d, d) for _ in range(n_layers)])
52
- self.proj_t = nn.ModuleList([nn.Linear(d, d) for _ in range(n_layers)])
53
- self.proj_v = nn.ModuleList([nn.Linear(d, d) for _ in range(n_layers)])
54
- self.fwd1 = nn.ModuleList([nn.Linear(3*d, d) for _ in range(n_layers)])
55
- self.fwd2 = nn.ModuleList([nn.Linear(d, d) for _ in range(n_layers)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  self.drop = nn.Dropout(0.1)
57
- self.act1, self.act2 = nn.GELU(), nn.Tanh()
 
 
 
58
  self.n = n_layers
59
 
60
  def forward(self, a, t, v):
 
61
  v_prev = None
 
62
  for i in range(self.n):
63
- va = self.act2(self.drop(self.proj_a[i](a)))
64
- vt = self.act2(self.drop(self.proj_t[i](t)))
65
- vv = self.act2(self.drop(self.proj_v[i](v)))
66
- cat = torch.cat([va, vt, vv] if v_prev is None else [va, vt, v_prev], -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  x = self.act1(self.fwd1[i](cat))
 
68
  v_prev = self.fwd2[i](x)
 
69
  return v_prev
70
 
 
 
71
  class AVVideoModel(nn.Module):
 
72
  def __init__(self, num_classes, n_layers=6):
 
73
  super().__init__()
74
- self.a_enc = Wav2Vec2Model.from_pretrained("facebook/wav2vec2-base-960h")
75
- self.t_enc = AutoModel.from_pretrained("bert-base-uncased")
 
 
 
 
 
 
 
76
  self.v_enc = ResNetVideoEncoder()
 
77
  self.hbf = HBF(n_layers=n_layers)
 
78
  self.fc = nn.Linear(768, num_classes)
 
79
  self.fc_audio = nn.Linear(768, num_classes)
80
  self.fc_text = nn.Linear(768, num_classes)
81
  self.fc_video = nn.Linear(768, num_classes)
82
 
83
- def forward(self, audio, audio_mask, text_ids, text_mask, video):
84
- a_out = self.a_enc(audio, attention_mask=audio_mask, return_dict=True)
85
- t_out = self.t_enc(input_ids=text_ids, attention_mask=text_mask, return_dict=True)
86
-
87
- a_pool = mean_pool(a_out.last_hidden_state, audio_mask)
88
- t_pool = mean_pool(t_out.last_hidden_state, text_mask)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  v_pool = self.v_enc(video)
90
-
91
- a_pool = torch.nan_to_num(a_pool, nan=0.0, posinf=1e4, neginf=-1e4)
92
- t_pool = torch.nan_to_num(t_pool, nan=0.0, posinf=1e4, neginf=-1e4)
93
- v_pool = torch.nan_to_num(v_pool, nan=0.0, posinf=1e4, neginf=-1e4)
94
-
95
- a_logits = self.fc_audio(a_pool)
96
- t_logits = self.fc_text(t_pool)
97
- v_logits = self.fc_video(v_pool)
98
-
99
  fused = self.hbf(a_pool, t_pool, v_pool)
100
- fused = torch.nan_to_num(fused, nan=0.0, posinf=1e4, neginf=-1e4)
 
 
101
  fused_logits = self.fc(fused)
102
-
103
- return fused_logits, a_logits, t_logits, v_logits
104
 
105
- # Load model
 
 
 
 
106
 
107
- model = AVVideoModel(num_classes=len(LABELS)).to(DEVICE)
 
 
108
 
109
- # Download model from Hugging Face Model Hub
110
  try:
 
111
  model_path = hf_hub_download(
112
- repo_id="ApurvaKondekar/emotion_model", # CHANGE THIS
113
- filename="model_weights.pth" # YOUR FILE NAME
114
  )
115
- model.load_state_dict(torch.load(model_path, map_location=DEVICE))
 
 
 
 
116
  model.eval()
117
- print("βœ… Model loaded from Hugging Face")
 
 
118
  except Exception as e:
119
- print(f"❌ Failed to load model: {e}")
120
- # Alternative: Create a dummy model for testing
121
- # Load trained weights (you'll need to upload this)
122
- if os.path.exists("model_weights.pth"):
123
- model.load_state_dict(torch.load("model_weights.pth", map_location=DEVICE))
124
- model.eval()
125
- print("βœ… Model loaded successfully")
126
- else:
127
- print("⚠️ No model weights found. Using untrained model for demo.")
128
-
129
- def extract_video_frames(video_path, max_frames=8, resize=(224, 224)):
130
- """Extract frames from video file"""
 
131
  cap = cv2.VideoCapture(video_path)
 
132
  if not cap.isOpened():
133
  return None
134
-
 
 
 
 
 
 
 
 
 
 
 
135
  frames = []
136
- while len(frames) < max_frames:
 
 
 
 
137
  ret, frame = cap.read()
 
138
  if not ret:
139
- break
140
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
 
 
 
 
 
141
  frame = cv2.resize(frame, resize)
 
142
  frames.append(frame)
143
-
144
  cap.release()
145
-
146
  if len(frames) == 0:
147
  return None
148
-
149
- # Pad if needed
150
  while len(frames) < max_frames:
151
  frames.append(frames[-1])
152
-
153
- frames = np.array(frames[:max_frames], dtype=np.uint8)
 
154
  return frames
155
 
 
 
 
 
156
  def extract_audio_from_video(video_path):
157
- """Extract audio from video file using ffmpeg"""
158
  try:
159
- audio_path = tempfile.NamedTemporaryFile(delete=False, suffix=".wav").name
160
- # Use ffmpeg to extract audio
161
- import subprocess
 
 
 
162
  command = [
163
- 'ffmpeg', '-i', video_path,
164
- '-vn', # No video
165
- '-acodec', 'pcm_s16le', # Audio codec
166
- '-ar', str(SAMPLE_RATE), # Sample rate
167
- '-ac', '1', # Mono
168
- '-y', # Overwrite
 
169
  audio_path
170
  ]
171
- subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
 
 
 
 
 
 
 
172
  return audio_path
 
173
  except Exception as e:
174
- raise ValueError(f"Could not extract audio from video: {str(e)}")
 
 
 
 
 
 
 
 
 
175
 
176
  def transcribe_audio(audio_path):
177
- """Transcribe audio using Whisper"""
178
  try:
179
- whisper_model = whisper.load_model("base")
180
  result = whisper_model.transcribe(audio_path)
 
181
  return result["text"].strip()
 
182
  except Exception as e:
183
- raise ValueError(f"Could not transcribe audio: {str(e)}")
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- def preprocess_inputs(audio_path, text, video_path):
187
- """Preprocess all three modalities"""
188
-
189
- # Audio
190
- wav, _ = librosa.load(audio_path, sr=SAMPLE_RATE)
191
- audio_inputs = processor(wav, sampling_rate=SAMPLE_RATE, return_tensors="pt")
192
  audio_values = audio_inputs.input_values.to(DEVICE)
193
- audio_mask = torch.ones_like(audio_values).to(DEVICE)
194
-
195
- # Text
196
- text_clean = re.sub(r"[^a-zA-Z0-9\s]", "", text.lower())
 
 
 
 
 
 
 
 
197
  text_inputs = tokenizer(
198
  text_clean,
199
  truncation=True,
@@ -201,110 +411,322 @@ def preprocess_inputs(audio_path, text, video_path):
201
  max_length=TEXT_MAX_LEN,
202
  return_tensors="pt"
203
  )
 
204
  text_ids = text_inputs.input_ids.to(DEVICE)
 
205
  text_mask = text_inputs.attention_mask.to(DEVICE)
206
-
207
- # Video
208
  frames = extract_video_frames(video_path)
 
209
  if frames is None:
210
- raise ValueError("Could not extract frames from video")
211
-
212
- frames_tensor = torch.tensor(frames).permute(0, 3, 1, 2).float() / 255.0
213
- frames_tensor = frames_tensor.unsqueeze(0).permute(0, 2, 1, 3, 4).to(DEVICE)
214
-
215
- return audio_values, audio_mask, text_ids, text_mask, frames_tensor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  def predict_emotion(video_file):
218
- """Main prediction function - takes only video input"""
219
-
220
  if video_file is None:
221
- return "Please provide a video file", None, ""
222
-
 
 
 
 
 
223
  try:
224
- # Extract audio from video
225
- audio_path = extract_audio_from_video(video_file)
226
-
227
- # Transcribe audio
228
- transcribed_text = transcribe_audio(audio_path)
229
-
230
- # Preprocess all modalities
231
- audio, audio_mask, text_ids, text_mask, video = preprocess_inputs(
232
- audio_path, transcribed_text, video_file
233
  )
234
-
235
- # Inference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  with torch.no_grad():
237
- fused_logits, a_logits, t_logits, v_logits = model(
238
- audio, audio_mask, text_ids, text_mask, video
 
 
 
 
 
239
  )
240
-
241
- # Get probabilities
242
- fused_probs = torch.softmax(fused_logits, dim=1)[0].cpu().numpy()
243
-
244
- # Format results
245
- result = {LABELS[i]: float(fused_probs[i]) for i in range(len(LABELS))}
246
-
247
- predicted_emotion = LABELS[fused_probs.argmax()]
248
- confidence = float(fused_probs.max())
249
-
250
- result_text = f"🎯 **Predicted Emotion: {predicted_emotion.upper()}**\n\n**Confidence: {confidence:.2%}**"
251
-
252
- # Clean up temporary audio file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  if os.path.exists(audio_path):
254
  os.remove(audio_path)
255
-
256
- return result_text, result, transcribed_text
257
-
 
 
 
 
258
  except Exception as e:
259
- return f"Error: {str(e)}", None, ""
260
-
261
- # Gradio Interface
262
- with gr.Blocks(title="Multimodal Emotion Recognition", theme=gr.themes.Soft()) as demo:
263
- gr.Markdown(
264
- """
265
- # 🎭 Multimodal Emotion Recognition
266
-
267
- This system predicts emotions from video by automatically extracting and analyzing:
268
- - 🎀 **Audio** (extracted from video)
269
- - πŸ“ **Text** (transcribed from audio using Whisper)
270
- - πŸŽ₯ **Video** (visual frames)
271
-
272
- ### How to use:
273
- 1. Upload a video file (MP4, AVI, MOV, etc.)
274
- 2. Click "Predict Emotion"
275
- 3. The system will automatically extract audio, transcribe speech, and analyze all modalities
276
-
277
- The model will provide emotion predictions based on all three inputs.
278
- """
279
- )
280
 
281
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  with gr.Row():
 
283
  with gr.Column():
284
- video_input = gr.Video(label="πŸŽ₯ Video Input")
285
- predict_btn = gr.Button("πŸš€ Predict Emotion", variant="primary", size="lg")
286
-
 
 
 
 
 
 
287
  with gr.Column():
288
- result_text = gr.Markdown(label="Result")
289
- result_output = gr.Label(label="πŸ“Š Prediction Results", num_top_classes=4)
290
- transcription_output = gr.Textbox(label="πŸ“ Transcribed Text", lines=3, interactive=False)
291
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  predict_btn.click(
293
  fn=predict_emotion,
294
  inputs=[video_input],
295
- outputs=[result_text, result_output, transcription_output]
 
 
 
 
 
296
  )
297
 
298
-
299
- gr.Markdown(
300
- """
301
- ---
302
- ### πŸ“Œ Notes:
303
- - Supported emotions: **Angry, Happy, Neutral, Sad**
304
- - Model uses Wav2Vec2 (audio), BERT (text), and ResNet18 (video)
305
- - Best results with clear audio, accurate transcripts, and visible faces
306
- """
307
- )
308
 
309
  if __name__ == "__main__":
310
- demo.launch()
 
 
 
 
5
  import librosa
6
  import cv2
7
  import re
8
+ from transformers import (
9
+ Wav2Vec2Processor,
10
+ Wav2Vec2Model,
11
+ AutoTokenizer,
12
+ AutoModel
13
+ )
14
  from torchvision import models
15
  import tempfile
16
  import os
 
18
  import whisper
19
  import subprocess
20
 
21
+ # =========================================================
22
+ # CONFIGURATION
23
+ # =========================================================
24
+
25
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
26
  SAMPLE_RATE = 16000
27
  TEXT_MAX_LEN = 64
28
  LABELS = ["angry", "happy", "neutral", "sad"]
29
 
30
+ gpu_status = "🟒 GPU Enabled" if torch.cuda.is_available() else "πŸ”΄ CPU Mode"
31
+
32
+ # =========================================================
33
+ # LOAD PROCESSORS
34
+ # =========================================================
35
+
36
+ processor = Wav2Vec2Processor.from_pretrained(
37
+ "facebook/wav2vec2-base-960h"
38
+ )
39
+
40
+ tokenizer = AutoTokenizer.from_pretrained(
41
+ "bert-base-uncased"
42
+ )
43
+
44
+ # =========================================================
45
+ # MODEL ARCHITECTURE
46
+ # =========================================================
47
 
 
48
  class ResNetVideoEncoder(nn.Module):
49
  def __init__(self, out_dim=768):
50
  super().__init__()
51
+
52
  base = models.resnet18(pretrained=False)
53
+
54
+ self.backbone = nn.Sequential(
55
+ *list(base.children())[:-1]
56
+ )
57
+
58
  self.proj = nn.Linear(512, out_dim)
59
 
60
  def forward(self, x):
61
+
62
  B, C, T, H, W = x.shape
63
+
64
  feats = []
65
+
66
  for t in range(T):
67
+
68
  ft = self.backbone(x[:, :, t])
69
+
70
+ feats.append(
71
+ ft.squeeze(-1).squeeze(-1)
72
+ )
73
+
74
  feats = torch.stack(feats, dim=1).mean(1)
75
+
76
  return self.proj(feats)
77
 
78
+ # =========================================================
79
+
80
  def mean_pool(x, mask):
81
+
82
  mask = mask[:, :x.size(1)]
83
+
84
  mask = mask.unsqueeze(-1).float()
85
+
86
+ return (
87
+ (x * mask).sum(1)
88
+ / mask.sum(1).clamp(min=1e-6)
89
+ )
90
+
91
+ # =========================================================
92
 
93
  class HBF(nn.Module):
94
+
95
  def __init__(self, d=768, n_layers=6):
96
+
97
  super().__init__()
98
+
99
+ self.proj_a = nn.ModuleList(
100
+ [nn.Linear(d, d) for _ in range(n_layers)]
101
+ )
102
+
103
+ self.proj_t = nn.ModuleList(
104
+ [nn.Linear(d, d) for _ in range(n_layers)]
105
+ )
106
+
107
+ self.proj_v = nn.ModuleList(
108
+ [nn.Linear(d, d) for _ in range(n_layers)]
109
+ )
110
+
111
+ self.fwd1 = nn.ModuleList(
112
+ [nn.Linear(3*d, d) for _ in range(n_layers)]
113
+ )
114
+
115
+ self.fwd2 = nn.ModuleList(
116
+ [nn.Linear(d, d) for _ in range(n_layers)]
117
+ )
118
+
119
  self.drop = nn.Dropout(0.1)
120
+
121
+ self.act1 = nn.GELU()
122
+ self.act2 = nn.Tanh()
123
+
124
  self.n = n_layers
125
 
126
  def forward(self, a, t, v):
127
+
128
  v_prev = None
129
+
130
  for i in range(self.n):
131
+
132
+ va = self.act2(
133
+ self.drop(self.proj_a[i](a))
134
+ )
135
+
136
+ vt = self.act2(
137
+ self.drop(self.proj_t[i](t))
138
+ )
139
+
140
+ vv = self.act2(
141
+ self.drop(self.proj_v[i](v))
142
+ )
143
+
144
+ cat = torch.cat(
145
+ [va, vt, vv]
146
+ if v_prev is None
147
+ else [va, vt, v_prev],
148
+ -1
149
+ )
150
+
151
  x = self.act1(self.fwd1[i](cat))
152
+
153
  v_prev = self.fwd2[i](x)
154
+
155
  return v_prev
156
 
157
+ # =========================================================
158
+
159
  class AVVideoModel(nn.Module):
160
+
161
  def __init__(self, num_classes, n_layers=6):
162
+
163
  super().__init__()
164
+
165
+ self.a_enc = Wav2Vec2Model.from_pretrained(
166
+ "facebook/wav2vec2-base-960h"
167
+ )
168
+
169
+ self.t_enc = AutoModel.from_pretrained(
170
+ "bert-base-uncased"
171
+ )
172
+
173
  self.v_enc = ResNetVideoEncoder()
174
+
175
  self.hbf = HBF(n_layers=n_layers)
176
+
177
  self.fc = nn.Linear(768, num_classes)
178
+
179
  self.fc_audio = nn.Linear(768, num_classes)
180
  self.fc_text = nn.Linear(768, num_classes)
181
  self.fc_video = nn.Linear(768, num_classes)
182
 
183
+ def forward(
184
+ self,
185
+ audio,
186
+ audio_mask,
187
+ text_ids,
188
+ text_mask,
189
+ video
190
+ ):
191
+
192
+ a_out = self.a_enc(
193
+ audio,
194
+ attention_mask=audio_mask,
195
+ return_dict=True
196
+ )
197
+
198
+ t_out = self.t_enc(
199
+ input_ids=text_ids,
200
+ attention_mask=text_mask,
201
+ return_dict=True
202
+ )
203
+
204
+ a_pool = mean_pool(
205
+ a_out.last_hidden_state,
206
+ audio_mask
207
+ )
208
+
209
+ t_pool = mean_pool(
210
+ t_out.last_hidden_state,
211
+ text_mask
212
+ )
213
+
214
  v_pool = self.v_enc(video)
215
+
216
+ a_pool = torch.nan_to_num(a_pool)
217
+ t_pool = torch.nan_to_num(t_pool)
218
+ v_pool = torch.nan_to_num(v_pool)
219
+
 
 
 
 
220
  fused = self.hbf(a_pool, t_pool, v_pool)
221
+
222
+ fused = torch.nan_to_num(fused)
223
+
224
  fused_logits = self.fc(fused)
 
 
225
 
226
+ return fused_logits
227
+
228
+ # =========================================================
229
+ # LOAD MODEL
230
+ # =========================================================
231
 
232
+ model = AVVideoModel(
233
+ num_classes=len(LABELS)
234
+ ).to(DEVICE)
235
 
 
236
  try:
237
+
238
  model_path = hf_hub_download(
239
+ repo_id="ApurvaKondekar/emotion_model",
240
+ filename="model_weights.pth"
241
  )
242
+
243
+ model.load_state_dict(
244
+ torch.load(model_path, map_location=DEVICE)
245
+ )
246
+
247
  model.eval()
248
+
249
+ print("βœ… Model loaded successfully")
250
+
251
  except Exception as e:
252
+
253
+ print(f"❌ Error loading model: {e}")
254
+
255
+ # =========================================================
256
+ # VIDEO PROCESSING
257
+ # =========================================================
258
+
259
+ def extract_video_frames(
260
+ video_path,
261
+ max_frames=8,
262
+ resize=(224, 224)
263
+ ):
264
+
265
  cap = cv2.VideoCapture(video_path)
266
+
267
  if not cap.isOpened():
268
  return None
269
+
270
+ total_frames = int(
271
+ cap.get(cv2.CAP_PROP_FRAME_COUNT)
272
+ )
273
+
274
+ indices = np.linspace(
275
+ 0,
276
+ total_frames - 1,
277
+ max_frames,
278
+ dtype=int
279
+ )
280
+
281
  frames = []
282
+
283
+ for idx in indices:
284
+
285
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
286
+
287
  ret, frame = cap.read()
288
+
289
  if not ret:
290
+ continue
291
+
292
+ frame = cv2.cvtColor(
293
+ frame,
294
+ cv2.COLOR_BGR2RGB
295
+ )
296
+
297
  frame = cv2.resize(frame, resize)
298
+
299
  frames.append(frame)
300
+
301
  cap.release()
302
+
303
  if len(frames) == 0:
304
  return None
305
+
 
306
  while len(frames) < max_frames:
307
  frames.append(frames[-1])
308
+
309
+ frames = np.array(frames[:max_frames])
310
+
311
  return frames
312
 
313
+ # =========================================================
314
+ # AUDIO EXTRACTION
315
+ # =========================================================
316
+
317
  def extract_audio_from_video(video_path):
318
+
319
  try:
320
+
321
+ audio_path = tempfile.NamedTemporaryFile(
322
+ delete=False,
323
+ suffix=".wav"
324
+ ).name
325
+
326
  command = [
327
+ "ffmpeg",
328
+ "-i", video_path,
329
+ "-vn",
330
+ "-acodec", "pcm_s16le",
331
+ "-ar", str(SAMPLE_RATE),
332
+ "-ac", "1",
333
+ "-y",
334
  audio_path
335
  ]
336
+
337
+ subprocess.run(
338
+ command,
339
+ stdout=subprocess.DEVNULL,
340
+ stderr=subprocess.DEVNULL,
341
+ check=True
342
+ )
343
+
344
  return audio_path
345
+
346
  except Exception as e:
347
+
348
+ raise ValueError(
349
+ f"Audio extraction failed: {str(e)}"
350
+ )
351
+
352
+ # =========================================================
353
+ # TRANSCRIPTION
354
+ # =========================================================
355
+
356
+ whisper_model = whisper.load_model("base")
357
 
358
  def transcribe_audio(audio_path):
359
+
360
  try:
361
+
362
  result = whisper_model.transcribe(audio_path)
363
+
364
  return result["text"].strip()
365
+
366
  except Exception as e:
 
367
 
368
+ raise ValueError(
369
+ f"Transcription failed: {str(e)}"
370
+ )
371
+
372
+ # =========================================================
373
+ # PREPROCESSING
374
+ # =========================================================
375
+
376
+ def preprocess_inputs(
377
+ audio_path,
378
+ text,
379
+ video_path
380
+ ):
381
+
382
+ # AUDIO
383
+ wav, _ = librosa.load(
384
+ audio_path,
385
+ sr=SAMPLE_RATE
386
+ )
387
+
388
+ audio_inputs = processor(
389
+ wav,
390
+ sampling_rate=SAMPLE_RATE,
391
+ return_tensors="pt"
392
+ )
393
 
 
 
 
 
 
 
394
  audio_values = audio_inputs.input_values.to(DEVICE)
395
+
396
+ audio_mask = torch.ones_like(
397
+ audio_values
398
+ ).to(DEVICE)
399
+
400
+ # TEXT
401
+ text_clean = re.sub(
402
+ r"[^a-zA-Z0-9\s]",
403
+ "",
404
+ text.lower()
405
+ )
406
+
407
  text_inputs = tokenizer(
408
  text_clean,
409
  truncation=True,
 
411
  max_length=TEXT_MAX_LEN,
412
  return_tensors="pt"
413
  )
414
+
415
  text_ids = text_inputs.input_ids.to(DEVICE)
416
+
417
  text_mask = text_inputs.attention_mask.to(DEVICE)
418
+
419
+ # VIDEO
420
  frames = extract_video_frames(video_path)
421
+
422
  if frames is None:
423
+ raise ValueError(
424
+ "Could not extract video frames"
425
+ )
426
+
427
+ frames_tensor = (
428
+ torch.tensor(frames)
429
+ .permute(0, 3, 1, 2)
430
+ .float() / 255.0
431
+ )
432
+
433
+ frames_tensor = (
434
+ frames_tensor
435
+ .unsqueeze(0)
436
+ .permute(0, 2, 1, 3, 4)
437
+ .to(DEVICE)
438
+ )
439
+
440
+ return (
441
+ audio_values,
442
+ audio_mask,
443
+ text_ids,
444
+ text_mask,
445
+ frames_tensor
446
+ )
447
+
448
+ # =========================================================
449
+ # PREDICTION FUNCTION
450
+ # =========================================================
451
 
452
  def predict_emotion(video_file):
453
+
 
454
  if video_file is None:
455
+
456
+ return (
457
+ "❌ Please upload a video",
458
+ None,
459
+ ""
460
+ )
461
+
462
  try:
463
+
464
+ # EXTRACT AUDIO
465
+ audio_path = extract_audio_from_video(
466
+ video_file
 
 
 
 
 
467
  )
468
+
469
+ # TRANSCRIBE
470
+ transcribed_text = transcribe_audio(
471
+ audio_path
472
+ )
473
+
474
+ # PREPROCESS
475
+ (
476
+ audio,
477
+ audio_mask,
478
+ text_ids,
479
+ text_mask,
480
+ video
481
+ ) = preprocess_inputs(
482
+ audio_path,
483
+ transcribed_text,
484
+ video_file
485
+ )
486
+
487
+ # INFERENCE
488
  with torch.no_grad():
489
+
490
+ logits = model(
491
+ audio,
492
+ audio_mask,
493
+ text_ids,
494
+ text_mask,
495
+ video
496
  )
497
+
498
+ probs = torch.softmax(
499
+ logits,
500
+ dim=1
501
+ )[0].cpu().numpy()
502
+
503
+ result = {
504
+ LABELS[i]: float(probs[i])
505
+ for i in range(len(LABELS))
506
+ }
507
+
508
+ predicted_emotion = LABELS[probs.argmax()]
509
+
510
+ confidence = float(probs.max())
511
+
512
+ emoji_map = {
513
+ "happy": "πŸ˜„",
514
+ "sad": "😒",
515
+ "angry": "😠",
516
+ "neutral": "😐"
517
+ }
518
+
519
+ result_text = f"""
520
+ # {emoji_map[predicted_emotion]} {predicted_emotion.upper()}
521
+
522
+ ## Confidence Score
523
+ ### {confidence:.2%}
524
+ """
525
+
526
+ # CLEANUP
527
  if os.path.exists(audio_path):
528
  os.remove(audio_path)
529
+
530
+ return (
531
+ result_text,
532
+ result,
533
+ transcribed_text
534
+ )
535
+
536
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
 
538
+ return (
539
+ f"❌ Error: {str(e)}",
540
+ None,
541
+ ""
542
+ )
543
+
544
+ # =========================================================
545
+ # CUSTOM CSS
546
+ # =========================================================
547
+
548
+ custom_css = """
549
+
550
+ body {
551
+ background: linear-gradient(
552
+ to right,
553
+ #0f172a,
554
+ #1e293b
555
+ );
556
+ }
557
+
558
+ .gradio-container {
559
+ max-width: 1200px !important;
560
+ margin: auto;
561
+ }
562
+
563
+ .main-title {
564
+ text-align: center;
565
+ font-size: 48px;
566
+ font-weight: 800;
567
+ color: white;
568
+ margin-top: 20px;
569
+ }
570
+
571
+ .subtitle {
572
+ text-align: center;
573
+ font-size: 18px;
574
+ color: #cbd5e1;
575
+ margin-bottom: 25px;
576
+ }
577
+
578
+ .footer {
579
+ text-align: center;
580
+ color: #94a3b8;
581
+ margin-top: 30px;
582
+ font-size: 14px;
583
+ }
584
+ """
585
+
586
+ # =========================================================
587
+ # UI
588
+ # =========================================================
589
+
590
+ with gr.Blocks(
591
+ theme=gr.themes.Glass(),
592
+ css=custom_css,
593
+ title="Emotion Recognition"
594
+ ) as demo:
595
+
596
+ # HEADER
597
+ gr.HTML("""
598
+ <div class="main-title">
599
+ 🎭 Multimodal Emotion Recognition
600
+ </div>
601
+
602
+ <div class="subtitle">
603
+ AI-powered Emotion Detection using Audio, Text & Video Fusion
604
+ </div>
605
+ """)
606
+
607
+ gr.Markdown(f"### {gpu_status}")
608
+
609
+ # INFO SECTION
610
  with gr.Row():
611
+
612
  with gr.Column():
613
+
614
+ gr.Markdown("""
615
+ ### πŸ” Modalities
616
+
617
+ - 🎀 Audio
618
+ - πŸ“ Text
619
+ - πŸŽ₯ Video
620
+ """)
621
+
622
  with gr.Column():
623
+
624
+ gr.Markdown("""
625
+ ### πŸ€– Models Used
626
+
627
+ - Wav2Vec2
628
+ - BERT
629
+ - ResNet18
630
+ - Whisper
631
+ """)
632
+
633
+ gr.Markdown("---")
634
+
635
+ # MAIN SECTION
636
+ with gr.Row(equal_height=True):
637
+
638
+ # LEFT SIDE
639
+ with gr.Column(scale=1):
640
+
641
+ gr.Markdown("## πŸ“€ Upload Video")
642
+
643
+ video_input = gr.Video(
644
+ label="Input Video",
645
+ height=350
646
+ )
647
+
648
+ predict_btn = gr.Button(
649
+ "πŸš€ Analyze Emotion",
650
+ variant="primary",
651
+ size="lg"
652
+ )
653
+
654
+ # RIGHT SIDE
655
+ with gr.Column(scale=1):
656
+
657
+ gr.Markdown("## πŸ“Š Results")
658
+
659
+ result_text = gr.Markdown(
660
+ value="Upload a video to begin analysis"
661
+ )
662
+
663
+ result_output = gr.Label(
664
+ label="Emotion Probabilities",
665
+ num_top_classes=4
666
+ )
667
+
668
+ transcription_output = gr.Textbox(
669
+ label="πŸ“ Transcribed Text",
670
+ lines=5,
671
+ interactive=False
672
+ )
673
+
674
+ gr.Markdown("---")
675
+
676
+ # ABOUT MODEL
677
+ with gr.Accordion(
678
+ "ℹ️ About This Model",
679
+ open=False
680
+ ):
681
+
682
+ gr.Markdown("""
683
+ This system combines:
684
+
685
+ ### 🎀 Audio Analysis
686
+ Wav2Vec2 captures emotional tone and speech patterns.
687
+
688
+ ### πŸ“ Text Analysis
689
+ BERT analyzes semantic meaning from transcripts.
690
+
691
+ ### πŸŽ₯ Video Analysis
692
+ ResNet18 extracts facial expression features.
693
+
694
+ ### 🧠 Fusion Network
695
+ HBF combines all modalities for final prediction.
696
+
697
+ ---
698
+ Supported Emotions:
699
+ - Angry
700
+ - Happy
701
+ - Neutral
702
+ - Sad
703
+ """)
704
+
705
+ # FOOTER
706
+ gr.HTML("""
707
+ <div class="footer">
708
+ Built with ❀️ using PyTorch, Transformers, Whisper & Gradio
709
+ </div>
710
+ """)
711
+
712
+ # BUTTON ACTION
713
  predict_btn.click(
714
  fn=predict_emotion,
715
  inputs=[video_input],
716
+ outputs=[
717
+ result_text,
718
+ result_output,
719
+ transcription_output
720
+ ],
721
+ show_progress=True
722
  )
723
 
724
+ # =========================================================
725
+ # LAUNCH
726
+ # =========================================================
 
 
 
 
 
 
 
727
 
728
  if __name__ == "__main__":
729
+
730
+ demo.queue()
731
+
732
+ demo.launch()