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():
|
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({
|
||||||
|
|||||||
Reference in New Issue
Block a user