ゲーム中の画面をGeminiに送って認識してコメントをしてくれるツールつくってみた

やったこと

  • ゲームのプレイ中の画面を3秒に1回とかの頻度でGeminiAPIに送る
  • それに対するコメントをGeminiで生成する
  • 生成したコメントをローカルのVOICEVOXで読み上げる
  • コメントのログとそのタイミングのキャプチャを保存する

注意事項

  • このコードを実行したことによって生じた損害等については自己責任でお願いします。
  • Mac(M4)で実行してるのでWindowsやLinuxで動くかどうかはわからん・・・たぶん動く?
  • このコードの改変、再配布はご自由にどうぞ。連絡も不要です。
  • むしろ、もっとこうしたらもっと良くなった!とかそういう話あったら教えてください
  • このコードはGeminiに作って貰ったので詳細はわからん・・・

準備

  • Pythonのインストール
    • バージョンは3.xなら動くんじゃ無いかな・・・知らんけど・・・
  • Pythonのライブラリのインストール
    • pip install -U google-genai opencv-python requests
  • OBSのインストール (ゲーム画面の認識用)
  • ffmpegのインストール (音声再生用)
  • ゲームのキャプチャボードを用意する
    • 1000円くらいの安いヤツでOK
  • HDMI分配器(あれば)
    • OBSの画面でプレイするなら低遅延のキャプチャボードじゃないとキツイかも
  • VOICEVOXのインストール
  • GeminiのAPIを取得

使い方

  1. OBSを起動
  2. ゲームのキャプチャを行い、OBSにて読み込む(音声はなしでOK)
  3. 仮想カメラをONにする
  4. 下記のコードを任意の場所に保存
  5. 取得したAPIをAPI_KEY = に入れて保存して実行

コード

import cv2
from google import genai
from google.genai import types
import time
import requests
import json
import random
import subprocess
from datetime import datetime
import os

# --- 設定項目 ---
API_KEY = "自分のAPIキーをここに貼り付け"
CAMERA_INDEX = 0 # OBSの仮想カメラのインデックスをここにいれる。認識しないときは0じゃないかも。調べ方はGeminiに聞いて。
VOICEVOX_URL = "http://localhost:50021"
MODEL_ID = "gemini-3.1-flash-lite-preview" # 他のモデル使いたかったら使えるモデルをGeminiに聞いて。

# 【追加】実況の頻度(秒)
INTERVAL_BASE = 2.0

# 【追加】話速の倍率 (1.0が標準。1.5なら1.5倍速)
SPEED_SCALE = 1.5

# 【追加】話者設定の辞書
# 課題:ここのプロンプトがちょっと甘めなので設定が結構ひどい。でもつむつむの語尾埼玉は最高埼玉。
CHARACTERS = {
    "metan": {
        "id": 2,
        "prompt": "あなたは視聴者の「四国めたん」です。気品あるお嬢様口調(〜ですわ、〜ですわね)で、ゲーム画面を10文字程度で実況してください。少し上から目線ですが、プレイは応援しています。"
    },
    "zundamon": {
        "id": 1,
        "prompt": "あなたは視聴者の「ずんだもん」です。元気な口調(〜なのだ、〜のだ)で、ゲーム画面を10文字程度で実況してください。ネットスラングを多用します。"
    },
    "zunko": {
        "id": 107,
        "prompt": "あなたは視聴者の「東北ずん子」です。おっとりした優しいお姉さん口調で、ゲーム画面を10文字程度で実況してください。ずんだ餅に絡めた発言をたまにします。"
    },
    "kiritan": {
        "id": 108,
        "prompt": "あなたは視聴者の「東北きりたん」です。少し生意気で冷静な毒舌家ですが、根は優しい妹キャラクターとして、ゲーム画面を10文字程度で実況してください。ゲーマーとしての視点を持ち、淡々としながらも鋭いツッコミを入れます。語尾は「〜です」「〜ですよ」「〜ですね」を基本にしてください。"
    },
    "itako": {
        "id": 109,
        "prompt": "あなたは視聴者の「東北イタコ」です。口調は(〜ですわ)で、語尾を伸ばすような実況を10文字程度でしてください。"
    },
    "tsukushi": {
        "id":8,
        "prompt": "あなたは視聴者の「春日部つくし」です。明るいバーチャル埼玉県民として、元気よくゲーム画面を10文字程度で実況してください。語尾は「〜埼玉」をたまに使いますが、ギャルっぽさもあります。"
    }
}

# 保存用ディレクトリの作成
SAVE_DIR = "snapshots"
os.makedirs(SAVE_DIR, exist_ok=True)

client = genai.Client(api_key=API_KEY.strip())

def write_log(speaker_name, text):
    """コメントをログファイルに保存する"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = f"[{timestamp}] {speaker_name}: {text}\n"
    with open("comment_log.txt", "a", encoding="utf-8") as f:
        f.write(log_entry)

def speak_vox(text, speaker_id, speed):
    """話速を指定してVOICEVOXで再生"""
    try:
        # 1. クエリ作成
        q_res = requests.post(f"{VOICEVOX_URL}/audio_query", params={'text': text, 'speaker': speaker_id})
        query_data = q_res.json()

        # 【追加】話速を上書きする
        query_data['speedScale'] = speed

        # 2. 音声合成
        s_res = requests.post(f"{VOICEVOX_URL}/synthesis", params={'speaker': speaker_id}, data=json.dumps(query_data))

        # 3. 再生
        play_process = subprocess.Popen(
            ['ffplay', '-nodisp', '-autoexit', '-'],
            stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
        play_process.stdin.write(s_res.content)
        play_process.stdin.close()
        play_process.wait()
    except Exception as e:
        print(f"VOICEVOXエラー: {e}")


def save_snapshot_and_log(speaker_name, text, frame):
    """画像とログをセットで保存する"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    # 画像の保存 (例: 20260506_223015_zundamon.jpg)
    img_filename = f"{timestamp}_{speaker_name}.jpg"
    img_path = os.path.join(SAVE_DIR, img_filename)
    cv2.imwrite(img_path, frame)
    
    # ログの保存 (画像名も記録しておく)
    log_entry = f"[{timestamp}] {speaker_name}: {text} (Img: {img_filename})\n"
    with open("comment_log.txt", "a", encoding="utf-8") as f:
        f.write(log_entry)

def main():
    cap = cv2.VideoCapture(CAMERA_INDEX)
    if not cap.isOpened():
        print("カメラが見つからないのだ...")
        return

    # 全キャラクターの名前をリストにしておく
    char_names = list(CHARACTERS.keys())
    
    print(f"AIマルチ実況(リスナー: {', '.join(char_names)})を開始するのだ!")
    consecutive_silent_count = 0

    try:
        while True:
            # 1. 密度(ランダム性)の制御
            skip_prob = max(0.2, 0.7 - (consecutive_silent_count * 0.15))
            if random.random() < skip_prob:
                print(".", end="", flush=True)
                time.sleep(1)
                continue

            # --- 喋るキャラクターをランダムに決定 ---
            current_name = random.choice(char_names)
            char_config = CHARACTERS[current_name]

            ret, frame = cap.read()
            if not ret: break

            _, buffer = cv2.imencode('.jpg', frame)
            image_bytes = buffer.tobytes()

            try:
                # 2. 選ばれたキャラのプロンプトでGeminiにリクエスト
                response = client.models.generate_content(
                    model=MODEL_ID,
                    contents=[
                        char_config["prompt"],
                        types.Part.from_bytes(data=image_bytes, mime_type='image/jpeg')
                    ]
                )
                
                comment = response.text.strip().replace("\n", "")

                if len(comment) > 1 and "なし" not in comment:
                    # ログと画像をセットで保存!
                    save_snapshot_and_log(current_name, comment, frame)
                    
                    print(f"\n[{time.strftime('%H:%M:%S')}] 【{current_name} 】: {comment}")
                    speak_vox(comment, char_config["id"], SPEED_SCALE)
                    consecutive_silent_count = 0
                else:
                    consecutive_silent_count += 1

            except Exception as e:
                print(f"\nAPIエラー: {e}")

            # 頻度調整
            wait_time = random.uniform(INTERVAL_BASE, INTERVAL_BASE + 2)
            time.sleep(wait_time)

    except KeyboardInterrupt:
        print("\n放送終了なのだ!")
    finally:
        cap.release()

if __name__ == "__main__":
    main()

備考

  • 最初はニコ生風に画面に流れるようにしたかったけど、OBS上とはいえ流れるのは邪魔だったので読み上げだけにした
  • Geminiの3.1 Flashだと2秒に1回くらいの頻度のコメだと制限レートには引っかからない感じ。6/15くらいで収まる。
  • ただし、待ち時間があるから良い感じになるだけなので、もっとコメント頻度上げると制限に引っかかるので注意。
  • 話者のプロンプトがちょっと甘めなのでもっと作り込めばもっとそれっぽくなりそう
  • この程度の生成ならローカルでもいける?今度試してみる
  • Geminiに送ってる画像はキャプチャしたままの画像なので、解像度下げたり圧縮してもいいかも。
  • コード中のVOICEVOXの話者IDはバージョンによって変わるかもしれないので、VOICEVOXを開いてから下記のURLをブラウザで開けば今の話者IDが分かります http://localhost:50021/speakers