AI VTuber18 MIN READ

AI VTuber 2人を1台のPCで喋らせる|音声・VTube Studio・字幕で詰まった設計ポイント

SHIRORASHI LAB NOTEDUO / AUDIO ROUTING実装・ログ・公式仕様確認 / 更新 2026.07.31
目次13項目

AI VTuberを1人だけ動かすところまでは、LLM、音声合成、VTube Studio、OBSを順番につなげれば形になります。しかし、キャラクターを2人に増やすと、単純に同じ処理を2回実行するだけでは済みません。

私が作っている姉妹2人組のAI VTuberでは、YouTube Liveのコメントを受け取り、単独発話、2人の掛け合い、短い後置相づちを生成します。ただし、音声は必ず1ターンずつ順番に再生します。実装を進める中で特に困ったのが、音声再生と字幕表示が「現在喋っている人は1人」という前提で作られていたことでした。

この記事では、実際のコードと動作ログを基に、1台のWindows PCで2人を動かすために分離した部分と、あえて同時発話を見送った理由をまとめます。

この記事で確認しているのは、主に音声経路、字幕、VTube Studio制御の構成です。OBS、VTube Studio、TTSを同時に動かしたときの性能はPC構成で変わるため、「どのPCでも同じ速度で動く」という性能保証ではありません。
音声合成とLLMを初めてつなぐ場合は、先にClaude API×VOICEVOXでAIキャラをしゃべらせる入門記事を読むと、この記事の構成を理解しやすくなります。

この記事で分かること

  • キャラクターごとに音声出力先を分ける理由
  • VB-CABLEを2系統使うときの注意点
  • VTube Studioを2インスタンス制御する設定
  • 字幕が1枚のままだと同時発話に対応できない理由
  • TTSの並列生成と音声の同時再生が別問題である理由
  • 最初から同時発話へ対応する場合に必要な設計

2人構成では何を分ける必要があるか

現在の構成では、姉と妹にそれぞれ専用の音声経路とVTube Studio接続先を割り当てています。一方、コメント取得、会話生成、字幕サーバーは共有しています。

1台のWindows PCで会話生成を共有しながら、2人の音声とVTube Studio接続を分離し、最後にOBSへまとめる全体構成図

図の見方:左側の1台のPCと会話制御は共有しますが、発話ターンが決まった後は青と赤のキャラクター別経路へ分かれます。さらに各キャラクター内でも、TTSからVB-CABLEを通ってOBSへ入る音声経路と、Public APIからVTube Studioを制御してOBSへ映像を入れる経路は並列です。音声がVTube Studioを経由する構成ではありません。

共有できるか
TTS設定姉用の声・モデル妹用の声・モデル分離する
音声出力CABLE-A InputCABLE-B Input分離する
VTube Studio127.0.0.1:8001127.0.0.1:8002分離する
VTS認証情報姉用トークンファイル妹用トークンファイル分離する
コメント取得同じ配信コメント同じ配信コメント共有できる
字幕サーバー同じWebSocket同じWebSocket直列再生なら共有できる

重要なのは、キャラクターの設定を分けるだけでなく、外部ソフトへ接続する出口も分けることです。

壁1:仮想オーディオケーブルが1本では足りない

現在の1プロセス構成で、OBSから2人の音量、ミュート、フィルターを別々に扱うには、音声を別のデバイスへ出力する必要があります。別プロセス化やOBSのアプリケーション音声キャプチャなど別方式もありますが、この記事では実装済みの仮想オーディオ分離を扱います。VTube Studioのマイクリップシンクを使う場合もキャラクターごとの入力が必要ですが、現在のプログラムは口と表情をPublic APIから制御しているため、VTube Studioのマイク入力をリップシンクには使っていません。

VB-Audio公式ページによると、通常のVB-CABLEとは別に、独立した2本の仮想ケーブルを追加するVB-CABLE A+Bが提供されています。A+Bは通常版と異なり、個人利用でもWebShopでライセンスを購入してダウンロード権を受け取る形式です。「Donationware for download」は金額を選べる販売形式を指し、A+Bを無料で入手できるという意味ではありません。2人分の独立した経路が必要なら、この追加費用を事前に想定しておく必要があります。

VB-Audio公式ライセンス案内では、企業・組織などの業務利用ではライセンス購入が必要と説明されています。収益化配信や業務で使う場合は、用途が条件に該当するかを含め、公開・運用前に最新のライセンスを確認してください。

現在の実装では、通常はキャラクターYAMLで出力先を設定し、必要なときだけ環境変数で上書きします。

yaml
# characters/sister_a.yml
audio_device: "CABLE-A Input"

# characters/sister_b.yml
audio_device: "CABLE-B Input"

一時的に出力先を変える場合は、次の環境変数がYAMLより優先されます。

text
SISTER_A_AUDIO_DEVICE=CABLE-A Input
SISTER_B_AUDIO_DEVICE=CABLE-B Input

この構成では、PythonからCABLE-A Inputへ再生した姉の音声はCABLE-A Output側で受け取り、妹はB側で受け取ります。OBSでは、受け取り側のOutputをキャラクターごとに音声ソースとして追加します。

姉と妹の音声を別々の仮想オーディオケーブルへ送り、OBSで2つの独立した音声チャンネルとして受け取る説明図

図の見方:上段の青い音声と下段の赤い音声は、それぞれ別の仮想ケーブルを通ってOBSへ届きます。OBS側でも2つのチャンネルとして残るため、片方だけの音量調整、ミュート、ノイズ抑制などを行えます。途中で同じデバイスへ合流させると、この個別調整ができなくなります。

デバイス番号より名前を使う

音声デバイスは1721のようなインデックス番号でも指定できます。しかし、USB機器の抜き差しやデバイス構成の変更後に番号が変わることがあります。そのため、現在の実装では名前からデバイスを探しています。

python
exact = [device for device in devices if device.name.lower() == lower_hint]
if exact:
    return exact[0].index

contains = [device for device in devices if lower_hint in device.name.lower()]
if contains:
    return contains[0].index

完全一致を先に試し、見つからない場合だけ部分一致を使います。ただし、CABLEのような短すぎる名前を指定すると、AとBの両方に一致する可能性があります。エラーが出ないまま2人が同じデバイスへ出力されるため、設定値にはCABLE-A Inputのような識別可能な名前を使う方が安全です。

見つからない場合は、利用可能な出力デバイス一覧を表示して停止させています。無音のまま処理を続けるより、配信前に設定ミスへ気づきやすくなります。

語尾が切れる場合は末尾無音を試す

この環境では、合成したWAVをそのまま再生すると、発話の最後が短く聞こえる場合がありました。対策として、再生前に発話種別ごとの無音を末尾へ追加しています。

python
silence_frames = int(samplerate * tail_silence_ms / 1000)
silence = np.zeros(silence_frames, dtype=audio.dtype)
audio = np.concatenate([audio, silence], axis=0)
発話種別現在の末尾無音
通常発話140ミリ秒
短い相づち80ミリ秒
思考・相談・深掘り220ミリ秒

audio_router.py単体の既定値は300ミリ秒ですが、メインランタイムは発話ごとに上記の値を明示的に渡すため、通常配信へ300ミリ秒がそのまま使われるわけではありません。

これはすべての環境で必須という意味ではなく、現在の音声経路で有効だった実装上の対策です。語尾が切れるときは、TTSモデルだけでなく、再生処理、バッファ、サンプルレート、仮想オーディオ経路も切り分ける必要があります。

実際のTTS経路はStyle-Bert-VITS2

姉妹の音声は、共通のStyle-Bert-VITS2サーバーへPOST /voiceでリクエストし、キャラクターごとにモデル名、speaker ID、styleを変えています。長い台詞は既定85文字を目安に分割し、HTTP 422になったチャンクは短縮して1回再試行します。

python
response = requests.post(
    f"{SBV2_URL}/voice",
    params={
        "text": normalize_for_tts(text),
        "model_name": profile.model_name,
        "speaker_id": profile.speaker_id,
        "style": delivery.style,
        "auto_split": True,
    },
    timeout=90,
)

サーバーは共有でも、返ってきたWAVの再生先はprofile.audio_deviceでA/Bへ分けています。ここを分離しなければ、声モデルを2つ用意してもOBS上では同じ音声ソースへ合流します。

壁2:VTube Studioをキャラクターごとに分離する

VTube Studioの公開APIは、複数インスタンスを識別できる仕組みを持っています。VTube Studio Public APIの公式仕様では、起動したインスタンスごとに異なるinstanceIDが割り当てられ、Windowsではウィンドウ名も区別されると説明されています。

同じPCで2つのAPIサーバーへ接続するには、待ち受けポートを重複させないようにします。現在は次のように設定しています。

yaml
characters:
  sister_a:
    host: "127.0.0.1"
    port: 8001
    plugin_name: "AI VTuber Duo Live Sister A"
    token_file: "secrets/vts_sister_a_token.txt"

  sister_b:
    host: "127.0.0.1"
    port: 8002
    plugin_name: "AI VTuber Duo Live Sister B"
    token_file: "secrets/vts_sister_b_token.txt"

ポートだけでなく、プラグイン名とトークン保存先も分けています。plugin_developerは共通ですが、プラグイン名が異なるため姉妹の認証を区別できます。初回接続時は各VTube Studioで許可操作が必要で、片方が接続できなくても、もう片方はVTS skipとして単独で続行できる実装です。

1つのランタイムから異なる接続先と認証情報を使い、2つのVTube Studioインスタンスを別々に制御する説明図

図の見方:青と赤の接続は、ポートだけでなく認証トークンの保存先も独立しています。2本の接続先が同じVTube Studioを指すと同じモデルを操作してしまうため、接続先・プラグイン名・トークンを1組としてキャラクターごとに分けます。

APIで口や表情を動かす場合の注意

VTube Studio APIのInjectParameterDataRequestは、指定したパラメータへ値を送信できます。modeを省略するかsetにすると現在値を上書きし、addにすると現在値へ加算します。公式仕様では、制御を維持したいパラメータは少なくとも1秒に1回再送する必要があるとされています。送信が途切れると制御を失い、以前の入力元または既定値へ戻ります。

現在の設定はuse_parameters: trueuse_hotkeys: falseです。0.35秒で表情プリセットへ遷移し、0.2秒間隔でパラメータを再送します。発話終了後はneutralへの送信を約1秒続けてから止めます。この1秒にはneutralへ移る遷移時間も含まれます。

yaml
use_parameters: true
use_hotkeys: false
parameter_mode: "set"
send_interval_sec: 0.2
transition_sec: 0.35
after_speech_hold_sec: 1.0

接続時にはInputParameterListRequestでモデル側の入力パラメータを取得します。設定に書いたFaceAngleXMouthOpenなどが存在しなければ、その項目を送信対象から外し、VTS parameter not foundとしてログへ残します。保存されている動作ログでは、姉妹とも8001/8002へ接続し、それぞれ127個の入力パラメータを取得できていました。

VTube Studio公式仕様では、同じ入力パラメータをsetで上書き制御できるAPIプラグインは同時に1つです。一方、addは現在値への加算なので、複数のプラグインが同じパラメータへ同時に値を加えられます。この記事の設定はparameter_mode: "set"のため、1パラメータにつき1プラグインという競合制限の対象です。

なお、addへ変えても、2人のモデルや接続先が自動的に分離されるわけではありません。2人分の接続設定が同じVTube Studioインスタンスを指していると、別キャラクターを動かしているつもりでも、同じモデルのパラメータへ値を送ってしまいます。

現在の実装は発話開始と終了にモーションを連動させる

現在のシステムでは、字幕を更新した後、音声再生の直前にon_turn_start()を呼び、再生終了後にon_turn_end()を呼びます。

python
broadcast_caption(caption, profile, text, expression)
motion_controller.on_turn_start(
    speaker=profile.key,
    expression=expression,
    text=text,
)
play_character_voice(text, profile, wavs=wavs)
motion_controller.on_turn_end(
    speaker=profile.key,
    expression=expression,
)

これは音声の波形や音素へ完全同期するリップシンクではありません。現在のMouthOpenは表情プリセットに含まれる固定の目標値で、発話中に口を波形どおり開閉する処理はありません。発話開始・終了と表情を同期させる制御と考える方が正確です。

波形に合わせた口の開閉が必要なら、キャラクターごとに分けた仮想マイク入力をVTube Studioへ渡す方法か、音声振幅を解析して口パラメータを継続送信する処理が別途必要です。

VTS通信が切れた場合は自動再接続しない

保存ログでは姉妹とも複数回正常接続できている一方、VTS parameter inject failedのタイムアウトも少数記録されていました。現在のコードはパラメータ送信に失敗すると、そのクライアントを未接続状態にして送信スレッドを終了します。セッション中の自動再接続は未実装です。

表情が途中から動かなくなった場合は、ログのVTS parameter inject failedを確認し、VTube StudioのAPI設定、ポート、許可状態を確認してランタイムを再起動します。長時間配信をより安定させる次の改善候補は、回数制限と待機時間を持つ再接続処理です。

壁3:字幕パネルが1枚しかない

字幕はローカルのWebSocketサーバーから、OBSのブラウザソースへ次のようなデータを送ります。

python
caption.broadcast({
    "type": "speak",
    "speaker_label": profile.label,
    "text": text,
    "expression": expression,
    "char_interval_ms": CAPTION_CHAR_INTERVAL_MS,
})

speaker_labelを含めているため、順番に喋る構成なら1枚の字幕パネルでも話者を表示できます。HTML側は話者名で色を変え、既定55ミリ秒間隔で文字送りをします。また、サーバーは最後に送ったメッセージを1件だけ保持し、OBSが再接続したときに復元します。

裏返すと、同時に保持できる字幕状態も1件だけです。新しい発話が届くと進行中の文字送りを止めて表示を書き換えるため、2人が同時に喋ると後から届いた字幕が前の字幕と競合します。

同時発話へ対応するなら、字幕データに話者IDと発話IDを持たせ、姉用と妹用の表示領域を分ける必要があります。音声だけ重ねても、視聴者がどちらの台詞か追えなければ配信として見づらくなります。

なぜ「2人の同時発話」を見送ったのか

姉妹の会話では、一方が話している途中にもう一方が短く反応する「かぶせ」を入れたくなります。しかし、現在は本格的な同時発話を実装せず、ターンごとに順番に再生しています。

理由は、音声再生だけを非同期にしても完成しないからです。

sounddeviceのsd.playは重ねて再生できない

現在の音声ルーターは、sounddeviceの便利関数を使っています。

python
sd.play(audio, samplerate, device=device)
sd.wait()

python-sounddeviceの公式ドキュメントには、play()は複数の重なった再生には使えず、呼び出すと実行中のplay()などをstop()で停止すると書かれています。同時再生が必要なら、キャラクターごとにOutputStreamまたはRawOutputStreamを明示的に作る必要があります。

つまり、blocking=Falseへ変えるだけでは解決しません。姉の再生中に妹がsd.play()を呼ぶと、独立した2本のストリームとして重なるのではなく、既存の再生へ影響します。

TTSの並列生成と音声の同時再生は別

現在の実装は、待ち時間を短くするために複数ターンのTTS生成を先読みしています。

python
with ThreadPoolExecutor(max_workers=max_workers) as executor:
    tts_futures = [
        executor.submit(synthesize_character_voice, ...)
        for turn in prepared_turns
    ]

    for index, turn in enumerate(prepared_turns):
        wavs = tts_futures[index].result()
        play_character_voice(..., wavs=wavs)

正確には、Python側がTTSリクエストを複数ワーカーから先行送信します。Style-Bert-VITS2側で推論が本当に同時実行されるかは、サーバーの実行方式とGPUに依存するため、「GPU推論そのものが必ず並列になる」という意味ではありません。

再生はforループの中で順番に行われます。この方式なら、後続台詞の準備を早く始められますが、2人の音声が同時に鳴るわけではありません。

VTube Studio側はすでにキャラクター別クライアントになっている

先ほどの版では、モーション制御がspeaker/listenerモデルへ強く依存していると説明していました。しかし現行設定を確認すると、この説明は正確ではありませんでした。

DuoVTSMotionControllerは姉妹それぞれに独立したVTubeStudioClientとパラメータ送信スレッドを持っています。聞き手用ホットキーを呼ぶコード経路はありますが、現在はuse_hotkeys: falseなので動作していません。発話していない側へ、明示的なlistenerモーションを送る処理も現在はありません。

同時発話へ進む場合、VTS層を最初から作り直す必要はありません。既存のキャラクター別クライアントを再利用しながら、メイン側で2人のon_turn_start()on_turn_end()を独立して管理する変更が必要です。一方、自然な掛け合いを目指すなら、talk、listen、idle、reactionを重ねられる状態管理は別途追加した方がよいでしょう。

同時発話は1か所の変更では完成しない

変更する層同時発話に必要な変更
音声キャラクター別のOutputStreamと再生キュー
字幕話者別の2パネルと発話IDによる更新管理
VTSモーション既存のキャラクター別クライアントを再利用し、開始・終了を独立管理
進行制御speak_turns()の直列ループをキャラクター別タスクへ変更

大きな作り直しが必要なのは音声、字幕、進行制御です。VTS接続はすでに分離されていますが、自然な口の動きや聞き手反応まで含めるなら追加設計が必要です。そのため現在は、部分的に音だけ重ねるより、直列再生の安定性を優先しています。

現在採用している処理順

現時点の処理は、次の順番です。

  1. 配信コメントから姉妹の発話ターンを作る
  2. 既定2ワーカーで複数ターンのTTSリクエストを先行送信する
  3. 現在の話者名と台詞を字幕へ送る
  4. 話者側のVTube Studioへ発話開始を通知する
  5. 話者専用の仮想オーディオデバイスへ音声を再生する
  6. 再生終了後に発話終了を通知する
  7. 発話種別に応じた短い間を空けて次の話者へ進む

この構成は完全な同時会話ではありませんが、音声の衝突、字幕の上書き、モーション状態の競合を避けながら、2人の掛け合いを安定して配信できます。

1人目の字幕・モーション・音声が終わってから短い間を空け、2人目の発話へ進む直列再生の時間順を示した説明図

図の見方:青い話者の字幕、モーション、音声を開始し、再生が終わってから赤い話者へ進みます。音声波形が時間上で重ならないため、1枚の字幕パネルと現在の再生処理でも話者が競合しません。短い相づちも同じ順序に乗せ、前の話者が終わった直後へ配置します。

通常ブロックで条件が合う場合は、既定20%で片側だけの発話、既定22%で2人目の短い相づちを選ぶ仕組みがあります。相づちは音声を重ねず、1人目の終了後に0.08〜0.22秒の短い間を置いて再生します。これは「かぶせ」ではなく、後置相づちで会話の空白を減らす方法です。

また、conversation_metrics.jsonlへTTS生成時間、WAVの実音声長、コメント採用から可聴開始までの時間、発話開始・終了を記録するコードがあります。同時発話へ進む前に、この計測を使って直列構成の遅延と会話テンポを評価できます。

現行実装で残っている改善点

改善点現在の状態次の変更候補
VTS再接続送信失敗後は未接続のまま待機時間と上限回数を持つ再接続
口の動き表情プリセットの固定値WAV振幅または仮想マイク連動
聞き手モーションdirect parameterモードでは未実装talk/listen/idleの状態レイヤー
字幕識別speaker_labelのみspeaker_keyutterance_idを追加
音声同時再生sd.play()による直列再生キャラクター別OutputStreamとキュー
デバイス名解決完全一致後に部分一致部分一致が複数ならエラーにする
TTS失敗時字幕とVTS開始処理まで進む場合がある音声0件なら字幕・モーションを止めて再試行
1台PCの負荷TTS、OBS、VTSが資源を共有TTS時間とOBSのフレーム落ちを一緒に監視

私の実装で行う配信前の切り分け

2人構成は、すべてを起動してから原因を探すと切り分けが難しくなります。以下は私のローカル実装に含めている診断コマンドです。同じコード一式がなくても、音声デバイス、A/B経路、字幕、VTSの順で確認する考え方は流用できます。

音声を使う手順では、先にvoice/Style-Bert-VITS2/Server.batを起動し、音声合成サーバーがhttp://127.0.0.1:5000で応答できる状態にしておきます。

1. 音声デバイス名を確認する

powershell
.\voice\Style-Bert-VITS2\venv\Scripts\python.exe .\scripts\audio_router.py --list

ここでCABLE-A InputCABLE-B Inputが別々に表示されることを確認します。

2. 姉妹の音声経路だけを確認する

powershell
.\voice\Style-Bert-VITS2\venv\Scripts\python.exe .\scripts\duo_voice_route_test.py --list-devices
.\voice\Style-Bert-VITS2\venv\Scripts\python.exe .\scripts\duo_voice_route_test.py

姉のテスト音声ではOBSのA側だけ、妹のテスト音声ではB側だけメーターが動くことを確認します。両方が同時に動くなら、Pythonより後ろのWindowsまたはOBS側で経路が合流しています。

この経路テストはルーティング確認用で、メインランタイムの発話種別別TTS設定や末尾無音を完全に再現するテストではありません。

3. 固定台詞で字幕と音声を確認する

powershell
.\run_duo_live_chat.bat --demo --no-vts

LLMとVTube Studioを外した状態で、字幕とA/B音声を確認します。ここで問題が出るなら、原因は会話生成やVTSではありません。

4. VTube Studioだけを確認する

powershell
.\run_duo_live_chat.bat --vts-test

姉妹それぞれの表情プリセットを順番に送ります。初回は2つのVTube Studioで接続許可を行い、ログにVTS connectedVTS input parametersが両方出ることを確認します。

この順番なら、音声デバイス、音声経路、字幕、VTS接続を分けて確認できます。

これから2人構成を作る人への設計チェックリスト

  • 音声出力先をキャラクターごとに独立させる
  • デバイス番号ではなく識別可能な名前を設定する
  • VTube Studioのポートとトークン保存先を分ける
  • 初回は2つのVTube StudioでそれぞれAPI接続を許可する
  • InputParameterListRequest後の不足パラメータログを確認する
  • 字幕データへ話者IDを含める
  • TTS生成の並列化と音声再生の並列化を区別する
  • 同時発話を行うか、設計の最初に決める
  • 同時発話するなら音声・字幕・モーションを一緒に設計する
  • 配信前に姉だけ、妹だけ、交互発話の順で疎通確認する

まとめ

AI VTuberを2人に増やすときの本当の難しさは、LLMを2回呼ぶことではありません。音声デバイス、VTube Studio、字幕、モーションが持っている「現在の話者は1人」という前提を、どこまで残し、どこから分離するかを決めることです。

現在の構成では、音声経路とVTube Studio接続をキャラクターごとに分け、TTSを先読みしながら音声は順番に再生しています。VTube Studioクライアントはすでに2人分へ分離できていますが、同時発話には音声ストリーム、字幕、進行制御の変更が必要です。

これから複数キャラクターのAI VTuberを作る場合は、将来2人が同時に喋る可能性があるかを、音声再生の実装を始める前に決めておくことをおすすめします。

あわせて読みたい

参考にした公式資料

この記事は、実際の姉妹AI VTuber実装と動作ログ、各ソフトの公式仕様を基にしています。VTube Studio連携や音声デバイスの挙動は、使用バージョンとWindows側の構成によって変わる可能性があります。
シロラシ
シロラシ — この記事を書いた人

開発・運用保守・インフラ・セキュリティに携わる新米エンジニア。趣味や勉強で試したことを、実体験とともに記録しています。

プロフィールを見る →
COMMENTS

コメント

コメント欄に近づくと読み込みます。