この記事は Livesense - 自 Advent Calendar 2017 20日目の記事です。
前置き
娘のクイズ
私には5歳の娘がいるのですが、ときどきポケモンクイズを出してくれます。
「"あ"の付くポケモンなーんだ」と聞かれて、
「アーボック」と答えると、「残念、アンノーンでした」とか
「アンノーン」と答えると、「残念、アリアドスでした」と
気分次第で答えが変わる難解なクイズです。
Google Home Mini がやってきたぞ
そんな我が家に、Google Home miniがやってきました。
さっそく、娘のおもちゃになったのですが、なんと質問すれば良いのか分からないので、ひたすら「明日の天気は?」と聞いています。
さすがに連続で明日の天気を聞かせられるのが辛い(もとい娘にもっと楽しく質問させたい)のと、理不尽なポケモンクイズを改善するために、Google Home Miniに「◯のつくポケモンは?」と聞いたらポケモンを教えてくれる仕組みを作ることにしました。
構成
今回は、DialogflowからAWS Lambdaで作成したAPIを呼ぶ形で下記のような構成を考えました。
Dialogflow
まず、Dialogflowの設定を行います。
今回は、画面のIntents
、Entities
、Fulfillment
を使用しました。
Entities
Entitiesを使用すると、同じ意味を持つ単語、例えば、「犬」でも「イヌ」でもどちらも認識するように設定することができます。
今回は、「◯のつく〜」の一文字目を抽出したいので、単語ではないのですが、ひらがなでもカタカナでも引っかかるようにしたかったので使用しました。
こんな感じで50音セットして、「ア」でも「あ」でも「ア」と認識するようにしました。
(カタカナにしたのは、単純にポケモンがカタカナで後の処理が楽だったからです)
Intents
続いて、Intentsの設定です。
この画面で、実際に話しかける内容を設定します。
Example mode
2種類の設定方法がありますが、デフォルトではExample mode
になっています。
まずは愚直に「ア のつくポケモンは」と入れてみます。
色のついたところが単語認識したところですが、なんということでしょう、Entity
が一文字なのでほぼ全て認識してしまいました。
不要な箇所はクリックで消していけるのですが、かなり面倒なのと、このままだと50パターン入れないといけないのでもう一つのTemplate mode
を使います。
Template mode
Template mode
に切り替えるには、同じ入力フォームの端にある、ダブルクォートをクリックします。
@
に切り替わればOKです。
Template mode
だと、Entity
に該当する箇所を、動的に埋め込むことができます。
@
を入れると補完されます。今回はEntity
をkana
という名前にしたので下記のようになりました。
先ほどと違って、頭文字だけが認識されました。
また、このEntity
で50音全てカバーできるので、あとは考えられるパターンの文章を入力していけばOKです。
最終的に下記のような文章を登録しました。
Fulfillment
Fulfillment
を使用するとWebhook
の設定から外部のAPIを呼び出すことができます。
今回は、Lambda
で作ったAPIを呼びたいので、そのURLと必要なヘッダ情報を設定しています。
テスト
ここまでできると、右カラムにあるフォームから実際に文章を投げて、どのように文章解析されるか、また実際にやり取りされるJSON
を確認することができます。
下部にある、kana
が認識されたEntity
です。
「あ のつくポケモンは」とひらがなで聞きましたが、Entities
の設定通りちゃんとカタカナに置き換わっています。
いったんここまでで、Dialogflow
の設定は終わりです。
ポケモンを探せるようになるまでもう一息です。
Lambda
次に、実際にポケモンを探して返すAPIをLambda
関数を使って実装します。
Lambda
関数を新規作成して、API Gateway
を下記の図のように配置します。
※今回API Gateway
の詳細は割愛します。
今回は、Lambda
関数をNode.js
で実装しました。
APIとしてのレスポンス
まず、Dialogflow
とか関係なしに、APIとしてレスポンスを返せるようにします。
Lambda
関数を新規作成した直後は、下記のようになっています。
exports.handler = (event, context, callback) => {
// TODO implement
callback(null, 'Hello from Lambda');
};
ただ、このままだと、API Gateway
でエラーになってしまうので、適切なレスポンスを返すように変更します。
フォーマットはこの辺りを参考にしました。
callback(null, {
"statusCode": 200,
"body": json_data
})
これでAPI Gateway
に適切にレスポンスが返せるようになります。
Dialogflowへのレスポンス
続いて、Dialogflow
に対してのレスポンスですが、こちらはAPIの返却値で、body
に含まれるJSON
のデータがそれにあたります。
こちらもフォーマットがあるのでそちらを参考にします。
最低限、speech
とdisplayText
というパラメータを返せれば良さそうなので、ここに返したいメッセージ入れています。
以下が、メッセージテキストから、レスポンスに含めるJSON
を作成するメソッドです。
const build_callback_data = (message) => {
const json = {
speech: message,
displayText: message
}
return JSON.stringify(json)
}
exports.handler = (event, context, callback) => {
const message = "こんにちは"
callback(null, {
"statusCode": 200,
"body": build_callback_data(message)
})
};
実装の詳細
これで好きなメッセージを返せるようになったので、あとはポケモンを探して返すだけです。
まず、Dialogflow
から渡されるパラメータに頭文字が入っているので取得します。
パラメータは、event
という引数のbody
から取得できます。
ただし、event.body
はJSON文字列なので、一度parse
を挟む必要があります。
具体的には下記のようになります。
const parameters = JSON.parse(event.body).result.parameters
const kana = parameters && parameters.kana ? parameters.kana : null
これで頭文字が取れたので、次に検索をしますが、今回はかなり雑で、同じLambda
関数内に、あらかじめ用意しておいたJSONデータからランダムに取得するという形を取りました。
ここまで書いておいてなんですが、私はポケモンにあんまり詳しくないので、元データは ポケモンWiki から引用させていただきました。
exports = module.exports = {
"ア":["アイアント","アギルダー","アクジキング",...],
"イ":["イシズマイ","イシツブテ","イトマル",...],
"ウ":["ウインディ","ウォーグル","ウソッキー",...],
"エ":["エアームド","エイパム","エテボース",...],
"オ":["オオスバメ","オオタチ","オクタン",...],
//...
}
最終的なコードは多少のエラーチェックも加えて、下記のようになりました。
const pokemonList = require("data/pokemon")
const build_callback_data = (message) => {
const json = {
speech: message,
displayText: message
}
return JSON.stringify(json)
}
exports.handler = (event, context, callback) => {
const parameters = JSON.parse(event.body).result.parameters
const kana = parameters && parameters.kana ? parameters.kana : null
if (kana === null || !pokemonList[kana]) {
const message = "すいません、見つかりませんでした。"
callback(null, {
"statusCode": 200,
"body": build_callback_data(message)
})
return
}
const suggestPokemonList = pokemonList[kana]
const suggestPokemon = suggestPokemonList[Math.floor(Math.random() * suggestPokemonList.length)]
const message = `${suggestPokemon}`
callback(null, {
"statusCode": 200,
"body": build_callback_data(message)
})
};
テストをする
これで準備が整ったので、実際にテキストを投げてテストをしてみます。
まず、Dialogflowから投げてみます。
Dialogflowからテスト
実際に投げてみた結果です。
ポケモンの名前が返ってきているのが分かります。
ランダムに返しているので実行するたびに結果は変わります。
Actions on Googleからテスト
続いて、Actions on GoogleのSimulatorからテストをします。
テキストでも入力できますが、音声でもテストをすることができます。
テスト用アプリにつなぐには、「テスト用アプリにつないで」と言います。
また、Google Home実機でも同じようにテスト用アプリにつなぐことができます。
ちなみに、最初実機でテスト用アプリにつないだら、戻し方が分からなくてちょっと焦りました。
「やめたい」と言うとテストをやめることができます。
質問を投げてみます。
成功です!
音声でやると下記のように文章解析している様子が見えておもしろいです。
娘でテスト
完成したので、Google Home Miniからも試してみました。
私が試すと、感覚的に7〜8割、意識して話すとほぼ確実に返ってくるようになったので、娘に話してみてもらいました。
が、娘はちょっと舌たらずなところがあり、全く認識しない!
「なんでお父さんだけ返事してくれるの 」と悲しい結果となりました。。
「お父さんすごーい」というのを期待していたので残念です(´・ω・`)
まとめ&反省点&改善点
-
Dialogflow
とLambda
で、Google Home Mini
に好きに喋らせることができました。 -
Google Home Mini
なのでGoogle Cloud Functions
使った方が親和性あるのだろうか?とも思ったけど、Lambda
でもシンプルに書けると分かって良かったです。 - ある程度、予測変換が入るので、一文字認識させるが難しかったです。
- ポケモンデータを
Lambda
内に持ってしまったので、DynamoDBとかに入れられると良かったです(今回は時間切れ) - 最初、
Intents
の登録で「あのつくポケモンは?」と最後にクエスチョンを付けていたのですが、これだとテキストでテストしているうちは問題なかったのですが、音声入力した際に?
がつかないので認識しないというのに少々はまりました。音声で早めにテストをするのがおすすめです。 - 最後に、娘があんまり喜んでくれなかったのが心残りです(お父さんは精進します)。