webkitSpeechRecgnitionを扱う

# はじめに

音声認識のためのインターフェイスである `webkitSpeechRecognition` を扱った際に得た知見をまとめました。本記事では、常にマイクから音声を聞きながら、声が発せられてから無音になるまでの間に話された言葉を文字列として取得する方法について述べていきます。

- https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition

# 構造を理解する

まずは以下のコードを実行します。 `onなんとか` 系が多いので、どのようなタイミングで発火するのかを確認しましょう。

```js:main.js
const rec = new webkitSpeechRecognition()
rec.continuous = false
rec.interimResults = false
rec.lang = 'ja-JP'

rec.onresult = e => {
  console.log('on result')

  for (var i = e.resultIndex; i < e.results.length; i++) {
    if (!e.results[i].isFinal) continue

    const { transcript } = e.results[i][0]
    console.log(`Recognised: ${transcript}`)
  }

  rec.stop()
}

rec.onstart = () => { console.log('on start') }
rec.onend = () => { console.log('on end') }

rec.onspeechstart = () => { console.log('on speech start') }
rec.onspeechend = () => { console.log('on speech end') }

rec.onosundstart = () => { console.log('on sound start') }
rec.onsoundend = () => { console.log('on sound end') }

rec.onaudiostart = () => { console.log('on audio start') }
rec.onaudioend = () => { console.log('on audio end') }

rec.start()
```

![](https://gyazo.com/3a73228c45c12a3fb879a8e914009564.png)

声を発した瞬間に `on speech start` と表示されましたので、 `onspeechstart` は声が発せられることがトリガーだと考えられます。その他についてもまとめてみましょう。

- `onstart`
    - `rec.start()` がされた時
- `onend`
    - `rec.stop()` がされた時
- `onspeechstart`
    - 発声の始めを声が受け付けた時
- `onsoundstart` 、 `onaudiostart`
    - 不明
- - `onspeechend` 、 `onsoundend` 、 `onaudioend` 、 `onresult`
    - 発声が終了した時
    - 判別できなかったが、順序はいつも上記の順番


# 常に音声を取得し続ける

常に音声を取得し続けるためには発声が終了し、結果が出た後にまた `rec.start()` をする必要があります。

```js
const rec = new webkitSpeechRecognition()
rec.continuous = false
rec.interimResults = false
rec.lang = 'ja-JP'

rec.onresult = e => {
  rec.stop()

  for (var i = e.resultIndex; i < e.results.length; i++) {
    if (!e.results[i].isFinal) continue

    const { transcript } = e.results[i][0]
    console.log(`Recognised: ${transcript}`)
  }
}

rec.onend = () => { rec.start() }

rec.start()
```

上記のコードのように、 `rec.onend` で `rec.start()` することで終了後に開始することができます。 `rec.stop()` の位置をfor文の下にすると上手く動作しなかったため注意してください。


# おまけ - audioデータも取得する

[MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) を使って文字列とaudioデータの両方を取得しましょう。ここではaudioデータはbase64に変換しています。

```js:main.js
const onSuccess = stream => {
  let base64 = ''
  let chunks = []

  const mediaRecorder = new MediaRecorder(stream)
  mediaRecorder.onstop = e => {
    const blob = new Blob(chunks, { type: 'audio/wav' })
    const reader = new window.FileReader()
    reader.readAsDataURL(blob)
    reader.onloadend = () => { base64 = reader.result }
    chunks = []
  }
  mediaRecorder.ondataavailable = e => { chunks.push(e.data) }

  const speechRecognition = new webkitSpeechRecognition()
  speechRecognition.continuous = false
  speechRecognition.interimResults = false
  speechRecognition.lang = 'ja-JP'

  speechRecognition.onresult = function (e) {
    speechRecognition.stop()
    for (let i = e.resultIndex; i < e.results.length; i++) {
      if (!e.results[i].isFinal) continue

      const { transcript } = e.results[i][0]
      console.log(`Recognized: ${transcript}`)
      console.log(`Base64: ${base64}`)
    }
  }

  speechRecognition.onspeechstart = () => { mediaRecorder.start() }
  speechRecognition.onaudioend = () => { mediaRecorder.stop() }
  speechRecognition.onend = () => { speechRecognition.start() }

  speechRecognition.start()
}

navigator.mediaDevices.getUserMedia({ audio: true })
  .then(onSuccess)
```

ブラウザに `getUserMedia()` が実装されていないことがあるので注意してください。また途中でエラーを吐くことがたまにあるため、原因がわかった方はコメント等で教えて頂けると幸いです。


# おわりに

私はこの知見を初参加のハッカソン中に得ました。開発が始まって1時間しないうちにパソコンに向かって「おにぎり」と連呼してテストを行っていたので、周囲からは変な人と思われていたでしょう...。何はともあれハッカソンは無事終わりました。この記事によって知見が共有され、どこかで使われることがあるならばとても嬉しいです。