こんにちは、皆さん。
会話を発話ごとに分割できても、「どの発話が同じ人のものか」がわからなければ、話者ごとの文字起こしや応答にはつなげられません。名前まで特定しなくても、同じ声を同じグループへまとめられるだけで、会話の構造はかなり扱いやすくなります。
さて、今日は ECAPA-TDNN の ONNX モデルで発話ごとの speaker embedding を生成し、cosine similarity を使って 2 人の発話を話者ごとにグループ化します。
今回検証する内容
前回は Pyannote Segmentation 3.0 で話者交代を検出し、14 秒ほどの会話を 6 発話へ分割しました。今回は、その各発話を ECAPA-TDNN へ入力して 192 次元の speaker embedding を作り、時間順にグループへ割り当てます。
確認するのは次の点です。
- 各発話から一貫して 192 次元の speaker embedding を生成できるか
- 話者数を事前に指定しない単純な逐次しきい値方式で、同じ話者の 3 発話ずつをまとめられるか
- 同じ話者と異なる話者の cosine similarity にどの程度の差が出るか
- ONNX Runtime の CPU 実行だけで実時間より高速に処理できるか
完全なコードと再現可能な環境は、kiarina/labs の ecapa-tdnn-onnx lab で公開しています。
検証環境を準備する
実行には、次のツールが必要です。
次のコマンドで 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/05/ecapa-tdnn-onnx
mise -C 2026/07/05/ecapa-tdnn-onnx run
初回実行時には、Hugging Face から次の 2 モデルをダウンロードします。
- SCD:
onnx-community/pyannote-segmentation-3.0 - speaker embedding:
pranjal-pravesh/ecapa_tdnn_onnx
ECAPA-TDNN は commit 04c3ffe4fd00b3b7853fd57db44e2e531d4817f2 のモデルを使い、実行前と推論コード内の両方で SHA-256 が次の値と一致することを確認します。
245eb5995cfffd74494862dee33da2b00c1c2579eb0c6703847784e9901ed458
モデルのライセンス
今回使用した onnx-community/pyannote-segmentation-3.0 と pranjal-pravesh/ecapa_tdnn_onnx は、いずれも配布元で MIT License と表示されています。利用や再配布を行う場合は、それぞれの配布元で最新のライセンス条件を確認してください。
発話を speaker embedding で比較する
入力には、2 人が交互に 3 回ずつ話す共有テスト音声を使いました。
tests/assets/mp3/conversation_2speaker_14s_16k.mp3
Pyannote Segmentation 3.0 と前回と同じ後処理で、入力を次の 6 発話へ分割します。
| segment | time | expected speaker |
|---|---|---|
| 1 | 0.000–1.851 秒 | Speaker 1 |
| 2 | 1.851–4.737 秒 | Speaker 2 |
| 3 | 4.737–7.317 秒 | Speaker 1 |
| 4 | 7.317–9.677 秒 | Speaker 2 |
| 5 | 9.677–11.834 秒 | Speaker 1 |
| 6 | 11.834–14.171 秒 | Speaker 2 |
したがって、期待する結果は segment 1, 3, 5 と 2, 4, 6 の 2 グループです。
ECAPA-TDNN モデルは、16 kHz、mono の生波形を受け取り、Fbank 特徴量の抽出、入力正規化、ECAPA-TDNN による推論までをモデル内部で行います。出力 shape は [1, 1, 192] です。実装ではこれを 192 要素の vector に変換し、L2 norm が 1 になるように正規化します。
主な設定は次のとおりです。
| 項目 | 値 |
|---|---|
| sample rate | 16 kHz |
| embedding dimension | 192 |
| embedding normalization | L2 |
| similarity | cosine similarity |
| speaker similarity threshold | 0.45 |
| execution provider | CPUExecutionProvider |
グループ化は、発話を時間順に 1 つずつ処理します。新しい embedding と、各既存グループに入っているすべての embedding の cosine similarity を計算し、その最大値を使います。
- 最大 similarity が
0.45以上なら、その embedding を持つグループへ追加する -
0.45未満なら、新しいグループを作る
正規化済み vector 同士では内積が cosine similarity になるため、比較は単純な dot product です。グループの重心ではなく、所属する発話のいずれかと似ていれば同じグループにします。また、話者数は指定していません。
この方法は実装しやすい一方、結果が発話順と閾値に依存します。誤って別の話者を追加すると、その embedding が後続の比較対象にもなるため、誤りが広がる可能性があります。
実行結果
Mac Studio でウォームアップなしに 1 回実行した結果、6 発話は期待どおり 2 グループに分かれました。
audio duration: 14.171s
SCD elapsed: 0.019s
SCD real-time factor: 0.001x
embedding elapsed: 0.114s
embedding real-time factor: 0.008x
similarity threshold: 0.450
segments: 6
groups: 2
1: 0.000s - 1.851s ( 1.851s) group=0 best_score= new
2: 1.851s - 4.737s ( 2.886s) group=1 best_score=0.095
3: 4.737s - 7.317s ( 2.581s) group=0 best_score=0.459
4: 7.317s - 9.677s ( 2.360s) group=1 best_score=0.556
5: 9.677s - 11.834s ( 2.156s) group=0 best_score=0.582
6: 11.834s - 14.171s ( 2.337s) group=1 best_score=0.595
割り当てを話者と照合すると、次の結果です。
| group | segments | expected speaker |
|---|---|---|
| 0 | 1, 3, 5 | Speaker 1 |
| 1 | 2, 4, 6 | Speaker 2 |
すべての embedding は 192 次元で、正規化後の L2 norm は浮動小数点誤差の範囲で 1.0 でした。生成した 6 個の発話ファイルも、すべて 16 kHz、mono の PCM WAV です。
6 発話を総当たりで比較した cosine similarity は次のとおりです。
| 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|
| 1 | 1.000 | 0.095 | 0.459 | 0.156 | 0.453 | 0.130 |
| 2 | 0.095 | 1.000 | 0.276 | 0.556 | 0.216 | 0.561 |
| 3 | 0.459 | 0.276 | 1.000 | 0.249 | 0.582 | 0.220 |
| 4 | 0.156 | 0.556 | 0.249 | 1.000 | 0.175 | 0.595 |
| 5 | 0.453 | 0.216 | 0.582 | 0.175 | 1.000 | 0.173 |
| 6 | 0.130 | 0.561 | 0.220 | 0.595 | 0.173 | 1.000 |
同じ話者の組み合わせは 0.453–0.595、異なる話者は 0.095–0.276 でした。この音声では 2 つの範囲が重ならず、embedding が話者の違いを捉えています。
実行環境は次のとおりです。
- machine: Mac Studio(Mac16,9)
- chip: Apple M4 Max
- memory: 128 GB
- OS: macOS 26.5.1(25F80)、arm64
- Python: 3.12.11
- NumPy: 2.5.1
- ONNX Runtime: 1.27.0
- execution provider: CPUExecutionProvider
SCD elapsed は SCD の推論と区間判定、embedding elapsed は 6 区間すべての ECAPA-TDNN 推論と L2 正規化にかかった時間です。モデル初期化、FFmpeg による入力のデコード、WAV と JSON の生成は含みません。単発の参考値であり、厳密なベンチマークではありません。
結果を考える
今回の入力では、話者数を事前に与えなくても、ECAPA-TDNN embedding と単純な逐次しきい値方式だけで 2 人の発話を正しくまとめられました。前回の SCD だけでは、モデルが出す一時的な speaker index に頼っていましたが、今回は声の特徴を直接比較してグループを作っています。長い音声や別々に切り出した発話を同じ人物へ結びつけるための一歩になりそうです。
処理速度も、6 発話の embedding 生成が合計 0.114 秒、入力音声長に対する real-time factor が 0.008x でした。測定範囲は限定的ですが、Apple Silicon の CPU だけでローカルの会話処理へ組み込める速度です。
ただし、閾値 0.45 が十分に安定しているとはいえません。segment 3 を group 0 に加えた similarity は 0.459 で、閾値との差はわずか 0.009 でした。さらに同じ話者である segment 1 と 5 の similarity は 0.453 です。録音条件や発話内容が少し変われば、この 2 組は別グループになる可能性があります。
反対に閾値を下げると、異なる話者を統合する危険が増えます。今回は異なる話者間の最大値が 0.276 だったため余裕がありますが、声質が似た人物や雑音を含む音声でも同じ差が保たれるとは限りません。0.45 はこの 1 本で動作した設定であり、一般的な推奨値ではありません。
評価対象も、短く明瞭な 2 話者音声 1 本だけです。重複発話、3 人以上の会話、短すぎる発話、雑音、残響、マイクの違いは試していません。また、SCD が誤った境界で複数人の声を同じ区間へ入れれば、その後の embedding とグループ化にも影響します。
試してみた感想
192 個の数値へ変換した声を dot product するだけで、交互に並んだ発話が綺麗に 2 人へ戻るのは気持ちのよい結果でした。モデル内に音響特徴量の抽出まで含まれているので、生波形から speaker embedding まで ONNX Runtime だけで完結するのも扱いやすいです。
一方で、最初の同一話者判定が閾値すれすれだったのは見逃せません。今回の成功は、ECAPA-TDNN が使えそうだという確認にはなりましたが、閾値 0.45 を固定すれば話者ダイアライゼーションが完成する、という結果ではありません。
音の embedding は、話者、音声の内容、環境音、音楽など、捉えたい特徴によって使うモデルも異なります。次は話者以外の embedding モデルも試し、それぞれが音をどのように表現し、検索や分類へどう活用できるのか確かめてみたいです。