オリジナルのスマートスピーカーを作ってみる 3 音声ファイル合成

2024/03/12に公開

この記事は?

この記事は オリジナルのスマートスピーカーを作ってみる シリーズの続き「その3」です。

音声合成 API をコールするプログラムを作成する

VOICEVOX ENGINE の API をコールするプログラムを作ります。
書き慣れた C# で作ろうと思います。

開発環境

開発マシン OS Windows 11 (x64)
アプリケーションプラットフォーム .NET 8
言語 C# 12
IDE Visual Studio Community 2022

アプリケーションサーバー

製品名 Raspberry Pi 5
OS Raspberry Pi OS (ベースは Debian GNU/Linux 12 (bookworm))
CPU アーキテクチャ aarch64/arm64
メモリ 8GB

アプリケーションのプロジェクトはコンソールアプリを選択しました。

実行ファイルの発行方法

ラズパイ向けの実行ファイルの発行方法は、以下ドキュメントを参考にしました。
https://learn.microsoft.com/ja-jp/dotnet/core/deploying/deploy-with-cli#framework-dependent-deployment

今回、実行ファイルの形式は SCD(自己完結型。実行環境に.net のインストールは必要ない)を取りました。
発行するときのコマンドは以下のようになりました。

powershell
#ラズパイ向けの実行ファイルを発行する
dotnet publish -c Release -r linux-arm64 --self-contained true

hello world プログラムをラズパイでテスト

実行ファイルをラズパイにコピペして、hello world プログラムを実行しました。
実行方法は以下ドキュメントを参考にし、無事動作することを確認しました。

https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2018/november/net-core-publishing-options-with-net-core

実行のコマンドは以下のようになりました。

bash on raspi
$ chmod +x <実行ファイル名>
$ ./<実行ファイル名>
Hello, World!

VOICEVOX ENGINE の ラズパイ(arm64/aarch64)向けリリースビルドが無いことが判明

VOICEVOX ENGINE をラズパイで動かそうと思ったら、 arm64/aarch64 向けリリースビルドが無いことが判明しました。 自分でビルドするのも大変そうなので、改めてほかの読み上げツールを検討しましたが、無料でここまでの品質は他に無さそうです。
とりあえず開発用PC(Windows 11 NVIDIA GPU 搭載)を VOICEVOX ENGINE サーバーとすることにしました。今後どうしてもラズパイ上でやりたくなったら、VOICEVOX CORE で自分で API を作ろうと思います。
(そもそもラズパイ上で voicevox 動かすのはキツイ説もある)

追記
Mac OS 向けがありましたので、それを選べばOKです。

ラズパイ to VOICEVOX ENGINE サーバーの開通を行う

VOICEVOX ENGINE はデフォルトではローカルホストからのアクセスしか受け付けないセキュリティ設定になっているので、それを変更する

  • VOICEVOX ENGINE を起動後、 http://127.0.0.1:50021/setting にアクセスし、ラズパイからアクセスできるように設定の変更を行う。
  • VOICEVOX ENGINE を再起動し、コマンドライン引数(--host <hostname or ip> --port <port>)で任意の IP とポートで公開する。

ラズパイから nmap コマンドを使って開通 (STATE: open) を確認できました。

bash on raspi
$ nmap -P0 -p <設定したポート番号> <設定した IP>
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-12 15:34 JST
Nmap scan report for <サーバー名> (<設定した IP>)
Host is up (0.0093s latency).

PORT      STATE SERVICE
<設定したポート番号>/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 0.06 seconds

ラズパイから音声合成 API をコールするプログラム

音声ファイルを合成するプログラムを作成しました。
成果物としては .wav のオーディオファイルとなります。
以下はそのプログラムです。

Program.cs
using NLog;

internal class Program
{
    private static readonly Logger logger = LogManager.GetCurrentClassLogger();
    static async Task Main(string[] args)
    {
        try
        {
            logger.Info("Application starts.");
            var voiceText = "これはテストです。";
            await VoiceFile.Synthesize_and_SaveFile(voiceText);
            logger.Info("Application closed.");
        }
        catch (Exception ex)
        {
            logger.Error(ex);
        }
    }
}
VoiceFile.cs
internal class VoiceFile
{
    public async static Task Synthesize_and_SaveFile(string voiceText)
    {
        var audioQuery = await AudioQueryAPICaller.Call(voiceText);
        var byteArray = await SynthesisAPICaller.Call(audioQuery);

        File.WriteAllBytes("./this_is_test.wav", byteArray);
    }
}
AudioQueryAPICaller.cs
internal class AudioQueryAPICaller : APICallerBase
{
    private const string path = "audio_query";
    public static async Task<string> Call(string voiceText)
    {
        var uRL = MakeURL(voiceText);
        using var jsonContent = NewJsonContent("");
        using var response = await client.PostAsync(uRL, jsonContent);
        response.EnsureSuccessStatusCode();
        var audioQuery = await response.Content.ReadAsStringAsync();

        logger.Info(audioQuery);
        return audioQuery;
    }
    private static string MakeURL(string voiceText)
    {
        var uRL = $"{voicevoxBaseURL}/{path}?text={voiceText}&speaker=0";
        return uRL;
    }
}
SynthesisAPICaller.cs
internal class SynthesisAPICaller : APICallerBase
{
    private const string path = "synthesis";
    public static async Task<byte[]> Call(string audioQuery)
    {
        var uRL = MakeURL();
        using var jsonContent = NewJsonContent(audioQuery);
        using var response = await client.PostAsync(uRL, jsonContent);
        response.EnsureSuccessStatusCode();
        var responseContent = await response.Content.ReadAsByteArrayAsync();
        return responseContent;
    }
    private static string MakeURL()
    {
        var uRL = $"{voicevoxBaseURL}/{path}?speaker=0";
        return uRL;
    }
}
APICallerBase.cs
using NLog;
using System.Text;

internal abstract class APICallerBase
{
    protected static readonly HttpClient client = new HttpClient();
    protected static readonly Logger logger = LogManager.GetCurrentClassLogger();

    private static readonly AppSettingsModel appSettings = AppSettingsFetcher.Fetch();
    protected static string voicevoxBaseURL = $"http://{appSettings.voicevox_engine_ip}:{appSettings.voicevox_engine_port}";
    protected static StringContent NewJsonContent(string jsonText) => new(jsonText, Encoding.UTF8, "application/json");
}

以下の記事に続く
オリジナルのスマートスピーカーを作ってみる 4 音声ファイル再生 & コンテナ構築

Discussion