"""Clips zusammenführen via ffmpeg concat.""" from __future__ import annotations import subprocess import tempfile from pathlib import Path from .config import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS def _run(cmd: list[str]) -> subprocess.CompletedProcess: return subprocess.run(cmd, capture_output=True, text=True, check=False) def image_to_clip( image_path: Path, output_path: Path, duration: float = 3.0, width: int = 1920, height: int = 1080, ) -> Path: """Standbild in Video-Clip konvertieren.""" output_path.parent.mkdir(parents=True, exist_ok=True) cmd = [ "ffmpeg", "-y", "-loop", "1", "-i", str(image_path), "-t", str(duration), "-vf", f"scale={width}:{height}:force_original_aspect_ratio=decrease," f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-r", "25", str(output_path), ] result = _run(cmd) if result.returncode != 0: raise RuntimeError(f"ffmpeg image-to-clip Fehler: {result.stderr}") return output_path def normalize_clip(input_path: Path, output_path: Path) -> Path: """Clip auf einheitliches Format re-encoden (für concat-Kompatibilität).""" output_path.parent.mkdir(parents=True, exist_ok=True) cmd = [ "ffmpeg", "-y", "-i", str(input_path), "-c:v", "libx264", "-c:a", "aac", "-pix_fmt", "yuv420p", "-r", "25", str(output_path), ] result = _run(cmd) if result.returncode != 0: raise RuntimeError(f"ffmpeg normalize Fehler: {result.stderr}") return output_path def merge_clips( clips: list[Path], output_path: Path, intro: Path | None = None, outro: Path | None = None, normalize: bool = True, ) -> Path: """Clips zusammenführen, optional mit Intro und Outro.""" output_path.parent.mkdir(parents=True, exist_ok=True) all_clips: list[Path] = [] if intro: all_clips.append(intro) all_clips.extend(clips) if outro: all_clips.append(outro) if not all_clips: raise ValueError("Keine Clips zum Zusammenführen.") if len(all_clips) == 1: if normalize: return normalize_clip(all_clips[0], output_path) cmd = ["ffmpeg", "-y", "-i", str(all_clips[0]), "-c", "copy", str(output_path)] result = _run(cmd) if result.returncode != 0: raise RuntimeError(f"ffmpeg copy Fehler: {result.stderr}") return output_path with tempfile.TemporaryDirectory() as tmp_dir: tmp = Path(tmp_dir) if normalize: ready_clips = [ normalize_clip(clip, tmp / f"norm_{i:04d}.mp4") for i, clip in enumerate(all_clips) ] else: ready_clips = all_clips list_file = tmp / "clips.txt" with open(list_file, "w", encoding="utf-8") as fh: for clip in ready_clips: fh.write(f"file '{clip.resolve()}'\n") cmd = [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(list_file), "-c", "copy", str(output_path), ] result = _run(cmd) if result.returncode != 0: raise RuntimeError(f"ffmpeg concat Fehler: {result.stderr}") return output_path