GAE/Go で Google Cloud Text-to-Speech API を試してみるの巻

こんにちは！３月末に初めて目黒川の桜:cherry_blossom:を見に行って来た @wezardnet です。地上は人混みでゆっくり花見はできませんが、舟から見上げる桜は十分に楽しめるのでオススメです♪

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">目黒川のお花見🌸は人混みでカオスでした。もう葉桜だったけど綺麗♪ <a href="https://t.co/uiYdsMlOcH">pic.twitter.com/uiYdsMlOcH</a></p>&mdash; wezardnet (@wezardnet) <a href="https://twitter.com/wezardnet/status/980078317935116288?ref_src=twsrc%5Etfw">2018年3月31日</a></blockquote>

ちょうど同じ頃に [Google Cloud Text-to-Speech API](https://cloud.google.com/text-to-speech/) がベータで一般公開されたので、興味本位で試してみました。バックエンドは GAE/Go で Text-to-Speech API をラップするサービスを作り、フロントエンドは Bootstrap4 を使って UI を手抜きし、次のようなデモページで試してみました。公式ページにはもっとイケてるデモが用意されていますが、実装方法を学ぶためなので超シンプルなのですｗｗ

![text2speech.png](https://qiita-image-store.s3.amazonaws.com/0/12023/a8855d22-1d47-bb8b-0a37-2a0c08b47201.png)


# 1. Cloud Text-to-Speech API
Cloud Text-to-Speech API はテキスト → 音声に変換します。逆に音声 → テキストに変換する場合は [Cloud Speech API](https://cloud.google.com/speech/) になります。詳細は以下のネット記事を読むと良いでしょう:sweat_smile:

* [Google Cloud Platform Japan Blog - Cloud TTS (Text-to-Speech) 一般提供開始](https://cloudplatform-jp.googleblog.com/2018/03/introducing-Cloud-Text-to-Speech-powered-by-Deepmind-WaveNet-technology.html)
* [Google、テキスト読み上げサービス「Cloud Text-to-Speech」を一般公開](https://cloud.watch.impress.co.jp/docs/news/1114243.html)
* [Google Cloudがテキスト読み上げサービス「Cloud Text-to-Speech」](https://japan.zdnet.com/article/35116787/)


# 2. 事前準備
お決まりの [Google Developer Console](https://console.cloud.google.com/) の API Manager で Cloud Text-to-Speech API を有効にします。

![image01.png](https://qiita-image-store.s3.amazonaws.com/0/12023/96c4d22c-d5ae-216f-f753-314b1313293d.png)


# 3. バックエンド
フロントエンド側で入力されたテキストを受け取り Cloud Text-to-Speech API に投げて、返ってきた結果をそのまま返すだけのサービスを作ります。

まず必要なパッケージをインポートします。尚、コードは見づらくなるので一部省略します。

```go
import (
    "encoding/base64"
    "encoding/json"
    "io/ioutil"
    "net/http"

    "bitbucket.org/{my project}/lib/helper"

    "github.com/bitly/go-simplejson"

    "golang.org/x/oauth2/google"

    "google.golang.org/appengine"
    "google.golang.org/appengine/log"
    "google.golang.org/appengine/urlfetch"
)
```

次にリクエストボディの JSON を組み立てます。ココでは変換に必要な最低限のパラメータに絞っていますが、ボリュームとかピッチなど細かい設定もできるようです。

```go
type m map[string]interface{}

values := m {
	"input": m {
		"text": "こんにちは", 
	}, 
	"voice": m {
		"languageCode": "ja-JP", 
		"name": "ja-JP-Standard-A", 
		"ssmlGender": "FEMALE", 
	}, 
	"audioConfig": m {
		"audioEncoding": "LINEAR16", 
		"speakingRate": 1, 
	}, 
}
payload, _ := json.MarshalIndent(values, "", "    ")
```

パラメータは次の意味を持ちます。後述しますが、 __音声に変換するテキストは 5,000 文字までという制限があります__。

|パラメータ|説明|
|:-------------|:------------|
|text|音声に変換するテキスト|
|languageCode|音声の言語を [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) で定義される言語タグで指定する|
|name|音声の名前[^1]（省略可）|
|ssmlGender|音声の性別を指定する（男性：「MALE」、女性：「FEMALE」、中性：「NEUTRAL」）|
|audioEncoding|音声のエンコーダを指定する（「AUDIO_ENCODING_UNSPECIFIED」、「LINEAR16」、「MP3」、「OGG_OPUS」のいずれかを選択）|
|speakingRate|音声のスピードを指定する（通常は「1」、２倍なら「2」という感じで指定します）|

組み立てたリクエストボディを API にセットしてリクエストを出します。Text-to-Speech API を使うための OAuth スコープは `https://www.googleapis.com/auth/cloud-platform` になります。認証／認可まわりはめんどいので App Engine サービスアカウントを使います (*ﾉω・*)ﾃﾍ


```go
ctx := appengine.NewContext(r)

client := &http.Client{
	Transport: &oauth2.Transport{
		Source: google.AppEngineTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform"), 
		Base: &urlfetch.Transport{Context: ctx}, 
	},
	Timeout: 30 * time.Second, 
}

req, err := http.NewRequest(
	"POST", 
	"https://texttospeech.googleapis.com/v1beta1/text:synthesize", 
	bytes.NewReader(payload), 
)
if err != nil {
	log.Errorf(ctx, "err = %s", err.Error())
	return
}
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
	log.Errorf(ctx, "err = %s", err.Error())
	return
}

body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
	log.Errorf(ctx, fmt.Sprintf("Text-to-Speech api request failure. %d: %s", resp.StatusCode, string(body)))
	return
}

data, err := simplejson.NewJson(body)
if err != nil {
	log.Errorf(ctx, "err = %s", err.Error())
	return
}
results, _ := json.MarshalIndent(data, "", "    ")
log.Infof(ctx, "results = %s", results)
```

上記のようにして Cloud Text-to-Speech API をコールして返ってきたレスポンスをそのまま返すだけのラップした Web サービスを作ってバックエンド側はおしまいです。

ちなみに「 **こんにちは** 」を入力テキストとして音声に変換した場合のレスポンスは次のように base64 形式でエンコードされた JSON になります。めっちゃ長いので一部省略します。

```json
{
	"audioContent": "UklGRpSoAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YXCoAAACAAAA/f8AAP////8AAP//AAD+/wAAAQAAAAAAAQD//wAAAAD//wIAAAABAP7///8BAP///v8BAP7/AAD//wAAAwACAAAAAwAAAAMAAAD//wIAAQD//wMAAAACAAAAAAADAAIAAgABAAAAAwAAAAIAAAAAAP//AQACAAAA/////wAA/f8BAP7/AAD9/wEA///9/wAA//8AAAIA//8AAAIAAAAAAAAA//8AAAAAAwABAAMAAQD//wAAAgABAAAAAQAAAAEA//8AAAEAAAAAAAAAAAAAAAMAAQD//wIAAgAAAP3/AAD/////AAD//wAA/v8AAAEAAAAAAAEAAAAAAAAA//8CAAAAAAD+////AAD///7/AQD+/wAAAAAAAAEAAAAAAAAAAAABAAAAAAAAAAAA//8CAAAAAgD//wAAAgAAAAAAAAAAAAAA///+/wEAAAAAAAAAAAACAAAAAgD//wAAAAAAAAAAAQAAAAEAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAAAAAAD//wAAAAAAAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD//wAA//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD//wAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAAAAAAD//wAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAD//wAAAAAAAP//AAAAAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAABAP//AQAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAP//AAAAAAAAAQAAAAAAAAABAAAAAAD//wAA//////////8AAP//AQAAAAEAAAABAAAAAAABAAAAAAABAP//AAAAAAAAAAD//wAA/v8AAP////8AAAAA//////7/AAD/////////////AAAAAAAA/v8AAAAAAQAAAAEAAQACAAAAAgAAAAIAAAAAAAAAAgD//wAAAAACAP//AgABAAAAAwABAAEAAQABAAEAAAD//wAA/v////7//v/8//v////7/////f///////v/+/wAAAAAAAP7//////wAAAAAAAAAAAgAAAAEAAQADAAAAAQAAAAEAAAD9/wMAAAABAP//AgACAAEAAQABAAEAAQAAAAAA/v8AAP7//f/7/////v8AAAAA/v8CAAAAAgD+/wAA//////3//f///wAA/v8FAAEABAADAAAABQAAAP///v8AAP//+v8FAPz/AAAAAAAAAAD//wAA/P8CAAAAAAD+/wMAAgAAAAIA/v8DAP//AAD9//3/+//7/wAAAAABAAkABgAIAAgACgAQAAMAAwD9/wgAAAD8/wAABwAFAAQACAAKAA4ABwAGAAAABQD3//7/9f/1/+j/8P/x//7/+P/5//f/CAABAAQACgAJAAUAAwAEAAgABQAAAPz////3//z///8AAP3///8FAAYACwAMAAQAFAD4/woA/v8DAPb/

（省略）

"
}
```


# 4. フロントエンド
Bootstrap4 を使って適当に UI まわりを作り、テキスト → 音声に変換されたデータをダウンロードさせます。ココでは JavaScript 部分のみ紹介します。（例外処理は割愛 :stuck_out_tongue_closed_eyes:）

```javascript
$(document).ready(function() {
    $('#btn_exec').click(function (e) {
        fetch('/api/v1/text2Speech?text=' + encodeURIComponent($('#wave_text').val()), {
            method: 'GET',
            mode: 'cors',
            credentials: 'include'
        })
        .then(response => {
            if ( !response.ok )
                throw Error(response.statusText);

            return response.json();
        })
        .then(json => {
            var bin = atob(json.audioContent.replace(/^.*,/, ''));
            var buffer = new Uint8Array(bin.length);
            for ( var i = 0; i < bin.length; i++ ) {
                buffer[i] = bin.charCodeAt(i);
            }

            var blobURL = window.URL.createObjectURL(new Blob([buffer.buffer], {type: "audio/wav"}));
            var a = document.createElement('a');
            a.download = 'file.wav';
            a.href = blobURL;
            a.click();
        })
        .catch(err => {
            console.log(err);
        });
    });
});
```

「実行」ボタン押下で前述のラップした自前 Web サービスに入力テキストを渡します。返ってきた JSON の audioContent が base64 でエンコードされた音声データなので、動的にダウンロードさせるために Blob オブジェクトを作ります。あとは download 属性を用いてファイルを開かずにダウンロードさせるようにしてます。

また、以下のように audio タグを使えば音声ファイルを再生させるようにもできます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12023/8be0eb56-7b3d-3d1d-1939-a4770932609c.png)

コードはベタですが、こんな感じ:sweat_smile:

```html:html
<div id="audio_controller"></div>
```

```javascript:js
.then(json => {
    var html = '<audio controls="controls">';
    html += '<source src="data:audio/wav;base64,' + json.audioContent + '" type="audio/wav" /> Your browser does not support the audio element.';
    html += '</audio>';
    $('#audio_controller').html(html);
})
```


# 5. GCS へ出力する場合

バックエンド側で　Text-to-Speech API をコールして、結果は Google Cloud Storage(GCS) に出力する場合は、こんな感じでバケットに出力すれば良いと思います。尚 GCS まわりの処理は独自のヘルパーパッケージにまとめてます。

```go
wavdata, _ := base64.StdEncoding.DecodeString(data.Get("audioContent").MustString())

storage, err := helper.NewStorage(ctx, req)
if err != nil {
}

_, err = storage.MultipartUpload("{my bucket}", wavdata, "file.wav")
if err != nil {
}
```


# 6. 料金

|機能|月無料枠|有料|
|:-------------|:------------|:------------|
|標準|0〜400 万文字|$4.00 / 100 万文字|
|WaveNet|0〜100 万文字|$16.00 / 100 万文字|

WaveNet ってなにかと言うと知りませんｗｗ
ちょっとググれば出てくると思うけど [公式ドキュメント](https://cloud.google.com/text-to-speech/docs/wavenet) を和訳すると、、、

>WaveNet は、他のテキスト読み上げシステムよりも自然な音を生成します。それは、人間のような強調や音節、音素、言葉の変奏音で音声を合成します。平均して WaveNet は、人々が他のテキスト読み上げ技術よりも好むスピーチオーディオを生成します。

とあるので、標準よりは人間の声に近い感じで音声合成されるということでしょう。（たぶん...）
ちなみに日本語による WaveNet 音声合成は、現在のところサポート（[Supported voices](https://cloud.google.com/text-to-speech/docs/voices)）されてないようです。


# 7. 制限

コンテンツの制限とリクエストの制限があります。

## 7.1. コンテンツの制限

|種類|使用制限|
|:-------------|------------:|
|リクエストごとの合計文字数|5,000|

## 7.2. リクエストの制限

|種類|使用制限|
|:-------------|------------:|
|1 分あたりのリクエスト数|300|
|1 分あたりの文字数|150,000|



---
[^1]: 5.料金を参照のこと