Edited at

LINEでボイスメッセージを送信し、その音声ファイルをGoogle Homeに喋らせる


背景

会社を出たときに妻に「今から帰る」LINEをするが、料理中で気づかないことが多いので、

LINEでメッセージを送ってGoogle Homeに読み上げてもらっていた。

ところが読み上げられるメッセージは、テキストから生成した音声でいかにも機械が喋ってる感じがして、なんとなく温かみがない。

そこで、LINEにはボイスメッセージという機能があるので、Google Homeで自分の声を喋ってもらうことにした。

その際、テキストメッセージの処理も残し、受け取ったのがテキストメッセージorテキストメッセージの場合によって処理を分けることとした。


処理概要


image.png


事前準備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トークンを登録

Sign upするとAuthtokenが発行される

image.png

クライアント(ここではラズパイ)にて以下コマンドを実行し、トークンを登録することでアカウントと紐づける

./ngrok authtoken 3zb2a*****kK


ポート番号を指定して実行

./ngrok http 5000    

マスク部分のForwardingが公開されたアドレスとなり、外部からアクセスするとHTTP Requestsにステータスコードを返す

image.png


事前準備2 Flask

FlaskとはPython用のウェブアプリケーションフレームワーク

APIサーバーが簡単に立てられる


インストール

pip3 install flask-assistant    


FlaskによるMP3ファイル供給サーバー

https://qiita.com/sousoumt/items/d815c813a91f3173c2a0

を参考にさせていただきました。port=8088はメインプログラムのpychromecastでMP3ファイルの場所を指定するところで使用します。


mp3server.py

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/

プロバイダーから「新規プロバイダー作成」を選択

image.png

作成したプロバイダーにチャネルを作成

image.png

「メッセージ送受信設定」Webhook URLにngrokで公開したアドレスを指定

image.png

その他、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以降のファイルは以下


main.py

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を使いたくない