"""ffmpeg helpers: locate the binary and normalize audio to 16 kHz mono WAV. Whisper/WhisperX expect 16 kHz mono; normalizing once up front also makes decoding of exotic inputs (m4a/opus/wma/video containers) reliable. We always call ffmpeg by an absolute path (missing-ffmpeg-on-PATH is the #1 cause of "the audio query does nothing"). """ from __future__ import annotations import shutil import subprocess import sys from pathlib import Path from typing import Optional from app.config import get_config AUDIO_EXTS = { ".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma", ".mp4", ".webm", ".mkv", ".mov", ".3gp", } _FFMPEG: Optional[str] = None def find_ffmpeg() -> str: """Locate ffmpeg: PATH first, then a bundled project ``bin/ffmpeg.exe``.""" global _FFMPEG if _FFMPEG: return _FFMPEG exe = shutil.which("ffmpeg") if not exe: binname = "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg" bundled = get_config().root / "bin" / binname if bundled.exists(): exe = str(bundled) if not exe: raise FileNotFoundError( "ffmpeg not found. Install it (winget install Gyan.FFmpeg) or place ffmpeg.exe in ./bin/." ) _FFMPEG = exe return exe def normalize_to_wav(src: str | Path, dst: str | Path) -> Path: """Decode any input to 16 kHz mono 16-bit PCM WAV at ``dst``.""" dst = Path(dst) dst.parent.mkdir(parents=True, exist_ok=True) cmd = [ find_ffmpeg(), "-y", "-hide_banner", "-loglevel", "error", "-i", str(src), "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", "-f", "wav", str(dst), ] proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0 or not dst.exists(): raise RuntimeError(f"ffmpeg failed for {src}: {proc.stderr.strip()[:500]}") return dst