AIが音楽に変える！「text2music」でツイートから音楽を作ってみよう


## はじめに
佐藤([@rayuron](https://twitter.com/rayuron))です。実務では推薦に関わる仕事をしています。
[Stable Diffusion](https://github.com/CompVis/stable-diffusion) や [ChatGPT](https://chat.openai.com/chat) を代表に AI が注目されることが多い毎日です。
趣味で音楽制作をしているので、言語を音楽に変換する text2music AI の [Mubert](https://mubert.com/) に興味を持っていました。ふと、自分の過去のツイートを音楽に変換するとどうなるのか興味が湧いたのでやってみることにします。
趣味では `House, Techno, Disco, Electro, Future` のようなタグで表されるの音楽を作っているので、今回生成される音楽との関連性が気になるところです。

▼ 本記事のタイトルは [ChatGPT](https://chat.openai.com/chat) という AI チャットボットに決めてもらいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518616/c9e174b0-cf9a-fb07-7049-af2b70626598.png)

## ゴール
自分の過去のツイートから音楽を生成することです

▼ツイートのイメージ
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518616/b466fa1b-f5f0-b4cb-cf3e-43ed659cde48.png">

ここで、自分はツイートをあまりしないことに気づきました。
普段ツイートをしないことがネックとなったので今回はリツイートも含めて音楽を作ります。
AI やスポーツについてよく言及しているので、どういう音楽が生成されるのか楽しみです。

## 流れ
やること + 実現方法については以下です。
日本語の過去のツイートをもとに音楽を生成したいのですが、 Mubert の入力文字列は英語である必要があるため今回は翻訳 AI として DeepL を組み合わせて使うことにしました。
```mermaid
graph TB
    1.ツイートを取得する-->2.取得したツイートを英語にする-->3.英語にしたツイートを前処理する-->4.前処理したツイートを音楽にする
    Twitter_API-->DeepL_API-->tweet_preprocessor-->Mubert_API
```

## 実装

:::note

- Jupyter Lab を使用して実装します。
- API の申請方法などの参考資料は最後にまとめて掲載しています。
- 必要に応じて環境にライブラリをインストールしてください。

:::

### 1.ツイートを取得する(Twitter API)
:::note
Twitter API の利用申請が必要です。
:::

Twitter API を使用して自分の過去のツイートを取得します。

<details><summary>Code</summary>

▼`key` や `user_id` には各々取得したものを入れてください。

```python
from requests_oauthlib import OAuth1Session

# Set keys
API_KEY, SECRET_KEY, ACCESS_TOKEN, ACCESS_TOKEN_SECRET = '', '', '', ''
auth = OAuth1Session(API_KEY, SECRET_KEY, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

# Set params
params ={'max_results': 100}
user_id = ''

# Request
res = auth.get(url=f'https://api.twitter.com/2/users/{user_id}/tweets',params=params)
tweets = json.loads(res.text)
tweets_text = [x['text'] for x in tweets['data']]
```

</details>

#### ▼Output

```python
['関わらせて頂きましたので是非ご覧ください。 https://t.co/oBh4QtEfd9', 'RT @OpenAI: We’ve just launched the DALL·E API so developers can integrate DALL·E directly into their own apps and ...]
```

### 2.取得したツイートを英語にする(DeepL API)
:::note
DeepL API の利用にはアカウントの作成が必要です。
:::
DeepL API を使用して取得した日本語のツイートを英語化していきます。
今回は DeepL API FREE プランを使用しています。

<details><summary>Code</summary>

▼`key`には取得したものを入れてください。
```python
import requests

DEEPL_API_KEY = ''

tweets_text_en = []
for tweet_text in tweets_text:
  params = {
    "auth_key": DEEPL_API_KEY,
    "text": tweet_text,
    "source_lang": 'JA',
    "target_lang": 'EN'
  }
  request = requests.post("https://api-free.deepl.com/v2/translate", data=params)
  result = request.json()
  tweets_text_en.append(result["translations"][0]["text"])
```

</details>

#### ▼Output
```python
['Please take a look at our involvement. https://t.co/oBh4QtEfd9', "RT @OpenAI: We've just launched the DALL-E API so developers can integrate DALL-E directly into their own apps and ...]
```

### 3.英語にしたツイートを前処理する(tweet preprocessor)
必要のない URL や記号、絵文字などを削除します。
ツイートの前処理には、ツイート前処理ライブラリ [tweet preprocessor](https://github.com/s/preprocessor) を利用することにしました。 `pip` でインストール可能です。
また、Mubert の入力に配列は渡せないので、結合して一つの文字列にします。

<details><summary>Code</summary>

```python
import preprocessor as p
tweets_text_en = [p.clean(t) for t in tweets_text_en]
prompt = ' '.join(tweets_text_en)
```

</details>

#### ▼Output
```python
'Please take a look at our involvement. : We\'ve just launched the DALL-E API so developers can integrate DALL-E directly into their own apps and ...'
```

### 4.前処理したツイートを音楽にする(Mubert)

:::note
Mubert API の利用にはアカウントの作成が必要です。
:::
Mubert の利用方法について GitHub に[ノートブック](https://github.com/MubertAI/Mubert-Text-to-Music/blob/main/Mubert_Text_to_Music.ipynb)が公開されています。
流れは以下です。

```mermaid
graph TD
    prompt:結合された過去のツイート-->|sentence-transformers|prompt_embeddings-->similar_tags
    tags:事前定義されたジャンルなどを表すタグ-->|sentence-transformers|tags_embeddings-->similar_tags-->|text2music|music
```
:::note
**prompt**
prompt は入力文字列のことです。今回は前段で結合したツイートです。
```python
'Please take a look at our involvement. : We\'ve just launched the DALL-E API so developers can integrate DALL-E directly into their own apps and ...'
```
**tags**
tags は事前定義されており、ジャンルや雰囲気を表すワードです。
```python
['tribal', 'action', 'kids', 'neo-classic', 'run 130', 'pumped', 'jazz / funk', 'ethnic', 'dubtechno', 'reggae', 'acid jazz',...]
```
:::

1. 初めに、学習済みの Sentence Transformers([all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)) を使用して prompt, tags からそれぞれ embeddings を取得します。
2. 次に、取得した prompt embeddings と tags embeddings のコサイン類似度を計算して類似度の高い tag を N(今回は10) 個取得します。
3. 最後に、取得した類似度の高い tags を学習済みの text2music のモデルに入力して音楽を生成します。

<details><summary>Code</summary>

**Setup Environment**

```python
import subprocess, time
print("Setting up environment...")
start_time = time.time()
all_process = [
    ['pip', 'install', 'torch==1.12.1+cu113', 'torchvision==0.13.1+cu113', '--extra-index-url', 'https://download.pytorch.org/whl/cu113'],
    ['pip', 'install', '-U', 'sentence-transformers'],
    ['pip', 'install', 'httpx'],
]
for process in all_process:
    running = subprocess.run(process,stdout=subprocess.PIPE).stdout.decode('utf-8')

end_time = time.time()
print(f"Environment set up in {end_time-start_time:.0f} seconds")
```

**Define Mubert methods and pre-compute things**

```python
import numpy as np
from sentence_transformers import SentenceTransformer
minilm = SentenceTransformer('all-MiniLM-L6-v2')

mubert_tags_string = 'tribal,action,kids,neo-classic,run 130,pumped,jazz / funk,ethnic,dubtechno,reggae,acid jazz,liquidfunk,funk,witch house,tech house,underground,artists,mystical,disco,sensorium,r&b,agender,psychedelic trance / psytrance,peaceful,run 140,piano,run 160,setting,meditation,christmas,ambient,horror,cinematic,electro house,idm,bass,minimal,underscore,drums,glitchy,beautiful,technology,tribal house,country pop,jazz & funk,documentary,space,classical,valentines,chillstep,experimental,trap,new jack swing,drama,post-rock,tense,corporate,neutral,happy,analog,funky,spiritual,sberzvuk special,chill hop,dramatic,catchy,holidays,fitness 90,optimistic,orchestra,acid techno,energizing,romantic,minimal house,breaks,hyper pop,warm up,dreamy,dark,urban,microfunk,dub,nu disco,vogue,keys,hardcore,aggressive,indie,electro funk,beauty,relaxing,trance,pop,hiphop,soft,acoustic,chillrave / ethno-house,deep techno,angry,dance,fun,dubstep,tropical,latin pop,heroic,world music,inspirational,uplifting,atmosphere,art,epic,advertising,chillout,scary,spooky,slow ballad,saxophone,summer,erotic,jazzy,energy 100,kara mar,xmas,atmospheric,indie pop,hip-hop,yoga,reggaeton,lounge,travel,running,folk,chillrave & ethno-house,detective,darkambient,chill,fantasy,minimal techno,special,night,tropical house,downtempo,lullaby,meditative,upbeat,glitch hop,fitness,neurofunk,sexual,indie rock,future pop,jazz,cyberpunk,melancholic,happy hardcore,family / kids,synths,electric guitar,comedy,psychedelic trance & psytrance,edm,psychedelic rock,calm,zen,bells,podcast,melodic house,ethnic percussion,nature,heavy,bassline,indie dance,techno,drumnbass,synth pop,vaporwave,sad,8-bit,chillgressive,deep,orchestral,futuristic,hardtechno,nostalgic,big room,sci-fi,tutorial,joyful,pads,minimal 170,drill,ethnic 108,amusing,sleepy ambient,psychill,italo disco,lofi,house,acoustic guitar,bassline house,rock,k-pop,synthwave,deep house,electronica,gabber,nightlife,sport & fitness,road trip,celebration,electro,disco house,electronic'
mubert_tags = np.array(mubert_tags_string.split(','))
mubert_tags_embeddings = minilm.encode(mubert_tags)

from IPython.display import Audio, display
import httpx
import json

def get_track_by_tags(tags, pat, duration, maxit=20, autoplay=False, loop=False):
  if loop:
    mode = "loop"
  else:
    mode = "track"
  r = httpx.post('https://api-b2b.mubert.com/v2/RecordTrackTTM', 
      json={
          "method":"RecordTrackTTM",
          "params": {
              "pat": pat, 
              "duration": duration,
              "tags": tags,
              "mode": mode
          }
      })

  rdata = json.loads(r.text)
  assert rdata['status'] == 1, rdata['error']['text']
  trackurl = rdata['data']['tasks'][0]['download_link']

  print('Generating track ', end='')
  for i in range(maxit):
      r = httpx.get(trackurl)
      if r.status_code == 200:
          display(Audio(trackurl, autoplay=autoplay))
          break
      time.sleep(1)
      print('.', end='')

def find_similar(em, embeddings, method='cosine'):
    scores = []
    for ref in embeddings:
        if method == 'cosine': 
            scores.append(1 - np.dot(ref, em)/(np.linalg.norm(ref)*np.linalg.norm(em)))
        if method == 'norm': 
            scores.append(np.linalg.norm(ref - em))
    return np.array(scores), np.argsort(scores)

def get_tags_for_prompts(prompts, top_n=10, debug=False):
    prompts_embeddings = minilm.encode(prompts)
    ret = []
    for i, pe in enumerate(prompts_embeddings):
        scores, idxs = find_similar(pe, mubert_tags_embeddings)
        top_tags = mubert_tags[idxs[:top_n]]
        top_prob = 1 - scores[idxs[:top_n]]
        if debug:
            print(f"Prompt: {prompts[i]}\nTags: {', '.join(top_tags)}\nScores: {top_prob}\n\n\n")
        ret.append((prompts[i], list(top_tags)))
    return ret
```

**Get personal access token in Mubert and define API methods**
`email` には作成したアカウントのメールアドレスを入力してください。
以下の `token` は GitHub で公開されています。

```python
email = '' #@param {type:"string"}

r = httpx.post('https://api-b2b.mubert.com/v2/GetServiceAccess', 
    json={
        "method":"GetServiceAccess",
        "params": {
            "email": email,
            "license":"ttmmubertlicense#f0acYBenRcfeFpNT4wpYGaTQIyDI4mJGv5MfIhBFz97NXDwDNFHmMRsBSzmGsJwbTpP1A6i07AXcIeAHo5",
            "token":"4951f6428e83172a4f39de05d5b3ab10d58560b8",
            "mode": "loop"
        }
    })

rdata = json.loads(r.text)
assert rdata['status'] == 1, "probably incorrect e-mail"
pat = rdata['data']['pat']
print(f'Got token: {pat}')
```
**Generate some music 🎵**
前段の処理で `prompt` には既に過去のツイートが代入されているので、`# prompt = ''` をコメントアウトしています。

```python
# prompt = '' #@param {type:"string"}
duration = 60 #@param {type:"number"}
loop = False #@param {type:"boolean"}

def generate_track_by_prompt(prompt, duration, loop=False):
  _, tags = get_tags_for_prompts([prompt,])[0]
  try:
    print(tags)
    get_track_by_tags(tags, pat, duration, autoplay=True, loop=loop)
  except Exception as e:
    print(str(e))
  print('\n')
  
generate_track_by_prompt(prompt, duration, loop)

```

</details>

上のコードを実行してみると、自分の過去のツイートと類似度の高い tags は以下でした。
```python
[
  'sensorium', 'electronica', 'ambient', 'edm', 'future pop', 
  'chillrave / ethno-house', 'gabber', 'italo disco', 'idm', 'electro'
]
```
これは、、、
主観的ですが、自分が興味のある `House, Techno, Disco, Electro, Future` と意味の近い tags になっていると思いました。期待大です。

## 結果
過去のツイートから生成された音楽は以下の様になりました。
実行毎に結果が変わるので 10 回ほど生成して良いと思った2曲をピックアップします。
*アートワークの画像は Stable Diffusion に tags を入れて生成した画像を使用しました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/rd74l9pAjcU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/9jrvtUzKleU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


最高に好きな曲という感じではありませんが、自分が好きなジャンルの曲を生成することができました。しかし、ここまでの曲ができるとは想像していませんでした、、

## おわりに

自分の過去のツイートから音楽を生成するという実験をしました。
自分の場合、普段音楽のツイートをしないのでデータ不足を感じていましたが、Mubert で生成された tags で自分の好みが当てられたので感動しました。同時に過去のツイートからその人が好きな音楽ジャンルを当てることができるという可能性を感じました。tags から生成された音楽に関しても、想像していたクオリティーを超えてきたので感動しました。サンプリングして遊んでみようと思います。Twitter のデータを株式市場の予測に使用する研究は見たことがあったのですが、音楽の分野においてもマルチモーダル AI などに役に立ちそうな可能性を感じました。

また、今回開発する上でやりたいことに対して利用可能な API が提供されているのがすごく助かりました。
さらに今回、記事のタイトル決めは AI ですし曲のアートワーク作成も AI です。
AI の民主化が進んできていることを実験と同時に実感しました。

## 参考資料
- Twitter API 利用申請: https://developer.twitter.com/en/docs/twitter-api/
- DeepL API 利用申請: https://www.deepl.com/ja/pro-api?cta=header-pro-api/
- tweet-preprocessor: https://pypi.org/project/tweet-preprocessor/
- Mubert API 利用申請: https://mubert.com/