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:
+29
-2
@@ -62,12 +62,31 @@ async def transcribe(file: UploadFile = File(...), language: str = "zh", diarize
|
|||||||
try:
|
try:
|
||||||
shutil.copyfileobj(file.file, tmp)
|
shutil.copyfileobj(file.file, tmp)
|
||||||
tmp.close()
|
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
|
# Run CPU-heavy whisperx in thread to not block health endpoint
|
||||||
loop = asyncio.get_running_loop()
|
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
|
return result
|
||||||
finally:
|
finally:
|
||||||
Path(tmp.name).unlink(missing_ok=True)
|
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):
|
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 = []
|
segments_with_speakers = []
|
||||||
if do_diarize:
|
if do_diarize:
|
||||||
try:
|
try:
|
||||||
|
import torch
|
||||||
diarize_model = get_diarize()
|
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)
|
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