1時間で作るgoogle homeアプリ（社内勉強会向け）

actions on googleとdialogflow(とその裏側で動くfirebase functions)で作る簡単なクイズアプリの作り方です。
社内の勉強会(1時間)向けなのですが、せっかくなので公開します。
口頭で説明しながらとなるのでけっこうざっくりです。

---
## actions on google -1/3-
https://developers.google.com/actions/

- 右上の**GO TO ACTIONS CONSOLE**押す
- **Add Import/Project**押す

---
### 2/3

|Project Name|language|countory|Genre|
|:-----------------|------------------:|:------------------:|:------------------:|
| 任意           |              Japanese|        Japanese| 任意|

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/27ca4e2e-3e22-7fab-9a48-c56175f900e9.png" width=50% />

---
### 3/3
1. Invacation -> Display Name入力(アプリ名)
2. Build -> Actions -> Custom intent -> BUILD

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/12ac5086-ccb5-1075-341d-fe910a276639.gif" width=50% />

---
## dialog flow -Entitys-
1. **CREATE ENTITY**を押す
2. Entity name = answers
3. Valueとsynonymを2,3入力

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/c2542fa7-2217-da68-5314-58d03f880657.png" width=50% />

### Intents
4つ作ります

| インテント名    | 用途 |サンプルフレーズ|
|:-----------------|------------------:|------------------:|
| QuizIntent|出題|「ではクイズです。パンはパンでも・・・」|
| AnswerIntent|解答|「正解！正解は・・・」|
| FinishIntent|終了|「遊んでくれてありがとう。またね」|
| HelpIntent|使い方|「このクイズはほにゃほにゃ|

---
### QuizIntent
- Output Contexts
 - QuizIntent-followup, lifespan:1

- Training phrases
 - クイズはじめる
 - スタート
 - 問題出して

- Fulfillment
 - [x] Enable webhook call for this intent

---
### Contextsについて

ざっくり言うとIntent同士をつなぐためのものです。
例えば```QuizIntent-followup``` という名前のContextを Input Contextsとして設定したIntentがある場合、このContextを含んでいる場合しか呼ばれません。
こんかいはQuizIntentでOutputに```QuizIntent-followup``` を設定するのでQuizIntent経由でしかAnswerIntentが呼ばれなくなります。
(不正解の場合にもAnswerIntentを呼びたいため、この仕組みを使っています)

---
### AnswerIntent
- Input Contexts
 - QuizIntent-followup

- Output Contexts
 - QuizIntent-followup, lifespan:1

- Training phrases
 - 鰺(=@answers)
 - くじら(=@answers)かな
 - 亀(=@sys.any)

- Fulfillment
 - 　[x] Enable webhook call for this intent

---
### @sys.anyとは
なんにでも合致してしまうジョーカー的なEntityです。
間違って使うとなんでも拾ってしまいます。
利用については推奨ではないみたいです。
今回はcontextで絞りをかけたインテントにて使っています。
これができるのがghomeアプリの強みだと思ってる。


---
### FinishIntent

- Training phrases
 - やめる
 - 終わる
 - 終了

- Responses 
 - 遊んでくれてありがとう。また遊んでね
 - また遊んでね。bye

- Set this intent as end of conversation
 - [x]

---
### HelpIntent

- Training phrases
 - 使い方
 - ヘルプ
 - どうやるの？

- Responses 
 - 「スタート」というとクイズがはじまるので・・

---
## Export and Import (Dialogflow)
DialogflowのIntentsやEntitiysについてはImport(Restore or Import)で複製も可能です。
ただし、fullfillmentのスクリプトは復元されません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/4a0a61b3-15f8-3dea-169f-65617d87e281.png" width=50% />



---
## fullfillment
QuizIntentとAnswerIntentは単純なやりとりではないため、バックエンドにnodejsを用います。
DialogFlowにはInline Editorがついており、nodejsのコードを書けるようになっているため今回はこれを使います。
fullfillment(左ナビ)を選択し、Inline EditorをEnableにします。

---
## サンプルコード

```nodejs:index.js
'use strict';
 
const functions = require('firebase-functions');
const { dialogflow, SimpleResponse } = require('actions-on-google');

//mp3
const drumrole_mp3 = "https://s3-ap-northeast-1.amazonaws.com/xxxxxxxx/quiz/drum-japanese.mp3";
const question_mp3 = "https://s3-ap-northeast-1.amazonaws.com/xxxxxxxx/quiz/question.mp3";
const correct_mp3 = "https://s3-ap-northeast-1.amazonaws.com/xxxxxxxx/quiz/correct.mp3";
const incorrect_mp3 = "https://s3-ap-northeast-1.amazonaws.com/xxxxxxxxx/quiz/incorrect.mp3";

const app = dialogflow({ debug: true });

const Quizzes = {
    "鯵": { 
      "q": "魚へんに<emphasis level='moderate'><sub alias='惨状のさん'>参</sub></emphasis>を書いて何という魚？",
      "a": "あじ"
    },
    "鯨": {
      "q": "魚へんに<emphasis level='moderate'><sub alias='京都のきょう'>京</sub></emphasis>を書いて何という魚？",
      "a": "くじら"
    },
    "鯱": {
      "q": "魚へんに<emphasis level='moderate'><sub alias='とら'>虎</sub></emphasis>を書いて何という魚？",
      "a": "しゃち"
    },
    "鯰": {
      "q": "魚へんに<emphasis level='moderate'><sub alias='念仏のねん'>念</sub></emphasis>を書いて何という魚？",
      "a": "なまず" 
    },
    "鯖": {
      "q": "魚へんに<emphasis level='moderate'><sub alias='あおの旧字体'>青の旧字体</sub></emphasis>を書いて何という魚？",
      "a": "さば" 
    }
};
var quizzes = Object.assign({}, Quizzes);

app.intent('QuizIntent', (conv) => {
  　console.log(quizzes);

    if (Object.keys(quizzes).length < 1){
      conv.close("あれ？もう出す問題がなくなっちゃいました。ごめんなさい。またやってね！");
      return true;
    }
    
    //quizを選択
    let keys = Object.keys(quizzes);
    let choice = Math.floor( Math.random() * keys.length);
    let q = quizzes[keys[choice]];
    q.k = keys[choice];
    //set data
    conv.data.current = q;
    conv.ask(new SimpleResponse({
      text: q.q,
      speech: `<speak><audio src="${question_mp3}" />${q.q}</speak>`
    }));
});

app.intent('AnswerIntent', (conv, params) => {
    let q = conv.data.current;
    let collect = false;
    console.log(conv.input.raw);
    if (conv.parameters.answers !== ''){
      console.log(`answers is ${conv.parameters.answers}`);
      if (conv.parameters.answers == q.k){
        collect = true;
      }
    }
    if (conv.parameters.any !== ''){
      console.log(`answers is ${conv.parameters.any}`);
    }
    
    let prize = [
      "すごい!","素晴らしい!","アメージング!","天才!","この、イケメン!"   
    ];
    let p = prize[Math.floor( Math.random() * prize.length)];
    if (collect){
      conv.ask(new SimpleResponse({
        text: `正解！答えは、${q.a}でした。${p}。次の問題もやりますか？はい、かいいえで答えてね。`,
        speech: 
          `<speak><audio src="${drumrole_mp3}" /><audio src="${correct_mp3}" />
           <prosody rate="110%" pitch="+10%">正解!</prosody><break time="500ms"/>
           答えは、、${q.a}でした。<break time="200ms"/>
           <prosody rate="110%" pitch="+10%">${p}</prosody>
           次の問題もやりますか？<break time="500ms"/>
           はい、かいいえで答えてね。</speak>`
       }));
    }else{
      conv.ask(new SimpleResponse({
        text: `残念！答えは、${q.a}でした。次の問題もやりますか？はい、かいいえで答えてね。`,
        speech: 
          `<speak><audio src="${drumrole_mp3}" /><audio src="${incorrect_mp3}" />
           <prosody rate="105%" pitch="+5%">残念!</prosody><break time="500ms"/>
           答えは、、${q.a}でした。
           次の問題もやりますか？<break time="500ms"/>
           はい、かいいえで答えてね。</speak>`
       }));
    }
    //今の問題は使用済みに
    delete quizzes[q.k];
    //clear 
    conv.data.current = {};
});
  
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

```

---
### SSML
https://developers.google.com/actions/reference/ssml

**Speech Synthesis Markup Language**
しゃべらせ方をコントロールするためのマークアップ言語。
```<speak>```と```</speak>```で囲んだ中で、<emphasis>等のタグで声の協調等ができます。

---
### デバッグ
出来たらテスト。
プロジェクトのgoogleアカウントと同じアカウントであればテストバージョンのアプリが使用可能になっています。
アルファ版のリリースで他ユーザーもテスト可能になる。

1. シミュレーター
2. 実機
 - スマホ with google assistant
 - google home

---
## 公開申請
必要なこと
- Overviewを埋める
 - Description
 - Sample invocations
 - ロゴ2種(192x192,1920x1080)を登録
 - Contact detailsを入力
 - プライバシーポリシーを作成([こういうの](https://docs.google.com/document/d/1isuXx-Uyx56MOuPLXMFX1-LLxvEmCmGcnJvOoxRL0iM/edit?usp=sharing)で十分)

---
## その他

- 各フレーズはEntityにしてしまったほうが管理が楽です
 - たとえばFinishIntentで$finishphrasesを設定
 - Valueには「終わる」、「やめる」、「終了」など
 - FinishIntentのTraning phrasesには「終わる(=@finishphrases)」
 - https://github.com/ikegam1/ghome-aroundfish-quiz が今回のコードです