初心者が Googleアシスタント と Vue.js を使ってスマートディスプレイアプリを作ってみた

# はじめに
いちあき（[@ichiaki_kazu](https://twitter.com/ichiaki_kazu))と言います。初めてのQiita記事です。

僕はこれまでアセンブラやHTMLくらいしか触ってこなかった（ほぼ）ノンプロフリーランスです。
それをフリーランスというのか置いといて…

11月に行った「Google Nest Hub対応スマートディスプレイスキルを作ろう！【Vue.js】」というハンズオンが楽しかったので、勉強も兼ねてハンズオン内容を参考にしつつ自分で作ってみました。

## この記事の目的

- 自身のやったことを整理して定着させる
- 共に音声アプリの概要を掴んでもらえたらな

もしかしたらこの通りやっても動かないかもしれないので鵜呑みにしないでください。
※知識不足により誤っている部分がある可能性があります。その時は優しく指摘してください…

## 参考資料
・「Google Nest Hub対応スマートディスプレイスキルを作ろう！【Vue.js】」（2020/1に大阪でも開催）
・Qiita[「滑舌チェックスキルをGoogle Nest Hubで実装してみた」](https://qiita.com/h-takauma/items/16e9b3cc8fc93aa88c82)

## 完成物
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【Google Nest Hub対応アプリつくった】<br>出てきた果物を英語で呼んであげるクイズアプリを作ってみました！<br><br>がおまるさん（<a href="https://twitter.com/gaomar?ref_src=twsrc%5Etfw">@gaomar</a>）が作られた滑舌チェックスキルを参考にさせていただいてます。元ネタは続きに…<a href="https://twitter.com/hashtag/GoogleNestHub?src=hash&amp;ref_src=twsrc%5Etfw">#GoogleNestHub</a> <a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E3%81%A4%E3%81%AA%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアとつながりたい</a> <a href="https://t.co/87ZMpjG2DX">pic.twitter.com/87ZMpjG2DX</a></p>&mdash; いちあき@ぷちスマーティスト (@ichiaki_kazu) <a href="https://twitter.com/ichiaki_kazu/status/1202861263509786625?ref_src=twsrc%5Etfw">December 6, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

音声でアプリを呼び出して、出てきたくだものの画像を見て名前を当てるゲームです。
幼児になら使ってもらえるかなって…

# 今回使ったもの
- Google Nest Hub：実機動作確認のため
- Dialogflow：自然言語処理
- Vue.js（VueCLI）：アプリの画面生成
- Azure Functions(CLI)：内部の処理（クイズ部分とか入出力とか）
- Vuetify：Vueで使えるフレームワーク
- IntaractiveCanvas：（重要）Googleアシスタントアプリで画面描写に必要なライブラリ

# 実際に作ってみる
作るに当たってポイント部分だけを解説していきます。
実際に作ってみたい人は「滑舌チェックスキルをGoogle Nest Hubで実装してみた」をまず確認すると良いと思います。

## 1.Dialogflow（言語処理をする）
Googleアシスタントによって入力された音声の処理を行います。
Intent（インテント）を作成することで、言葉に反応して何かを返します。

### 1-1.インテント作成
![20191213_056_dialogflow.cloud.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/a0e1d045-c22b-b6a1-d2ff-9ed4c8a25a37.png)
作成するIntentは4つ

| Intent名         |      内容　 |
|:-----------------|------------------:|
| Default Welcome Intent             |         アプリ起動の言葉|
| EndIntent           |            アプリ終了の言葉 |
| MainIntent           |            アプリ起動中の言葉 |
| StartIntent           |            アプリ起動後に開始する言葉 |

### 1-2.実際にインテントを作る（例：StartIntent）
アプリ起動後にゲームを開始するための言葉を登録します。
![20191213_061_dialogflow.cloud.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/51206dcd-4d7e-5f84-8382-15c2a36ac269.png)

- Training phrases：反応する言葉を登録
- Fulfillment：webhookでやりとりするためチェックを入れる

その他のインテントも同じように作ります。
ちなみに「Response」には反応時返すメッセージを登録できます。
詳しくは参考記事をご覧ください。

## 2.Vue.js（アプリ画面を作る）
アプリの画面描写部分を作っていきます。
ざっくりとやることは以下の通り

- プロジェクト作成（vuetifyも入れる・今回はrouterも）
- index.htmlにIntaractivCanvasのAPI追加
- App.vueの修正
- Home.vueの修正
- HelloWorld.vueの修正

### 2-1.プロジェクト作成
プロジェクトを作ってvuetifyを入れます。

```
vue create kudamonoquiz-app
vue add vuetify
```

今回はプロジェクト作成時にrouterも入れておきました。

### 2-2.index.htmlにIntaractiveCanvasのAPI追加
画面描写のキモである「InteractiveCanvas」のAPIを引っ張ってきます。

```Vue:public/index.html（12行目あたり）
<script type="text/javascript" src="https://www.gstatic.com/assistant/interactivecanvas/api/interactive_canvas.min.js"></script>
```

### 2-3.App.vueの修正
ここではヘッダー部分とrouterへの連携をしています。
routerは初期状態でHome.vueに流れます。

```Vue:src/App.vue
<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title class="headline text-uppercase">
        <span>くだものくいず</span>
      </v-toolbar-title>
    </v-app-bar>

    <v-content>
      <router-view />
    </v-content>
  </v-app>
</template>
```

### 2-4.Home.vueの修正
コンポーネント「HelloWorld.Vue」を表示するようにします。

```Vue:src/Home.vue
<template>
  <HelloWorld />
</template>

<script>
import HelloWorld from '../components/HelloWorld.vue'

export default {
  name: 'home',
  components: {
    HelloWorld
  }
}
</script>
```

### 2-5.HelloWorld.vueを修正（ポイント）
app側のメインコンテンツです。

```Vue:src/Home.vue
<template>
  <v-container>
    <!-- スタートページ -->
    <v-layout
      text-center
      wrap
      v-show="target === 'top'"
    >
      <v-flex
        xs12
        md-10
      >
        <h3 class="display-3 font-weight-bold mb-10">
        </h3>
      </v-flex>

      <v-flex xs12>
        <h1 class="display-2 font-weight-bold mb-10">
          くだものえいご
        </h1>
      </v-flex>
      <v-flex xs12 mb-4>
        <v-btn color="success" @click="start">スタート</v-btn>
      </v-flex>   
    </v-layout>

    <!-- 問題表示ページ -->
    <v-layout
      text-center
      wrap
      v-show="target === 'kudamono'"
    >
      <v-flex 
        xs12 
        md-10
      >
        <h3 class="display-3 font-weight-bold mb-10"></h3>
      </v-flex>

      <v-flex xs12 mb-4>
          <h1 class="display-1 font-weight-bold mb-3">
              【くだものえいご】
          </h1>
      </v-flex>

      <v-flex xs12 mb-4>
        <img class="img" :src="imgurl" alt="くだもの画像">
      </v-flex>

      <v-flex xs12 mb-4>
          <h5 class="display-1 font-weight-bold mb-3">
              このくだものはなーんだ？
          </h5>
      </v-flex>
    </v-layout>

      <!-- 正解後の表示ページ -->
    <v-layout
      text-center
      column
      align-center
      v-show="target === 'congratulation'"
    >
      <v-flex
        xs12
        mb-10
      >
        <h3 class="display-3 font-weight-bold">
        </h3>
      </v-flex>

      <v-flex
        xs12
        mb-4
      >
        <img class="img" alt="congratulation" src="../assets/congratulation.png">
      </v-flex>

      <v-flex xs12 mb-4>
        <img class="img" :src="imgurl" alt="くだもの画像">
      </v-flex>

      <v-flex xs12 mb-4>
        <h2>だいせいかい！<br>これは{{kotae}}（{{tango}}）だよ！</h2>
      </v-flex>

      <v-flex
        xs12
        mb-10
      >
      <v-btn large color="success" @click="start">スタート</v-btn>
      </v-flex>
    </v-layout>

  </v-container>
</template>

<style scoped>
  .img{
    width: 300px;
  }

  .endimg{
    width: 200px;
  }
</style>

<script>
export default {
    data () {
      return {
        target: 'top'
      }
    },
    created(){
        var me = this
        const callbacks = {
            onUpdate(data){
                if('kudamono' in data){
                    me.kotae = data.kudamono.kotae,
                    me.tango = data.kudamono.tango,
                    me.imgurl = data.kudamono.imgurl,
                    me.target = data.kudamono.target
                }
            },
        }
        interactiveCanvas.ready(callbacks)
    },    
    methods: {
      start(){
        interactiveCanvas.sendTextQuery('スタート');
      }
    }
};
</script>
```

ざっくり解説するとこのファイルでは3つがポイントだと思います。

- targetの状態で表示するコンテンツ（開始時・ゲーム時・正解時）を分ける
- functionから送られてくるdata（画像URLや問題）をセットし、使用する
- 最初にスタートボタンを押した時 `interactiveCanvas.sendTextQuery('スタート');` でDialogflowに「スタート」という文字列を送る

特に文字列を送る昨日はinteractiveCanvasならではなので、ポイントかと思います。

### 2-6.package.json確認
後々動かす際にnpmインストールするのでpackage.jsonの確認をします。

```
{
  "name": "kudamonoquiz-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "core-js": "^3.4.3",
    "vue": "^2.6.10",
    "vue-router": "^3.1.3",
    "vuetify": "^2.1.0",
    "vuex": "^3.1.2"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.1.0",
    "@vue/cli-plugin-router": "^4.1.0",
    "@vue/cli-plugin-vuex": "^4.1.0",
    "@vue/cli-service": "^4.1.0",
    "sass": "^1.19.0",
    "sass-loader": "^8.0.0",
    "vue-cli-plugin-vuetify": "^2.0.2",
    "vue-template-compiler": "^2.6.10",
    "vuetify-loader": "^1.3.0"
  }
}
```

おそらくこの状態だと思いますが、念のための確認です。

## 3.Azure Functions（内部のプログラムを作る）
### 3-1.CLIツールをインストールする
とりあえずCLIツールをインストールします

```
npm install -g azure-functions-core-tools 
```

### 3-2.プロジェクトを作成する
このあたりは完全に[この記事](https://qiita.com/h-takauma/items/16e9b3cc8fc93aa88c82)と同じです。

```
$ mkdir kudamonoquiz-app-functions
$ cd kudamonoquiz-app-functions
$ func init                             # 選択肢が出てくるのでnodeとjavascriptを選ぶ
$ func new                              # Http triggerを選択し、kudamonoquiz-appという名前で作成する
$ npm init -y
$ npm i -s actions-on-google@2.10.0     # 2.10.0を入れる
$ npm i -s azure-function-express       # azure-function-expressを入れる
$ npm i -s express
$ npm i -s firebase-admin
```

### 3-3.functions.jsonを編集
完全に[この記事](https://qiita.com/h-takauma/items/16e9b3cc8fc93aa88c82)と同（ry
GETのみに変更して、どこからでもアクセスできるようanonymousにします。

```kudamonoquiz-app/functions.json
{
  "bindings": [
    {
      "authLevel": "anonymous",        // anonymousにしておく
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "post"      // getは使わないので消しておく
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}
```

### 3-4. index.jsを編集
ワードが入力された際にインテントに応じて処理を変えます。

```js:kudamonoquiz-app/index.js
const createHandler = require("azure-function-express").createHandler;
const express = require("express");
const {dialogflow, HtmlResponse} = require('actions-on-google');

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

//スタート処理
app.intent('StartIntent', async (conv) =>{
    //問題の生成
    const kudamonoquiz = [
        {"imgurl": "/img/banana.jpg", "kotae": "バナナ", "tango": "banana"},
        {"imgurl": "/img/cherry.jpg", "kotae": "チェリー", "tango": "cherry"},
        {"imgurl": "/img/grape.jpg", "kotae": "グレープ", "tango": "grape"},
        {"imgurl": "/img/melon.jpg", "kotae": "メロン", "tango": "melon"},
        {"imgurl": "/img/orange.jpg", "kotae": "オレンジ", "tango": "orange"},
        {"imgurl": "/img/peach.jpg", "kotae": "ピーチ", "tango": "peach"},
        {"imgurl": "/img/remon.jpg", "kotae": "レモン", "tango": "remon"},
        {"imgurl": "/img/strawberry.jpg", "kotae": "ストロベリー", "tango": "strawberry"}
    ];
    const wordIndex = Math.floor(Math.random() * kudamonoquiz.length);
    const selKudamono = kudamonoquiz[wordIndex];
    conv.contexts.set('game', 5, selKudamono);
    conv.ask('このくだものを英語で言ってみてね');
    selKudamono["target"] = "kudamono";
    conv.ask(new HtmlResponse({
        data: {
            kudamono: selKudamono
        }
    }));
});

//ゲーム処理
app.intent('MainIntent', async (conv, {any}) => {
    const context = conv.contexts.get('game');

    if(context.parameters.kotae === any){
        conv.contexts.delete('game');
        conv.ask(`大正解。答えは ${context.parameters.kotae} でした！ もう一度クイズをするなら「する」終了するなら「終了」と言ってください。`);
        context.parameters["target"] = "congratulation"
    }else{
        conv.ask('よくわかりませんでした。もういちど言ってみてね。');
    }

    conv.ask(new HtmlResponse({
        data: {
            kudamono: context.parameters
        }
    }));

});

//起動時
app.intent('Default Welcome Intent', (conv) => {
    conv.ask('果物英語をはじめるには、スタートボタンを押してください。');
    
    conv.ask(new HtmlResponse({
        url: 'https://{表示させたいホームページのURL}',
        supperss: true
    }));

});

//functionの名前を一致させておく
const expressApp = express();
expressApp.post('/api/kudamonoquiz-app', app);
module.exports = createHandler(expressApp);
```

# 実際に動かしてみる
プログラムを動かしていきます。
## Vue.jsの起動
### npmインストール
kudamonoquiz-appでnpmインストールします。

```
cd ./kudamonoquiz-app
npm install
```

### プログラム実行
```
npm run serve
```
これで[http://localhost:8080](http://localhost:8080)にアクセスできます。
トップ画面が表示されます。
僕はこのあとngrokを使いましたがとりあえずこれでも動くと思います。

## Azurefunctionsの起動
### npmインストール
kudamonoquiz-functionでnpmインストールします。

```
cd ./kudamonoquiz-function
npm install
```

### index.js内にアプリのURLを記述する

```js:kudamonoquiz-function/kudamonoquiz-app/index.js（抜粋）

//起動時
app.intent('Default Welcome Intent', (conv) => {
    conv.ask('果物英語をはじめるには、スタートボタンを押してください。');
    
    conv.ask(new HtmlResponse({
        url: 'http://localhost:8080',
        supperss: true
    }));

});
```

### ローカルサーバを起動する
```
func host start
```
[http://localhost:8080/](http://localhost:8080/)でアプリが起動します。

## Dialogfrowの設定
### FulfillmentのwebhookURLを指定する
![20191214_062_dialogflow_cloud_google_com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/14b38dee-4123-fede-1f6e-a9cf1b28f990.png)

### テストする
IntegrationからGoogle Assistantを選択
<img width="1237" alt="Dialogflow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/aace233b-bf34-358c-7649-04bbf42b7d27.png">

出てきたポップアップで「Auto-preview changes」にチェック入れて「TEST」
<img width="793" alt="Dialogflow2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/5145c5f1-f9b3-c3d2-420c-2b2c07eb9bbc.png">

Developから名前（アプリ呼び出しの呼び名）
<img width="1210" alt="Develop.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/71483811-f6c0-43aa-814d-db261bf4996a.png">

Deployで「category」を「Games&fun」にする。※InteractiveCanvasに必須
<img width="1178" alt="Deploy2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/6ed31f71-5199-6798-2d94-61c24c808736.png">

InteractiveCanvasをYesにする。
<img width="531" alt="Deploy3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/17dea14c-5eb9-f8cd-f820-ccbfd0e1e28c.png">

Testから実際に動作を確認する。
Androidスマホがある方はそちらからでも確認できます。
<img width="1206" alt="Test.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/b6f3108e-a1f7-bce7-21bc-83c07574a92c.png">

今回はテストまでなのでデブロイはなしで、ここまでとなります。
ここまで実施すれば実機でテストバージョンとして動作確認もできます。

以上です。
もしかしたら手順漏れなどあるかもしれませんが大体こんな流れです。
興味ある方は是非是非試してください〜

僕も答えられるかわかりませんが、不明点がありましたらお願いします！

# 宣伝みたいな
<img width="1054" alt="【大阪】Google_Nest_Hub対応スマートディスプレイスキルを作ろう！【Vue_js】_-_connpass.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541097/d4c04693-39f4-456b-0772-e5810eb088d3.png">
https://atlabo.connpass.com/event/157824/
2020年の1/23（木）に僕がスマートディスプレイにハマったきっかけのハンズオンが大阪でも開催されます。
興味ある方はおすすめですのでぜひ！※僕もスタッフとして行きます