0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pyannote Segmentation 3.0 と ONNX Runtime で話者交代を検出する

0
Last updated at Posted at 2026-07-05

こんにちは、皆さん。

会話音声を聞いていると、誰が話しているかは自然に追えます。
しかし、プログラムで同じことをしようとすると、
発話の有無だけでなく「ここで話者が変わった」という境界も見つけなければなりません。

さて、今日は Pyannote Segmentation 3.0 の ONNX モデルを使い、2 人の会話音声から話者交代を検出して、発話ごとの WAV ファイルへ分割します。

今回検証する内容

検証では、14 秒ほどの会話を FFmpeg で 16 kHz、mono の波形へ変換し、Pyannote の segmentation モデルと単純な後処理で、時間的に連続した話者区間を作ります。

確認するのは次の点です。

  • 2 人が交互に話す 6 発話を、それぞれ別の区間へ分割できるか
  • 検出した話者インデックスが音声全体で一貫するか
  • ONNX Runtime の CPU 実行だけで実時間より高速に処理できるか
  • 各区間を個別の WAV ファイルとして保存できるか

完全なコードと再現可能な環境は、kiarina/labs の pyannote-scd lab で公開しています。

なお、今回行うのは segmentation モデルが出力する話者インデックスを使った区間化です。話者埋め込みの照合や clustering は行わないため、長い音声を通して人物を識別する完全な話者ダイアライゼーションではありません。

検証環境を準備する

実行には、次のツールが必要です。

次のコマンドで lab だけを取得し、共有テスト音声をダウンロードして実行できます。

git clone --depth 1 --filter=blob:none --sparse \
  https://github.com/kiarina/labs.git
cd labs
git sparse-checkout set .gitignore .mise/tasks Makefile mise.toml \
  2026/07/04/pyannote-scd
mise -C 2026/07/04/pyannote-scd run

初回実行時には、Hugging Face の onnx-community/pyannote-segmentation-3.0 から通常精度の onnx/model.onnx をダウンロードします。その後、uv が Python の依存パッケージを用意し、検出処理を実行します。

モデルのライセンス

今回使用した onnx-community/pyannote-segmentation-3.0 は、配布元で MIT License と表示されています。利用や再配布を行う場合は、配布元で最新のライセンス条件を確認してください。

どのように話者区間を検出するか

入力には、共有テストアセットの次のファイルを使いました。

tests/assets/mp3/conversation_2speaker_14s_16k.mp3

音声のシナリオは次のとおりです。

Speaker 1: もしもし、もう駅に着いた?

Speaker 2: うん。今、改札出たところ。そっちは?

Speaker 1: こっちはまだ電車。あと5分くらいかな。

Speaker 2: 了解。じゃあ、カフェの前で待ってるね。

Speaker 1: 助かる。今日は結構寒いね。

Speaker 2: ほんと、マフラー持ってきて正解だった。

FFmpeg で MP3 を 16 kHz、mono の波形へデコードし、10 秒ずつモデルへ入力します。最後のチャンクは 10 秒になるまでゼロで埋め、推論後に元の音声長を超えるフレームを取り除きます。

検出設定は次のとおりです。

項目
sample rate 16 kHz
inference window 10 秒
model speakers 3
maximum simultaneous speakers 2
active speaker threshold 0.5
overlap margin 0.1
minimum speaker change 100 ms
minimum speech segment 100 ms
execution provider CPUExecutionProvider

モデルは各フレームについて、無音、単独話者、2 人の組み合わせからなる 7 クラスの log probability を出力します。この powerset 表現を 3 つの話者確率へ変換し、確率 0.5 以上の話者をアクティブと判定します。

2 人以上がアクティブで、上位 2 話者の確率差が 0.1 以下なら overlap とします。ただし、これは確率が拮抗したフレームを表す規則であり、実際の重複発話を直接保証するものではありません。

話者確率をそのまま区間へ変換すると、無音や短い揺れによって細かな断片が生じます。そこで、無音フレームを前後の話者へ割り当て、100 ms 未満の短い話者状態を隣接区間へ統合します。今回のモデル出力では 1 フレームが約 16.978 ms なので、100 ms はおよそ 6 フレームです。

最後に、各区間を PCM 16-bit、16 kHz、mono の WAV として output/ に保存します。既存の出力は実行のたびに削除して作り直します。

実行結果

Mac Studio で実行した結果、14.171 秒の入力から 6 個の話者区間が検出されました。

audio duration: 14.171s
SCD elapsed: 0.019s
SCD real-time factor: 0.001x
frame duration: 16.978ms
segments: 6
001:   0.000s -   1.851s ( 1.851s) speaker_2
002:   1.851s -   4.737s ( 2.886s) speaker_1
003:   4.737s -   7.317s ( 2.581s) speaker_2
004:   7.317s -   9.677s ( 2.360s) speaker_1
005:   9.677s -  11.834s ( 2.156s) speaker_2
006:  11.834s -  14.171s ( 2.337s) speaker_1

6 区間は隙間や重複なく、入力の 14.171 秒全体を覆っています。生成された 6 ファイルの合計は 226,736 samples で、すべて PCM 16-bit、16 kHz、mono でした。

各ファイルを聴いて台本と照合した結果は次のとおりです。

file model output speech
speaker_2_001.wav speaker_2 もしもし、もう駅に着いた?
speaker_1_002.wav speaker_1 うん。今、改札出たところ。そっちは?
speaker_2_003.wav speaker_2 こっちはまだ電車。あと5分くらいかな。
speaker_1_004.wav speaker_1 了解。じゃあ、カフェの前で待ってるね。
speaker_2_005.wav speaker_2 助かる。今日は結構寒いね。
speaker_1_006.wav speaker_1 ほんと、マフラー持ってきて正解だった。

6 ファイルはそれぞれ台本上の 1 発話に対応し、発話途中で不自然に分割されたファイルも、隣の話者が混ざったファイルもありませんでした。モデル内の speaker_2 は Speaker 1、speaker_1 は Speaker 2 に対応し、全区間で交互に切り替わりました。

実行環境は次のとおりです。

  • machine: Mac Studio
  • chip: Apple M4 Max
  • memory: 128 GB
  • OS: macOS 26.5.1(25F80)、arm64
  • Python: 3.12.11
  • ONNX Runtime: 1.27.0
  • execution provider: CPUExecutionProvider

SCD elapsed は、ONNX 推論と話者区間判定だけにかかった時間です。モデルの初期化、FFmpeg による入力デコード、WAV の生成は含みません。ウォームアップなしの 1 回の測定なので厳密なベンチマークではありませんが、入力の長さに対する real-time factor は 0.001x でした。

結果を考える

今回の音声では、台本上の 6 発話と検出された 6 区間が一致しました。発話間の無音を別ファイルにせず前後へ分配したため、入力全体を欠けなく保持しながら、話者交代を境界に分割できています。文字起こしの前処理として使うなら、各区間に一人の発話だけが入る形は扱いやすそうです。

CPU だけで 14.171 秒を 0.019 秒で判定した点も良好です。ただし、これは推論と区間判定に限った時間で、単発の参考値です。ファイルの読み書きを含む処理全体の速度や、別のマシンでの速度を示すものではありません。

一方で、speaker_1speaker_2 は人物の永続的な ID ではありません。今回の実装は音声を 10 秒窓に分けて独立に推論しており、話者埋め込みで窓同士を照合していません。この入力では 10 秒の境界を越えてもインデックスが一貫しましたが、別の音声でも同じになる保証はありません。

評価対象も、交互に話す 2 人の短い音声 1 本だけです。重複発話、3 人以上の会話、雑音、長時間音声は試しておらず、時刻単位の正解ラベルによる定量評価も行っていません。今回の結果から確認できるのは、この音声と設定の組み合わせで 6 発話を正確に分離できたことまでです。

試してみた感想

Pyannote の segmentation モデルは、VAD より一段踏み込んで「誰かが話している」だけでなく「話者が変わった」を扱えるのが面白いところでした。単純な閾値判定と平滑化だけでも、この短い会話では発話単位に綺麗に分けられました。CPU 上の ONNX Runtime で十分に速い点も、ローカル処理へ組み込みやすい印象です。

ただ、綺麗な 6 ファイルが出ると、つい話者ダイアライゼーションまで完成したように見えます。実際には、窓をまたぐ話者 ID の照合が抜けています。この違いは長時間の録音を扱ったときに効いてきそうです。

次は重複発話や 3 人以上の会話で境界を評価し、話者埋め込みと clustering を加えて、長い音声でも同じ人物へ一貫した ID を付けられるところまで試したいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?