| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- from __future__ import annotations
- import asyncio
- import json
- import subprocess
- import tempfile
- from dataclasses import dataclass
- from pathlib import Path
- @dataclass(frozen=True)
- class AudioInfo:
- path: Path
- codec: str | None
- sample_rate: int | None
- channels: int
- channel_layout: str | None
- duration: float | None
- class AudioProcessor:
- async def inspect(self, path: str | Path) -> AudioInfo:
- path = Path(path)
- if not path.is_file():
- raise FileNotFoundError(path)
- return await asyncio.to_thread(
- self._inspect_sync,
- path,
- )
- @staticmethod
- def _inspect_sync(path: Path) -> AudioInfo:
- result = subprocess.run(
- [
- "ffprobe",
- "-v", "error",
- "-select_streams", "a:0",
- "-show_entries",
- "stream=codec_name,sample_rate,channels,channel_layout",
- "-show_entries",
- "format=duration",
- "-of", "json",
- str(path),
- ],
- capture_output=True,
- text=True,
- check=True,
- )
- data = json.loads(result.stdout)
- stream = (data.get("streams") or [{}])[0]
- fmt = data.get("format") or {}
- return AudioInfo(
- path=path,
- codec=stream.get("codec_name"),
- sample_rate=(
- int(stream["sample_rate"])
- if stream.get("sample_rate")
- else None
- ),
- channels=int(stream.get("channels") or 1),
- channel_layout=stream.get("channel_layout"),
- duration=(
- float(fmt["duration"])
- if fmt.get("duration")
- else None
- ),
- )
- async def prepare_for_transcription(
- self,
- path: str | Path,
- ) -> list[Path]:
- info = await self.inspect(path)
- if info.channels <= 1:
- return [Path(path)]
- return await asyncio.to_thread(
- self._split_stereo_sync,
- info.path,
- )
- @staticmethod
- def _split_stereo_sync(path: Path) -> list[Path]:
- tmp = Path(
- tempfile.mkdtemp(prefix="3cx-audio-")
- )
- outputs = []
- for channel in (0, 1):
- output = tmp / f"channel-{channel}.wav"
- subprocess.run(
- [
- "ffmpeg",
- "-hide_banner",
- "-loglevel", "error",
- "-i", str(path),
- "-map_channel", f"0.0.{channel}",
- "-ar", "16000",
- "-ac", "1",
- "-c:a", "pcm_s16le",
- str(output),
- ],
- check=True,
- )
- outputs.append(output)
- return outputs
|