エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

.NET CoreからGCPのText-to-Speech APIでテキスト読み上げ音声を生成する

様々な企業がテキストから読み上げ音声を生成するAPIを公開していますが、その中でもGCPのText-to-Speech APIは日本語読み上げのクオリティが高かった為、実装をしてみました。

↓Text-to-Speech APIは過去記事でラズパイ(Python)でのテキスト読み上げの時にも使っています。今回は.NET Coreからの利用方法になります。

tm-progapp.hatenablog.com

Text-to-Speech APIとは

GCPが提供するテキスト読み上げAPIです。日本語テキストを付与してリクエストすると、そのテキストを読み上げた音声ファイルをレスポンスしてくれます。

ただテキスト読み上げするのではなく、文脈に合わせて抑揚を付けてくれたり、読み仮名もそれなりに解釈してくれるのでなかなか高品質です。 声の性別、高さ、速さを指定して、ある程度好みの声のトーンに近づけることも可能です。

日本語のVoice typeには"Basic", "WaveNet"があり、"WaveNet"は料金は少し高くなりますが、より人間の発声に近くなります。

↓のページから音声サンプルが聞けます。

cloud.google.com

AWSでも似たサービスで「Amazon Polly」がありますが、こちらは日本語を喋らせると若干カタコト感が否めません。

ただこちらはAWSの各サービスと連携しやすいので、品質的に許容できるなら採用しても良いかもしれません。

↓のページから音声サンプルが聞けます。

aws.amazon.com

やりたいこと

  1. .NET CoreからGCP Text-to-Speech APIへテキスト読み上げリクエストをする
  2. Text-to-Speech APIのレスポンスに含まれる読み上げ音声をローカルに保存する

実行環境

$ docker --version
Docker version 20.10.10, build b485636

$ docker-compose --version
Docker Compose version v2.1.1

Text-to-Speech APIの準備

GCPではまず使いたいAPIを有効化する必要があります。 「APIとサービス」から「APIとサービスの有効化」を押します。

f:id:tansantktk:20211201193625p:plain

APIの検索ボックスが表示されますので、Cloud Text-to-Speech APIを探します。

見つけられたら、「有効にする」を押して有効化します。

f:id:tansantktk:20211201193708p:plain

次に外部からAPIへリクエストする為のサービスアカウントをGCPで作成します。

「IAMと管理」-「サービスアカウント」から「サービスアカウントを作成」を押し、新しくサービスアカウントを作成します。アカウント名は識別しやすい名前にしておきます。

f:id:tansantktk:20211201193841p:plain

作成したサービスアカウントの詳細画面の「キー」の一覧から「鍵を追加」を押します。

f:id:tansantktk:20211201193856p:plain

作成するキーのタイプはJSONを選び作成すると、自動的にブラウザからjsonがダウンロードされます。このjsonAPIのリクエスト時に必要な認証情報になります。とりあえずこの時点ではファイル名をgcp-service-account.jsonに変更だけしておきます。

f:id:tansantktk:20211201193903p:plain

サービスアカウントを作成すると認証情報が記載されたjsonがダウンロードされますが、後で使うので一時保存しておきます。

.NET Core環境の構築

ここからは具体的なソースコード記述に入っていきます。

今回.NET Core環境はDockerで構築します。諸事情でDockerが使えない場合はローカルにインストールした.NET Core SDKから構築してもOKです。

Dockefile

FROM mcr.microsoft.com/dotnet/sdk:5.0
WORKDIR /project
EXPOSE 5000

docker-compose.yml

version: '3'
services:
  dotnet:
    build: .
    ports:
      - 5000:5000
    volumes:
      - .:/TextToSpeechTest
    tty: true

Dockerfile, docker-compose.ymlが作成できたら、コンテナを立ち上げてdotnet CLIからCUIアプリを新規作成します。

# Dockerコンテナの立ち上げ
$ docker-compose up -d
# コンテナに入る
$ docker-compose exec dotnet bash
# コンテナからCUIアプリを新規作成
root@*******:/project# dotnet new console
Getting ready...
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /project/project.csproj...
  Determining projects to restore...
  Restored /project/project.csproj (in 57 ms).
Restore succeeded.

dotnet new console実行後、Program.cs, TextToSpeechTest.csprojと付随する各種ファイルが自動生成されるはずです。

NuGetパッケージの追加

Text-to-Speech APIの利用の為にはNuGetパッケージGoogle.Cloud.TextToSpeech.V1が必要ですので、パッケージをインストールをファイルに定義し、インストールします。

パッケージの追加には色々な方法がありますが、ここではNuGetパッケージを定義するファイルであるcsprojに追記してrestoreする方法を書きます。

TextToSpeechTest.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    // ↓を追記
    <PackageReference Include="Google.Cloud.TextToSpeech.V1" Version="2.4.0"/>
  </ItemGroup>
</Project>
# csprojの変更を反映させる
root@*******:/project# dotnet restore

サービスアカウントの認証情報をロード

前のステップで一時保管していたgcp-service-account.jsonの場所を移動させ、プロジェクトのルートパスから見て./credentials/gcp/gcp-service-account.jsonに配置します。(このパスはあくまで当記事で指定しているだけですので、任意で変えてもOKですが、以下のソースコードcredentialPathも合わせて修正ください。)

Program.csと同階層にGCPCredentialManager.csを作成し、jsonのパスを環境変数で指定します。これでAPIへリクエストする際にjsonを利用して認証が行われるようになります。

GCPCredentialManager.cs

using System;
using System.IO;

public static class GCPCredentialManager
{
    private static readonly string credentialPath = Path.Combine(Environment.CurrentDirectory, @"credentials/gcp/gcp-service-account.json");

    /// <summary>GCPへアクセスするための認証情報設定をする。</summary>
    public static bool SetCredentials()
    {
        // GCPのNugetパッケージは環境変数"GOOGLE_APPLICATION_CREDENTIALS"から認証情報を参照しようとするので、ここでパスを指定する
        Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", credentialPath);
        return true;
    }
}

APIを呼ぶクラスを作成

TextSpeechAPIクラスにはメインロジックであるText-to-Speech APIを呼び出す処理を記述します。 処理が完了すると./outputoutput.mp3という音声ファイルが出力されるようになっています。

読み上げる声については細かくカスタマイズできますので、いろいろパラメータをいじってみると楽しいです😊

Google.Cloud.TextToSpeech.V1のリファレンスは下記からどうぞ。

cloud.google.com

TextSpeechAPI.cs

using Google.Cloud.TextToSpeech.V1;
using System;
using System.IO;

public static class TextToSpeechAPI
{
    private static readonly string localBasePath = Path.Combine(Environment.CurrentDirectory, @"output");

    /// <summary>テキストから音声データを取得する</summary>
    public static string GetSpeechSound(string text, string languageCode = "ja-JP", SsmlVoiceGender ssmlVoiceGender = SsmlVoiceGender.Female)
    {
        // 読み上げるテキストをSynthesisInputで定義。
        SynthesisInput input = new SynthesisInput { Text = text };
        // 声の各種設定
        VoiceSelectionParams voiceSelection = new VoiceSelectionParams
        {
            // 言語コード
            LanguageCode = languageCode,
            // 使用する声の種類
            Name = "ja-JP-Wavenet-A",
            // 声の性別
            SsmlGender = ssmlVoiceGender,
        };
        var audioConfig = new AudioConfig {
            // エンコーディング方法
            AudioEncoding = AudioEncoding.Mp3,
            // 音量の増加値
            VolumeGainDb = 5,
            // 声のピッチ
            Pitch = 1.0,
            // 読み上げ速度
            SpeakingRate = 1.0,
        };

        // サービスアカウントの認証情報をロードする
        GCPCredentialManager.SetCredentials();
        // Text-To-Speech APIから音声ファイル取得
        TextToSpeechClient client = TextToSpeechClient.Create();
        SynthesizeSpeechResponse response = client.SynthesizeSpeech(input, voiceSelection, audioConfig);
        return WriteLocalFile(Path.Combine(localBasePath, "output.mp3"), response);
    }

    private static string WriteLocalFile(string writePath, SynthesizeSpeechResponse response)
    {
        // mp3ファイルへ書き込み
        if (!Directory.Exists(localBasePath))
        {
            Directory.CreateDirectory(localBasePath);
        }

        using (FileStream stream = File.Create(writePath))
        {
            response.AudioContent.WriteTo(stream);
        }
        return writePath;
    }
}

Program.csからAPIを呼ぶ

Program.csには、アプリ実行時のトップレベルの処理を記述します。

ここでは実際に読み上げる文字列を定義し、TextToSpeechAPIクラスのメソッドを叩くだけです。

Program.cs

using System;

namespace TextToSpeechTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("now generating...");
            // 読み上げるテキスト
            string text = "あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモリーオ市、郊外のぎらぎらひかる草の波。";
            TextToSpeechAPI.GetSpeechSound(text);
            Console.WriteLine("output completed!");
        }
    }
}

デバッグする

準備ができましたのでdotnet runをコンソールに入力してアプリを実行してみましょう。

# Dockerコンテナの立ち上げ
$ docker-compose up -d
# コンテナに入る
$ docker-compose exec dotnet bash
# コンテナからCUIアプリを新規作成
root@*******:/project# dotnet run

うまく動作すれば./output/output.mp3が生成されているはずです。

あとは音声が気にくわなければ各種パラメータを変えて微調整するだけです。

ハマったところ

サービスアカウントの認証情報をロードするためには、環境変数jsonのパスをセットしないといけなかったのですが、この方法に辿り着くまで結構時間がかかりました。

今回は単純にローカルでデバッグするのでファイルをそのままプロジェクトのディレクトリに配置しましたが、AWS Lambda上で実行させたい場合はS3からjson取得処理を挟むといった工夫が必要になります。