mattrguerra/transcription-pipelinemainPrivate on GitHub, published here in full

audio.py

Language Python2.5 KB74 lines

transcription_pipeline/audio.py

  1. """ffmpeg: any audio or video in, 16 kHz mono out.
  2. Uses the system ffmpeg when there is one, else the static binary that the
  3. imageio-ffmpeg package ships, so nothing needs installing system-wide.
  4. """
  5. from __future__ import annotations
  6. import shutil
  7. import subprocess
  8. import wave
  9. from pathlib import Path
  10. AUDIO_EXTS = {".mp3", ".m4a", ".wav", ".aac", ".flac", ".ogg", ".opus", ".wma", ".aiff", ".aif",
  11. ".mp4", ".mov", ".m4v", ".mkv", ".webm", ".avi"}
  12. class AudioError(RuntimeError):
  13. pass
  14. def ffmpeg_path() -> str:
  15. found = shutil.which("ffmpeg")
  16. if found:
  17. return found
  18. try:
  19. import imageio_ffmpeg
  20. return imageio_ffmpeg.get_ffmpeg_exe()
  21. except Exception as e: # pragma: no cover - depends on the machine
  22. raise AudioError("ffmpeg not found: install it, or `pip install imageio-ffmpeg`.") from e
  23. def _run(args: list[str]) -> None:
  24. cmd = [ffmpeg_path(), "-nostdin", "-hide_banner", "-loglevel", "error", "-y", *args]
  25. p = subprocess.run(cmd, capture_output=True, text=True)
  26. if p.returncode != 0:
  27. msg = (p.stderr or "").strip().splitlines()
  28. raise AudioError(f"ffmpeg could not read this file: {msg[-1] if msg else 'unknown error'}")
  29. def to_wav16k(src: Path, dst: Path) -> Path:
  30. """Decode to 16 kHz mono 16-bit WAV: what the local models expect."""
  31. _run(["-i", str(src), "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", str(dst)])
  32. return dst
  33. def to_flac16k(src: Path, dst: Path) -> Path:
  34. """16 kHz mono FLAC for upload: lossless, and about half the size of WAV."""
  35. _run(["-i", str(src), "-vn", "-ac", "1", "-ar", "16000", "-c:a", "flac", str(dst)])
  36. return dst
  37. def wav_duration(path: Path) -> float:
  38. with wave.open(str(path), "rb") as w:
  39. return w.getnframes() / float(w.getframerate())
  40. def hms(seconds: float) -> str:
  41. s = int(round(seconds))
  42. return f"{s // 3600}:{s // 60 % 60:02d}:{s % 60:02d}"
  43. def load_wav16k(path: Path):
  44. """The 16 kHz mono WAV as float32 samples in [-1, 1], the array WhisperX takes.
  45. Read directly rather than through whisperx.load_audio, which shells out to
  46. an `ffmpeg` on PATH and so would bring back the system dependency.
  47. """
  48. import numpy as np
  49. with wave.open(str(path), "rb") as w:
  50. if w.getframerate() != 16000 or w.getnchannels() != 1 or w.getsampwidth() != 2:
  51. raise AudioError(f"{path.name}: expected 16 kHz mono 16-bit WAV")
  52. frames = w.readframes(w.getnframes())
  53. return np.frombuffer(frames, dtype="<i2").astype(np.float32) / 32768.0