LoginSignup
52
65

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-04-07

背景

会社を出たときに妻に「今から帰る」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を使いたくない
52
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
65