Zum Inhalt springen

LLMs sind kein Allheilmittel: Praxistest zur Musik-Klassifikation anhand von Metadaten

    Die Fragestellung war, ob aktuelle Large Language Models (LLMs) wie GPT-4 oder DeepSeek in der Lage sind, Musikstücke – speziell Salsa-Songs – anhand von Titel, Künstler, Songtext und Metadaten automatisch und zuverlässig in „Salsa Cubana“ oder „Salsa Línea“ zu klassifizieren. Es war bekannt, dass die Informationslage (Metadaten, Genre-Tags, Lyrics) lückenhaft und teilweise uneinheitlich ist. Der Test diente explizit dazu, die praktischen Grenzen heutiger LLMs in diesem Kontext zu ermitteln.

    Vorgehen
    Für jeden Song einer Spotify-Playlist wurden sämtliche verfügbaren Metadaten erfasst: Titel, Künstler, Album, Genres, Veröffentlichungsdatum, Lyrics (über Genius), zusätzliche Tags und Biografien (über Last.fm). Diese Daten wurden in strukturierter Form an das LLM übergeben. Der Prompt war detailliert gestaltet: Neben Rollendefinition und Klassifikationskriterien enthielt er Beispiele und klare Instruktionen, damit das Modell ausschließlich „Cubana“ oder „Línea“ auswählt.

    You are a highly specialized music expert with in-depth knowledge of all salsa dance variants and their musical characteristics.

    Reference songs and artists for Cuban Style ("Cubana"):
    - Los Van Van ("Soy Todo", "Sandunguera")
    - Havana D’Primera ("Pasaporte", "Me Dicen Cuba")
    - Maykel Blanco ("Recoge y Vete")
    - Buena Vista Social Club ("Chan Chan")
    - Adalberto Álvarez ("Para Bailar Casino")

    Reference songs and artists for Salsa Línea ("Línea"):
    - Marc Anthony ("Valió La Pena")
    - Frankie Ruiz ("Tu Con El")
    - Victor Manuelle ("Tengo Ganas")
    - Grupo Galé ("Ven a Medellín")

    Classification criteria:
    - "Cubana": Lyrics are playful, social, Cuban-themed, humorous, or political; music is polyrhythmic, improvisational, references Cuba or uses Cuban artists.
    - "Línea": Lyrics are romantic, dramatic, or about heartbreak; music is polished, commercial, typically NY/LA/PR based.

    Instructions:
    1. If the artist or song is in the Cubana reference list, answer "Cubana".
    2. If the artist or song is in the Línea reference list, answer "Línea".
    3. If genres/tags include "romantica", "salsa romantica", "balada", "latin pop", "bachata", or if lyrics are about love, heartbreak, or have a dramatic/romantic style, answer "Línea".
    4. If genres/tags include "son cubano", "timba", "charanga", "cuban", "guaguancó", or lyrics are playful/social/political, answer "Cubana".
    5. If mixed, **decide based on what dominates most (count the indicators).**
    6. If unsure, pick the most likely based on genres/tags and lyrics. NEVER output "Cubana" by default if unsure, always follow these rules.

    Your output must be exactly one of: Cubana or Línea.

    EXAMPLES:

    Song: "Valió La Pena"
    Artist: Marc Anthony
    Genres: salsa, salsa romantica
    Tags: salsa romantica, latin pop
    Lyrics: romantic
    Output: Línea

    Song: "Chan Chan"
    Artist: Buena Vista Social Club
    Genres: son cubano, salsa, guajira
    Tags: cuba, son cubano, timba
    Lyrics: references Cuba, social themes
    Output: Cubana

    Now classify this song:

    Song: {track_name}
    Artist: {artist_name}
    Album: {album_name}
    Release Date: {release_date}
    Genres: {artist_genres}
    Last.fm Info: {lastfm_info}
    Lyrics: {lyrics}
    prompt.txt
    You are a salsa music expert. Only reply with Cubana or Línea. Never reply with "Both" or "Unknown" or any explanation.
    system_prompt.txt

    Beobachtungen und Erkenntnisse

    • Die Klassifikation durch das LLM erfolgte rein textbasiert. Ein wirkliches Musikverständnis ist damit ausgeschlossen.
    • Die verfügbaren Metadaten reichten für die Stilunterscheidung nicht aus: Genre-Tags waren allgemein, Labels wie „Salsa Cubana“ oder „Línea“ fehlten meistens.
    • Songtexte lieferten selten eindeutige Anhaltspunkte, da die thematische Bandbreite in der Salsa sehr groß ist.
    • LLMs zeigten bei identischen Prompts und Daten zum Teil inkonsistente Ergebnisse. Unterschiedliche Modelle (z.B. DeepSeek, GPT-4o) kamen bei gleicher Eingabe teilweise zu unterschiedlichen Resultaten.
    • Auch durch das Hinzufügen weiterer Metadaten und externer Quellen wie Last.fm ließ sich die Qualität der Zuordnung nicht steigern. Die Ergebnisse blieben unzuverlässig, oft wurde pauschal „Cubana“ zugeordnet, unabhängig von tatsächlichen Stilmerkmalen.
    import spotipy
    from spotipy.oauth2 import SpotifyClientCredentials
    import requests
    import time
    import lyricsgenius

    # --------- Toggles ----------
    USE_LOCAL_DEEPSEEK = False # True = use local DeepSeek via Ollama, False = use OpenAI online
    USE_GENIUS = True # True = use Genius for lyrics, False = skip lyrics
    USE_LASTFM = True # True = use Last.fm for additional info, False = skip
    SHOW_LLM_PROMPT_DEBUG = False # True = show the full prompt with all fields, False = skip printing it

    # ---------- Spotify API credentials ----------
    CLIENT_ID = '6a....9f'
    CLIENT_SECRET = '56....7'
    PLAYLIST_ID = '1mZELHyKrXNyLOumaDiQwh'

    # ---------- Genius API credentials ----------
    GENIUS_TOKEN = 'cu....25'

    # ---------- Last.fm API credentials ----------
    LASTFM_API_KEY = 'd1....8g'

    # ---------- Local DeepSeek/Ollama ----------
    OLLAMA_URL = 'http://localhost:11434/api/generate'
    MODEL_LOCAL = 'deepseek-coder-v2'

    # ---------- OpenAI Online (GPT-3.5/4/4o) ----------
    OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions'
    OPENAI_API_KEY = 'sk....1B'
    MODEL_ONLINE = 'gpt-4o'

    # ---------- Prompt templates ----------
    with open('prompt.txt', encoding='utf-8') as f:
    PROMPT_TEMPLATE = f.read()
    with open('system_prompt.txt', encoding='utf-8') as f:
    SYSTEM_PROMPT = f.read()

    sp = spotipy.Spotify(
    auth_manager=SpotifyClientCredentials(
    client_id=CLIENT_ID,
    client_secret=CLIENT_SECRET
    )
    )

    genius = lyricsgenius.Genius(GENIUS_TOKEN, skip_non_songs=True, verbose=False, remove_section_headers=True)

    def clean_track_name(name):
    import re
    name = re.sub(r"\(.*?\)", "", name)
    name = name.replace("- En Vivo", "")
    name = name.strip()
    return name

    def get_lyrics(track_name, artist_name):
    if not USE_GENIUS:
    return ""
    cleaned_name = clean_track_name(track_name)
    song = genius.search_song(cleaned_name, artist_name)
    if song and getattr(song, "lyrics", None):
    return song.lyrics[:1000]
    song = genius.search_song(cleaned_name)
    if song and getattr(song, "lyrics", None):
    return song.lyrics[:1000]
    return ""

    def get_lastfm_info(track_name, artist_name):
    if not USE_LASTFM:
    return ""
    try:
    url_track = (
    f"https://ws.audioscrobbler.com/2.0/?method=track.getInfo"
    f"&api_key={LASTFM_API_KEY}&artist={requests.utils.quote(artist_name)}"
    f"&track={requests.utils.quote(track_name)}&format=json"
    )
    resp_track = requests.get(url_track, timeout=10)
    info_track = resp_track.json().get("track", {})
    tags = [t["name"] for t in info_track.get("toptags", {}).get("tag", [])]
    wiki = info_track.get("wiki", {}).get("summary", "")
    url_artist = (
    f"https://ws.audioscrobbler.com/2.0/?method=artist.getInfo"
    f"&api_key={LASTFM_API_KEY}&artist={requests.utils.quote(artist_name)}&format=json"
    )
    resp_artist = requests.get(url_artist, timeout=10)
    info_artist = resp_artist.json().get("artist", {})
    artist_tags = [t["name"] for t in info_artist.get("tags", {}).get("tag", [])]
    bio = info_artist.get("bio", {}).get("summary", "")
    extra = ""
    if tags:
    extra = f"Last.fm track tags: {', '.join(tags)}. "
    if artist_tags:
    extra = f"Last.fm artist tags: {', '.join(artist_tags)}. "
    if wiki:
    extra = f"Track summary: {wiki[:300]}... "
    if bio:
    extra = f"Artist bio: {bio[:300]}... "
    return extra.strip()
    except Exception:
    return ""

    def classify_track(
    track_name, artist_name, lyrics, album_name, release_date, artist_genres, lastfm_info, temperature=0.0
    ):
    prompt = PROMPT_TEMPLATE.format(
    track_name=track_name,
    artist_name=artist_name,
    album_name=album_name,
    release_date=release_date,
    artist_genres=artist_genres,
    lastfm_info=lastfm_info,
    lyrics=lyrics
    )
    # --- Print FULL prompt for debugging and transparency if toggle is active ---
    if SHOW_LLM_PROMPT_DEBUG:
    print("\n================= LLM PROMPT =================")
    print(f"Song: {track_name}")
    print(f"Artist: {artist_name}")
    print(f"Album: {album_name}")
    print(f"Release Date: {release_date}")
    print(f"Genres: {artist_genres}")
    print(f"Last.fm Info: {lastfm_info}")
    print(f"Lyrics: {lyrics[:400]}")
    print("==============================================\n")
    # ------------------------------------------------------
    if USE_LOCAL_DEEPSEEK:
    data = {
    "model": MODEL_LOCAL,
    "prompt": prompt,
    "stream": False
    }
    try:
    response = requests.post(OLLAMA_URL, json=data, timeout=120)
    if response.ok:
    answer = response.json().get('response', '').strip()
    if answer in ["Cubana", "Línea"]:
    return answer
    return None
    else:
    return None
    except Exception:
    return None
    else:
    headers = {
    "Authorization": f"Bearer {OPENAI_API_KEY}",
    "Content-Type": "application/json"
    }
    data = {
    "model": MODEL_ONLINE,
    "messages": [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": prompt}
    ],
    "max_tokens": 5,
    "temperature": temperature
    }
    try:
    response = requests.post(OPENAI_API_URL, headers=headers, json=data, timeout=120)
    if response.ok:
    answer = response.json()['choices'][0]['message']['content'].strip()
    if answer in ["Cubana", "Línea"]:
    return answer
    return None
    else:
    return None
    except Exception:
    return None

    # -- Fetch tracks --
    tracks = []
    offset = 0
    limit = 100
    while True:
    results = sp.playlist_items(PLAYLIST_ID, offset=offset, limit=limit)
    items = results.get('items', [])
    if not items:
    break
    tracks.extend(items)
    offset = len(items)

    for idx, item in enumerate(tracks):
    track = item.get('track')
    if not track:
    continue
    name = track.get('name', 'Unknown Title')
    artist = ', '.join([a.get('name', 'Unknown Artist') for a in track.get('artists', [])])
    album = track.get('album', {}).get('name', 'Unknown Album')
    release_date = track.get('album', {}).get('release_date', 'Unknown Date')
    print(f"\n[{idx 1}] {name} - {artist}")
    # Last.fm info
    #print(" Fetching Last.fm info ...")
    lastfm_info = get_lastfm_info(name, artist)
    # Lyrics
    #print(" Fetching lyrics ...")
    lyrics = get_lyrics(name, artist)
    # Genres for first artist
    artist_id = track.get('artists', [{}])[0].get('id', None)
    artist_genres = []
    if artist_id:
    try:
    artist_info = sp.artist(artist_id)
    artist_genres = artist_info.get('genres', [])
    except Exception:
    artist_genres = []
    genres_str = ', '.join(artist_genres)
    # Klassifikation (zeigt Prompt im classify_track)
    style = classify_track(
    track_name=name,
    artist_name=artist,
    album_name=album,
    release_date=release_date,
    artist_genres=genres_str,
    lastfm_info=lastfm_info,
    lyrics=lyrics
    )
    if style not in ["Cubana", "Línea"]:
    style = classify_track(
    track_name=name,
    artist_name=artist,
    album_name=album,
    release_date=release_date,
    artist_genres=genres_str,
    lastfm_info=lastfm_info,
    lyrics=lyrics,
    temperature=0.3
    )
    if style not in ["Cubana", "Línea"]:
    style = "Cubana"
    print(f" => {style}")
    time.sleep(1.5)
    analyzer.py

    Fazit
    Eine zuverlässige Musikstil-Klassifikation nur anhand von Metadaten und Lyrics ist mit aktuellen LLMs nicht möglich. Die Resultate sind inkonsistent, die Fehlerquote hoch und die Zuordnungen meist nicht nachvollziehbar. Zusätzliche Metadaten und aufwändige Prompt-Templates mit Beispielen und Instruktionen konnten die Grenzen der Modelle nicht überwinden. Für eine präzise stilistische Musik-Klassifikation bleibt menschliches Fachwissen oder eine spezifische musikalische Analyse weiterhin erforderlich.