Edited at

Google Home と Lambda で「"あ"のつくポケモンなーんだ」

More than 1 year has passed since last update.

この記事は Livesense - 自 Advent Calendar 2017 20日目の記事です。


前置き


娘のクイズ

私には5歳の娘がいるのですが、ときどきポケモンクイズを出してくれます。

「"あ"の付くポケモンなーんだ」と聞かれて、

「アーボック」と答えると、「残念、アンノーンでした」とか

「アンノーン」と答えると、「残念、アリアドスでした」と

気分次第で答えが変わる難解なクイズです。


Google Home Mini がやってきたぞ

そんな我が家に、Google Home miniがやってきました。

さっそく、娘のおもちゃになったのですが、なんと質問すれば良いのか分からないので、ひたすら「明日の天気は?」と聞いています。

さすがに連続で明日の天気を聞かせられるのが辛い(もとい娘にもっと楽しく質問させたい)のと、理不尽なポケモンクイズを改善するために、Google Home Miniに「◯のつくポケモンは?」と聞いたらポケモンを教えてくれる仕組みを作ることにしました。


構成

今回は、DialogflowからAWS Lambdaで作成したAPIを呼ぶ形で下記のような構成を考えました。

AWS Icons.png


Dialogflow

まず、Dialogflowの設定を行います。

今回は、画面のIntentsEntitiesFulfillmentを使用しました。

Dialogflow.jpg


Entities

Entitiesを使用すると、同じ意味を持つ単語、例えば、「犬」でも「イヌ」でもどちらも認識するように設定することができます。

今回は、「◯のつく〜」の一文字目を抽出したいので、単語ではないのですが、ひらがなでもカタカナでも引っかかるようにしたかったので使用しました。

Dialogflow.jpg

こんな感じで50音セットして、「ア」でも「あ」でも「ア」と認識するようにしました。

(カタカナにしたのは、単純にポケモンがカタカナで後の処理が楽だったからです)


Intents

続いて、Intentsの設定です。

この画面で、実際に話しかける内容を設定します。


Example mode

2種類の設定方法がありますが、デフォルトではExample modeになっています。

まずは愚直に「ア のつくポケモンは」と入れてみます。

Dialogflow.jpg

色のついたところが単語認識したところですが、なんということでしょう、Entityが一文字なのでほぼ全て認識してしまいました。

不要な箇所はクリックで消していけるのですが、かなり面倒なのと、このままだと50パターン入れないといけないのでもう一つのTemplate modeを使います。


Template mode

Template modeに切り替えるには、同じ入力フォームの端にある、ダブルクォートをクリックします。

Dialogflow.jpg

@に切り替わればOKです。

Dialogflow.jpg

Template modeだと、Entityに該当する箇所を、動的に埋め込むことができます。

@を入れると補完されます。今回はEntitykanaという名前にしたので下記のようになりました。

Dialogflow.jpg

先ほどと違って、頭文字だけが認識されました。

また、このEntityで50音全てカバーできるので、あとは考えられるパターンの文章を入力していけばOKです。

最終的に下記のような文章を登録しました。

Dialogflow.jpg


Fulfillment

Fulfillmentを使用するとWebhookの設定から外部のAPIを呼び出すことができます。

今回は、Lambdaで作ったAPIを呼びたいので、そのURLと必要なヘッダ情報を設定しています。

Dialogflow.jpg


テスト

ここまでできると、右カラムにあるフォームから実際に文章を投げて、どのように文章解析されるか、また実際にやり取りされるJSONを確認することができます。

Dialogflow.jpg

下部にある、kanaが認識されたEntityです。

「あ のつくポケモンは」とひらがなで聞きましたが、Entitiesの設定通りちゃんとカタカナに置き換わっています。

いったんここまでで、Dialogflowの設定は終わりです。

ポケモンを探せるようになるまでもう一息です。


Lambda

次に、実際にポケモンを探して返すAPIをLambda関数を使って実装します。

Lambda関数を新規作成して、API Gatewayを下記の図のように配置します。

※今回API Gatewayの詳細は割愛します。

Lambda_Management_Console.png

今回は、Lambda関数をNode.jsで実装しました。


APIとしてのレスポンス

まず、Dialogflowとか関係なしに、APIとしてレスポンスを返せるようにします。

Lambda関数を新規作成した直後は、下記のようになっています。

exports.handler = (event, context, callback) => {

// TODO implement
callback(null, 'Hello from Lambda');
};

ただ、このままだと、API Gatewayでエラーになってしまうので、適切なレスポンスを返すように変更します。

フォーマットはこの辺りを参考にしました。

- http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html

callback(null, {

"statusCode": 200,
"body": json_data
})

これでAPI Gatewayに適切にレスポンスが返せるようになります。


Dialogflowへのレスポンス

続いて、Dialogflowに対してのレスポンスですが、こちらはAPIの返却値で、bodyに含まれるJSONのデータがそれにあたります。

こちらもフォーマットがあるのでそちらを参考にします。

- https://developers.google.com/actions/reference/v1/dialogflow-webhook#response

最低限、speechdisplayTextというパラメータを返せれば良さそうなので、ここに返したいメッセージ入れています。

以下が、メッセージテキストから、レスポンスに含める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からテスト

Dialogflow.png

実際に投げてみた結果です。

ポケモンの名前が返ってきているのが分かります。

ランダムに返しているので実行するたびに結果は変わります。


Actions on Googleからテスト

続いて、Actions on GoogleのSimulatorからテストをします。

テキストでも入力できますが、音声でもテストをすることができます。

テスト用アプリにつなぐには、「テスト用アプリにつないで」と言います。

また、Google Home実機でも同じようにテスト用アプリにつなぐことができます。

ちなみに、最初実機でテスト用アプリにつないだら、戻し方が分からなくてちょっと焦りました。

「やめたい」と言うとテストをやめることができます。

Actions_on_Google.png

質問を投げてみます。

Actions_on_Google.png

成功です! :thumbsup:

音声でやると下記のように文章解析している様子が見えておもしろいです。

googlehome2.gif


娘でテスト 

完成したので、Google Home Miniからも試してみました。

私が試すと、感覚的に7〜8割、意識して話すとほぼ確実に返ってくるようになったので、娘に話してみてもらいました。

が、娘はちょっと舌たらずなところがあり、全く認識しない!

「なんでお父さんだけ返事してくれるの :cry:」と悲しい結果となりました。。

「お父さんすごーい」というのを期待していたので残念です(´・ω・`)


まとめ&反省点&改善点



  • DialogflowLambdaで、Google Home Miniに好きに喋らせることができました。


  • Google Home MiniなのでGoogle Cloud Functions使った方が親和性あるのだろうか?とも思ったけど、Lambdaでもシンプルに書けると分かって良かったです。

  • ある程度、予測変換が入るので、一文字認識させるが難しかったです。

  • ポケモンデータをLambda内に持ってしまったので、DynamoDBとかに入れられると良かったです(今回は時間切れ)

  • 最初、Intentsの登録で「あのつくポケモンは?」と最後にクエスチョンを付けていたのですが、これだとテキストでテストしているうちは問題なかったのですが、音声入力した際に?がつかないので認識しないというのに少々はまりました。音声で早めにテストをするのがおすすめです。

  • 最後に、娘があんまり喜んでくれなかったのが心残りです(お父さんは精進します)。


参考