原神のコンテンツクリエイタープログラムに応募するために音声認識を利用したアプリを3日で作った話

# はじめに
[こんなアプリ](https://hehe-te-nandayo.vercel.app/)をリリースしました。
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/b2362902-f0b7-3576-38c4-c0724ac13fe1.gif)

内容はいたってシンプルで、マイクに向かって「えへ」って言うと原神のキャラクターであるパイモンが反応して「えへってなんだよ！」とツッコミを入れてくれるアプリです。

ソースは[GitHub](https://github.com/WinterYukky/HeheTeNandayo)にあります。

## 経緯
普段はVueやGolangを用いたWEBシステムを開発している大阪のエンジニアです。
最近、機械学習の勉強と[原神](https://genshin.mihoyo.com/ja)というゲームにハマっていて、機械学習の勉強の合間に原神を進めていたらなんとTwitterで原神の公式アカウントがこんな事を呟いているではありませんか。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【コンテンツクリエーター募集開始】<br>旅人さん、「原神」を一緒に盛り上げていただけるコンテンツクリエイターを募集いたします！<br><br>募集期間<br>2020年10月31日(土)〜11月30日(月)23:59<br><br>▼詳細はこちら<a href="https://t.co/LSssNbM4sO">https://t.co/LSssNbM4sO</a><a href="https://twitter.com/hashtag/%E5%8E%9F%E7%A5%9E?src=hash&amp;ref_src=twsrc%5Etfw">#原神</a> <a href="https://twitter.com/hashtag/Genshin?src=hash&amp;ref_src=twsrc%5Etfw">#Genshin</a> <a href="https://t.co/friBOUKLQ9">pic.twitter.com/friBOUKLQ9</a></p>&mdash; 原神（Genshin）公式 (@Genshin_7) <a href="https://twitter.com/Genshin_7/status/1322463322881495041?ref_src=twsrc%5Etfw">October 31, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

乗るしかない、このビックウェーブに。って事で絶賛勉強中の機械学習でインディーズゲームを作る事にしました。

## 元ネタ
題材にした「えへってなんだよ！」とは原神の序章第2幕にてパイモンがウェンティに言い放つ言葉で、その可愛いさからTwitterで少し話題になっていました。
<blockquote class="twitter-tweet"><p lang="ht" dir="ltr">Paimon.EXE - [Hehe te nanda yo] - 1 MILLION Times Edition <a href="https://t.co/3vRptAQsKB">https://t.co/3vRptAQsKB</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>より</p>&mdash; ゆっきー (@WinterYukke) <a href="https://twitter.com/WinterYukke/status/1324958473201709057?ref_src=twsrc%5Etfw">November 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">パイモンちゃんで「てってってー」<a href="https://twitter.com/hashtag/%E5%8E%9F%E7%A5%9E?src=hash&amp;ref_src=twsrc%5Etfw">#原神</a>　<a href="https://twitter.com/hashtag/Genshin?src=hash&amp;ref_src=twsrc%5Etfw">#Genshin</a>　<a href="https://twitter.com/hashtag/%E9%9F%B3MAD?src=hash&amp;ref_src=twsrc%5Etfw">#音MAD</a> <a href="https://t.co/6aFKDx7qru">pic.twitter.com/6aFKDx7qru</a></p>&mdash; ゼ民です (@HATENAYOSE_mani) <a href="https://twitter.com/HATENAYOSE_mani/status/1317883067713810432?ref_src=twsrc%5Etfw">October 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

パイモンの可愛さと、「えへ」に反応するだけというシンプルな要点から題材にする事にしました。

# 制作
### 使用したもの
- 言語・フレームワーク
    - Nuxt.js v2.14.6
    - TypeScript 
    - TensorFlow.js
- ツール・インフラ
    - Teachable Machine
    - Figma
    - GitHub
    - Vercel

### 1日目
#### 音声認識モデル
まず、メインコンテンツであるパイモンの音声認識の仕組みを作ります。
機械学習は絶賛勉強中ですがまだまだ実運用レベルではないため[Teachable Machine](https://teachablemachine.withgoogle.com/)の手順に沿って簡単に音声認識のモデルを作成します。
![TeachableMachine.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/b26e4bf9-0260-8fa5-dfd5-1ba21a7ed113.gif)
次にモデルをエクスポート及びアップロードしてサンプルコードを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/260959d2-7b22-4c34-5d8d-768a12ed7f16.png)
取得したサンプルコードをhtmlファイルとして保存します。そのままブラウザで開いても動作を確認出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/8f8990e4-2fdb-339b-074c-f890d041ea55.png)



#### Nuxtプロジェクト作成
次のコマンドでNuxtプロジェクトを立ち上げます。今回はTypeScript、PWA、SSG、Eslint、PritterとUI FrameworkにVuetify.jsを選択しました。

```bash
yarn create nuxt-app
```
サンプルで使用していたパッケージと、Vue3に向けてNuxt用のcomposition-apiを追加します。

```bash
yarn add @tensorflow/tfjs @tensorflow-models/speech-commands @nuxtjs/composition-api
```
次に`nuxt.config.js`ファイルの名前を`nuxt.config.ts`に書き換えます。
そのままだと`vuetify`の型で怒られるので`tsconfig.json`の`types`に`vuetify`を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/bb39945e-ee03-6ab9-9f59-117657b54de8.png)
`nuxt.config.ts`を修正します。
<details><summary>修正後の`nuxt.config.ts`</summary><div>

```typescript
import colors from 'vuetify/es5/util/colors'

export default {
  // Target (https://go.nuxtjs.dev/config-target)
  target: 'static',

  // Global page headers (https://go.nuxtjs.dev/config-head)
  head: {
    titleTemplate: '%s - a',
    title: 'a',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

  // Global CSS (https://go.nuxtjs.dev/config-css)
  css: [],

  // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
  plugins: [],

  // Auto import components (https://go.nuxtjs.dev/config-components)
  components: true,

  // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
  buildModules: [
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
    // https://go.nuxtjs.dev/vuetify
    '@nuxtjs/vuetify',
    '@nuxtjs/composition-api',
  ],

  // Modules (https://go.nuxtjs.dev/config-modules)
  modules: [
    // https://go.nuxtjs.dev/pwa
    '@nuxtjs/pwa',
  ],

  // Vuetify module configuration (https://go.nuxtjs.dev/config-vuetify)
  vuetify: {
    customVariables: ['~/assets/variables.scss'],
    theme: {
      dark: true,
      themes: {
        dark: {
          primary: colors.blue.darken2,
          accent: colors.grey.darken3,
          secondary: colors.amber.darken3,
          info: colors.teal.lighten1,
          warning: colors.amber.base,
          error: colors.deepOrange.accent4,
          success: colors.green.accent3,
        },
      },
    },
  },

  // Build Configuration (https://go.nuxtjs.dev/config-build)
  build: {
    extend(config: any, _: any) {
      config.node = {
        fs: 'empty',
      }
    },
  },
}

```
</div></details>

#### サンプルをnuxtで動かす
pages/index.vueを修正してサンプルのソースをvue用に書き換えてみます。
<details><summary>修正後の`index.vue`</summary><div>

```vue
<template>
  <div>
    <div>Teachable Machine Audio Model</div>
    <v-btn @click="init()">Start</v-btn>
    <div v-for="prediction in predictions" :key="prediction.label">
      {{ prediction.label }}: {{ prediction.score.toFixed(2) }}
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@nuxtjs/composition-api'
import * as speechCommands from '@tensorflow-models/speech-commands'

interface Prediction {
  score: number
  label: string
}
export default defineComponent({
  setup() {
    const URL = 'https://teachablemachine.withgoogle.com/models/{modelのID}/'
    async function createModel() {
      const recognizer = speechCommands.create(
        'BROWSER_FFT',
        undefined,
        `${URL}model.json`,
        `${URL}metadata.json`
      )
      await recognizer.ensureModelLoaded()
      return recognizer
    }
    const predictions = ref<Prediction[]>([])
    async function init() {
      if (process.server) return
      const recognizer = await createModel()
      const labels = recognizer.wordLabels()
      recognizer.listen(
        (result) => {
          predictions.value = labels.map((label, index) => {
            return { score: result.scores[index] as number, label }
          })
          return new Promise(() => {})
        },
        {
          includeSpectrogram: true,
          probabilityThreshold: 0.75,
          invokeCallbackOnNoiseAndUnknown: true,
          overlapFactor: 0.5,
        }
      )
    }
    return {
      init,
      predictions,
    }
  },
})
</script>
```
</div></details>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/781f54c3-5ba1-6455-e84f-613cba822bc2.png)
これだけでもかなりリッチな見た目になった気がします。

#### パイモン素材作成
仕組みは出来上がっても素材がなければインディーゲームとは言い難いです。素材を作りましょう。
素材作りにはいつも画面デザインで利用している[Figma](https://www.figma.com/)を使いました。Figmaは画面デザイン用のアプリケーションですが、成果物をパーツ毎にSVG形式で吐き出せるため素材作成に便利です。SVGで吐き出せばサイズもほとんど気にしなくていいですし、アニメーション追加も簡単です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">こんなけありゃおkやろ <a href="https://t.co/hF3r1suJF4">pic.twitter.com/hF3r1suJF4</a></p>&mdash; ゆっきー (@WinterYukke) <a href="https://twitter.com/WinterYukke/status/1324019299611103232?ref_src=twsrc%5Etfw">November 4, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">前にあげたパイモンのロード画面怖いって言われたからこれにした<a href="https://twitter.com/hashtag/%E5%8E%9F%E7%A5%9E?src=hash&amp;ref_src=twsrc%5Etfw">#原神</a> <a href="https://t.co/NHyJCjFFBo">pic.twitter.com/NHyJCjFFBo</a></p>&mdash; ゆっきー (@WinterYukke) <a href="https://twitter.com/WinterYukke/status/1324217079415779330?ref_src=twsrc%5Etfw">November 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#### ひたすら実装
素材も揃ったので後はサンプルを修正して「えへ」のスコアが高かった時にだけ怒り顔パイモンの素材を描画するように書きなおし、ほとんど仕組みが出来上がって1日目を終えました。

### 2日目
1日目の時点でほとんど出来上がっていてデプロイとIOS端末でのバグ修正がメインでした。

#### デプロイ
デプロイ先をどこにするか考えました。簡単かつ無料でデプロイ出来る所と言えばこの3つが思い浮かび、単純に今まで使用した事がないVercelでやってみることにしました。

- GitHub Pages
- GitLab Pages
- Vercel

普段はGitLab Pagesを利用しているのでCIを書いていたのですが、Vercelはとても簡単で**Import Git Repository**を選んで自分のGitHubアカウント及びプロジェクトと紐づけるだけでスイスイ進んでいきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/4167f201-e830-9a4e-3aa4-47d04b723d98.png)
こんな感じに自動で`Nuxt.js`プロジェクトだと認識してくれていたので`Nuxt.js`の仕組みにのっていれば勝手にやってくれる素晴らしいサービスです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/14b963a4-4eb2-fcc2-7f1a-61d7e1a2a199.png)


・・・が、少しだけ詰まりました。
`nuxt generate`とプレースホルダーに書いてあったので安心していましたが、`yarn build`が実行されてしまい「`dist`パッケージがねぇ！！！」と怒られてしまいました。そのため、VercelのBUILD COMMAND設定を`yarn run generate`に変更しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/9a04810f-5525-dd8d-c0b7-b66994f94570.png)
無事デプロイ成功！
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Oh yes!!!! <a href="https://t.co/rdbywr2n5M">pic.twitter.com/rdbywr2n5M</a></p>&mdash; ゆっきー (@WinterYukke) <a href="https://twitter.com/WinterYukke/status/1324372217535160320?ref_src=twsrc%5Etfw">November 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#### IOSでの不具合
デプロイ後、自前IPhoneのsafariからアクセスするとエラーページしか表示されない。。。
何が問題なのか調べるためにIPhoneに入っているChromeから[chrome://inspect](chrome://inspect)にアクセスしてログを見ると
>ReferenceError: Can't find variable: Notification

:innocent:

実はこの時のバージョンではパイモンが「えへってなんだよ！」って言う際にWEBの通知機能を利用して通知させるようにしていました。そのためNotificationが存在しなければ通知機能を使わないように修正したりと試行錯誤したのですが、どうやっても表示されなかったため完全に通知系は諦める事にしました。:pensive:

残りは細々とした修正で2日目も終了です。

### 3日目
嫁からandroid端末を借りて動きを確認したりエラー画面を修正して、本格的にリリースの準備です。
告知用にツイートを用意して完了です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【パイモンに「えへってなんだよ！」って言わせるだけのアプリ】をリリースしました🚀<br><br>【遊び方】<br>1. <a href="https://t.co/c9ehIREAOk">https://t.co/c9ehIREAOk</a> にアクセス<br>2. マイクを許可して「えへ」って言うだけ😶<a href="https://twitter.com/hashtag/%E5%8E%9F%E7%A5%9E?src=hash&amp;ref_src=twsrc%5Etfw">#原神</a> <a href="https://twitter.com/hashtag/%E3%81%88%E3%81%B8%E3%81%A3%E3%81%A6%E3%81%AA%E3%82%93%E3%81%A0%E3%82%88?src=hash&amp;ref_src=twsrc%5Etfw">#えへってなんだよ</a> <a href="https://t.co/HzZLzc7S0t">pic.twitter.com/HzZLzc7S0t</a></p>&mdash; ゆっきー (@WinterYukke) <a href="https://twitter.com/WinterYukke/status/1324934806627495936?ref_src=twsrc%5Etfw">November 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 最後に
3日で作ったとは書きましたがPC限定にするなど色々諦めれば1日で出来たと思います。もっと踏み入ったコンテンツを作るためにも機械学習の勉強は欠かせませんね。原神のコンテンツクリエイタープログラムにも選ばれれば完璧です！

それとお願いです。RTして下さい。。。:sob:
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【パイモンに「えへってなんだよ！」って言わせるだけのアプリ】をリリースしました🚀<br><br>【遊び方】<br>1. <a href="https://t.co/c9ehIREAOk">https://t.co/c9ehIREAOk</a> にアクセス<br>2. マイクを許可して「えへ」って言うだけ😶<a href="https://twitter.com/hashtag/%E5%8E%9F%E7%A5%9E?src=hash&amp;ref_src=twsrc%5Etfw">#原神</a> <a href="https://twitter.com/hashtag/%E3%81%88%E3%81%B8%E3%81%A3%E3%81%A6%E3%81%AA%E3%82%93%E3%81%A0%E3%82%88?src=hash&amp;ref_src=twsrc%5Etfw">#えへってなんだよ</a> <a href="https://t.co/HzZLzc7S0t">pic.twitter.com/HzZLzc7S0t</a></p>&mdash; ゆっきー (@WinterYukke) <a href="https://twitter.com/WinterYukke/status/1324934806627495936?ref_src=twsrc%5Etfw">November 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>