MENU

AiTuber開発日誌 8 会話が続くようにする。

前回は声・頭脳・身体の連携に成功しました。が、
プログラムの実行後に一度会話して リアクションしたら、そこでプログラムが終了してしまうので、一回しか会話ができません。ちゃんと会話が続くように、修正します!

目次

LMに進捗を報告

つくる人

うまくいきました! だけど、会話の度に都度
main.pyの実行を手動で繰り返しています。

AITuber 開用LMの回答

うまくいって本当によかったです!AITuberとしての基本システムが完成しましたね。

「会話のたびに main.py を再起動している」という状況は、おそらくエラーが発生してプログラムが落ちているか、あるいはループ処理が途中で止まってしまう設計になっている可能性があります。

本来、AITuberは「一度起動したら、あなたが止めるまで永遠に待機し続ける(常駐する)」のが正しい動きです。

これを解決するために、「エラーが起きても無視して動き続ける(死なない)」 ように main.py を少し改造します。以下の「不死身のループ」版コードに書き換えてください。

つくる人

不死身のループwww
言葉選びがいつも逸品だな。

修正のポイント

1. try-except をループの中に入れる

ネットワークエラーや一時的な不具合が起きても、エラーメッセージを表示して、プログラムを停止させずに次のループへ進むようにします。

2. 待機時間の調整

コメントがない時やエラー時は少し長めに休むことで、PCへの負担とAPIの無駄遣いを防ぎます

このコードに全置換してください
import asyncio
import random
import traceback  # エラーの詳細を表示するために追加
from config.settings import settings
# 各種モジュールのインポート
from modules.cognition.llm_client import LLMClient
from modules.expression.vtube_studio import VTubeStudioClient
from modules.expression.voicevox import VoicevoxClient
from modules.perception.youtube import YouTubeCommentFetcher

# キャラクター設定
SYSTEM_PROMPT = """
あなたは「AITuber」のアイです。
元気で明るい、少しおっちょこちょいな性格です。
リスナーのコメントに対して、短くテンポよく返事をしてください。
"""

async def main():
    print("🚀 AITuberシステムを起動します...")
    print("✨ 'Ctrl + C' を押すまで、永遠に動き続けます!")

    # 1. 各モジュールの初期化
    brain = LLMClient(system_prompt=SYSTEM_PROMPT)
    voice = VoicevoxClient()
    body = VTubeStudioClient()
    youtube = YouTubeCommentFetcher()

    # 2. VTube Studioに接続
    # 接続に失敗しても、何度でも再トライするループにする
    while True:
        try:
            if await body.connect():
                print("✅ VTube Studio接続成功!")
                break
            else:
                print("❌ VTube Studioが見つかりません。5秒後に再試行します...")
                await asyncio.sleep(5)
        except Exception as e:
            print(f"⚠️ VTS接続エラー: {e}")
            await asyncio.sleep(5)

    # 会話履歴
    history = []
    print(f"✅ 準備完了!YouTube配信(ID: {settings.YOUTUBE_VIDEO_ID})のコメントを監視中...")

    # --- 無限ループ開始 ---
    while True:
        try:
            # --- 1. YouTubeコメントの取得 ---
            comments = youtube.fetch_comments()

            # コメントが来ていない場合は待機して次へ
            if not comments:
                await asyncio.sleep(5)  # 負荷軽減のため5秒待つ
                continue

            # 最新のコメントを取得
            last_comment = comments[-1]
            user_name = last_comment["user"]
            user_text = last_comment["text"]

            print(f"\n💬 {user_name}: {user_text}")

            # "終了" コマンドでシステムを安全に停止
            if user_text == "終了":
                print("システムを終了します。")
                break

            # --- 2. 思考 (Brain) ---
            print("🤖 (思考中...)")
            prompt_text = f"リスナーの「{user_name}」さんが「{user_text}」と言っています。返事をしてあげて。"
            
            # AI応答生成(ここでもエラーが起きても止まらないようにする)
            try:
                response_text = await brain.generate_response(prompt_text, history)
            except Exception as e:
                print(f"❌ AI生成エラー: {e}")
                response_text = "ごめんね、ちょっと頭が回らなかったみたい!もう一回言ってくれる?"

            print(f"アイ: {response_text}")

            # 履歴更新
            history.append({"role": "user", "content": f"{user_name}: {user_text}"})
            history.append({"role": "assistant", "content": response_text})
            
            # 履歴が長くなりすぎたら古いのを削除(メモリ節約)
            if len(history) > 20:
                history = history[-10:]

            # --- 3. 表現 (Voice & Body) ---
            
            # 口パクのタスク
            async def lip_sync_simulation():
                while True:
                    await body.set_parameter("MouthOpen", random.uniform(0.0, 1.0))
                    await asyncio.sleep(0.1)
            
            # 音声再生と口パクを開始
            lip_task = asyncio.create_task(lip_sync_simulation())
            
            # 声を再生(エラーが起きても止まらないように)
            try:
                await asyncio.to_thread(voice.speak, response_text)
            except Exception as e:
                print(f"❌ 音声再生エラー: {e}")
            
            # 喋り終わったら口パク停止
            lip_task.cancel()
            await body.set_parameter("MouthOpen", 0.0)

        except KeyboardInterrupt:
            # Ctrl+C が押されたらループを抜ける
            print("\n🛑 強制終了します")
            break
        except Exception as e:
            # 予期せぬエラーが起きても、ログを出してループを継続(これが重要!)
            print(f"⚠️ 予期せぬエラーが発生しましたが、復旧します: {e}")
            traceback.print_exc() # エラーの詳しい場所を表示
            await asyncio.sleep(3) # エラー連打を防ぐため少し休む

    # 終了処理
    await body.close()

if __name__ == "__main__":
    asyncio.run(main())

この修正で変わること

1. エラーでも止まらない

try...except Exception というブロックで処理を囲んだため、例えば「YouTubeの接続が一瞬切れた」「OpenAIが混雑してエラーを返した」といった場合でも、⚠️ 予期せぬエラーが発生しましたが、復旧します と表示して、プログラム自体は動き続けます

2. VTube Studio接続待機

起動時にVTube Studioがまだ立ち上がっていなくても、エラー終了せず「5秒後に再試行」しながら待ち続けるようになります。

3. メモリ管理

会話履歴(history)が増えすぎるとAIの動作が重くなるため、20ラリーを超えたら古いものを忘れる機能を追加しました

修正という名のコピペ!

つくる人

ありがとうLM!
さあ、コピペで直すぞ!

main.py内を全部コピペで差し替え
つくる人

完了! はやいはやい!
ファイルを保存したら実行だ!

修正結果は・・・

ターミナルに実行コードを打ち込んで起動します。
実行コード

python main.py
エラー発生!

2026-02-06 12:29:06 | ERROR | modules.expression.vtube_studio:connect:57 – VTube Studio接続エラ ー: [WinError 1225] リモート コンピューターによりネットワーク接続が拒否されました。
❌ VTube Studioが見つかりません。5秒後に再試行します…

つくる人

あ、 VTube Studio立ち上げるの忘れてた。
ちゃんとエラーが表示されたのでOK!

仕切りなおして。。。

つくる人

各アプリ起動準備よし!
実行します!!

やっぱり一個ずつしか実行しません。

🚀 AITuberシステムを起動します…
✨ ‘Ctrl + C’ を押すまで、永遠に動き続けます!

つくる人

いいえ、一回で止まりました

シンプルにプログラムを繰り返す命令を追加するだけでいい気もするのでLMに聞いてみます。

つくる人

main.py に繰り返しの文
入れるだけでいいんじゃないの

AITuber 開用LMの回答

はい、その通りです!大正解です。
main.py の処理を while True などのループに入れるだけで、起動しっぱなしでコメントを待ち受ける「常駐型」のプログラムになります。

つくる人

あ、うん
なんかありがと

LM回答の続き

ただし、単純にループさせるだけだと3つの問題が発生します。それを防ぐための「おまじない」を加えた**「完成版・無限ループコード」**を用意しました。

単純なループで起きる3つの問題と対策
1. PCが固まる(CPU暴走)

対策: コメントがない時に await asyncio.sleep() で「休憩」を入れます。これを入れないと、プログラムが全速力で「コメントまだ?まだ?まだ?」と確認し続けてPCが重くなります。

2. エラーで即死する

対策: ネットが一瞬切れたり、APIがエラーを返したりしただけで止まらないよう、try-except でエラーを無視してループを続けるようにします

3. 記憶があふれる

対策: 会話履歴(history)が無限に増えると、AIの記憶容量(トークン)オーバーでエラーになります。古い記憶を捨てる処理を追加します

【コピペでOK】完成版・常駐型 main.py

このコードに全書き換えしてください。これで「Ctrl+C」を押して強制終了するまで、永遠に動き続けます。

コード
import asyncio
import random
import traceback  # エラーの詳細を表示するために追加
from config.settings import settings
# 各種モジュールのインポート
from modules.cognition.llm_client import LLMClient
from modules.expression.vtube_studio import VTubeStudioClient
from modules.expression.voicevox import VoicevoxClient
from modules.perception.youtube import YouTubeCommentFetcher

# キャラクター設定
SYSTEM_PROMPT = """
あなたは「AITuber」のアイです。
元気で明るい、少しおっちょこちょいな性格です。
リスナーのコメントに対して、短くテンポよく返事をしてください。
"""

async def main():
    print("🚀 AITuberシステムを起動します...")
    print("✨ 'Ctrl + C' を押すまで、永遠に動き続けます!")

    # 1. 各モジュールの初期化
    brain = LLMClient(system_prompt=SYSTEM_PROMPT)
    voice = VoicevoxClient()
    body = VTubeStudioClient()
    youtube = YouTubeCommentFetcher()

    # 2. VTube Studioに接続(失敗してもリトライし続ける)
    while True:
        try:
            if await body.connect():
                print("✅ VTube Studio接続成功!")
                break
            else:
                print("❌ VTube Studioが見つかりません。5秒後に再試行します...")
                await asyncio.sleep(5)
        except Exception as e:
            print(f"⚠️ VTS接続エラー: {e}")
            await asyncio.sleep(5)

    # 会話履歴
    history = []
    print(f"✅ 準備完了!YouTube配信(ID: {settings.YOUTUBE_VIDEO_ID})のコメントを監視中...")

    # --- 無限ループ開始 ---
    while True:
        try:
            # --- 1. YouTubeコメントの取得 ---
            comments = youtube.fetch_comments()

            # コメントが来ていない場合は休憩して次へ
            if not comments:
                await asyncio.sleep(5)  # 5秒待機(負荷軽減のため重要!)[1]
                continue

            # 最新のコメントを取得
            last_comment = comments[-1]
            user_name = last_comment["user"]
            user_text = last_comment["text"]

            print(f"\n💬 {user_name}: {user_text}")

            # "終了" コマンドでシステムを安全に停止
            if user_text == "終了":
                print("システムを終了します。")
                break

            # --- 2. 思考 (Brain) ---
            print("🤖 (思考中...)")
            prompt_text = f"リスナーの「{user_name}」さんが「{user_text}」と言っています。返事をしてあげて。"
            
            # AI応答生成(エラーが起きても止まらないようにガード)
            try:
                response_text = await brain.generate_response(prompt_text, history)
            except Exception as e:
                print(f"❌ AI生成エラー: {e}")
                response_text = "ごめんね、ちょっと頭が回らなかったみたい!もう一回言ってくれる?"

            print(f"アイ: {response_text}")

            # 履歴更新
            history.append({"role": "user", "content": f"{user_name}: {user_text}"})
            history.append({"role": "assistant", "content": response_text})
            
            # 【重要】記憶の整理:履歴が20個を超えたら古いものを忘れる[4]
            if len(history) > 20:
                history = history[-10:] # 最新10個だけ残す

            # --- 3. 表現 (Voice & Body) ---
            
            # 口パクのタスク
            async def lip_sync_simulation():
                while True:
                    await body.set_parameter("MouthOpen", random.uniform(0.0, 1.0))
                    await asyncio.sleep(0.1)
            
            # 音声再生と口パクを開始
            lip_task = asyncio.create_task(lip_sync_simulation())
            
            # 声を再生
            try:
                await asyncio.to_thread(voice.speak, response_text)
            except Exception as e:
                print(f"❌ 音声再生エラー: {e}")
            
            # 喋り終わったら口パク停止
            lip_task.cancel()
            await body.set_parameter("MouthOpen", 0.0)

        except KeyboardInterrupt:
            # Ctrl+C が押されたらループを抜ける
            print("\n🛑 強制終了します")
            break
        except Exception as e:
            # 予期せぬエラーが起きても、ログを出してループを継続(これが重要!)
            print(f"⚠️ 予期せぬエラーが発生しましたが、復旧します: {e}")
            traceback.print_exc() # エラーの詳しい場所を表示
            await asyncio.sleep(3) # エラー連打を防ぐため少し休む

    # 終了処理
    await body.close()

if __name__ == "__main__":
    asyncio.run(main())

リトライ!でも。。。

つくる人

main.pyを差し替えてそのまま保存!
いざ、リトライ!

実行コード

python main.py

コードを実行したあと、

(.venv) PS D:\02-my-app\00_AIvtube\my-aituber> 

こちらのコード「次の命令待ってます」状態になるので、問題が全く解決していません。

つくる人

解決方法がLMにはないようだな。こんな時は
たのむぜGPT!

別のAI(チャットGPT)にファイルと、ターミナルのログを見せて問題を解消してもらいます。

助っ人GPTの活躍!

ChatGPTにコードを共有して、解決方法を訊きます。

質問の内容

ログを拾ってコメントを返すプログラムだけど
ターミナルでプログラムを一周実行するたびに終了してしまう。

ちゃんと繰り返してほしい

import asyncio
import random
import traceback # エラーの詳細を表示するために追加
from config.settings import settings

各種モジュールのインポート

from modules.cognition.llm_client import LLMClient
from modules.expression.vtube_studio import VTubeStudioClient
from modules.expression.voicevox import VoicevoxClient
from modules.perception.youtube import YouTubeCommentFetcher

キャラクター設定

SYSTEM_PROMPT = “””
あなたは「AITuber」のアイです。
元気で明るい、少しおっちょこちょいな性格です。
リスナーのコメントに対して、短くテンポよく返事をしてください。
“””

async def main():
print(“🚀 AITuberシステムを起動します…”)
print(“✨ ‘Ctrl + C’ を押すまで、永遠に動き続けます!”)

# 1. 各モジュールの初期化
brain = LLMClient(system_prompt=SYSTEM_PROMPT)
voice = VoicevoxClient()
body = VTubeStudioClient()
youtube = YouTubeCommentFetcher()

# 2. VTube Studioに接続(失敗してもリトライし続ける)
while True:
    try:
        if await body.connect():
            print("✅ VTube Studio接続成功!")
            break
        else:
            print("❌ VTube Studioが見つかりません。5秒後に再試行します...")
            await asyncio.sleep(5)
    except Exception as e:
        print(f"⚠️ VTS接続エラー: {e}")
        await asyncio.sleep(5)

# 会話履歴
history = []
print(f"✅ 準備完了!YouTube配信(ID: {settings.YOUTUBE_VIDEO_ID})のコメントを監視中...")

# --- 無限ループ開始 ---
while True:
    try:
        # --- 1. YouTubeコメントの取得 ---
        comments = youtube.fetch_comments()

        # コメントが来ていない場合は休憩して次へ
        if not comments:
            await asyncio.sleep(5)  # 5秒待機(負荷軽減のため重要!)[1]
            continue

        # 最新のコメントを取得
        last_comment = comments[-1]
        user_name = last_comment["user"]
        user_text = last_comment["text"]

        print(f"\n💬 {user_name}: {user_text}")

        # "終了" コマンドでシステムを安全に停止
        if user_text == "終了":
            print("システムを終了します。")
            break

        # --- 2. 思考 (Brain) ---
        print("🤖 (思考中...)")
        prompt_text = f"リスナーの「{user_name}」さんが「{user_text}」と言っています。返事をしてあげて。"

        # AI応答生成(エラーが起きても止まらないようにガード)
        try:
            response_text = await brain.generate_response(prompt_text, history)
        except Exception as e:
            print(f"❌ AI生成エラー: {e}")
            response_text = "ごめんね、ちょっと頭が回らなかったみたい!もう一回言ってくれる?"

        print(f"アイ: {response_text}")

        # 履歴更新
        history.append({"role": "user", "content": f"{user_name}: {user_text}"})
        history.append({"role": "assistant", "content": response_text})

        # 【重要】記憶の整理:履歴が20個を超えたら古いものを忘れる[4]
        if len(history) > 20:
            history = history[-10:] # 最新10個だけ残す

        # --- 3. 表現 (Voice & Body) ---

        # 口パクのタスク
        async def lip_sync_simulation():
            while True:
                await body.set_parameter("MouthOpen", random.uniform(0.0, 1.0))
                await asyncio.sleep(0.1)

        # 音声再生と口パクを開始
        lip_task = asyncio.create_task(lip_sync_simulation())

        # 声を再生
        try:
            await asyncio.to_thread(voice.speak, response_text)
        except Exception as e:
            print(f"❌ 音声再生エラー: {e}")

        # 喋り終わったら口パク停止
        lip_task.cancel()
        await body.set_parameter("MouthOpen", 0.0)

    except KeyboardInterrupt:
        # Ctrl+C が押されたらループを抜ける
        print("\n🛑 強制終了します")
        break
    except Exception as e:
        # 予期せぬエラーが起きても、ログを出してループを継続(これが重要!)
        print(f"⚠️ 予期せぬエラーが発生しましたが、復旧します: {e}")
        traceback.print_exc() # エラーの詳しい場所を表示
        await asyncio.sleep(3) # エラー連打を防ぐため少し休む

   # 終了処理
    await body.close()

if __name__ == "__main__":
    asyncio.run(main())

GPT回答

原因はほぼコレです。

**await body.close()while True: の中に入っちゃってる(インデント崩れ)**ので、1周回ったタイミングでVTube Studio接続を閉じて、そのまま処理が終わったように見えます。
(さらに if __name__ == "__main__": asyncio.run(main()) があるのに、この断片だけだと main() 定義が見えないので、実コード側での構造も要確認)

ここもチェック(よくある“1周で終わる”原因)

  • await body.close() がループ内にある(今回の症状ドンピシャ)
  • 例外で落ちてるのに、上位で握れてない(main() の外で落ちてる等)
  • youtube.fetch_comments() が例外を投げて、その例外が外側で終了してる(この修正版なら継続)

直し方

  • 無限ループの外で close する(finally: に入れるのが安全)
  • ついでに、口パクtaskは cancel() 後に 握りつぶすCancelledError 対策)
“このまま置き換えできる形” の例です。(これだけだと直りません!)
import asyncio, random, traceback

async def main():
    # 1. 各モジュールの初期化
    brain = LLMClient(system_prompt=SYSTEM_PROMPT)
    voice = VoicevoxClient()
    body = VTubeStudioClient()
    youtube = YouTubeCommentFetcher()

    # 2. VTube Studioに接続(失敗してもリトライし続ける)
    while True:
        try:
            if await body.connect():
                print("✅ VTube Studio接続成功!")
                break
            else:
                print("❌ VTube Studioが見つかりません。5秒後に再試行します...")
                await asyncio.sleep(5)
        except Exception as e:
            print(f"⚠️ VTS接続エラー: {e}")
            await asyncio.sleep(5)

    history = []
    print(f"✅ 準備完了!YouTube配信(ID: {settings.YOUTUBE_VIDEO_ID})のコメントを監視中...")

    try:
        # --- 無限ループ開始 ---
        while True:
            try:
                comments = youtube.fetch_comments()

                if not comments:
                    await asyncio.sleep(5)
                    continue

                last_comment = comments[-1]
                user_name = last_comment["user"]
                user_text = last_comment["text"]

                print(f"\n💬 {user_name}: {user_text}")

                if user_text == "終了":
                    print("システムを終了します。")
                    break

                print("🤖 (思考中...)")
                prompt_text = f"リスナーの「{user_name}」さんが「{user_text}」と言っています。返事をしてあげて。"

                try:
                    response_text = await brain.generate_response(prompt_text, history)
                except Exception as e:
                    print(f"❌ AI生成エラー: {e}")
                    response_text = "ごめんね、ちょっと頭が回らなかったみたい!もう一回言ってくれる?"

                print(f"アイ: {response_text}")

                history.append({"role": "user", "content": f"{user_name}: {user_text}"})
                history.append({"role": "assistant", "content": response_text})

                if len(history) > 20:
                    history = history[-10:]

                # --- 口パク ---
                async def lip_sync_simulation():
                    try:
                        while True:
                            await body.set_parameter("MouthOpen", random.uniform(0.0, 1.0))
                            await asyncio.sleep(0.1)
                    except asyncio.CancelledError:
                        # cancelされたら静かに終了
                        return

                lip_task = asyncio.create_task(lip_sync_simulation())

                try:
                    await asyncio.to_thread(voice.speak, response_text)
                except Exception as e:
                    print(f"❌ 音声再生エラー: {e}")
                finally:
                    lip_task.cancel()
                    # cancel完了を待つ(CancelledErrorを握りつぶす)
                    try:
                        await lip_task
                    except asyncio.CancelledError:
                        pass
                    await body.set_parameter("MouthOpen", 0.0)

            except KeyboardInterrupt:
                print("\n🛑 強制終了します")
                break
            except Exception as e:
                print(f"⚠️ 予期せぬエラーが発生しましたが、復旧します: {e}")
                traceback.print_exc()
                await asyncio.sleep(3)

    finally:
        # ★終了処理は「無限ループの外」で必ず実行
        try:
            await body.close()
        except Exception as e:
            print(f"⚠️ close中にエラー: {e}")

if __name__ == "__main__":
    asyncio.run(main())

NotebookLMはユーザーが入れたデータのみを情報源として推論を行うので、特定の分野に特化したAIとして大活躍しますが、反面。閉鎖的な環境なので応用力や修正作業に制限が出てしまいがち。
それを補うためにはChatGPTなど、情報源をネット全体から取得してくれるAIに問いを投げるのが有効です。 こちらは膨大な情報源から推論を行うため、ハルシネーション(誤情報やウソ)が混在しやすくなりますが、それは応用や大胆な方針転換の提案もしてくれるため。場合によっては問題解決の突破口を開いてくれます

つくる人

ふむ、コピペしたがうまくいかんな。。。

何度もmain.pyを修正 → ターミナルで実行を繰り返して、そのたびに結果(ターミナルのログをコピペ)をGPTに報告。繰り返して解決にたどりつけました。

最終的に安定したmain.pyのコード
import asyncio
import random
import traceback
import uuid
import multiprocessing as mp
import signal
import sys

from config.settings import settings

from modules.cognition.llm_client import LLMClient
from modules.expression.vtube_studio import VTubeStudioClient
from modules.expression.voicevox import VoicevoxClient
from modules.perception.youtube import YouTubeCommentFetcher


SYSTEM_PROMPT = """
あなたは「AITuber」のアイです。
元気で明るい、少しおっちょこちょいな性格です。
リスナーのコメントに対して、短くテンポよく返事をしてください。
"""

MAX_LIP_SECONDS = 12


def drain_queue(q: mp.Queue, max_items: int = 50):
    """
    キューに溜まっている古い音声を捨てる(最新優先)
    ※ mp.Queue の empty/qsize はWindowsで信用できないので get_nowait で抜く
    """
    for _ in range(max_items):
        try:
            q.get_nowait()
        except Exception:
            break


def voice_worker(req_q: mp.Queue):
    voice = VoicevoxClient()
    while True:
        item = req_q.get()
        if item is None:
            break

        _req_id, text = item
        try:
            voice.speak(text)
        except KeyboardInterrupt:
            # Ctrl+C が worker に飛んでも main で止めたいので、ここでは無視して継続
            continue
        except BaseException:
            # worker側が落ちても main は動かし続ける
            traceback.print_exc()


def restart_voice_worker(old_proc: mp.Process, req_q: mp.Queue) -> mp.Process:
    try:
        if old_proc is not None and old_proc.is_alive():
            old_proc.terminate()
            old_proc.join(timeout=2)
    except Exception:
        pass

    proc = mp.Process(target=voice_worker, args=(req_q,), daemon=True)
    proc.start()
    return proc


async def wait_or_stop(seconds: float, stop_event: asyncio.Event):
    try:
        await asyncio.wait_for(stop_event.wait(), timeout=seconds)
    except asyncio.TimeoutError:
        return


async def lip_sync_for(body, seconds: float, stop_event: asyncio.Event):
    end = asyncio.get_running_loop().time() + seconds
    try:
        while not stop_event.is_set() and asyncio.get_running_loop().time() < end:
            await body.set_parameter("MouthOpen", random.uniform(0.0, 1.0))
            await wait_or_stop(0.1, stop_event)
    finally:
        try:
            await body.set_parameter("MouthOpen", 0.0)
        except Exception:
            pass


async def connect_vts_forever(body, stop_event: asyncio.Event):
    while not stop_event.is_set():
        try:
            if await body.connect():
                print("✅ VTube Studio接続成功!")
                return True
            print("❌ VTube Studioが見つかりません。5秒後に再試行します...")
            await wait_or_stop(5, stop_event)
        except Exception as e:
            print(f"⚠️ VTS接続エラー: {e}")
            await wait_or_stop(5, stop_event)
    return False


async def stop_listener(stop_event: asyncio.Event):
    """
    Windows/multiprocessing環境で Ctrl+C が不安定なので、
    「q + Enter」で確実に止める。
    """
    print("🛑 停止したいときは 'q' を入力して Enter(Ctrl+Cは環境により不安定)")
    while not stop_event.is_set():
        try:
            line = await asyncio.to_thread(sys.stdin.readline)
        except Exception:
            await asyncio.sleep(0.5)
            continue

        if not line:
            continue
        if line.strip().lower() in ("q", "quit", "exit"):
            stop_event.set()
            return


async def main():
    print("🚀 AITuberシステムを起動します...")
    print("✨ 'Ctrl + C' を押すまで、永遠に動き続けます!")

    stop_event = asyncio.Event()
    loop = asyncio.get_running_loop()

    # 可能ならSIGINTも stop_event にする(Windowsでは効かない場合あり)
    try:
        loop.add_signal_handler(signal.SIGINT, stop_event.set)
    except NotImplementedError:
        pass

    # 停止入力リスナー(確実に止める保険)
    asyncio.create_task(stop_listener(stop_event))

    brain = LLMClient(system_prompt=SYSTEM_PROMPT)
    body = VTubeStudioClient()
    youtube = YouTubeCommentFetcher()

    req_q: mp.Queue = mp.Queue()
    vp = restart_voice_worker(None, req_q)

    history = []

    try:
        ok = await connect_vts_forever(body, stop_event)
        if not ok:
            return

        print(f"✅ 準備完了!YouTube配信(ID: {settings.YOUTUBE_VIDEO_ID})のコメントを監視中...")

        last_seen_key = None
        last_alive = asyncio.get_running_loop().time()

        while not stop_event.is_set():
            try:
                comments = youtube.fetch_comments()

                if not comments:
                    now = asyncio.get_running_loop().time()
                    if now - last_alive > 15:
                        print("…待機中(コメントなし)")
                        last_alive = now
                    await wait_or_stop(5, stop_event)
                    continue

                last_comment = comments[-1]
                user_name = last_comment.get("user", "unknown")
                user_text = last_comment.get("text", "")

                comment_id = last_comment.get("id")
                key = comment_id if comment_id is not None else (user_name, user_text)

                if key == last_seen_key:
                    await wait_or_stop(1, stop_event)
                    continue
                last_seen_key = key

                print(f"\n💬 {user_name}: {user_text}")

                if user_text.strip() == "終了":
                    print("システムを終了します。")
                    stop_event.set()
                    break

                print("🤖 (思考中...)")
                prompt_text = f"リスナーの「{user_name}」さんが「{user_text}」と言っています。返事をしてあげて。"

                try:
                    response_text = await brain.generate_response(prompt_text, history)
                except Exception as e:
                    print(f"❌ AI生成エラー: {e}")
                    response_text = "ごめんね、ちょっと頭が回らなかったみたい!もう一回言ってくれる?"

                if stop_event.is_set():
                    break

                print(f"アイ: {response_text}")

                history.append({"role": "user", "content": f"{user_name}: {user_text}"})
                history.append({"role": "assistant", "content": response_text})
                if len(history) > 20:
                    history = history[-10:]

                if not vp.is_alive():
                    print("⚠️ 音声ワーカーが停止していました。再起動します。")
                    vp = restart_voice_worker(vp, req_q)

                # ★古い音声を捨てて、最新だけ喋らせる(テキストと音声のズレ防止)
                drain_queue(req_q)
                req_q.put((str(uuid.uuid4()), response_text))

                await lip_sync_for(body, MAX_LIP_SECONDS, stop_event)

                print("✅ ループ継続:次のコメント取得へ")

            except KeyboardInterrupt:
                stop_event.set()
                break
            except Exception as e:
                print(f"⚠️ ループ内エラー(継続します): {e}")
                traceback.print_exc()
                await wait_or_stop(3, stop_event)

    finally:
        try:
            await body.close()
        except Exception:
            pass

        try:
            req_q.put(None)
        except Exception:
            pass

        try:
            if vp is not None and vp.is_alive():
                vp.terminate()
                vp.join(timeout=2)
        except Exception:
            pass

        print("👋 終了しました")


if __name__ == "__main__":
    mp.freeze_support()
    asyncio.run(main())

助っ人につくってもらったコードは、念のためLMの方にも入れて共有しておきます。(プログラムは読んでくれないのでtxt形式で!)

つくる人

ありがとうGPT!

解決したのでLMに報告とコードの共有

LMのソースにも入れましたが、進捗状況としてmain.py内のコードをチャット画面で共有して、次にすすみます。

つくる人

main.pyの中身は最後はこちらにしたよ!
ファイル名:main.pyの中身(一例).txt

コードをチャット欄に貼り付けると長いので、新しく入力したソースファイルの名前で共有しています。

AITuber 開用LMの回答

共有いただいた main.py のコードを確認しました。 非常に素晴らしい完成度です!

単に動くだけでなく、実運用で必ず直面する「音声生成中のフリーズ」や「終了時のトラブル」まで考慮された、プロ仕様に近い設計になっています。

特に以下の3点が、初心者レベルを超えていて素晴らしいです。

1. マルチプロセス化 (multiprocessing

重たい音声合成処理(VOICEVOX)を別プロセス(voice_worker)に逃がしているため、アイちゃんが喋っている最中でも、メインループ(コメント取得やVTube Studioとの通信)が止まらずスムーズに動きます。

2. キューのドレイン処理 (drain_queue)

コメントが殺到したときに、古い音声生成待ちを捨てて「最新の話題」に即座に反応できる仕組みが入っています。これにより、会話のラグ(遅延)を最小限に抑えられます

3. Windows対応の停止処理 (stop_listener

Windows環境での Ctrl+C の不安定さを考慮し、「q + Enter」で安全に終了できる仕組みを入れているのは、非常に実戦的な配慮です。

さあ、次のステージへ!(Next Steps)

システム(脳・声・体・耳)の統合は完璧です。次は、このアイちゃんを**「配信者」としてデビューさせるための環境構築と、「表現力」の強化**に進みましょう。

次回、いよいよ配信環境の構築!

つくる人

ChatGPTと作ったコードを
LMがベタ褒めしてくれたぜ。頑張った甲斐があったよ

どうやら次回は配信環境を整える作業になりそうです。まだまだ改善点はありますが。テストで生配信いきたいですね!

よかったらシェアしてね!
  • URLをコピーしました!
目次