Initial commit: auto-video-cut project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
117
auto_video_cut/merger.py
Executable file
117
auto_video_cut/merger.py
Executable 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
|
||||
Reference in New Issue
Block a user