150 lines
4.8 KiB
Python
Executable File
150 lines
4.8 KiB
Python
Executable File
"""sequence.yaml parsen und geordnete Clip-Liste aufbauen."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
from .config import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
|
|
|
|
|
|
@dataclass
|
|
class ClipEntry:
|
|
"""Ein aufgelöster Clip mit Metadaten."""
|
|
path: Path
|
|
media_type: str # "video" | "image"
|
|
remove_silence: bool = False
|
|
trim_silence: bool = False
|
|
overlay_text: str | None = None
|
|
overlay_position: str = "bottom"
|
|
overlay_duration: float | None = None
|
|
image_duration: float = 3.0
|
|
|
|
|
|
def _resolve_media_type(path: Path) -> str:
|
|
ext = path.suffix.lower()
|
|
if ext in VIDEO_EXTENSIONS:
|
|
return "video"
|
|
if ext in IMAGE_EXTENSIONS:
|
|
return "image"
|
|
raise ValueError(f"Unbekannter Dateityp: {path}")
|
|
|
|
|
|
def _sort_paths(paths: list[Path], sort: str) -> list[Path]:
|
|
if sort == "alphabetical":
|
|
return sorted(paths, key=lambda p: p.name.lower())
|
|
elif sort == "date":
|
|
return sorted(paths, key=lambda p: p.stat().st_mtime)
|
|
else:
|
|
return paths
|
|
|
|
|
|
def _expand_folder(entry: dict[str, Any], default_image_duration: float) -> list[ClipEntry]:
|
|
"""Ordner-Eintrag in einzelne Clips auflösen."""
|
|
folder = Path(entry["path"])
|
|
if not folder.exists():
|
|
raise FileNotFoundError(f"Ordner nicht gefunden: {folder}")
|
|
|
|
all_extensions = VIDEO_EXTENSIONS | IMAGE_EXTENSIONS
|
|
files = [f for f in folder.iterdir() if f.suffix.lower() in all_extensions]
|
|
files = _sort_paths(files, entry.get("sort", "alphabetical"))
|
|
|
|
return [
|
|
ClipEntry(
|
|
path=f,
|
|
media_type=_resolve_media_type(f),
|
|
remove_silence=entry.get("remove_silence", False),
|
|
trim_silence=entry.get("trim_silence", False),
|
|
image_duration=entry.get("image_duration", default_image_duration),
|
|
)
|
|
for f in files
|
|
]
|
|
|
|
|
|
def parse_sequence(
|
|
sequence_path: Path,
|
|
resources_folder: Path | None = None,
|
|
default_image_duration: float = 3.0,
|
|
) -> tuple[list[ClipEntry], dict[str, Any]]:
|
|
"""
|
|
sequence.yaml einlesen.
|
|
|
|
Gibt zurück:
|
|
clips — geordnete Liste von ClipEntry-Objekten
|
|
music_cfg — Musik-Konfiguration aus der Sequenz-Datei (oder leer)
|
|
"""
|
|
if not sequence_path.exists():
|
|
raise FileNotFoundError(f"Sequenz-Datei nicht gefunden: {sequence_path}")
|
|
|
|
with open(sequence_path, encoding="utf-8") as fh:
|
|
data = yaml.safe_load(fh) or {}
|
|
|
|
clips: list[ClipEntry] = []
|
|
|
|
for entry in data.get("sequence", []):
|
|
entry_type = entry.get("type")
|
|
|
|
if entry_type == "video":
|
|
raw_file = entry.get("file", "")
|
|
path = _resolve_path(raw_file, resources_folder, "videos")
|
|
clips.append(ClipEntry(
|
|
path=path,
|
|
media_type="video",
|
|
remove_silence=entry.get("remove_silence", False),
|
|
trim_silence=entry.get("trim_silence", False),
|
|
overlay_text=entry.get("overlay_text"),
|
|
overlay_position=entry.get("overlay_position", "bottom"),
|
|
overlay_duration=entry.get("overlay_duration"),
|
|
))
|
|
|
|
elif entry_type == "image":
|
|
raw_file = entry.get("file", "")
|
|
path = _resolve_path(raw_file, resources_folder, "images")
|
|
clips.append(ClipEntry(
|
|
path=path,
|
|
media_type="image",
|
|
image_duration=float(entry.get("duration", default_image_duration)),
|
|
))
|
|
|
|
elif entry_type == "text":
|
|
# Text-Clips werden als Platzhalter gespeichert;
|
|
# merger.py erzeugt den echten Clip später.
|
|
clips.append(ClipEntry(
|
|
path=Path("__text__"),
|
|
media_type="text",
|
|
image_duration=float(entry.get("duration", default_image_duration)),
|
|
overlay_text=entry.get("content", ""),
|
|
overlay_position=entry.get("style", {}).get("position", "center"),
|
|
))
|
|
# Stil-Infos direkt am Entry speichern
|
|
clips[-1].__dict__["text_style"] = entry.get("style", {})
|
|
|
|
elif entry_type == "folder":
|
|
clips.extend(_expand_folder(entry, default_image_duration))
|
|
|
|
else:
|
|
raise ValueError(f"Unbekannter Sequenz-Typ: {entry_type!r}")
|
|
|
|
music_cfg = data.get("music", {})
|
|
return clips, music_cfg
|
|
|
|
|
|
def _resolve_path(
|
|
raw: str,
|
|
resources_folder: Path | None,
|
|
subfolder: str,
|
|
) -> Path:
|
|
"""Datei-Pfad auflösen: absolut, relativ oder aus resources/."""
|
|
path = Path(raw)
|
|
if path.is_absolute() or path.exists():
|
|
return path
|
|
if resources_folder:
|
|
candidate = resources_folder / subfolder / raw
|
|
if candidate.exists():
|
|
return candidate
|
|
return path # Existenz-Prüfung erfolgt später
|