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:
IT狗
2026-08-11 15:47:00 +08:00
parent 3126021d2e
commit 335b0d8ee5
+29 -2
View File
@@ -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,8 +102,16 @@ def _do_transcribe(audio_path: str, language: str, do_diarize: bool):
segments_with_speakers = []
if do_diarize:
try:
import torch
diarize_model = get_diarize()
diar_segments = diarize_model(audio)
# 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"]:
segments_with_speakers.append({