File size: 2,952 Bytes
345855e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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")