Style-Bert-VITS2の読み上げをUnityで使う(補足あり)

Style-Bert-VITS2のAPIにHTTPリクエストを送信し、生成された読み上げ用サウンドファイルをUnityで再生します。

・Style-Bert-VITS2のダウンロード
・ローカルセットアップ
・モデルインポート

はすでにできているとします。
Style-Bert-VITS2のAPIでの読み上げは100文字が限界ですが、AIキャラクターとのやり取りの場合は100文字がちょうどいいくらいなので、プロンプトで「50文字以内、かつ最大2文で返答してください」と指定するといいかと思います。
こんな感じです。初回は少し時間がかかりますが、2回目以降はレスポンスがVOICEVOXくらいにはなったかなと思います。


補足情報 同一ネットワークの別のマシンでSBV2を動かす

同一ネットワークの別のマシンでSBV2を動かして、読み上げのみメインマシンでやることができましたので、その方法もメモしました。

本体を起動する

1.「Style-Bert-VITS2」フォルダの「App.bat」をダブルクリックして起動する
2.その状態でコマンドプロンプトなどを立ち上げ、「cd Style-Bert-VITS2フォルダのパス」を入力してStyle-Bert-VITS2フォルダまで移動する

3.「python server_fastapi.py」を入力してエンターキーを押す
4.モジュールが足りないというエラーが出た場合は下記をインストールする。インストール後に「python server_fastapi.py」を入力してエンターキーを押す

pip install gputil
pip install loguru
pip install pyannote.audio
pip install pyworld
pip install num2words

HTTPリクエストしてAPIからレスポンスをもらう準備をする

APIが動作するかテストする

1.「server_fastapi.py」が無事起動できたら、ブラウザを立ち上げて「http://127.0.0.1:5000/docs」と入力してエンターキーを押す。そうするとAPIの詳細が出てくる
2.「GET model/info」を探し、「Try it out」をクリックする

3.「Execute」をクリックすると、インポートしているモデルの詳細が出てくるので、「モデル名(以下だと"holocoma")」と「モデルID(以下だと"1")」をどこかに控えておく
※このあたりもUnityで実装して自動で設定できれば良いのですが、今回は割愛しました

4.「GET /voice」を探し、以下を入力する。あとはそのままでOK
・「セリフ」にしゃべらせるテキストを入力
・「モデルID」に3.でメモしたIDを入力
・「話者名」に3.でメモしたIDを入力

5.「Execute」をクリックすると音声ファイルが作成される

6.音声ファイルが生成されたら成功なので、Unityを立ち上げる

Unityで音声読み上げのコードを作る

7.UniTaskをインポートする

8.JSONを成形する「Newtonsoft Json」をインポートするため、PackageManagerから「Add package from git URL...」→「com.unity.nuget.newtonsoft-json」と入力する
9.下記の2つのコードをそれぞれ作成する
・読み上げた音声をオーディオクリップにするコード「SBV2WavUtility.cs」
・APIとやり取りするコード

・読み上げた音声をオーディオクリップにするコード「SBV2WavUtility.cs」

using System;
using UnityEngine;

public static class SBV2WavUtility
{
    public static AudioClip ToAudioClip(byte[] data)
    {
        int channels = BitConverter.ToInt16(data, 22);
        int sampleRate = BitConverter.ToInt32(data, 24);
        int pos = 44;
        int samples = (data.Length - pos) / 2;

        float[] audioData = new float[samples];
        int sampleIndex = 0;

        while (pos < data.Length)
        {
            short sample = (short)((data[pos + 1] << 8) | data[pos]);
            audioData[sampleIndex] = sample / 32768f;
            pos += 2;
            sampleIndex++;
        }

        AudioClip audioClip = AudioClip.Create("SynthesizedVoice", samples, channels, sampleRate, false);
        audioClip.SetData(audioData, 0);
        return audioClip;
    }
}

・APIとやり取りするコード
制作にあたり、ayutazさまのこちらのリポジトリを参考にさせて頂きました。
1.インスペクタに読み上げたいテキストを入力する
2.UIのボタンを作成
3.インスペクタにボタンを登録する
4.実行してボタンを押すと、インスペクタで設定したテキストを読み上げる


using System;
using UnityEngine;
using UnityEngine.UI; // UIを使うために必要
using System.Text;
using UnityEngine.Networking;
using Cysharp.Threading.Tasks;

public class SBV2SpeechStyle : MonoBehaviour
{
    public string baseUrl = "http://127.0.0.1:5000/";
    public TextToSpeechParameters parameters;

    [SerializeField]
    private AudioSource audioSource;

    [SerializeField]
    private string textToRead; // インスペクタで設定するテキスト

    [SerializeField]
    private Button readButton; // 読み上げを開始するためのボタン

    private void Awake()
    {
        if (audioSource == null)
        {
            audioSource = gameObject.AddComponent<AudioSource>();
        }

        // ボタンが設定されていれば、ボタンのクリックイベントに読み上げメソッドを登録
        if (readButton != null)
        {
            readButton.onClick.AddListener(() => ReadText(textToRead));
        }
    }

    public void ReadText(string text)
    {
        StartTextToSpeech(text).Forget();
    }

    private async UniTaskVoid StartTextToSpeech(string text)
    {
        await TextToSpeechAsync(text);
    }

    private async UniTask TextToSpeechAsync(string text)
    {
        var url = $"{baseUrl}voice?{ToQueryString(parameters, text)}";
        using var request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Accept", "audio/wav");
        await request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"Error: {request.error}");
        }
        else
        {
            var audioData = request.downloadHandler.data;
            var audioClip = SBV2WavUtility.ToAudioClip(audioData);
            PlayAudioClip(audioClip);
        }
    }

    private void PlayAudioClip(AudioClip clip)
    {
        if (audioSource == null)
        {
            audioSource = gameObject.AddComponent<AudioSource>();
        }
        audioSource.clip = clip;
        audioSource.Play();
    }

    private static string ToQueryString(TextToSpeechParameters parameters, string text)
    {
        StringBuilder query = new StringBuilder();
        query.Append($"text={UnityWebRequest.EscapeURL(text)}")
            .Append($"&encoding={UnityWebRequest.EscapeURL(parameters.Encoding)}")
            .Append($"&model_id={parameters.ModelId}")
            .Append($"&speaker_id={parameters.SpeakerId}");

        if (!string.IsNullOrEmpty(parameters.SpeakerName))
        {
            query.Append($"&speaker_name={UnityWebRequest.EscapeURL(parameters.SpeakerName)}");
        }

        query.Append($"&sdp_ratio={parameters.SdpRatio}")
            .Append($"&noise={parameters.Noise}")
            .Append($"&noisew={parameters.Noisew}")
            .Append($"&length={parameters.Length}")
            .Append($"&language={UnityWebRequest.EscapeURL(parameters.Language)}")
            .Append($"&auto_split={parameters.AutoSplit.ToString().ToLower()}")
            .Append($"&split_interval={parameters.SplitInterval}");

        if (!string.IsNullOrEmpty(parameters.AssistText))
        {
            query.Append($"&assist_text={UnityWebRequest.EscapeURL(parameters.AssistText)}");
        }

        query.Append($"&assist_text_weight={parameters.AssistTextWeight}")
            .Append($"&style={UnityWebRequest.EscapeURL(parameters.Style)}")
            .Append($"&style_weight={parameters.StyleWeight}");

        if (!string.IsNullOrEmpty(parameters.ReferenceAudioPath))
        {
            query.Append($"&reference_audio_path={UnityWebRequest.EscapeURL(parameters.ReferenceAudioPath)}");
        }

        return query.ToString();
    }

    [Serializable]
    public class TextToSpeechParameters
    {
        public string Encoding = "utf-8";
        public int ModelId = 0;
        public int SpeakerId = 0;
        public string SpeakerName;
        public float SdpRatio = 0.2f;
        public float Noise = 0.6f;
        public float Noisew = 0.8f;
        public float Length = 1.0f;
        public string Language = "JP";
        public bool AutoSplit = true;
        public float SplitInterval = 0.5f;
        public string AssistText = string.Empty;
        public float AssistTextWeight = 1.0f;
        public string Style = "Neutral";
        public float StyleWeight = 4.0f;
        public string ReferenceAudioPath;
    }
}

・おまけ 読み上げ音声の大きさに応じてオブジェクトが拡大/縮小する機能もつけたコード(上のAPIとやり取りするコードの代わりに使います)

using System;
using UnityEngine;
using System.Text;
using UnityEngine.Networking;
using Cysharp.Threading.Tasks;

public class SBV2SpeechMicVolScale : MonoBehaviour
{
    public string baseUrl = "http://127.0.0.1:5000/";
    public TextToSpeechParameters parameters;
    [SerializeField]
    private AudioSource audioSource;
    [SerializeField]
    private GameObject visualizerObject;
    [SerializeField]
    private float updateInterval = 0.1f;
    [SerializeField]
    private float minScale = 1f;
    [SerializeField]
    private float maxScale = 5f;

    private float timer = 0f;

    private void Awake()
    {
        if (audioSource == null)
        {
            audioSource = gameObject.AddComponent<AudioSource>();
        }
    }

    private void Update()
    {
        if (audioSource.isPlaying)
        {
            timer += Time.deltaTime;
            if (timer >= updateInterval)
            {
                float volume = GetAudioVolume();
                UpdateObjectScale(volume);
                timer = 0f;
            }
        }
    }

    public void OnButtonClick()
    {
        StartTextToSpeech().Forget();
    }

    private async UniTaskVoid StartTextToSpeech()
    {
        await TextToSpeechAsync();
    }

    private async UniTask TextToSpeechAsync()
    {
        var url = $"{baseUrl}voice?{ToQueryString(parameters)}";
        using var request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Accept", "audio/wav");
        await request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"Error: {request.error}");
        }
        else
        {
            var audioData = request.downloadHandler.data;
            var audioClip = SBV2WavUtility.ToAudioClip(audioData);
            PlayAudioClip(audioClip);
        }
    }

    private void PlayAudioClip(AudioClip clip)
    {
        if (audioSource == null)
        {
            audioSource = gameObject.AddComponent<AudioSource>();
        }
        audioSource.clip = clip;
        audioSource.Play();
    }

    private float GetAudioVolume()
    {
        float[] samples = new float[256];
        audioSource.GetOutputData(samples, 0);
        float sum = 0f;
        for (int i = 0; i < samples.Length; i++)
        {
            sum += Mathf.Abs(samples[i]);
        }
        return sum / samples.Length;
    }

    private void UpdateObjectScale(float volume)
    {
        if (visualizerObject != null)
        {
            float normalizedVolume = Mathf.Clamp01(volume);
            float scaleFactor = Mathf.Lerp(minScale, maxScale, normalizedVolume);
            visualizerObject.transform.localScale = Vector3.one * scaleFactor;
        }
    }

    private static string ToQueryString(TextToSpeechParameters parameters)
    {
        StringBuilder query = new StringBuilder();
        query.Append($"text={UnityWebRequest.EscapeURL(parameters.Text)}")
            .Append($"&encoding={UnityWebRequest.EscapeURL(parameters.Encoding)}")
            .Append($"&model_id={parameters.ModelId}")
            .Append($"&speaker_id={parameters.SpeakerId}");

        if (!string.IsNullOrEmpty(parameters.SpeakerName))
        {
            query.Append($"&speaker_name={UnityWebRequest.EscapeURL(parameters.SpeakerName)}");
        }

        query.Append($"&sdp_ratio={parameters.SdpRatio}")
            .Append($"&noise={parameters.Noise}")
            .Append($"&noisew={parameters.Noisew}")
            .Append($"&length={parameters.Length}")
            .Append($"&language={UnityWebRequest.EscapeURL(parameters.Language)}")
            .Append($"&auto_split={parameters.AutoSplit.ToString().ToLower()}")
            .Append($"&split_interval={parameters.SplitInterval}");

        if (!string.IsNullOrEmpty(parameters.AssistText))
        {
            query.Append($"&assist_text={UnityWebRequest.EscapeURL(parameters.AssistText)}");
        }

        query.Append($"&assist_text_weight={parameters.AssistTextWeight}")
            .Append($"&style={UnityWebRequest.EscapeURL(parameters.Style)}")
            .Append($"&style_weight={parameters.StyleWeight}");

        if (!string.IsNullOrEmpty(parameters.ReferenceAudioPath))
        {
            query.Append($"&reference_audio_path={UnityWebRequest.EscapeURL(parameters.ReferenceAudioPath)}");
        }

        return query.ToString();
    }

    [Serializable]
    public class TextToSpeechParameters
    {
        public string Text;
        public string Encoding = "utf-8";
        public int ModelId = 0;
        public int SpeakerId = 0;
        public string SpeakerName;
        public float SdpRatio = 0.2f;
        public float Noise = 0.6f;
        public float Noisew = 0.8f;
        public float Length = 1.0f;
        public string Language = "JP";
        public bool AutoSplit = true;
        public float SplitInterval = 0.5f;
        public string AssistText = string.Empty;
        public float AssistTextWeight = 1.0f;
        public string Style = "Neutral";
        public float StyleWeight = 5.0f;
        public string ReferenceAudioPath;
    }
}

10.「APIとやり取りするコード」または「おまけ」をGameObjectに適用する
11.AudioSourceコンポーネントを適当なオブジェクトに適用する
12.UIのボタンを作成する
13.ボタンの「On Click」に「APIとやり取りするコード」または「おまけ」を適用したオブジェクトをドラック&ドロップし、「OnButtonClick」を選択する

14.「APIとやり取りするコード」または「おまけ」を適用したGameObjectをクリックし、下記を入力する。後はひとまずそのままでOK
・「Text」に読み上げさせたいテキストを入力
・「Model Id」にメモしたモデル番号を入力
・「Speaker Name」にメモしたモデル名を入力
・「Audio Source」にAudio Sourceを適用したオブジェクトをドラッグ&ドロップする
・「Visualizer Object」に音のボリュームによって拡大縮小させたいオブジェクトをドラッグ&ドロップする

これで実行してボタンを押せば、Style-Bert-Vits2経由で読み上げが行われるハズです。初回は少し時間がかかるかもしれません。

同一ネットワークの別のマシンでSBV2を動かす

SBV2はかなりVRAMを使うようで、VRAM8GB程度ではQuest3+PCなどで動かすことができません。
そのため同一ネットワークの別のマシンでSBV2の生成のみ行い、読み上げをメインマシンで行います。
やり方は簡単で、インスペクタの「baseUrl」を別マシンのIPアドレスにするだけです。ただしUnity側でhttp://の接続がデフォルトではブロックされているので、その設定を変える必要があります。

1.別マシンで「server_fastapi.py」を起動するところまでやる
2.Unityの上部メニューから、「Edit」→「Project Settings」を選択する
3.「Player」→「Other Settings」→「Configuration」を探す
4.「Allow downloads over HTTP (非推奨)」があるので、「Always Allow」を選択する
5.インスペクタに別マシンで動かしているSBV2の設定を入力する。
「Model Id」「Speaker Name」は特に注意。メインマシンの設定ではなく、同一ネットワークの別マシンで動かしているSBV2の設定を入力する

いいなと思ったら応援しよう!