オープンソースのウェイクワード検出ライブラリ「openWakeword」を試す
以下で別のウェイクワード検出ライブラリを試してたんだけど、一番最後にちょっとだけ触れてたやつ。
GitHubレポジトリ
openWakeWord
openWakeWordは、音声対応アプリケーションやインターフェースの作成に使用できるオープンソースのウェイクワードライブラリである。実環境で効果的に機能する一般的な単語やフレーズの事前学習済みモデルが含まれている。
これを使っている別のプロジェクトを見つけて、ウェイクワードを変更したいので、トレーニングをやってみたい。
一旦ローカルのMacでやってみるけど、多分ライブラリ周りとかでLinuxでやったほうがいいっていう結果になりそうな気はする。
作業ディレクトリを作成
$ mkdir openwakeword-test && cd openwakeword-test
あとでインストールする
仮想環境を作成
$ python -m venv venv
$ source venv/bin/activate
パッケージインストール
$ pip install openwakeword
レポジトリにサンプルコードがあるのでレポジトリをダウンロードしてサンプルコードのディレクトリに移動
$ git clone https://github.com/dscripka/openWakeWord
$ cd openWakeWord/examples
サンプルコードはpyaudioが必要、あと、tflite-runtimeも必要になるのでインストール。
$ pip install pyaudio
ではサンプルコードを実行
$ python detect_from_microphone.py
しばらく待ってるとエラー
ValueError: Tried to import the tflite runtime for provided tflite models, but it was not found. Please install it using `pip install tflite-runtime`
調べてみると、ウェイクワードのモデルはTensorFlow Liteで作成されているらしく、これを動かすためのランタイムが必要になる。ただ、これがApple Silicon向けにはどうも提供されていないらしい。
$ pip install tflite-runtime
ERROR: Could not find a version that satisfies the requirement tflite-runtime (from versions: none)
ERROR: No matching distribution found for tflite-runtime
自分でビルドする必要がある様子。
で、ウェイクワードのモデルはTensorFlow Lite形式だけじゃなくてONNX形式でも提供されているので、サンプルコードではこれを指定できるようになっている。
$ python detect_from_microphone.py --inference_framework=onnx
エラー
onnxruntime.capi.onnxruntime_pybind11_state.NoSuchFile: [ONNXRuntimeError] : 3 : NO_SUCHFILE : Load model from /Users/kun432/work/openwakeword-test/.venv/lib/python3.12/site-packages/openwakeword/resources/models/alexa_v0.1.onnx failed:Load model /Users/kun432/work/openwakeword-test/.venv/lib/python3.12/site-packages/openwakeword/resources/models/alexa_v0.1.onnx failed. File doesn't exist
どうやらモデルファイルをダウンロードする必要がある模様。モデルをダウンロードするスクリプトは用意されていないので、Usageのコードからこんな感じで作成。
import openwakeword
openwakeword.utils.download_models()
実行するとこんな感じでダウンロードされる。
$ python download.py
embedding_model.tflite: 100%|█████████████████████████████████████████████████████████| 1.33M/1.33M [00:00<00:00, 10.9MiB/s]
embedding_model.onnx: 100%|███████████████████████████████████████████████████████████| 1.33M/1.33M [00:00<00:00, 9.80MiB/s]
melspectrogram.tflite: 100%|██████████████████████████████████████████████████████████| 1.09M/1.09M [00:00<00:00, 8.93MiB/s]
melspectrogram.onnx: 100%|████████████████████████████████████████████████████████████| 1.09M/1.09M [00:00<00:00, 9.31MiB/s]
silero_vad.onnx: 100%|████████████████████████████████████████████████████████████████| 1.81M/1.81M [00:00<00:00, 9.61MiB/s]
alexa_v0.1.tflite: 100%|████████████████████████████████████████████████████████████████| 855k/855k [00:00<00:00, 8.90MiB/s]
alexa_v0.1.onnx: 100%|██████████████████████████████████████████████████████████████████| 854k/854k [00:00<00:00, 8.16MiB/s]
hey_mycroft_v0.1.tflite: 100%|██████████████████████████████████████████████████████████| 860k/860k [00:00<00:00, 5.72MiB/s]
hey_mycroft_v0.1.onnx: 100%|████████████████████████████████████████████████████████████| 858k/858k [00:00<00:00, 7.85MiB/s]
hey_jarvis_v0.1.tflite: 100%|█████████████████████████████████████████████████████████| 1.28M/1.28M [00:00<00:00, 8.22MiB/s]
hey_jarvis_v0.1.onnx: 100%|███████████████████████████████████████████████████████████| 1.27M/1.27M [00:00<00:00, 8.52MiB/s]
hey_rhasspy_v0.1.tflite: 100%|██████████████████████████████████████████████████████████| 416k/416k [00:00<00:00, 6.14MiB/s]
hey_rhasspy_v0.1.onnx: 100%|████████████████████████████████████████████████████████████| 204k/204k [00:00<00:00, 7.07MiB/s]
timer_v0.1.tflite: 100%|██████████████████████████████████████████████████████████████| 1.74M/1.74M [00:00<00:00, 9.80MiB/s]
timer_v0.1.onnx: 100%|████████████████████████████████████████████████████████████████| 1.74M/1.74M [00:00<00:00, 9.75MiB/s]
weather_v0.1.tflite: 100%|████████████████████████████████████████████████████████████| 1.15M/1.15M [00:00<00:00, 9.43MiB/s]
weather_v0.1.onnx: 100%|██████████████████████████████████████████████████████████████| 1.15M/1.15M [00:00<00:00, 9.67MiB/s]
モデルはsite-packagesの下にダウンロードされる。
$ pip show openwakeword | grep Location
Location: /Users/kun432/work/openwakeword-test/.venv/lib/python3.12/site-packages
$ tree /Users/kun432/work/openwakeword-test/.venv/lib/python3.12/site-packages/openwakeword
/Users/kun432/work/openwakeword-test/.venv/lib/python3.12/site-packages/openwakeword
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-312.pyc
│ ├── custom_verifier_model.cpython-312.pyc
│ ├── data.cpython-312.pyc
│ ├── metrics.cpython-312.pyc
│ ├── model.cpython-312.pyc
│ ├── train.cpython-312.pyc
│ ├── utils.cpython-312.pyc
│ └── vad.cpython-312.pyc
├── custom_verifier_model.py
├── data.py
├── metrics.py
├── model.py
├── resources
│ └── models
│ ├── alexa_v0.1.onnx
│ ├── alexa_v0.1.tflite
│ ├── embedding_model.onnx
│ ├── embedding_model.tflite
│ ├── hey_jarvis_v0.1.onnx
│ ├── hey_jarvis_v0.1.tflite
│ ├── hey_mycroft_v0.1.onnx
│ ├── hey_mycroft_v0.1.tflite
│ ├── hey_rhasspy_v0.1.onnx
│ ├── hey_rhasspy_v0.1.tflite
│ ├── melspectrogram.onnx
│ ├── melspectrogram.tflite
│ ├── silero_vad.onnx
│ ├── timer_v0.1.onnx
│ ├── timer_v0.1.tflite
│ ├── weather_v0.1.onnx
│ └── weather_v0.1.tflite
├── train.py
├── utils.py
└── vad.py
4 directories, 33 files
用意されているウェイクワードはここにある
では再度スクリプトを実行。
$ python detect_from_microphone.py --inference_framework=onnx
こんな感じで発話待ちになるので、適当に発話してみる。
Model Name | Score | Wakeword Status
--------------------------------------
alexa | 0.000 | --
hey_mycroft | 0.000 | -- #####################################
hey_jarvis | 0.000 | --
hey_rhasspy | 0.001 | -- #####################################
1_minute_timer | 0.000 | --
5_minute_timer | 0.000 | --
10_minute_timer | 0.000 | --
20_minute_timer | 0.000 | --
30_minute_timer | 0.000 | --
1_hour_timer | 0.000 | --
weather | 0.000 | --
例えば「アレクサ」と発話してみると、以下のようにウェイクワード検出していることがわかる。
Model Name | Score | Wakeword Status
--------------------------------------
alexa | 0.983 | Wakeword Detected!
hey_mycroft | 0.000 | -- #####################################
hey_jarvis | 0.000 | --
hey_rhasspy | 0.001 | -- #####################################
1_minute_timer | 0.000 | --
5_minute_timer | 0.000 | --
10_minute_timer | 0.000 | --
20_minute_timer | 0.000 | --
30_minute_timer | 0.000 | --
1_hour_timer | 0.000 | --
weather | 0.000 | --
同じように「What's the weather」と発話してみるとこう。
Model Name | Score | Wakeword Status
--------------------------------------
alexa | 0.000 | --
hey_mycroft | 0.000 | -- #####################################
hey_jarvis | 0.000 | --
hey_rhasspy | 0.001 | -- #####################################
1_minute_timer | 0.000 | --
5_minute_timer | 0.000 | --
10_minute_timer | 0.000 | --
20_minute_timer | 0.000 | --
30_minute_timer | 0.000 | --
1_hour_timer | 0.000 | --
weather | 0.998 | Wakeword Detected!
サンプルコードを読んでみる。
引数処理はパスして、まず、PyAudioでマイクからの入力をストリームで受け取るように初期化、そしてopenWakewordでモデルを定義する。このとき推論フレームワークを"tflite"か"onnx"で指定する。
import pyaudio
import numpy as np
from openwakeword.model import Model
import argparse
(snip)
# マイクのストリームを取得
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = args.chunk_size
audio = pyaudio.PyAudio()
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)
# 事前学習済みの openwakeword モデルを読み込み
if args.model_path != "":
owwModel = Model(wakeword_models=[args.model_path], inference_framework=args.inference_framework)
else:
owwModel = Model(inference_framework=args.inference_framework)
n_models = len(owwModel.models.keys())
(snip)
ループで音声入力をキャプチャして、取得した音声をopenWakewordに渡して、一定のスコア以上ならウェイクワードを検出するという感じ。
# キャプチャループを連続して実行し、ウェイクワードをチェック。
if __name__ == "__main__":
# 出力文字列ヘッダを生成
print("\n\n")
print("#"*100)
print("Listening for wakewords...")
print("#"*100)
print("\n"*(n_models*3))
while True:
# 音声を取得
audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16)
# 音声をopenWakeWordモデルに渡す
prediction = owwModel.predict(audio)
# タイトル列
n_spaces = 16
output_string_header = """
Model Name | Score | Wakeword Status
--------------------------------------
"""
for mdl in owwModel.prediction_buffer.keys():
# フォーマットされた表にスコアを追加
scores = list(owwModel.prediction_buffer[mdl])
curr_score = format(scores[-1], '.20f').replace("-", "")
output_string_header += f"""{mdl}{" "*(n_spaces - len(mdl))} | {curr_score[0:5]} | {"--"+" "*20 if scores[-1] <= 0.5 else "Wakeword Detected!"}
"""
# 結果を表で出力
print("\033[F"*(4*n_models+1))
print(output_string_header, " ", end='\r')
シンプルに「アレクサ」に反応するだけのものにしてみた。
import pyaudio
import numpy as np
from openwakeword.model import Model
import sys
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024
audio = pyaudio.PyAudio()
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)
model_name = "alexa_v0.1.onnx"
model = Model(
wakeword_models=[model_name],
inference_framework="onnx"
)
print("Listening for wakeword \"Alexa\"...")
print()
prev_detect=False
while True:
audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16)
prediction = model.predict(audio)
scores = model.prediction_buffer[model_name]
curr_score = format(scores[-1], '.20f')
detect = True if float(curr_score) > 0.5 else False
if detect:
if detect != prev_detect:
print(f"Detected!({curr_score[:5]})")
prev_detect=True
else:
prev_detect=False
もう一つサンプルがある。
こちらは、ウェイクワードを検出したら、5秒間バッファを録音するというもの。ただし、
- ノイズサプレッションのところは、Macでは使用できないライブラリが使用されている
- PyAudioでブロッキングモードの場合に入力バッファオーバーフローが起きることがある
- ウェイクワード検出したら、どうもそこからさかのぼって5秒間バッファを取得する、普通はウェイクワード検出「後」を録音すべき
というところで、o1-previewにお願いしながら、修正してみた。
import os
import platform
import time
if platform.system() == "Windows":
import pyaudiowpatch as pyaudio
else:
import pyaudio
import numpy as np
from openwakeword.model import Model
import openwakeword
import scipy.io.wavfile
import datetime
import argparse
from utils.beep import playBeep
import queue
# 引数の解析
parser = argparse.ArgumentParser()
parser.add_argument(
"--output_dir",
help="アクティベーション結果の音声を保存する場所",
type=str,
default="./",
required=True
)
parser.add_argument(
"--threshold",
help="アクティベーションのスコア閾値",
type=float,
default=0.5,
required=False
)
parser.add_argument(
"--vad_threshold",
help="""openWakeWordインスタンスで使用する音声活動検出(VAD)の閾値。
デフォルト(0.0)はVADを無効にします。""",
type=float,
default=0.0,
required=False
)
parser.add_argument(
"--chunk_size",
help="一度に予測する音声の量(16kHzサンプル数)",
type=int,
default=1280,
required=False
)
parser.add_argument(
"--model_path",
help="特定のモデルをロードするパス",
type=str,
default="",
required=False
)
parser.add_argument(
"--inference_framework",
help="使用する推論フレームワーク('onnx'または'tflite')",
type=str,
default='tflite',
required=False
)
parser.add_argument(
"--disable_activation_sound",
help="アクティベーション音を無効にし、クリップを静かにキャプチャします",
action='store_true',
required=False
)
args = parser.parse_args()
# マイクロフォンストリームの取得
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = args.chunk_size
audio = pyaudio.PyAudio()
# オーディオキューの作成
audio_queue = queue.Queue()
# コールバック関数の定義
def callback(in_data, frame_count, time_info, status):
audio_queue.put(in_data)
return (None, pyaudio.paContinue)
# ストリームのオープン(コールバックを使用)
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True,
frames_per_buffer=CHUNK, stream_callback=callback)
# ストリームの開始
mic_stream.start_stream()
# プリトレーニングされたopenwakewordモデルのロード
if args.model_path:
model_paths = openwakeword.get_pretrained_model_paths()
for path in model_paths:
if args.model_path in path:
model_path = path
if model_path:
owwModel = Model(
wakeword_models=[model_path],
vad_threshold=args.vad_threshold,
inference_framework=args.inference_framework
)
else:
print(f'モデル \"{args.model_path}\" が見つかりませんでした')
exit()
else:
owwModel = Model(
vad_threshold=args.vad_threshold
)
# 出力ディレクトリの作成(存在しない場合)
if not os.path.exists(args.output_dir):
os.mkdir(args.output_dir)
# 録音の管理変数
recording_active = False
recording_start_time = None
recording_buffer = []
last_activation_time = 0 # 最後のアクティベーション時間
cooldown = 4 # 次の録音までのクールダウン(秒)
save_delay = 1 # 録音開始を遅らせる時間(秒)
current_mdl = None # 現在のモデル名を保持
# ホットワードをチェックしながらキャプチャループを実行
if __name__ == "__main__":
print("\n\nウェイクワードを待機中...\n")
while True:
# オーディオの取得
try:
in_data = audio_queue.get(timeout=1)
mic_audio = np.frombuffer(in_data, dtype=np.int16)
except queue.Empty:
continue
# openWakeWordモデルへのフィード
prediction = owwModel.predict(mic_audio)
# モデルのアクティベーション(スコアが閾値を超えた場合)をチェック
for mdl in prediction.keys():
if prediction[mdl] >= args.threshold and not recording_active and (time.time() - last_activation_time) >= cooldown:
recording_active = True
recording_start_time = time.time()
recording_buffer = []
current_mdl = mdl # 現在のモデル名を保存
print(f'ウェイクワードが検出されました。録音を開始します。(モデル: {current_mdl})')
if not args.disable_activation_sound:
playBeep(os.path.join(os.path.dirname(__file__), 'audio', 'activation.wav'), audio)
# 録音開始を遅らせる
time.sleep(save_delay)
break # 一度に一つのウェイクワードのみ処理するためにループを抜ける
# 録音がアクティブな場合、データをバッファに追加
if recording_active:
recording_buffer.append(mic_audio)
if time.time() - recording_start_time >= 5:
# 録音を停止し、データを保存
audio_data = np.concatenate(recording_buffer).astype(np.int16)
detect_time = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
fname = detect_time + f"_{current_mdl}.wav"
scipy.io.wavfile.write(os.path.join(os.path.abspath(args.output_dir), fname), 16000, audio_data)
recording_active = False
last_activation_time = time.time()
print(f'録音が完了しました。ファイル {fname} に保存しました。')
実行
$ python capture_activations.py --inference_framework onnx --output_dir ./
こんな感じでウェイクワードを検出後録音される
ウェイクワードを待機中...
ウェイクワードが検出されました。録音を開始します。(モデル: alexa)
録音が完了しました。ファイル 2024_10_12_06_27_23_alexa.wav に保存しました。
ウェイクワードが検出されました。録音を開始します。(モデル: hey_mycroft)
録音が完了しました。ファイル 2024_10_12_06_27_33_hey_mycroft.wav に保存しました。
ノイズサプレッションとかVADについて。VADしきい値とアクティベーションスコアの違いは、
- VADしきい値でまず音声入力の有無を判断する
- アクティベーションスコアでウェイクワードかどうかを判断する
だと思ってる。
使用に関する推奨事項
ノイズ抑制と音声活動検出(VAD)
openWakeWordのデフォルト設定は多くの場合で良好に機能しますが、一部の導入シナリオでは、openWakeWordの調整可能なパラメータでパフォーマンスを向上させることができます。
サポートされているプラットフォーム(現時点では、X86およびArm64のLinuxのみ)では、openWakeWordモデルをインスタンス化する際に、
enable_speex_noise_suppression=True
を設定することで、Speexノイズ抑制を有効にすることができます。これにより、比較的安定したバックグラウンドノイズがある場合のパフォーマンスが改善されます。次に、Silero 社の音声活動検出(VAD)モデルが openWakeWord に含まれており、openWakeWord モデルをインスタンス化する際に
vad_threshold
引数を 0 から 1 の間の値に設定することで有効にすることができます。これにより、VAD モデルが同時に指定した閾値以上のスコアを持つ場合にのみ、openWakeWord から肯定的な予測が可能になります。これにより、非音声ノイズが存在する場合の誤検出を大幅に削減することができます。アクティベーションの閾値スコア
付属の openWakeWord モデルはすべて、正の予測のためのデフォルトの閾値 0.5 で良好に動作するようにトレーニングされていますが、テストを通じて、環境や使用事例に最適な閾値を決定することをお勧めします。特定の導入環境では、実際にはより低い閾値またはより高い閾値を使用することで、大幅に優れたパフォーマンスが得られる場合があります。
ユーザー固有のモデル
openWakeWordモデルの基本性能が特定のアプリケーションに十分でない場合(特に、誤った起動率が容認できないほど高い場合)、予測の2段階目のフィルタとして機能する特定の音声用のカスタム検証モデルをトレーニングすることができます(つまり、既知の音声セットによって話された可能性が高い起動のみを許可します)。これにより、openWakeWordシステムが新しい音声に反応しにくくなるという代償を払うことになりますが、性能を大幅に向上させることができます。
カスタムなウェイクワードのトレーニングはnotebookが用意されているのでそれを使えばできそう?
新しいモデルのトレーニング
openWakeWordには、カスタムモデルのトレーニングプロセスを大幅に簡素化する自動化ユーティリティが含まれています。これは2つの方法で使用できます。
- 使いやすいインターフェースとシンプルなエンドツーエンドのプロセスを備えたシンプルなGoogle Colabノートブック。これにより、開発経験がなくても誰でも非常に短時間(1時間未満)でカスタムモデルを作成できますが、展開シナリオによってはモデルのパフォーマンスが低い場合があります。
- より詳細なノートブック(同じくGoogle Colab用)では、トレーニングプロセスがより詳細に説明されており、よりカスタマイズが可能です。これにより、高品質なモデルを作成できますが、より開発経験が必要です。
Home Assistant コミュニティが上記のノートブックを使用してトレーニングしたモデルのコレクション(@fwartner 氏に感謝します)については、こちらの優れたリポジトリをご覧ください。
モデルトレーニングの背後にある基本的な概念を理解したいユーザー向けに、より詳細な教育用チュートリアルノートブックも用意されています。ただし、この特定のノートブックは本番モデルのトレーニング用ではなく、その目的には上記の自動化プロセスが推奨されます。
基本的に、新しいモデルには2つのデータ生成と収集のステップが必要です。
- オープンソースの音声テキスト変換システムを使用して、目的のキーワード/フレーズの新しいトレーニングデータを生成します(詳細は「合成データの生成」を参照)。これらのモデルと生成コードは、別のリポジトリでホストされています。必要な生成例の数はさまざまですが、最低でも数千例は推奨され、データセットのサイズが大きくなるにつれてパフォーマンスがスムーズに向上するようです。
- モデルの誤認受容率を低く抑えるために、ネガティブデータ(例えば、ウェイクワード/フレーズが存在しない音声)を収集します。これも規模のメリットがあり、付属のモデルはすべて、音声、ノイズ、音楽を表す約30,000時間のネガティブデータでトレーニングされています。トレーニングデータの管理および準備の詳細については、個々のモデルのドキュメントページを参照してください。
言語サポート
現在のところ、openWakeWordは英語しかサポートしていません。これは主に、訓練データを生成するために使用される訓練済みの音声合成モデルが、すべて英語のデータセットに基づいているためです。 他の言語で訓練されたテキスト読み上げモデルもうまく機能すると思われますが、英語以外のモデル & データセットはあまり一般的に利用されていません。
将来のリリースロードマップでは、英語以外の言語がサポートされるかもしれません。 特に、Mycroft.AIs Mimic 3 TTS エンジンは、他の言語へのサポートを拡張するのに役立つかもしれません。
notebook見ても日本語には直接対応していなさそうではあるが、英語ですら無理くり認識させるような雰囲気があるのでやろうと思えばできそう。
その他についてもREADMEには丁寧に色々書かれているので参考に。