昨年末から愛用してきたGoogle Home Mini。
手に入れて最初にやったのがNode.jsの「google-home-notifier」で色んな通知を喋らせるというもの。
しかし、自分のjavascriptの知識なんてjQueryでウェブサイトに動きをつけて遊んだりする程度。よくわからないまま先人の知恵たるブログを参考にして実装しましたが、仕組みがよくわからなくてモヤモヤしていました。
でもようやく出来ました!Pythonで!これを「google-home-notifier-vt」と名付けます。
できるようになること
- Slack→ngrok→Raspberry Pi→VoiceText→Google Homeを喋らせる
- 声や読み上げ速度を選べる(Voice Text API)
参考にした記事
このQiita記事を見てこれはやれる!と思いました。
Pythonを使って、Google Homeに喋らせてみる - Qiita
決定打はこれ。Flaskで作られていますが素晴らしい出来栄え。
Chromecastデバイスに喋らせるPython スクリプト · GitHub
最初はBottleで自分なりにやってみたもののキャストしたあとの遅延が長くて3日間くらい悩みましたが、Slash Nephyさんのようにファイル名を時間から出力してユニークなものにすることで多分解決?
そして楽したいのでそのままFlaskでやります笑
改良点
Voice Text API対応
GoogleのText To Speechでは細かな調整ができないっぽい?ので色々いじれるVoice Text APIで音声ファイルを生成できるように機能を置き換えました。
また、VoiceTextのライブラリでは音声ファイルがWAVで出力されるためWAVをキャストするパターンにも対応するよう書き換えました。
slackの特定チャンネルに投稿したら喋る
slackの発信Webhooksによってslackの特定チャンネルに投稿されたメッセージは自宅のGoogle Homeで喋らせる。
我が家では#googlehomeという専用チャンネルを用意してここに投稿されたものは全てGoogle Homeが読み上げるようにしてあります。 例えばIFTTTやヤフーのmyThingsで天気・雨雲接近注意報・熱中症警報・地震・外出・帰宅などの通知を全てSlackに投稿するようにしておけばngrokのURLが再起動などで変更になってしまった場合も発信webhookのURLを変更するだけで済みます。
もっと言えば発言者のIDを判別させてVoiceTextの声を使い分けさせることも可能です。(これはまた今度実装して記事にします)
文字化け対策
投げたあとのレスポンスがJSON形式にするとき文字化けしていたが以下の記事を参考に修正
Flask で Restful API を作る - jsonify で日本語が文字化けする時の解決方法 - datalove’s diary
謎の空白対策
Google Assistant→IFTTTで文字列を受け取ると何故か不要な空白が入るのでPythonで受け取った後に置換して削除。
コード全文
# encoding: utf-8 import os import json import time import pychromecast import requests from threading import Thread from datetime import datetime from flask import Flask, request, send_from_directory, jsonify from voicetext import VoiceText slack_webhook_url = "ここにSlackのWebhookURL" VT_APIKEY = "ここにVoiceTextのAPIキー" # VoiceText API Key VT_DEFAULT_SPEAKER = "hikari" # Speaker CACHE_DIR = "cache" CACHE_WIPE_INTERVAL = 60 * 5 IP_ADDRESS = "192.168.1.64" # Chromecast IP Address device = next(x for x in pychromecast.get_chromecasts() if x.host == IP_ADDRESS) device.wait() app = Flask(__name__) app.config['JSON_AS_ASCII'] = False # Avoid garbled characters app.config['port'] = 5000 url = None def get_ngrok_address(): global url if url is None: localhost_url = "http://localhost:4040/api/tunnels" # Url with tunnel details url = requests.get(localhost_url).text # Get the tunnel information j = json.loads(url) url = j['tunnels'][0]['public_url'] # Do the parsing of the get url = url.replace("http:", "https:") print(f"GET ngrok Address: {url}") requests.post(slack_webhook_url, data=json.dumps({'text': f'Google-Home-Notifier Address: {url}'})) return url def play_vt(url, text, speaker, emotion, speed, pitch, volume): filename = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.wav" vt = VoiceText(VT_APIKEY).speaker(speaker) if speed: vt.speed(int(speed)) if emotion: vt.emotion(emotion) if pitch: vt.pitch(int(pitch)) if volume: vt.volume(int(volume)) with open(f"cache/{filename}", 'wb') as f: f.write(vt.to_wave(text)) cast(f"{url}/cache/{filename}", "audio/wav") def cast(url, mimetype): print(f"wav URL is {url}.") mc = device.media_controller mc.play_media(url, mimetype) mc.block_until_active() @app.route("/cache/<path:path>") def send_cache(path): return send_from_directory("cache", path) @app.route("/notifier") def notifier(): text = request.args.get("text").replace(" ", "") if not text: audiourl = request.args.get("url") if not audiourl: return jsonify({"status": "ERROR", "message": "text is required."}) cast(audiourl, "audio/mp3") return jsonify({"status": "OK", "cast": audiourl}) speaker = request.args.get("speaker") or VT_DEFAULT_SPEAKER emotion = request.args.get("emotion") speed = request.args.get("speed") pitch = request.args.get("pitch") volume = request.args.get("volume") print(f"ChromeCast device will speak \"{text}\" in {speaker}. by Voice_Text") play_vt(get_ngrok_address(), text, speaker, emotion, speed, pitch, volume) return jsonify({"status": "OK", "text": text, "speaker": speaker, "emotion": emotion, "speed": speed, "pitch": pitch, "volume": volume }) @app.route('/slack', methods=['POST']) def slack(): text = request.form['text'].replace(" ", "")[0:199] if not text: return jsonify({"status": "ERROR", "message": "text is required."}) speaker = VT_DEFAULT_SPEAKER emotion = None speed = None pitch = None volume = None print(f"ChromeCast device will speak \"{text}\" in {speaker}. by Voice_Text") play_vt(get_ngrok_address(), text, speaker, emotion, speed, pitch, volume) return '' @app.route("/") def index(): return "[example] " + get_ngrok_address() + "/notifier?text=テストメッセージ" def wipe_cache_task(): print(f"Cache wiping task started. Cache wiping interval is {CACHE_WIPE_INTERVAL} seconds.") while True: for path in os.listdir(CACHE_DIR): os.remove(f"{CACHE_DIR}/{path}") time.sleep(CACHE_WIPE_INTERVAL) if __name__ == "__main__": if not os.path.isdir(CACHE_DIR): os.makedirs(CACHE_DIR) Thread(target=wipe_cache_task).start() Thread(target=get_ngrok_address).start() app.run()
導入手順
① 各種ライブラリをインストール
必要なライブラリ - flask・・・webサーバー - requests・・・ngrokのAddressやSlack投稿など - pychromecast・・・pythonでクロームキャストを可能に - python-voicetext・・・VoiceTextWEBAPIをお手軽に実行
それぞれインストールしておきます。
$ pip install flask $ pip install requests $ pip install pychromecast $ pip install python-voicetext
python-voicetextのインストールでエラーが発生した場合
おそらく以下のようなエラーではないでしょうか。
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple Collecting PyAudio Using cached https://files.pythonhosted.org/packages/ab/42/b4f04721c5c5bfc196ce156b3c768998ef8c0ae3654ed29ea5020c749a6b/PyAudio-0.2.11.tar.gz Building wheels for collected packages: PyAudio Running setup.py bdist_wheel for PyAudio ... error Complete output from command /usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-2t3wdpz1/PyAudio/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" bdist_wheel -d /tmp/pip-wheel-_30pg5kd --python-tag cp35: running bdist_wheel running build running build_py creating build creating build/lib.linux-armv6l-3.5 copying src/pyaudio.py -> build/lib.linux-armv6l-3.5 running build_ext building '_portaudio' extension creating build/temp.linux-armv6l-3.5 creating build/temp.linux-armv6l-3.5/src arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -g -fdebug-prefix-map=/build/python3.5-RUbMX3/python3.5-3.5.3=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/usr/include/python3.5m -c src/_portaudiomodule.c -o build/temp.linux-armv6l-3.5/src/_portaudiomodule.o src/_portaudiomodule.c:29:23: fatal error: portaudio.h: そのようなファイルやディレクトリはありません #include "portaudio.h" ^ compilation terminated. error: command 'arm-linux-gnueabihf-gcc' failed with exit status 1 ---------------------------------------- Failed building wheel for PyAudio Running setup.py clean for PyAudio Failed to build PyAudio Installing collected packages: PyAudio Running setup.py install for PyAudio ... error Complete output from command /usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-2t3wdpz1/PyAudio/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-cbmc9nru/install-record.txt --single-version-externally-managed --compile: running install running build running build_py creating build creating build/lib.linux-armv6l-3.5 copying src/pyaudio.py -> build/lib.linux-armv6l-3.5 running build_ext building '_portaudio' extension creating build/temp.linux-armv6l-3.5 creating build/temp.linux-armv6l-3.5/src arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -g -fdebug-prefix-map=/build/python3.5-RUbMX3/python3.5-3.5.3=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/usr/include/python3.5m -c src/_portaudiomodule.c -o build/temp.linux-armv6l-3.5/src/_portaudiomodule.o src/_portaudiomodule.c:29:23: fatal error: portaudio.h: そのようなファイルやディレクトリはありません #include "portaudio.h" ^ compilation terminated. error: command 'arm-linux-gnueabihf-gcc' failed with exit status 1 ---------------------------------------- Command "/usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-2t3wdpz1/PyAudio/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-cbmc9nru/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-install-2t3wdpz1/PyAudio/
要はpython-voicetextを使うために必要なライブラリ「pyaudio」をインストールしようとしたけど、さらにpyaudioに必要な「portaudio」が見つかりませんということ。
$ apt-get install portaudio19-dev
これで改善するはずですのでもう一度python-voicetextをインストールしてみましょう。
$ pip install python-voicetext
② ngrokの準備
ngrokとは 簡単にいうと、ローカルPC上で稼働しているネットワーク(TCP)サービスを外部公開できるサービスです。例えば、ローカルPCのWebサーバを外部公開することができます。
引用 https://qiita.com/mininobu/items/b45dbc70faedf30f484e
インストール
wgetでzipファイルを取得し解凍→binフォルダに移動することで利用可能になります。
$ wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip $ unzip ngrok-stable-linux-arm.zip $ sudo mv ngrok /usr/local/bin/ $ ngrok version
2019/03/11追記
ngrokの新バージョンとRaspberry PiのCPUの相性によっては「illegal instruction」というエラーが発生して実行できない場合がありました。その場合旧バージョンを利用することになりそうです。
Raspberry Pi Zero WHで発生したため各バージョンを検証したところVersion 2.2.8以前であれば動作しました。
↓こちらのVersion 2.2.8/Linux/.ZIP/ARM64 https://bin.equinox.io/a/nmkK3DkqZEB/ngrok-2.2.8-linux-arm64.zip
を使いましょう。
https://dl.equinox.io/ngrok/ngrok/stable/archive
ユーザー登録・認証
ngrokのユーザー登録と認証を行います。
未登録だと利用時間に制限があるため必ず登録しましょう。GoogleアカウントやGithubアカウントでログインすることも可能です。
https://dashboard.ngrok.com/user/login
ログインしたあとの画面中③にあるコマンドをコピーします。
$ cd /usr/local/bin $ ./ngrok authtoken XXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXX Authtoken saved to configuration file: /home/pi/.ngrok2/ngrok.yml
これで認証作業は完了。時間制限なく利用できるようになりました。
③ VoiceText APIユーザー登録
VoiceTextがGoogleのTextToSpeech的なものなんですが、声や読み上げ速度、ピッチなどが細かく選べる点が優秀です。
ちなみにモヤモヤさまぁ~ずのナレーションはVoiceTextのショウくんが務めています。つまりGoogle Homeで喋らせる声をshowと指定しておけばモヤさまっぽく色んな通知をしてもらえます。
利用にはユーザー登録が必要なので以下から登録。
https://cloud.voicetext.jp/webapi
登録が完了するとAPIキーがメールで送られてきます。大事なので無くさないように保存しておきましょう。
④ Slackのチャンネルと着信Webフックを用意
関係ない会話まで読み上げられてはウザいので専用チャンネルを作りましょう。#googlehomeや#notifierなどがいいんではないでしょうか。
次にカスタムインテグレーションを作成します。
着信 Webフックの「設定を追加」で例として以下のように設定します。アイコンもGoogleAssistantにしたりすると雰囲気が出ますね。
このときWebhook URL
をコピーしておいてください。次のコード編集時に必要です。
⑤ Pythonコード編集
環境に合わせて変更する箇所は以下の通り
12行目にslackの設定を書き込む
slack_webhook_url = "ここにSlackのWebhookURL"
14-15行目にVoicetextの設定を書き込む
VT_APIKEY = "ここにVoiceTextのAPIキー" # VoiceText API Key VT_DEFAULT_SPEAKER = "hikari" # Speaker
speakerはshow
(男性)haruka
(女性)hikari
(女性)takeru
(男性)santa
(サンタ)bear
(凶暴なクマ) から選べます。
20行目にGoogleHomeのIPアドレスを書き込む
IP_ADDRESS = "192.168.1.64" # Chromecast IP Address
IPアドレスはスマホのHomeアプリ→デバイス→設定の最下部に記載されています。
⑥ 実行
ngrokを起動
$ ngrok http 5000
もう一つコンソールを立ち上げてgoogle-home-notifier-vtを起動
$ python vt.py
数秒後にSlackへngrokで生成されたURLが投稿されるのでアクセスしてテスト。
https://xxxxxxxxx.ngrok.io/notifier?text=テストメッセージ
SSHで実行する場合
SSH接続して実行した場合はクライアントを閉じるとプロセスも終了してしまいます。
これをバックグラウンドで実行し、クライアント終了後も持続させるにはnohup
を先頭につければOKです。
nohup ngrok http 5000 nohup pipenv run python vt.py
⑦ Slackの発信Webフックを用意
URLの項目にhttps://xxxxxxxxx.ngrok.io/slack
のように入力します。
これで#googlehomeチャンネルに投稿されたメッセージは全てRaspberry PiへpostされてVoiceTextで音声ファイルに変換後、Google Homeにクロームキャストすることで喋らせることが可能になりました。
その他オプションなど
slackからは細かい声の設定は出来ない状態になっていますが、今後できるように変更していこうと思います。
また、直接パラメーターを付与してアクセスすればVoiceTextの仕様どおりに各種調整が可能です。
例えばhttps://xxxxxxxxx.ngrok.io/notifier?speaker=show&text=テストメッセージ
とすれば声をshow
君に変更できます。
その他にもpitch
で高さ、volume
で音量、emotion
で感情、speed
で読み上げ速度などを調整可能です。
パラメータ | 説明 | 制限 | 初期値 |
---|---|---|---|
text | 合成するテキスト。エンコーディングは UTF-8。 | 制限 | 初期値 |
speaker | 話者名。後述の「話者一覧」の中のいずれかを指定します。 | 必須 | |
emotion | 感情カテゴリの指定。 話者 haruka、hikari、takeru、santa、bear にのみ使用できます。 以下のいずれかを指定します。 happiness 喜anger 怒sadness 悲 |
||
pitch | 音の高低を数値で指定します。値が小さいほど低い音になります。 | 50から200(%)まで | 100(%) |
volume | 音量を数値で指定します。値が小さいほど小さい音になります。 | 50から200(%)まで | 100(%) |
詳しくは公式のマニュアルをご確認ください。
あとがき
今回はflaskを使ったことでwebサービスの仕組みに触れられて大いに勉強になりました。
そしてやっぱりRaspberry Piは色んな可能性を秘めていて楽しいですね。
近々Raspberry PiにおけるPython環境構築(pipenv)についても書こうかなと思っています。探り探りなので気になった点などあればご指摘いただければ幸いです。

Raspberry Pi Zero W Starter Kit
- 出版社/メーカー: ケイエスワイ
- メディア: エレクトロニクス
- この商品を含むブログ (1件) を見る