File size: 12,596 Bytes
b016061
 
 
 
 
 
 
 
 
 
 
4236c99
b016061
 
 
 
 
 
4236c99
b016061
 
 
070b30a
b016061
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
070b30a
b016061
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4236c99
 
 
 
 
 
 
 
 
 
 
b016061
4236c99
b016061
4236c99
 
b016061
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# NLP + Emotion model
from transformers import AutoTokenizer, AutoModelForSequenceClassification,AutoModelForSeq2SeqLM, MarianTokenizer, MarianMTModel
import lyricsgenius
import torch
import numpy as np
import joblib
import gradio as gr
import matplotlib.pyplot as plt
import warnings
from langdetect import detect
import os
import requests

warnings.filterwarnings("ignore")



# πŸ”‘ Replace these with your real tokens
#GENIUS_API_TOKEN = os.getenv("GENIUS_API_TOKEN")
HF_TOKEN = os.getenv("HF_TOKEN")

# Initialize Genius API
#genius = lyricsgenius.Genius(GENIUS_API_TOKEN)

# Load emotion model from Hugging Face
model_name = "bhadresh-savani/bert-base-uncased-emotion"
tokenizer = AutoTokenizer.from_pretrained(model_name, use_auth_token=HF_TOKEN)
model = AutoModelForSequenceClassification.from_pretrained(model_name, use_auth_token=HF_TOKEN)
model.eval()


# Load Meta's NLLB model
nllb_tokenizer = AutoTokenizer.from_pretrained("facebook/nllb-200-1.3B")
nllb_model = AutoModelForSeq2SeqLM.from_pretrained("facebook/nllb-200-1.3B")


# Load ML-based model
#emotion_model = joblib.load("emotion_model.pkl")
#song_encoder = joblib.load("song_encoder.pkl")
#emotion_decoder = joblib.load("emotion_decoder.pkl")

# Genius API
#genius = lyricsgenius.Genius(GENIUS_API_TOKEN)

def translate_to_english(text):
    try:
        language = detect(text)
        if language == 'en':
            print("βœ… Detected English β€” no translation needed.")
            return text
        elif language == 'tl' or language == 'fil':
            print("πŸ” Detected Tagalog β€” translating to English...")
        else:
            print(f"🌐 Detected '{language}' β€” attempting translation anyway.")
    except Exception as e:
        print(f"⚠️ Language detection failed: {e}. Proceeding with translation.")

    # Translate using NLLB
    inputs = nllb_tokenizer(
        text, return_tensors="pt", truncation=True, padding=True, max_length=512
    )
    inputs["forced_bos_token_id"] = nllb_tokenizer.lang_code_to_id["eng_Latn"]
    translated_tokens = nllb_model.generate(**inputs, max_length=512)
    return nllb_tokenizer.decode(translated_tokens[0], skip_special_tokens=True)


def get_translated_lyrics(title, artist):
    try:
        print(f"🎡 Searching for \"{title}\" by {artist} using Lyrics.ovh...")
        url = f"https://api.lyrics.ovh/v1/{artist}/{title}"
        response = requests.get(url)

        if response.status_code == 200:
            lyrics = response.json().get("lyrics", "")
            if lyrics.strip():
                print("πŸ“„ Original Lyrics Snippet:\n", lyrics[:300], "\n")
                return translate_to_english(lyrics)
            else:
                print(f"⚠️ Lyrics found but empty for: \"{title}\" by {artist}")
        else:
            print(f"⚠️ Lyrics.ovh returned status code {response.status_code} for: \"{title}\" by {artist}")
    except Exception as e:
        print(f"⚠️ Lyrics.ovh error: {e}")
    
    return None


def get_emotion_distribution(text):
    inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
    with torch.no_grad():
        outputs = model(**inputs)
    probs = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
    labels = model.config.id2label
    return {labels[i]: float(probs[i]) for i in range(len(probs))}


def average_emotions(song_emotions):
    emotion_keys = list(song_emotions[0].keys())
    vectors = [[song.get(k, 0) for k in emotion_keys] for song in song_emotions]
    avg_vector = np.mean(vectors, axis=0)
    return dict(zip(emotion_keys, avg_vector))


def display_emotion_pie_chart(emotion_scores, top_n=6, min_threshold=0.01):
    sorted_emotions = sorted(emotion_scores.items(), key=lambda x: x[1], reverse=True)
    filtered = [(e, s) for e, s in sorted_emotions if s > min_threshold][:top_n]
    if not filtered:
        return None
    labels = [e.title() for e, _ in filtered]
    sizes = [s for _, s in filtered]
    explode = [0.05 if i == 0 else 0 for i in range(len(sizes))]
    colors = plt.cm.Pastel1.colors[:len(labels)]
    fig, ax = plt.subplots(figsize=(6, 6))
    ax.pie(sizes, labels=labels, autopct=lambda p: f"{p:.1f}%" if p > 3 else "",
           startangle=140, explode=explode, colors=colors,
           textprops=dict(color="black", fontsize=12),
           wedgeprops=dict(width=0.5, edgecolor='white'))
    ax.set_title("🎭 Emotion Composition", fontsize=14, fontweight='bold')
    plt.tight_layout()
    return fig


def analyze_lyrics(song_title, artist_name):
    if not song_title or not song_title.strip():
        return "Please enter a song title.", None

    translated = get_translated_lyrics(song_title, artist_name)
    if not translated:
        return "❌ Lyrics not found or translation failed.", None

    emotions = get_emotion_distribution(translated)
    summary = f"🧠 Dominant Emotion: {max(emotions, key=emotions.get).upper()}"
    pie = display_emotion_pie_chart(emotions)
    return summary, pie


def analyze_multiple_songs(song1, artist1, song2, artist2, song3, artist3):
    all_emotions = []

    for title, artist in [(song1, artist1), (song2, artist2), (song3, artist3)]:
        if not title or not title.strip():
            print(f"⚠️ Skipping empty input: {title}")
            continue
        lyrics = get_translated_lyrics(title, artist)
        if lyrics:
            emotions = get_emotion_distribution(lyrics)
            all_emotions.append(emotions)

    if not all_emotions:
        return "❌ No valid songs found.", None

    avg_emotions = average_emotions(all_emotions)
    dominant_emotion = max(avg_emotions, key=avg_emotions.get).lower()

    # 🧠 Store mapped mood for recommendation
    general_mood = goemotion_to_general.get(dominant_emotion, "calm")
    detected_mood["mood"] = general_mood

    summary = f"🧠 Dominant Emotion: {dominant_emotion.upper()} β†’ Mood: {general_mood.upper()}"
    pie = display_emotion_pie_chart(avg_emotions)
    return summary, pie


import pandas as pd
import matplotlib.pyplot as plt
from collections import Counter

# Load pseudo-labeled songs
labeled_df = pd.read_csv("labeled_songs.csv")
label_map = {0: "sad", 1: "happy", 2: "energetic", 3: "calm"}
inverse_map = {v: k for k, v in label_map.items()}
labeled_df["predicted_emotion_name"] = labeled_df["predicted_emotion"].map(label_map)

# Mood-shift rules
mood_shift_map = {
    "sad": "happy",
    "happy": "calm",
    "calm": "energetic",
    "energetic": "calm"
}

# Track last detected mood
detected_mood = {"mood": None}

# Mapping from GoEmotion to general moods
goemotion_to_general = {
    "admiration": "happy", "amusement": "happy", "anger": "sad", "annoyance": "energetic",
    "approval": "happy", "caring": "calm", "confusion": "sad", "curiosity": "energetic",
    "desire": "energetic", "disappointment": "sad", "disapproval": "energetic", "disgust": "energetic",
    "embarrassment": "sad", "excitement": "energetic", "fear": "sad", "gratitude": "happy",
    "grief": "sad", "joy": "happy", "love": "happy", "nervousness": "energetic", "optimism": "happy",
    "pride": "happy", "realization": "calm", "relief": "calm", "remorse": "sad", "sadness": "sad",
    "surprise": "energetic", "neutral": "calm"
}

# Analyze 3 songs, get dominant emotion, and store mapped general mood
def analyze_and_store_lyrics(song1, artist1, song2, artist2, song3, artist3):
    summary, fig = analyze_multiple_songs(song1, artist1, song2, artist2, song3, artist3)

    # Extract dominant GoEmotion from summary
    if "Playlist Mood:" in summary:
        raw_goemotion = summary.split(":")[1].strip().lower()
        detected_mood["mood"] = goemotion_to_general.get(raw_goemotion, "calm")
        summary += f"\nπŸ—‚οΈ Mapped Mood: {detected_mood['mood'].upper()}"

    return summary, fig

# Recommend songs with the same mood as detected
def recommend_similar_mood_songs():
    mood = detected_mood["mood"]
    if not mood:
        return pd.DataFrame([{"⚠️": "Analyze songs first"}])
    
    filtered = labeled_df[labeled_df["predicted_emotion_name"] == mood]
    if filtered.empty:
        return pd.DataFrame([{"⚠️": f"No songs found for mood '{mood}'"}])
    
    return filtered.sample(n=min(5, len(filtered)))[["name", "artists", "predicted_emotion_name"]]

# Recommend songs with a shifted mood
def recommend_mood_shift_songs():
    mood = detected_mood["mood"]
    if not mood:
        return pd.DataFrame([{"⚠️": "Analyze songs first"}])
    
    target = mood_shift_map.get(mood)
    if not target:
        return pd.DataFrame([{"⚠️": f"No shift rule for '{mood}'"}])
    
    filtered = labeled_df[labeled_df["predicted_emotion_name"] == target]
    if filtered.empty:
        return pd.DataFrame([{"⚠️": f"No songs found for mood shift to '{target}'"}])
    
    return filtered.sample(n=min(5, len(filtered)))[["name", "artists", "predicted_emotion_name"]]

import gradio as gr
css = """
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500&display=swap');

body, h1, h2, h3, h4, h5, h6, p, label, button, input, textarea {
    font-family: 'Poppins', sans-serif !important;
    color: #222 !important;
    font-size: 16px !important;
}

body {
    background: linear-gradient(to right, #e0f7fa, #f3e5f5);
    margin: 0;
    padding: 0;
}

.gradio-container, .gradio-interface, .gradio-box {
    background-color: rgba(255, 255, 255, 0.9) !important;
    border-radius: 16px !important;
    padding: 24px !important;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}

.gr-textbox, .gr-input, input, textarea {
    background-color: #fff !important;
    color: #222 !important;
    border: 1px solid #ccc !important;
    border-radius: 12px !important;
    padding: 12px !important;
    font-size: 16px !important;
}

.gr-textbox:focus, .gr-input:focus {
    border-color: #a78bfa !important;
}

.track-btn, .gr-button {
    background: linear-gradient(135deg, #cfd9df, #e2ebf0) !important;
    border: none !important;
    color: #333 !important;
    font-weight: 600 !important;
    border-radius: 10px !important;
    padding: 12px 24px !important;
    font-size: 16px !important;
    transition: background 0.4s ease, transform 0.2s ease !important;
    margin-top: 16px !important;
}

.track-btn:hover, .gr-button:hover {
    background: linear-gradient(135deg, #e2ebf0, #cfd9df) !important;
    transform: scale(1.02);
}

label {
    font-weight: 500 !important;
    font-size: 14px !important;
    color: #444 !important;
}
"""


# Define your Gradio interface
with gr.Blocks(css=css, title="Lyric Mood Tracker") as app:
    gr.Markdown(
        "<h1 style='text-align: center; font-weight: 500; color: #4A4A4A;'>🌿 Lyric Mood Tracker</h1>"
        "<p style='text-align: center; color: #666; font-size: 16px;'>Understand the emotional landscape of your favorite songs</p>"
    )

    with gr.Row():
        with gr.Column():
            song1 = gr.Textbox(label="🎡 Song 1 Title")
            artist1 = gr.Textbox(label="🎀 Artist 1")
        with gr.Column():
            song2 = gr.Textbox(label="🎡 Song 2 Title")
            artist2 = gr.Textbox(label="🎀 Artist 2")
        with gr.Column():
            song3 = gr.Textbox(label="🎡 Song 3 Title")
            artist3 = gr.Textbox(label="🎀 Artist 3")

    run_btn = gr.Button("πŸ” Analyze Mood", elem_classes=["track-btn"])
    output_text = gr.Textbox(label="🧠 Mood Summary")
    output_plot = gr.Plot(label="🎭 Emotion Pie Chart")

    with gr.Row():
        rec_similar_btn = gr.Button("🎯 Recommend Similar Mood Songs", elem_classes=["track-btn"])
        rec_shift_btn = gr.Button("πŸ”„ Recommend Mood-Shift Songs", elem_classes=["track-btn"])

    similar_output = gr.Dataframe(label="🎧 Similar Mood Recommendations")
    shift_output = gr.Dataframe(label="πŸͺ„ Mood Shift Recommendations")

    # All functions now use all 3 songs as inputs
    run_btn.click(
        analyze_multiple_songs,
        inputs=[song1, artist1, song2, artist2, song3, artist3],
        outputs=[output_text, output_plot]
    )

    run_btn.click(
        analyze_and_store_lyrics,
        inputs=[song1, artist1, song2, artist2, song3, artist3],
        outputs=[output_text, output_plot]
    )

    run_btn.click(
        analyze_lyrics,
        inputs=[song1, artist1, song2, artist2, song3, artist3],
        outputs=[output_text, output_plot]
    )

    rec_similar_btn.click(recommend_similar_mood_songs, outputs=similar_output)
    rec_shift_btn.click(recommend_mood_shift_songs, outputs=shift_output)

app.launch(share=True)