Natural Language API × Spotify APIを利用した音楽レコメンドアプリケーション

# はじめに
プログラミングスクールで半年間プログラミングやAI・データ分析について学びました。
今回は学んだことを活かして音楽レコメンドアプリを開発しましたので、その振り返り
として記事にまとめたいと思います。

アプリ開発では、たくさんのQiitaを拝見させていただきました。（ありがとうございます！）
この記事もこれからアプリ開発をされる方の役に少しでも立てれば幸いです。

# 解決したい課題
私は、自身の好きなアーティストの音楽しか聴きません。
しかし、世の中には知らないだけで、たくさん良い音楽があると思い、それらを発見したいと思いました。
また、今日の気分や感情をもとに音楽をレコメンドができれば、より自身の状況に合う最適な音楽に出会えるのではないかと考えました。

# 主な機能
- 入力されたテキストをGoogle Cloud Natural Language APIで感情分析し、感情スコアを返す
- Spotify APIから指定したプレイリストを取得する
- 感情スコアと指定したプレイリストの各音楽のエネルギー属性の差を計算する
- 最小値の差だった音楽の情報（音楽名、アーティスト名、URL）をチャットボットに返す
- UIはチャットボット形式

# Google Cloud Natural Language APIについて
Natural Language APIは、Google Cloudが提供する自然言語処理APIです。
感情分析以外にも、構文解析やエンティティ分析等も可能です。
Google Cloudは無料トライアルも用意されており、米300ドルまで無償です。（2024年9月）
感情分析機能では、テキストの感情スコアを1 ~ -1の値でポジティブかネガティブかニュートラルかを判断します。
API連携の手順では下記を参考にしました。
[Natural Language AI](https://cloud.google.com/natural-language?hl=ja)
[Natural Language APIを触ってみる【GCP】](https://qiita.com/yyyyy__/items/8f4f14fe54597d4dffa0)

# Spotify APIについて
Spotify APIは、Spotifyが提供する音楽情報のAPIです。
音楽情報の取得、再生、検索等が可能です。
利用するには、事前にSpotifyのアカウントを作成する必要があります。
Spotify APIも基本的には無償でした。（条件を超えると有償です）
音楽再生機能を実装する場合には、プレミアムアカウント（有償）が必要です。

今回、感情スコアとの差を計算するエネルギー属性とは、
Spotify側で用意している各音楽のエネルギーの高さを数値化したデータです。
1だとエネルギーが高いハイテンションの曲、0だとしっとりしたバラード曲が該当します。

API連携の手順は下記を参考にしました。
[Web API](https://developer.spotify.com/documentation/web-api)
[【Python】Spotify Web APIで楽曲データを分析してみる](https://note.com/bunsekiya_tech/n/n2e3151a6b8fa)
[Spotipy: PythonでSpotify Web API を利用する](https://qiita.com/toxic_apple/items/20a0fce60e337bbe8716)

# 実行環境
パソコン：MacBook Air
開発環境：ローカル環境（venvで仮想環境構築）
[venv --- 仮想環境の作成](https://docs.python.org/ja/3/library/venv.html)
言語：Python 3.8.16
フレームワーク:Django 4.2.16
ライブラリ：numpy 1.24.4, spotipy 2.24.0, google.cloud.language_v2 2.14.0, 
python-dotenv 1.0.1, os, Path

# 開発の流れ
- 環境変数にAPIキーを設定
- APIキーを取得する設定
- ライブラリのインポート
- Natural Language APIを利用した感情分析メソッドの実装
- 感情スコアと指定したプレイリストの各音楽のエネルギー属性の差を計算するメソッドの実装
- Spotify APIを利用した音楽レコメンドを行うメソッドの実装
※ロジックの方を中心にご紹介しております。

# 開発の過程
以下の手順で音楽をレコメンドするロジックを書きました。

# 環境変数にAPIキーを設定
```Python:.env
# Spotifyの認証情報を設定
SPOTIFY_CLIENT_ID = '自身のクライアントIDを記載'
SPOTIFY_CLIENT_SECRET = '自身のクライアントシークレットを記載'

# Google Cloud Natural Language APIパスを設定
GOOGLE_APPLICATION_CREDENTIALS='Google CloudからダウンロードしたAPIキーのパスを記載'
```
SpotifyのAPIキーはアカウント登録とアプリ作成を行えば取得できます。
詳しくは「Spotify APIについて」に添付したURLをご参照ください。
わかりやすく書かれており、私も参考にしました。

Google Cloudも、アカウント作成→API有効化→サービスアカウント設定の順番で
APIキーのJSONファイルがダウンロードできます。
パスには、JSONファイルが格納されているフォルダのパスを記載します。
詳しい手順は「Google Cloud Natural Language APIについて」に添付したURLを
ご参照ください。私もそちらを参考にしました。

# APIキーを取得する設定
```python:settings.py
import os
from dotenv import load_dotenv 

# .envファイルの内容を読み込む
load_dotenv()

# 環境変数からAPIキーを取得
SPOTIFY_CLIENT_ID = os.getenv('SPOTIFY_CLIENT_ID')
SPOTIFY_CLIENT_SECRET = os.getenv('SPOTIFY_CLIENT_SECRET')

google_credentials_path = os.getenv('GOOGLE_APPLICATION_CREDENTIALS')

# Google Cloud APIの認証で利用する
os.environ['GOOGLE_APPLICATION_CREDENTIALS']
```

# ライブラリのインポート
実行したコード
```Python:views.py
from django.shortcuts import render 
from django.http import JsonResponse 
from django.http import HttpResponse
from django.conf import settings 
from google.cloud import language_v2
import os
import spotipy 
from spotipy import Spotify 
from spotipy.oauth2 import SpotifyClientCredentials 
import numpy as np 
```

# 感情分析を行うメソッド
```Python:views.py
# 感情分析を行う
def analyze_sentiment(text_content: str) -> dict:
    """テキストの感情を分析し、結果を返す関数"""
    client = language_v2.LanguageServiceClient()

    document_type_in_plain_text = language_v2.Document.Type.PLAIN_TEXT
    language_code = 'ja'

    document = {
        'content': text_content,
        'type_': document_type_in_plain_text,
        'language_code': language_code,
    }

    encoding_type = language_v2.EncodingType.UTF8

    response = client.analyze_sentiment(
        request={'document': document, 'encoding_type': encoding_type}
    )

    return response.document_sentiment.score

```
公式ドキュメントのプログラム例を参考にしました。
[感情分析 | Cloud Natural Language API](https://cloud.google.com/natural-language/docs/analyzing-sentiment?hl=ja)

# 感情スコアとエネルギー属性の差を計算するメソッド
```Python:views.py
# 感情スコアとプレイリストのenergy属性の差を計算し、最小値のenergy属性の数値を返す
def get_nearest_value(energy_list, num):
    idx = np.abs(np.asarray(energy_list) - num).argmin()
    return energy_list[idx]
```

# 音楽レコメンドを行うメソッド
```Python:views.py
def reply(input):
    try:
        # クライアントIDとクライアントシークレットを設定
        client_id = settings.SPOTIFY_CLIENT_ID
        client_secret = settings.SPOTIFY_CLIENT_SECRET

        # 認証情報を設定
        client_credentials_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)
        sp = Spotify(client_credentials_manager=client_credentials_manager)

        # Spotifyプレイリストの選択
        playlist_id = '6KfKvkN7YEUFTwJHkczxdb'
        results = sp.playlist_tracks(playlist_id)

        if not results['items']:
            return HttpResponse('<h2>プレイリストが空です。</h2>', status=400)
    
        # プレイリスト内の全トラックIDを取得
        track_ids = [track['track']['id'] for track in results['items']]

        # 一度にまとめてトラックのaudio_featuresを取得
        audio_features = sp.audio_features(track_ids)

        track_info = []

        for track, audio_feature in zip(results['items'], audio_features):
            if audio_feature:
                song_energy = audio_feature['energy']
                track_info.append({
                    'name': track['track']['name'],
                    'artist': track['track']['artists'][0]['name'],
                    'url': track['track']['external_urls']['spotify'],
                    'energy': song_energy
                })

        # 感情スコアに最も近いenergy属性を見つける
        nearest_energy = get_nearest_value([track['energy'] for track in track_info], float(input))

        # レコメンド楽曲情報を取得
        recommended_music = next(track for track in track_info if track['energy'] == nearest_energy)

        return recommended_music
    
    except spotipy.exceptions.SpotifyException as e:
        print(f'Spotify APIエラー: {e}')
        return HttpResponse('<h2>Spotify APIに問題が発生しました。</h2>', status=500)
```
プレイリストは下記を指定しました。
少し前の音楽がまとめられているため、現在の音楽とは違って新鮮に感じます。
[2000年代 J-POPヒットソング100](https://open.spotify.com/playlist/6KfKvkN7YEUFTwJHkczxdb)

プレイリストを指定する際は、プレイリストURLのplaylist/以降を記載すると取得できます。
https://open.spotify.com/playlist/6KfKvkN7YEUFTwJHkczxdb
→ 6KfKvkN7YEUFTwJHkczxdbを記載する。

# フォームからの入力を処理する
```Python:views.py
def bot_response(request):
    if request.method == 'POST':
        input_text = request.POST.get('input_text')

        # 入力されたテキストが空かどうかチェック
        if not input_text or input_text.strip() == "":
            return HttpResponse('<h2>テキストが入力されていません。</h2>', status=400)
    
        # 感情分析を実行
        sentiment_result = analyze_sentiment(input_text)

        # 感情スコアを元にSpotifyからレコメンド取得
        recommend_result = reply(sentiment_result)

        # JSON形式に変換
        response_data = {
            'recommend_music': recommend_result['name'],
            'recommend_artist': recommend_result['artist'],
            'recommend_url': recommend_result['url']
        }

        return JsonResponse(response_data, safe=False)
    else:
        return HttpResponse('<h2>空のデータを受け取りました。</h2>', status=400)
```

# 実行結果
実行結果を表示します。
1. 今日はいい日でした。　→ 感情スコア：0.9290000200271606
2. 食事中にテレビを見ていたら舌を噛みました。　→ 感情スコア：-0.3140000104904175

1番目の方は感情スコアも高めのため、楽曲もエネルギッシュな曲がレコメンドされました。
2番目の方は感情スコアがマイナスになっているため、テンポが遅いバラード系がレコメンドされたました。
![スクリーンショット 2024-09-30 23.31.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3897801/f3ed4b5a-5245-56b7-21cb-2d015d5e5183.png)

# 実行結果2
3\. 久しぶりの...新鮮でした。 → 感情スコア：0.6930000185966492
4\. フォームを空の状態で入力

3番目は長い文章で入力をしてみましたが、感情スコアは問題なく返されました。
感情スコアはポジティブ寄りでしたが、レコメンドはしっとりとした音楽が返されたました。
4番目はフォームを空欄にして送信しましたが、問題なくエラー表示が出ました。
![スクリーンショット 2024-09-30 23.34.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3897801/97d598ba-3b00-ecce-a440-06127a20b0e5.png)

# 課題
エネルギー属性のみでレコメンドロジックを計算していたため、レコメンドの精度が低かった場合がありました。
エネルギー属性はハイテンションであったり、逆にテンションが低かったりした際には感情スコアと数値が似る傾向にあり、適切な音楽がレコメンドされました。
ただ、どちらか微妙なライン（感情スコアが0.6とか）になると精度が悪いと感じました。

また、感情スコアがマイナスになると、選ばれる音楽も決まった曲になる点も改善が必要だと考えます。（今回の場合は、感情スコアがマイナスになると森山直太朗さんのさくら（独唱）ばかりレコメンドされる）

エネルギー属性以外の属性も含めてレコメンドの計算をできるようにすると精度が上がると考えられるので今後挑戦してみたいと思います。
そのほか、Spotifyのレコメンド機能が利用できるRecommendations APIを活用することも検討します。

# まとめ
これまで学んだことを活かして、音楽レコメンデーションアプリを実装しました。
目的であった状況に合う最適なレコメンドまでは至りませんでしたが、テンションが高い・低いといった場合には、気分や感情に近いレコメンドができたのではないかと思います。

また、テキストの感情分析では、想像よりも精度が高いと感じました。特に感情スコア以外にも感情の強さを測るマグニチュードという指標も同時に返してくれるため、より詳細にテキストの感情を読み取れるようになっていると驚きました。（今回のアプリでは感情スコアのみを利用しております）

今後は協調フィルタリングやコンテンツベースフィルタリングなどのAI技術も学習し、より精度の高いレコメンド機能を実装してみたいと思います。

アプリ開発は今回が初めてでしたが、うまく動作できホッとしました。
実際に手を動かすことでコードへの理解力が少し身についたと感じます。
これからもたくさんコードを書いて勉強していきたいと思います。