ドライブ用音声チャットボットの開発〜ノイズフルな環境での音声認識〜

## はじめに

１人で車を運転する時、話し相手がほしいと思ったことはありませんか？
今まではチャットボットでまともな会話なんて無理でした。でも、最近は、プログラム初心者でも簡単なチャットボットを作ることが出来るようになりました。
私も音声チャットボットを作って、運転中におしゃべりしてみました。しかし、なかなか上手く行きませんでした。
ここでは、音声チャットボット開発で遭遇した課題と対処の記録を紹介します。

![A_realistic,_photorealistic_depiction_of_the_inter_flipped.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734397/4da06150-1d79-1db3-c0aa-69ef82c78dd1.png)

## 走行中の車内の環境 車だとダメ

想像してみて下さい、時速１００キロで走る車の車内を。
中国自動車道や新名神高速道路など、山岳部を走る高速道路です。

  1. __騒音 〜 うるさいです__
  エンジン音、風切り音、ロードノイズ。高音から低音まで大音量のノイズだらけ。

  2. __通信状態 〜 切れます！__
  山岳部で電波が弱く、車は移動しているので基地局ハンドオーバーも頻繁に。
  もちろん、時々切れます。

  3. __電源 〜 しょぼいです__
忘れちゃいけない電源容量。DC-ACコンバータを使いますが心細い電源容量、ノートPC程度しか動かせません。普通のデスクトップでも動かせそうな気もしますが、車のバッテリに負担をかけたくないし。消費電力の大きいGPUなんてちょっとどうかなと。
  4. __電気的ノイズ 〜 パチパチ、ジリジリ__
電源のノイズも酷いです。マイクにも影響して、パチパチと変な音が入ってきます。

こんな車内環境で、音声チャットボットを動かすには、ノイズに強い音声認識が必要となります。

## 初期の音声認識プログラム

最初に作ったプログラムは、比較的シンプルな構成です。
よくサンプルコードで使われているSpeechRecognitionでGoogleのAPIで音声認識して、gTTSで喋らせる構成です。

```mermaid
graph LR
  Mic-->|オーディオ\nストリーム|SR[SpeechRecognition]
  SR-->|オーディオ\nセグメント|Google
  Google-->|Text| OpenAI
  OpenAI-->|Text|gTTS
  gTTS-->|Audio|Speekr
```
### この構成での課題 〜 車で使えない
この構成で、静かな部屋ならば普通に会話できます。
走行中の車内では、全然だめでした。音声認識がほとんど認識してくれない。

### 音声認識できない原因
1. __音声検出ロジック(VAD)__
デバッグログを大量に仕込んで何度も試したところ、声の検出ロジックに問題がありそうです。SpeechRecognitionでは、サウンドエナジーを使って、マイクの音から声の開始終了を検出しています。
もっと詳しく言うと、まず一番最初、起動時に1秒ぐらいサウンドエナジーを計測して、音量の基準を決めます。その計測した音量レベルを閾値にして音声を切り出す処理になっています。
でもね、走行中の車内なので、周囲の雑音は、時々刻々と変化するので、対応できないわけです。

2. __電気的ノイズ__
車内で録音した音声データを再生して聴いてみると、プチプチという感じの音が頻繁に聞こえます。おそらくエンジンやオルタネータから発生する電気的なノイズが電源から入っているか、電波的にマイクの信号線に干渉しているのかもしれません。

3. __APIコールの接続エラー__
デバッグログを見てみると、通信エラーが大量に記録されていました。騒音が少ない状況だと、それなりに音声認識しようとしているのですが、ConnectTimeoutとかReadTimeoutでAPIコールが失敗しています。しかも、デフォルトTimeoutが600秒とかなので、通信に失敗すると10分間そこで止まっている状況でした。

## 試行錯誤

- __電気的ノイズにはフェライトコア__
とりあえず、Amazonで安いフェライトコアのセットを購入して、電源コンバータと、ノートPCの電源アダプタと、マイクの配線にフェライトコアを付けてみました。なんとなく効果があるような気がします。

- __通信タイムアウト設定とリトライ処理__
googleのAPIとOpenAIのAPIコール部分に、Timeoutを設定しました。これがまた面倒でドキュメント通りに指定しても効かなくて、結局ライブラリのソースコードを追いかけてゴニョゴニョすることになりました。
実際のTimeout値は、Connectを3〜10秒程度、ReadTimeoutを10〜30秒程度に設定しました。
あと、リトライ処理は、めんどうくせーです。ConnectTimeoutとReadTimeoutの時は対応を変えないといけないし、通信エラーじゃなくてAPIのレート制限でエラーになる場合もあります。
やっかいなのが、OpenAIのReadTimeout設定。たまに、OpenAIが長文吐き出す時とか単純に遅い時ってあるので、ReadTimeoutが悩ましい。(この当時はStreamモードじゃなかったので)

- __音声検出処理の改善__
音声区間の識別方法について、ネットの情報を色々と漁ってみました。
サウンドエナジー以外にも、F0(基本周波数)とかゼロ交錯数とかあるんですが、だいたい上手く行かなくて。

https://www.ai-shift.co.jp/techblog/1093


## VADライブラリ
VAD(Voice Activity Detection)について、調べるほどに、機械学習とかニューラルネットでなんとかするような話に落ち着きます。

けど、面倒だし、誰かが良いライブラリを作っているのでは？
ということで、使えそうなライブラリを漁ってみました。

- __librosa.pyin__
https://librosa.org/doc/0.10.1/generated/librosa.pyin.html#librosa-pyin
F0を計算するときに、ついでにvoiced_flagというのも計算できます。これはあまりアテになりませんでした。そもそも使用目的が違うのでしょう。

- __inaSpeechSegmenter__
https://github.com/ina-foss/inaSpeechSegmenter
人の声、音楽、ノイズを判定できるライブラリです。男性女性も区別できるのが興味深いです。
実際に使ってみましたが、今回の用途(車載)にはちょっと精度がいまいちでした。

- __WebRTCVAD__
https://github.com/wiseman/py-webrtcvad
google謹製ライブラリ。ウエブアプリのチャットボット等によく使われているようです。普通に使うなら十分な精度があると感じます。

- __SileroVAD__
https://github.com/snakers4/silero-vad
faster-whisperに組み込まれているVADライブラリです。WebRTCVADより更に高精度と感じました。

## WebRTCVADを使ってみた

WebRTCVADは、チャットボット開発や音声アプリケーションを手掛けるIT技術者にとって、音声検出のための定番ライブラリの一つです。そこで、私も最初に試してみました。この時点ではSileroVADの存在を知らなかったので、手始めにWebRTCVADを使ったわけです。
```mermaid
graph LR
  subgraph "VAD(Voice Activity Detection\n"
    WebRTCVAD[VAD判定\nWebRTCVAD]
  end
  Mic[マイク]-->|オーディオ\nストリーム|WebRTCVAD
  WebRTCVAD-->|オーディオ\nセグメント|Google[音声認識\nGoogle]
```

__実装がめんどくさいぞ！__
実装自体はそれほど難しくありません。ただ、平均値を求めるために、過去10回分の数値を保持して処理する必要があり、コードが少し複雑になる点がやや難点でした。しかし、検出精度は意外と良い感じで、人の声はかなり正確にキャッチします。

__どんな音でも反応するぞ！__
ただ、欠点もありました。人の声以外にも反応してしまうのです。たとえば、マウスのクリック音や衣服の擦れる音、さらには車の中のロードノイズなど、色々なノイズを音声と誤認してしまうことがありました。この部分の調整は、プログラムの改良や設定の工夫が必要になりそうです。

このように、WebRTCVADは比較的使いやすく高精度なライブラリですが、誤検出のリスクがあるので、その点を理解して使う必要があると感じました。

## VOSKをVADとして使ってみた

WebRTC VADの複雑さと、誤認識の問題に頭を抱えた結果、思いついたのがVOSKを組み合わせて使うという方法でした。WebRTCでVADを行い、そこから音声区間を検出するのはこれまで通りですが、次のステップでVOSKに音声認識させて、何か音声として認識できたら、それは確実に音声区間だと判定するようにしたんです。

### 2段階のVAD判定
この手法によって、まず音声区間を特定し、次にその区間で本当に音声が存在するかを確認するという2段階のプロセスを作りました。これで、誤検出が減り、音声区間の検出精度が上がりました。

```mermaid
graph LR
  subgraph "VAD(Voice Activity Detection\n"
    WebRTCVAD[VAD判定\nWebRTCVAD]
    VOSK1[VAD判定\nVOSK]
  end
  Mic[マイク]-->|オーディオ\nストリーム|WebRTCVAD
  WebRTCVAD-->|オーディオ\nセグメント|VOSK1
  VOSK1-->|オーディオ\nセグメント|Google[音声認識\nGoogle]
```

### VOSKの欠点〜遅い！
VOSKによって精度は向上したのですが、 すごく時間がかかるという問題が出てきました。私のCPUが遅いのもあって、1秒の音声を認識するのに3秒かかったりするので、話してから判定完了まで3倍の時間がかかって応答が遅れてしまう。

### 少しでも早く！〜部分的に認識

VOSKに求めることは、性格な音声認識ではなく、人の声の有無だけとなので、オーディオセグメントの先頭部分だけVOSKで処理することで、 判定時間を短縮することが出来ました。これでだいたい1秒のやつが1.5秒とか2秒ぐらいで識別できるようになりました。

### もっと早く！〜並列化
部分認識でも、まだ遅いので、スレッドを使って並列処理することにして、1秒の音声を、およそ1秒ぐらいに判定できるようになりました。並列処理したあとで、元通りに並べ直すのが超めんどくせーです。

```mermaid
graph LR
  subgraph "VAD(Voice Activity Detection\n"
    WebRTCVAD[VAD判定\nWebRTCVAD]
    subgraph "並列処理"
      VOSK1[VAD判定\nVOSK]
      VOSK2[VAD判定\nVOSK]
    end
    SORT[並べ直し\nQueue]
  end
  Mic[マイク]-->|オーディオ\nストリーム|WebRTCVAD
  WebRTCVAD-->|オーディオ\nセグメント|VOSK1
  WebRTCVAD-->|オーディオ\nセグメント|VOSK2
  VOSK1-->SORT
  VOSK2-->SORT
  SORT-->|オーディオ\nセグメント|Google[音声認識\nGoogle]
```

## ノイズ対策〜周波数フィルタを適用してみた
次に試したのは、音声ノイズ対策です。静かな部屋だとかなりの確率で音声認識出来ます。しかし、走行中の車内ではだめです。信号待ちの時は、けっこう認識出来ますが、走り出すと全然だめ。
そこで、走行中の音声をファイル保存して、FFTでどんな周波数が出ているかみてみました。

### FFTで周波数分布を確認してみた
FFTの結果、低周波のノイズがすごく高いレベルで記録されていることが分かりました。 10Hzとか一桁レベルの周波数がものすごくレベル高かったり、 30Hz、50Hzとかこの辺の騒音がすごいということが分かりました。

__マイクの周波数特性にご注意__
これはマイク特性も影響していると考えています。私が使っているマイク仕様を見ると、100Hz以下の低周波からきちんと拾うマイクだったみたいです。低音ノイズが酷い要因の一つでしょう。amazonとかで他のメーカーのマイクの仕様を見ると、100Hz以下を拾わないものが多いです。

### フィルタ処理を実装してみた

低音ノイズが酷いことがわかったので、オーディオデータにフィルター処理をかけることにしました。特定の周波数以上をパスして、下の方をカットするというハイパスフィルターですね。またの名をローカットフィルタ。

__低音をカットしたら認識精度が良くなった！__
このハイパスフィルターも、カットする周波数とか、レベルカットするレベルの設定とか、 そういう数値を設定する必要があるんですけども、 これもですね、なかなか良い数値を見つけるっていうのが結構苦労したんですよ。で、低音をカットしたら、走行中の音声認識の精度が格段に向上しました。

ネットのブログとか見ていると、フィルターで雑音をカットしても精度は変わらなかったって記事が多いんですけど、多分それは、静かな部屋で試しているからじゃないかなと思っています。走行中の車の車内だと、ものすごく効果があります。

## この時点での構成
色々と試行錯誤した結果、VADの構成は以下のようになってます。
ややこしいですね。ソースコードも人に見せられないほと汚くなりました。

```mermaid
graph LR
  subgraph "VAD(Voice Activity Detection\n"
  　Filter1[ローカット\nフィルタ]
    WebRTCVAD[VAD判定\nWebRTCVAD]
    subgraph "並列処理"
      VOSK1[VAD判定\nVOSK]
      VOSK2[VAD判定\nVOSK]
    end
    SORT[並べ直し\nQueue]
  end
  Mic[マイク]-->|オーディオ\nストリーム|Filter1
  Filter1-->WebRTCVAD
  WebRTCVAD-->|オーディオ\nセグメント|VOSK1
  WebRTCVAD-->|オーディオ\nセグメント|VOSK2
  VOSK1-->SORT
  VOSK2-->SORT
  SORT-->|オーディオ\nセグメント|Google[音声認識\nGoogle]
```

### VADに注力する言い訳
この構成になった要因は、Google Speech RecognitionへのAPIコールを最低限に抑える必要があったためです。Google Speech Recognitionは、レート制限と回数制限があるし、無料範囲で抑える為にも、音声かどうかわからないオーディオデータをAPIコールしないようにする必要があります。また、通信が増えると、車で移動中の通信切断の影響も大きくなります。なので、確実に音声が含まれるオーディオデータをAPIコールする必要があります。

### この構成における課題 〜 やっぱりVOSKが遅い！
この構成のボトルネックは、VOSKの遅さです。並列化してレイテンシを上げていますが、それでも、1秒のオーディオを判定するのに、0.5〜1.5秒程度の時間がかかります。
CPUを現代の高速なやつに交換すれば簡単に解決しそうなんですが、ノートPCだけじゃなくてラズパイのようなIOTデバイスでも動作するものにしたかったので、古いCPUで、クロックはあえて1.4Ghzに抑えています。

## 遅いVOSKの処理回数を減らしたい
そもそも、遅いのを承知でVOSKを使っているのは、WebRTCVADが声以外の音も拾うからです。（普通に使うなら十分な精度だとおもいますが）仕方無しに、VOSKで二次判定を行っています。
そこで、VADの精度が上がれば、VOSKで処理する回数を減らせるのでは？と考えました。

## SileroVADを使ってみた

もっと良いVADを求めてネットをさまよい歩き、SileroVADをみつけました。
これは、Faster-Whisperでも使われているライブラリです。
__CPUでも実用的な処理時間__
GPUが必要かと思いましたが、自分のPCで試したところ、1.4GhzのCPUでも十分な処理速度であることが判りました。WebRTCVADの10倍程度の遅い時間ですが、処理単位の32msのオーディオフレームで0.1ms以下の時間なので十分実用範囲です。
__WebRTCVADより簡単な判定処理__
SileroVADは、webrtcvadのような過去数回分の結果を合わせて判定するというような面倒な処理が不要なのも利点の一つです。
__WebRTCVADよりも高精度__
WebRTCVADでは、マウスのクリック音や衣服の擦れる音にも反応していましたが、SileroVADではちゃんと無視してくれました。完全ではないですが、WebRTCVADよりも遥かによい精度で人の声だけを拾ってくれます。
__VOSKの処理回数を減らせた！__
SieroVADを使うことによって、VOSKの処理が大幅に減って、以前の構成よりスムーズな会話ができるようになりました。webrtcvadを使っていた時、10秒〜20秒待たされることも多かったのですが、SieroVADでVOSKの処理が減った結果、2〜3秒で音声認識が完了するようになりました。

## VOSKの高速化

VOSKの処理回数を減らせたと言っても、以前としてVOSK処理は残っており、遅いことに変わりはありません。レイテンシをもっと改善するために、VOSKの処理時間を短縮できないか試行錯誤しました。

__コマンドワード検出なら早いぞ？__
音声から特定のコマンドワードを検出する方法なら、かなり早い応答が得られる事がわかりました。
コマンドワード検出というのは、音声から文章を書き起こすのではなく、特定の単語があれば反応する処理です。「Ok google!」とか「Hey Siri」とかですね。
__コマンド検出の使い方を工夫する 〜 声があることが解ればええねん__
といっても、通常の会話なので、特定のキーワードだけ識別すれば良い訳ではありません。けれど、なにか音声のようなものを識別できればいいので、たとえば、「あ」「い」「う」「え」「お」みたいな一文字コマンドを与えて試してみました。そうすると、VOSKはエラーを出して認識しませんでした。どうやらVOSKの辞書にある単語でないとダメなようです。
__辞書ファイルから短い単語を抽出して使え！__
そこで、VOSKの辞書ファイルの単語リストから、2〜3文字でカタカナの単語を全部抽出して、コマンドワードとして与えてみました。そうすると、9割くらいの確率で、オーディオデータから、カタカナワードを拾うようになりました。処理時間も2秒の音声を処理するのに0.3秒程度と十分な速度でした。

### 早くなったぜ！！ 〜 しかし完全ではない・・・

VOSKのコマンドワード方式による有声無声判定を採用することで、音声認識のレイテンシは格段に向上しました。が、この９割程度というのが問題で、音声の一部が欠けて認識されることが多いので、もうちょっとなんとかならないか考えました。

### VOSKも２段階

そこで、VOSKの処理を２段階に分けて、コマンドワード方式で無声と判定されたら、通常の処理で音声判定を行うようにしました。これで、ときどきレイテンシは落ちますが、満足のいく音声認識精度を得ることができました。
さらに、SileroVADとVOSKでは、特性の違うフィルタ処理を行うことで、ほんのちょっと精度を高めています。

```mermaid
graph LR
  subgraph "VAD(Voice Activity Detection\n"
  　Filter1[ローカット\nフィルタ]
    SileroVAD[VAD判定\nSileroVAD]
    subgraph "並列処理"
    　Filter21[ローカット\nフィルタ]
      VOSK1a[VAD判定\nVOSK]
      VOSK1b[VAD判定\nVOSK]
    　Filter22[ローカット\nフィルタ]
      VOSK2a[VAD判定\nVOSK]
      VOSK2b[VAD判定\nVOSK]
    end
    SORT[並べ直し\nQueue]
  end
  Mic[マイク]-->|オーディオ\nストリーム|Filter1
  Filter1-->SileroVAD
  SileroVAD-->|オーディオ\nセグメント|Filter21
  SileroVAD-->|オーディオ\nセグメント|Filter22
  Filter21-->VOSK1a
  Filter22-->VOSK2a
  VOSK1a-->VOSK1b
  VOSK2a-->VOSK2b
  VOSK1b-->SORT
  VOSK2b-->SORT
  SORT-->|オーディオ\nセグメント|Google[音声認識\nGoogle]
```

最終的に、静かな部屋で音声認識することに比べると精度は低いけれど、走行中の車内でも結構認識する音声認識プログラムになりました。十分とは言えませんが、一旦これでよしとします。


## 将来課題
また時間があったら、これを試してみよう

https://tam5917.hatenablog.com/entry/2020/01/25/010629