🎵

PlaylistGPT: ChatGPTにSpotifyの曲でプレイリストを作らせる

2023/03/13に公開

初手宣伝

https://qwegat-chatgptplaylist-main-gyigsk.streamlit.app/
ここで試せます。
今回、いろいろなサービス(StreamLit、Spotify API、OpenAI API)を縦断してシステムを構築したのですが、そのすべてに1ドルも払っていないので、おそらく試せるのは数日以内でしょう。裏返せば、試すなら今しかないということです。
さあ。
https://qwegat-chatgptplaylist-main-gyigsk.streamlit.app/

基本的な理屈

もう寝たいので、いつもの記事のように丁寧な構成はしません。理屈だけ話します。

周辺技術

  • 基盤はOpenAI APIです
  • フロントエンドをやる体力がなかったので、Streamlit(Pythonだけで書けてめっちゃ便利なヤツ)に全部任せました
  • Spotify APIを検索用に使っています

曲を選ばせる

『曲を選ばせる』と言っても色々なアプローチがあると思いますが、今回は

  • プレイリストの「テーマ」を文字列で入力し、
  • それを元にAIに検索ワードを10個くらい考えさせて、
  • 10個のワードで検索して出てきた曲を上から10個ずつくらいリストアップし、
  • 完成した100曲分のリストからAIに指定の曲数を選ばせる

という感じで行きます。

1. 検索ワードを考えさせる

これは厳密にはChatGPTではなく、OpenAIのInsert APIを使用しています。

10 words to search for on Spotify when you want to create a '{ここにテーマ}' themed songs playlist:
1. [Insert]

というプロンプトを投げて実行するとうまいこと補完してくれるので、それをちょちょっとパースしてイエーイします(かなりの説明の放棄)。
コード:

import openai
import os
import re

openai.api_key = os.getenv("CHATGPT_API_KEY")
sKillReg = re.compile(r"^\d+\. ", re.MULTILINE)
def getSearchWords(theme):
    res = openai.Completion.create(
        model="text-davinci-003",
        prompt=f"10 words to search for on Spotify when you want to create a '{theme}' themed songs playlist:\n1. ",
        suffix="",
        temperature=1,
        max_tokens=256,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0
    )
    return sKillReg.sub("", res["choices"][0]["text"]).split("\n")

2. 検索する

Spotify APIをPythonで叩くためのモジュールであるSpotipyを使いました。
前述のとおり各検索ワードで10曲ずつくらいピックしています。ついでにBPMを楽曲情報の一つとして保存してみたりしましたが、AIがBPMを考慮しているかは不明です。

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
ccm = SpotifyClientCredentials(client_id=SPOTIFY_CLIENT_ID, client_secret=SPOTIFY_CLIENT_SECRET)
spotify = spotipy.Spotify(client_credentials_manager=ccm)

def searchMusic(words, additional_word, market):
    id_list = []
    meta_list = []
    while len(words) == 1:
        words = words[0]
    for word in words:
        sq = word
        if len(additional_word):
            sq += " " + additional_word
        res = spotify.search(sq, limit=10, offset=0, type='track', market=market)
        for track in res['tracks']['items']:
            id_list.append(track['id'])
            meta_list.append({
                "id": track["id"],
                "title": track["name"],
                "artist":",".join([x["name"] for x in track["artists"]]),
                "uri": track["uri"],
            })
    features = spotify.audio_features(id_list)
    c = 0
    for f in features:
        for k in ["tempo", "energy", "instrumentalness", "duration_ms"]:
            meta_list[c][k] = f[k]
        c += 1
    return meta_list

3. 検索結果から選ばせる

今度こそChatGPTです。

I am thinking of making a playlist about '{テーマ}'. I just searched Spotify for songs to put in the playlist and found the following {検索して手に入った曲数} songs. Please choose {プレイリストの曲数} songs from these {検索して手に入った曲数} songs to make a playlist. The playlist should be in the form of a Markdown numbered list,  Don't just arrange the songs, rearrange them with the order in mind. Do not include BPM in the result

というプロンプトの後ろに先ほどの検索結果を改行区切りで接続してやると、イイ感じにプレイリストを出してくれます。

def createPlayList(theme,meta_list,tracks_length):
    prompt = f"I am thinking of making a playlist about '{theme}'. I just searched Spotify for songs to put in the playlist and found the following {len(meta_list)} songs. Please choose {tracks_length} songs from these {len(meta_list)} songs to make a playlist. The playlist should be in the form of a Markdown numbered list,  Don't just arrange the songs, rearrange them with the order in mind. Do not include BPM in the result.\n\n"
    c = 1
    for i in meta_list:
        prompt += f"No{c}: {i['title']} - {i['artist']} BPM:{i['tempo']}\n"
        c += 1
    res = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {
                "role": "system",
                "content": prompt
            }
        ]
    )
    ressp = sKillReg.sub("", res["choices"][0]["message"]["content"]).split("\n")
    result_ids = []
    for m in ressp:
        sp = m.split(" - ")
        title_match = list(filter(lambda x:x["title"] == sp[0],meta_list))
        if len(title_match):
            title_artist_match =  list(filter(lambda x:x["artist"] == sp[1],title_match))
            if len(title_artist_match):
                result_ids.append(title_artist_match[0])
            else:
                result_ids.append(title_match[0])

    return result_ids #result_idsと言っているが実際はresult 直すのがめんどいです(カス)

4. 繋げる

あとはこれらを繋げてやれば終わりです。

def generate(theme,tracks_length,market,additional_word):
    words = getSearchWords(theme),
    searchResult = searchMusic(words,additional_word,market)
    playlist = createPlayList(theme,searchResult,tracks_length)
    return playlist

作例

地球破壊爆弾

ニコニコ動画

野球

課題

  • 海外の知らない曲が出てきすぎ
  • プレイリストという割にSpotifyの曲を羅列しているだけ
  • マネタイズしてない
  • もっと選定におけるパラメータを増やしたい
  • 歌詞情報とかも参照させたい

Discussion