60
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-07

はじめに

こんなアプリをリリースしました。
sample.gif

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

ソースはGitHubにあります。

経緯

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

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

元ネタ

題材にした「えへってなんだよ!」とは原神の序章第2幕にてパイモンがウェンティに言い放つ言葉で、その可愛いさからTwitterで少し話題になっていました。

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

制作

使用したもの

  • 言語・フレームワーク
    • Nuxt.js v2.14.6
    • TypeScript
    • TensorFlow.js
  • ツール・インフラ
    • Teachable Machine
    • Figma
    • GitHub
    • Vercel

1日目

音声認識モデル

まず、メインコンテンツであるパイモンの音声認識の仕組みを作ります。
機械学習は絶賛勉強中ですがまだまだ実運用レベルではないためTeachable Machineの手順に沿って簡単に音声認識のモデルを作成します。
TeachableMachine.gif
次にモデルをエクスポート及びアップロードしてサンプルコードを取得します。
image.png
取得したサンプルコードをhtmlファイルとして保存します。そのままブラウザで開いても動作を確認出来ます。
image.png

Nuxtプロジェクト作成

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

yarn create nuxt-app

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

yarn add @tensorflow/tfjs @tensorflow-models/speech-commands @nuxtjs/composition-api

次にnuxt.config.jsファイルの名前をnuxt.config.tsに書き換えます。
そのままだとvuetifyの型で怒られるのでtsconfig.jsontypesvuetifyを追加します。
image.png
nuxt.config.tsを修正します。

修正後の`nuxt.config.ts`
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',
      }
    },
  },
}

サンプルをnuxtで動かす

pages/index.vueを修正してサンプルのソースをvue用に書き換えてみます。

修正後の`index.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>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276566/781f54c3-5ba1-6455-e84f-613cba822bc2.png) これだけでもかなりリッチな見た目になった気がします。

パイモン素材作成

仕組みは出来上がっても素材がなければインディーゲームとは言い難いです。素材を作りましょう。
素材作りにはいつも画面デザインで利用しているFigmaを使いました。Figmaは画面デザイン用のアプリケーションですが、成果物をパーツ毎にSVG形式で吐き出せるため素材作成に便利です。SVGで吐き出せばサイズもほとんど気にしなくていいですし、アニメーション追加も簡単です。

ひたすら実装

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

2日目

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

デプロイ

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

  • GitHub Pages
  • GitLab Pages
  • Vercel

普段はGitLab Pagesを利用しているのでCIを書いていたのですが、Vercelはとても簡単でImport Git Repositoryを選んで自分のGitHubアカウント及びプロジェクトと紐づけるだけでスイスイ進んでいきました。
image.png
こんな感じに自動でNuxt.jsプロジェクトだと認識してくれていたのでNuxt.jsの仕組みにのっていれば勝手にやってくれる素晴らしいサービスです。
image.png

・・・が、少しだけ詰まりました。
nuxt generateとプレースホルダーに書いてあったので安心していましたが、yarn buildが実行されてしまい「distパッケージがねぇ!!!」と怒られてしまいました。そのため、VercelのBUILD COMMAND設定をyarn run generateに変更しました。
image.png
無事デプロイ成功!

IOSでの不具合

デプロイ後、自前IPhoneのsafariからアクセスするとエラーページしか表示されない。。。
何が問題なのか調べるためにIPhoneに入っているChromeからchrome://inspectにアクセスしてログを見ると

ReferenceError: Can't find variable: Notification

:innocent:

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

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

3日目

嫁からandroid端末を借りて動きを確認したりエラー画面を修正して、本格的にリリースの準備です。
告知用にツイートを用意して完了です。

最後に

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

それとお願いです。RTして下さい。。。:sob:

60
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?