FIX: pre-encode mp3→wav + preloaded audio dict for pyannote (FFmpeg 8 / torchcodec 0.7 broken)
ROOT CAUSE
- whisperx venv has torchcodec 0.7.0 + PyTorch 2.8.0 → torchcodec dylibs target
FFmpeg 4-7 (libavutil.56-.59) but Mac brew now ships FFmpeg 8 (libavutil.60).
- All audio loading via torchcodec silently fails (warning but hangs/returns 0).
FIX
- /transcribe endpoint: pre-encode uploaded mp3/webm → 16kHz mono WAV via
subprocess ffmpeg (whisperx.load_audio then reads clean PCM file).
- _do_transcribe diarize branch: pass {'waveform': tensor, 'sample_rate': int}
dict to pyannote Pipeline instead of file path → skips torchcodec entirely.
Tested
- chunk_0.mp3 (4MB, 5min): diarize=0 → 200 OK + hallucination filter triggered
- Confirmed working in live HTTP POST at 15:40 HKT
This commit is contained in:
+28
-1
@@ -62,12 +62,31 @@ async def transcribe(file: UploadFile = File(...), language: str = "zh", diarize
|
||||
try:
|
||||
shutil.copyfileobj(file.file, tmp)
|
||||
tmp.close()
|
||||
# BUGFIX (2026-08-11): torchcodec 0.7.0 in venv can't load FFmpeg 8 dylibs
|
||||
# (libavutil.59 missing — brew only has .60). Re-mux to 16kHz mono WAV
|
||||
# via subprocess ffmpeg so whisperx.load_audio gets a clean PCM file.
|
||||
wav_tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
||||
wav_tmp.close()
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-y", "-i", tmp.name, "-ac", "1", "-ar", "16000",
|
||||
"-acodec", "pcm_s16le", wav_tmp.name],
|
||||
capture_output=True, timeout=120
|
||||
)
|
||||
if r.returncode != 0 or not Path(wav_tmp.name).exists() or Path(wav_tmp.name).stat().st_size < 1000:
|
||||
raise RuntimeError(f"ffmpeg re-encode failed: {r.stderr.decode()[:200]}")
|
||||
audio_path = wav_tmp.name
|
||||
except Exception as e:
|
||||
log.warning(f"WAV re-encode failed ({e}), trying raw mp3")
|
||||
audio_path = tmp.name
|
||||
# Run CPU-heavy whisperx in thread to not block health endpoint
|
||||
loop = asyncio.get_running_loop()
|
||||
result = await loop.run_in_executor(None, _do_transcribe, tmp.name, language, do_diarize)
|
||||
result = await loop.run_in_executor(None, _do_transcribe, audio_path, language, do_diarize)
|
||||
return result
|
||||
finally:
|
||||
Path(tmp.name).unlink(missing_ok=True)
|
||||
if 'wav_tmp' in dir() and Path(wav_tmp.name).exists():
|
||||
Path(wav_tmp.name).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _do_transcribe(audio_path: str, language: str, do_diarize: bool):
|
||||
@@ -83,7 +102,15 @@ def _do_transcribe(audio_path: str, language: str, do_diarize: bool):
|
||||
segments_with_speakers = []
|
||||
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):
|
||||
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)
|
||||
result = whisperx.assign_word_speakers(diar_segments, result)
|
||||
for seg in result["segments"]:
|
||||
|
||||
Reference in New Issue
Block a user