Spaces:
Sleeping
Sleeping
| """ | |
| Timeline Engine — the core abstraction the whole renderer is built around. | |
| Rule #1 from the PRD: never concatenate FFmpeg commands directly inside API | |
| handlers. Everything goes: raw request -> Timeline (this module) -> FilterGraph | |
| (compiler.py) -> ffmpeg. Adding a new animation/transition means touching | |
| compiler.py, never main.py or renderer.py. | |
| """ | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| class Clip: | |
| id: str | |
| kind: str # "video" | "image" | |
| local_path: str | |
| start: float # absolute start time on the timeline, seconds | |
| end: float # absolute end time on the timeline, seconds | |
| animation: str = "none" | |
| transition_in: str = "none" | |
| transition_duration: float = 0.5 | |
| def duration(self) -> float: | |
| return round(self.end - self.start, 3) | |
| class Timeline: | |
| clips: list[Clip] = field(default_factory=list) | |
| voice_path: Optional[str] = None | |
| bgm_path: Optional[str] = None | |
| bgm_volume: float = 0.15 | |
| subtitle_path: Optional[str] = None | |
| subtitle_style: str = "default" | |
| def total_duration(self) -> float: | |
| return max((c.end for c in self.clips), default=0.0) | |
| def add_clip(self, clip_id: str, kind: str, local_path: str, | |
| duration: float, animation: str = "none", | |
| transition_in: str = "none", transition_duration: float = 0.5): | |
| start = self.total_duration | |
| end = start + duration | |
| self.clips.append(Clip( | |
| id=clip_id, kind=kind, local_path=local_path, | |
| start=start, end=end, animation=animation, | |
| transition_in=transition_in, transition_duration=transition_duration, | |
| )) | |
| def build_timeline(resolved_clips: list[dict], voice_path=None, bgm_path=None, | |
| bgm_volume=0.15, subtitle_path=None, subtitle_style="default") -> Timeline: | |
| """ | |
| resolved_clips: list of dicts already downloaded to local disk, each with | |
| keys: id, type, local_path, duration (probed for video, given for image), | |
| animation, transition_in, transition_duration. | |
| """ | |
| tl = Timeline( | |
| voice_path=voice_path, bgm_path=bgm_path, bgm_volume=bgm_volume, | |
| subtitle_path=subtitle_path, subtitle_style=subtitle_style, | |
| ) | |
| for c in resolved_clips: | |
| tl.add_clip( | |
| clip_id=c["id"], kind=c["type"], local_path=c["local_path"], | |
| duration=c["duration"], animation=c.get("animation", "none"), | |
| transition_in=c.get("transition_in", "none"), | |
| transition_duration=c.get("transition_duration", 0.5), | |
| ) | |
| return tl | |