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の設定を入力する