transcribe_server: bypass whisperx DiarizationPipeline, use raw pyannote Pipeline

Speaker labels were empty because whisperx 3.8.5's DiarizationPipeline wrapper
has a compatibility bug with pyannote.audio 4.0.4: the wrapper silently returns
empty segments (the 'fallback to plain text' message in 3.8.5 logs), so
segments.speaker becomes '' instead of 'SPEAKER_XX'.

Fix: load pyannote.audio.Pipeline directly and convert DiarizeOutput →
DataFrame the same way whisperx/diarize.py does internally. Verified with 30s
Cantonese clip from real recording (chunk_0.mp3):

  speaker: SPEAKER_00  (was '')
  text: '那就係F5嘅Low Balancer用喺你哋嘅...特別感謝Sherry...'
  took: 34s for 30s audio (1.13x realtime, CPU int8 + diarize=1)
  curl http://localhost:8765/transcribe -F file=@test.wav -F language=yue -F diarize=1
This commit is contained in:
IT狗
2026-08-12 01:34:45 +08:00
parent 335b0d8ee5
commit 9a93cb6ffd
+26 -10
View File
@@ -47,9 +47,18 @@ def get_model():
def get_diarize(): def get_diarize():
global _diarize global _diarize
if _diarize is None: if _diarize is None:
from whisperx.diarize import DiarizationPipeline # BUGFIX (2026-08-12): whisperx 3.8.5's DiarizationPipeline wrapper has a bug
print("Loading diarization model...") # with pyannote.audio 4.0.4 (returns empty segs, speaker=""). Use raw pyannote
_diarize = DiarizationPipeline(token=os.environ["HF_TOKEN"], device="cpu", model_name="pyannote/speaker-diarization-3.1") # Pipeline directly — confirmed working: 60s audio → 1 seg with SPEAKER_00.
from pyannote.audio import Pipeline
import torch
print("Loading diarization model (raw pyannote Pipeline)...")
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
token=os.environ["HF_TOKEN"],
)
pipeline.to(torch.device("cpu"))
_diarize = pipeline
print("Diarization loaded!") print("Diarization loaded!")
return _diarize return _diarize
@@ -103,15 +112,22 @@ def _do_transcribe(audio_path: str, language: str, do_diarize: bool):
if do_diarize: if do_diarize:
try: try:
import torch import torch
diarize_model = get_diarize() import pandas as pd
# BUGFIX (2026-08-11): pyannote's torchcodec can't load (FFmpeg 8 only has diarize_pipeline = get_diarize()
# libavutil.60, venv needs .59). Pre-load audio as tensor dict to bypass. # BUGFIX (2026-08-12): raw pyannote Pipeline; bypass whisperx wrapper.
if isinstance(audio, np.ndarray): # Pass dict {waveform, sample_rate} — same shape whisperx used internally.
audio_input = {"waveform": torch.from_numpy(audio).unsqueeze(0).float(), audio_input = {"waveform": torch.from_numpy(audio).unsqueeze(0).float(),
"sample_rate": 16000} "sample_rate": 16000}
diar_segments = diarize_model(audio_input) diarize_output = diarize_pipeline(audio_input, min_speakers=1, max_speakers=4)
else: # Raw pyannote returns DiarizeOutput; convert Annotation → DataFrame
diar_segments = diarize_model(audio) # the same way whisperx/diarize.py does.
diarization = diarize_output.speaker_diarization
diar_segments = pd.DataFrame(
diarization.itertracks(yield_label=True),
columns=['segment', 'label', 'speaker'],
)
diar_segments['start'] = diar_segments['segment'].apply(lambda x: x.start)
diar_segments['end'] = diar_segments['segment'].apply(lambda x: x.end)
result = whisperx.assign_word_speakers(diar_segments, result) result = whisperx.assign_word_speakers(diar_segments, result)
for seg in result["segments"]: for seg in result["segments"]:
segments_with_speakers.append({ segments_with_speakers.append({