ipetrousov commited on
Commit
c9966f3
·
verified ·
1 Parent(s): c5e2c4f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -0
app.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces
2
+ import torch
3
+ import torch.nn as nn
4
+ from huggingface_hub import hf_hub_download
5
+ import librosa
6
+ import numpy as np
7
+ import gradio as gr
8
+
9
+
10
+ # Vars
11
+ repo_name = "ipetrousov/ser_supcon"
12
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
13
+ weights_dict = "supcon_stage_ft_acc60.pt"
14
+
15
+ # Classes
16
+ class EmotionFeatureExtractorBase(nn.Module):
17
+ """
18
+ Core CNN to extract spatial features from Mel-Spectrograms.
19
+ Outputs a flat 256-dimensional feature vector.
20
+ """
21
+
22
+ def __init__(self):
23
+ super(EmotionFeatureExtractorBase, self).__init__()
24
+
25
+ self.conv1 = nn.Sequential(
26
+ nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=1),
27
+ nn.BatchNorm2d(32),
28
+ nn.ReLU(),
29
+ nn.MaxPool2d(kernel_size=2, stride=2)
30
+ )
31
+
32
+ self.conv2 = nn.Sequential(
33
+ nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1),
34
+ nn.BatchNorm2d(64),
35
+ nn.ReLU(),
36
+ nn.MaxPool2d(kernel_size=2, stride=2)
37
+ )
38
+
39
+ self.conv3 = nn.Sequential(
40
+ nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
41
+ nn.BatchNorm2d(128),
42
+ nn.ReLU(),
43
+ nn.MaxPool2d(kernel_size=2, stride=2)
44
+ )
45
+
46
+ self.conv4 = nn.Sequential(
47
+ nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
48
+ nn.BatchNorm2d(256),
49
+ nn.ReLU(),
50
+ nn.MaxPool2d(kernel_size=2, stride=2)
51
+ )
52
+
53
+ self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
54
+ self.flatten = nn.Flatten()
55
+
56
+ def forward(self, x):
57
+ x = self.conv1(x)
58
+ x = self.conv2(x)
59
+ x = self.conv3(x)
60
+ x = self.conv4(x)
61
+ x = self.global_pool(x)
62
+ x = self.flatten(x)
63
+ return x # Output shape: (batch_size, 256)
64
+
65
+
66
+ class SupConStage2(nn.Module):
67
+ """ Model B (Stage 2): Frozen Encoder + Linear Classifier. """
68
+
69
+ def __init__(self, trained_encoder, num_classes=8):
70
+ super(SupConStage2, self).__init__()
71
+
72
+ # Pass trained encoder from Stage 1
73
+ self.encoder = trained_encoder
74
+
75
+ # Freeze encoder weights
76
+ for param in self.encoder.parameters():
77
+ param.requires_grad = False
78
+
79
+ # Linear classifier
80
+ self.classifier = nn.Linear(256, num_classes)
81
+
82
+ def forward(self, x):
83
+ # Disable gradient tracking for the encoder to save memory/compute
84
+ with torch.no_grad():
85
+ features = self.encoder(x)
86
+
87
+ return self.classifier(features)
88
+
89
+
90
+ # Function
91
+ def preprocess_drive_audio(file_path, target_sample_rate=22050, n_mels=128, noise_std=0.0):
92
+ """
93
+ Loads audio and generates a normalized Mel-spectrogram using librosa.
94
+ Returns a PyTorch tensor shaped (1, 1, Mels, Time).
95
+ """
96
+
97
+ y, sr = librosa.load(file_path, sr=target_sample_rate, mono=True)
98
+
99
+ mel_spectrogram = librosa.feature.melspectrogram(
100
+ y=y,
101
+ sr=target_sample_rate,
102
+ n_fft=512,
103
+ hop_length=256,
104
+ n_mels=n_mels,
105
+ fmax=8000
106
+ )
107
+
108
+ mel_spectrogram_db = librosa.power_to_db(mel_spectrogram, ref=1.0)
109
+
110
+ mean = np.mean(mel_spectrogram_db)
111
+ std = np.std(mel_spectrogram_db)
112
+ mel_normalized = (mel_spectrogram_db - mean) / (std + 1e-6)
113
+
114
+ final_tensor = torch.tensor(mel_normalized, dtype=torch.float32)
115
+ final_tensor = final_tensor.unsqueeze(0).unsqueeze(0)
116
+
117
+ # Noise Injection Pipeline
118
+ if noise_std > 0.0:
119
+ noise_matrix = torch.randn_like(final_tensor)
120
+ final_tensor = final_tensor + (noise_matrix * noise_std)
121
+
122
+ return final_tensor
123
+
124
+
125
+ def run_local_inference(model, file_path, device, noise_std=0.0):
126
+ """
127
+ Runs a forward pass
128
+ """
129
+
130
+ emotion_classes = ["Neutral", "Calm", "Happy", "Sad", "Angry", "Fearful", "Disgust", "Surprised"]
131
+
132
+ input_tensor = preprocess_drive_audio(file_path, noise_std=noise_std)
133
+ input_tensor = input_tensor.to(device)
134
+
135
+ model.eval()
136
+ with torch.no_grad():
137
+ logits = model(input_tensor)
138
+
139
+ # Get the top prediction index
140
+ predicted_index = torch.argmax(logits, dim=1).item()
141
+ top_class = emotion_classes[predicted_index]
142
+
143
+ # Apply softmax to convert logits to probabilities between 0 and 1
144
+ probabilities = torch.softmax(logits, dim=1).squeeze().tolist()
145
+
146
+ # Map the probabilities to their respective class names for Gradio
147
+ prob_dict = {emotion_classes[i]: probabilities[i] for i in range(len(emotion_classes))}
148
+
149
+ return top_class, prob_dict
150
+
151
+
152
+ # Load SOTA SupConFN
153
+ blank_encoder = EmotionFeatureExtractorBase()
154
+ model = SupConStage2(trained_encoder=blank_encoder, num_classes=8)
155
+
156
+ # Download & Load weights
157
+ model_path = hf_hub_download(repo_id=repo_name, filename=weights_dict)
158
+ model.load_state_dict(torch.load(model_path, map_location=device))
159
+ model.eval()
160
+ model = model.to(device)
161
+
162
+
163
+ # Gradio Interface Setup
164
+ @spaces.GPU
165
+ def predict_emotion(audio_path, noise_std):
166
+ if audio_path is None:
167
+ return "Upload an audio file."
168
+
169
+ try:
170
+ pred = run_local_inference(model, audio_path, device, noise_std=noise_std)
171
+ return pred
172
+ except Exception as e:
173
+ return f"Error processing audio: {str(e)}"
174
+
175
+
176
+ description_html = """
177
+ Upload a short audio clip and test the architecture's resilience. The slider injects mathematically controlled Gaussian noise into the normalized Mel-spectrogram before classification.
178
+
179
+ <div style="display: flex; gap: 10px; margin-top: 15px;">
180
+ <a href="https://github.com/gpetrousov/dl_assignment_demokritos" target="_blank">
181
+ <img src="https://img.shields.io/badge/GitHub-View_Repository-181717?style=for-the-badge&logo=github" alt="GitHub Repository" />
182
+ </a>
183
+ <a href="https://docs.google.com/presentation/d/1ShSmXY2OqCEbQQjYiLYCFc8Psu2bioECoFHhliiDEiQ/edit?usp=drive_link" target="_blank">
184
+ <img src="https://img.shields.io/badge/Presentation-View_Slides-F4B400?style=for-the-badge&logo=googleslides&logoColor=white" alt="Presentation Slides" />
185
+ </a>
186
+ </div>
187
+ """
188
+
189
+ demo = gr.Interface(
190
+ fn=predict_emotion,
191
+ inputs=[
192
+ gr.Audio(type="filepath", label="Upload Audio (.mp3 or .wav)"),
193
+ gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.0, label="Gaussian Noise Injection (STD)")
194
+ ],
195
+ outputs=[
196
+ gr.Textbox(label="Final Prediction"),
197
+ gr.Label(num_top_classes=8, label="Probability Distribution")
198
+ ],
199
+ title="Robust Speech Emotion Recognition (SupCon)",
200
+ description=description_html,
201
+ )
202
+
203
+ if __name__ == "__main__":
204
+ demo.launch()