Pythonを使って、Google Homeに喋らせてみる

Google Homeに自発的に喋ってもらいたくなったので色々と調べてみたのですが、`google-home-notifier`が大多数を占めていたので、Pythonのみで書いてみました。

# 面倒な人向け

[Gistにソースがあるので見てください](https://gist.github.com/attakei/68936f7378ca4b95cb75e5d0cfd21c2a)

# 序：`google-home-notifier`について(ざっくりと

node.js製の、Google Homeに対してテキストを喋らせるライブラリです。

雑にコードを読んだり、[Qiitaにある記事](https://qiita.com/SatoTakumi/items/c9de7ff27e5b70508066#google-home-notifier%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF)なんかを読んところ、

1. mDNSでLAN内のGoogle Homeを探す
2. テキストをGoogle Text-to-Speechを使ってmp3のURLを作成する
3. (2)のURLをGoogle Homeに渡して再生させる

といった感じになるようです。

# 破：google-home-notifierの仕組みはPythonのライブラリにあるか

* [pychromecast](https://github.com/balloob/pychromecast)
* [gTTS](https://github.com/pndurette/gTTS)

とりあえず目についたのが、この2個。
うまい具合に不足部分を補えれば、再現に近いことができそうです。

## pychromecast

Chromecastとの通信をしてくれるライブラリです。

> Check out Home Assistant for a ready-made solution using PyChromecast for controlling and automating your Chromecast or Cast-enabled device like Google Home.

とREADMEにあるとおり、Google HomeはGoogleCast仕様に基づくらしく、このライブラリでそのまま拾うことができます。

```py3:pychromecast_sample
>>> import pychromecast
>>> chromecasts = pychromecast.get_chromecasts()
>>> len(chromecasts)
>>> chromecasts[0]
Chromecast(
    '192.168.0.12',
    port=8009,
    device=DeviceStatus(
        friendly_name='自室のGoogle Home',
        model_name='Google Home',
        manufacturer='Google Inc.',
        uuid=UUID('XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'),
        cast_type='audio'
    )
)
```

## gTTS

Google Text-to-speechを利用して、テキストをmp3に変換してローカルに保存する事ができるライブラリです。

```py3:gtts_sample
>>> from gtts import gTTS
>>> tts = gTTS(text='ハローワールド', lang='ja')
>>> tts.save('./hello_world_ja.mp3')
```

わずか3行で変換できる優れもの。ただし、このライブラリのみでは`google-home-notifier`のようにGoogle側にあるURLを扱えないのが考えものです。
（とはいえ、色々実装するの面倒なので、今回は眼をつぶることにします）

# 急：実際に作ってみる

## 用意するもの

`bottle`も使って、ローカルにWebサーバーを用意します。
パスはこんな感じ。

* `/form`
    * テキスト送信フォームのURL
* `/talks/xxxxxxxx`
    * 保存済みテキストmp3のURL

## `/form`の処理概要

1. ブラウザからフォームでテキストと言語を送信
2. POSTリクエスト内で、
    1. `gTTS`を使ってテキストのmp3を作成してローカルに保存
    2. `pychromecast`を使って、保存済みのmp3を示すURLをGoogleHomeに再生させる
3. Google Homeが喋る

## ソース抜粋

※処理の流れが終える程度に圧縮

```py3:server.py
# -*- coding: utf-8 -*-
# インポート周りは省略

BASE_DIR = Path(__file__).parent
TALK_DIR = BASE_DIR / 'var'
app = Bottle()


@app.route('/talks/<file_path:path>')
def get_talk_mp3(file_path):
    return static_file(file_path, root=TALK_DIR)


@app.route('/form', method='GET')
def get_talk_form():
    """フォーム表示"""
    content = BASE_DIR / 'template.html'
    text = request.forms.text or ''
    lang = request.forms.lang or 'ja'
    return template(
        content.open().read(), langs=gTTS.LANGUAGES, lang=lang, text=text)


@app.route('/form', method='POST')
def post_talk_form():
    """トークテキストを保存して、再生指示"""
    text_token = generate_talk(request.forms.text, request.forms.lang)
    # 面倒なので、目についたGoogleCast端末に決め打ち
    chromecasts = pychromecast.get_chromecasts()
    chromecasts[0].media_controller.play_media(
        f"http://192.168.0.20:8080/talks/{text_token}", 'audio/mp3')
    return get_talk_form()


def generate_talk(text, lang):
    """言語もテキストも一致したらキャッシュを使うように"""
    text_token = hashlib.sha256((lang + text).encode()).hexdigest()
    talk_path = TALK_DIR / text_token
    if not talk_path.exists():
        tts = gTTS(text=text, lang=lang)
        tts.save(talk_path)
    return text_token


if __name__ == '__main__':
    if not TALK_DIR.exists():
        TALK_DIR.mkdir()
    app.run(host='0.0.0.0', port='8080', reloader=True)
```

（時間があったら、動画でも用意します）

# あとがき

`google-home-notifier`でも十分だったりするのですが、「この辺の仕組みを上手く広げていくと面白い何かができないかなー」なんて考えてみたりしたいですね。

[こんなのもあったので](https://github.com/harperreed/google-home-notifier-python)、参考にしたいと思います。