Initial commit: auto-video-cut project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-04-06 21:51:01 +02:00
commit 267070ad52
15 changed files with 2635 additions and 0 deletions

117
auto_video_cut/merger.py Executable file
View File

@@ -0,0 +1,117 @@
"""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