イマジナリーフレンドを生成AIで作ってVRの世界に召喚できるのか

:::note info
召喚出来ました！
VRChat内で自分のアバター以外のなにかとお話出来るようにする流れを作りました
:::

VRChatで遊んでいて、「ログインした時間にはフレンドがいない…」なんてことがあります。
※VRChatとはVRSNS、メタバースサービスです

そんなとき、AITuberに取り組んでいたり、AIキャラコンテンツや開発者さんのSNSをTLで見てた時、ふと「AIキャラクターをVR空間に召喚して、一緒におしゃべりできたら面白そうだな」と思いました。
今回は **自分でやってみたい！** というところから、その「VR空間内でAI対話キャラクターを登場させて会話する」仕組みを作ってみたので、制作の流れや試行錯誤のポイントをまとめます。

Twitterに投稿した映像URL

https://x.com/meyukumin/status/1866141002416988572

# はじめに

**やりたかったこと**
> VR空間内にAI対話キャラクターを出現させる
「イマジナリーフレンド」的な存在
フレンドが不在な時間にAIキャラクターと楽しく会話できるようにする

このアイデアは、以前から取り組んでみたかった「OSC通信」練習も兼ねています

今回の制作では、VRChatそのものへ大掛かりな接続はありません。
VRChat側との連携は以下程度です。

1\.APIから取得したテキストをVRChat内の吹き出しとして表示
2\.音声入力はPCローカルで実行
3\.テキスト生成AIとのやりとりはコンソール上で完結
4\.AIの返答音声（VOICEVOX生成）はマイク音声としてVRChat側に出力
5\.自分の声も同時に入れられるよう、ミキシングソフトを活用
6\.ハムスターアイテム（既存製品）をそのまま活用
UnityやVRChat特有の複雑な設定はあまり多くありませんでした。
※ハムスターアイテムは既存品で、とてもわかりやすい導入マニュアルがありました！

**使用環境とツール一覧**
無料ツール・サービス
Python 3.12
Voicemeeter Banana（仮想オーディオミキサー）
マイク（手元のマイク）
VOICEVOX
OpenAI API (gpt4o-mini)
Unity
VRChat（Steamで無料！！！）

有料ツール
Boothで購入したハムスターアイテム（今回の本体的存在）1,000円
OSC通信参考資料

https://phio.booth.pm/items/6215455

https://kintone-labo.booth.pm/items/4188811

# システム構成
今回用いたパッケージは大まかに「4+2」の構成となりました。


```plantuml

title 全体システム構成シーケンス図

actor User as U
participant "メイン.py" as M
participant "音声入力.py" as VI
participant "OpenAI API" as OA
participant "VOICEVOX連携.py" as VVA
participant "voicevox_api(ローカル)" as VVP
participant "音声出力.py" as VO
participant "VRChat" as VRC

U -> M: キーボード入力 (テキスト)
note left: ユーザーはキーボードで指示を出す

U -> VI: マイク音声入力(認識要求)
VI -> VI: 音声→テキスト変換(ローカルSR)
VI --> M: テキスト返却
note over M: 入力テキストを集約

M -> OA: OpenAI APIへプロンプト送信
OA --> M: 応答テキスト

M -> VVA: TTS要求(応答テキスト)
VVA -> VVP: 音声合成リクエスト
VVP --> VVA: 音声データ応答
VVA --> M: 音声データ返却

M -> VO: 音声データ再生要求
VO --> VRC: 仮想マイク経由で音声出力

M -> VRC: OSC経由でテキストチャットメッセージ送信

U -> VRC: （ユーザーの生マイク音声入力）
note right: ユーザー自身もVRChatへマイク音声送信

```
（o1に上記書いてみてもらいました、多分いい感じ）
pythoファイルは基本4ファイルで構成
+2はこの後の音声絡みで必要なものがあってです。（後述）


# 音声構成図
ハムスターアイテム・VRChatの都合でAI音声もマイク入力に統一する必要がありました。（後述）

```mermaid
stateDiagram-v2
    [*] --> マイク入力
    マイク入力 --> VOICEVOX: マイク音声を直接入力
    VOICEVOX --> 仮想オーディオミキサー: AI音声出力
    マイク入力 --> 仮想オーディオミキサー: ユーザー音声入力
    仮想オーディオミキサー --> VRChatマイク入力: 統合音声出力
```


# 制作の流れ
**コンセプト**
あくまで自分がログインしたときに付随して現れる、イマジナリーフレンドのような存在を目指しました。
つまり、「自分専用のおしゃべり相手をその場で召喚して話したい！」という発想がベースです。

# 段階的な取り組み
基本構造はsaldraさんの本を参考に自分でも手を動かしたAITuberの構成です。

https://amzn.asia/d/5FgYQ9m

その流れから、まずはVRChatにAIの返答を出せるようにしたかったです。

https://x.com/meyukumin/status/1861366979833274783

そしてその次に「自分の声」をAIに入力できるようにステップアップ

https://x.com/meyukumin/status/1864347580521652511

**音声入力への取り組み**
YouTubeコメントではなく、「VR空間で自分が声でAIに話しかける」風にするために、音声入力が欲しくなりました。
ここではChatGPT（o1-preview版）に相談して実装方法を模索。音声認識にはいくつか既存のやり方もあるかと思いますが、今回はとにかく「VRでしゃべりたい！」という欲求が先行したため、まずは完成優先でAIに任せる形を取りました。

<details open><summary>抜粋コード</summary>

```
# 音声入力によるユーザー入力取得
key_input = get_voice_input()
if key_input is None:
    continue  # 音声認識失敗時は再度入力要求

print("あなた: " + key_input)

# Voicevoxアダプターを初期化（main.pyの前部で行っている場合は重複削除）
voicevox = VoicevoxAdapter(speaker_id=3)

# OpenAIなどのプロセスを経て、生成した返信をtextとして得たと仮定
reply_content = "AIによる生成テキスト"

# VOICEVOXへテキスト送信し、合成音声データを取得
data, sample_rate = voicevox.get_voice(reply_content)

# 合成音声を再生
play_audio(data, sample_rate)
```
</details>



# アバターの口以外から声を出す
こちらのBooth商品を購入しました

https://phio.booth.pm/items/6215455

これは、Unityの知識があまりなくても、アバター以外のオブジェクトから音声を発する手助けをしてくれるマニュアル付きの素敵なアイテムです。まさに求めていた機能を補うのにピッタリでした。

# 音声処理上の問題点と解決策
課題1：**自分の声とAIの声を分けられない**
VRChatでは「マイク入力は1本」で扱います。そのため、自分の声とAIボイス（VOICEVOX出力）を分けて扱うことが難しいです。

課題2：**PC内音声がすべて混ざってしまう**
もしPC内で再生されるYouTube音声やVRChatの環境音までもが、AIキャラクターの声と混ざってしまったら？ 全てがひとつのマイク音声として流れてしまい、非常に不便です。

解決策：**仮想オーディオミキサーソフトの導入**
上記2つの課題を解消するため、仮想オーディオミキサーソフトを使いました。

参考記事

https://magazine.vket.com/n/nc6be519da67e#bbb0a503-745d-439c-b9e5-2d5e23013e76

このソフトを使えば、

自分のマイク音声
VOICEVOXによるAI音声
PC内の他アプリ音声
これらを細かくルーティング・ミックスできます。

今回の私のミキサーソフトのキャプチャ
![スクリーンショット 2024-12-09 232837.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3772541/9d4893f1-a003-89e5-c9b2-d9299d98d47f.png)


**マイク・スピーカー番号の確認**
PC内でマイクやスピーカー番号を取得するためにコードを実行し、出力された番号にあわせてプログラム内を調整します。ただし、PCを再起動するとデバイス番号が変わることもあるため、そのたびに確認が必要です。

<details open><summary>音声入力用コード</summary>

```
# voice_input.py

import speech_recognition as sr

def get_voice_input():
    """
    事前に指定したマイクデバイスから音声を入力する。
    """
    recognizer = sr.Recognizer()

    # 使用するマイクのインデックスを指定
    mic_index = 13  # 使用したいマイクのインデックス番号を指定してください

    try:
        # マイクを指定して音声を取得
        with sr.Microphone(device_index=mic_index) as source:
            print(f"マイク '{sr.Microphone.list_microphone_names()[mic_index]}' を使用します。")
            print("音声入力を開始します。話しかけてください...")
            recognizer.adjust_for_ambient_noise(source)  # 環境ノイズを調整
            audio = recognizer.listen(source)

        # 音声をテキストに変換
        text = recognizer.recognize_google(audio, language='ja-JP')
        print("あなた（音声入力）: " + text)
        return text
    except sr.UnknownValueError:
        print("音声を認識できませんでした。もう一度お試しください。")
        return None
    except sr.RequestError as e:
        print(f"音声認識サービスにアクセスできませんでした; {e}")
        return None
    except IndexError:
        print("指定したマイクインデックスが無効です。マイクのリストを確認してください。")
        return None
    except Exception as e:
        print(f"予期しないエラーが発生しました: {e}")
        return None

```
</details>

<details open><summary>音声出力用コード</summary>

```
# voice_output.py

import numpy as np
import sounddevice as sd

def play_audio(data: np.ndarray, sample_rate: int):
    """
    指定されたデバイスを使用して音声を再生する。
    """
    # 使用するオーディオデバイスのインデックスを指定
    output_device_id = 29  # 使用したいデバイスのインデックス番号（例: 1 = ヘッドホン）

    try:
        print(f"出力デバイス: '{sd.query_devices()[output_device_id]['name']}' を使用します。")
        sd.play(data, sample_rate, device=output_device_id)
        sd.wait()
    except Exception as e:
        print(f"エラーが発生しました: {e}")

```
</details>

<details open><summary>マイク番号取得プログラム</summary>

```
# マイク番号取得プログラム

import speech_recognition as sr

# 使用可能なマイクデバイスをリスト表示
print("使用可能なマイクデバイス:")
for index, name in enumerate(sr.Microphone.list_microphone_names()):
    print(f"{index}: {name}")

```
</details>

<details open><summary>スピーカー番号取得プログラム</summary>

```
# スピーカー番号取得プログラム

import sounddevice as sd

# デバイス一覧を表示
print("使用可能なオーディオデバイス:")
devices = sd.query_devices()
for idx, device in enumerate(devices):
    print(f"{idx}: {device['name']}")

```
</details>


また、VRChatで他のフレンドと合流出来て、会話する可能性も考慮して、「自分の声」をVRChatへ常時流せるようにもしておきます。
マイク音声をVRChatアプリケーションに対してカットは絶対しません。
つまり、「AI対話用」と「フレンドとの会話用」のどちらにも入出力できるようにする必要がありました。


# 将来の改善ポイントメモ
遅延対策
音声認識精度の向上
Real Time API対応
任意の音声モデルやAPI先の差し替え（Dify等）
⇒バーチャルアシスタント機能拡張（リアルタイム情報活用）
⇒カレンダーとかタスクの相談、メモとか出来てもいいですよね

12月12日現在、Googleの**Gemini**のニュースなども流れ始めていて、今後はよりリアルタイムな情報取得や、規約面をクリアしたうえで映像の解析なども可能になれば、さらに面白いVRアシスタントが実現しそうだとわくわくしています。

あとは改善じゃないけど、この制作に触発されてやりたいこと出来ちゃったんでそれも世間的な冬休み期間にやりたいなあと思ってまする..

# まとめ
今回は、VRChat内にAIキャラを召喚して対話するまでの流れを紹介しました。まだまだ改良の余地はありますが、ひとまず「VRで独りでもAIと会話できる環境」を実現できたのは、私にとっては大きな一歩です！
楽しい！
これからも技術の進歩やサービスの充実に合わせて、ちょぼちょぼとより自然なVR空間でのAIとのコミュニケーションができるよう遊んでいきたいと思ってます！


# 自己紹介
名前：toMo
X：@meyukumin

https://x.com/meyukumin

飛行機✈️と特撮と性の話とサイエンスとメタバースとAIが好きな掛け合わせも楽しんでいく物好き

とりあえずのノリも含め気軽に生成AIの話題とかVR、メタバースの話題を楽しんでいます！
交流したり一緒にお話も出来たら嬉しいです！