rinnaのKoeiromap API(v1.0)を使ってUnityでテキスト読み上げをする
AIキャラクターの実装にあたって、Style-Bert-VITS2を使ってテキストの読み上げをしていたのですが、Quest3+PCで動かしたときに(おそらく)VRAMが足りずQuest3の画面に黒いブロックノイズが乗ってしまいました。
動作もかなり重く遅くなっていたので、別の読み上げ手段を探していたところ、rinnaのKoeiromap APIを使うことにしました。
サーバ上で処理をしているWhisperと同じケースですが、かなり高速でしゃべり方も自然なので、キャラクターイメージに合うことから使うことにしました。
準備
とりあえずUnity側のコードを共有します。登録の仕方はこちらを参照してください。
1.ディベロッパー登録する
上記のリンク先のコードはAPIの種類とバージョンが変わったことで使えなくなっているので、最新版のKoeiromap API(v1.0)を使ったコードをメモしておきます。
なおwavファイルで出力するようにしたので、サブスクへの加入が必要です。加入するとスタイルも選べるようです。
2.UniTaskをインポートする
こちらからUnityパッケージをダウンロード、インポートします。
Koeiromap API(v1.0)接続用のコード
こちらのみをGameObjectに適用します。また今回はボタンを押すと読み上げるようにしているので、ボタンの「On Click」を以下のようにします。
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using Cysharp.Threading.Tasks;
public class RinnaKoeiromapConnect : MonoBehaviour
{
[Header("API Settings")]
public string apiKey;
public string textToRead;
[Header("Voice Settings")]
public float speakerX = 0.2f;
public float speakerY = -3.1f;
public string style = "happy";
public double seed = 8E+18;
public float speed = 1.2f;
public float volume = 3.8f;
public string outputBitrate = "64k";
[Header("Audio Source")]
public AudioSource audioSource;
private string apiUrl = "https://api.rinna.co.jp/koeiromap/v1.0/infer";
void Start()
{
if (string.IsNullOrWhiteSpace(textToRead))
{
Debug.LogError("Text to read is required.");
return;
}
}
public void StartTextToSpeech()
{
SendRequest().Forget();
}
private async UniTaskVoid SendRequest()
{
RequestData requestData = new RequestData
{
text = textToRead,
speaker_x = speakerX,
speaker_y = speakerY,
style = style,
seed = seed,
speed = speed,
volume = volume,
output_format = "wav",
output_bitrate = outputBitrate
};
string jsonData = JsonUtility.ToJson(requestData);
Debug.Log("Sending JSON data: " + jsonData);
byte[] postData = Encoding.UTF8.GetBytes(jsonData);
using (UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"))
{
request.uploadHandler = new UploadHandlerRaw(postData);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Ocp-Apim-Subscription-Key", apiKey);
await request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
Debug.Log("Response Text: " + responseText);
var responseData = JsonUtility.FromJson<ResponseData>(responseText);
Debug.Log("Audio data: " + responseData.audio);
if (string.IsNullOrEmpty(responseData.audio))
{
Debug.LogError("Audio data is empty or null.");
return;
}
try
{
string audioBase64 = responseData.audio.Trim();
// プレフィックスを削除
string base64Data = audioBase64.Substring(audioBase64.IndexOf(",") + 1);
byte[] audioData = Convert.FromBase64String(base64Data);
PlayAudio(audioData);
}
catch (FormatException e)
{
Debug.LogError("Failed to decode Base64 audio data: " + e.Message);
return;
}
}
else
{
Debug.LogError($"Error: {request.error}");
Debug.LogError($"Response: {request.downloadHandler.text}");
return;
}
}
}
private void PlayAudio(byte[] audioData)
{
var audioClip = rinnaWavAudioUtility.ToAudioClip(audioData, "Generated Audio");
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
}
audioSource.clip = audioClip;
audioSource.Play();
}
[Serializable]
private class ResponseData
{
public string audio;
}
[Serializable]
private class RequestData
{
public string text;
public float speaker_x;
public float speaker_y;
public string style;
public double seed;
public float speed;
public float volume;
public string output_format;
public string output_bitrate;
}
}
AudioClipに変換するコード
こちらはAssetsフォルダ→Scriptsフォルダなどに入れておくだけでOKです。オブジェクトへの適用は必要ありません。
using System;
using UnityEngine;
public static class rinnaWavAudioUtility
{
public static AudioClip ToAudioClip(byte[] data, string clipName)
{
try
{
WAV wav = new WAV(data);
AudioClip audioClip = AudioClip.Create(clipName, wav.SampleCount, 1, wav.Frequency, false);
audioClip.SetData(wav.LeftChannel, 0);
return audioClip;
}
catch (Exception e)
{
Debug.LogError("Failed to create audio clip: " + e.Message);
return null;
}
}
private class WAV
{
public float[] LeftChannel { get; private set; }
public int SampleCount { get; private set; }
public int Frequency { get; private set; }
public WAV(byte[] data)
{
if (data.Length < 44) throw new Exception("Invalid WAV data, too short.");
int channels = BitConverter.ToInt16(data, 22);
Frequency = BitConverter.ToInt32(data, 24);
int bitDepth = BitConverter.ToInt16(data, 34);
int pos = 12;
while (pos + 4 < data.Length && !(data[pos] == 100 && data[pos + 1] == 97 && data[pos + 2] == 116 && data[pos + 3] == 97))
{
pos += 4;
}
if (pos + 4 >= data.Length) throw new Exception("Invalid WAV data, 'data' chunk not found.");
pos += 8; // Skip 'data' identifier and size
if (pos >= data.Length) throw new Exception("Invalid WAV data, no audio data present.");
SampleCount = (data.Length - pos) / (bitDepth / 8);
LeftChannel = new float[SampleCount];
int i = 0;
while (pos < data.Length && i < SampleCount)
{
LeftChannel[i] = BitConverter.ToInt16(data, pos) / 32768.0f; // Normalize the 16-bit sample
pos += 2;
i++;
}
}
}
}