File size: 2,669 Bytes
9459cd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
67
68
69
70
71
72
73
"""
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