ChatGPTと仲良くなるための旅(2)

今回の目的

 前回作ったなんちゃって作曲プログラムを改良してみる。ChatGPTに関するドキュメント読んだり自分なりに調べたりした結果、プログラムを書くときはChatGPTが苦手な部分を人間が補って、得意な部分をメインでお任せするのが良い、という感触を得たのでそれを試してみる感じだ。

 変更イメージはこんな感じ。

ChatGPTに直接コード生成させないようにした

 前回の構成との大きな変更は、「プログラムに不確定性を含めない」という点だ。前の構成だと人間の作曲オーダーをInputにしてMIDI作成プログラムそのものをChatGPTに書かせていたが、これだとプログラム実行エラーが出まくってお話にならなかった。前回は試行回数増やせばどうにか…とか書いていたが、試した結果、絶対にエラーの出ないコードを吐かせることがあまりにも難しくて断念した。かける労力にリターンが見合ってなさそうだったのでやめた…(‘_’)

 そういうわけで、ChatGPTにはMIDIの元ネタになるjsonを作るところまでやってもらうことにした。MIDI作成はjsonをReadしてmidoライブラリにあわせて突っ込むだけなので、ここはプログラムを人間が作る。ここにAIの不確定性を入れる意味はあまりない(※)ので、ChatGPTくんには作曲行為に集中してもらうのだ。

※意味があるとすると、作曲されたデータによってライブラリを使い分けたいとかそういう場合だが、自分のレベルでは関係ないので…

 参考:前回の構成

従来はMIDIファイルを作るところのコードまで一気に書かせていた。いま見るとヤババ(‘_’)

作業内容

 新しい構成で人間が作らなければならないものは3つある。

  • 作曲オーダーをもとに指定のフォーマットでjson出力してくれるChatGPT
  • jsonデータをReadしてMIDIファイルを生成するプログラム
  • MIDIファイルをMP3変換するプログラム

 最後の一つは前回作ったものを流用できるので、実質は2つだ。ヤッタネ!

作曲オーダーをもとに指定のフォーマットでjson出力してくれるChatGPT

 無からjsonフォーマットを考えるのは大変だし、自分には作曲のノウハウが全くない。なのでChatGPTくんに協力してもらうことにした。まずは前回作ったPython作曲プログラムを読ませて、このプログラムにあわせたjsonデータを書かせてみる。

省略しているがこの前にコードを読ませている

 おっ、なかなかそれっぽいフォーマットが出てきた。ただこのままだとmidoにパラメータを渡すときに怒濤の変換処理が必要になりそうなので、そのへんを改良する必要がありそうだ。先生、教えてくださいッ!

めっちゃ直すところあるやん(‘_’)

 4つも指摘もらっちゃった(‘_’) これまでの経験上、これを一気に修正かけようとするとChatGPTくんが暴走したり文字切れしたりするので、ちゃんと指摘内容を精査していくことにする。1つめは妥当な指摘。採用するべきだろう。2つめはコードを書くときの注意点なので無視。3つめ、4つめもそうだ。っということで、指摘は1つめだけ受け入れる。

直してくれた

余談(‘_’)

 こういうやりとりやっていると、人間相手にレビューやってんのとほとんど同じだな~っと感じる。人間と違うのは、ChatGPTくんはめちゃくちゃ…驚嘆すべきレベルで素直ということだろうか。こちらに逆らうことがない。

 これには良い点と悪い点があって、人間相手に上のようなやりとりをすると、採用しなかった指摘2~4について不採用の理由を説明する必要が出てくるんだけど、ここで相手と認識齟齬だったり設計思想が違ったりすると、議論が白熱してしまうときがある。このとき自分の不採用の理由が間違っていることに気付く場合もあれば、相手が勘違いしてエキサイトしているだけの場合もある。前者ならレビューしてよかったネだし、後者なら時間の浪費だ。これはレビューしてみるまでどっちに転ぶかわからない。ChatGPTくん相手だと、こういう議論が起きない。いいのか悪いのか…。

 さておき(‘_’)

 ここまでjsonばっかりChatGPTくんに書いてもらってきて、本来のやりたいことが忘れられていたらヤダな~と思ったので、振り返りのためにjsonデータ仕様を書いてもらうことにした。

コイツ…やりおる

 かるくお願いしたらめっちゃ気合いの入ったマークダウンで書き始めたのでびっくりしたw 読んでみると、このまま採用するとマズそうな場所がいくつかあったので指摘する。マズそうな場所…つまり制限事項だ。仕様書を書くときにここは気をつけないとマジヤバだ。

かしこ~い(‘_’)

 うおッ、2つの指摘が8つの反省点になって返ってきた…こいつマジヤバだな。マジで優秀すぎる。人間がかすんでしまうゥ…とはいえ、とはいえ、勘違いして変な修正入れられても困るので、また内容を精査していこう。

中略

 このあと、仕様書を書かせてレビュー&フォロー⇒仕様書をもとにコードを書かせてレビュー&フォロー⇒コードをもとにjsonを書かせてレビュー&フォロー⇒仕様書を…というループをぐるぐる回した。最初のたたき台が低レベルだったせいか、周回するごとに指摘が細かくなっていった。だいたい3周目くらいで、まあまあこんなもんで、そろそろコード書くのに入れそうかな…という感触を得たのでループ作業を打ち切った。最終仕様はこんな感じだ。

正直…コントロールチェンジがなんなのかわかっていない…

 今回作りたい「作曲オーダーをもとに指定のフォーマットでjson出力してくれるChatGPT」は、最初のsystemプロンプトにこの仕様書の内容を突っ込むことにした。これを事前情報として与えておいた上で、人間の言葉で「20秒くらいの明るいカワイイ曲を作ってください」みたいにお願いして、jsonを出してもらうのだ。コードスニペットはこんな感じ。

if __name__ == "__main__":

    # 外部ファイルからjson仕様書を読み込みます
    jsonformat_filename = "jsonformat.txt"
    jsonformat = read_textfile(jsonformat_filename)

    # 外部ファイルからユーザーのオーダーを読み込みます
    order_filename = "order.txt"
    order = read_textfile(order_filename)

    # 外部ファイルからsystemの期待役割を読み込みます
    systemrole_filename = "systemrole.txt"
    systemrole = read_textfile(systemrole_filename)

    # 外部ファイルからChatGPTの初回応答を読み込みます
    assistant_filename = "1stRes.txt"
    assistant = read_textfile(assistant_filename)

    # json仕様書に基づいた作曲データを生成します
    generated_json = generate_json(jsonformat, order, systemrole, assistant, openai.api_key)

    # 生成されたコードを指定されたフォーマットの名前で出力します
    timestamp = datetime.datetime.now().strftime("%Y%m%d")
    output_filename = f"ordermade_{timestamp}.json"
    write_json(generated_json, output_filename)

 自分も最近知ったのですが、ChatGPT APIを使うときは、ChatGPTの会話ログを捏造できるらしいです。は?って感じですけど、できるみたいですw そのため、まったく同じプロンプトをGPT-4に入力したときに返ってきた応答を1stRes.txtとして読み込ませています。彼はGPT-3.5だがスタートダッシュの理解力だけはGPT-4にドーピングなのじゃガハハ!ってかんじです(?)※GPT-4のAPI利用はウィッシュリストに登録したけどお返事こないです(‘_’)

 generate_json関数の中身は汚すぎてお見せできないのですが、ざっくりやっている内容はこんな感じです。

  • systemに期待役割を設定
  • 最初のユーザ要求でjson仕様書をテキスト形式に直したものを設定
  • 最初のユーザ要求に対する応答に捏造データ(GPT-4に同じ要求を投げたときの応答)を設定
  • 次のユーザ要求で作曲してほしい内容を設定
  • ↑ここまでの内容を履歴データにまとめる※実際にやりとりしないないけど、やりとりしたかのようなデータを作るってことです
  • 履歴データをChatGPTくんに渡して、最後のユーザ要求に対する応答を出力してもらう
  • 応答データを整形してファイルに保存

 プログラムを実行すると無事にjsonが出力されました。なんか音符が2個しかないクソ曲っぽいですが…まあこれは自分の指示がマズかったということなのでしょう。次からは「音符は20個以上使うこと」みたいなオーダーをちゃんと入れるか、systemの期待役割に「彼(=ChatGPT)は20個以上の音符を含む曲が大好きだ」みたいな背景を入れるかですね(‘_’)

 ちなみにこれらのやりとりで消費したトークンはだいたい2400でした。Maxが4096なので、もうちょっと頑張れそうな感じがありますね。

jsonデータをReadしてMIDIファイルを生成するプログラム

 先ほど作ったjsonデータをMIDI変換するプログラムです。こちらもjson仕様書をもとにChatGPTにコード生成してもらって、らくちん進行…かと思いきや、クソむずかしかったです。

 上記で書いたみたいに、最初に生成されたjsonコードが2つの音符かつポーン、ポーンみたいな単音で鳴らすようになっていました。普通の曲って、複数の音が一度に鳴る「和音」を使ってますよね。この和音に対応できるプログラムを書こうとしたらめちゃくちゃ大変で(一度でもMIDIで和音作ったことのある人なら問題なくできると思われるが…ウググ)、ここで5時間つかってしまいました。昼過ぎからはじめて、いま19:40です。コワすぎる。今日が土曜日であることに感謝しかありません。

 コードスニペットはこんな感じ。ここはChatGPTのAPIを使っている場所はなくて、純粋にjsonフォーマットのデータを機械的にMIDI変換するだけ。だけ…だけなのに…和音がクソむず(‘_’)

def main(input_file_path: str, output_file_path: str) -> None:
    json_data = load_json_data(input_file_path)
    music = parse_json_data(json_data)
    create_midi_file_from_music(music, output_file_path)

if __name__ == "__main__":
    input_file_path = "a.json"
    output_file_path = "a.mid"
    main(input_file_path, output_file_path)

 これでMIDIファイルを生成したあと、前回のMIDIをMP3に変換するプログラムに食わせて完成。和音対応させた都合でjson仕様書も修正が入って命令文がかなり長くなってしまった。トークンは3500使用。4096まであとちょっとしか余裕がない。危ない。

作ったものまとめ

 構想おさらい。

人間が作るところがめっちゃ多い。ChatGPTくんは作曲に特化してもらう作戦だ。

 作曲オーダーはこんな感じ。和音入れてって指定しないと短音2音の曲を作ってくるので困る。このデータは毎回変えてOK。最初は先頭の2行だけ書いていたが、曲とも言えない変なのしか作ってくれなかったのでいろいろ付け足した。

森の中をゆったり散歩するような、静かで優しいピアノ曲。
和音でなめらかな伴奏を作り、流れるようなメインメロディーを作ってほしい。

各小節の最初の拍には必ず音符が存在するようにしてください。
楽曲は80BPM、1小節4拍子、16小節で構成してほしい。

基本的に4小節ごとにフレーズを使い回してください。
少しずつ音程を変えることで、曲の進行を表現してください。
最後の4小節目には、印象的なメロディーを入れてください。

作曲が難しいときは以下を参考にしてね。
・まず演奏に使う楽器の種類と数を決めます。「ピアノ一台」や、「ベース」など。
・次にテンポ(96など)、拍子(4/4など)を決めます。
・次に楽器をメインのメロディーに使うか、伴奏に使うかを決めます。「ピアノでメロディー、ベースで伴奏」など。
・楽器が1種類の場合でも、同じ楽器を複数台使います。「ピアノ1でメロディー、ピアノ2で伴奏」など。
・メロディー用の楽器の場合、単音符を組み合わせてメロディーを作ります。
・伴奏用の楽器の場合、和音でメロディーを引き立てます。
・複数の楽器を使うときは、必ず演奏時間(合計duration)が同じになるようにします。

 作曲家のプロフィールはこんな感じ。このデータも毎回変えてOK。何回か試してみたが、systemに設定されるこのデータ、あまり効いている感じがしなかった。

あなたは有能な作曲家です。ユーザーの依頼を満たす作曲をこなします。
あなたはMIDIで楽器演奏する方法について詳しいです。

 json仕様書はこんな感じ。ここはChatGPTくんとの共同作業。めちゃくちゃ頑張った。このデータは変えてはいけない。構成管理とかして大事に持っておこう。

【概要】
この JSON データは、曲を表現するためのデータ構造を定義します。
この定義は、Pythonのライブラリであるmidoで利用されることを前提としています。

■Music:楽曲全体を表現するオブジェクト。
tempo (int): 楽曲のテンポ。BPM (拍/分) で表現されます。例: 80
time_signature (dict): 楽曲の拍子記号。例: "4/4"
  numerator (int): 分子。
  denominator (int): 分母。
instruments (list): 楽器のリスト。

■Instrument:楽器を表現するオブジェクト。
name (str): 楽器名。
midi_program (int): MIDIプログラム番号。
sections_order (list): セクションの順序を表す文字列のリスト。例: ["intro", "main", "ending"] 各Instrumentは、同じ名前のsectionsを持つ必要があります。
sections (dict): セクション名をキーとし、セクションの情報を格納する辞書。

■Section:楽曲のセクションを表現するオブジェクト。
elements (list): セクション内の要素(Note、Chord、Control Change Event)のリスト。

■Note:単音符を表現するオブジェクト。
type (str): 要素の種類を表す文字列。単音符の場合、"note"。
pitch (int): 音高。MIDIノート番号。例: 60
duration (int): 音の長さ。MIDIティック。例: 480
velocity (int): 音の強さ。0~127。

■Chord:和音コードを表現するオブジェクト。
type (str): 要素の種類を表す文字列。和音コードを指定する場合、"chord"。
chord_name (str):和音の種類を示すコード。例:C,Dm
duration (int): 音の長さ。MIDIティック。
velocity (int): 音の強さ。

■Control Change Event:コントロールチェンジイベント
type (str): 要素の種類を表す文字列。コントロールチェンジイベントの場合、"control_change"。
control (int): コントロール番号。0~127。
value (int): コントロール値。0~127。
time (int): イベントの発生タイミング。MIDIティック。

■MIDIプログラム番号について
https://ja.wikipedia.org/wiki/General_MIDI
General MIDI (GM)仕様に従って、Instrumentのmidi_programは0~127で指定する。
当てはまる楽器がない場合、もっとも近い音色を選ぶ。

■注意点
和音で伴奏している最中にメロディーを鳴らしたい場合、伴奏用のinstrumentsとメインメロディーのinstrumentsをわけること。
複数の楽器Instrumentを使用する場合、演奏時間(durationの合計値)が同じになるようにする。演奏時間は必ずあわせる。

■ここからは参考情報です。
jsonサンプル 120BPM、ピアノとベース、1小節4拍構成、2小節ぶんの楽曲データ。

{
"tempo": 120,
    "time_signature": {
        "numerator": 4,
        "denominator": 4
    },
    "instruments": [
        {
            "name": "piano",
            "midi_program": 0,
            "sections_order": ["main_section"],
            "sections": {
                "main_section": {
                    "elements": [
                        {
                            "type": "note",
                            "pitch": 60,
                            "duration": 480,
                            "velocity": 64
                        },
                        {
                            "type": "note",
                            "pitch": 60,
                            "duration": 240,
                            "velocity": 64
                        },
                        {
                            "type": "chord",
                            "chord_name": "C",
                            "duration": 240,
                            "velocity": 64
                        }
                    ]
                }
            }
        },
        {
            "name": "bass",
            "midi_program": 33,
            "sections_order": ["main_section"],
            "sections": {
                "main_section": {
                    "elements": [
                        {
                            "type": "note",
                            "pitch": 60,
                            "duration": 480,
                            "velocity": 64
                        },
                        {
                            "type": "note",
                            "pitch": 60,
                            "duration": 120,
                            "velocity": 64
                        },
                        {
                            "type": "note",
                            "pitch": 63,
                            "duration": 120,
                            "velocity": 64
                        },
                        {
                            "type": "chord",
                            "chord_name": "C",
                            "duration": 240,
                            "velocity": 64
                        }
                    ]
                }
            }
        }
    ]
}

 これらのデータをプログラムにぶっこんで出力されたものがこちらです。生成にかかった時間は1分弱くらい。RPGの村BGMっぽい。(音量注意)

 プロンプトで「リコーダー2つでせわしないピロピロを演奏してほしい」をオーダーしたもの。最初に実行したときトークン数が4096を突破してしまったので、json仕様の重複表現とかを一度削除してやりなおした。ぜんぜんせわしなくないし、なんならボソボソしている。(音量注意)

 前回もいちおう和音は鳴らせていたが、鳴らすときにコード指定ではなくテキトーな2音を重ねているだけだったので、今回のほうが曲っぽさが増している気がする。参考までに前回のやつ。コードという概念を知らないAIくんの作った曲…

 キツかった作業はいろいろあるが、和音がクソむずい、2楽器以上の演奏がクソむずい、左記の問題を解決するためプロンプトに手を入れるとMaxトークンを突破しまくってしまう、というあたりが大変だった。てか自分で勉強して作曲した方が早くね?という自意識が沸くのも大変だった。い、いちおうパラメータ変えて再利用可能っていう利点があるし…。

 ただまあ、次のメジャーバージョンアップとかでマルチモーダルが来るとすると、jsonとかpythonとか噛ませずに、フツーに自然言語で楽曲出力できるAPIが増えるかもしれないですね。そのときこの作業は虚無になるわけだが…気にしないッ(‘_’)

次回の目的

 これでいったん作曲マシーンAIは終了だ。次はカクヨムのイベントに向けたAI小説を作ってみる。きっと作曲マシーンでつちかった知見が活きるはず…たぶん…

 旅は続く! 旅は終わらない!