こんにちは、パレイド技術部の夏目です。
前回の議事録ワークフローの記事で会議録音を文字に起こすところまで整理しましたが、宿題が一つ残っています。「誰がいつ話したか」は 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: 音声 → 話者ラベル付きの時間区間
今回やるのは、この二つの出力を時間で突き合わせて一本に統合することです。pyannote は声の特徴で話者をクラスタリングするだけなので名前まではわからず、出るのは SPEAKER_00 SPEAKER_01 という匿名の通し番号。後から「話者A/B」と読み替えます。
準備:HuggingFace トークンとモデル利用許諾
pyannote の標準モデルは HuggingFace 上でゲート(利用許諾付き)配布で、pip install だけでは動きません。現行の既定モデルは pyannote/speaker-diarization-community-1(後述の WhisperX 3.8.6 も内部でこれを呼ぶことをログで確認しました)。手順は三つです。
- HuggingFace アカウントを作る(無料)
pyannote/speaker-diarization-community-1のモデルページで利用条件に同意(Agree)する- Settings → Access Tokens から
read権限のトークンを発行(hf_xxxxxxxxの形)
罠は、トークンを発行しただけでは通らない点です。「Agree」を押していないと、有効なトークンでも実行時に 403 GatedRepoError で弾かれます。実際、同意前のトークンで叩いて一度止まりました。旧 speaker-diarization-3.1 も併存しますが、依存する segmentation-3.0 にも別途同意が要ります。
トークンはパスワードと同じです。コードに直書きせず環境変数や .env から読み込みます(以下は hf_xxx をプレースホルダにしています)。
pyannote.audio で話者を分ける(Apple Silicon の罠まで)
インストールは一行です。
# 本体(PyTorch などの依存も一緒に入る)
pip install pyannote.audio
最小コードは次のとおり。pyannote-audio 4.x で API が変わり、認証は 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 を渡す。話者数がわかれば 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}")
出力は時間区間と話者ラベルの羅列です。
0.0s - 3.2s : SPEAKER_00
3.2s - 7.8s : SPEAKER_01
7.8s - 9.5s : SPEAKER_00
人数が事前にわかるなら num_speakers=2 で固定すると安定します。指定しないとノイズや相槌を別話者として拾い、想定より多くの SPEAKER_xx が出ます。
ここで Apple Silicon + torch 2.8 の罠。上のようにファイルパスを直接渡すと、内部の 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
# 辞書で渡す(ファイルパスを渡さない)
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)対応が部分的、未対応の演算が実質 CPU にフォールバックして回ることが多く、今回の M5(CPU)では community-1 が 12 秒の音声におよそ 5 秒(ウォーム状態)でした。ただしこれは合成した短い音声での値です。精度も、say で 2 声を合成した音源では SPEAKER がうまく割れませんでした。話者分離は 実音声で、長さがあり、声質に差があるほど安定する傾向です。数字は目安とし、まず本物の会議録音の短い区間で当たりを取るのが堅実です。
Whisper の文字起こしと統合する
本題です。Whisper はタイムスタンプ付きのセグメント(--output_format json、または Python API の segments の start / end)を出せます。各テキストセグメントに対し、時間的に最も重なりの大きい pyannote の話者区間を探して割り当てます。
# whisper_segments: [{"start": 0.0, "end": 3.0, "text": "おはようございます"}, ...]
# diarization: 上記 out.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 と読み替えれば議事録の体裁です。ただしターンの境界付近では取り違えが起きやすく、相槌が重なる場面や区間が細切れな場面では精度が落ちます。重要な箇所は目視補正する前提で使うのが堅実です。
WhisperX という選択肢(使い分け)
ここまでは Whisper と pyannote を自前で組み合わせる構成でした。利点は仕組みが見えることで、精度が悪いときにどこを直すかがわかります。一方で手数は多い。「とにかく話者付きの結果が欲しい」なら、文字起こし・単語単位のタイムスタンプ・話者分離を一本化した WhisperX という選択肢があります。
- pyannote 自前構成: 仕組みを理解して組みたい / 各段を細かく制御したい
- WhisperX 全部入り: 構成を意識せず、まとまった結果を手早く得たい
WhisperX も内部で pyannote を使うので、HuggingFace の許諾という壁は共通です。手順はWhisperX の記事(単語タイムスタンプ+話者分離を一本で)にまとめました。
まとめ
骨格は Whisper が「何を」、pyannote が「誰がいつ」を担当し、時間で突き合わせて一本にすることに尽きます。
| 工程 | 担当 | 出力 |
|---|---|---|
| 文字起こし | Whisper | テキスト + タイムスタンプ |
| 話者分離 | pyannote | 話者ラベル + 時間区間 |
| 統合 | 自前コード | 「話者A: ……」付きテキスト |
つまずきは三つ。HuggingFace の利用条件への同意(トークン発行だけでは 403)、Apple Silicon + torch 2.8 で音声のメモリ事前ロードが要ること、話者分離が 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 を入れて音声を文字に起こすところまでは終えました。ただ、いざ動かす…