話者ダイアライゼーション用ツールキット「pyannote.audio」を試す(VADも)
GitHubレポジトリ
pyannote.audio
オープンソースツールキットを本番環境で使用していますか?
より優れた高速なオプションを提供する pyannoteAI への移行を検討してください。
pyannote.audio
話者ダイアライゼーションツールキット
pyannote.audio
は Python で書かれた話者ダイアライゼーション用のオープンソースツールキットです。
PyTorch 機械学習フレームワークに基づいており、最先端の事前学習済みモデルとパイプラインを備えています。これらはさらに自分のデータに合わせて微調整することで、さらに高いパフォーマンスを得ることができます。
特徴
- :hugs: :hugs: モデルハブ に事前学習済みパイプラインとモデル
- :exploding_head: 最先端のパフォーマンス (ベンチマーク を参照)
- :snake: Python優先のAPI
- :zap: pytorch-lightning を用いたマルチGPUトレーニング
ベンチマーク
pyannote.audio
話者ダイアライゼーションパイプライン v3.1 は、v2.x よりも大幅に優れた(かつ高速な)パフォーマンスを期待できます。以下の数値は話者ダイアライゼーションエラー率(%)を示しています。
ベンチマーク v2.1 v3.1 pyannoteAI AISHELL-4 14.1 12.2 11.9 AliMeeting (channel 1) 27.4 24.4 22.5 AMI (IHM) 18.9 18.8 16.6 AMI (SDM) 27.1 22.4 20.9 AVA-AVD 66.3 50.0 39.8 CALLHOME (part 2) 31.6 28.4 22.2 DIHARD 3 (full) 26.9 21.7 17.2 Earnings21 17.0 9.4 9.0 Ego4D (dev.) 61.5 51.2 43.8 MSDWild 32.8 25.3 19.8 RAMC 22.5 22.2 18.4 REPERE (phase2) 8.2 7.8 7.6 VoxConverse (v0.3) 11.2 11.3 9.4
そういえば、kotoba-whisperでも話者ダイアライぜーションはpyannote.audioが使用されていた。
話者ダイアライぜーションだけでなく、VADなどにも使えるようなので、試してみる。
pyannote.audioで使用するパイプラインはgatedなので、事前にHuggingFaceのサイトで利用許諾の承認が必要になる。
Colaboratory T4で。
パッケージインストール。セッションの再起動が求められる。
!pip install pyannote.audio
!pip freeze | grep -i pyannote
pyannote.audio==3.3.2
pyannote.core==5.0.0
pyannote.database==5.1.3
pyannote.metrics==3.2.1
pyannote.pipeline==3.0.1
日本語での会話音声のサンプルとして、kotoba-whisperのサンプル音声を使用させていただく。
!wget https://huggingface.co/kotoba-tech/kotoba-whisper-v2.2/resolve/main/sample_audio/sample_diarization_japanese.mp3
余談だが、mp3では失敗することも多い(torchaudioのバックエンドでうまくいかないらしい)ので、wavに変換する。なお、pyannote.audioが期待するフォーマットは16000Hz・モノラルのようだが、これはパイプラインが自動的に変換してくれる。
!ffmpeg -i sample_diarization_japanese.mp3 sample_diarization_japanese.wav
実際に音声は確認できる。二人の男性が会話していて、少し後ろで声が聞こえたり、最後に女性だけの声が含まれるようなサンプルになっている。
from IPython.display import Audio, display
display(Audio("sample_diarization_japanese.mp3", autoplay=True))
READMEのサンプルを実行
from pyannote.audio import Pipeline
from google.colab import userdata
import torch
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token=userdata.get('HF_TOKEN')
)
pipeline.to(torch.device("cuda"))
diarization = pipeline("sample_diarization_japanese.mp3")
for turn, _, speaker in diarization.itertracks(yield_label=True):
print(f"start={turn.start:.1f}s stop={turn.end:.1f}s speaker_{speaker}")
以下のように、各話者が発話している期間が出力される。
start=0.0s stop=13.7s speaker_SPEAKER_01
start=13.7s stop=18.3s speaker_SPEAKER_02
start=18.7s stop=21.9s speaker_SPEAKER_02
start=22.2s stop=24.9s speaker_SPEAKER_00
パイプラインの実行結果は、notebookだと以下のようにすると可視化できる。
from pyannote.core import notebook, Segment
notebook.crop = Segment(0, 30) # 可視化の範囲を0〜30秒に限定
diarization
結果はRTTMフォーマットで保存できる。
with open("audio.sample_diarization_japanese.rttm", "w") as rttm:
diarization.write_rttm(rttm)
保存したRTTMはload_rttm
で読み出せる
from pyannote.database.util import load_rttm
past_result = load_rttm("sample_diarization_japanese.rttm")["sample_diarization_japanese"]
for turn, _, speaker in past_result.itertracks(yield_label=True):
print(f"start={turn.start:.1f}s stop={turn.end:.1f}s speaker_{speaker}")
display(past_result)
Whisperと組み合わせれば、話者ごとに書き起こしができる。
GitHubにあるノートブックのサンプルは豊富なんだけど、探しにくい感もある。話者分離を単純に実行したい、ならば、HuggingFaceのモデルカードを見るほうが迷わなくて済みそう。
他にもパイプラインがある。
from huggingface_hub import HfApi
available_pipelines = [p.modelId for p in HfApi().list_models(filter="pyannote-audio-pipeline")]
list(filter(lambda p: p.startswith("pyannote/"), available_pipelines))
['pyannote/speaker-diarization-3.1',
'pyannote/speaker-diarization',
'pyannote/overlapped-speech-detection',
'pyannote/voice-activity-detection',
'pyannote/speaker-diarization-3.0',
'pyannote/speech-separation-ami-1.0',
'pyannote/speaker-segmentation']
VAD
VADについては、上記のvoice-activity-detectionとsegmentationのどちらでもできるみたいだけど、どっちがいいとかあるんだろうか?個人的な印象としてはsegmentationってのはなんか包括的な印象を受ける。
とりあえずvoice-activity-detectionを使った場合
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained(
"pyannote/voice-activity-detection",
use_auth_token=userdata.get('HF_TOKEN')
)
output = pipeline("sample_diarization_japanese.wav")
for speech in output.get_timeline().support():
print(speech)
[ 00:00:00.165 --> 00:00:09.953]
[ 00:00:10.307 --> 00:00:21.884]
[ 00:00:22.356 --> 00:00:25.022]
output
ストリーミングでやりたい場合は以下が参考になりそう
上記のサイトでは中でこういうのを使っている
Issueを色々見てたら、リアルタイムな話者ダイアライぜーションで以下が紹介されていた。
Diartは、AI搭載のリアルタイム音声アプリケーションを構築するためのPythonフレームワークです。その主な特徴は、一般的に「話者ダイアライゼーション」として知られているタスクを、最先端のパフォーマンスでリアルタイムに異なる話者を認識する能力です。
まとめ
話者分離やVADがかなり手軽に実現できる。
ただ、モデルがいろいろあってどれを選べばいいのかわかりにくいかな
ローカルのMac上でVAD試してみた
Python仮想環境を作成。自分はmiseを使うが、適宜。
mkdir pyannote-audio-vad-work && cd pyannote-audio-vad-work
mise use python@3.12
cat << 'EOS' >> .mise.toml
[env]
_.python.venv = { path = ".venv", create = true }
EOS
パッケージインストール
pip install pyannote.audio sounddevice numpy torch
ChatGPTに書いてもらったスクリプト
import numpy as np
import sounddevice as sd
from pyannote.audio import Model
from pyannote.core import SlidingWindow, SlidingWindowFeature
import torch
import os
class MicrophoneBuffer:
def __init__(self, sample_rate=16000, duration=5.0, step=1.0):
self.sample_rate = sample_rate
self.duration = duration
self.step = step
self.buffer_size = int(sample_rate * duration)
self.step_size = int(sample_rate * step)
self.buffer = np.zeros(self.buffer_size, dtype=np.float32)
def stream_audio(self):
"""Start streaming audio from the microphone."""
# マイクデバイスの番号は以下で確認できる
# import sounddevice as sd
# print(sd.query_devices())
with sd.InputStream(device=4, channels=1, samplerate=self.sample_rate, callback=self._audio_callback):
print("Listening to the microphone. Press Ctrl+C to stop.")
while True:
yield self._get_current_buffer()
def _audio_callback(self, indata, frames, time, status):
"""Callback to update the buffer."""
if status:
print(f"Audio input error: {status}")
self.buffer = np.roll(self.buffer, -frames)
self.buffer[-frames:] = indata[:, 0]
def _get_current_buffer(self):
"""Return the current buffer as a SlidingWindowFeature."""
resolution = SlidingWindow(start=0.0, duration=1.0 / self.sample_rate, step=1.0 / self.sample_rate)
return SlidingWindowFeature(self.buffer.copy().reshape(-1, 1), resolution)
class VoiceActivityDetection:
def __init__(self):
print("Loading VAD model...")
self.model = Model.from_pretrained(
"pyannote/segmentation",
use_auth_token=os.environ["HF_TOKEN"]
)
self.model.eval()
def __call__(self, current_buffer: SlidingWindowFeature):
with torch.no_grad():
# numpy.ndarray を torch.Tensor に変換
waveform_tensor = torch.from_numpy(current_buffer.data.T[np.newaxis]).float()
# モデルに渡してセグメンテーション結果を取得
segmentation = self.model(waveform_tensor).numpy()[0]
# モデル出力から直接フレーム情報を取得
num_frames = segmentation.shape[0]
frame_duration = current_buffer.sliding_window.duration / num_frames
resolution = SlidingWindow(
start=current_buffer.sliding_window.start,
duration=frame_duration,
step=frame_duration
)
# 音声活動確率を計算
speech_probability = np.max(segmentation, axis=-1, keepdims=True)
return SlidingWindowFeature(speech_probability, resolution)
def main():
sample_rate = 16000
buffer_duration = 1.0
buffer_step = 0.5
# Initialize microphone buffer and VAD
buffer = MicrophoneBuffer(sample_rate=sample_rate, duration=buffer_duration, step=buffer_step)
vad = VoiceActivityDetection()
# 状態を初期化
previous_state = None
try:
for current_buffer in buffer.stream_audio():
result = vad(current_buffer)
# スピーチ確率の平均を計算
speech_prob = result.data.mean()
# 有音・無音の閾値を設定
threshold = 0.9
current_state = "Speech" if speech_prob > threshold else "Silence"
# 状態が変化したときのみ出力
if current_state != previous_state:
print(f"Detected: {current_state}")
previous_state = current_state
except KeyboardInterrupt:
print("\nStopped listening to the microphone.")
if __name__ == "__main__":
main()
実行するとこんな感じになる。
Loading VAD model...
(snip)
Listening to the microphone. Press Ctrl+C to stop.
Detected: Silence
Detected: Speech
Detected: Silence
Detected: Speech
Detected: Silence
Detected: Speech
Detected: Silence
Detected: Speech
Detected: Silence
Detected: Speech
Detected: Silence
Detected: Speech
Detected: Silence
^C
Stopped listening to the microphone.
ちゃんと中身を追いかけていないのだけど、いい感じのパラメータに設定するのがむずい感はある。
VADだけなら、Silero VADのほうがシンプルで使いやすいかなぁ。