import asyncio import math import os import subprocess from pathlib import Path class FfmpegError(RuntimeError): pass async def run_ffmpeg(args: list[str]) -> None: process = await asyncio.create_subprocess_exec( *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode != 0: detail = stderr.decode("utf-8", errors="replace")[-4000:] or stdout.decode("utf-8", errors="replace")[-4000:] raise FfmpegError(detail) def probe_duration_seconds(path: Path) -> float | None: result = subprocess.run( [ "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", str(path), ], capture_output=True, text=True, check=False, ) if result.returncode != 0: return None try: return float(result.stdout.strip()) except ValueError: return None async def compress_audio(input_path: Path, output_path: Path) -> None: await run_ffmpeg( [ "ffmpeg", "-y", "-i", str(input_path), "-vn", "-ar", "16000", "-ac", "1", "-c:a", "libmp3lame", "-b:a", "32k", str(output_path), ] ) async def chunk_audio(input_path: Path, output_dir: Path, chunk_duration_seconds: int) -> list[Path]: output_dir.mkdir(parents=True, exist_ok=True) output_pattern = output_dir / "chunk_%03d.mp3" await run_ffmpeg( [ "ffmpeg", "-y", "-i", str(input_path), "-f", "segment", "-segment_time", str(chunk_duration_seconds), "-c", "copy", str(output_pattern), ] ) return sorted(output_dir.glob("chunk_*.mp3")) def size_mb(path: Path) -> float: return os.path.getsize(path) / (1024 * 1024) def needs_chunking(path: Path, target_size_mb: int) -> bool: return size_mb(path) > target_size_mb def estimated_chunk_duration(duration_seconds: float | None, final_size_mb: float, target_size_mb: int, fallback_seconds: int) -> int: if not duration_seconds or final_size_mb <= 0 or final_size_mb <= target_size_mb: return fallback_seconds chunks = max(2, math.ceil(final_size_mb / target_size_mb)) return max(60, math.floor(duration_seconds / chunks))