note_記事見出し_googlehome_1280_670___7

Google Home, Nest Hub向けにアクションを作ろう - 第7話: 簡易版動物しりとりの作成

この回から本格的なアクションにこのプロジェクトをしていきましょう。

私がGoogle Homeが出たばかりのころつくった「動物しりとり」というアクションあります。

これはGoogle Homeを相手に動物の名前を使ったしりとりができるというアクションなのですが、これに近いものを作って行きたいと思います。

不要な魚Entityの削除

DialogFlowのIntentsから、what_is_thisインテントを選択します。

Training phrasesから、「マグロって何?」と「サメはなんですか?」の例文を削除します。削除方法は、それぞれのフレーズを選択すると、右端にゴミ箱のアイコンがでてくるので、それをクリックします。

スクリーンショット 2020-01-29 19.48.52

するとその下のAction and parametersからfishエンティティが消えて下のようになっていることがわかります。

スクリーンショット 2020-01-29 19.49.07

これで、what_is_thisインテントは、動物(animal)か、それ以外(any)で言葉をピックアップするようになりました。(言い換えると魚を判定することはできなくなりました。)

もう少しだけTraining phrasesをいじります。

いま現在、「犬って何?」のような会話の受け皿になっていますが、動物しりとりでは、「犬」、「くま」のようにユーザーは動物名しか言わないため、いかのようにTraining phrasesを削ぎ落としてください。

スクリーンショット 2020-01-30 12.32.41


動物判定

まず、動物しりとりではもっとたくさんの動物を判定しますので、動物のEntityを強化しましょう。

ここに私が動物しりとりで使っているEntityをエクスポートしたものを公開しますので、こちらを使ってください。

Dialog FlowのEntitiesで、さきほどanimalエンティティを選択し、

そこから、右側の設定ボタン(ドットが縦に3つ並んだアイコンのボタン)を選択、ドロップダウンの中からSwitch to raw modeを選択します。

ここで、CSV形式でEntityの中身が見えるので、全部中身を一旦消し、上のanimal.csvの内容で置き換えてください。

これで、一気に250以上の動物を認識してくれるようになりました。

次にこの動物をFirebase Functionsで受けて、しりとりをする箇所を作って行きましょう。

今回、実際にしりとりのロジックを実装をするのは少し大変なので、簡易版のしりとりを実装します。

- 動物名をお互いに言い合う
- 言った動物がもうすで言われたものだったら負け
- 動物でないモノを言ったらまけ

という簡易ルールで実装します。

実際のしりとりは、

- 「ん」がついたらまけ、

- 語尾と同じ文字から始めないと行けない、

- 濁点、例えば「だ」で終わった場合濁点なしの「た」から始める動物でもオッケーにする

- 語尾の文字検出(漢字、カタカナ、ひらがなどれでも認識)

- カンガルーのように最後が伸びている場合は、ルで続ける

などのような多様なルールを実装するのが意外と複雑で、本ブログの趣旨であるActions on Googleの勉強とかけ離れてしまうので、簡易ルールでの実装ですすめたいと思います。

Firebase Functions側の作業

ここからはほとんどFirebase側の作業になります。

1.動物以外を言った場合「負けですよ」を言って終わる

2.動物だったら、すでに言った動物リストにあるかないかを確認する。すでに言ったリストにあったら負け(2回同じ動物が言えない)

3.ここまできて相手の負けじゃなかったら、動物リストから動物を1つピックアップして話す

4.話してみて、その動物がすでに言われた動物リストにあったらGoogle Homeの負け

というルールを実装してみましょう。

動物リストの準備

動物しりとり(簡易版)では、Entitiesで持っている動物のリストと同じリストをFirebase Functions側も持っている必要があります。

このリストをanimalsという変数でもつ、animal.jsというファイルをつくりました。中身はシンプルでいかのような内容になっています。

var animals = [
   "イヌ",
   "ウシ",
   "ウマ",
   "カバ",
   "クマ",
   "サイ",
  ...
  
   "ルーセットオオコウモリ"
];

module.exports = { animals: animals };​


これをindex.jsと同じフォルダの階層においてください。

これで、index.jsのコードの先頭で、下のようにanimal.jsをimportして、animalsという動物名の配列を取り出すことによって、Entityで定義した動物リスト同じ動物名(カタカナ)のリストanimalsをindex.jsの中から参照できるようになりました。

var animal = require('./animal');
var animals = animal.animals

ゲームルールの実装

まず、

1.動物以外を言った場合NGを言って終わる

ですが、下のように、animalがnullだったら動物Entityのキーワードがなかったということで、負けになり、負けですねというセリフを言ってアプリを閉じます

// 'what_is_this'というIntentのハンドラーの実装
app.intent('what_is_this', (conv, {anima, any}) => {
   if (animal) {
       ...
   }else {
       conv.close(any + 'は動物はありません。あなたの負けです');
       consumedAnimals.length = 0;
   }
});

次に、animalが入っていた場合、animalsのリストにも入っているかをチェックします。これは、Entitiesに動物を追加したが、animals.jsの中の動物リスト(animals)に追加を忘れた場合のケースにエラーを返すための実装です。

app.intent('what_is_this', (conv, {animal, any}) => {
   if (animal) {
       var result = animals.indexOf(animal);

       if(result === -1){
           //Entitiesに登録したアニマルが万が一animalsに登録されていなかった場合。
           //動物ではないとみなしゲームを終了する
           conv.close(animal + 'は動物はありません。あなたの負けです');
           console.error("Entityとして登録された動物で、animalsリストにない動物を発見しました。コードを修正してください。", animal);
       }

それでは、次のゲームルールを実装していきましょう。

2.動物だったら、すでに言われた動物リストにないかを確認する。

consumedAnimalsというリストに過去に言われた動物が入っていますので、このリストとパラメーターのanimalの値を比べてすでに言ったかどうかを調べます。

consumedAnimalsの中にanimalがあればconv.closeで相手の負けを伝えてアプリをとじます。

const consumedAnimals = []

// 'what_is_this'というIntentのハンドラーの実装
app.intent('what_is_this', (conv, {animal, any}) => {
   if (animal) {   
 
       ...    

       //過去に言われた動物を調べる
       result = consumedAnimals.indexOf(animal);
       if(result !== -1){
           conv.close(animal + 'は2回目ですね。あなたの負けです');
           consumedAnimals.length = 0;
           return
       }

       //ユーザーの動物が正しいものだったので、動物を登録
       consumedAnimals.push(animal);

consumedAnimalsに動物がなければ、ユーザーの動物が初めて言われた動物ということで、consumedAnimalsに今後同じ動物使えないように登録しておきます。consumedAnimals.push(newAnimal);の部分です。

次のルール、

3.ここまできて相手の負けじゃなかったら、動物リストから動物を1つピックアップして話す

を実装しましょう。

動物リストからランダムに動物を取り出し、ユーザーに返答します。

重要なのはここからで、まず動物の名前を話、その直後、この動物が今まで言った動物のリストにあるかどうかをさきほどと同じ方法でチェックします。

あれば、今度は自分の負けですので、負けを認めてアプリをクローズします。


app.intent('what_is_this', (conv, {animal, any}) => {
   if (animal) {
       
        ...

       //今度はAssistantの読み上げる動物をランダムに選ぶ
       let newAnimal = getRandomMember(animals);

       //過去に読み上げたアニマルを調べる
       result = consumedAnimals.indexOf(newAnimal);

       if(result !== -1){
           //Assistantが動物を答える
           conv.ask(newAnimal);

           //すでに応えた動物なので、あれ?、ととぼけた後負けを認める
           conv.close('あれ' + newAnimal + 'は2回目ですね。私の負けです');
           consumedAnimals.length = 0;
           return
       }

       ...

   }else {
       conv.close(any + 'は動物はありません。あなたの負けです');
       consumedAnimals.length = 0;
   }
});
「コアラ」、「あ、コアラは2回目ですね。私の負けです。」

というように、うっかり喋って負けちゃったというような会話をGoogle Homeにさせます。

ここまできて、ユーザーもGoogle Homeも負けていなければ、Google Homeに「コアラ」のように動物の名前を話させます。

app.intent('what_is_this', (conv, {animal, any}) => {
   if (animal) {
       ...

       //Assistantの選んだ動物が正しいものだったので、動物を登録
       consumedAnimals.push(newAnimal);

       //Assistantが動物を答える
       conv.ask(newAnimal);

   }else {
       conv.close(any + 'は動物はありません。あなたの負けです');
       consumedAnimals.length = 0;
   }
});

Google Homeの喋った動物も、consumedAnimalsに登録しておきます。

そして、このあとまたユーザーの次の言葉を待つモードに入ります。

これで簡単な動物しりとり、というより動物の言い合いゲームが完成しました。

下が最終的なindex.jsのコードになります。


'use strict';

// Actions on Google client libraryから、Dialogflowモジュールをインポートする
const {dialogflow} = require('actions-on-google');

// firebase-functions packageをインポートする
const functions = require('firebase-functions');

var animalModule = require('./animal');
var animals = animalModule.animals

// Dialogflow clientインスタンスを作る
const app = dialogflow({debug: true});

const consumedAnimals = []

// 'what_is_this'というIntentのハンドラーの実装
app.intent('what_is_this', (conv, {animal, any}) => {
   if (animal) {
       var result = animals.indexOf(animal);

       if(result === -1){
           //Entitiesに登録したアニマルが万が一animalsに登録されていなかった場合。
           //動物ではないとみなしゲームを終了する
           conv.close(animal + 'は動物はありません。あなたの負けです');
           console.error("Entityとして登録された動物で、animalsリストにない動物を発見しました。コードを修正してください。", animal);
       }

       //過去に言われた動物を調べる
       result = consumedAnimals.indexOf(animal);
       if(result !== -1){
           conv.close(animal + 'は2回目ですね。あなたの負けです');
           consumedAnimals.length = 0;
           return
       }

       //ユーザーの動物が正しいものだったので、動物を登録
       consumedAnimals.push(animal);

       //今度はAssistantの読み上げる動物をランダムに選ぶ
       let newAnimal = getRandomMember(animals);

       //過去に読み上げたアニマルを調べる
       result = consumedAnimals.indexOf(newAnimal);

       if(result !== -1){
           //Assistantが動物を答える
           conv.ask(newAnimal);

           //すでに応えた動物なので、あれ?、ととぼけた後負けを認める
           conv.close('あれ' + newAnimal + 'は2回目ですね。私の負けです');
           consumedAnimals.length = 0;
           return
       }

       //Assistantの選んだ動物が正しいものだったので、動物を登録
       consumedAnimals.push(newAnimal);

       //Assistantが動物を答える
       conv.ask(newAnimal);

   }else {
       conv.close(any + 'は動物はありません。あなたの負けです');
       consumedAnimals.length = 0;
   }
});


function getRandomMember(array){
   let length = array.length;
   if(length === 0){
       return null;
   }

   if(length === 1){
       return array[0];
   }

   let index = getRandomInt(0, length - 1);
   return array[index];
}

function getRandomInt(min, max) {
   return Math.floor(Math.random() * (max - min + 1)) + min;
}


// DialogflowApp オブジェクトをHTTPS POSTリクエストのハンドラーとして登録
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

(上で添付したanimals.jsをインポートするのを忘れずに!)

それでは、

firebase deploy

を実行して、このコードをディプロイし、

DialogFlowのIntegrationsメニューから、Actions on Googleシミューレータを起動し、動作確認してみてください。

下のように動物言い合いゲームができれば成功です!

スクリーンショット 2020-01-30 12.44.28

お疲れさまでした!

今回作った動物言い合いゲームは山手線ゲームの動物版のようなものですので、animalsの辞書とEntityを変更して、山手線ゲームや芸能人の名前ゲームなどをつくることもできます。また、しりとりの判定ロジックを入れれば動物しりとりにも変更できるのでチャレンジしてみてください。

本日のまとめ

- 不要となる魚のEntityを削除しました。

- また、Training Phrasesを「犬」と「ばけつ」の2つに修正しました。

- 動物判定のためのanimal Entityに250以上の動物をいれて、動物判定の幅を拡大しました。

- Firebase FunctionsプロジェクトにEntityと同じ動物辞書animals.jsを追加しました。

- Index.jsに山手線ゲーム動物版とも言える動物言い合いっこゲームのロジックを実装しました。

- Firebaseにコードをディプロイし、Actions on Googleシミュレーターで動作を確認しました。


おまけ:デバッグの手順

実は、この実装をやっている間に渡しもいくつかハマってしまい、デバッグを行いました。バグの例と、原因の調べ方の参考になるかと思いましたので、ここで紹介します。

下のように、動物以外のもの「サメ」を入れたときにちゃんと負けになるかをテストしたときのことですが、以下のような状況になりました。

スクリーンショット 2020-01-29 19.40.57

Webhook側で問題があると上のように、「すみません、テスト用アプリから応答がありません。後ほどもう一度試してください。」という返答が返ってきます。(Actionの開発者であれば最も聞きたく無いセリフでしょう笑)

このようなときには、右の画面のERRORSというところにエラーの種類が表示されます。
Malformed Response
Webhook call failed. Error: UNAVAILABLE
などの文字が読み取れ、Webhook (Firebase Functions)側で問題が置きていることがわかります。

次にRESPONSEというタブを見てみましょう。ここでもエラーの内容が確認できます。

画像6

またDEBUGタブでは、よし詳細なこのトランザクションのリクエスト・レスポンスの内容が見えるため、よく読むとERRORSやRESPONSEではわからなかった情報がわかることがあります。

画像7

上のエラーの内容からWebhook側に問題があることがわかりました。

このようなときはFirebase Consoleに飛び、自分のプロジェクト(私の場合parrot)を開きます。

左のメニューからFunctionsを選び、右に表示されたページの中でログというタブを開きます。

スクリーンショット 2020-01-30 13.00.43

ここに、Firebase Functionsが受け付けたリクエストのログを見ることができます。

今回エラーがあったときはしたのように、1つだけ赤い△+!マークのエラーログがありました。

画像9

ここをクリックしてみると下のように詳細がでてきます。

画像10

any is undefined

というエラーが出ていてピンときました。

anyをIntentのハンドラーのパラメータから消してしまっていたことが原因でした。

app.intent('what_is_this', (conv, {animal, any}) => {

上のように記述しなければならないところをfishパラメータを消す際、勢いでanyパラメータも消してしまって下のようになっていました。

app.intent('what_is_this', (conv, {animal}) => {

この状態でハンドラーのなかでanyにアクセスしていたので、any is undefinedというエラーがでていました。

このように、問題が置きたときは、

1.DialogFlowのERRORS, RESPONSE, DEBUGタブのログをみて原因を推測する。

2.原因がWebhookにありそうであれば、Firebase Functionsのログタブから原因の詳細を見る

という手順でデバッグをしてみてください。


このブログに関する質問やActions on Googleの開発の相談はこちらから↓↓↓

@mizutory
mizutori@goldrushcomputing.com

次回はアクションにグラフィカルユーザーインターフェースを持たせ、Google Nest HubなどのスマートディスプレイやAndroidスマートフォンにGUIを表示する方法を説明します↓↓↓


この記事が気に入ったらサポートをしてみませんか?