from __future__ import annotations from renderer.core.models import RenderRequest, Scene class Timeline: def __init__(self, scenes: list[Scene]) -> None: self.scenes = sorted(scenes, key=lambda scene: scene.start) self.validate() @classmethod def from_payload(cls, payload: dict) -> "Timeline": return cls([Scene(**scene) for scene in payload.get("scenes", [])]) @classmethod def request_from_payload(cls, payload: dict) -> RenderRequest: scenes = [Scene(**scene) for scene in payload.get("scenes", [])] return RenderRequest( scenes=scenes, template=payload.get("template", "tiktok_classic"), preset=payload.get("preset"), creative_style=payload.get("creative_style"), platform=payload.get("platform"), output_name=payload.get("output_name", "render.mp4"), voiceover=payload.get("voiceover"), background_music=payload.get("background_music"), music_volume=payload.get("music_volume", 0.316), music_fade_in=payload.get("music_fade_in", 0.0), music_fade_out=payload.get("music_fade_out", 0.0), music_loop=payload.get("music_loop", True), music_start=payload.get("music_start", 0.0), music_ducking=payload.get("music_ducking", True), voice_volume=payload.get("voice_volume", 1.0), subtitle_format=payload.get("subtitle_format", "ass"), auto_subtitles=payload.get("auto_subtitles", False), subtitle_language=payload.get("subtitle_language"), whisper_model_size=payload.get("whisper_model_size"), preview=payload.get("preview", False), audio_normalize=payload.get("audio_normalize", False), watermark=payload.get("watermark"), watermark_position=payload.get("watermark_position", "bottom-right"), intro=payload.get("intro"), outro=payload.get("outro"), callback_url=payload.get("callback_url"), export_target=payload.get("export_target"), priority=payload.get("priority", 0), scheduled_at=payload.get("scheduled_at"), normalize=payload.get("normalize", True), metadata=payload.get("metadata", {}), ) @property def total_duration(self) -> float: return max((scene.start + scene.duration for scene in self.scenes), default=0.0) def validate(self) -> None: if not self.scenes: raise ValueError("At least one scene is required") for scene in self.scenes: if scene.duration <= 0: raise ValueError("Scene duration must be greater than zero") if scene.start < 0: raise ValueError("Scene start must be non-negative") if not scene.media: raise ValueError("Scene media path is required")