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++;
            }
        }
    }
}

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