日本語音声のマイク入力をオフラインでリアルタイム音声認識：「VOSK」を JavaScript（Node.js）で扱う

この記事の内容は、オフラインでリアルタイム音声認識ができ、日本語にも対応している以下の「VOSK」を試してみた話です。

●VOSK Offline Speech Recognition API
　https://alphacephei.com/vosk/
![VOSKの公式ページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ae6c8944-54fa-9d86-7c08-32c0bc2f089c.png)

そして今回の記事で、VOSK を扱う開発言語・環境は「JavaScript（Node.js）」です（自分がよく使っているから、という選定理由です）。
なお、対応している他の開発言語などは、公式ページの「[Installation](https://alphacephei.com/vosk/install)」を見ると確認でき、例えばスマホ向け（Android・iOS）や Python・Java・C# などもあるようです。

## VOSK を「JavaScript（Node.js）」で扱う
それでは、タイトルや冒頭にも書いた JavaScript（Node.js）で VOSK を扱う話へと進んでいきます。

### 公式ドキュメントの情報を確認する
公式ページ内の「[Installation](https://alphacephei.com/vosk/install)」で、以下の「Javascript/Nodejs」と書かれた部分を見ていきます。
![InstallationのJavascript/Nodejs](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0e15128e-d99d-119e-40ea-c5018eddc8e0.png)


上記のキャプチャ画像のとおり、準備はとてもシンプルです。
以下の npmコマンドを実行するのみ、となります。

```zsh
% npm install vosk
```

また、JavaScript用のサンプルが GitHub の以下にあるという情報が書かれています。

https://github.com/alphacep/vosk-api/tree/master/nodejs/demo
![JavaScript用の公式サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/fa46a554-a61d-62eb-01f6-82b9700fb0f7.png)

### まずは軽いお試し
#### マイクからの入力を利用するサンプルをそのまま利用してみる
上記のサンプルの中で、まずは 1つを選んで試してみます。
マイクから入力して動かしてみたかったので、それができそうな名前の「[test_microphone.js](https://github.com/alphacep/vosk-api/blob/master/nodejs/demo/test_microphone.js)」を試すことにしました。

●vosk-api/test_microphone.js at master · alphacep/vosk-api
　https://github.com/alphacep/vosk-api/blob/master/nodejs/demo/test_microphone.js

それでは、npmコマンドでパッケージのインストールを行い、その後に上記のサンプルを nodeコマンドで実行してみます。
サンプルに関して、最初の行の `var vosk = require("..");` を `var vosk = require("vosk");` に書きかえるということだけは、実行前に対応しました。

![お試しの最初の実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/5188299b-5dac-b96f-c15d-718993566921.png)

そうすると、上記のとおり `Please download the model from https://alphacephei.com/vosk/models and unpack as model in the current folder.` というメッセージが表示されました。

どうやら https://alphacephei.com/vosk/models という URL にアクセスし、音声認識用の機械学習モデルをとってくる必要があるようです。

#### 日本語音声用の機械学習モデルを取得
それでは、[上で出てきた URL](https://alphacephei.com/vosk/models) にアクセスし、機械学習モデルを取得することにします。

![機械学習モデルのページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/db7623a4-828f-d752-3fb8-b0191007fa68.png)

上記のページの「Model list」を下へたどっていくと、日本語用の「vosk-model-small-ja-0.22」という項目がありました。
![日本語用の機械学習モデル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ed5a07f9-9b0c-c262-8df2-7be581e1653e.png)
この部分をクリックすると、「vosk-model-small-ja-0.22.zip」という名前の ZIPファイルがダウンロードでき、その ZIPファイルを解凍すると以下のファイルなどを得られました。
![ZIPファイルを展開して得られたファイルなど](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2c9115c8-3eb7-876e-42f7-454e244df5db.png)

それでは、ここで得られたファイルなどを使って、お試しの続きを進めていきます。

#### サンプルを使ったお試しの続き（音声認識が実行できた状態）
先ほどのサンプルの JavaScript の処理をあらためて見てみると、 `MODEL_PATH = "model";` という部分があるため、nodeコマンドを実行したフォルダと同じ階層に「model」という名前のフォルダを作成し、そこに ZIPファイルを解凍して得られたファイル一式を置けば良さそうに見えます。

そのようにしてみてから、再度、nodeコマンドでサンプルを実行してみると、以下のように日本語の音声認識の出力を得ることができました。
（ちなみに、この時にテストでしゃべった内容は「マイクのテスト」⇒「日本語の音声認識のテスト」⇒「マイクのテスト」という 3つの文で、適当な間を空けてしゃべりました）
![日本語の音声認識が実行されている様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9773b1ce-7969-75d2-9e5f-76a2d133504c.png)

結果を見てみると、しゃべった内容がうまく認識され、テキストで出力されているのが分かります。

さらに、少ししゃべる内容を変えた別のお試しもやってみました。
こちらは動画で処理の様子をキャプチャしたものですが、画面キャプチャソフトの設定で、デスクトップ上の画面キャプチャと PC のマイク入力とを、合わせて 1つの動画に収録しています。

https://twitter.com/youtoy/status/1541751906254848001

動画の中でマイクにしゃべっている内容は、「オフラインでのマイク入力の音声認識を試します」⇒「マイクのテストです」⇒「日本語の音声認識のテストです」となっています。

とりあえずの簡単なお試しでしたが、この内容なら精度・処理速度もすごく良い感じでした。
さらに、これを活用した作品を作ってみたいな。

#### 用いたプログラム（書きかえ前の内容もコメントアウトして残したもの）
今回実行した、JavaScript（Node.js）のプログラムを以下に書いておきます。
上で書いていたように、冒頭の1行目を書きかえただけで、あとは元のサンプルそのままです（個人的には、ちょこちょこ書き直したい部分とかがある...）。

```javascript
// var vosk = require("..");
var vosk = require("vosk");

const fs = require("fs");
var mic = require("mic");

MODEL_PATH = "model";
SAMPLE_RATE = 16000;

if (!fs.existsSync(MODEL_PATH)) {
  console.log(
    "Please download the model from https://alphacephei.com/vosk/models and unpack as " +
      MODEL_PATH +
      " in the current folder."
  );
  process.exit();
}

vosk.setLogLevel(0);
const model = new vosk.Model(MODEL_PATH);
const rec = new vosk.Recognizer({ model: model, sampleRate: SAMPLE_RATE });

var micInstance = mic({
  rate: String(SAMPLE_RATE),
  channels: "1",
  debug: false,
  device: "default",
});

var micInputStream = micInstance.getAudioStream();

micInputStream.on("data", (data) => {
  if (rec.acceptWaveform(data)) console.log(rec.result());
  else console.log(rec.partialResult());
});

micInputStream.on("audioProcessExitComplete", function () {
  console.log("Cleaning up");
  console.log(rec.finalResult());
  rec.free();
  model.free();
});

process.on("SIGINT", function () {
  console.log("\nStopping");
  micInstance.stop();
});

micInstance.start();
```

### 【追記】 記事公開後の追加のお試し
記事を公開した後、上で用いていたサンプルの中身を見直してみました。
そして、ここで得られた音声認識の結果を使った、何らかの続きの処理を付け加えていくために、その下準備となることを少しやってみました。

#### サンプルプログラムのちょっとした書きかえ
サンプルを見ていて、 `micInputStream.on()` という処理の中の `rec.result()` を使ってやると良さそうかと思い、以下のような書きかえをして実行してみました（あと、所々で var が混じってたのが気になったので、その部分の書きかえもしてみたり）。

```javascript
const vosk = require("vosk"),
  fs = require("fs"),
  mic = require("mic");

const MODEL_PATH = "model",
  SAMPLE_RATE = 16000;

if (!fs.existsSync(MODEL_PATH)) {
  console.log(
    "Please download the model from https://alphacephei.com/vosk/models and unpack as " +
      MODEL_PATH +
      " in the current folder."
  );
  process.exit();
}

vosk.setLogLevel(0);
const model = new vosk.Model(MODEL_PATH),
  rec = new vosk.Recognizer({ model: model, sampleRate: SAMPLE_RATE });

const micInstance = mic({
  rate: String(SAMPLE_RATE),
  channels: "1",
  debug: false,
  device: "default",
});

const micInputStream = micInstance.getAudioStream();

micInputStream.on("data", (data) => {
  if (rec.acceptWaveform(data)) {
    const recognizedResult = rec.result();
    const recognizedText = recognizedResult.text;
    console.log(recognizedResult);
    console.log(recognizedText);
  } else console.log(rec.partialResult());
});

micInputStream.on("audioProcessExitComplete", function () {
  console.log("Cleaning up");
  console.log(rec.finalResult());
  rec.free();
  model.free();
});

process.on("SIGINT", function () {
  console.log("\nStopping");
  micInstance.stop();
});

micInstance.start();
```

書きかえ後のプログラムを動かしてみたところ、以下の矢印で示した部分で、認識結果を取り出せているのが分かります。

あとは、結果の文字列にスペースが入っているように見えるので、それは除去する必要がありそうです。
そして、認識結果の中に何か特定のキーワードが含まれるかをチェックして、そのキーワードの有無に合わせた処理をするとか、そういったことをやれば良さそうかな。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ae78287e-30ae-3493-1e7d-8bd46a065f62.png)

### 【追記2】 日本語音声用のビッグモデルの追加
Twitter で情報を見かけて知ったのですが、ビッグモデルが追加されたようです。

https://twitter.com/youtoy/status/1547562789992280065