"""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