#背景
会社を出たときに妻に「今から帰る」LINEをするが、料理中で気づかないことが多いので、
LINEでメッセージを送ってGoogle Homeに読み上げてもらっていた。
ところが読み上げられるメッセージは、テキストから生成した音声でいかにも機械が喋ってる感じがして、なんとなく温かみがない。
そこで、LINEにはボイスメッセージという機能があるので、Google Homeで自分の声を喋ってもらうことにした。
その際、テキストメッセージの処理も残し、受け取ったのがテキストメッセージorテキストメッセージの場合によって処理を分けることとした。
#処理概要
事前準備1,2とメイン処理1~4からなる
##事前準備
1. ngrokでポートに穴を空けておく
- LINE Messaging APIよりWebhook送信される音声orテキストファイルを自宅LAN内ラズパイが受信するため
- セキュリティ的にngrokは気が進まなかったが、まずは動くものをと思ってひとまず妥協
2. FlaskでMP3ファイルを返すサーバーを立てる
- Googleデバイスを操作するPyChromecastは、再生時にローカルファイルをそのまま指定(例: /home/pi/tmp.mp3など)して再生ができないため(←違ったら教えてください!)
##メイン処理
1. クライアント端末からLINE Developersで作成したアカウントにメッセージを送信
- ボイスメッセージ「今から帰るよ!」と喋る、もしくはテキストメッセージで「今から帰るよ!」)と送信
2. LINE Messaging APIのWebhookより自宅ラズパイに音声orテキストファイルを送信(転送?)
3. ラズパイにて、受信したファイルから読み上げ用MP3ファイルを作成
- 受け取ったファイルが音声ファイルなら、ffmpegでファイル形式をAAC->MP3に変換
- 受け取ったファイルがテキストファイルならば、gTTSを使ってテキスト->MP3を生成
4. PyChromecastを使って3.で作成したMP3ファイルをGoogle Homeに再生させる
- 事前準備2.で作成したサーバーから再生
#実装
上記 事前準備1,2とメイン処理1~4の番号に対応
##事前準備1 ngrok
ngrokとはローカル環境をLANの外からアクセスするためのツール
ポートを指定するだけでローカル環境を簡単に外部ネットワークに公開可能
####インストール
https://ngrok.com/download にて該当するファイルを選ぶ
任意のディレクトリにダウンロード
wget -O ngrok-stable-linux-arm.zip 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip'
Authトークンを登録
クライアント(ここではラズパイ)にて以下コマンドを実行し、トークンを登録することでアカウントと紐づける
./ngrok authtoken 3zb2a*****kK
ポート番号を指定して実行
./ngrok http 5000
マスク部分のForwarding
が公開されたアドレスとなり、外部からアクセスするとHTTP Requests
にステータスコードを返す
##事前準備2 Flask
FlaskとはPython用のウェブアプリケーションフレームワーク
APIサーバーが簡単に立てられる
####インストール
pip3 install flask-assistant
FlaskによるMP3ファイル供給サーバー
https://qiita.com/sousoumt/items/d815c813a91f3173c2a0
を参考にさせていただきました。port=8088はメインプログラムのpychromecastでMP3ファイルの場所を指定するところで使用します。
import os
from flask import Flask, make_response
app = Flask(__name__)
@app.route("/<string:file_name>", methods=['GET'])
def getMP3File(file_name):
response = make_response()
if not os.path.exists(file_name):
return response
response.data = open(file_name, "rb").read()
response.headers['Content-Disposition'] = 'attachment; filename=' + file_name
response.mimetype = 'audio/mp3'
return response
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=8088)
##メイン処理1 クライアント端末からLINE Developersで作成したアカウントにメッセージを送信
下記「メイン処理2」で作成したアカウントにメッセージを送る
##メイン処理2 LINE Messaging APIのWebhookより自宅ラズパイに音声orテキストファイルを送信(転送?)
LINEでクライアント端末から、送信先に指定するアカウント
このアカウントに対して送ったボイス/テキストメッセージが、Webhookで送信(転送?)されて最終的にGoogle Homeで読まれる
####Messaging API:
https://developers.line.biz/ja/services/messaging-api/
プロバイダーから「新規プロバイダー作成」を選択
「メッセージ送受信設定」Webhook URLにngrokで公開したアドレスを指定
その他、Channel Secret
とアクセストークン(ロングターム)
は後で使うので控えておく
##メイン処理3-1 ffmpegでAAC->MP3変換
- LINEのボイスメッセージはAAC(*.m4a)形式で送られてくる
- 一方、PyChromecastで再生可能なオーディオはMP3
- よって、受信した音声ファイルをAACからMP3に変換する必要がある
- ffmpegというツールを使えば簡単に可能
- ファイル名はtmp.m4a、tmp.mp3とする
- 実はエンコード時にUnknown encoder 'libmp3lame'のエラーで5時間くらいハマった。解決方法は後日
#####コード中ではsubprocessで実行
subprocess.call([
"/home/pi/opt/ffmpeg-git-20190331-armhf-static/ffmpeg",
"-i", '/home/pi/work/tmp.m4a',
"-acodec", "libmp3lame", "-b:a", "143k",
'/home/pi/work/tmp.mp3'
])
##メイン処理3-2 gTTSでテキスト->MP3変換
- テキストメッセージをそのままPyChromecastで再生できない
- そこで、テキストからMP3を生成する必要がある
- gTTSというツールを使えば簡単に可能
- ファイル名はtmp.mp3とする
#####テキストはevent.message.text
に入っている
いきなりメッセージが読み上げられるとびっくりするので「LINEです」とつけておく
readtext = 'LINEです。'+event.message.text
tts = gTTS(text=readtext, lang='ja')
tts.save('/home/pi/work/tmp.mp3')
##メイン処理4 PyChromecastで再生
- 事前に作成したFlaskサーバーからtmp.mp3を呼び出す
##メイン処理3以降のファイルは以下
from linebot import (
LineBotApi, WebhookHandler
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage, AudioMessage
)
import pychromecast
from gtts_token import gtts_token
import urllib.parse
from flask import Flask, request, abort
import subprocess
import time
app = Flask(__name__)
# 各クライアントライブラリのインスタンス作成
line_bot_api = LineBotApi('4TQa***') #LINE Developersページのアクセストークン(ロングターム)
handler = WebhookHandler('b9a0***') #LINE DevelopersページのChannel Secret
@app.route("/readmessage", methods=['POST'])
def receivemessage():
# get X-Line-Signature header value
signature = request.headers['X-Line-Signature']
# get request body as text
body = request.get_data(as_text=True)
app.logger.info("Request body: " + body)
# handle webhook body
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(400)
return 'OK'
#Google Homeを探す
googlehome_name = "Living Room"
chromecasts = pychromecast.get_chromecasts()
cast = next(cc for cc in chromecasts if cc.device.friendly_name == googlehome_name)
#テキストの場合
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
print(event.message.text)
readtext = 'LINEです。'+event.message.text
#テキストメッセージからmp3ファイルを生成
tts = gTTS(text=readtext, lang='ja')
tts.save('/home/pi/work/tmp.mp3')
#変換するまで待つ
time.sleep(3)
#再生
cast.wait()
mc = cast.media_controller
mc.play_media('http://192.168.0.18:8088/tmp.mp3', 'audio/mp3')
mc.block_until_active()
#読み込まれる前に削除されないよう待つ
time.sleep(5)
#音声ファイルを削除する
subprocess.call(['rm', '/home/pi/work/tmp.mp3'])
#音声の場合→音声ファイルをそのまま再生
@handler.add(MessageEvent, message=AudioMessage)
def handle_audio_message(event):
#音声ファイルを保存する
content = line_bot_api.get_message_content(event.message.id)
with open('/home/pi/work/tmp.m4a', 'wb') as f:
for c in content.iter_content():
f.write(c)
#保存するまで待つ
time.sleep(1)
#音声ファイルをACC->MP3に変換する
subprocess.call([
"/home/pi/opt/ffmpeg-git-20190331-armhf-static/ffmpeg",
"-i", '/home/pi/work/tmp.m4a',
"-acodec", "libmp3lame", "-b:a", "143k",
'/home/pi/work/tmp.mp3'
])
#変換するまで待つ
time.sleep(3)
#再生
cast.wait()
mc = cast.media_controller
mc.play_media('http://192.168.0.18:8088/tmp.mp3', 'audio/mp3')
mc.block_until_active()
#読み込まれる前に削除されないよう待つ
time.sleep(5)
#音声ファイルを削除する
subprocess.call(['rm', '/home/pi/work/tmp.m4a'])
subprocess.call(['rm', '/home/pi/work/tmp.mp3'])
if __name__ == "__main__":
app.run(host='127.0.0.1', port=5000)
#今後の課題
使用するだけなら問題ないが、気が向いたら対応したい
- クライアントまでステータスコードが返ってこないので、ラズパイが異常終了していた場合などGoogle Homeで読み上げられなくてもそれがわからない
- 投稿から再生まで時間がかかる
- 連続しての再生ができない
- セキュリティ的にngrokを使いたくない