見出し画像

GPT-4Vを使って、AIにニワトリレースの実況をさせてみた

2日前に開催されたOpenAI初の開発者向けカンファレンス、DevDay 2023。
そこでは、開発者にとってはうれしい多くの発表があった。
その中でも、GPT-4VのAPI開放は多くの開発者が待ち望んでいたものの一つだろう。

で、ひょんなきっかけで、このGPT-4Vを使って、実験的に、XANAのRooster Fighter(ベータ版)というWeb3系のニワトリレースに実況をつけてみたところ、かなり面白いことになった。

というわけで、この記事で、これを作るに至った経緯と共に、今回何をやったかを具体的に紹介していく。


GPT-4Vについて

まず、GPT-4Vについて簡単に説明しておくと、これは、ChatGPTの「目」にあたると思って頂ければ話が早い。
今までは、ChatGPTと対話するのに、テキストを入力していたのが、画像を読み込ませて、それをインプットの一部として使えるようになったのである。

これはかなり革命的で、たとえば、眼の前にあるものが何か知りたいときに、写真撮って、これ何?と聞いたり、Webサイトを作ってもらいたいときに、チマチマ文章で説明しなくても、手書きのイラストを写真に撮って、こういう感じのやつ作って!とお願いできるわけである。

で、今回、これがAPI経由で使えるようになったのである。これの何がすごいかというと、これまではChatGPTというインタフェースを通してしか使えなかった技術が、自分のプログラムから直接呼び出せるようになり、いろんなことに応用できるようになったのである。

Rooster Fighterとは

(GPT-4Vだけに興味のある人は、この章はすっ飛ばしてください)
で、今回、題材として選んだのが、現在、XANAがベータ版としてリリースしているRooster FighterというWeb3ゲーム。ニワトリ・ファイターという原作漫画があり、それとのコラボNFTなのだが、それがゲーム化され、最近ベータ版がリリースされ、NFT保有者限定のテスト運用を行っている。

このゲーム、サイバーな街中にある直線コースを8羽のニワトリがレースするというシュールなゲームなのだが、自分でベットしたり、自分のNFT(ルースター)を出走させたりできる。ちなみに、NFTはここで買える。

Rooster Fighterの問題点

それで実際に出走させて、ストリーミングで中継されるレースを見ていて思ったのが、何か物足りない。
そう、このゲームには、「実況」がないのである。
紆余曲折を経て、1年という長い開発期間の末、なんとかベータローンチまでこぎつけたというゲームなので、あまり多くは求められないのは重々承知なのだが、やはり、これだと、この手のゲームの一番の重要なファクター、「熱狂感」が生み出せない。

大学時代、京都競馬場に毎週通ってたような時期もあり、こういうレースものには、多少の思い入れがあり、やはり、どうしても実況を求めてしまう。そういうこともあり、実は、GPT使って、実況できないかなぁと個人的に実験したりしていた。

GPT-4V APIが出てくる前の実験

で、どういうことを実験していたかというと、出走ルースターのデータが画面に表示されるので、そこから数字を拾ってきて、以下のようにプロンプトの入力の一部にする。

#Race Info:
- Race Name: Rooster Daily Race 21
- Surface: Turf
- Distance: 250m
- Race Course: Green Field

#Stats and Current Status
Gate, Name, Speed, Stamina, Career(1st/2nd/3rd/Total), Odds, No of Votes, Current Place, Remaining distance(m)
1, #1705, 47, 53, 3/1/0/5, 19.15, 7, 3, 140
2, #0205, 64, 36, 0/1/2/5, 33.52, 4, 2, 134
3, #2469, 56, 44, 0/0/1/5, 1.3, 103, 1, 120
4, #1836, 37, 63, 1/0/1/5, 26.82, 5, 4, 152
5, #1838, 47, 53, 1/2/0/5, 22.34, 6, 5, 173

ここでキモは、スタッツの一部に、現在の順位と、ゴールまでの残りの距離を入れているところである。コース距離もデータの一部として入れているので、これで各ニワトリが、ゴールまでどのくらいの距離にいるかが分かる。

それで、このデータをレース中、コマ送りのように送りながら、それに基づいて実況をGPTに生成させるというアイディアである。
で、実際に生成されたテキストの例はこんな感じ。
以下は、レース中盤のサンプル。

『ルースター・デイリー・レース21』の中盤戦の模様をお伝えしています!

リードしているのは3番ゲートの#2469!残り距離は120mを切ってきました。オッズ1.3という圧倒的な本命の期待に応える形で、先頭をキープしています!

2番の#0205がその背後を追いかける形となっており、6mの差をつけられています。スピード64という高速さを活かして、逆転のチャンスを狙っている様子。

そして、1番ゲートの#1705が3位につけており、彼も後半の巻き返しを期待していますね!

4番の#1836と5番の#1838は少し後退気味。しかし、ルースターレースは予想が難しく、これからの進行でどんなドラマが待っているかわかりません。

#2469の先頭での逃げ切りとなるのか、それとも#0205や#1705が追いつくのか、非常に興奮する展開となっています!お見逃しなく!

これがレース後半の例。

『ルースター・デイリー・レース21』、最終局面を迎えています!

先頭はなんと3番の#2469!残りわずか16mとなりました。オッズ1.3と圧倒的な支持を背負いながら、その期待に応える形で、ゴールに迫っています!

しかし、その背後には2番の#0205と1番の#1705、そして5番の#1838がほぼ同じ位置で迫る大接戦!3mの差しかない!このタイトなバトル、果たして誰がゴールラインを駆け抜けるのか!

スタンドは大興奮の渦!ファンたちは息をのみ、目を離せない展開となっています!

#2469の逃げ切りとなるのか、それとも#0205や#1705、#1838が最後の最後で逆転の一撃を放つのか!?ここからの数秒がすべてを決める!目が離せません!

あー、これは歴史に残るルースターレースとなるでしょう!最後までどうぞお見逃しなく!!

と、ここまでやって、もうちょっとプロンプトをしっかり調整すれば、なんとかなりそうかなぁと思っていたのだが、トラブルで自分のニワトリがレースに出走できなくなってしまい、しばらく放置していた。

GPT-4V + TTS = AIスポーツ実況

やっと、ここからが本題。
そして、今週に入って、GPT-4Vがアナウンスされ、とあるXのポストが目についた。

これ、サッカーの試合の映像を、GPT-4Vに送り、実況をつけているのだが、これがかなりの出来で、コメント欄に、ソースまで貼り付けてくれてたので、中身をチェックしたところ、かなりシンプルな作りになっていた。

ちなみに、元ネタは、OpenAIのCookbookにあった以下のコードのようだ。

AI実況のワークフロー解説

ワークフローとしては、以下のような流れになる。

  1. 動画ファイルを読み込み、フレーム分割して画像データに変換

  2. 分割したフレーム画像とプロンプトをGPT-4V APIに送信し、実況テキストを生成

  3. 生成したテキストを、TTS(Text to Speech) APIに送信し、読み上げ音声データを生成

たったこれだけ。コードを見れば分かるが、かなりシンプルなことが分かる。これには驚いた。

以下、簡単にコード説明していく。

ライブラリの読み込み

まず、必要なライブラリの読み込み。

from IPython.display import display, Image, Audio

import cv2  # We're using OpenCV to read video
import base64
import time
import openai
import os
import requests

ここで、cv2は、動画ファイルの読み込みに使うのだが、手元にない場合は、以下のコマンドでインストールする。

pip install opencv-python

他のライブラリも必要に応じてインストールしておく。

1. 動画ファイルを読み込み、フレーム分割して画像データに変換

video = cv2.VideoCapture("data/bison.mp4")

base64Frames = []
while video.isOpened():
    success, frame = video.read()
    if not success:
        break
    _, buffer = cv2.imencode(".jpg", frame)
    base64Frames.append(base64.b64encode(buffer).decode("utf-8"))

video.release()
print(len(base64Frames), "frames read.")

cv2を使って、動画ファイル(mp4)を読み込み、それをフレーム分割して、jpg形式の画像データとして、base64Framesという名前の配列(メモリ上)に格納。自分の場合、1分5秒の動画で1637フレームのJPGに分割された。

以下は、分割された画像がちゃんと読み込めるか確認するためのコード。

display_handle = display(None, display_id=True)
for img in base64Frames:
    display_handle.update(Image(data=base64.b64decode(img.encode("utf-8"))))
    time.sleep(0.025)


2. 分割したフレーム画像とプロンプトをGPT-4V APIに送信し、実況テキストを生成

そして、いよいよここからが、GPT-4Vの出番。

PROMPT_MESSAGES = [
    {
        "role": "user",
        "content": [
            "These are frames of a video. Create a short voiceover script in the style of David Attenborough. Only include the narration.",
            *map(lambda x: {"image": x, "resize": 768}, base64Frames[0::10]),
        ],
    },
]
params = {
    "model": "gpt-4-vision-preview",
    "messages": PROMPT_MESSAGES,
    "api_key": os.environ["OPENAI_API_KEY"],
    "headers": {"Openai-Version": "2020-11-07"},
    "max_tokens": 500,
}

result = openai.ChatCompletion.create(**params)
print(result.choices[0].message.content)

GPT-4Vには、画像を複数一度にわたすことができるのだが、GPT-4V自体がステートレスなので、前回送った画像は後続のリクエストに引き継がれず、今回のように連続した画像を取り扱う場合は、一度にすべて送ってしまう必要がある。

上記コードでは、その前に分割した画像データの配列base64Framesから、10枚ごとに1枚選んで、それらの画像をデータとしてセットしている。つまり、1637フレームある場合、164枚ほどの画像がAPIに送られることになる。

GPT-4V APIの使い方の詳細は以下を参照。

上記で、画像と共に渡しているプロンプトは以下。

These are frames of a video. Create a short voiceover script in the style of David Attenborough. Only include the narration."
(DeepL訳)
これはビデオのフレームです。デービッド・アッテンボローのスタイルで短いナレーションスクリプトを作ってください。ナレーションだけを入れてください

ちなみに、ここに出てくる、デービッド・アッテンボローというのはイギリス人ならみんな知ってる、超有名なドキュメンタリーのナレーター。BBCとか見てると、絶対一度は聞いたことのある馴染み深い声と語り口調だ。

なお、最初に紹介したサッカー実況を生成するのに使われたプロンプトはこんな感じである。

These are frames of a video. Create a short voiceover script in the style of a super excited brazilian sports narrator who is narrating his favorite match. He is a big fan of Messi, the player who scores in this clip. Use caps and exclamation marks where needed to communicate excitement. Only include the narration, your output must be in english. When the ball goes into the net, you must scream GOL either once or multiple times.
(DeepL訳)
これはビデオのフレームです。お気に入りの試合のナレーションをする、超興奮したブラジル人スポーツナレーターのスタイルで、短いナレーションスクリプトを作成してください。彼はメッシの大ファンです。興奮を伝えるために、必要に応じて大文字や感嘆符を使ってください。ナレーションのみで、出力は英語で行ってください。ボールがネットに入ったら、1回または複数回GOLと叫んでください。

このプロンプトを見て、あぁなるほど、だからあんなメッシとゴールを連呼してたのかと腑に落ちたw

3. 生成したテキストを、TTS(Text to Speech) APIに送信し、読み上げ音声データを生成

で、最後は生成したテキストを基に、ボイスオーバーの作成。

response = requests.post(
    "https://api.openai.com/v1/audio/speech",
    headers={
        "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
    },
    json={
        "model": "tts-1",
        "input": result.choices[0].message.content,
        "voice": "onyx",
    },
)

audio = b""
for chunk in response.iter_content(chunk_size=1024 * 1024):
    audio += chunk
Audio(audio)

ここでは、OpenAIのTTS(Text to Speech) APIを使用。
インプットは上記で生成したテキストをそのまま突っ込んでいる。
モデルは、tts-1を指定しているが、tts-1-hdという高品質なモデルも用意されている。

声の種類は、現在のところ、6種類用意されているが、自前の声は指定不可である。以下のサイトで声は確認可能。

なお、生成した読み上げデータを音声ファイルとして保存するには、以下のコードでローカルに保存できる。

with open('output_en.mp3', 'wb') as file:
    file.write(audio)


ニワトリレース実況の実験結果

それで、上記のサッカー実況に戻るが、これを見ていて、ぱっとあるアイディアが頭に降りてきた。
そうだ、あのニワトリ実況、この技術使えば、できるんじゃ?

実験1: 日本語実況

というわけで、早速実験。
まずは過去のレースから、適当に選んで、レース内容を録画。
そして、それを上のコードをちょこっと変更して、GPT-4Vに突っ込んだら・・・

かなり実況っぽいテキストが出てきた!

レースのスタートから、鶏たちが一斉にダッシュを切りました!それぞれのレーンを勢いよく走り出す鶏たち、羽ばたきながら一生懸命に前に進もうとしています!観客席では歓声が上がり、一番人気のレーサーも猛スピードでトップを争っています!

コーナーを曲がり、鶏たちの間にわずかに差がつき始めました!トップを走るのは、どの鶏だ?!スピードを上げて、ゴールに近づくたびに緊張が高まってきました!ファンの皆さん、声援を送って!

そして、ついにこの瞬間が!ゴールラインが見えてきた!リードしている鶏が、ラストスパートをかけている!他の鶏も負けじとスピードアップ!会場が熱狂の渦に包まれています!

こ、これは驚き!信じられない展開になってきました!あとわずか!ゴールラインが目の前!

そしてゴールイン!スピードと勇気の見事なる駆け引き!勝者のゲート番号は……「ゲート4」!ルースターファイター1840号が見事に勝利を飾りました!これぞまさに、チキンレースの真骨頂!おめでとうございます!

せっかくなので、実況者として、我らがGenesis (XANAのAIチャットできるNFT)を採用。自分のGenesisは、アニメーションプラグインがついてないので、公式Genesisであるカレンちゃんが口パクしてる様子を適当に録画して貼り付けた。

それでできたのがこちら。

テキスト自体はいいのだが、読み上げが日本語だからか、カタコトの日本語で棒読み。全く臨場感が伝わってこないw

今回TTSとして使ったのは、これまた2日前のDevDayで発表されたばかりのOpenAI製のTTS。一応マルチ言語対応してるというので、使ってみたけど、やっぱ日本語はお世辞にも自然とは言えない。まして、実況となると、ハードル高いし、かなり厳しくなる。

実験2: 英語実況

というわけで、今度は英語でテキストを生成させてみた。
こんな感じ。

LADIES AND GENTLEMEN, HOLD ONTO YOUR HATS, BECAUSE THIS IS THE MOMENT WE'VE ALL BEEN WAITING FOR! THEY'RE OFF! AND WE ARE WATCHING PURE POULTRY PERFECTION IN MOTION AT THE GRAND ROOSTER DASH! FEATHERS ARE RUFFLED AND THE CACKLING CROWD IS GOING WILD!

LOOK AT THEM GO! WAIT, WAIT, IS THAT – YES, IT’S NUMBER #1840 FROM GATE 4 TAKING THE LEAD! THAT'S OUR CHICKEN CHAMP, LADIES AND GENTS! COMIN' AROUND THE BACKSTRETCH WITH UNBELIEVABLE SPEED!

I CAN'T BELIEVE WHAT I'M SEEING! #1840 IS SHOWING 'EM HOW IT’S DONE, FOLKS! IT'S A BIRD, IT'S A PLANE, IT'S SUPERCHICKEN! WATCH THOSE WINGS, LADIES AND GENTLEMEN, THIS BIRD IS LITERALLY FLYING WITHOUT LEAVING THE GROUND!

AND AS WE APPROACH THE FINAL STRETCH, THE ATMOSPHERE IS ELECTRIC, HEARTS ARE THUMPING IN TIME WITH THOSE POWERFUL POULTRY STEPS! NUMBER #1840, WHO DREAMED OF NOTHING BUT VICTORY WHEN IT STEPPED OUT OF THE COOP THIS MORNING, IS NOW STEAMING TOWARDS THE FINISH LINE LIKE A FREIGHT TRAIN MADE OF FEATHERS!

CAN YOU FEEL THAT, PEOPLE?! OH, HO, HO, IT'S A FLURRY OF FLAP-TASTIC FURY! AND HERE IT COMES—YES! ACROSS THE LINE! GATE 4, GATE 4, GATE 4, THE CROWD IS GOING INSANE! CHICKEN #1840 FROM GATE 4 HAS DONE IT! THAT BIRD HAS JUST BOOKED A PLACE IN THE ETERNAL COOP OF CHAMPIONS! OH, THE POETRY IN MOTION, THE DRAMA, THE GLORY! WINNER, WINNER, CHICKEN DINNER! AND A THOUSAND DREAMS HATCH TONIGHT! CONGRATULATIONS TO OUR FEATHERED FRIEND FROM GATE 4, ROOSTER FIGHTER #1840!!!

(GPT訳)
皆さん、帽子をしっかり握ってくださいよ!待ちに待った瞬間がついに訪れました!彼らは飛び出しました!ここに見せるのは、グランド・ルースターダッシュで繰り広げられる、鶏の動きの極致です!羽はふわり、観客の笑い声と歓声が渦を巻いています!

見てください、彼らの勢いを!ちょっと待って、それは、ええとー、はい!4番ゲートの#1840が先頭に出ました!そうです、あれが我々のチャンピオン・チキンですよ!驚異のスピードで後半戦を駆け抜けています!

信じられない光景です!1840が見せるそのテクニック、凄いですね!鳥か、飛行機か、いやいや、それはスーパーチキン!その翼をご覧ください、まさに地上を飛んでいるようです!

そしてファイナルストレッチに向かって、場の雰囲気は最高潮に、心臓の鼓動はその力強い足取りと同調して脈打っています!今朝、鶏小屋から一つの勝利を夢見て飛び出した#1840が、羽のついた列車のようにゴールに向けて突き進んでいます!

皆さん、この興奮を感じますか?おっとっと、これは羽ばたく驚異の嵐だ!そして、ここに来て...そう、ゴールラインを超えました!4番ゲート!4番ゲート!4番ゲート!観客は大興奮です!4番ゲートから出た#1840のチキンがやり遂げたんです!その鳥はまさにチャンピオンズ・クープにその名を刻みました!ああ、その動きの美しさ、そのドラマ、その栄光!勝者にはチキンディナー!そして今宵、千の夢が孵化するのです!4番ゲートの羽根の仲間、ルースターの戦士#1840、おめでとうございます!!!

今度は、XANA Discordのマスコットとして活躍中のサナちゃんに実況をバトンタッチ(特に意味なし)。
んで、できあがったのが、一番最初に紹介したポスト。

これがなかなかのできで、実況内容のシュールさと、微妙にテンション高い読み上げが相まって、結構笑ってしまったw

ちなみに、英語版を作っているとき、テキスト生成時に、GPT-4Vがエラーをはくようになって、インプットの画像の数を減らしたのだが、これが良くなかったのか、単に英語の方が長くなるからなのか、映像の長さに比べて、TTSで読み上げた時間の方が30秒ほど長くなってしまった。。
(1分ちょいの動画に対して、1分30秒ちょいの読み上げ)

なので、適当にレース動画を付け足して帳尻合わせしたのだが、この辺はもっと色々実験して、データを取りたいところ。

なお、今回上記の実況テキストを生成するのに使用したプロンプトは以下。

This is a frame from a chicken racing video. Create a short narration script in the style of a super excited horse racing play-by-play announcer narrating his favourite match. He is a big fan of chicken racing. Use capital letters and exclamation marks where necessary to convey excitement. Please use narration only and output in English. When the finish line is approaching, give the best and most excited play-by-play. When the finish line is reached, read out the gate number of the chicken.

ほぼほぼ、上のサッカー実況プロンプトで、ちょこっと変えただけ。
この辺は、まだ改善の余地はありそうだけど、今回はとりあえずの実験だし、ある程度の成果は出せたのでよしとしよう。

さいごに

本当は、このブログを書く前に、レース1本分まるまる実況させようと思って試してたんだけど、なんかやたらRateLimitErrorで、エラーが返ってくるようになり、結局試せなかった。

昨日OpenAIのサーバーダウンしてたっぽいし、今みんなこぞって色々触ってるだろうし、一時的にリミット制限かけてるのかも?

また、そのうち実験できたら、続報をお伝えしたい。
んなわけで、今日のところは、ここまで。

このブログが誰かの参考になれば、幸いである。

ぜひ、参考になったら、X(ツイッター)アカウントのフォローと、このNoteアカウントのフォロー、そして、記事拡散して頂けるとうれしい。

よろしければサポートお願いします!すごく励みになります!