こんにちは。GMOソリューションパートナーのH.Tです。
Google Homeを買いました。
リビングにBluetoothスピーカーがほしいなあと思っていた矢先、
半額セールをしていたので会社の昼休みに衝動買いしてしまいました。
因みに社内の他の方はみなさんAmazon Echo派の方が多いですね。
色々試してみて便利だったのですが、しりとりができなかったんです。
僕はGoogle Homeにしりとりの相手をしてほしかったのですができないようです。
正確に言いますとやってみるもののなぜか速攻で「ん」を付けて自爆してしまうんです。
相手すんのめんどくさいってことでしょうか。
だったら。Google Assistantに本気のしりとりというものををたたきこんでやる!
と、アプリを作ってみることにしました。
Google Homeとはご存じのとおりスマートスピーカーで
中身はGoogle AssistantというAIになっています。
そのGoogle Assistantから呼び出されるサードパーティのアプリは
Actions On Googleというプラットフォームから作ることができます。
こちらをご一読いただくと概要がつかめるかと思います。
Google Home アプリ開発の流れ
以下、今回行ったアプリ開発の簡単な流れとなります。
1.Actions On Google
→プロジェクトを作る。
2.Dialogflow
→会話の設計、
アプリの実装(FirebaseというMBaasが使われます。)
3.Firebase
→アプリのログ、データベースの確認など
(Firebase Functions→ロジック)
(Firebase Databse→DB)
今回作ったしりとりアプリの概要
※Googleのチュートリアルを参考に筆者が作成
1.Actions On Goole
から新規プロジェクトを作成します。
「ACTIONS CONSOLE」をクリックします。
↓ Add/import projectをクリックします。
↓ プロジェクトネームを入れてリージョンを選択して「CREATE PROJECT」をクリックします。
↓ 今回はDialogflowでアプリをつくりました。Dialogflowの「BUILD」をクリックします。
↓ ポップアップが開いたら「CREATE ACTIONS ON DIALOGFLOW」をクリックします。
この後はDialogflowでの作業になります。
2.Dialogflow
Dialogflowは音声認識の処理からアプリをトリガーするところを設定できます。
アプリのソースコード自体もInline Editorという画面で編集することができます。
(今回はInline Editorは使わずローカルで開発、デプロイしました。)
まずはAgentの作成になります。(AgentとはDialogflowでのプロジェクト)
「CREATE」をクリックします。
↓ 次にインテントの一覧画面になります。
・Intent
Intentは処理のトリガーとなる言葉とそれに紐づく処理を定義します。
デフォルトでアプリ呼び出し用のウェルカムインテントと
会話が途切れた時のフォールバックインテントが存在します。
今回作ったしりとりアプリにはデフォルトのインテントに加えて下記を追加しました。
・shiritori メイン処理用
・shiritoriOwari 「しりとり終わり」と言われた時の処理用
・end 「やめる」とか「おわる」とかで中断したとき用
↓ しりとりアプリのウェルカムはこのようになっています。
呼び出しワードをいろいろ登録しましたw 「User Says」の欄に入力してエンターで登録されます。
「Action」が呼び出し関数のキーとなります。デフォルトのままで大丈夫です。
fulfillmentのwebhookにチェックを入れましょう。
(後述のフルフィルメントの設定をしないとここの選択肢が出ません。フルフィルメントを作成したら戻ってきましょう。)
↓ shiritoriのインテントです。
「User says」に「めだか」と入れてありますが何でもいいです。
「Action」を「shiritori」とました。これが呼び出し関数のキーとなります。
「User says」の「めだか」をダブルクリックするとメニューが出てきますので「@sys.any」を選んで画像のようになればOKです。
フルフィルメントは前述のとおりです。
↓ endのインテントです。
終わりたいときの単語を登録しText responseにグーグルアシスタントの返事を入力します。
↓ shiritoriOwariのインテントです。
画像のとおり設定します。
ここまででインテントの設定は一通り終わりです。
これだけでも発話のテストができます。「Try it now」に入力してみましょう。
・Entities
会話の中で抽出したい単語と表記ゆれを定義します。
(今回は使わないので説明は省きます。)
・Fulfillment(ドキュメント)
実行処理の定義です。WebhookかInline Editorをのどちらかを選べます。
ここでwebhookの設定をします。
Inline Editorは使わないのですが、アプリのソースコードのベースにしたいのでコピーして保存しておきます。
その後Inline EditorのスライダーをDISABLEDにします。
WebhookにダミーのURLを入れて(まだデプロイしてないので)saveしておきます。(これでインテントのwebhookが選択できるようになります。)
さあ。やっとここからソースコードを書き始めます。
今回はローカルでの開発をご紹介します。
まずはお使いのPCかMacにnode.js(v6.11.1)をインストールしてください。
それから任意のディレクトリを切って移動します。
そしたらfirebaseようの初期設定をしていきます。
1 2 3 |
npm install -g firebase-tools firebase login firebase init functions |
JavaScriptかTypeScriptかとか聞かれますので適宜選択します。
うまくいったら、functionsフォルダが作成されますので下記の通り実行します。
1 2 3 4 |
cd ./functions npm install --save firebase-admin npm install --save actions-on-google npm install --save kuromoji |
しりとりの処理に形態素解析が必要だったのでKuromojiを入れています。
そしたら./functions/index.jsを編集します。
ソースきたないですがこんなです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
const functions = require('firebase-functions'); // Cloud Functions for Firebase library const DialogflowApp = require('actions-on-google').DialogflowApp; // Google Assistant helper library const admin = require("firebase-admin"); admin.initializeApp(functions.config().firebase); const db = admin.database(); const kuromoji = require('kuromoji'); const kuromojiBuilder = kuromoji.builder({dicPath: 'node_modules/kuromoji/dict/'}); exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => { if (request.body.result) { processRequest(request, response); } else { return response.status(400).end('Invalid Webhook Request'); } }); function processRequest (request, response) { let action = request.body.result.action; // https://dialogflow.com/docs/actions-and-parameters const app = new DialogflowApp({request: request, response: response}); //db reference const statusRef = db.ref("/shiritori/status"); const dictionaryPath = "/shiritori/dictionary/"; let tell = ""; let status = { lastChar: 'め', preWords: [ 'しりとりはじめ' ] } const actionHandlers = { 'input.welcome': () => { statusRef.set(status); tell += "本気のしりとりですね。私から行きます。しりとり始め。"; response.json({"speech": tell, "displayText": tell}); }, 'shiritoriOwari': () => { let shiritoriWord = app.getArgument("shiritoriWord"); statusRef.once("value", function(snapshot){ status = snapshot.val(); if(status['lastChar'] == 'し' && shiritoriWord == 'しりとり終わり'){ shiritoriEnd(2,0); }else{ shiritoriEnd(3,0); } }); }, 'shiritori': () => { //しりとりワード let shiritoriWord = app.getArgument("shiritoriWord"); const shiritoriWordOrigin = shiritoriWord; console.log("shiritoriWord"); console.log(shiritoriWord); //形態素解析 kuromojiBuilder.build(function(err, tokenizer){ if (err) { console.log(err); tell += err; response.json({"speech": tell, "displayText": tell}); response.status(200).end(); } console.log("tokenize start"); const tokens = tokenizer.tokenize(shiritoriWord); console.log("tokenize end"); if(checkToken(tokens.length, tokens[0]['word_type']) === false) shiritoriEnd(0,1); // 読み仮名に変換、以降読み仮名で扱う shiritoriWord = tokens[0]['reading']; // 末尾の長音記号を削除 if(shiritoriWord.slice(-1) == "ー"){ shiritoriWord = shiritoriWord.slice(0,-1); } // ひらがなに変換 shiritoriWord = katakanaToHiragana(shiritoriWord); console.log('shiritoriWord to hiragana'); console.log(shiritoriWord); // ステータス取得 statusRef.once("value", function(snapshot){ status = snapshot.val(); //人間 console.log("ningen"); console.log(status['lastChar']); //チェック if (status['lastChar'] == 'し' && shiritoriWord == 'しりとり終わり') shiritoriEnd(2,0); if (checkWord(shiritoriWord,status,0) === false) shiritoriEnd(0,0); status['preWords'].unshift(shiritoriWord); status['lastChar'] = komojiToOmoji(shiritoriWord.slice(-1)); //ステータス更新 console.log(status); statusRef.set(status); //googlehome console.log("google home "); tell += shiritoriWordOrigin+"の、「"+status['lastChar']+"」、ですね。私の番です。"; //辞書検索 db.ref(dictionaryPath+status['lastChar']).once("value",function(snapshot){ const dw = snapshot.val(); console.log(dw); if (dw === null) { tell += "うーん。思いつかないです。"; shiritoriEnd(1,0); } dw.sort(function(){ return Math.random() - .5;}); console.log(dw); const dictWord = dw[0]; console.log("dictword"); console.log(dictWord); tell += dictWord+"。"; //チェック if (checkWord(dictWord,status,1) === false) shiritoriEnd(1,0); status['preWords'].unshift(dictWord); status['lastChar'] = dictWord.slice(-1); //ステータス更新 console.log(status); statusRef.set(status); response.json({"speech": tell, "displayText": tell}); }); }); }); } } actionHandlers[action](); //チェック const checkToken = (length, word_type) => { console.log('checkToken'); console.log(length); console.log(word_type); if(length > 1 || word_type === 'UNKNOWN'){ tell += "すみません、私が知らない言葉のようです。わかり易い「名詞」でお願いします。" return false; } return true; } const katakanaToHiragana = (st) => { return st.replace(/[\u30a1-\u30f6]/g, function(match) { const chr = match.charCodeAt(0) - 0x60; return String.fromCharCode(chr); }); } const komojiToOmoji = (st) => { komojiMap = {"ぁ":"あ", "ぃ":"い", "ぅ":"う", "ぇ":"え", "ぉ":"お", "ゃ":"や", "ゅ":"ゆ", "ょ":"よ", "ゎ":"わ"}; if(st in komojiMap){ return komojiMap[st]; } return st; } const checkWord = (sw,st,player) => { //前に出てきたかチェック console.log("preword check"); console.log(st['preWords'].indexOf(sw)); if (st['preWords'].indexOf(sw) >= 0){ if(player === 0){ tell += "前に出てきた単語です。"; }else{ tell += "これ、前に出てきましたっけ。"; } return false; } //んチェック console.log("nn check"); const lc = sw.slice(-1) ; console.log(lc); if (lc == "ん") { if(player === 0){ tell += "「ん」、が最後にきましたよ。"; }else{ tell += "…あーー。「ん」、つけちゃった。"; } return false; } //最後の文字が繋がってるかチェック console.log("saigo check"); console.log(st['lastChar']); console.log(sw.slice(0,1)); if(st['lastChar'] != sw.slice(0,1)){ tell+="前の単語の最後の文字は、「"+st['lastChar']+"」、ですよ。"; return false; } //一文字チェック console.log("one check"); console.log(sw.length); if(sw.length <= 1){ tell +="一文字はダメですよ。ズルです。"; return false; } return true; }; const shiritoriEnd = (flg, again) => { if(again === 0){ if(flg === 0){ tell += "あなたの負けです。"; }else if(flg === 1){ tell += "私の負けです。"; }else if(flg === 2){ tell += "うっかりしました。あなたの勝ちです。"; }else if(flg === 3){ tell += "騙そうとしても無駄ですよ。あなたの負けです。" } tell +="もう一回やりましょう。しりとり始め。" status = {lastChar: 'め', preWords: [ 'しりとりはじめ' ]}; statusRef.set(status); } response.json({"speech": tell, "displayText": tell}); exit; }; } |
編集したらデプロイします。
1 |
firebase deploy --only functions |
ここまででうまくいったらfirebaseのコンソールに行ってみましょう。
ログインしてコンソールに移動します。
左メニューの「Database」を選択して
データベースにしりとり辞書用のマスターデータをインポートします。
辞書の作成はこちらのAPIを使わせていただきました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "shiritori" : { "dictionary" : { "あ" : [ "あたり", "あられ", "あらいずみるい", "ありあり", "ありなし", "あきたこまち", "あん", "あやめ" ], "い" : [ "いつもここから", "いちご" ], ...つづく }, "status" : { "lastChar" : "め", "preWords" : [ "しりとりはじめ" ] } } } |
こんな感じで辞書のマスターデータと状態保持のためのjsonデータを登録しました。
これでdatabaseとfunctionがすべて揃ったのでdialogflowのフルフィルメントにWebhookを設定してあげます。
これでアプリは動くはずです。
ソースコードに関する説明がほとんどできず申し訳ありません。
ドキュメントをご参考ください。
Dialogflowに戻ります。
Integrationsを選択し、Google Assistantの「INTEGRATION SETTINGS」をクリックします。
↓「Implicit invocation」にインテントを入力して「TEST」をクリックします。
↓するとActions on GoogleのコンソールのSimulator画面に飛ぶのでこちらのweb画面上から動作テストができます。
また、この状態で「テスト用アプリにつないで」とGoogle Homeに言うと実機テストもできます。
あとはFirebaseコンソールに行ってログなどを確認しましょう。
3.Firebase
コンソールからログやアクセス状況などを確認できます。
リアルタイムデータベースなのでGUI上で更新されるデータを確認できます。
以上となります。
実際の動きを動画にしてみました。
データの更新がリアルに見えるのでテストしやすいですね。