| """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 |
|
|