Help us understand the problem. What is going on with this article?

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

はじめに

いちあき(@ichiaki_kazu)と言います。初めてのQiita記事です。

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

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

この記事の目的

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

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

参考資料

・「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」(2020/1に大阪でも開催)
・Qiita「滑舌チェックスキルをGoogle Nest Hubで実装してみた」

完成物

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

今回使ったもの

  • 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
作成するIntentは4つ

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

1-2.実際にインテントを作る(例:StartIntent)

アプリ起動後にゲームを開始するための言葉を登録します。
20191213_061_dialogflow.cloud.google.com.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を引っ張ってきます。

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に流れます。

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」を表示するようにします。

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側のメインコンテンツです。

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.プロジェクトを作成する

このあたりは完全にこの記事と同じです。

$ 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を編集

完全にこの記事と同(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を編集

ワードが入力された際にインテントに応じて処理を変えます。

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にアクセスできます。
トップ画面が表示されます。
僕はこのあとngrokを使いましたがとりあえずこれでも動くと思います。

Azurefunctionsの起動

npmインストール

kudamonoquiz-functionでnpmインストールします。

cd ./kudamonoquiz-function
npm install

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

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/でアプリが起動します。

Dialogfrowの設定

FulfillmentのwebhookURLを指定する

20191214_062_dialogflow_cloud_google_com.png

テストする

IntegrationからGoogle Assistantを選択
Dialogflow.png

出てきたポップアップで「Auto-preview changes」にチェック入れて「TEST」
Dialogflow2.png

Developから名前(アプリ呼び出しの呼び名)
Develop.png

Deployで「category」を「Games&fun」にする。※InteractiveCanvasに必須
Deploy2.png

InteractiveCanvasをYesにする。
Deploy3.png

Testから実際に動作を確認する。
Androidスマホがある方はそちらからでも確認できます。
Test.png

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

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

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

宣伝みたいな

【大阪】Google_Nest_Hub対応スマートディスプレイスキルを作ろう!【Vue_js】_-_connpass.png
https://atlabo.connpass.com/event/157824/
2020年の1/23(木)に僕がスマートディスプレイにハマったきっかけのハンズオンが大阪でも開催されます。
興味ある方はおすすめですのでぜひ!※僕もスタッフとして行きます

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした