music-app/backend/app/services/mood_engine.py
2026-07-03 01:06:35 +00:00

152 lines
8.2 KiB
Python

import json
import os
from typing import Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from ..models.song import Song
from ..models.mood import MoodCategory, MoodSong, LyricsCache
from ..services.lyrics import fetch_lyrics
MOOD_KEYWORDS: Dict[str, List[Tuple[str, float]]] = {
"Sad": [("cry", 3), ("alone", 3), ("tears", 3), ("hurt", 2), ("lonely", 3), ("heartbreak", 3), ("pain", 2), ("lost", 2), ("goodbye", 2), ("miss", 2), ("broken", 3), ("empty", 2), ("dark", 1), ("rain", 2), ("fall", 1)],
"Happy": [("happy", 3), ("joy", 3), ("smile", 2), ("sunshine", 2), ("dance", 2), ("celebrate", 2), ("laugh", 2), ("bright", 2), ("free", 2), ("light", 1), ("party", 2), ("fun", 2), ("good", 1), ("wonderful", 2), ("beautiful", 1)],
"Energetic": [("fire", 3), ("power", 3), ("strong", 2), ("fight", 2), ("run", 2), ("fast", 2), ("beat", 2), ("rise", 2), ("burn", 2), ("wild", 2), ("storm", 2), ("thunder", 2), ("war", 2), ("crash", 2), ("break", 1)],
"Focused": [("think", 3), ("mind", 2), ("clear", 2), ("flow", 2), ("calm", 2), ("deep", 2), ("still", 2), ("quiet", 2), ("concentrate", 3), ("focus", 3), ("work", 1), ("study", 2), ("peace", 2), ("steady", 2), ("control", 2)],
"Chill": [("relax", 3), ("chill", 3), ("smooth", 2), ("easy", 2), ("vibes", 2), ("groove", 2), ("lazy", 2), ("slow", 2), ("soft", 2), ("gentle", 2), ("mellow", 3), ("unwind", 2), ("breeze", 2), ("cloud", 1), ("drift", 2)],
"Romantic": [("love", 3), ("heart", 3), ("kiss", 2), ("baby", 2), ("desire", 2), ("passion", 3), ("touch", 2), ("embrace", 2), ("forever", 2), ("sweetheart", 2), ("romance", 3), ("lover", 2), ("darling", 2), ("soul", 1), ("together", 2)],
"Angry": [("anger", 3), ("hate", 3), ("fury", 3), ("rage", 3), ("scream", 2), ("destroy", 2), ("enemy", 2), ("betray", 2), ("lie", 2), ("fight", 2), ("burn", 2), ("kill", 3), ("war", 2), ("hell", 2), ("damn", 2)],
"Nostalgic": [("memory", 3), ("remember", 3), ("past", 3), ("yesterday", 3), ("old", 2), ("back", 2), ("days", 2), ("childhood", 2), ("home", 2), ("then", 2), ("once", 2), ("before", 2), ("gone", 2), ("time", 1), ("golden", 2)],
"Melancholy": [("sorrow", 3), ("grief", 3), ("blue", 2), ("fade", 2), ("shadow", 2), ("silence", 2), ("void", 2), ("night", 2), ("cold", 2), ("end", 2), ("dying", 2), ("falling", 2), ("heavy", 2), ("darkness", 2), ("whisper", 1)],
"Dreamy": [("dream", 3), ("sky", 2), ("cloud", 2), ("float", 2), ("star", 2), ("moon", 2), ("space", 2), ("cosmos", 2), ("ethereal", 3), ("magic", 2), ("fantasy", 2), ("wonder", 2), ("shimmer", 2), ("glow", 2), ("haze", 2)],
}
MOOD_NAMES = list(MOOD_KEYWORDS.keys())
CONFIDENCE_THRESHOLD = 0.3
def analyze_lyrics(lyrics: str) -> Dict[str, float]:
if not lyrics:
return {mood: 0.0 for mood in MOOD_NAMES}
words = set(lyrics.lower().split())
scores: Dict[str, float] = {mood: 0.0 for mood in MOOD_NAMES}
for mood, keywords in MOOD_KEYWORDS.items():
for word, weight in keywords:
if word in words:
scores[mood] += weight
max_score = max(scores.values()) if scores else 0
if max_score > 0:
scores = {mood: (score / max_score) for mood, score in scores.items()}
return scores
def get_or_fetch_lyrics(song_id: str, db: Session) -> Optional[str]:
cached = db.query(LyricsCache).filter(LyricsCache.song_id == song_id).first()
if cached and cached.lyrics_text:
return cached.lyrics_text
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
return None
lyrics = fetch_lyrics(song.title, song.artist)
if lyrics:
scores = analyze_lyrics(lyrics)
cache = LyricsCache(
song_id=song_id,
lyrics_text=lyrics,
mood_tags_json=json.dumps(scores),
source_url="",
)
existing = db.query(LyricsCache).filter(LyricsCache.song_id == song_id).first()
if existing:
existing.lyrics_text = lyrics
existing.mood_tags_json = json.dumps(scores)
existing.source_url = ""
else:
db.add(cache)
db.commit()
return lyrics
def analyze_song_mood(song_id: str, db: Session) -> Dict:
lyrics = get_or_fetch_lyrics(song_id, db)
if not lyrics:
return {"song_id": song_id, "scores": [], "top_mood": None, "confidence": 0}
scores = analyze_lyrics(lyrics)
sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)
top_mood = sorted_scores[0][0] if sorted_scores else None
confidence = sorted_scores[0][1] if sorted_scores else 0
mood_scores = [
{"mood": mood, "score": score, "keywords": []}
for mood, score in sorted_scores
if score >= CONFIDENCE_THRESHOLD
]
# Store mood association
for mood, score in sorted_scores:
if score >= CONFIDENCE_THRESHOLD:
existing = db.query(MoodSong).filter(MoodSong.song_id == song_id, MoodSong.mood_id == mood.lower()).first()
if not existing:
ms = MoodSong(mood_id=mood.lower(), song_id=song_id, confidence_score=score)
db.add(ms)
db.commit()
return {
"song_id": song_id,
"scores": mood_scores,
"top_mood": top_mood,
"confidence": confidence,
}
def get_mood_playlist(mood: str, db: Session, limit: int = 50) -> List[Song]:
mood_lower = mood.lower()
# Try mood songs first
mood_songs = (
db.query(MoodSong, Song)
.join(Song, MoodSong.song_id == Song.id)
.filter(MoodSong.mood_id == mood_lower)
.order_by(MoodSong.confidence_score.desc())
.limit(limit)
.all()
)
songs = [song for _, song in mood_songs]
# If not enough songs, add random songs
if len(songs) < limit:
remaining = db.query(Song).filter(~Song.id.in_([s.id for s in songs])).order_by(Song.added_at.desc()).limit(limit - len(songs)).all()
songs.extend(remaining)
return songs
def seed_mood_categories(db: Session):
categories = [
{"id": "sad", "name": "Sad", "color_hex": "#1a2a4a", "description": "Melancholic and reflective tracks", "background_image": "/moods/sad.jpg", "icon_path": "/icons/mood-sad.svg"},
{"id": "happy", "name": "Happy", "color_hex": "#f5c542", "description": "Uplifting and cheerful tunes", "background_image": "/moods/happy.jpg", "icon_path": "/icons/mood-happy.svg"},
{"id": "energetic", "name": "Energetic", "color_hex": "#e63946", "description": "High-energy and driving beats", "background_image": "/moods/energetic.jpg", "icon_path": "/icons/mood-energetic.svg"},
{"id": "focused", "name": "Focused", "color_hex": "#2d6a4f", "description": "Concentration and productivity music", "background_image": "/moods/focused.jpg", "icon_path": "/icons/mood-focused.svg"},
{"id": "chill", "name": "Chill", "color_hex": "#48957e", "description": "Relaxed and smooth vibes", "background_image": "/moods/chill.jpg", "icon_path": "/icons/mood-chill.svg"},
{"id": "romantic", "name": "Romantic", "color_hex": "#bc6a7e", "description": "Love songs and intimate melodies", "background_image": "/moods/romantic.jpg", "icon_path": "/icons/mood-romantic.svg"},
{"id": "angry", "name": "Angry", "color_hex": "#9d0208", "description": "Intense and powerful tracks", "background_image": "/moods/angry.jpg", "icon_path": "/icons/mood-angry.svg"},
{"id": "nostalgic", "name": "Nostalgic", "color_hex": "#a67c52", "description": "Throwback and sentimental favorites", "background_image": "/moods/nostalgic.jpg", "icon_path": "/icons/mood-nostalgic.svg"},
{"id": "melancholy", "name": "Melancholy", "color_hex": "#5a189c", "description": "Deep and contemplative soundscapes", "background_image": "/moods/melancholy.jpg", "icon_path": "/icons/mood-melancholy.svg"},
{"id": "dreamy", "name": "Dreamy", "color_hex": "#9b5de5", "description": "Ethereal and atmospheric music", "background_image": "/moods/dreamy.jpg", "icon_path": "/icons/mood-dreamy.svg"},
]
for cat in categories:
existing = db.query(MoodCategory).filter(MoodCategory.id == cat["id"]).first()
if not existing:
mood = MoodCategory(**cat)
db.add(mood)
db.commit()