はじめに
いちあき(@ichiaki_kazu)と言います。初めてのQiita記事です。
僕はこれまでアセンブラやHTMLくらいしか触ってこなかった(ほぼ)ノンプロフリーランスです。
それをフリーランスというのか置いといて…
11月に行った「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」というハンズオンが楽しかったので、勉強も兼ねてハンズオン内容を参考にしつつ自分で作ってみました。
この記事の目的
- 自身のやったことを整理して定着させる
- 共に音声アプリの概要を掴んでもらえたらな
もしかしたらこの通りやっても動かないかもしれないので鵜呑みにしないでください。
※知識不足により誤っている部分がある可能性があります。その時は優しく指摘してください…
参考資料
・「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」(2020/1に大阪でも開催)
・Qiita「滑舌チェックスキルをGoogle Nest Hubで実装してみた」
完成物
【Google Nest Hub対応アプリつくった】
— いちあき@ぷちスマーティスト (@ichiaki_kazu) December 6, 2019
出てきた果物を英語で呼んであげるクイズアプリを作ってみました!
がおまるさん(@gaomar)が作られた滑舌チェックスキルを参考にさせていただいてます。元ネタは続きに…#GoogleNestHub #駆け出しエンジニアとつながりたい pic.twitter.com/87ZMpjG2DX
音声でアプリを呼び出して、出てきたくだものの画像を見て名前を当てるゲームです。
幼児になら使ってもらえるかなって…
今回使ったもの
- Google Nest Hub:実機動作確認のため
- Dialogflow:自然言語処理
- Vue.js(VueCLI):アプリの画面生成
- Azure Functions(CLI):内部の処理(クイズ部分とか入出力とか)
- Vuetify:Vueで使えるフレームワーク
- IntaractiveCanvas:(重要)Googleアシスタントアプリで画面描写に必要なライブラリ
実際に作ってみる
作るに当たってポイント部分だけを解説していきます。
実際に作ってみたい人は「滑舌チェックスキルをGoogle Nest Hubで実装してみた」をまず確認すると良いと思います。
1.Dialogflow(言語処理をする)
Googleアシスタントによって入力された音声の処理を行います。
Intent(インテント)を作成することで、言葉に反応して何かを返します。
1-1.インテント作成
Intent名 | 内容 |
---|---|
Default Welcome Intent | アプリ起動の言葉 |
EndIntent | アプリ終了の言葉 |
MainIntent | アプリ起動中の言葉 |
StartIntent | アプリ起動後に開始する言葉 |
1-2.実際にインテントを作る(例:StartIntent)
- 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を引っ張ってきます。
<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に流れます。
<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」を表示するようにします。
<template>
<HelloWorld />
</template>
<script>
import HelloWorld from '../components/HelloWorld.vue'
export default {
name: 'home',
components: {
HelloWorld
}
}
</script>
2-5.HelloWorld.vueを修正(ポイント)
app側のメインコンテンツです。
<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にします。
{
"bindings": [
{
"authLevel": "anonymous", // anonymousにしておく
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"post" // getは使わないので消しておく
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
3-4. 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を記述する
//起動時
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を指定する
テストする
IntegrationからGoogle Assistantを選択
出てきたポップアップで「Auto-preview changes」にチェック入れて「TEST」
Deployで「category」を「Games&fun」にする。※InteractiveCanvasに必須
Testから実際に動作を確認する。
Androidスマホがある方はそちらからでも確認できます。
今回はテストまでなのでデブロイはなしで、ここまでとなります。
ここまで実施すれば実機でテストバージョンとして動作確認もできます。
以上です。
もしかしたら手順漏れなどあるかもしれませんが大体こんな流れです。
興味ある方は是非是非試してください〜
僕も答えられるかわかりませんが、不明点がありましたらお願いします!
宣伝みたいな
https://atlabo.connpass.com/event/157824/
2020年の1/23(木)に僕がスマートディスプレイにハマったきっかけのハンズオンが大阪でも開催されます。
興味ある方はおすすめですのでぜひ!※僕もスタッフとして行きます