Zenn
Closed9

話者ダイアライゼーション用ツールキット「pyannote.audio」を試す(VADも)

kun432kun432

GitHubレポジトリ

https://github.com/pyannote/pyannote-audio

pyannote.audioオープンソースツールキットを本番環境で使用していますか?
より優れた高速なオプションを提供する pyannoteAI への移行を検討してください。

pyannote.audio 話者ダイアライゼーションツールキット

pyannote.audio は Python で書かれた話者ダイアライゼーション用のオープンソースツールキットです。
PyTorch 機械学習フレームワークに基づいており、最先端の事前学習済みモデルとパイプラインを備えています。これらはさらに自分のデータに合わせて微調整することで、さらに高いパフォーマンスを得ることができます。

特徴

ベンチマーク

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が使用されていた。

https://zenn.dev/kun432/scraps/c3111462f8b70f

話者ダイアライぜーションだけでなく、VADなどにも使えるようなので、試してみる。

kun432kun432

pyannote.audioで使用するパイプラインはgatedなので、事前にHuggingFaceのサイトで利用許諾の承認が必要になる。

https://huggingface.co/pyannote/speaker-diarization-3.1


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のサンプル音声を使用させていただく。

https://huggingface.co/kotoba-tech/kotoba-whisper-v2.2/tree/main/sample_audio

!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)

kun432kun432

他にもパイプラインがある。

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']
kun432kun432

VAD

VADについては、上記のvoice-activity-detectionとsegmentationのどちらでもできるみたいだけど、どっちがいいとかあるんだろうか?個人的な印象としてはsegmentationってのはなんか包括的な印象を受ける。

とりあえずvoice-activity-detectionを使った場合

https://huggingface.co/pyannote/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

ストリーミングでやりたい場合は以下が参考になりそう

https://herve.niderb.fr/fastpages/2021/08/05/Streaming-voice-activity-detection-with-pyannote.html

上記のサイトでは中でこういうのを使っている

https://github.com/python-streamz/streamz?tab=readme-ov-file

kun432kun432

Issueを色々見てたら、リアルタイムな話者ダイアライぜーションで以下が紹介されていた。

https://github.com/juanmc2005/diart

Diartは、AI搭載のリアルタイム音声アプリケーションを構築するためのPythonフレームワークです。その主な特徴は、一般的に「話者ダイアライゼーション」として知られているタスクを、最先端のパフォーマンスでリアルタイムに異なる話者を認識する能力です。

kun432kun432

まとめ

話者分離やVADがかなり手軽に実現できる。

ただ、モデルがいろいろあってどれを選べばいいのかわかりにくいかな

kun432kun432

ローカルの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のほうがシンプルで使いやすいかなぁ。

https://zenn.dev/kun432/scraps/f1b92c6510c87f

このスクラップは3ヶ月前にクローズされました
ログインするとコメントできます