From 335b0d8ee565361919279dbbd3ccbfe1a5bc0da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?IT=E7=8B=97?= Date: Tue, 11 Aug 2026 15:47:00 +0800 Subject: [PATCH] =?UTF-8?q?FIX:=20pre-encode=20mp3=E2=86=92wav=20+=20prelo?= =?UTF-8?q?aded=20audio=20dict=20for=20pyannote=20(FFmpeg=208=20/=20torchc?= =?UTF-8?q?odec=200.7=20broken)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- transcribe_server.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/transcribe_server.py b/transcribe_server.py index 8f4e919..8635209 100644 --- a/transcribe_server.py +++ b/transcribe_server.py @@ -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({