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