LabHub
はじめる
学ぶ 学習パス コース

音声 AI エージェント — 聞いて、調べて、話すパイプライン

話し終えたか — VAD でターンを分け、割り込みを見つける

LabHubで続きを見る

目標

エネルギーVADとニューラルネットワークVADを同じ通話に適用して誤検出を数え、VAD区間をターンにまとめるルールを作って、初めて見る通話で試します。エコーが混ざったマイクから、人が割り込んだ時刻を見つけます。

なぜ重要なのか

エンドポイント判定の遅れは、モデルを速くしても減らない応答遅延です。短くすると話の途中で切れ、長くすると遅くなります。このラボの通話(/opt/lab/fixtures/voice/vad/call_a.wav)は、合成した文の断片を決まった時刻に置いて作ったので、「実際に話した区間」がcall_a.truth.jsonにあります。採点ツールは、turns.pyを見たことのない2つ目の通話でもう一度動かします。1つのファイルにしきい値を合わせたコードは、そこで失敗します。

ステップ

  1. 30 ms(480サンプル)フレームのdBFSが-35より大きい区間をつなげたエネルギーVADの結果を、/root/voice/vad/energy.jsonに書いてください。
  2. silero VAD(voicekit.models.vad(threshold=0.5, min_silence=0.1, min_speech=0.1)、512サンプルずつ入力)の区間を、/root/voice/vad/silero.jsonに書いてください。
  3. 2つの結果をcall_a.truth.jsonのspeech_segmentsと重ねて、false_alarms・missedを数えた/root/voice/vad/score.jsonを作ってください。
  4. detect_turns(samples, sr, gap_s)がある/root/voice/vad/turns.pyを作ってください。silero区間のうち、間隔がgap_sより短いものをつなげて、[(시작, 끝), …]を返します(プレースホルダーは開始時刻と終了時刻です)。
  5. gap 0.2・0.3・0.5・0.8・1.2秒でcall_aのターン数を数えた/root/voice/vad/sweep.jsonを作ってください。
  6. ターン数が合うgapのうち最も短い値で、ターンごとの応答遅延、つまり(VADが見た話の終わり + gap) − 実際の話の終わりを、/root/voice/vad/latency.jsonに書いてください。
  7. /opt/lab/fixtures/voice/bargein/mic.wavからplayback.wavのエコーの遅延と倍率を推定して引き、/root/voice/vad/residual.wavを保存したあと、元のものと引いたあとの最初の発話時刻を/root/voice/vad/bargein.jsonに書いてください。
  8. 前のステップの数字をまとめた/root/voice/vad/report.jsonを作ってください。

参考

エネルギーVAD: 大きな音はすべて話し声

/opt/lab/fixtures/voice/vad/call_a.wavを30 ms(480サンプル)のフレームに分けて、dBFSが-35より大きいフレームが続いた区間を、/root/voice/vad/energy.jsonに[{"start_s": …, "end_s": …}](小数第2位)で書いてください。末尾の足りない断片は捨てます。

フレームiの開始はi×480/16000秒です。話し声のフレームが始まったら番号を覚えておき、終わるフレームの開始時刻で閉じます。voicekit.dbfsがRMS dBFSを返します。

silero VADの区間

voicekit.models.vad(threshold=0.5, min_silence=0.1, min_speech=0.1)にcall_aを512サンプルずつ入れてflush()したあと、出てきた区間を/root/voice/vad/silero.jsonに同じ形で書いてください(開始 = front.start ÷ 16000、終了 = 開始 + len(front.samples) ÷ 16000)。

区間はVADの中のキューにたまります。empty()が真になるまで、frontを読んでpop()します。flush()を呼ばないと、ファイル末尾で開いたままの区間が出てきません。

正解と重ねて誤検出・見逃しを数える

call_a.truth.jsonのspeech_segmentsを正解として、energyとsileroのそれぞれについて、false_alarms(正解とまったく重ならない予測区間の数)とmissed(どの予測とも重ならない正解区間の数)を、/root/voice/vad/score.jsonに書いてください。

2つの区間(a0,a1)と(b0,b1)が重なる = a0 < b1 and b0 < a1です。エネルギーVADは、6秒のドアのノックをどう検出したでしょうか。

VAD区間をターンにまとめる

/root/voice/vad/turns.pyにdetect_turns(samples, sr=16000, gap_s=0.5)を作ってください。silero VAD(threshold 0.5・min_silence 0.1・min_speech 0.1)の区間を順に見て、前のターンの終わりとの間隔がgap_sより短ければつなげ、そうでなければ新しいターンを始めて、[(시작 초, 끝 초), …]を返します(プレースホルダーは開始秒と終了秒です)。採点ツールは、gap 0.5でcall_aと初めて見る通話の両方を動かします。

細かく切る作業(VAD)とまとめる作業(gap)を分けると、gapだけを変えながら実験できます。実行コードはif name == 'main':の下に置いてください。採点ツールがこのファイルをモジュールとして読み込みます。

gapを順に試す: 切れるか、つながるか

turns.pyで、gap 0.2・0.3・0.5・0.8・1.2秒でcall_aのターン数を数え、/root/voice/vad/sweep.jsonに{"0.2": {"turns": n}, …}の形で書いてください(キーは文字列)。

正解のターンはtruthのturnsに3つあります。ターンがそれより多ければ話の途中の間で切ったことになり、少なければ別の人の番までつなげたことになります。

ターンを渡すまでに待った時間

ステップ5でターン数が正解と同じgapのうち、最も短い値をgap_sとして選び、ターンごとに(VAD 가 본 턴 끝 + gap_s) − 실제 턴 끝をper_turn_delay_sに、その平均をmean_delay_sに書いた/root/voice/vad/latency.jsonを作ってください(プレースホルダーは、VADが見たターンの終わりと実際のターンの終わりです)。

ターンが終わったと判定する瞬間は、VADが話の終わりを見たあと、gapの分だけさらに静かだったときです。実際のターンの終わりは、truthのturnsにあります。

エコーを引いて割り込んだ時刻を探す

/opt/lab/fixtures/voice/bargein/mic.wav(ユーザー + スピーカーのエコー)とplayback.wav(こちらが出した音)から、最初の1.5秒で相互相関が最大になる遅延(0–200 ms)と最小二乗の倍率を求めて、エコーを引いた/root/voice/vad/residual.wavを保存してください。/root/voice/vad/bargein.jsonに、echo_delay_ms・echo_gain・naive_first_speech_s(元のマイクでsileroが最初に話し声として検出した時刻)・barge_in_s(residualで最初に話し声として検出した時刻)を書いてください。

遅延dごとにnp.dot(mic[d:], play[:n-d])を測って、最も大きいdを選びます。倍率はg = (mic·ref)/(ref·ref)です。ユーザーが話す前の区間だけを使う必要があります。そうしないと、人の声が推定をぼやけさせます。

レポート

/root/voice/vad/report.jsonに、energy_false_alarms・silero_false_alarms(score.json)・gap_s・mean_delay_s(latency.json)・turns_call_a(正解のターン数)・barge_in_s(bargein.json)を書き写してください。

前のステップのファイルを読んで、そのまま書き写せば済みます。手で書くと、四捨五入が変わってずれます。