""" Audio clip extraction using ffmpeg. Cuts clips from original broadcast MP3s for use in Audition/Audacity. """ import subprocess from pathlib import Path from rich.console import Console console = Console() def extract_clip( source_path: Path, start: float, end: float, output_path: Path, padding: float = 1.5, fade_ms: int = 200, ) -> Path: """ Extract a clip from source_path between start and end seconds. Adds padding on both sides and applies fade in/out. Returns the output path. """ source_path = Path(source_path) output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) clip_start = max(0.0, start - padding) clip_end = end + padding duration = clip_end - clip_start fade_s = fade_ms / 1000.0 cmd = [ "ffmpeg", "-y", "-ss", f"{clip_start:.3f}", "-i", str(source_path), "-t", f"{duration:.3f}", "-af", f"afade=t=in:st=0:d={fade_s},afade=t=out:st={duration - fade_s:.3f}:d={fade_s}", "-q:a", "2", str(output_path), ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"ffmpeg failed: {result.stderr[-500:]}") return output_path def extract_clips_for_results(results, output_dir: Path, padding: float = 1.5) -> dict[int, Path]: """ Extract clips for a list of QAResult or SearchResult objects. Returns {index: clip_path}. """ output_dir = Path(output_dir) clip_paths = {} for i, result in enumerate(results): episode = result.episode_id audio_path = Path(result.audio_path) if not audio_path.exists(): console.print(f"[yellow]Audio not found: {audio_path}[/yellow]") continue # Determine time range if hasattr(result, "question_start"): # QAResult start = result.question_start end = result.answer_end else: # SearchResult start = result.start end = result.end def fmt(s): m, sec = divmod(int(s), 60) h, m = divmod(m, 60) return f"{h}h{m:02d}m{sec:02d}s" if h else f"{m}m{sec:02d}s" clip_name = f"{episode}_{fmt(start)}.mp3" clip_path = output_dir / clip_name try: extract_clip(audio_path, start, end, clip_path, padding=padding) clip_paths[i] = clip_path console.print(f"[green]Clip {i+1}:[/green] {clip_name}") except Exception as e: console.print(f"[red]Clip {i+1} failed:[/red] {e}") return clip_paths def format_timestamp(seconds: float) -> str: """Format seconds as H:MM:SS or M:SS.""" h = int(seconds // 3600) m = int((seconds % 3600) // 60) s = int(seconds % 60) if h: return f"{h}:{m:02d}:{s:02d}" return f"{m}:{s:02d}"