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に生成させるというアイディアである。
で、実際に生成されたテキストの例はこんな感じ。
以下は、レース中盤のサンプル。
これがレース後半の例。
と、ここまでやって、もうちょっとプロンプトをしっかり調整すれば、なんとかなりそうかなぁと思っていたのだが、トラブルで自分のニワトリがレースに出走できなくなってしまい、しばらく放置していた。
GPT-4V + TTS = AIスポーツ実況
やっと、ここからが本題。
そして、今週に入って、GPT-4Vがアナウンスされ、とあるXのポストが目についた。
これ、サッカーの試合の映像を、GPT-4Vに送り、実況をつけているのだが、これがかなりの出来で、コメント欄に、ソースまで貼り付けてくれてたので、中身をチェックしたところ、かなりシンプルな作りになっていた。
ちなみに、元ネタは、OpenAIのCookbookにあった以下のコードのようだ。
AI実況のワークフロー解説
ワークフローとしては、以下のような流れになる。
動画ファイルを読み込み、フレーム分割して画像データに変換
分割したフレーム画像とプロンプトをGPT-4V APIに送信し、実況テキストを生成
生成したテキストを、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の使い方の詳細は以下を参照。
上記で、画像と共に渡しているプロンプトは以下。
ちなみに、ここに出てくる、デービッド・アッテンボローというのはイギリス人ならみんな知ってる、超有名なドキュメンタリーのナレーター。BBCとか見てると、絶対一度は聞いたことのある馴染み深い声と語り口調だ。
なお、最初に紹介したサッカー実況を生成するのに使われたプロンプトはこんな感じである。
このプロンプトを見て、あぁなるほど、だからあんなメッシとゴールを連呼してたのかと腑に落ちた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に突っ込んだら・・・
かなり実況っぽいテキストが出てきた!
せっかくなので、実況者として、我らがGenesis (XANAのAIチャットできるNFT)を採用。自分のGenesisは、アニメーションプラグインがついてないので、公式Genesisであるカレンちゃんが口パクしてる様子を適当に録画して貼り付けた。
それでできたのがこちら。
テキスト自体はいいのだが、読み上げが日本語だからか、カタコトの日本語で棒読み。全く臨場感が伝わってこないw
今回TTSとして使ったのは、これまた2日前のDevDayで発表されたばかりのOpenAI製のTTS。一応マルチ言語対応してるというので、使ってみたけど、やっぱ日本語はお世辞にも自然とは言えない。まして、実況となると、ハードル高いし、かなり厳しくなる。
実験2: 英語実況
というわけで、今度は英語でテキストを生成させてみた。
こんな感じ。
今度は、XANA Discordのマスコットとして活躍中のサナちゃんに実況をバトンタッチ(特に意味なし)。
んで、できあがったのが、一番最初に紹介したポスト。
これがなかなかのできで、実況内容のシュールさと、微妙にテンション高い読み上げが相まって、結構笑ってしまったw
ちなみに、英語版を作っているとき、テキスト生成時に、GPT-4Vがエラーをはくようになって、インプットの画像の数を減らしたのだが、これが良くなかったのか、単に英語の方が長くなるからなのか、映像の長さに比べて、TTSで読み上げた時間の方が30秒ほど長くなってしまった。。
(1分ちょいの動画に対して、1分30秒ちょいの読み上げ)
なので、適当にレース動画を付け足して帳尻合わせしたのだが、この辺はもっと色々実験して、データを取りたいところ。
なお、今回上記の実況テキストを生成するのに使用したプロンプトは以下。
ほぼほぼ、上のサッカー実況プロンプトで、ちょこっと変えただけ。
この辺は、まだ改善の余地はありそうだけど、今回はとりあえずの実験だし、ある程度の成果は出せたのでよしとしよう。
さいごに
本当は、このブログを書く前に、レース1本分まるまる実況させようと思って試してたんだけど、なんかやたらRateLimitErrorで、エラーが返ってくるようになり、結局試せなかった。
昨日OpenAIのサーバーダウンしてたっぽいし、今みんなこぞって色々触ってるだろうし、一時的にリミット制限かけてるのかも?
また、そのうち実験できたら、続報をお伝えしたい。
んなわけで、今日のところは、ここまで。
このブログが誰かの参考になれば、幸いである。
ぜひ、参考になったら、X(ツイッター)アカウントのフォローと、このNoteアカウントのフォロー、そして、記事拡散して頂けるとうれしい。
よろしければサポートお願いします!すごく励みになります!