Google Home とラズパイなどを組み合わせて音声認識コミュニケーションツールを作ってみた

「スマートスピーカー Advent Calendar 2017 」 23日目です。
Google Home Mini やRaspberryPi（ラズパイ）、サーマルプリンタなどを使って親子コミュニケーションツールを作ったので、Google Home とDialogFlow まわりを中心に書いていきます。

# 作ったもの
[『ミミ子のただいまプリンター』](https://hacklog.jp/works/52241)は、共働きの我が家で、家に子ども達だけになってしまう時間が多いことから、親子でうまく連絡をとれる仕組みが欲しいという課題解決を目指して作ったものです。

うさぎのマスコット”ミミ子”が、可愛い声で子どもと会話しながら親にメッセージを送ったり、親からのメッセージを子どもにお知らせしてくれます。子どもに連絡する時は”ミミ子”からの音声だけでなく、サーマルプリンタからレシート状の紙を出力するので、子どもに携帯を持たせていなくても連絡できます。
![ミミ子のただいまプリンター](https://ma10.s3.amazonaws.com/uploads/works/52241/2.jpg)
「Google Home 使って無いやん！」って声が聞こえてきそうですが、うさぎの”ミミ子”の下にある木箱にラズパイと一緒に収まっています。
木箱に収めると「音声が通らないのでは？」という心配がありますが、木箱の下側と側面に穴が開いているので（音質はともかく）案外聞こえます。

なお、この作品はMASHUP AWARDS 2017 に応募し、[テーマ賞「チームワークが向上したで賞（by サイボウズ）」をいただきました！](http://mashupaward.jp/winninglist-2017/)（関係者の皆さんありがとうございました！）

# できること
『ミミ子のただいまプリンター』を使って以下の事ができます。
1. 音声操作でGoogle カレンダーに登録した予定を紙に出力できる
2. 親がLINE で送ったメッセージを子ども向けに紙で出力できる
3. 自宅付近で雨が降りそうな時にも子ども向けに紙と音声で通知できる
4. 音声操作で子どもでも簡単に親にメッセージを送信できる
5. 子どもからのメッセージを感情解析して「にこにこカレンダー」形式で記録できる
6. ミミ子からの通知は子機で受け取れる

子どもに携帯やスマホを持たせれば解決することも多いですが、まだ小さい子もいるため、今回は別のアプローチで対応しました。

なお、うさぎの”ミミ子”は可愛い声にしたかったので、Google Home デフォルトの真面目な音声ではなく、[AITalk](https://www.ai-j.jp/) を使って生成した音声を使っています。

# システム構成
![システム構成図](https://ma10.s3.amazonaws.com/uploads/works/52241/3.png)
色んなサービスを組み合わせているので、線がゴチャゴチャしていますが、今回はGoogle Home - Actions on Google - Dialogflow - Google Cloud functions 周りでどんなことをしているかを説明します。

# Fulfillment の設定
DialogFlow でIntent（ユーザーがどんなことを要求しているかなどの意図）判別後に実行されるWebhook 先のURL を登録できます。URL側ではDialogflow が判別した情報（Intent やEntity）を受け取って、処理ができます。
![recog.child](http://www.app-labo.net/blog_images/20171223_google_home/Fulfillment.png)
ここでは、Google Cloud functions のURLを登録しています。

# Intent：子どもを認識
Dialogflow で子どもの名前を認識するIntent を作っています。
![recog.child](http://www.app-labo.net/blog_images/20171223_google_home/Intent_recog.child.png)
PARAMETER NAME 欄にある「`@`child」が子どもの名前を格納する必須パラメータですが、子どもの呼び方は色々ありますし、その時々で変わりますよね。
例えば、本名が”あいこ”だとしても、呼び方は”あいちゃん”や”あーちゃん”、”あいさん”などさまざまで呼び方の”揺れ”があります。
その”揺れ”を、Dialogflow ではEntity を使って解決できます。

上画像のENTITY 欄にある「`@`child」がEntity 名となっており、「child」 Entity で定義された音声を認識できるようになっています。
![entity child](http://www.app-labo.net/blog_images/20171223_google_home/Entity_child.png)
child というEntity に"aiko"と"tarou"という2データが登録されています。
データ毎（この場合は子ども毎）に、呼び方の”揺れ”を登録できます。
例えば、"aiko"というデータを、”あいちゃん”や”あーちゃん”など呼び方で認識できるようになります。
下の"tarou"というデータは、”たろさん”や”たーぼう”などと呼びかけても認識してくれます。

# Intent：予定を印刷
次は予定を印刷するIntent です。
『ミミ子のただいまプリンター』は音声操作で、予定を印刷、もしくは親にメッセージ送信することができますが、それを判別するIntent となります。
![print.event_list](http://www.app-labo.net/blog_images/20171223_google_home/Intent_print.event_list.png)
ここでは、印刷するかどうかを判別する「`@`print_order」と、印刷したい日付を判別する「`@`sys.date」、2つのパラメータが定義されています。

「`@`print_order」は先ほどの「child」と同様に、アプリ開発者側で定義したEntity で、印刷指示の呼び方の揺れを吸収してくれます。
![entity print_order](http://www.app-labo.net/blog_images/20171223_google_home/Entity_print_order.png)
ここでは、”印刷して”だけでなく”予定を教えて”という呼び方でも予定の印刷指示と判別できるように設定しています。

もう一つの「`@sys.date`」はDialogflow 側であらかじめ用意されている定義済みEntity で、日付を判別してくれます。
”12月23日”という呼び方だけでなく、”今日”や”明日”、”来週の月曜日”といった曖昧な呼び方でも判別してくれ、Webhook 先では日付形式で受け取れます。
例として、”来週月曜日”と呼びかけた時には、"date"パラメータの値として"2017-12-25"という日付形式の値で受け取れます。

``` json:受け取れるデータ（一部）
textPayload:  "context: {
	"name": "current_child",
	"parameters": {
		"date.original": "来週月曜日",
		"date": "2017-12-25",
		"print_order.original": "予定を教えて",
		"print_order": "go_print",
		"child.original": "たろう",
		"child": "tarou"
	},
	"lifespan": 3
```

# Intent：メッセージ送信
”メッセージを送って”というフレーズと共にメッセージの送信先と、送信するメッセージ内容を判別するIntent です。
![send.message](http://www.app-labo.net/blog_images/20171223_google_home/Intent_send.message.png)
このIntent では親を判別する「`@`parent」と、メッセージ内容を表す「`@`sys.any」（呼びかけ全体を表す定義済みEntity）の2つのパラメータが定義されています。
両パラメータともに必須で定義しているので、「`@`parent」パラメータでメッセージ送信先となる親をEntity 「parent」の内容で判別した後に、メッセージ内容を確認するよう問いかけます。

Intent 定義内のFulfillment 欄の”Use webhook for slot-filling”項目にチェックを入れておく（上画像の赤枠内）と、必須パラメータの判別が不足している場合にもWebhook を実行することができます。
そうすることで、Google Home デフォルトの真面目な声でなく、”ミミ子”の可愛い声で問いかけることができます。

なお、必須パラメータの判別が不足している場合には、"${Intent 名}__dialog_context"という名のContext （ここでは'メッセージ送信_dialog_context'というContext 名）が渡されるので、それを基に判別できます。以下のコードを参考にしてください。

``` js:Webhook先のコード（一部）
exports.tadaimaprinter = (request, response) => {
  const app = new App({request, response});
  // Action名と処理するfunction を紐付け
  const actionMap = new Map();
  actionMap.set('input.welcome', handleInputWelcomeIntent);
  actionMap.set('recog.child', handleRecogChildIntent);
  actionMap.set('print.event_list', handlePrintEventListIntent);
  actionMap.set('end.conversation', handleEndConversationIntent);
  actionMap.set('send.message', handleSendMessageIntent);
  app.handleRequest(actionMap);
  // Intent: メッセージ送信
  function handleSendMessageIntent(app) {
    console.log('function handleSendMessageIntent called!');
    // 引数
    let target_parent = app.getArgument('parent');
    let send_message = app.getArgument('message');
    // 必要な情報が不足しているかどうか判定
    let dialog_context = app.getContext('メッセージ送信_dialog_context'); //メッセージ送信に必要な情報が不足している場合のみ存在するContext名
    let ask_message = '送りたいメッセージを教えてね。';
    let ask_message_ssml = '<speak><audio src="' + AUDIO_MESSAGE_WHAT_IS_THE_MESSAGE_TO_SEND_URL + '">' + ask_message + '</audio><break time="500ms" /></speak>';
    if (dialog_context) {
        console.log('メッセージ送信に必要な情報が不足しています。');
        if (!target_parent) {
            console.log('宛先となる親が特定されていません。');
            ask_message = '誰にメッセージを送るのか教えてね。';
            ask_message_ssml = '<speak><audio src="' + AUDIO_MESSAGE_WHO_IS_THE_MESSAGE_RECEPIENT_URL + '">' + ask_message + '</audio><break time="500ms" /></speak>';
        } else if (!send_message) {
            console.log('送信するメッセージが入力されていません。');
            let target_parent_message_url = AUDIO_MESSAGE_STORAGE_URL_PREFIX + 'to_' + target_parent + AUDIO_MESSAGE_STORAGE_URL_SUFFIX;
            ask_message_ssml = '<speak><audio src="' + target_parent_message_url + '">' + target_parent 
                + '</audio><audio src="' + AUDIO_MESSAGE_WHAT_IS_THE_MESSAGE_TO_SEND_URL + '">' + ask_message + '</audio><break time="500ms" /></speak>';
        }
        app.setContext('send_messsage', 5);
        app.ask({
            speech: ask_message_ssml,
            displayText: ask_message
        });
    } else {
    // ---------- メッセージ送信のコードは省略 ----------
    }
  }
// ---------- 後略 ----------
};
```
ここでは、メッセージ内容や送信先の親を聞き返す際に、SSML を使って”ミミ子”の可愛い声で問いかけるようにしています。
（定数 AUDIO_MESSAGE_WHO_IS_THE_MESSAGE_RECEPIENT_URL や、変数 target_parent_message_url には再生したいMP3 ファイルのURLが入ります）

# デモ動画
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">『ミミ子のただいまプリンター』デモ動画<a href="https://t.co/6C9UHoVDGM">https://t.co/6C9UHoVDGM</a></p>&mdash; Sumihiro Kagawa (@sumihiro3) <a href="https://twitter.com/sumihiro3/status/944176512227405825?ref_src=twsrc%5Etfw">2017年12月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 最後に
考えていたことは一通り実現できて、MASHUP AWARDS 2017 も終了しましたが、実際に運用してみると色々と課題が出てきました。
例えば、メッセージを送る時には一息のうちに喋らないとメッセージが送られてしまいます。予め送るメッセージを考えてから喋ればよいですが、子どもではそうはいきません。「これで送っていい？」と聞き返すなどの対策が要るようです。

amazon echo も入手したので、Alexa skill の開発もやってみたいですね～！