名取さなとお話せぇへん？

#全せんせえ待望のアプリがついに誕生したみたいだぞ！？
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">使用例(キモオタボイス注意) <a href="https://t.co/niSIhIY46T">pic.twitter.com/niSIhIY46T</a></p>&mdash; まがたま (@hidarikikineko) <a href="https://twitter.com/hidarikikineko/status/1192829322022666240?ref_src=twsrc%5Etfw">November 8, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
#はじめに
###宣伝
この記事は [Tokyo City University Advent Calendar 2019](https://adventar.org/calendars/4282) の3日目の記事になります。
よければ他の記事も見てみるとQOLが微小なりとも向上するんじゃないですかね？

###犯行動機
最近自分はVTuberにハマってて中でもお気に入りはPPHちゃんだったり名取さなちゃんだったりするんですが、↑で紹介したアドカレに招待された時になんか爪痕残したいなっていう思いで自分の少ない技術力とアイデアで名取さなとお話しする方法を考えつきました

#ここから本編
##やりたいこと
名取さなとお話しできる全せんせえ待望のGoogleAssistantアプリを作ります

##開発環境
MacBook(多分2017のモデル)メモリ8GB
Node.js v8.16.1
Actions on Google(npm) v2.2.0 
多分こんくらいです

##作るに当たって目標とか
アプリの流れとしては
- 起動ワードでアプリ起動(おはようございナース🍆)
- せんせえの発話に合わせて名取さなが返答
- 終了文言で終了
ってな感じの会話をできるようにしたいなと思ってます

##具体的に会話に必要な処理
↑で言ったような会話の流れを作るに当たっていろいろ処理を考える必要が有りますね。
まずは発話周りは[Dialogflow](https://dialogflow.com/)に任せるとします。

また、名取さなが具体的にどんなことを喋るかについてですが、有志のせんせえが作ったファンサイトである[さなボタン](https://www.natorisana.love/)様から音源を拝借して発話を行います。

音源に一つ一つ丁寧丁寧丁寧♪にタグ付を行って音源URLごとにまとめたものをFirestoreに保存して会話の中で発話に合わせて取り出すようにします。

##アプリの作り方
実際のアプリの作り方を解説したいわけですが......ここに書き込むとありえん話が長くなりそうなので重要な部分以外は割愛します。(アプリの作り方に関して１から知りたいって人は[これ](https://qiita.com/knanco/items/f3044dd447178b94cbf9)とか参考にしてみてください)
###Actions on Google編
まずは[Actions on Google](https://console.actions.google.com)に行ってもらって新しいProjectを立ち上げてください
プロジェクトを立ち上げた時に```Welcome to your new project!```って書いてあるページに飛ばされるんですが何も考えずにActions SDKとかいうの選んどけばOKです

その後アプリの名前とか呼び出し方とか言語設定とか丸々設定したらActions欄に行ってDialogflowとActions on Googleをリンクさせましょう
この時Costom intentがデフォルトで選択されてるのでそのままBUILD

###Dialogflow編
飛ばされた先のページで言語設定を済ませたらDialogflowの部分を作り込んでいきます。
とりあえずはどんな発話に反応させたいかを考えたならそれに沿ってEntityを作成していく感じです。
次にintentに飛んだのなら先ほど作ったEntitiを発動させるためのintentをそれぞれ作り込んでいきます
ここで一つ個人的なテクニック何ですが、intentのtraining phrasesを作り込む時にEntitiyの割り振りで、欲しい文脈と例外文言を組み合わせると前後の文脈によらずにキーワードに反応してくれるようになるのでおすすめです
<img width="600" alt="スクリーンショット 2019-11-23 11.03.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278057/8935320d-acae-f7db-6488-79b36378f6ab.png">
↑こんな感じです
あとは、一番下にあるFulfillment欄の```Enable webhock call for this intent```を有効にしておけばintentの設定は一旦完了
最後にIntegrationsでGoogleAssistantを設定してテストを有効にすればリンクは完了です。
###Firebase編
テキトーなディレクトリを用意してそこにアプリの処理を書き込むためのfirebaseの関数を用意します。(ここからはコンソール上での作業です)

事前に```$npm install firebase-tools```でfirebaseコマンドを使える環境を用意しておいてください(なんか言い方がワザップジョルノみたいですこ)

まずは、任意のディレクトリで```$firebase init```してその先で作ったプロジェクトのプロジェクトIDを選択、FunctionsとFirestoreを有効にしてテキトーにEnter押しといてください。

そしたらできたfunctionsディレクトリにindex.jsなるファイルができているハズなので確認したら```$firebase deploy```で関数を有効にします。

この時にコンソール上に```Function URL```という項目とともにurlが書かれると思うのでこれをコピってDialogflowのFulfillmentのWebhock欄に書き足しておいてください

##Firebaseにデプロイするコードを作り込む
ようやくプログラミングするよーん
ここまででは、コードは何も設定してないのでそれっぽく動くように仕上げていきます。(ここからはNode.jsの話)

```javascript:index.js
'use strict';
const functions = require("firebase-functions");
const { dialogflow } = require('actions-on-google');
const app = dialogflow({ debug: true });
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();

async function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min)) + min;
}

async function fetch_sound_url(theme){
  let theme_snap = await db.collection("urls").doc(theme).get()
  console.log("theme_snap", theme_snap)
  let theme_data = await theme_snap.data()
  console.log("theme_data",theme_data)
  console.log(Object.keys(theme_data).length)
  let random_numbar = await getRandomInt(0, Object.keys(theme_data).length)
  let sound_url = theme_data[random_numbar.toString()]
  console.log("sound_url",sound_url)
  return sound_url  
}

app.intent('Default Welcome Intent', async conv =>{
  //起動時挨拶
  let url = await fetch_sound_url("hello")
  conv.ask(" <speak><audio src='" + url + "'/></speak>");
});

app.intent('Default Fallback Intent', async conv =>{
  //設定外文言
  let url = await fetch_sound_url("anything")
  conv.ask(" <speak><audio src='" + url + "'/></speak>");
});
// ここからはintentを増やしてるだけなので割愛

exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

```

↑これは実際に記述したコードの一部です。
今回のミソなポイントはurlを一つ一つタグ付してFirestoreにurlをおいているので会話に合わせてタグごとにランダムにurl呼び出すだけってとこです。

あっ、Firestoreの構成書き忘れたんで下にこんな感じだよって画像乗っけます↓
<img width="864" alt="スクリーンショット 2019-11-23 17.49.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278057/ceb1a1be-af6a-d075-6feb-23deebe493ca.png">

ここではタグはドキュメント名で設定してます。(多分そっちのが検索しやすいので)
とりあえずはアプリの作り込みの解説は以上です。

#作ってみての感想
意外と内部の処理が単純なのでアプリの構築自体は簡単でした。ただセリフを一つ一つタグ付していく作業とDialogflowでintentを作成していくのは精神的に辛かったですね。(人間工学的には単純作業の集中力は30分が限界だそうなのでこういうのは休み休みやりましょうね〜)

あとテスト段階でしきりにスマホに向かって喋りかけるキモオタクの図が完成してしまったのでこれもまたキツかったです。

ただ最終的に名取さなとおしゃべり出来たので満足です。

あと世の中にはさなボタンの他にも美兎ボタンとかはねるボタンとかまだまだあるらしいのでおしゃべりアプリもっと増えろよなー

###もっかい宣伝
明日のカレンダーの記事もみるんだぞ！
明日の担当は@mikuta0407君！
詳しくは[こちら](https://adventar.org/calendars/4282)から