← [ TECH / 技術部 ] に戻る
OBSERVATION · 其の4861 · 2026.06.09

Whisperで話者分離:pyannoteで「誰がいつ話したか」をMacローカルで

Whisperで話者分離:pyannoteで「誰がいつ話したか」をMacローカルで — Whisper, pyannote, 話者分離

こんにちは、パレイド技術部の夏目です。

前回の議事録ワークフローの記事で、会議録音を文字に起こすところまでは整理しました。ただ、そこで一つ宿題を残しています。「誰がいつ話したか」という話者の区別は、Whisper 単体ではできないという点です。議事録として読むには「話者A: ……」「話者B: ……」と発言者がわかってほしい。今回はその受け皿として、pyannote.audio という話者分離ツールを足します。

Whisper が「何を話したか」を担当し、pyannote が「誰がいつ話したか」を担当する。この二つを時間で突き合わせれば、発言者付きの議事録になります。すべて Mac のローカルで完結する範囲の話ですが、HuggingFace の利用許諾という最初の壁が少し高く、さらに Apple Silicon 特有の罠もあったので、そこを実際に踏んだ手順で丁寧にたどります。

今回の検証環境は次のとおりです。バージョン依存の挙動が多い領域なので、最初に明記しておきます。

項目 内容
マシン Apple M5 / macOS
Python 3.12
pyannote-audio 4.0.4
torch 2.8.0
(参考)whisperx 3.8.6 / faster-whisper 1.2.1

本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。

話者分離とは(Whisper単体の限界)

Whisper は音声を文字に起こしますが、「いま話しているのが誰か」というラベルは付けません。一人語りのナレーションなら問題になりませんが、複数人の会議録だと全員の発言が一本のテキストに溶けてしまい、「この発言は誰のものか」が読み取れなくなります。

ここで必要になるのが 話者分離(speaker diarization) です。これは「いつ・誰が話していたか」を時間区間で区切る処理で、音声を「0.0〜3.2秒は話者A、3.2〜7.8秒は話者B」のように塗り分けます。何を言ったか(Whisper の仕事)と、誰がいつ話したか(pyannote の仕事)は、まったく別のタスクだと考えると整理しやすいです。

  • Whisper: 音声 → テキスト(タイムスタンプ付き)
  • pyannote: 音声 → 話者ラベル付きの時間区間

つまり今回やることは、この二つの出力を時間で突き合わせて一本に統合することです。pyannote は声の特徴から話者をクラスタリングするため、名前まではわかりません。出てくるのは SPEAKER_00 SPEAKER_01 といった匿名の通し番号で、それを後から「話者A/B」と読み替える形になります。

準備:HuggingFace トークンとモデル利用許諾(壁を越える)

pyannote の標準モデルは HuggingFace 上でゲート(利用許諾付き)配布されています。pip install だけでは動かず、事前にアカウントと許諾、アクセストークンの三点をそろえる必要があります。ここが最初の関門です。今回使う現行の既定モデルは pyannote/speaker-diarization-community-1 で、後述する WhisperX 3.8.6 も内部でこのモデルを呼びます(ログで確認しました)。

手順は次のとおりです。特に見落としやすいのが、トークンを発行しただけでは通らないという点です。

  1. HuggingFace アカウントを作る(無料)
  2. モデルページで利用条件に同意する
  3. pyannote/speaker-diarization-community-1
  4. ※モデルページの「Agree(同意)」を押していないと、有効なトークンを持っていても実行時に 403 GatedRepoError で弾かれます。「トークンを発行した=即使える」ではないことに注意してください。実際、同意前のトークンで叩いてここで一度止まりました
  5. アクセストークンを発行する
  6. Settings → Access Tokens から read 権限のトークンを作成(hf_xxxxxxxx の形)

なお旧 speaker-diarization-3.1 も併存していますが、こちらは依存する segmentation-3.0 にも別途同意が必要です。本記事では現行の既定である community-1 を主に扱います。

発行したトークンはパスワードと同じものです。他人に渡さない、公開リポジトリにコミットしない、本文中のコードに直書きしない。環境変数や .env から読む形にして、記事やスクショに実物が写り込まないよう注意してください。以下のコードでは hf_xxx をプレースホルダとして置いています。

pyannote.audio で話者を分ける(インストール〜実行コード)

準備ができたらインストールは一行です。

# pyannote.audio 本体を入れる(PyTorch などの依存も一緒に入る)
pip install pyannote.audio

話者分離の最小コードは次のようになります。pyannote-audio 4.x では API がいくつか変わっているので、そこに合わせます。具体的には、認証は use_auth_token= ではなく token= を使い(use_auth_token は廃止)、結果の取り出しは DiarizeOutput 型を介して out.speaker_diarization.itertracks(...) を呼びます。

from pyannote.audio import Pipeline

# 4.x では token= が正(use_auth_token= は廃止)。hf_xxx は自分のトークンに
pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-community-1",
    token="hf_xxx",
)

# 16kHz モノラルの WAV を渡すのが無難(Whisper と同じ前処理でよい)
# 話者数がわかっているなら num_speakers で固定すると安定する
out = pipeline("audio.wav", num_speakers=2)

# 4.x では DiarizeOutput.speaker_diarization から区間を取り出す
for turn, _, speaker in out.speaker_diarization.itertracks(yield_label=True):
    print(f"{turn.start:.1f}s - {turn.end:.1f}s : {speaker}")

なお、上の pipeline("audio.wav", ...) のようにファイルパスを直接渡す形は、Apple Silicon + torch 2.8 の環境では torchcodec 由来のエラーで落ちることがあります(実際に踏みました)。その場合は後述の「Apple Silicon での実行」で示す 音声のメモリ事前ロード に置き換えてください。

出力は次のように、時間区間と話者ラベルの羅列になります。

0.0s - 3.2s : SPEAKER_00
3.2s - 7.8s : SPEAKER_01
7.8s - 9.5s : SPEAKER_00

話者の人数があらかじめわかっているなら、上のように num_speakers=2人数を固定すると安定します。逆に人数不明のままだと、ノイズや相槌を別話者として拾い、想定より多くの SPEAKER_xx が出ることがあります。まずは人数を指定して試すのが現実的です。

Whisper の文字起こしと統合する(タイムスタンプで突き合わせ)

ここからが本題の統合です。Whisper はタイムスタンプ付きのセグメント(--output_format json、または Python API の segments にある start / end)を出せます。各テキストセグメントが、pyannote のどの話者区間に重なるかを照合して、話者ラベルを貼っていきます。

突き合わせの考え方はシンプルです。Whisper の各セグメントについて、時間的に最も重なりの大きい話者区間を探し、その話者を割り当てます。

# whisper_segments: [{"start": 0.0, "end": 3.0, "text": "おはようございます"}, ...]
# diarization: 上記 out.speaker_diarization(DiarizeOutput.speaker_diarization)

def assign_speaker(seg, diarization):
    best_speaker, best_overlap = "SPEAKER_??", 0.0
    for turn, _, speaker in diarization.itertracks(yield_label=True):
        # セグメントと話者区間の重なり秒数を測る
        overlap = min(seg["end"], turn.end) - max(seg["start"], turn.start)
        if overlap > best_overlap:
            best_overlap, best_speaker = overlap, speaker
    return best_speaker

# 話者ラベル付きで議事録テキストに整形する(out.speaker_diarization を渡す)
for seg in whisper_segments:
    spk = assign_speaker(seg, out.speaker_diarization)
    print(f"{spk}: {seg['text']}")

これで SPEAKER_00: おはようございます のような発言者付きのテキストが得られます。あとは SPEAKER_00 → 話者A と読み替えれば議事録の体裁です。ただし発言の切れ目(ターンの境界)付近では話者の取り違えが起きやすいのが正直なところで、相槌が重なる場面や、発言が短く区間が細切れな場面では精度が落ちます。重要な箇所は目視で補正する前提で使うのが堅実です。

Apple Silicon での実行と速度感(正直に・目安)

まず、Apple Silicon + torch 2.8 で実際に踏んだ罠を共有します。pyannote を Python から直接呼ぶと、内部の音声読み込みで torchaudio.load() が「適切なバックエンドが無い」という趣旨のエラーで落ちました。原因は torchcodec が torch 2.8 と非互換だったことです。回避策は単純で、音声を自分でメモリに読み込み、波形と sample_rate を辞書にしてパイプラインへ直接渡すことです。

import wave, numpy as np, torch

# WAV をメモリに読み込み、float32 [-1, 1] に正規化する
w = wave.open("audio.wav")
sr = w.getframerate()
data = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0

# pyannote には {waveform, sample_rate} の辞書で渡す(ファイルパスを渡さない)
audio = {"waveform": torch.from_numpy(data).unsqueeze(0), "sample_rate": sr}
out = pipeline(audio, num_speakers=2)

なお、WhisperX の CLI 経由(後述)では別経路にフォールバックして動くため、この前処理は不要でした。pyannote を Python から直接使うときに限って、この事前ロードが要る、という整理になります。

速度についても正直に書きます。pyannote は PyTorch ベースで、Apple Silicon の GPU(MPS)対応は部分的です。一部の演算が MPS に未対応で、実質 CPU にフォールバックして回ることが多いのが現状です。今回の M5(CPU)では、community-1 の話者分離が 12 秒の音声に対しておよそ 5 秒(ウォーム状態)でした。ただしこれは合成した短い音声での値で、長尺・実音声では変わります。数字はあくまで目安として捉えてください。

正直に言うと、精度の面では合成音声で苦戦しましたsay コマンドで 2 声を合成した音源では、SPEAKER がうまく割れず、想定どおりに 2 話者へ分離できないことがありました。話者分離は、実音声で、十分な長さがあり、話者の声質に差があるほど安定する傾向です。手元で試すときは、まず本物の会議録音の短い区間で当たりを取るのが堅実です。

もっと手軽に:WhisperX という選択肢(全部入り)

ここまでは「Whisper と pyannote を自分で組み合わせる」自前構成でした。利点は仕組みが見えることです。どこで文字を起こし、どこで話者を分け、どう突き合わせているかを把握できるので、精度が悪いときにどこを直せばいいかがわかります

一方で、手数が多いのも事実です。「とにかく話者付きの結果が欲しい」なら、文字起こし・単語単位のタイムスタンプ・話者分離を一本化した WhisperX という選択肢があります。住み分けとしては——

  • pyannote 自前構成: 仕組みを理解して組みたい / 各段を細かく制御したい
  • WhisperX 全部入り: 構成を意識せず、まとまった結果を手早く得たい

という整理になります。WhisperX も内部で pyannote を使うので、HuggingFace の許諾という壁は同じく越える必要がある点だけは共通です。WhisperX を実際に動かす手順はWhisperX の記事(単語タイムスタンプ+話者分離を一本で)にまとめました。

まとめ

要点を一枚に畳んでおきます。Whisper が「何を」、pyannote が「誰がいつ」を担当し、時間で突き合わせて一本にする——これが話者付き議事録の骨格です。

工程 担当 出力
文字起こし Whisper テキスト + タイムスタンプ
話者分離 pyannote 話者ラベル + 時間区間
統合 自前コード 「話者A: ……」付きテキスト

つまずきやすいのは HuggingFace のモデル利用条件への同意(トークン発行だけでは 403 で弾かれる)Apple Silicon + torch 2.8 では torchaudio の読み込みが落ち、音声のメモリ事前ロードが要ること、そして 話者分離が CPU 寄りで待たされやすい点の三つです。速度は環境依存の目安として捉え、まず本物の音声の短い区間で精度を確かめてから本番に進めてください。会議録音を発言者付きで残せるようになると、議事録の実用度は一段上がります。

議事録の前段である文字起こしそのものの実用ワークフローは字幕・議事録・音声入力の記事に、どの Whisper 実装を選ぶかは4実装の違いを実測で比較した決定版ガイドにまとめています。

リンク集

パレイドMacでWhisperをインストールして音声認識を試す(ローカル実行・Apple Silicon対応)こんにちは、パレイド技術部の夏目です。 (2026-06-08 更新)モデル表に large-v3-turbo を追加し、高速化の選択肢として MLX 編(③)… パレイドMacでWhisperを高速化②whisper.cpp編:導入・モデル(ggml)DL・CLI実行の使い方【Apple Silicon】こんにちは、パレイド技術部の夏目です。 (2026-06-08 更新)Apple Silicon での GPU/Metal 利用と現行 CLI 名(whispe… パレイドMacでWhisperを高速化する方法③:MLX編(Apple Silicon ネイティブ・mlx-whisper)こんにちは、パレイド技術部の夏目です。 MacでWhisperを高速化するシリーズの第3弾です。これまで①faster-whisper(Python実装・CTr… パレイドMac版Whisper比較ガイド【決定版】openai-whisper・faster-whisper・whisper.cpp・MLX、4実装を同条件で再計測こんにちは、パレイド技術部の夏目です。 (2026-06-09 更新)MLX-Whisper を加えた4実装を同条件で再計測し、決定版に改稿しました。 Macで… パレイドWhisper実用ワークフロー:字幕(SRT/VTT)・議事録・文字起こしをMacローカルでこんにちは、パレイド技術部の夏目です。 これまでの記事で、Mac に Whisper を入れて音声を文字に起こすところまでは終えました。ただ、いざ動かす…

━━ 観るのを再開 ━━
次の回を読む
【日本人面地形 06】福島 ── 磐梯山、山体崩壊が刻んだ爆裂火口
技術部を一覧で
部門アーカイブ
[NEXT] FRONT · 其の4640
【日本人面地形 06】福島 ── 磐梯山、山体崩壊が刻んだ爆裂火口
[NEXT] FRONT · 其の4700
夢十夜・第四夜 ── 手拭が蛇になると言ったまま。果たされない約束