LINEのメッセージをGoogle Homeで読み上げる(機能強化版)

## はじめに

LINEのメッセージをGoogle Homeで読み上げるというのは既にたくさんの人がやってますが、今回自分がその仕組みを作った際に手を加えた点について書いておきます。なお、誰でも設定できるstep by step的な文章にはなっていません。今回作成したnode-redのフローまわりは重点的に説明・公開しますが、実際にそれを使ってシステムを構築するには以下の知識が必要になります。

- node-redの使い方
- LINE botとかMessaging APIまわり
- Beebotteの使い方
- gitの使い方

## 特徴

- Raspberry Piを外部ネットワークに公開しなくて大丈夫
    - ngrokではなくBeebotteを使って実現
- インターネット側は既存サービスの組み合わせで実現。コーディング不要
- google-home-notifierを機能強化
 - 読み上げの際の音量を変更可能
- Raspberry Pi + node-redを使って、なるべくコード書かずに機能強化
 - 連続してメッセージが投稿されても、取りこぼさずに順番に読上げ
 - LINE音声メッセージの再生にも対応
 - 「○○からLINE。<メッセージ本文>」と投稿者読み上げ
 - グループに投稿されたメッセージは、先頭に空白があるメッセージだけ読み上げ

## システムの構成
基本的には定番の構成です。2.だけちょっとめずらしいかも。

1. LINE bot宛てにメッセージが来ると、LINEのMessaging APIがWebhookリクエストを送信
    - 参考：[LINE Messaging APIの設定](https://qiita.com/ghoul_shana/items/7d62489240490f12f14a#line-messaging-api%E3%81%AE%E8%A8%AD%E5%AE%9A)

2. Microsoft flowでリクエストを受けて、内容加工後BeebotteにWebhookリクエスト送信
    - 設定については後述

3. Beebotteでリクエストを受けて、内容をMQTTメッセージとしてpublish (channel:line, resource:message)
    - 参考：[Beebotte側の設定](https://qiita.com/mayfair/items/e761c788a9d8787bc610#beebotte%E5%81%B4%E3%81%AE%E8%A8%AD%E5%AE%9A)

4. Raspberry Pi上のnode-redでMQTTメッセージを受け、google-home-notifierを使ってGoogle Homeに読み上げリクエスト
    - 参考: [Beebotte のメッセージを Node-RED で受ける](https://qiita.com/HeRo/items/9978bedf353fc790a5b5)

## Microsoft flowを使ってWebhookのJSONデータを加工・中継
[LINEのWebhookリクエストの内容](https://developers.line.me/ja/reference/messaging-api/#webhook-event-objects)は{events:[{<メッセージ内容>}]となっていますが、[BeebotteのREST API](https://beebotte.com/docs/auth)は{data:{}}の形式しか受け付けないため、要素名を置き換える必要があります。定番のIFTTTではこれができません。

herokuなどでサービス立ち上げるのも面倒。いったんはGoogle Apps ScriptのWeb API機能を使ってJSONデータの加工・中継に対応したのですが、いまひとつデバッグがしにくい。インターネット側はもうちょっとシンプルにありもので済ませたい。

そこで、IFTTT類似のサービスを調べたところ以下のものがありました。

- Zapier: 高機能でJSON書き換えも楽勝っぽかったけど、無料プランでは100回/月の制限が致命的。
- Microsoft flow: HTTP triggerがJSONスキーマを理解し、HTTP actionで好きな要素名にして返せる。無料で750回/月なのでまあまあ。

ということで、いまひとつ影の薄いMicrosoft flowが、今回の目的にはベストでした。作成したフローは以下になります。JSONスキーマは、サンプルペイロードとしてLINE Developersの[Webhookイベントオブジェクトの例](https://developers.line.me/ja/reference/messaging-api/#webhook-event-objects)を貼り付ければ生成してくれます。

追記：知らなかったのですが、2020年時点で、HTTPアクションは有料機能になってました。有料化以前に作ったフローは引き続き無料で使えてます。

<img width="611" alt="msflow.png" src="https://qiita-image-store.s3.amazonaws.com/0/12960/159cc194-4348-5704-e280-84e468a8b8e2.png">

## node-red側でいろいろ機能追加
独自コードはnode-red側に集中させました。最初に書いた「特徴」のほとんどはここで実現しています。

フローはgithubで公開しています：
https://github.com/tinoue/node-red-flows

### google-home-notifierを改造
- メッセージの読み上げは、通常よりも音量を大きくしたかったので、[google-home-notifier](https://github.com/noelportugal/google-home-notifier)にvolume() APIを追加 ([Pull request中](https://github.com/noelportugal/google-home-notifier/pull/44))。
- また、google-home-notifierに複数の読み上げリクエストが来ると、後勝ちとなってしまうため、排他処理がしやすいようにnotify() APIのコールバックタイミングを読み上げ終了後に変更しました。

### google-home-notifierフロー

追記：google-home-notifierのフローの最新版解説はこちら - [高音質のCloud Text-To-Speechを使ってnode-red + Google Homeで読み上げる](https://qiita.com/tinoue@github/items/4bb71799b40f09446767)

LINE以外の読み上げにも使うため、google-home-notifier関連は単体のフローとし、他のフローからはLinkノードで接続します。

![google-home-notifier.png](https://qiita-image-store.s3.amazonaws.com/0/12960/7bfe54ec-7e83-3ac5-4318-a2e083ce4def.png)

#### 読上げの排他処理
 [node-red-contrib-semaphore](https://github.com/teastman/node-red-contrib-semaphore)を使って排他処理を実現。Javascriptで直に書いてもいいんですが、このノードを使うとsemaphoreの状況をステータスとして表示してくれるのでわかりやすいです。念のため、例外やタイムアウトでsemaphoreの開放処理も追加。

#### Google Homeのホスト名設定
google-home-notifierでは、mDNSを使ってGoogle HomeのIPアドレスを自動取得するのですが、自分の環境ではうまくいきませんでした。しょうがないので、Raspberry pi上で'avahi-browse _googlezone._tcp'して表示されるGoogle Homeのホスト名('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx.local')をip() APIに指定しました。
ホスト名はJavascriptに直接書き込まず、[node-red-contrib-credentials](https://github.com/Steveorevo/node-red-contrib-credentials)を使って設定しています。
なお、mDNSでのIPアドレス取得がちゃんと動作する場合は、この設定は不要です。

## LINEメッセージの読み上げフロー
<img width="743" alt="line-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/12960/5f05f2d7-40ac-77df-9197-7b321485ad33.png">

BeebotteのMQTTメッセージから、LINEのメッセージ本文を抜き出して、google-home-notifierフローにリンク経由で渡します。

- 読み上げ先頭に投稿者の名前を追加します。投稿者の名前は、やろうと思えばLINEのAPIで別途取得することが可能ですが、家族グループで使用するには下の実名だけが欲しかったので、/home/pi/.node-red/line_users.jsonに、要素名がuserId、値が名前の対応データをハードコードしています。LINEのuserIdはどうやって調べるかというと、実際にメッセージを読上げさせて、node-redのデバッグタブに表示される値を拾いました。

```javascript
{
   "U4af4980629...": "たけし",
   "Ua3bcedfea2...": "たかし"
}
```

- groupIdがついているメッセージ(グループ宛投稿)の場合、先頭に空白が入っているメッセージだけを読み上げ対象にしています。家族グループですべてを読み上げられてしまうとうるさいためこうしました。
- [音声メッセージ](https://developers.line.me/ja/reference/messaging-api/#wh-audio)が来た場合、改めて[LINEのcontent endpoint](https://developers.line.me/ja/reference/messaging-api/#get-content)にアクセスして音声データを取得する必要があります。
ただし、ヘッダにアクセストークンをつける必要があるので、node-red側でLINEへのリクエストを中継・代行することにし、google-home-notifierにはnode-redのURL `http://<node-redのip address>:1880/linecontent` を渡すようにしています。

### フローの初期化
前後しますが、LINEメッセージ読上げフローの初期化周り。

<img width="821" alt="line-3.png" src="https://qiita-image-store.s3.amazonaws.com/0/12960/72053335-a98c-d3f7-eb17-eb9d180ac4f1.png">

- 前述のように、/home/pi/.node-red/line_users.json からユーザ名データを読み出し。結果はflow変数に格納します。ファイルがない場合は、例外をキャッチしてデバッグメッセージとして表示。
- これも前述のように、音声ファイル取得代行のために、node-red(Raspberry pi)自身のURL(IPアドレス)が必要になりますが、[node-red-contrib-hostip](https://github.com/fchanson/node-red-contrib-hostip)で取得しています。起動時に、node-redのグローバル変数にIPアドレスをセットします。

### LINE音声メッセージの取得代行

<img width="806" alt="line-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/12960/1e572a19-cee5-9334-94c5-dc7ef1091be3.png">

Google Homeから http://localhost:1880/linecontent にアクセスがあった場合、accessTokenをヘッダに付加して[LINEのcontent endpoint](https://developers.line.me/ja/reference/messaging-api/#get-content)から音声データを取得し、Google Homeに渡します。LINE側のURLについては、flow変数での受け渡しです。
ここでも、[node-red-contrib-credentials](https://github.com/Steveorevo/node-red-contrib-credentials)を使って、accessTokenの値を隠蔽しています。
なお、LINEの音声データはmp3ではなくm4aだったのですが、google-home-notifierのplay() APIでは、content-typeが現状mp3決めうちです。これはだめかと思ったのですが、試してみたら問題なく再生されました。

## 音声でのLINEへの投稿にも対応
Google HOMEに話しかけて、LINEにメッセージを投稿する対応もついでに行いました。これはIFTTTだけで即実現できるので割愛します。

## 使用したRaspberry Piについて
Raspberry Pi B+を使用しています。普通、Raspberry PiではUSB電源の容量に気をつける必要があります。しかしB+でキーボードもマウスも接続しない場合だと、消費電流は500mA以下なので、その辺のUSBポートから電源をとっても普通に動作します。
余ってるB+の活用法としてはちょうどいい感じです。

行った設定：

- 最小構成でインストール。GUIは入れない。Timezoneは忘れずに設定しておく。
- 長期可動のため、swapを無効化。メモリの使用率は50%程度と問題ないようです。
- 念の為、Macherelで監視。参考：[Raspberry Pi を Mackerel で監視する](https://qiita.com/ww24/items/5409aefd87f14beff504)。こちらはIFTTTのLINE Notifyを使って、死活監視結果をLINEで送ることができます。

## 課題
- 喋りが遅い。これはgoogle-home-notifierが勝手に?使っているGoogle翻訳用APIでは、速度の変更が出来ないため（正確には、更に遅くしかできない）。Google Cloud Text-to-Speechを使えばいいんだけどちょっと面倒。 → 追記：対応しました - [高音質のCloud Text-To-Speechを使ってnode-red + Google Homeで読み上げる](https://qiita.com/tinoue@github/items/4bb71799b40f09446767)
- LINEから送られてくるデータは複数メッセージを配列として送信可能ですが、現状は先頭しか見てません。そのため、複数人が同時に投稿したりすると、読上げの取りこぼしがおきる可能性があります。
- LINEグループに投稿したとき、botが既読つけてしまう。家族用グループには、メッセージ読上げ用のbotと、投稿用のbotの２つがグループに参加しているせいで、投稿の直後に既読2がついてしまい紛らわしく感じます。既読をつけない機能が、LINE botにあるといいのですが。
- Google Homeで音楽再生中に読み上げを行うと音楽が止まってしまう。これはどうしようもないかもしれません。自分の場合、音楽再生はAmazon Echoの役割で、Google Homeは読み上げが主目的で購入したのでとりあえずいいかなと...