VTube StudioのキャラをPythonから口パクさせる

最近、サンプルを作ったのでメモ的に残しておきます。

音声ファイルにあわせて口パクさせていますが、音声を分析してリップシンクをしているわけではないので、適当に口パクさせてそれっぽくしています。

あまりWebで情報が見つからなかったので参考までに。(ステレオミキサー使ったものなら結構ありましたが、使いたくなかったので・・・)

公式のドキュメントはこちら

import asyncio
import json
import websockets
import random
import pygame
import os

AUDIO_FOLDER = "audio/Word2Motion"
TOKEN_FILE = "token.txt"
PARAMETER_ID = "MouthOpen"
VTS_URI = "ws://localhost:8001"

async def send_mouth_open(ws, value, request_id):
    await ws.send(json.dumps({
        "apiName": "VTubeStudioPublicAPI",
        "apiVersion": "1.0",
        "requestID": request_id,
        "messageType": "InjectParameterDataRequest",
        "data": {
            "faceFound": False,
            "mode": "set",
            "parameterValues": [
                {
                    "id": PARAMETER_ID,
                    "value": value
                }
            ]
        }
    }))

async def get_or_create_token(ws):
    if os.path.exists(TOKEN_FILE):
        with open(TOKEN_FILE, "r", encoding="utf-8") as f:
            return f.read().strip()

    await ws.send(json.dumps({
        "apiName": "VTubeStudioPublicAPI",
        "apiVersion": "1.0",
        "messageType": "AuthenticationTokenRequest",
        "requestID": "get-token",
        "data": {
            "pluginName": "InjectTester",
            "pluginDeveloper": "You"
        }
    }))
    token_response = json.loads(await ws.recv())
    token = token_response["data"]["authenticationToken"]

    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        f.write(token)

    return token

async def authenticate(ws):
    token = await get_or_create_token(ws)

    await ws.send(json.dumps({
        "apiName": "VTubeStudioPublicAPI",
        "apiVersion": "1.0",
        "messageType": "AuthenticationRequest",
        "requestID": "auth",
        "data": {
            "pluginName": "InjectTester",
            "pluginDeveloper": "You",
            "authenticationToken": token
        }
    }))

    resp = await ws.recv()
    print("✅ 認証成功:", resp)

async def keep_alive(ws):
    while True:
        try:
            await ws.ping()
            await asyncio.sleep(5)  # pingを5秒おきに送信して切断防止
        except Exception as e:
            print(f"⚠️ KeepAlive失敗: {e}")
            break

async def play_with_mouth_sync(file_path, ws):
    print(f"🎵 再生開始: {file_path}")
    pygame.mixer.music.load(file_path)
    pygame.mixer.music.play()

    while pygame.mixer.music.get_busy():
        open_val = round(random.uniform(0.2, 1.0), 2)
        await send_mouth_open(ws, open_val, "inject-open")
        await asyncio.sleep(random.uniform(0.05, 0.15))
        await send_mouth_open(ws, 0.0, "inject-close")
        await asyncio.sleep(random.uniform(0.05, 0.15))

    print(f"🛑 再生終了: {file_path}")

async def run_all_audio():
    pygame.mixer.init()

    files = sorted([
        os.path.join(AUDIO_FOLDER, f)
        for f in os.listdir(AUDIO_FOLDER)
        if f.lower().endswith(".wav")
    ])

    async with websockets.connect(VTS_URI, ping_interval=None) as ws:
        await authenticate(ws)

        # KeepAliveをバックグラウンドで走らせる
        keepalive_task = asyncio.create_task(keep_alive(ws))

        for file_path in files:
            await play_with_mouth_sync(file_path, ws)
            await asyncio.sleep(0.3)

        keepalive_task.cancel()

asyncio.run(run_all_audio())

最低限このあたりの設定はVtube Studio側で必要です。

スムージングを大きくすることで、口パクの動きがスムーズになります。

目次

機能概要

  • 指定フォルダ内の音声(WAV)を順番に再生
  • 再生中に口の開閉をランダムな値で送信
  • VTube StudioとのWebSocket認証とトークンの永続化
  • WebSocketが切断されないように定期的にping送信

ファイル構成

  • audio/Word2Motion/:音声ファイルフォルダ(WAV形式)
  • token.txt:VTS APIの認証トークン保存用ファイル

主な処理の流れ

1. VTSとの接続と認証

async with websockets.connect(VTS_URI, ping_interval=None) as ws:
    await authenticate(ws)

認証トークンがなければ生成・保存し、次回以降は再利用することで毎回の「許可」操作を不要化しています。

音声再生と口パク制御

while pygame.mixer.music.get_busy():
    open_val = round(random.uniform(0.2, 1.0), 2)
    await send_mouth_open(ws, open_val, "inject-open")
    await asyncio.sleep(...)

pygame.mixer で音声を再生

再生中に MouthOpen パラメータへランダム値(0.2〜1.0)を送信

開→閉を繰り返して自然な口パクを表現

接続維持の工夫(KeepAlive)

async def keep_alive(ws):
    await ws.ping()

WebSocketの自動切断を防ぐために、バックグラウンドで5秒ごとにpingを送信しています。

まとめ

このスクリプトを使えば、VTube Studioと連携して任意の音声再生中に自動で口パクを再現できます。簡易的なライブ演出や音声ボットのアバター化などに活用できます。

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