[Python] Google Text-to-SpeechでGoogle Homeを喋らせたい！

# 概要
**Google Homeに任意の内容を喋ってほしかった**ので，さっくり構築してみました。

全ソースは[nerikosans/google-home-notify](https://github.com/nerikosans/google-home-notify)にあります。

調べてみると google-home-notifier や gTTS + pychromecast を使った例がヒットします。pychromecastは便利で、音声ファイルを投げればHomeに喋らせてくれます。

いっぽうgTTSについては、

- 喋りのクオリティが微妙  
- 速度などのパラメータ調整がきかない

などすこし難点を感じたので、Google Text-to-Speechで置き換えて**柔軟さを手に入れる**ことにします。

また、Google Cloud Storageに音声ファイルをあげることにしたので、**ローカルサーバの構築も省略できます**。

Googleづくしですね！

# Google Homeと接続 (pychromecast)
まずはローカルネットワーク上のHomeと接続しましょう。[pychromecast](https://github.com/balloob/pychromecast)を使います。
IPアドレスはわかっている前提とし、ここでは `192.168.0.2` とします。

```
$ pip install pychromecast
``` 

```main.py
import pychromecast

def find_device(ip_addr):
    device = pychromecast.Chromecast(ip_addr)
    return device

if __name__ == '__main__':
    home = find_device('192.168.0.2')
    home.wait()
    print(home)
```

```
Chromecast(
    '192.168.0.2', 
    port=XXXX, 
    device=DeviceStatus(
        friendly_name='Living Room speaker',
        model_name='Unknown model name',
        manufacturer='Unknown manufacturer',
        uuid=UUID('XXXX-XXXX-XXXX-XXXX'),
        cast_type='cast'
    )
)
```

OKですね。
なお、 `pychromecast.get_chromecasts()` だと `[]` が返ってしまいなぜかうまくいきませんでした。このissue([Empty List Returned by get_chromecasts · Issue #117 · balloob/pychromecast] (https://github.com/balloob/pychromecast/issues/117)) と関係ありそうですが、謎です。わかる方いたらぜひ教えて下さい。

# 音声データの生成 (Google Text-to-Speech)
Homeに喋らせる音声を作りましょう。
[Google TTS](https://cloud.google.com/text-to-speech/?hl=JA) はGoogle先生の培った機械学習技術の賜物であり、かなり自然な音声を合成してくれます。というか、おそらくGoogle Homeが普段しゃべってるのってこれですよね。なので、より違和感なくHomeに喋ってもらえる気がします。

なお、月に100万文字までは無料のようです。これは**『吾輩は猫である』(約37万字)を3回ほど朗読できる**量に相当します。

サービスアカウントのキーを `service-account.json` とします。
[公式実装](https://cloud.google.com/text-to-speech/docs/reference/libraries?hl=JA)を参考に作っていきます。

```
$ pip install --upgrade google-cloud-texttospeech
```

``` main.py
from google.cloud import texttospeech
from datetime import timedelta

account_json = 'service-account.json'
tts_cli = texttospeech.TextToSpeechClient.from_service_account_json(
    account_json)

def make_synthesis_args(
    text,
    lang='ja-JP',
    gender=texttospeech.enums.SsmlVoiceGender.NEUTRAL,
    volume=8.0,
    speed=1.2,
    profile='small-bluetooth-speaker-class-device',
):
    synthesis_input = texttospeech.types.SynthesisInput(text=text)

    voice = texttospeech.types.VoiceSelectionParams(
        language_code=lang,
        ssml_gender=gender)

    audio_config = texttospeech.types.AudioConfig(
        audio_encoding=texttospeech.enums.AudioEncoding.MP3,
        volume_gain_db=volume,
        speaking_rate=speed,
        effects_profile_id=[profile])
    return (synthesis_input, voice, audio_config)
```

ここでは声の性別と音量、しゃべる速度を指定可能としました。
性別 `gender` は `texttospeech.enums.SsmlVoiceGender` 以下 `MALE`, `FEMALE`, `NEUTRAL` が選択可能で、
音量 `volume` は `[-96.0, 16.0]` (dB) から、
速度 `speed` は `[0.25, 4.0]` の範囲で指定可能です。

`profile` は **スピーカーの特性に合わせた処理を施してくれる** 機能で、 [ここ](https://cloud.google.com/text-to-speech/docs/audio-profiles?hl=ja) にある値から指定可能です。うちにあるのはGoogle Home Miniなので、それ用のものをデフォルトにしておきます。

このあと

``` .py
tts_cli.synthesize_speech(*make_synthesis_args(text=text))
```

すれば音声データをつくることができます。

# 音声データの作成、アップロード (Google Cloud Storage)
音声ファイルのURLをつくらないといけないので、GCSにあげることにします。

```
$ pip install --upgrade google-cloud-storage
```

``` main.py
from google.cloud import storage
gcs_cli = storage.Client.from_service_account_json(account_json)
bucket = gcs_cli.get_bucket('your-project-name.appspot.com')

def generate_audio_url(text, audio_config=None):
    filename = 'audio.mp3'
    filepath = f'tts-audio/{filename}'
    filepath_local = f'./tts-audio/{filename}'

    if audio_config is None:
        audio_config = {}

    response = tts_cli.synthesize_speech(
        *make_synthesis_args(text=text, **audio_config))

    # The response's audio_content is binary.
    with open(filepath_local, 'wb') as out:
        # Write the response to the output file.
        out.write(response.audio_content)

    blob = bucket.blob(filepath)
    blob.upload_from_filename(filepath_local)

    return blob.generate_signed_url(expiration=timedelta(minutes=1))
```

Storageに上げたファイルから、1分間有効なSigned URLを発行しました。

なお、うっかり `blob.generate_signed_url` に `content_type='audio/mpeg'` など を指定したりすると動かなくなります。
これは、Google Homeからの参照はGET methodなので `Content-Type` を指定できず、
> The request signature we calculated does not match the signature you provided.

でエラーになるためです。

# しゃべってもらおう！
ということで、準備ができたので喋ってもらいましょう！

``` main.py
def send_sound(device, audio_path):
    mc = device.media_controller
    mc.play_media(audio_path, 'audio/mp3', autoplay=True)
    mc.block_until_active()

if __name__ == '__main__':
    home = find_device('192.168.0.2')
    home.wait()

    audio_url = generate_audio_url(text='できました')
    send_sound(home, audio_url)
```

## output
**Ω<「できましたー！」**

できました！

# まとめ
- Google TTS、べんり！
- Google Homeにしゃべってもらうとかわいい

コメント、issue、PRお待ちしてます。