from __future__ import annotations class TransitionBuilder: """Build FFmpeg xfade filters for scene joins.""" TRANSITIONS = { "basic": "fade", "fade": "fade", "zoom": "zoomin", "slide": "slideleft", "slide_left": "slideleft", "slide_right": "slideright", "slide_up": "slideup", "slide_down": "slidedown", "push": "slideup", "blur": "fade", "whip": "smoothleft", "dissolve": "dissolve", "flash": "fadewhite", "glitch": "hlslice", "light_leak": "fadewhite", "film_burn": "fadegrays", "camera_shake": "smoothleft", "spin": "circleopen", "3d_flip": "vertopen", "flip": "vertopen", "cube": "rectcrop", "ripple": "radial", "ink": "distance", "morph": "dissolve", "stretch": "squeezeh", "liquid": "pixelize", "elastic": "smoothup", "motion_blur": "smoothleft", "wipe": "wipeleft", "wipe_left": "wipeleft", "wipe_right": "wiperight", "wipe_up": "wipeup", "wipe_down": "wipedown", "smooth_left": "smoothleft", "smooth_right": "smoothright", "fadeblack": "fadeblack", "fade_white": "fadewhite", "pixel": "pixelize", } def map_transition(self, name: str) -> str: return self.TRANSITIONS.get(name, "fade") def list_transitions(self) -> list[str]: return list(self.TRANSITIONS.keys()) def xfade_chain(self, stream_count: int, durations: list[float], transitions: list[str], transition_duration: float = 0.45) -> tuple[str, str]: if stream_count <= 1: return "", "[0:v]" filters: list[str] = [] current = "[0:v]" offset = max(0.1, durations[0] - transition_duration) for idx in range(1, stream_count): out = f"[vx{idx}]" transition = self.map_transition(transitions[idx - 1] if idx - 1 < len(transitions) else "fade") filters.append( f"{current}[{idx}:v]xfade=transition={transition}:duration={transition_duration}:offset={offset:.3f}{out}" ) current = out if idx < len(durations): offset += max(0.1, durations[idx] - transition_duration) return ";".join(filters), current