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:
+26
-10
@@ -47,9 +47,18 @@ def get_model():
|
||||
def get_diarize():
|
||||
global _diarize
|
||||
if _diarize is None:
|
||||
from whisperx.diarize import DiarizationPipeline
|
||||
print("Loading diarization model...")
|
||||
_diarize = DiarizationPipeline(token=os.environ["HF_TOKEN"], device="cpu", model_name="pyannote/speaker-diarization-3.1")
|
||||
# BUGFIX (2026-08-12): whisperx 3.8.5's DiarizationPipeline wrapper has a bug
|
||||
# with pyannote.audio 4.0.4 (returns empty segs, speaker=""). Use raw pyannote
|
||||
# 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!")
|
||||
return _diarize
|
||||
|
||||
@@ -103,15 +112,22 @@ def _do_transcribe(audio_path: str, language: str, do_diarize: bool):
|
||||
if do_diarize:
|
||||
try:
|
||||
import torch
|
||||
diarize_model = get_diarize()
|
||||
# BUGFIX (2026-08-11): pyannote's torchcodec can't load (FFmpeg 8 only has
|
||||
# libavutil.60, venv needs .59). Pre-load audio as tensor dict to bypass.
|
||||
if isinstance(audio, np.ndarray):
|
||||
import pandas as pd
|
||||
diarize_pipeline = get_diarize()
|
||||
# BUGFIX (2026-08-12): raw pyannote Pipeline; bypass whisperx wrapper.
|
||||
# Pass dict {waveform, sample_rate} — same shape whisperx used internally.
|
||||
audio_input = {"waveform": torch.from_numpy(audio).unsqueeze(0).float(),
|
||||
"sample_rate": 16000}
|
||||
diar_segments = diarize_model(audio_input)
|
||||
else:
|
||||
diar_segments = diarize_model(audio)
|
||||
diarize_output = diarize_pipeline(audio_input, min_speakers=1, max_speakers=4)
|
||||
# Raw pyannote returns DiarizeOutput; convert Annotation → DataFrame
|
||||
# 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)
|
||||
for seg in result["segments"]:
|
||||
segments_with_speakers.append({
|
||||
|
||||
Reference in New Issue
Block a user