new app.py
#3
by ApurvaKondekar - opened
app.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import torch
|
| 3 |
import torch.nn as nn
|
|
@@ -23,11 +28,18 @@ import subprocess
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# =========================================================
|
| 33 |
# LOAD PROCESSORS
|
|
@@ -46,7 +58,9 @@ tokenizer = AutoTokenizer.from_pretrained(
|
|
| 46 |
# =========================================================
|
| 47 |
|
| 48 |
class ResNetVideoEncoder(nn.Module):
|
|
|
|
| 49 |
def __init__(self, out_dim=768):
|
|
|
|
| 50 |
super().__init__()
|
| 51 |
|
| 52 |
base = models.resnet18(pretrained=False)
|
|
@@ -119,6 +133,7 @@ class HBF(nn.Module):
|
|
| 119 |
self.drop = nn.Dropout(0.1)
|
| 120 |
|
| 121 |
self.act1 = nn.GELU()
|
|
|
|
| 122 |
self.act2 = nn.Tanh()
|
| 123 |
|
| 124 |
self.n = n_layers
|
|
@@ -176,10 +191,6 @@ class AVVideoModel(nn.Module):
|
|
| 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,
|
|
@@ -213,17 +224,15 @@ class AVVideoModel(nn.Module):
|
|
| 213 |
|
| 214 |
v_pool = self.v_enc(video)
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
fused = torch.nan_to_num(fused)
|
| 223 |
|
| 224 |
-
|
| 225 |
|
| 226 |
-
return
|
| 227 |
|
| 228 |
# =========================================================
|
| 229 |
# LOAD MODEL
|
|
@@ -246,11 +255,11 @@ try:
|
|
| 246 |
|
| 247 |
model.eval()
|
| 248 |
|
| 249 |
-
print("
|
| 250 |
|
| 251 |
except Exception as e:
|
| 252 |
|
| 253 |
-
print(f"
|
| 254 |
|
| 255 |
# =========================================================
|
| 256 |
# VIDEO PROCESSING
|
|
@@ -316,58 +325,42 @@ def extract_video_frames(
|
|
| 316 |
|
| 317 |
def extract_audio_from_video(video_path):
|
| 318 |
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
subprocess.
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
check=True
|
| 342 |
-
)
|
| 343 |
-
|
| 344 |
-
return audio_path
|
| 345 |
-
|
| 346 |
-
except Exception as e:
|
| 347 |
|
| 348 |
-
|
| 349 |
-
f"Audio extraction failed: {str(e)}"
|
| 350 |
-
)
|
| 351 |
|
| 352 |
# =========================================================
|
| 353 |
-
#
|
| 354 |
# =========================================================
|
| 355 |
|
| 356 |
whisper_model = whisper.load_model("base")
|
| 357 |
|
| 358 |
def transcribe_audio(audio_path):
|
| 359 |
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
result = whisper_model.transcribe(audio_path)
|
| 363 |
-
|
| 364 |
-
return result["text"].strip()
|
| 365 |
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
raise ValueError(
|
| 369 |
-
f"Transcription failed: {str(e)}"
|
| 370 |
-
)
|
| 371 |
|
| 372 |
# =========================================================
|
| 373 |
# PREPROCESSING
|
|
@@ -379,7 +372,6 @@ def preprocess_inputs(
|
|
| 379 |
video_path
|
| 380 |
):
|
| 381 |
|
| 382 |
-
# AUDIO
|
| 383 |
wav, _ = librosa.load(
|
| 384 |
audio_path,
|
| 385 |
sr=SAMPLE_RATE
|
|
@@ -397,7 +389,6 @@ def preprocess_inputs(
|
|
| 397 |
audio_values
|
| 398 |
).to(DEVICE)
|
| 399 |
|
| 400 |
-
# TEXT
|
| 401 |
text_clean = re.sub(
|
| 402 |
r"[^a-zA-Z0-9\s]",
|
| 403 |
"",
|
|
@@ -416,14 +407,8 @@ def preprocess_inputs(
|
|
| 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)
|
|
@@ -446,7 +431,7 @@ def preprocess_inputs(
|
|
| 446 |
)
|
| 447 |
|
| 448 |
# =========================================================
|
| 449 |
-
# PREDICTION
|
| 450 |
# =========================================================
|
| 451 |
|
| 452 |
def predict_emotion(video_file):
|
|
@@ -454,24 +439,21 @@ def predict_emotion(video_file):
|
|
| 454 |
if video_file is None:
|
| 455 |
|
| 456 |
return (
|
| 457 |
-
"
|
| 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,
|
|
@@ -484,7 +466,6 @@ def predict_emotion(video_file):
|
|
| 484 |
video_file
|
| 485 |
)
|
| 486 |
|
| 487 |
-
# INFERENCE
|
| 488 |
with torch.no_grad():
|
| 489 |
|
| 490 |
logits = model(
|
|
@@ -505,25 +486,20 @@ def predict_emotion(video_file):
|
|
| 505 |
for i in range(len(LABELS))
|
| 506 |
}
|
| 507 |
|
| 508 |
-
predicted_emotion = LABELS[
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
-
## Confidence Score
|
| 523 |
-
### {confidence:.2%}
|
| 524 |
-
"""
|
| 525 |
-
|
| 526 |
-
# CLEANUP
|
| 527 |
if os.path.exists(audio_path):
|
| 528 |
os.remove(audio_path)
|
| 529 |
|
|
@@ -536,51 +512,75 @@ def predict_emotion(video_file):
|
|
| 536 |
except Exception as e:
|
| 537 |
|
| 538 |
return (
|
| 539 |
-
f"
|
| 540 |
None,
|
| 541 |
""
|
| 542 |
)
|
| 543 |
|
| 544 |
# =========================================================
|
| 545 |
-
#
|
| 546 |
# =========================================================
|
| 547 |
|
| 548 |
custom_css = """
|
| 549 |
|
| 550 |
body {
|
| 551 |
-
background:
|
| 552 |
-
to right,
|
| 553 |
-
#0f172a,
|
| 554 |
-
#1e293b
|
| 555 |
-
);
|
| 556 |
}
|
| 557 |
|
| 558 |
.gradio-container {
|
| 559 |
-
max-width:
|
| 560 |
margin: auto;
|
|
|
|
| 561 |
}
|
| 562 |
|
| 563 |
.main-title {
|
| 564 |
text-align: center;
|
| 565 |
font-size: 48px;
|
| 566 |
font-weight: 800;
|
| 567 |
-
color:
|
| 568 |
-
margin-
|
| 569 |
}
|
| 570 |
|
| 571 |
.subtitle {
|
| 572 |
text-align: center;
|
| 573 |
-
font-size:
|
| 574 |
-
color: #
|
| 575 |
-
margin-bottom:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
}
|
| 577 |
|
| 578 |
.footer {
|
| 579 |
text-align: center;
|
| 580 |
-
color: #
|
| 581 |
margin-top: 30px;
|
| 582 |
font-size: 14px;
|
| 583 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
"""
|
| 585 |
|
| 586 |
# =========================================================
|
|
@@ -588,76 +588,89 @@ body {
|
|
| 588 |
# =========================================================
|
| 589 |
|
| 590 |
with gr.Blocks(
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
) as demo:
|
| 595 |
|
| 596 |
# HEADER
|
|
|
|
| 597 |
gr.HTML("""
|
| 598 |
<div class="main-title">
|
| 599 |
-
|
| 600 |
</div>
|
| 601 |
|
| 602 |
<div class="subtitle">
|
| 603 |
-
AI-
|
| 604 |
</div>
|
| 605 |
""")
|
| 606 |
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
|
| 609 |
-
# INFO SECTION
|
| 610 |
with gr.Row():
|
| 611 |
|
| 612 |
with gr.Column():
|
| 613 |
|
| 614 |
-
gr.
|
| 615 |
-
### 🔍 Modalities
|
| 616 |
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
with gr.Column():
|
| 623 |
|
| 624 |
-
gr.
|
| 625 |
-
### 🤖 Models Used
|
| 626 |
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
gr.Markdown("---")
|
| 634 |
|
| 635 |
# MAIN SECTION
|
|
|
|
| 636 |
with gr.Row(equal_height=True):
|
| 637 |
|
| 638 |
-
# LEFT
|
|
|
|
| 639 |
with gr.Column(scale=1):
|
| 640 |
|
| 641 |
-
gr.Markdown("##
|
| 642 |
|
| 643 |
video_input = gr.Video(
|
| 644 |
label="Input Video",
|
| 645 |
-
height=
|
| 646 |
)
|
| 647 |
|
| 648 |
predict_btn = gr.Button(
|
| 649 |
-
"
|
| 650 |
variant="primary",
|
| 651 |
size="lg"
|
| 652 |
)
|
| 653 |
|
| 654 |
-
# RIGHT
|
|
|
|
| 655 |
with gr.Column(scale=1):
|
| 656 |
|
| 657 |
-
gr.Markdown("##
|
| 658 |
|
| 659 |
result_text = gr.Markdown(
|
| 660 |
-
value="Upload a video
|
| 661 |
)
|
| 662 |
|
| 663 |
result_output = gr.Label(
|
|
@@ -666,50 +679,46 @@ with gr.Blocks(
|
|
| 666 |
)
|
| 667 |
|
| 668 |
transcription_output = gr.Textbox(
|
| 669 |
-
label="
|
| 670 |
-
lines=
|
| 671 |
interactive=False
|
| 672 |
)
|
| 673 |
|
| 674 |
gr.Markdown("---")
|
| 675 |
|
| 676 |
-
# ABOUT
|
|
|
|
| 677 |
with gr.Accordion(
|
| 678 |
-
"
|
| 679 |
open=False
|
| 680 |
):
|
| 681 |
|
| 682 |
gr.Markdown("""
|
| 683 |
-
This system combines:
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 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
|
| 709 |
</div>
|
| 710 |
""")
|
| 711 |
|
| 712 |
# BUTTON ACTION
|
|
|
|
| 713 |
predict_btn.click(
|
| 714 |
fn=predict_emotion,
|
| 715 |
inputs=[video_input],
|
|
|
|
| 1 |
+
# =========================================================
|
| 2 |
+
# PROFESSIONAL MULTIMODAL EMOTION RECOGNITION UI
|
| 3 |
+
# Sky Blue + White Theme
|
| 4 |
+
# =========================================================
|
| 5 |
+
|
| 6 |
import gradio as gr
|
| 7 |
import torch
|
| 8 |
import torch.nn as nn
|
|
|
|
| 28 |
# =========================================================
|
| 29 |
|
| 30 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 31 |
+
|
| 32 |
SAMPLE_RATE = 16000
|
| 33 |
+
|
| 34 |
TEXT_MAX_LEN = 64
|
| 35 |
+
|
| 36 |
LABELS = ["angry", "happy", "neutral", "sad"]
|
| 37 |
|
| 38 |
+
gpu_status = (
|
| 39 |
+
"GPU Enabled"
|
| 40 |
+
if torch.cuda.is_available()
|
| 41 |
+
else "Running on CPU"
|
| 42 |
+
)
|
| 43 |
|
| 44 |
# =========================================================
|
| 45 |
# LOAD PROCESSORS
|
|
|
|
| 58 |
# =========================================================
|
| 59 |
|
| 60 |
class ResNetVideoEncoder(nn.Module):
|
| 61 |
+
|
| 62 |
def __init__(self, out_dim=768):
|
| 63 |
+
|
| 64 |
super().__init__()
|
| 65 |
|
| 66 |
base = models.resnet18(pretrained=False)
|
|
|
|
| 133 |
self.drop = nn.Dropout(0.1)
|
| 134 |
|
| 135 |
self.act1 = nn.GELU()
|
| 136 |
+
|
| 137 |
self.act2 = nn.Tanh()
|
| 138 |
|
| 139 |
self.n = n_layers
|
|
|
|
| 191 |
|
| 192 |
self.fc = nn.Linear(768, num_classes)
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
def forward(
|
| 195 |
self,
|
| 196 |
audio,
|
|
|
|
| 224 |
|
| 225 |
v_pool = self.v_enc(video)
|
| 226 |
|
| 227 |
+
fused = self.hbf(
|
| 228 |
+
a_pool,
|
| 229 |
+
t_pool,
|
| 230 |
+
v_pool
|
| 231 |
+
)
|
|
|
|
|
|
|
| 232 |
|
| 233 |
+
logits = self.fc(fused)
|
| 234 |
|
| 235 |
+
return logits
|
| 236 |
|
| 237 |
# =========================================================
|
| 238 |
# LOAD MODEL
|
|
|
|
| 255 |
|
| 256 |
model.eval()
|
| 257 |
|
| 258 |
+
print("Model loaded successfully")
|
| 259 |
|
| 260 |
except Exception as e:
|
| 261 |
|
| 262 |
+
print(f"Model loading failed: {e}")
|
| 263 |
|
| 264 |
# =========================================================
|
| 265 |
# VIDEO PROCESSING
|
|
|
|
| 325 |
|
| 326 |
def extract_audio_from_video(video_path):
|
| 327 |
|
| 328 |
+
audio_path = tempfile.NamedTemporaryFile(
|
| 329 |
+
delete=False,
|
| 330 |
+
suffix=".wav"
|
| 331 |
+
).name
|
| 332 |
+
|
| 333 |
+
command = [
|
| 334 |
+
"ffmpeg",
|
| 335 |
+
"-i", video_path,
|
| 336 |
+
"-vn",
|
| 337 |
+
"-acodec", "pcm_s16le",
|
| 338 |
+
"-ar", str(SAMPLE_RATE),
|
| 339 |
+
"-ac", "1",
|
| 340 |
+
"-y",
|
| 341 |
+
audio_path
|
| 342 |
+
]
|
| 343 |
+
|
| 344 |
+
subprocess.run(
|
| 345 |
+
command,
|
| 346 |
+
stdout=subprocess.DEVNULL,
|
| 347 |
+
stderr=subprocess.DEVNULL,
|
| 348 |
+
check=True
|
| 349 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
+
return audio_path
|
|
|
|
|
|
|
| 352 |
|
| 353 |
# =========================================================
|
| 354 |
+
# WHISPER
|
| 355 |
# =========================================================
|
| 356 |
|
| 357 |
whisper_model = whisper.load_model("base")
|
| 358 |
|
| 359 |
def transcribe_audio(audio_path):
|
| 360 |
|
| 361 |
+
result = whisper_model.transcribe(audio_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
+
return result["text"].strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
# =========================================================
|
| 366 |
# PREPROCESSING
|
|
|
|
| 372 |
video_path
|
| 373 |
):
|
| 374 |
|
|
|
|
| 375 |
wav, _ = librosa.load(
|
| 376 |
audio_path,
|
| 377 |
sr=SAMPLE_RATE
|
|
|
|
| 389 |
audio_values
|
| 390 |
).to(DEVICE)
|
| 391 |
|
|
|
|
| 392 |
text_clean = re.sub(
|
| 393 |
r"[^a-zA-Z0-9\s]",
|
| 394 |
"",
|
|
|
|
| 407 |
|
| 408 |
text_mask = text_inputs.attention_mask.to(DEVICE)
|
| 409 |
|
|
|
|
| 410 |
frames = extract_video_frames(video_path)
|
| 411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
frames_tensor = (
|
| 413 |
torch.tensor(frames)
|
| 414 |
.permute(0, 3, 1, 2)
|
|
|
|
| 431 |
)
|
| 432 |
|
| 433 |
# =========================================================
|
| 434 |
+
# PREDICTION
|
| 435 |
# =========================================================
|
| 436 |
|
| 437 |
def predict_emotion(video_file):
|
|
|
|
| 439 |
if video_file is None:
|
| 440 |
|
| 441 |
return (
|
| 442 |
+
"Please upload a video.",
|
| 443 |
None,
|
| 444 |
""
|
| 445 |
)
|
| 446 |
|
| 447 |
try:
|
| 448 |
|
|
|
|
| 449 |
audio_path = extract_audio_from_video(
|
| 450 |
video_file
|
| 451 |
)
|
| 452 |
|
|
|
|
| 453 |
transcribed_text = transcribe_audio(
|
| 454 |
audio_path
|
| 455 |
)
|
| 456 |
|
|
|
|
| 457 |
(
|
| 458 |
audio,
|
| 459 |
audio_mask,
|
|
|
|
| 466 |
video_file
|
| 467 |
)
|
| 468 |
|
|
|
|
| 469 |
with torch.no_grad():
|
| 470 |
|
| 471 |
logits = model(
|
|
|
|
| 486 |
for i in range(len(LABELS))
|
| 487 |
}
|
| 488 |
|
| 489 |
+
predicted_emotion = LABELS[
|
| 490 |
+
probs.argmax()
|
| 491 |
+
]
|
| 492 |
|
| 493 |
confidence = float(probs.max())
|
| 494 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
result_text = f"""
|
| 496 |
+
## Predicted Emotion
|
| 497 |
+
|
| 498 |
+
### {predicted_emotion.upper()}
|
| 499 |
+
|
| 500 |
+
Confidence Score: {confidence:.2%}
|
| 501 |
+
"""
|
| 502 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
if os.path.exists(audio_path):
|
| 504 |
os.remove(audio_path)
|
| 505 |
|
|
|
|
| 512 |
except Exception as e:
|
| 513 |
|
| 514 |
return (
|
| 515 |
+
f"Error: {str(e)}",
|
| 516 |
None,
|
| 517 |
""
|
| 518 |
)
|
| 519 |
|
| 520 |
# =========================================================
|
| 521 |
+
# CLEAN PROFESSIONAL CSS
|
| 522 |
# =========================================================
|
| 523 |
|
| 524 |
custom_css = """
|
| 525 |
|
| 526 |
body {
|
| 527 |
+
background: #eaf6ff;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
}
|
| 529 |
|
| 530 |
.gradio-container {
|
| 531 |
+
max-width: 1250px !important;
|
| 532 |
margin: auto;
|
| 533 |
+
padding-top: 20px;
|
| 534 |
}
|
| 535 |
|
| 536 |
.main-title {
|
| 537 |
text-align: center;
|
| 538 |
font-size: 48px;
|
| 539 |
font-weight: 800;
|
| 540 |
+
color: #0f4c81;
|
| 541 |
+
margin-bottom: 10px;
|
| 542 |
}
|
| 543 |
|
| 544 |
.subtitle {
|
| 545 |
text-align: center;
|
| 546 |
+
font-size: 20px;
|
| 547 |
+
color: #3b82b6;
|
| 548 |
+
margin-bottom: 30px;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.section-box {
|
| 552 |
+
background: white;
|
| 553 |
+
border-radius: 16px;
|
| 554 |
+
padding: 20px;
|
| 555 |
+
box-shadow: 0px 4px 12px rgba(0,0,0,0.08);
|
| 556 |
}
|
| 557 |
|
| 558 |
.footer {
|
| 559 |
text-align: center;
|
| 560 |
+
color: #4b5563;
|
| 561 |
margin-top: 30px;
|
| 562 |
font-size: 14px;
|
| 563 |
}
|
| 564 |
+
|
| 565 |
+
.gr-button {
|
| 566 |
+
background: #38bdf8 !important;
|
| 567 |
+
border: none !important;
|
| 568 |
+
color: white !important;
|
| 569 |
+
font-weight: 600 !important;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.gr-button:hover {
|
| 573 |
+
background: #0ea5e9 !important;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
h1, h2, h3, h4 {
|
| 577 |
+
color: #0f4c81 !important;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
label {
|
| 581 |
+
color: #0f4c81 !important;
|
| 582 |
+
font-weight: 600 !important;
|
| 583 |
+
}
|
| 584 |
"""
|
| 585 |
|
| 586 |
# =========================================================
|
|
|
|
| 588 |
# =========================================================
|
| 589 |
|
| 590 |
with gr.Blocks(
|
| 591 |
+
title="Multimodal Emotion Recognition",
|
| 592 |
+
theme=gr.themes.Soft(),
|
| 593 |
+
css=custom_css
|
| 594 |
) as demo:
|
| 595 |
|
| 596 |
# HEADER
|
| 597 |
+
|
| 598 |
gr.HTML("""
|
| 599 |
<div class="main-title">
|
| 600 |
+
Multimodal Emotion Recognition
|
| 601 |
</div>
|
| 602 |
|
| 603 |
<div class="subtitle">
|
| 604 |
+
AI-based Emotion Detection using Audio, Text and Video Fusion
|
| 605 |
</div>
|
| 606 |
""")
|
| 607 |
|
| 608 |
+
# STATUS
|
| 609 |
+
|
| 610 |
+
gr.Markdown(
|
| 611 |
+
f"### System Status: {gpu_status}"
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
# INFO PANELS
|
| 615 |
|
|
|
|
| 616 |
with gr.Row():
|
| 617 |
|
| 618 |
with gr.Column():
|
| 619 |
|
| 620 |
+
with gr.Group():
|
|
|
|
| 621 |
|
| 622 |
+
gr.Markdown("""
|
| 623 |
+
### Modalities Used
|
| 624 |
+
|
| 625 |
+
- Audio Analysis
|
| 626 |
+
- Speech Transcription
|
| 627 |
+
- Facial Expression Analysis
|
| 628 |
+
""")
|
| 629 |
|
| 630 |
with gr.Column():
|
| 631 |
|
| 632 |
+
with gr.Group():
|
|
|
|
| 633 |
|
| 634 |
+
gr.Markdown("""
|
| 635 |
+
### Models Used
|
| 636 |
+
|
| 637 |
+
- Wav2Vec2
|
| 638 |
+
- BERT
|
| 639 |
+
- ResNet18
|
| 640 |
+
- Whisper
|
| 641 |
+
""")
|
| 642 |
|
| 643 |
gr.Markdown("---")
|
| 644 |
|
| 645 |
# MAIN SECTION
|
| 646 |
+
|
| 647 |
with gr.Row(equal_height=True):
|
| 648 |
|
| 649 |
+
# LEFT
|
| 650 |
+
|
| 651 |
with gr.Column(scale=1):
|
| 652 |
|
| 653 |
+
gr.Markdown("## Upload Video")
|
| 654 |
|
| 655 |
video_input = gr.Video(
|
| 656 |
label="Input Video",
|
| 657 |
+
height=400
|
| 658 |
)
|
| 659 |
|
| 660 |
predict_btn = gr.Button(
|
| 661 |
+
"Analyze Emotion",
|
| 662 |
variant="primary",
|
| 663 |
size="lg"
|
| 664 |
)
|
| 665 |
|
| 666 |
+
# RIGHT
|
| 667 |
+
|
| 668 |
with gr.Column(scale=1):
|
| 669 |
|
| 670 |
+
gr.Markdown("## Results")
|
| 671 |
|
| 672 |
result_text = gr.Markdown(
|
| 673 |
+
value="Upload a video and click Analyze Emotion."
|
| 674 |
)
|
| 675 |
|
| 676 |
result_output = gr.Label(
|
|
|
|
| 679 |
)
|
| 680 |
|
| 681 |
transcription_output = gr.Textbox(
|
| 682 |
+
label="Transcribed Text",
|
| 683 |
+
lines=6,
|
| 684 |
interactive=False
|
| 685 |
)
|
| 686 |
|
| 687 |
gr.Markdown("---")
|
| 688 |
|
| 689 |
+
# ABOUT SECTION
|
| 690 |
+
|
| 691 |
with gr.Accordion(
|
| 692 |
+
"About the Model",
|
| 693 |
open=False
|
| 694 |
):
|
| 695 |
|
| 696 |
gr.Markdown("""
|
| 697 |
+
This multimodal system combines:
|
| 698 |
+
|
| 699 |
+
- Audio features using Wav2Vec2
|
| 700 |
+
- Text understanding using BERT
|
| 701 |
+
- Video feature extraction using ResNet18
|
| 702 |
+
- Hybrid Fusion Block for final prediction
|
| 703 |
+
|
| 704 |
+
Supported emotions:
|
| 705 |
+
|
| 706 |
+
- Angry
|
| 707 |
+
- Happy
|
| 708 |
+
- Neutral
|
| 709 |
+
- Sad
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
""")
|
| 711 |
|
| 712 |
# FOOTER
|
| 713 |
+
|
| 714 |
gr.HTML("""
|
| 715 |
<div class="footer">
|
| 716 |
+
Built using PyTorch, Transformers, Whisper and Gradio
|
| 717 |
</div>
|
| 718 |
""")
|
| 719 |
|
| 720 |
# BUTTON ACTION
|
| 721 |
+
|
| 722 |
predict_btn.click(
|
| 723 |
fn=predict_emotion,
|
| 724 |
inputs=[video_input],
|