この記事の概要を簡単まとめ!
- “YukariWhisper”はv0.0.4にアップデートされた
- 使っていて気になる「異常な音声認識」の発生
- 喋っていないタイミングで発生し、異常に認識時間が長い
- 仮定:タイムアウトを実装すれば認識が快適になる
- 調査:音声認識している箇所を探し出す
- 改造:音声認識直後にタイムアウトのソースコードを実装、iniもパラメータを追加
- 検証:タイムアウト時間を変更して正しく動作するかチェック
- 結果:実装箇所は狙い通り、動作は想定通り
- 異常の発生は少しの改造で99%抑えられる
faster-whisperをベースにゆかコネ用に調整されたYukariWhisperは、様々な調整を加えて利便性が向上した。現在v0.0.4に更新されたことを確認している。ただv0.0.4は軽微な修正であり、現在v0.0.3を使用している人は更新する必要がないと連絡されている。あとはベースとなっているfaster-whisperのアップデートを待つだけの状態であろう。
音声認識精度が非常に高い反面、要求されるリソースが高いことで積極的に使用する人も少ないこれだが、1つ気になっていたことがある。時々喋ってもいないのに異常な結果を出力することがあり、その場合は異常に認識時間がかかっている。これをどうにかできれば使い勝手も上がるのではないかと考えた。そこで再び、YukariWhisperの改造に乗り出した。
ひとっ飛びできる目次
「時間で区切る」はどのプログラミングでも有効
YukariWhisperに慣れてきた
現在のYukariWhisper
TYAPA氏によって製作されたYukariWhisperの存在が確認されたのは、ゆかコネで使用できる音声認識の結果比較を行った記事から、1月中旬ほどである。調査段階でv0.0.2のそれを使用したが、その際は単に使用できるようにしただけのため、詳細な調査は行っていない。その後YukariWhisperについて解説した最初の記事を作成し、詳細にその中身を調査していった。当時のバージョンはまだ製作段階のようなもので不備もあったが、それはPythonを元々触っていたこともあって自分で対処し、すぐ解消した。
その後は私の記事が効いたか定かではないが、v0.0.3からは量子化の規格をint8_flaot16
をデフォルトとし、GPUの世代毎に異なるCUDA CC1)YukariWhisperのGitHubでは、使用可能なGPUはGTX1000番台以降と解説している。これに従うとPascal以降が使えるものとなるが、それ以降の世代のCUDA CCバージョンは次の通りである。 Pascal: 6.1, Turing: 7.5(1600番台含む), Ampere: 8.6, Ada Lovelace: 8.9 これはGPUのグレードによらず、世代毎で固定である。を参照して、Pascalはint8_float32
, Ampere以降はint8_bfloat16
で自動で切り替えるように変更された。またNao氏からのプルリクであるゆかコネのポート番号自動追従機能を実装し、OSCサーバのポートがVRChatのOSCを有効している場合、ポートが被ることによって動作しないのを回避するために設定ファイルからそれを変更可能になった。
現在はv0.0.4にアップデートされているがこれはv0.0.3ユーザーは不要なアップデートである。というのも、これはfaster-whisper側のミスの修正を行ったもので、現在v0.0.3ユーザーには影響がないものであるためだ。また、faster-whisper自体も正式バージョンということで1.0.0になっているようであるが、まだ安定することを確認できていないため現行は0.10.1に合わせている。安定して利用できることを確認できた場合はバージョンを上げ、その頃にはYukariWhisperも1.0.0になるかもしれない。
「異常な音声認識」の出現
YukariWhisperも、ゲームによっては負荷が軽いものがあるのでその時には使用しているが、使用時に気になることがあった。それが異常な音声認識である。これの発生タイミングは不定期不確定で、しかし発生したときはログに記録される内容から判断しやすいものになっている。これについてはもはやfaster-whisper側の問題であり、TYAPA氏がどうにかしても抑えるのがやっとなほどの厄介なノイズである。
このノイズについて既に事前対策として、確認できた結果をあらかじめNGワード一覧というテキストファイルで用意し、それを読み込んで参照する形でノイズを除去するようにPythonスクリプトが組まれている。設定ファイルの側では音のエネルギーレベルの閾値(パラメータは合計5個)を設定することで、マイクから取得されたノイズ(環境音やブレスなど)を認識しないように設定することができる。この部分は感覚調整になるため、使用しながら適宜調整する。
とはいえ、外部音声入力はある程度これで制御できても、内部的なノイズに関しては除去が非常に難しい。いわば電気的なノイズであるこれを完全に除去するとなれば、多くの機器はUSBで接続するため、電磁シールドを施したコネクタや高級なケーブルを使わなければ実現できないはずだ。とはいえノイズが許されない世界に存在するわけではないこと、それを実現するための設備投資は馬鹿にならないものであることを考慮すれば、ある程度は許容しなければならないことであろう。
喋っていないタイミングで発生し、異常に認識時間が長い
ノイズが発生する原因を追究し、それを解消するためにまずはログを取ることにした。ただし負荷が強いため、負荷が軽量なゲームでの実験となる。そのログの一部で次のようなものが取得できた。
以前から使用していて、異常な結果が出力されることに関しては既に確認していた。今回調査するにあたってログを取得し、その結果と実際に動画を見ながら確認していくと、喋っていないのに勝手に認識したものがいくつか存在した。結果出力時は認識にかかった時間も同時に表示され、それを参照すると結果(長さ)に対して時間が明らかに合わないものがいくつも存在した。中には喋った内容と全く異なる認識結果を出力しているものがあり、これらからノイズの影響が明らかにあると分かる。
スポンサーリンク
スポンサーリンク
仮定:タイムアウトを実装すれば認識が快適になる
このことからエネルギー閾値だけでは到底ノイズ除去を完璧にすることは難しいと分かる。よってNGワードに異常結果を登録することで回避こそできるようになるが、それでも異常認識中は音声認識が詰まり、リアルタイム性が損なわれる問題がある。これではゆかコネの目指すものが実現できなくなる。だがこのログを観察して分かったのが、異常な結果は軒並み認識に時間がかかっているということである。そこからある仮定を行った。
「ある一定以上認識に時間がかかるなら、その結果を破棄して次の認識に移行する」ことで、YukariWhisperの快適性を上げることができる、ということだ。つまり、異常な音声認識には時間がかかっているという点に狙いをつけ、YukariWhisperにタイムアウトを実装してやれば今よりもずっと快適になり、使用したいと思うユーザーが増えるはずだ。そうと決まれば、早速改造を行うことにした。
YukariWhisper(無許可)改造計画2
音声認識している箇所を探す
実装するにあたって、まずは音声認識が実際に行われる個所を探す必要がある。初回改造の時にメインのPythonスクリプトは中身を確認済みのため、どこで何をするのかという大まかな機能は分かっている。まず目をつけたのは音声認識のメイン部分であるrecognizer.pyである。関数recognize_worker
が音声認識実行スレッドとして機能しており、try-exceptのtryセクション内、if-elseのelse側にWhisperの処理が書かれている。その部分を抽出して確認する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 上のコードから else: start_t = time.time() #audioデーターのフォーマットをwhisper用の16k 16bitに変換する frame_bytes = audio.get_raw_data(convert_rate=16000) speech_array = np.frombuffer(frame_bytes, dtype=np.int16) if self.vad.is_speech(speech_array): #whisperで認識 segments = self.model_wrapper.transcribe(speech_array) for segment in segments: # uniocode Normalization normalized_text = unicodedata.normalize('NFC', segment.text) if self.check_ng_words(normalized_text): continue if self.ini_file.debug_out_text: print(f"whisper[{(time.time()-start_t):.4f}]({len(segment.text)})" + normalized_text) # send text to websocket self.wsocket.send(normalized_text) # 下に続く |
上記は10行目以降(実際は118行目)のfor文から、NGワード判定と結果出力のコードがあり、ここでWhisperの目に見える処理をしていると分かる。ということは、大抵はその少し前で目に見えない処理、ここでは音声認識について処理を行うはずだと分かる。したがって最も怪しいのは、segments = self.model_wrapper.transcribe(speech_array)
である。だがこれ自体は関数でしかなく、具体的な処理内容は不明である。
実は枠外なのだが、このPythonスクリプトでは頭で他のPythonスクリプトをモジュールとして読み込んで使用している。その際、whisper_utils.pyをモジュールとして読み込み、WhisperModelWrapper
クラスをロードし、それをmodel_wrapper
として宣言している。それの関数であるtranscribe(speech_array)
を実行しているため、調べるべきはwhisper_utils.pyに移る。
スポンサーリンク
スポンサーリンク
whisper_utils.pyで処理が行われる
次にwhisper_utils.pyを確認する。中身は非常に簡素である。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from faster_whisper import WhisperModel # faster-Whisper本体 class WhisperModelWrapper: def __init__(self, model_size_or_path, dev, type, index): #print(f"compute_type: {type}") self.model = WhisperModel(model_size_or_path, device=dev, device_index=index ,compute_type=type) def transcribe(self, audio): segments, _ = self.model.transcribe( audio=audio, beam_size=5, language="ja", without_timestamps=True, ) return segments |
目的のtranscribe(self, audio)
の中身は、見ただけでは果たして何をしているかわからない。しかしここでようやくfaster-whisperが登場する。ということは、この関数についてリファレンスが存在するはずである。残念ながら公式リファレンスではよくわからなかったが、検索するとfaster-whisperのパラメータについて調査した人がおり、そこにはtranscribeメソッドのパラメータの解説もしていた。
詳細は正直なところ見てもよくわからなかったのだが、しかしこれで音声認識を実行する箇所が分かった。これにより再び見るべき場所をrecognizer.pyに移す。
結果を比較後にタイムアウトを実施
recognizer.pyの先の箇所、segments = self.model_wrapper.transcribe(speech_array)
を再度確認する。ここが実際に音声認識を実行する部分となる。print()
を利用してデバッグしたところ、ここにかかる時間はミリ秒レベルのものだった。最も時間がかかっているのがNGワード判定の部分である。これをtime()
メソッドを使った時間測定で、NGワード判定後に時間判定を行う。これは以下の通りに実装した。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 上のコードから segments = self.model_wrapper.transcribe(speech_array) for segment in segments: # uniocode Normalization normalized_text = unicodedata.normalize('NFC', segment.text) if self.check_ng_words(normalized_text): continue # 時間が指定秒以上かかる場合は結果を破棄 ここで判定しないと正しく動作しない if round((time.time()-start_t), 1) >= self.recognizers.recognition_timeout: continue if self.ini_file.debug_out_text: print(f"whisper[{(time.time()-start_t):.4f}]({len(segment.text)})" + normalized_text) # 下に続く |
追加部分はif round~continue
までである。時間判定では後に設定ファイルと各影響のあるPythonスクリプトへの追加も行うが、一般的な時間の扱いである小数点第一位を基準に、(現在時間-開始時間)を行い、それ以下の値は四捨五入によってカットし、設定ファイルに入力したタイムアウトの制限時間(小数点第一位まで入力)を参照する形式としている。もし指定時間以上かかっている場合はその時点で結果を破棄し、次の音声認識を開始するよう処理を追加した。実装はあまりにも簡単であった。
しかしこれだけでは動かない。このまま実行するとエラーになるため、他の部分にも手を加えなければならない。
スポンサーリンク
スポンサーリンク
yukariwhisper.iniと各影響のあるPythonスクリプトへの追加
タイムアウトしたい時間は環境によって異なるものになるため、任意に変更できるパラメータとして扱いたい。よってyukariwhisper.iniには新たなパラメータとしてrecognition_timeout = 5.0
をセットした。これは小数点第一位まで入力する。同時に、この値を使用するにあたって、各影響のあるPythonスクリプトにこれを追加する。追加しないと未定義エラーにより動作しないため、必須作業である。
まずsetting.pyに初期値をセットする。これはclass inifile_settings:~def __init__
セクション内の任意の場所に記述していいが、設定ファイルの順序的に
energy_threshold_Low = 10.0
の下に追加するので、energy_threshold_Low = 100
の下に挿入するといい。次に関数def parse_recognizer(self):
の終端にself.recognition_timeout = ini_recognizer.getfloat('recognition_timeout')
を追加し、値をrecognizer.pyに渡せるようにしておく。
次にrecognizer.pyにも同じものを追加する。#音声認識器のパラメータ設定
セクションの終端に、self.recognizers.recognition_timeout = self.ini_file.recognition_timeout
を追加する。これにより先に追加したタイムアウト処理が正常に動作するようになり、エラーも発生しなくなる。これで準備が完了する。
タイムアウト実装試験開始
試験動画の掲載
試験動画はタイムアウト時間を5秒と2秒の2つパターンを設定し、それぞれの場合で音声認識を行い、その結果がどのように変わるかを確認した。
タイムアウト時間:5.0秒
タイムアウト時間:2.0秒
5.0秒は私が想定するデフォルト設定で、異常な音声認識結果が5秒以上かかるものが多いことから来ている。この場合5.0秒以内に収まってしまうノイズには対応できないが、これにより殆どの異常な音声認識を途中で破棄することができるようになる。この秒数については、場合によっては4.0秒にセットしても通常通り機能するとみている。ただし、faster-whisperの仕様上、まれに通常の音声認識さえも長時間かかってしまうことがあるため、それがタイムアウトに引っかかってしまうとリアルタイム性と正確性が失われる可能性がある。しかしこれも許容しなければならない「誤差」として、現時点では一旦保留とする。
2.0秒のものは、タイムアウトが正確に機能するかどうかを実験したものである。ノイズを意図的に発生させることは難しいため、手動で2.0秒以上かかる音声認識を行い、その結果が表示されなければ実験は成功としている。それは実際その通りとなり、2.0秒以上かかったものは確かに破棄され表示されなかった。これによって意図したことが正確に動作していると確認が取れた。よって、今回の改造は成功と言える。
スポンサーリンク
スポンサーリンク
今後の改良への考察
今回の改造は実のところ、暫定処置のようなものである。ただ、内部構造を読み取った結果最適な位置にタイムアウトを実装したので、余計な負荷や遅延が発生することはないはずだ。しかしこれだけで終わらせると発展しないため、もっと快適に使用するために考察が必要である。
タイムアウトのデフォルトタイムは5.0秒としているが、話し方によってはこれをもう少し短くしても効果があると考えられる。faster-whisperは割と早口でも認識精度が良いため、スムーズに話せる人であれば4.0秒などにすればノイズ発生率を低くしつつ正確な音声認識ができるはずだ。逆にゆっくりめに喋る人であればもう少し猶予を長くするといい。ただしこの場合はノイズ発生率が必然的に高くなるため、その対応はNGワード判定も利用した複合的なやり方になる。面倒であるが、この方法が確実ではある。
また、所謂「ブレスノイズ」を考えたとき、短すぎる場合も結果の破棄対象とするかどうかを考えている。この場合はカットタイムを1.0秒以下でセットすることになると考えている。しかしあまり長いと通常の音声認識も破棄することになるため、実装するかどうかは実際に使って弊害があると確認できた場合、あるいはユーザーが欲しいと言ったときくらいである。
YukariWhisper自体の改良としては、パラメータの動的変更対応であるがこれはおそらく難しいものである。設定ファイルの読み込みは最初に行って以降は参照しない形式であり、NGワードについても同様の動作となる。そのため設定を変更した場合はYukariWhisper自体を再起動する必要がある。再起動はそのたびに多少時間を消費するのと、GPUに対する負荷を考えた場合頻繁に行うものではない。しかしそもそも、一度設定したパラメータはそうそう変えないことと、99%正常で1%異常というくらいの比率であるならそこまで気にするものではないので、これも実装しなくてもまだ問題ないであろう。
TYAPA氏が前から気にしている、音声認識に使用するデバイスをリスト取得した際に日本語の文字化けが生じる件については、調査を行い書いてみたものの、解決するコードを実装するには至らなかった。ヒントとしてはPyAudioの文字化け修正方法を書いたnoteがあり、これをどうにかうまく改造することで実現は可能になると思われる。ただ、そもそもこの問題は解決不可能な事象の可能性もある。デバイス選択時に文字化けしていてもfaster-whisperの動作そのものには影響せず、デバイス選択も文字化けしていない部分を参照すれば正しく選択できることから、しばらく保留していても問題ないであろう。
異常の発生は少しの改造で99%抑えられる
faster-whisperならびにYukariWhisperは、優秀な音声認識である反面、どうしてもAI特有のガバが発生しやすく、それによって利便性が低下することはよくあることだ。加えて元々の要求されるスペックも高いものであり、その懸念点によって使用者が少なく、使用していたがやめてしまったという報告も聞いている。要求スペックに関しては、調査した結果一般ユーザー向けのGTX/RTXシリーズではなく、旧名Quadro, 現在はワークステーション系に分類される「NVIDIA [世代の頭文字][モデルナンバー]」(Ada Lovelace以降命名法則が異なる)を利用すれば消費電力やPCIeレーン数を気にすることなく解決できるものと見ている。ただしその場合はGTX/RTXのエントリーモデルよりもできることが少なくなるが。
AI特有のガバは元のAI本体の性能(もっと言えばソースコード)が改善されない限りは修正が難しいものであり、それゆえ一般ユーザーがどうにかできるものではないことが多い。だがYukariWhisperに関しては既に何度も改造を行った関係で、特に躊躇なく改造することができるようになった。今回施した改造は異常な音声認識結果と音声認識にかかる時間の関係性を見つけたことから、タイムアウトを実装すればそれを除去できるという仮定から行ったものだ。
その結果は合っていた。実装にあたってはソースコードを読む必要はあったが、追っていけば何をしているかが分かり、タイムアウトの適切な挿入位置も判明し、それにより効率的な記述も可能となった。この改造も全体で見れば暫定処置的なものであるとは思うが、しかし何もしないよりは明らかにいい。異常の発生は少しの改造で99%抑えられる。言いすぎなような気もするが、実はそうでもなかったりする。もっとも、負荷の関係で使用タイミングが限定されているため、今欲しいのはHEVCもできる軽量なGPUである。それさえあれば、快適な環境下での音声認識ができるはずだ。
以上、ゆかコネ検証報告:YukariWhisper異常認識除去改造編、であった。完璧なものはできないが完璧に近づけることはできる。やれることはやっていくべきだ。
KIBEKIN at 00:00 Mar. 5th, 2024
スポンサーリンク
脚注
本文へ1 | YukariWhisperのGitHubでは、使用可能なGPUはGTX1000番台以降と解説している。これに従うとPascal以降が使えるものとなるが、それ以降の世代のCUDA CCバージョンは次の通りである。 Pascal: 6.1, Turing: 7.5(1600番台含む), Ampere: 8.6, Ada Lovelace: 8.9 これはGPUのグレードによらず、世代毎で固定である。 |
---|