Spaces:
Running
Running
Upload app.py
Browse files
app.py
ADDED
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# NLP + Emotion model
|
2 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification,AutoModelForSeq2SeqLM, MarianTokenizer, MarianMTModel
|
3 |
+
import lyricsgenius
|
4 |
+
import torch
|
5 |
+
import numpy as np
|
6 |
+
import joblib
|
7 |
+
import gradio as gr
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import warnings
|
10 |
+
from langdetect import detect
|
11 |
+
import os
|
12 |
+
|
13 |
+
|
14 |
+
warnings.filterwarnings("ignore")
|
15 |
+
|
16 |
+
|
17 |
+
|
18 |
+
# π Replace these with your real tokens
|
19 |
+
GENIUS_API_TOKEN = os.getenv("GENIUS_API_TOKEN")
|
20 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
21 |
+
|
22 |
+
# Initialize Genius API
|
23 |
+
genius = lyricsgenius.Genius(GENIUS_API_TOKEN)
|
24 |
+
|
25 |
+
# Load emotion model from Hugging Face
|
26 |
+
model_name = "bhadresh-savani/bert-base-uncased-emotion"
|
27 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name, use_auth_token=HF_TOKEN)
|
28 |
+
model = AutoModelForSequenceClassification.from_pretrained(model_name, use_auth_token=HF_TOKEN)
|
29 |
+
model.eval()
|
30 |
+
|
31 |
+
|
32 |
+
# Load Meta's NLLB model
|
33 |
+
nllb_tokenizer = AutoTokenizer.from_pretrained("facebook/nllb-200-1.3B")
|
34 |
+
nllb_model = AutoModelForSeq2SeqLM.from_pretrained("facebook/nllb-200-1.3B")
|
35 |
+
|
36 |
+
|
37 |
+
# Load ML-based model
|
38 |
+
#emotion_model = joblib.load("emotion_model.pkl")
|
39 |
+
#song_encoder = joblib.load("song_encoder.pkl")
|
40 |
+
#emotion_decoder = joblib.load("emotion_decoder.pkl")
|
41 |
+
|
42 |
+
# Genius API
|
43 |
+
genius = lyricsgenius.Genius(GENIUS_API_TOKEN)
|
44 |
+
|
45 |
+
def translate_to_english(text):
|
46 |
+
try:
|
47 |
+
language = detect(text)
|
48 |
+
if language == 'en':
|
49 |
+
print("β
Detected English β no translation needed.")
|
50 |
+
return text
|
51 |
+
elif language == 'tl' or language == 'fil':
|
52 |
+
print("π Detected Tagalog β translating to English...")
|
53 |
+
else:
|
54 |
+
print(f"π Detected '{language}' β attempting translation anyway.")
|
55 |
+
except Exception as e:
|
56 |
+
print(f"β οΈ Language detection failed: {e}. Proceeding with translation.")
|
57 |
+
|
58 |
+
# Translate using NLLB
|
59 |
+
inputs = nllb_tokenizer(
|
60 |
+
text, return_tensors="pt", truncation=True, padding=True, max_length=512
|
61 |
+
)
|
62 |
+
inputs["forced_bos_token_id"] = nllb_tokenizer.lang_code_to_id["eng_Latn"]
|
63 |
+
translated_tokens = nllb_model.generate(**inputs, max_length=512)
|
64 |
+
return nllb_tokenizer.decode(translated_tokens[0], skip_special_tokens=True)
|
65 |
+
|
66 |
+
|
67 |
+
def get_translated_lyrics(title, artist):
|
68 |
+
try:
|
69 |
+
song = genius.search_song(title, artist)
|
70 |
+
if song and song.lyrics:
|
71 |
+
print("π Original Lyrics Snippet:\n", song.lyrics[:300], "\n")
|
72 |
+
return translate_to_english(song.lyrics)
|
73 |
+
else:
|
74 |
+
print("β οΈ No lyrics found for:", title)
|
75 |
+
except Exception as e:
|
76 |
+
print("Genius error:", e)
|
77 |
+
return None
|
78 |
+
|
79 |
+
|
80 |
+
def get_emotion_distribution(text):
|
81 |
+
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
82 |
+
with torch.no_grad():
|
83 |
+
outputs = model(**inputs)
|
84 |
+
probs = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
|
85 |
+
labels = model.config.id2label
|
86 |
+
return {labels[i]: float(probs[i]) for i in range(len(probs))}
|
87 |
+
|
88 |
+
|
89 |
+
def average_emotions(song_emotions):
|
90 |
+
emotion_keys = list(song_emotions[0].keys())
|
91 |
+
vectors = [[song.get(k, 0) for k in emotion_keys] for song in song_emotions]
|
92 |
+
avg_vector = np.mean(vectors, axis=0)
|
93 |
+
return dict(zip(emotion_keys, avg_vector))
|
94 |
+
|
95 |
+
|
96 |
+
def display_emotion_pie_chart(emotion_scores, top_n=6, min_threshold=0.01):
|
97 |
+
sorted_emotions = sorted(emotion_scores.items(), key=lambda x: x[1], reverse=True)
|
98 |
+
filtered = [(e, s) for e, s in sorted_emotions if s > min_threshold][:top_n]
|
99 |
+
if not filtered:
|
100 |
+
return None
|
101 |
+
labels = [e.title() for e, _ in filtered]
|
102 |
+
sizes = [s for _, s in filtered]
|
103 |
+
explode = [0.05 if i == 0 else 0 for i in range(len(sizes))]
|
104 |
+
colors = plt.cm.Pastel1.colors[:len(labels)]
|
105 |
+
fig, ax = plt.subplots(figsize=(6, 6))
|
106 |
+
ax.pie(sizes, labels=labels, autopct=lambda p: f"{p:.1f}%" if p > 3 else "",
|
107 |
+
startangle=140, explode=explode, colors=colors,
|
108 |
+
textprops=dict(color="black", fontsize=12),
|
109 |
+
wedgeprops=dict(width=0.5, edgecolor='white'))
|
110 |
+
ax.set_title("π Emotion Composition", fontsize=14, fontweight='bold')
|
111 |
+
plt.tight_layout()
|
112 |
+
return fig
|
113 |
+
|
114 |
+
|
115 |
+
def analyze_lyrics(song_title, artist_name):
|
116 |
+
if not song_title or not song_title.strip():
|
117 |
+
return "Please enter a song title.", None
|
118 |
+
|
119 |
+
translated = get_translated_lyrics(song_title, artist_name)
|
120 |
+
if not translated:
|
121 |
+
return "β Lyrics not found or translation failed.", None
|
122 |
+
|
123 |
+
emotions = get_emotion_distribution(translated)
|
124 |
+
summary = f"π§ Dominant Emotion: {max(emotions, key=emotions.get).upper()}"
|
125 |
+
pie = display_emotion_pie_chart(emotions)
|
126 |
+
return summary, pie
|
127 |
+
|
128 |
+
|
129 |
+
def analyze_multiple_songs(song1, artist1, song2, artist2, song3, artist3):
|
130 |
+
all_emotions = []
|
131 |
+
|
132 |
+
for title, artist in [(song1, artist1), (song2, artist2), (song3, artist3)]:
|
133 |
+
if not title or not title.strip():
|
134 |
+
print(f"β οΈ Skipping empty input: {title}")
|
135 |
+
continue
|
136 |
+
lyrics = get_translated_lyrics(title, artist)
|
137 |
+
if lyrics:
|
138 |
+
emotions = get_emotion_distribution(lyrics)
|
139 |
+
all_emotions.append(emotions)
|
140 |
+
|
141 |
+
if not all_emotions:
|
142 |
+
return "β No valid songs found.", None
|
143 |
+
|
144 |
+
avg_emotions = average_emotions(all_emotions)
|
145 |
+
dominant_emotion = max(avg_emotions, key=avg_emotions.get).lower()
|
146 |
+
|
147 |
+
# π§ Store mapped mood for recommendation
|
148 |
+
general_mood = goemotion_to_general.get(dominant_emotion, "calm")
|
149 |
+
detected_mood["mood"] = general_mood
|
150 |
+
|
151 |
+
summary = f"π§ Dominant Emotion: {dominant_emotion.upper()} β Mood: {general_mood.upper()}"
|
152 |
+
pie = display_emotion_pie_chart(avg_emotions)
|
153 |
+
return summary, pie
|
154 |
+
|
155 |
+
|
156 |
+
import pandas as pd
|
157 |
+
import matplotlib.pyplot as plt
|
158 |
+
from collections import Counter
|
159 |
+
|
160 |
+
# Load pseudo-labeled songs
|
161 |
+
labeled_df = pd.read_csv("labeled_songs.csv")
|
162 |
+
label_map = {0: "sad", 1: "happy", 2: "energetic", 3: "calm"}
|
163 |
+
inverse_map = {v: k for k, v in label_map.items()}
|
164 |
+
labeled_df["predicted_emotion_name"] = labeled_df["predicted_emotion"].map(label_map)
|
165 |
+
|
166 |
+
# Mood-shift rules
|
167 |
+
mood_shift_map = {
|
168 |
+
"sad": "happy",
|
169 |
+
"happy": "calm",
|
170 |
+
"calm": "energetic",
|
171 |
+
"energetic": "calm"
|
172 |
+
}
|
173 |
+
|
174 |
+
# Track last detected mood
|
175 |
+
detected_mood = {"mood": None}
|
176 |
+
|
177 |
+
# Mapping from GoEmotion to general moods
|
178 |
+
goemotion_to_general = {
|
179 |
+
"admiration": "happy", "amusement": "happy", "anger": "sad", "annoyance": "energetic",
|
180 |
+
"approval": "happy", "caring": "calm", "confusion": "sad", "curiosity": "energetic",
|
181 |
+
"desire": "energetic", "disappointment": "sad", "disapproval": "energetic", "disgust": "energetic",
|
182 |
+
"embarrassment": "sad", "excitement": "energetic", "fear": "sad", "gratitude": "happy",
|
183 |
+
"grief": "sad", "joy": "happy", "love": "happy", "nervousness": "energetic", "optimism": "happy",
|
184 |
+
"pride": "happy", "realization": "calm", "relief": "calm", "remorse": "sad", "sadness": "sad",
|
185 |
+
"surprise": "energetic", "neutral": "calm"
|
186 |
+
}
|
187 |
+
|
188 |
+
# Analyze 3 songs, get dominant emotion, and store mapped general mood
|
189 |
+
def analyze_and_store_lyrics(song1, artist1, song2, artist2, song3, artist3):
|
190 |
+
summary, fig = analyze_multiple_songs(song1, artist1, song2, artist2, song3, artist3)
|
191 |
+
|
192 |
+
# Extract dominant GoEmotion from summary
|
193 |
+
if "Playlist Mood:" in summary:
|
194 |
+
raw_goemotion = summary.split(":")[1].strip().lower()
|
195 |
+
detected_mood["mood"] = goemotion_to_general.get(raw_goemotion, "calm")
|
196 |
+
summary += f"\nποΈ Mapped Mood: {detected_mood['mood'].upper()}"
|
197 |
+
|
198 |
+
return summary, fig
|
199 |
+
|
200 |
+
# Recommend songs with the same mood as detected
|
201 |
+
def recommend_similar_mood_songs():
|
202 |
+
mood = detected_mood["mood"]
|
203 |
+
if not mood:
|
204 |
+
return pd.DataFrame([{"β οΈ": "Analyze songs first"}])
|
205 |
+
|
206 |
+
filtered = labeled_df[labeled_df["predicted_emotion_name"] == mood]
|
207 |
+
if filtered.empty:
|
208 |
+
return pd.DataFrame([{"β οΈ": f"No songs found for mood '{mood}'"}])
|
209 |
+
|
210 |
+
return filtered.sample(n=min(5, len(filtered)))[["name", "artists", "predicted_emotion_name"]]
|
211 |
+
|
212 |
+
# Recommend songs with a shifted mood
|
213 |
+
def recommend_mood_shift_songs():
|
214 |
+
mood = detected_mood["mood"]
|
215 |
+
if not mood:
|
216 |
+
return pd.DataFrame([{"β οΈ": "Analyze songs first"}])
|
217 |
+
|
218 |
+
target = mood_shift_map.get(mood)
|
219 |
+
if not target:
|
220 |
+
return pd.DataFrame([{"β οΈ": f"No shift rule for '{mood}'"}])
|
221 |
+
|
222 |
+
filtered = labeled_df[labeled_df["predicted_emotion_name"] == target]
|
223 |
+
if filtered.empty:
|
224 |
+
return pd.DataFrame([{"β οΈ": f"No songs found for mood shift to '{target}'"}])
|
225 |
+
|
226 |
+
return filtered.sample(n=min(5, len(filtered)))[["name", "artists", "predicted_emotion_name"]]
|
227 |
+
|
228 |
+
import gradio as gr
|
229 |
+
css = """
|
230 |
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500&display=swap');
|
231 |
+
|
232 |
+
body, h1, h2, h3, h4, h5, h6, p, label, button, input, textarea {
|
233 |
+
font-family: 'Poppins', sans-serif !important;
|
234 |
+
color: #222 !important;
|
235 |
+
font-size: 16px !important;
|
236 |
+
}
|
237 |
+
|
238 |
+
body {
|
239 |
+
background: linear-gradient(to right, #e0f7fa, #f3e5f5);
|
240 |
+
margin: 0;
|
241 |
+
padding: 0;
|
242 |
+
}
|
243 |
+
|
244 |
+
.gradio-container, .gradio-interface, .gradio-box {
|
245 |
+
background-color: rgba(255, 255, 255, 0.9) !important;
|
246 |
+
border-radius: 16px !important;
|
247 |
+
padding: 24px !important;
|
248 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
249 |
+
}
|
250 |
+
|
251 |
+
.gr-textbox, .gr-input, input, textarea {
|
252 |
+
background-color: #fff !important;
|
253 |
+
color: #222 !important;
|
254 |
+
border: 1px solid #ccc !important;
|
255 |
+
border-radius: 12px !important;
|
256 |
+
padding: 12px !important;
|
257 |
+
font-size: 16px !important;
|
258 |
+
}
|
259 |
+
|
260 |
+
.gr-textbox:focus, .gr-input:focus {
|
261 |
+
border-color: #a78bfa !important;
|
262 |
+
}
|
263 |
+
|
264 |
+
.track-btn, .gr-button {
|
265 |
+
background: linear-gradient(135deg, #cfd9df, #e2ebf0) !important;
|
266 |
+
border: none !important;
|
267 |
+
color: #333 !important;
|
268 |
+
font-weight: 600 !important;
|
269 |
+
border-radius: 10px !important;
|
270 |
+
padding: 12px 24px !important;
|
271 |
+
font-size: 16px !important;
|
272 |
+
transition: background 0.4s ease, transform 0.2s ease !important;
|
273 |
+
margin-top: 16px !important;
|
274 |
+
}
|
275 |
+
|
276 |
+
.track-btn:hover, .gr-button:hover {
|
277 |
+
background: linear-gradient(135deg, #e2ebf0, #cfd9df) !important;
|
278 |
+
transform: scale(1.02);
|
279 |
+
}
|
280 |
+
|
281 |
+
label {
|
282 |
+
font-weight: 500 !important;
|
283 |
+
font-size: 14px !important;
|
284 |
+
color: #444 !important;
|
285 |
+
}
|
286 |
+
"""
|
287 |
+
|
288 |
+
|
289 |
+
# Define your Gradio interface
|
290 |
+
with gr.Blocks(css=css, title="Lyric Mood Tracker") as app:
|
291 |
+
gr.Markdown(
|
292 |
+
"<h1 style='text-align: center; font-weight: 500; color: #4A4A4A;'>πΏ Lyric Mood Tracker</h1>"
|
293 |
+
"<p style='text-align: center; color: #666; font-size: 16px;'>Understand the emotional landscape of your favorite songs</p>"
|
294 |
+
)
|
295 |
+
|
296 |
+
with gr.Row():
|
297 |
+
with gr.Column():
|
298 |
+
song1 = gr.Textbox(label="π΅ Song 1 Title")
|
299 |
+
artist1 = gr.Textbox(label="π€ Artist 1")
|
300 |
+
with gr.Column():
|
301 |
+
song2 = gr.Textbox(label="π΅ Song 2 Title")
|
302 |
+
artist2 = gr.Textbox(label="π€ Artist 2")
|
303 |
+
with gr.Column():
|
304 |
+
song3 = gr.Textbox(label="π΅ Song 3 Title")
|
305 |
+
artist3 = gr.Textbox(label="π€ Artist 3")
|
306 |
+
|
307 |
+
run_btn = gr.Button("π Analyze Mood", elem_classes=["track-btn"])
|
308 |
+
output_text = gr.Textbox(label="π§ Mood Summary")
|
309 |
+
output_plot = gr.Plot(label="π Emotion Pie Chart")
|
310 |
+
|
311 |
+
with gr.Row():
|
312 |
+
rec_similar_btn = gr.Button("π― Recommend Similar Mood Songs", elem_classes=["track-btn"])
|
313 |
+
rec_shift_btn = gr.Button("π Recommend Mood-Shift Songs", elem_classes=["track-btn"])
|
314 |
+
|
315 |
+
similar_output = gr.Dataframe(label="π§ Similar Mood Recommendations")
|
316 |
+
shift_output = gr.Dataframe(label="πͺ Mood Shift Recommendations")
|
317 |
+
|
318 |
+
# All functions now use all 3 songs as inputs
|
319 |
+
run_btn.click(
|
320 |
+
analyze_multiple_songs,
|
321 |
+
inputs=[song1, artist1, song2, artist2, song3, artist3],
|
322 |
+
outputs=[output_text, output_plot]
|
323 |
+
)
|
324 |
+
|
325 |
+
run_btn.click(
|
326 |
+
analyze_and_store_lyrics,
|
327 |
+
inputs=[song1, artist1, song2, artist2, song3, artist3],
|
328 |
+
outputs=[output_text, output_plot]
|
329 |
+
)
|
330 |
+
|
331 |
+
run_btn.click(
|
332 |
+
analyze_lyrics,
|
333 |
+
inputs=[song1, artist1, song2, artist2, song3, artist3],
|
334 |
+
outputs=[output_text, output_plot]
|
335 |
+
)
|
336 |
+
|
337 |
+
rec_similar_btn.click(recommend_similar_mood_songs, outputs=similar_output)
|
338 |
+
rec_shift_btn.click(recommend_mood_shift_songs, outputs=shift_output)
|
339 |
+
|
340 |
+
app.launch(share=True)
|