Video-Gen / app /core /timeline.py
jacky3102's picture
Upload 33 files
9459cd0 verified
Raw
History Blame Contribute Delete
2.67 kB
"""
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
@dataclass
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
@property
def duration(self) -> float:
return round(self.end - self.start, 3)
@dataclass
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"
@property
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