aniruddhakumarpaul commited on
Commit
6650f28
Β·
1 Parent(s): cee4eb8

Save local changes before LFS migration

Browse files
backend/__init__.py ADDED
File without changes
backend/model_manager.py CHANGED
@@ -12,6 +12,7 @@ from tensorflow.keras.callbacks import EarlyStopping
12
  import pickle
13
  import joblib
14
  from joblib import Parallel, delayed
 
15
 
16
  # Parameters
17
  MAX_PAD_LEN = 174
@@ -30,10 +31,38 @@ def extract_features_static(file_path, duration=DURATION, sample_rate=SAMPLE_RAT
30
  # Normalize path
31
  file_path = os.path.normpath(os.path.abspath(file_path))
32
 
33
- # Load audio
34
- # res_type='kaiser_fast' is faster
35
- audio, sr = librosa.load(file_path, res_type='kaiser_fast', duration=duration, sr=sample_rate)
36
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  mfccs = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=n_mfcc)
38
  pad_width = max_pad_len - mfccs.shape[1]
39
  if pad_width > 0:
@@ -45,6 +74,21 @@ def extract_features_static(file_path, duration=DURATION, sample_rate=SAMPLE_RAT
45
  print(f"Error extracting features from {file_path}: {e}")
46
  return None
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  class EmotionClassifier:
49
  def __init__(self):
50
  self.model = None
@@ -108,22 +152,11 @@ class EmotionClassifier:
108
  log("DEBUG: No .wav files found in os.walk")
109
  raise ValueError("No .wav files found for training.")
110
 
111
- from .utils import get_label_from_filename
112
-
113
  log(f"Processing {len(files)} files for training utilizing parallel processing...")
114
-
115
- # Helper to process a single file and return (features, label)
116
- def process_file(file):
117
- lbl = get_label_from_filename(file)
118
- if lbl:
119
- feat = extract_features_static(file)
120
- if feat is not None:
121
- return (feat, lbl)
122
- return None
123
 
124
  # Run in parallel
125
- # n_jobs=-1 uses all available cores
126
- results = Parallel(n_jobs=-1, verbose=5)(delayed(process_file)(f) for f in files)
127
 
128
  # Filter None results
129
  valid_results = [r for r in results if r is not None]
 
12
  import pickle
13
  import joblib
14
  from joblib import Parallel, delayed
15
+ from .utils import get_label_from_filename
16
 
17
  # Parameters
18
  MAX_PAD_LEN = 174
 
31
  # Normalize path
32
  file_path = os.path.normpath(os.path.abspath(file_path))
33
 
34
+ audio = None
35
+ sr = sample_rate
36
+
37
+ # Try loading with librosa first
38
+ try:
39
+ audio, sr = librosa.load(file_path, res_type='kaiser_fast', duration=duration, sr=sample_rate)
40
+ except Exception as e_librosa:
41
+ print(f"Librosa load failed for {file_path}: {e_librosa}. Trying soundfile...")
42
+ try:
43
+ import soundfile as sf
44
+ audio, file_sr = sf.read(file_path)
45
+ # If we read successfuly, we might need to resample or crop/pad
46
+ if len(audio.shape) > 1:
47
+ audio = audio[:, 0] # Take first channel if stereo
48
+
49
+ # Simple resampling if needed (though librosa is better at this, we can try to use librosa.resample if load failed but resample works)
50
+ if file_sr != sample_rate:
51
+ audio = librosa.resample(audio, orig_sr=file_sr, target_sr=sample_rate)
52
+
53
+ # Manual duration crop
54
+ max_samples = int(duration * sample_rate)
55
+ if len(audio) > max_samples:
56
+ audio = audio[:max_samples]
57
+
58
+ sr = sample_rate
59
+ except Exception as e_sf:
60
+ print(f"Soundfile fallback also failed for {file_path}: {e_sf}")
61
+ return None
62
+
63
+ if audio is None:
64
+ return None
65
+
66
  mfccs = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=n_mfcc)
67
  pad_width = max_pad_len - mfccs.shape[1]
68
  if pad_width > 0:
 
74
  print(f"Error extracting features from {file_path}: {e}")
75
  return None
76
 
77
+ def process_file(file):
78
+ """
79
+ Helper to process a single file and return (features, label).
80
+ Must be at module level for joblib on Windows.
81
+ """
82
+ try:
83
+ lbl = get_label_from_filename(file)
84
+ if lbl:
85
+ feat = extract_features_static(file)
86
+ if feat is not None:
87
+ return (feat, lbl)
88
+ except Exception as e:
89
+ print(f"Error processing {file}: {e}")
90
+ return None
91
+
92
  class EmotionClassifier:
93
  def __init__(self):
94
  self.model = None
 
152
  log("DEBUG: No .wav files found in os.walk")
153
  raise ValueError("No .wav files found for training.")
154
 
 
 
155
  log(f"Processing {len(files)} files for training utilizing parallel processing...")
 
 
 
 
 
 
 
 
 
156
 
157
  # Run in parallel
158
+ # n_jobs=1 avoids Windows multiprocessing issues
159
+ results = Parallel(n_jobs=1, verbose=5)(delayed(process_file)(f) for f in files)
160
 
161
  # Filter None results
162
  valid_results = [r for r in results if r is not None]
backend/reproduce_error.py DELETED
@@ -1,31 +0,0 @@
1
-
2
- import os
3
- import librosa
4
- import traceback
5
- import soundfile as sf
6
-
7
- # Path to the specific file
8
- file_path = r"c:\Users\aniru\OneDrive\Desktop\EDUVN\data sets\Actor_01\03-01-01-01-01-01-01.wav"
9
-
10
- print(f"Testing loading: {file_path}")
11
- print(f"Does file exist? {os.path.exists(file_path)}")
12
-
13
- try:
14
- # Mimic parameters from model_manager.py
15
- DURATION = 3
16
- SAMPLE_RATE = 22050
17
- audio, sample_rate = librosa.load(file_path, res_type='kaiser_fast', duration=DURATION, sr=SAMPLE_RATE)
18
- print("Success! Audio loaded.")
19
- print(f"Shape: {audio.shape}, Sample Rate: {sample_rate}")
20
- except Exception as e:
21
- print("FAILED to load audio.")
22
- print(f"Error: {e}")
23
- traceback.print_exc()
24
-
25
- print("-" * 20)
26
- print("Testing soundfile directly...")
27
- try:
28
- data, samplerate = sf.read(file_path)
29
- print(f"Soundfile read success. Shape: {data.shape}, Rate: {samplerate}")
30
- except Exception as e:
31
- print(f"Soundfile direct read failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/requirements.txt CHANGED
@@ -1,15 +1,10 @@
1
- fastapi
2
- uvicorn
3
- python-multipart
4
- tensorflow
5
- librosa
6
- pydub
7
  numpy
8
- scikit-learn
 
9
  soundfile
10
- joblib
11
- resampy
12
- SpeechRecognition
13
- transformers
14
- tf-keras
15
- torch
 
 
 
 
 
 
 
1
  numpy
2
+ pandas
3
+ librosa
4
  soundfile
5
+ tensorflow>=2.10.0
6
+ scikit-learn
7
+ pydub
8
+ streamlit
9
+ streamlit-audiorecorder
10
+ plotly
backend/test_training.py DELETED
@@ -1,37 +0,0 @@
1
-
2
- import os
3
- import sys
4
-
5
- # When running as a module 'backend.test_training', we can use relative imports
6
- try:
7
- from .model_manager import EmotionClassifier
8
- except ImportError:
9
- # Fallback if run as script, but this will break relative imports in model_manager
10
- # So we must fix path to import 'backend.model_manager'
11
- sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
12
- from backend.model_manager import EmotionClassifier
13
-
14
- def test_training():
15
- print("Initializing Classifier...")
16
- classifier = EmotionClassifier()
17
-
18
- # Correct data path assuming we are in backend/
19
- base_dir = os.path.dirname(os.path.abspath(__file__))
20
- data_dir = os.path.join(base_dir, "../data sets")
21
-
22
- print(f"Data directory: {data_dir}")
23
-
24
- def log_callback(msg):
25
- print(f"[TRAIN] {msg}")
26
-
27
- print("Starting training test...")
28
- try:
29
- classifier.train_model(data_dir, log_callback=log_callback)
30
- print("Training test passed!")
31
- except Exception as e:
32
- print(f"Training test failed: {e}")
33
- import traceback
34
- traceback.print_exc()
35
-
36
- if __name__ == "__main__":
37
- test_training()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy_to_hf.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from huggingface_hub import HfApi
3
+
4
+ def deploy():
5
+ token = os.environ.get("HF_TOKEN")
6
+ if not token:
7
+ raise ValueError("HF_TOKEN environment variable is not set")
8
+
9
+ api = HfApi()
10
+
11
+ # Upload the entire current directory to the Space
12
+ # exclude .git to avoid recursive confusion
13
+ print("Starting upload to Hugging Face Space...")
14
+ api.upload_folder(
15
+ folder_path=".",
16
+ repo_id="aniruddhakumarpaul/Vocal-Vibe",
17
+ repo_type="space",
18
+ token=token,
19
+ ignore_patterns=[".git/*", ".github/*", "venv/*", "__pycache__/*", "*.pyc"]
20
+ )
21
+ print("Upload complete!")
22
+
23
+ if __name__ == "__main__":
24
+ deploy()
encoder.pkl CHANGED
Binary files a/encoder.pkl and b/encoder.pkl differ
 
streamlit_app.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+ import time
6
+ from backend.model_manager import EmotionClassifier
7
+ from audiorecorder import audiorecorder
8
+
9
+ # Page Config
10
+ st.set_page_config(
11
+ page_title="VocalVibe - Emotion Recognition",
12
+ page_icon="πŸŽ™οΈ",
13
+ layout="wide",
14
+ initial_sidebar_state="collapsed"
15
+ )
16
+
17
+ # Load External CSS
18
+ def load_css(file_name):
19
+ with open(file_name) as f:
20
+ st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
21
+
22
+ # Inject Custom CSS to override Streamlit defaults and apply original styles
23
+ st.markdown("""
24
+ <style>
25
+ /* 1. GLOBAL RESETS */
26
+ .stApp {
27
+ background-color: #0f172a; /* Dark Blue */
28
+ }
29
+
30
+ /* 2. BACKGROUND BLOBS ANIMATION */
31
+ @keyframes float {
32
+ 0%, 100% { transform: translate(0, 0); }
33
+ 50% { transform: translate(30px, -30px); }
34
+ }
35
+ @keyframes pulse-glow {
36
+ 0%, 100% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.5); }
37
+ 50% { box-shadow: 0 0 40px rgba(99, 102, 241, 0.8); }
38
+ }
39
+
40
+ /* Hide Streamlit Default Chrome */
41
+ #MainMenu, footer, header {visibility: hidden;}
42
+
43
+ /* 3. MAIN GLASS CARD (The .block-container) */
44
+ .block-container {
45
+ background: rgba(255, 255, 255, 0.03);
46
+ backdrop-filter: blur(20px);
47
+ -webkit-backdrop-filter: blur(20px);
48
+ border: 1px solid rgba(255, 255, 255, 0.08); /* Faint border */
49
+ border-radius: 32px; /* Smoother corners */
50
+ padding: 3rem 2rem !important;
51
+
52
+ /* Layout Constraints */
53
+ max-width: 480px; /* Tighter width like the image */
54
+ margin: auto;
55
+ margin-top: 8vh;
56
+
57
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); /* Deep shadow */
58
+ }
59
+
60
+ /* 4. TYPOGRAPHY */
61
+ h1 {
62
+ text-align: center;
63
+ font-family: 'Outfit', sans-serif !important;
64
+ font-size: 3.2rem !important;
65
+ color: white !important;
66
+ margin-bottom: 0px !important;
67
+ padding: 0 !important;
68
+ letter-spacing: -1px;
69
+ }
70
+
71
+ p.subtitle {
72
+ text-align: center;
73
+ font-family: 'Outfit', sans-serif !important;
74
+ color: #94a3b8 !important; /* Muted blue-grey */
75
+ font-size: 0.95rem !important;
76
+ margin-top: 5px !important;
77
+ margin-bottom: 2rem !important;
78
+ font-weight: 300;
79
+ }
80
+
81
+ .highlight {
82
+ background: linear-gradient(135deg, #a855f7 0%, #ec4899 100%); /* Purple to Pink */
83
+ background-clip: text;
84
+ -webkit-background-clip: text;
85
+ -webkit-text-fill-color: transparent;
86
+ font-weight: 700;
87
+ }
88
+
89
+ /* 5. TRAIN MODEL BUTTON (Small Pill) */
90
+ /* Target only the first button (Train) using nth-of-type or specific positioning logic if possible.
91
+ Since it's the first button in the DOM usually... */
92
+ div.stButton > button {
93
+ background: rgba(255, 255, 255, 0.05);
94
+ color: #818cf8; /* Light Indigo */
95
+ border: 1px solid rgba(99, 102, 241, 0.3);
96
+ border-radius: 9999px; /* Full Pill */
97
+ padding: 0.4rem 1.2rem;
98
+ font-size: 0.75rem;
99
+ font-family: 'Outfit', sans-serif;
100
+ text-transform: uppercase;
101
+ letter-spacing: 1px;
102
+ margin: 0 auto;
103
+ display: block;
104
+ }
105
+ div.stButton > button:hover {
106
+ background: rgba(99, 102, 241, 0.2);
107
+ border-color: #818cf8;
108
+ color: white;
109
+ }
110
+
111
+ /* 6. RECORDER AREA */
112
+ /* Since we can't fully style the iframe button, we create a visual wrapper 'look' around it or center it perfectly */
113
+ .recorder-wrapper {
114
+ display: flex;
115
+ justify-content: center;
116
+ align-items: center;
117
+ margin: 2rem 0;
118
+ flex-direction: column;
119
+ }
120
+ .recorder-label {
121
+ color: white;
122
+ font-family: 'Outfit', sans-serif;
123
+ font-size: 0.9rem;
124
+ margin-top: 1rem;
125
+ }
126
+
127
+ /* 7. DIVIDER */
128
+ .divider-box {
129
+ display: flex;
130
+ align-items: center;
131
+ color: #475569;
132
+ font-family: 'Outfit', sans-serif;
133
+ font-size: 0.8rem;
134
+ margin: 2.5rem 0;
135
+ }
136
+ .divider-line {
137
+ flex-grow: 1;
138
+ height: 1px;
139
+ background-color: #334155;
140
+ }
141
+ .divider-text {
142
+ margin: 0 1rem;
143
+ }
144
+
145
+ /* 8. UPLOAD BOX (Dashed) */
146
+ [data-testid='stFileUploader'] {
147
+ border: 2px dashed #334155; /* Darker dash */
148
+ border-radius: 20px;
149
+ padding: 1.5rem;
150
+ background-color: rgba(15, 23, 42, 0.3); /* Dark fill */
151
+ }
152
+ [data-testid='stFileUploader'] section {
153
+ padding: 0;
154
+ }
155
+ /* Hide the 'Drag and drop file here' default text via font-size 0 hack?
156
+ No, that hides the file name too on upload.
157
+ We rely on adding our custom specific label. */
158
+
159
+ /* 9. WATERMARK Pill */
160
+ .watermark-pill {
161
+ position: fixed;
162
+ bottom: 30px;
163
+ right: 30px;
164
+ background: rgba(15, 23, 42, 0.6);
165
+ border: 1px solid rgba(255,255,255,0.1);
166
+ padding: 8px 16px;
167
+ border-radius: 20px;
168
+ color: #cbd5e1;
169
+ font-family: 'Outfit', sans-serif;
170
+ font-size: 0.8rem;
171
+ backdrop-filter: blur(4px);
172
+ }
173
+ </style>
174
+ """, unsafe_allow_html=True)
175
+
176
+ # Google Fonts
177
+ st.markdown('<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">', unsafe_allow_html=True)
178
+ # FontAwesome
179
+ st.markdown('<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">', unsafe_allow_html=True)
180
+
181
+ # Initialize Classifier
182
+ @st.cache_resource
183
+ def get_classifier():
184
+ return EmotionClassifier()
185
+
186
+ try:
187
+ classifier = get_classifier()
188
+ except Exception as e:
189
+ st.error(f"Failed to load model: {e}")
190
+ st.stop()
191
+
192
+
193
+ def main():
194
+ # Background Blobs (Fixed Position)
195
+ st.markdown("""
196
+ <div style="position: fixed; top: -100px; left: -100px; width: 500px; height: 500px; background: radial-gradient(circle, rgba(99,102,241,0.4) 0%, rgba(0,0,0,0) 70%); z-index: -1; animation: float 8s ease-in-out infinite;"></div>
197
+ <div style="position: fixed; bottom: -100px; right: -100px; width: 600px; height: 600px; background: radial-gradient(circle, rgba(168,85,247,0.3) 0%, rgba(0,0,0,0) 70%); z-index: -1; animation: float 12s ease-in-out infinite alternate;"></div>
198
+ """, unsafe_allow_html=True)
199
+
200
+ # --- CONTENT INSIDE GLASS CARD ---
201
+
202
+ # 1. Header
203
+ st.markdown("""
204
+ <h1>Vocal<span class="highlight">Vibe</span></h1>
205
+ <p class="subtitle">AI-Powered Speech Emotion Recognition</p>
206
+ """, unsafe_allow_html=True)
207
+
208
+ # 2. Train Model Button (Pill)
209
+ # Using a column to ensure it centers nicely if the CSS margin check fails
210
+ col_t1, col_t2, col_t3 = st.columns([1, 1, 1])
211
+ with col_t2:
212
+ if st.button("Training Mode πŸ”’"):
213
+ st.toast("Admin access required for training.", icon="⚠️")
214
+
215
+ # 3. Recorder Section (The Big Feature)
216
+ st.markdown("<br>", unsafe_allow_html=True)
217
+
218
+ # We use columns to center the recorder component
219
+ col_r1, col_r2, col_r3 = st.columns([1, 1.5, 1])
220
+ with col_r2:
221
+ # User requested "Click & Hold". The lib allows 'Click to record'.
222
+ # We can't change the component's internal logic, but we can match the text.
223
+ audio = audiorecorder("Click & Hold to Record", "Recording...")
224
+
225
+ # 4. Divider
226
+ st.markdown("""
227
+ <div class="divider-box">
228
+ <div class="divider-line"></div>
229
+ <div class="divider-text">OR</div>
230
+ <div class="divider-line"></div>
231
+ </div>
232
+ """, unsafe_allow_html=True)
233
+
234
+ # 5. Upload Section
235
+ # Custom Icon Header for the Upload Box
236
+ st.markdown("""
237
+ <div style="text-align: center; color: #cbd5e1; margin-bottom: 5px; font-size: 1.2rem;">
238
+ <i class="fa-solid fa-cloud-arrow-up"></i>
239
+ </div>
240
+ """, unsafe_allow_html=True)
241
+
242
+ uploaded_file = st.file_uploader("Upload Audio (WAV)", type=['wav'], label_visibility="collapsed")
243
+ if not uploaded_file:
244
+ st.markdown("""
245
+ <div style="text-align: center; color: #64748b; font-size: 0.8rem; margin-top: -10px;">
246
+ Drag & Drop Audio File <br>
247
+ <span style="font-size: 0.7rem; opacity: 0.7;">Limit 200MB per file β€’ WAV</span>
248
+ </div>
249
+ """, unsafe_allow_html=True)
250
+
251
+
252
+ # --- PROCESSING ---
253
+ audio_file = None
254
+ source_type = ""
255
+
256
+ if len(audio) > 0:
257
+ ts = int(time.time())
258
+ temp_filename = f"temp_rec_{ts}.wav"
259
+ audio.export(temp_filename, format="wav")
260
+ audio_file = temp_filename
261
+ source_type = "recording"
262
+
263
+ if uploaded_file is not None:
264
+ audio_file = uploaded_file
265
+ source_type = "upload"
266
+
267
+ if audio_file:
268
+ st.markdown("---")
269
+ st.audio(audio_file)
270
+
271
+ # We style the Analyze button to look like a primary action
272
+ # Streamlit 'primary' type is usually red/pink. We use custom CSS above to target it if needed,
273
+ # or just accept the default primary which is often red/orange in dark mode, but we can verify.
274
+ if st.button("Analyze Emotion", type="primary", use_container_width=True):
275
+ with st.spinner("Processing Audio..."):
276
+ try:
277
+ target_path = "temp_analysis.wav"
278
+ if source_type == "upload":
279
+ with open(target_path, "wb") as f:
280
+ f.write(audio_file.getbuffer())
281
+ else:
282
+ target_path = audio_file
283
+
284
+ result = classifier.predict_emotion(target_path)
285
+
286
+ if os.path.exists(target_path) and target_path != audio_file:
287
+ try: os.remove(target_path)
288
+ except: pass
289
+
290
+ # Result Card
291
+ st.markdown(f"""
292
+ <div style="background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 24px; padding: 2rem; text-align: center; margin-top: 2rem;">
293
+ <div style="font-size: 4rem; margin-bottom: 10px; filter: drop-shadow(0 0 20px rgba(255,255,255,0.3));">{get_emoji(result['label'])}</div>
294
+ <h2 style="font-family: 'Outfit'; color: white; margin: 0; font-size: 2rem; letter-spacing: 1px;">{result['label'].title()}</h2>
295
+ <div style="display: inline-block; background: rgba(99, 102, 241, 0.2); color: #818cf8; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; margin-top: 10px;">
296
+ Confidence: {result['confidence']:.1%}
297
+ </div>
298
+ </div>
299
+ """, unsafe_allow_html=True)
300
+
301
+ # Plot
302
+ dist = result['distribution']
303
+ labels = list(dist.keys())
304
+ values = list(dist.values())
305
+
306
+ fig = go.Figure(data=[go.Bar(
307
+ x=labels, y=values,
308
+ marker_color=['#a855f7' if l == result['label'] else '#334155' for l in labels],
309
+ text=[f"{v:.0%}" for v in values],
310
+ textposition='auto',
311
+ )])
312
+ fig.update_layout(
313
+ paper_bgcolor='rgba(0,0,0,0)',
314
+ plot_bgcolor='rgba(0,0,0,0)',
315
+ font=dict(color='#94a3b8', family="Outfit"),
316
+ height=220,
317
+ margin=dict(l=10, r=10, t=10, b=10),
318
+ yaxis=dict(showgrid=False, showticklabels=False),
319
+ xaxis=dict(showgrid=False)
320
+ )
321
+ st.plotly_chart(fig, use_container_width=True)
322
+
323
+ # Feedback
324
+ with st.expander("πŸ“ Provide Feedback"):
325
+ correct_label = st.selectbox("Actual Emotion", options=classifier.le.classes_)
326
+ if st.button("Submit Feedback"):
327
+ save_feedback(audio_file, correct_label, source_type)
328
+ st.success("Thank you for your feedback!")
329
+
330
+ except Exception as e:
331
+ st.error(f"Analysis Failed: {e}")
332
+
333
+ # WATERMARK
334
+ st.markdown("""
335
+ <div class="watermark-pill">
336
+ Made by : Aniruddha Paul
337
+ </div>
338
+ """, unsafe_allow_html=True)
339
+
340
+ def get_emoji(label):
341
+ emojis = {
342
+ 'anger': '😠', 'neutral': '😐', 'sadness': '😒', 'happiness': '😊',
343
+ 'fear': '😨', 'disgust': '🀒', 'surprise': '😲', 'calm': '😌'
344
+ }
345
+ return emojis.get(label, 'πŸ€”')
346
+
347
+ def save_feedback(audio_source, label, source_type):
348
+ try:
349
+ feedback_dir = os.path.join("data sets", "user_feedback")
350
+ os.makedirs(feedback_dir, exist_ok=True)
351
+ timestamp = int(time.time())
352
+ filename = f"feedback_{label}_{timestamp}.wav"
353
+ target_path = os.path.join(feedback_dir, filename)
354
+ if source_type == "upload":
355
+ with open(target_path, "wb") as f:
356
+ f.write(audio_source.getbuffer())
357
+ else:
358
+ import shutil
359
+ shutil.copy(audio_source, target_path)
360
+ except Exception as e:
361
+ st.error(f"Could not save feedback: {e}")
362
+
363
+ if __name__ == "__main__":
364
+ main()