118 lines
3.3 KiB
Python
Executable File
118 lines
3.3 KiB
Python
Executable File
"""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
|