From 8e79b159c9fee8e51ffe15a0f97745a1a95727f9 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:54:46 +0530 Subject: [PATCH 01/26] feat: add timelinev2 --- videodb/timeline_v2.py | 250 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 videodb/timeline_v2.py diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py new file mode 100644 index 0000000..2f4732c --- /dev/null +++ b/videodb/timeline_v2.py @@ -0,0 +1,250 @@ +from typing import List, Optional, Union +from enum import Enum + + +class AssetType(str, Enum): + video = "video" + image = "image" + + +class Fit(str, Enum): + crop = "crop" + cover = "cover" + contain = "contain" + none = "none" + + +class Position(str, Enum): + top = "top" + bottom = "bottom" + left = "left" + right = "right" + center = "center" + top_left = "top-left" + top_right = "top-right" + bottom_left = "bottom-left" + bottom_right = "bottom-right" + + +class Filter(str, Enum): + """A filter effect to apply to the Clip.""" + + blur = "blur" + boost = "boost" + contrast = "contrast" + darken = "darken" + greyscale = "greyscale" + lighten = "lighten" + muted = "muted" + negative = "negative" + + +class Offset: + def __init__(self, x: float = 0, y: float = 0): + self.x = x + self.y = y + + def to_json(self): + return { + "x": self.x, + "y": self.y, + } + + +class Crop: + def __init__(self, top: int = 0, right: int = 0, bottom: int = 0, left: int = 0): + self.top = top + self.right = right + self.bottom = bottom + self.left = left + + def to_json(self): + return { + "top": self.top, + "right": self.right, + "bottom": self.bottom, + "left": self.left, + } + + +class Transition: + def __init__(self, in_: str = None, out: str = None): + self.in_ = in_ + self.out = out + + def to_json(self): + return { + "in": self.in_, + "out": self.out, + } + + +class BaseAsset: + """The type of asset to display for the duration of the Clip.""" + + type: AssetType + + +class VideoAsset(BaseAsset): + """The VideoAsset is used to create video sequences from video files. The src must be a publicly accessible URL to a video resource""" + + type = AssetType.video + + def __init__( + self, + id: str, + trim: int = 0, + volume: float = 1, + crop: Optional[Crop] = None, + ): + if trim < 0: + raise ValueError("trim must be non-negative") + if not (0 <= volume <= 2): + raise ValueError("volume must be between 0 and 2") + + self.id = id + self.trim = trim + self.volume = volume + self.crop = crop if crop is not None else Crop() + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "volume": self.volume, + "crop": self.crop.to_json(), + } + + +class ImageAsset(BaseAsset): + """The ImageAsset is used to create video from images to compose an image. The src must be a publicly accessible URL to an image resource such as a jpg or png file.""" + + type = AssetType.image + + def __init__(self, id: str, trim: int = 0, crop: Optional[Crop] = None): + if trim < 0: + raise ValueError("trim must be non-negative") + + self.id = id + self.trim = trim + self.crop = crop if crop is not None else Crop() + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "crop": self.crop.to_json(), + } + + +AnyAsset = Union[VideoAsset, ImageAsset] + + +class Clip: + """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or html. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" + + def __init__( + self, + asset: AnyAsset, + start: Union[float, int], + length: Union[float, int], + transition: Optional[Transition] = None, + effect: Optional[str] = None, + filter: Optional[Filter] = None, + scale: float = 1, + opacity: float = 1, + fit: Optional[Fit] = Fit.crop, + position: Position = Position.center, + offset: Optional[Offset] = None, + ): + if start < 0: + raise ValueError("start must be non-negative") + if length <= 0: + raise ValueError("length must be positive") + if not (0 <= scale <= 10): + raise ValueError("scale must be between 0 and 10") + if not (0 <= opacity <= 1): + raise ValueError("opacity must be between 0 and 1") + + self.asset = asset + self.start = start + self.length = length + self.transition = transition + self.effect = effect + self.filter = filter + self.scale = scale + self.opacity = opacity + self.fit = fit + self.position = position + self.offset = offset if offset is not None else Offset() + + def to_json(self): + json = { + "asset": self.asset.to_json(), + "start": self.start, + "length": self.length, + "effect": self.effect, + "scale": self.scale, + "opacity": self.opacity, + "fit": self.fit, + "position": self.position, + "offset": self.offset.to_json(), + } + + if self.transition: + json["transition"] = self.transition.to_json() + if self.filter: + json["filter"] = self.filter.value + + return json + + +class Track: + clips: List[Clip] + + def __init__(self, clips: List[Clip] = []): + self.clips = clips + + def add_clip(self, clip: Clip): + self.clips.append(clip) + + def to_json(self): + return { + "clips": [clip.to_json() for clip in self.clips], + } + + +class TimelineV2: + def __init__(self, connection): + self.connection = connection + self.background: str = "#000000" + self.resolution: str = "1280x720" + self.tracks: List[Track] = [] + self.stream_url = None + self.player_url = None + + def add_track(self, track: Track): + self.tracks.append(track) + + def add_clip(self, track_index: int, clip: Clip): + self.tracks[track_index].clips.append(clip) + + def to_json(self): + return { + "timeline": { + "background": self.background, + "resolution": self.resolution, + "tracks": [track.to_json() for track in self.tracks], + } + } + + def generate_stream(self): + stream_data = self.connection.post( + path="timeline_v2", + data=self.to_json(), + ) + self.stream_url = stream_data.get("stream_url") + self.player_url = stream_data.get("player_url") + return stream_data.get("stream_url", None) From a8fdf4ee943f4d977851e130189730a729a6459e Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:28:13 +0530 Subject: [PATCH 02/26] fix: fit --- videodb/timeline_v2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 2f4732c..8e973d0 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -11,7 +11,6 @@ class Fit(str, Enum): crop = "crop" cover = "cover" contain = "contain" - none = "none" class Position(str, Enum): From 1e4a5f61259e3979853fbef4c605e03923a6e30f Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:24:41 +0530 Subject: [PATCH 03/26] build: update v --- videodb/__about__.py | 2 +- videodb/timeline_v2.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/videodb/__about__.py b/videodb/__about__.py index 3cc2806..fe85f3d 100644 --- a/videodb/__about__.py +++ b/videodb/__about__.py @@ -2,7 +2,7 @@ -__version__ = "0.2.15" +__version__ = "0.2.16" __title__ = "videodb" __author__ = "videodb" __email__ = "contact@videodb.io" diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 8e973d0..c6b82ba 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -201,8 +201,6 @@ def to_json(self): class Track: - clips: List[Clip] - def __init__(self, clips: List[Clip] = []): self.clips = clips From 2b51bfc22c2a13009ef4aa11bd9d301f972d4e98 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:12:58 +0530 Subject: [PATCH 04/26] fix: image asset --- videodb/timeline_v2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index c6b82ba..8c98e65 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -133,7 +133,6 @@ def to_json(self): return { "type": self.type, "id": self.id, - "trim": self.trim, "crop": self.crop.to_json(), } From 2940995c6b67985c538ea5123f0aefc7f07d0713 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:30:39 +0530 Subject: [PATCH 05/26] feat: add audio asset --- videodb/__about__.py | 2 +- videodb/timeline_v2.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/videodb/__about__.py b/videodb/__about__.py index fe85f3d..1a52acf 100644 --- a/videodb/__about__.py +++ b/videodb/__about__.py @@ -2,7 +2,7 @@ -__version__ = "0.2.16" +__version__ = "0.2.17" __title__ = "videodb" __author__ = "videodb" __email__ = "contact@videodb.io" diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 8c98e65..b323893 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -137,7 +137,26 @@ def to_json(self): } -AnyAsset = Union[VideoAsset, ImageAsset] +class AudioAsset(BaseAsset): + """The AudioAsset is used to create audio sequences from audio files. The src must be a publicly accessible URL to an audio resource""" + + type = AssetType.audio + + def __init__(self, id: str, trim: int = 0, volume: float = 1): + self.id = id + self.trim = trim + self.volume = volume + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "volume": self.volume, + } + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset] class Clip: From 2672b729ab597b09986e73459b02d864e022b8dc Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:35:43 +0530 Subject: [PATCH 06/26] fix: asset enum --- videodb/timeline_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index b323893..5482cad 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -5,6 +5,7 @@ class AssetType(str, Enum): video = "video" image = "image" + audio = "audio" class Fit(str, Enum): From 3537e396d00e4210229883275cc5cb642e561fec Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:07:45 +0530 Subject: [PATCH 07/26] feat: add text asset --- videodb/timeline_v2.py | 223 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 5482cad..f2e01c0 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -6,6 +6,7 @@ class AssetType(str, Enum): video = "video" image = "image" audio = "audio" + text = "text" class Fit(str, Enum): @@ -39,6 +40,36 @@ class Filter(str, Enum): negative = "negative" +class TextAlignment(str, Enum): + """Place the text in one of nine predefined positions of the background.""" + + top = "top" + top_right = "top_right" + right = "right" + bottom_right = "bottom_right" + bottom = "bottom" + bottom_left = "bottom_left" + left = "left" + top_left = "top_left" + center = "center" + + +class HorizontalAlignment(str, Enum): + """Horizontal text alignment options.""" + + left = "left" + center = "center" + right = "right" + + +class VerticalAlignment(str, Enum): + """Vertical text alignment options.""" + + top = "top" + center = "center" + bottom = "bottom" + + class Offset: def __init__(self, x: float = 0, y: float = 0): self.x = x @@ -157,7 +188,197 @@ def to_json(self): } -AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset] +class Font: + """Font styling properties for text assets.""" + + def __init__( + self, + family: str = "Clear Sans", + size: int = 48, + color: str = "#FFFFFF", + opacity: float = 1.0, + weight: Optional[int] = None, + ): + if size < 1: + raise ValueError("size must be at least 1") + if not (0.0 <= opacity <= 1.0): + raise ValueError("opacity must be between 0.0 and 1.0") + if weight is not None and not (100 <= weight <= 900): + raise ValueError("weight must be between 100 and 900") + + self.family = family + self.size = size + self.color = color + self.opacity = opacity + self.weight = weight + + def to_json(self): + data = { + "family": self.family, + "size": self.size, + "color": self.color, + "opacity": self.opacity, + } + if self.weight is not None: + data["weight"] = self.weight + return data + + +class Border: + """Text border properties.""" + + def __init__(self, color: str = "#000000", width: float = 0.0): + if width < 0.0: + raise ValueError("width must be non-negative") + self.color = color + self.width = width + + def to_json(self): + return { + "color": self.color, + "width": self.width, + } + + +class Shadow: + """Text shadow properties.""" + + def __init__(self, color: str = "#000000", x: float = 0.0, y: float = 0.0): + if x < 0.0: + raise ValueError("x must be non-negative") + if y < 0.0: + raise ValueError("y must be non-negative") + self.color = color + self.x = x + self.y = y + + def to_json(self): + return { + "color": self.color, + "x": self.x, + "y": self.y, + } + + +class Background: + """Text background styling properties.""" + + def __init__( + self, + width: float = 0.0, + height: float = 0.0, + color: str = "#000000", + border_width: float = 0.0, + opacity: float = 1.0, + text_alignment: TextAlignment = TextAlignment.center, + ): + if width < 0.0: + raise ValueError("width must be non-negative") + if height < 0.0: + raise ValueError("height must be non-negative") + if border_width < 0.0: + raise ValueError("border_width must be non-negative") + if not (0.0 <= opacity <= 1.0): + raise ValueError("opacity must be between 0.0 and 1.0") + + self.width = width + self.height = height + self.color = color + self.border_width = border_width + self.opacity = opacity + self.text_alignment = text_alignment + + def to_json(self): + return { + "width": self.width, + "height": self.height, + "color": self.color, + "border_width": self.border_width, + "opacity": self.opacity, + "text_alignment": self.text_alignment.value, + } + + +class Alignment: + """Text alignment properties.""" + + def __init__( + self, + horizontal: HorizontalAlignment = HorizontalAlignment.center, + vertical: VerticalAlignment = VerticalAlignment.center, + ): + self.horizontal = horizontal + self.vertical = vertical + + def to_json(self): + return { + "horizontal": self.horizontal.value, + "vertical": self.vertical.value, + } + + +class TextAsset(BaseAsset): + """The TextAsset is used to create text sequences from text strings with full control over the text styling and positioning.""" + + type = AssetType.text + + def __init__( + self, + text: str, + font: Optional[Font] = None, + border: Optional[Border] = None, + shadow: Optional[Shadow] = None, + background: Optional[Background] = None, + alignment: Optional[Alignment] = None, + tabsize: int = 4, + line_spacing: float = 0, + width: Optional[int] = None, + height: Optional[int] = None, + ): + if tabsize < 1: + raise ValueError("tabsize must be at least 1") + if line_spacing < 0.0: + raise ValueError("line_spacing must be non-negative") + if width is not None and width < 1: + raise ValueError("width must be at least 1") + if height is not None and height < 1: + raise ValueError("height must be at least 1") + + self.text = text + self.font = font if font is not None else Font() + self.border = border + self.shadow = shadow + self.background = background + self.alignment = alignment if alignment is not None else Alignment() + self.tabsize = tabsize + self.line_spacing = line_spacing + self.width = width + self.height = height + + def to_json(self): + data = { + "type": self.type, + "text": self.text, + "font": self.font.to_json(), + "alignment": self.alignment.to_json(), + "tabsize": self.tabsize, + "line_spacing": self.line_spacing, + } + if self.border: + data["border"] = self.border.to_json() + if self.shadow: + data["shadow"] = self.shadow.to_json() + if self.background: + data["background"] = self.background.to_json() + if self.width is not None: + data["width"] = self.width + if self.height is not None: + data["height"] = self.height + + return data + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset] class Clip: From ac78d552712e66e9e361dc01ac27378c3aed3551 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:53:45 +0530 Subject: [PATCH 08/26] fix: volume range --- videodb/timeline_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index f2e01c0..b74bc62 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -130,8 +130,8 @@ def __init__( ): if trim < 0: raise ValueError("trim must be non-negative") - if not (0 <= volume <= 2): - raise ValueError("volume must be between 0 and 2") + if not (0 <= volume <= 5): + raise ValueError("volume must be between 0 and 5") self.id = id self.trim = trim From fcf4796ed6cfd18ba96209bddeffcb7686f9edc2 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:09:52 +0530 Subject: [PATCH 09/26] feat: add caption asset --- videodb/timeline_v2.py | 168 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index b74bc62..27c2903 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -7,6 +7,7 @@ class AssetType(str, Enum): image = "image" audio = "audio" text = "text" + caption = "caption" class Fit(str, Enum): @@ -62,6 +63,40 @@ class HorizontalAlignment(str, Enum): right = "right" +class CaptionBorderStyle(int, Enum): + """Border style properties for caption assets.""" + + no_border = 1 + opaque_box = 3 + outline = 4 + + +class CaptionAlignment(int, Enum): + """Caption alignment properties for caption assets.""" + + bottom_left = 1 + bottom_center = 2 + bottom_right = 3 + middle_left = 9 + middle_center = 10 + middle_right = 11 + top_left = 5 + top_center = 6 + top_right = 7 + + +class CaptionAnimation(str, Enum): + """Caption animation properties for caption assets.""" + + float_in_bottom = "float_in_bottom" + box_highlight = "box_highlight" + color_highlight = "color_highlight" + reveal = "reveal" + karioke = "karioke" + impact = "impact" + supersize = "supersize" + + class VerticalAlignment(str, Enum): """Vertical text alignment options.""" @@ -378,7 +413,138 @@ def to_json(self): return data -AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset] +class FontStyling: + """Font styling properties for caption assets.""" + + def __init__( + self, + name: str = "Clear Sans", + size: int = 30, + bold: bool = False, + italic: bool = False, + underline: bool = False, + strikeout: bool = False, + scale_x: float = 1.0, + scale_y: float = 1.0, + spacing: float = 0.0, + angle: float = 0.0, + ): + self.name = name + self.size = size + self.bold = bold + self.italic = italic + self.underline = underline + self.strikeout = strikeout + self.scale_x = scale_x + self.scale_y = scale_y + self.spacing = spacing + self.angle = angle + + def to_json(self): + return { + "font_name": self.name, + "font_size": self.size, + "bold": self.bold, + "italic": self.italic, + "underline": self.underline, + "strikeout": self.strikeout, + "scale_x": self.scale_x, + "scale_y": self.scale_y, + "spacing": self.spacing, + "angle": self.angle, + } + + +class BorderAndShadow: + """Border and shadow properties for caption assets.""" + + def __init__( + self, + style: CaptionBorderStyle = CaptionBorderStyle.no_border, + outline: int = 1, + outline_color: str = "&H00000000", + shadow: int = 0, + ): + self.style = style + self.outline = outline + self.outline_color = outline_color + self.shadow = shadow + + def to_json(self): + return { + "style": self.style.value, + "outline": self.outline, + "outline_color": self.outline_color, + "shadow": self.shadow, + } + + +class Positioning: + """Positioning properties for caption assets.""" + + def __init__( + self, + alignment: CaptionAlignment = CaptionAlignment.bottom_center, + margin_l: int = 30, + margin_r: int = 30, + margin_v: int = 30, + ): + self.alignment = alignment + self.margin_l = margin_l + self.margin_r = margin_r + self.margin_v = margin_v + + def to_json(self): + return { + "alignment": self.alignment.value, + "margin_l": self.margin_l, + "margin_r": self.margin_r, + "margin_v": self.margin_v, + } + + +class CaptionAsset(BaseAsset): + """The CaptionAsset is used to create captions from text strings with full styling and ass support.""" + + type = AssetType.caption + + def __init__( + self, + src: str = "auto", + font: Optional[FontStyling] = None, + primary_color: str = "&H00FFFFFF", + secondary_color: str = "&H000000FF", + back_color: str = "&H00000000", + border: Optional[BorderAndShadow] = None, + position: Optional[Positioning] = None, + animation: Optional[CaptionAnimation] = None, + ): + self.src = src + self.font = font if font is not None else FontStyling() + self.primary_color = primary_color + self.secondary_color = secondary_color + self.back_color = back_color + self.border = border if border is not None else BorderAndShadow() + self.position = position if position is not None else Positioning() + self.animation = animation + + def to_json(self): + data = { + "type": self.type, + "src": self.src, + "font": self.font.to_json(), + "primary_color": self.primary_color, + "secondary_color": self.secondary_color, + "back_color": self.back_color, + "border": self.border.to_json(), + "position": self.position.to_json(), + } + if self.animation: + data["animation"] = self.animation.value + return data + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset, CaptionAsset] class Clip: From 9d5a6b0853485ca14270299cbf5cee9284747746 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:41:09 +0530 Subject: [PATCH 10/26] fix: caption animation --- videodb/timeline_v2.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 27c2903..925d755 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -63,32 +63,32 @@ class HorizontalAlignment(str, Enum): right = "right" -class CaptionBorderStyle(int, Enum): +class CaptionBorderStyle(str, Enum): """Border style properties for caption assets.""" - no_border = 1 - opaque_box = 3 - outline = 4 + no_border = "no_border" + opaque_box = "opaque_box" + outline = "outline" -class CaptionAlignment(int, Enum): +class CaptionAlignment(str, Enum): """Caption alignment properties for caption assets.""" - bottom_left = 1 - bottom_center = 2 - bottom_right = 3 - middle_left = 9 - middle_center = 10 - middle_right = 11 - top_left = 5 - top_center = 6 - top_right = 7 + bottom_left = "bottom_left" + bottom_center = "bottom_center" + bottom_right = "bottom_right" + middle_left = "middle_left" + middle_center = "middle_center" + middle_right = "middle_right" + top_left = "top_left" + top_center = "top_center" + top_right = "top_right" class CaptionAnimation(str, Enum): """Caption animation properties for caption assets.""" - float_in_bottom = "float_in_bottom" + # float_in_bottom = "float_in_bottom" box_highlight = "box_highlight" color_highlight = "color_highlight" reveal = "reveal" @@ -424,8 +424,8 @@ def __init__( italic: bool = False, underline: bool = False, strikeout: bool = False, - scale_x: float = 1.0, - scale_y: float = 1.0, + scale_x: float = 100, + scale_y: float = 100, spacing: float = 0.0, angle: float = 0.0, ): From 92dc137f82cb1969b1c76fa31bf6142ab4291dcf Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:47:11 +0530 Subject: [PATCH 11/26] fix: border style --- videodb/timeline_v2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 925d755..efd560d 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -66,8 +66,7 @@ class HorizontalAlignment(str, Enum): class CaptionBorderStyle(str, Enum): """Border style properties for caption assets.""" - no_border = "no_border" - opaque_box = "opaque_box" + outline_and_shadow = "outline_and_shadow" outline = "outline" @@ -460,7 +459,7 @@ class BorderAndShadow: def __init__( self, - style: CaptionBorderStyle = CaptionBorderStyle.no_border, + style: CaptionBorderStyle = CaptionBorderStyle.outline_and_shadow, outline: int = 1, outline_color: str = "&H00000000", shadow: int = 0, From 339e3a0b9c251b1fb081bcdce703b7826cc3f12b Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:43:23 +0530 Subject: [PATCH 12/26] feat: add timeline download --- videodb/timeline_v2.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index efd560d..01a50f9 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -650,3 +650,8 @@ def generate_stream(self): self.stream_url = stream_data.get("stream_url") self.player_url = stream_data.get("player_url") return stream_data.get("stream_url", None) + + def download_stream(self, stream_url: str): + return self.connection.post( + path="timeline_v2/download", data={"stream_url": stream_url} + ) From 5027f9a0df0263fecf6af6c937cc54543bcb7704 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:15:57 +0530 Subject: [PATCH 13/26] fix: position enum --- videodb/timeline_v2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 01a50f9..0a98abf 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -22,10 +22,10 @@ class Position(str, Enum): left = "left" right = "right" center = "center" - top_left = "top-left" - top_right = "top-right" - bottom_left = "bottom-left" - bottom_right = "bottom-right" + top_left = "top_left" + top_right = "top_right" + bottom_left = "bottom_left" + bottom_right = "bottom_right" class Filter(str, Enum): From b28968ab8169821ee6125315f46dd0d63ffedf1c Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 8 Sep 2025 22:29:14 +0530 Subject: [PATCH 14/26] fix: border style enum --- videodb/timeline_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 0a98abf..5427a3b 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -67,7 +67,7 @@ class CaptionBorderStyle(str, Enum): """Border style properties for caption assets.""" outline_and_shadow = "outline_and_shadow" - outline = "outline" + opaque_box = "opaque_box" class CaptionAlignment(str, Enum): From b15ab4f6363d75319b97a14d4c9e95a6b00e4fbe Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:30:51 +0530 Subject: [PATCH 15/26] docs: add docstrings --- videodb/timeline_v2.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py index 5427a3b..f832554 100644 --- a/videodb/timeline_v2.py +++ b/videodb/timeline_v2.py @@ -3,6 +3,8 @@ class AssetType(str, Enum): + """The type of asset to display for the duration of the Clip.""" + video = "video" image = "image" audio = "audio" @@ -11,12 +13,16 @@ class AssetType(str, Enum): class Fit(str, Enum): + """The fit mode to apply to the asset.""" + crop = "crop" cover = "cover" contain = "contain" class Position(str, Enum): + """The position of the asset on the timeline.""" + top = "top" bottom = "bottom" left = "left" @@ -547,7 +553,7 @@ def to_json(self): class Clip: - """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or html. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" + """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or caption. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" def __init__( self, From 33a271fc2340c3dcd375a37f6b7cfcfeab264aba Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:05:34 +0530 Subject: [PATCH 16/26] feat: increase api gateway timeout --- videodb/_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/videodb/_constants.py b/videodb/_constants.py index b98ddab..10b3f10 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -90,7 +90,7 @@ class Status: class HttpClientDefaultValues: max_retries = 1 - timeout = 30 + timeout = 59 backoff_factor = 0.1 status_forcelist = [502, 503, 504] From c379a8663b972758880e31bf2d541d6d7a9f7279 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:53:30 +0530 Subject: [PATCH 17/26] feat: add editor --- videodb/editor.py | 663 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 663 insertions(+) create mode 100644 videodb/editor.py diff --git a/videodb/editor.py b/videodb/editor.py new file mode 100644 index 0000000..764e018 --- /dev/null +++ b/videodb/editor.py @@ -0,0 +1,663 @@ +from typing import List, Optional, Union +from enum import Enum + + +class AssetType(str, Enum): + """The type of asset to display for the duration of the Clip.""" + + video = "video" + image = "image" + audio = "audio" + text = "text" + caption = "caption" + + +class Fit(str, Enum): + """The fit mode to apply to the asset.""" + + crop = "crop" + cover = "cover" + contain = "contain" + + +class Position(str, Enum): + """The position of the asset on the timeline.""" + + top = "top" + bottom = "bottom" + left = "left" + right = "right" + center = "center" + top_left = "top_left" + top_right = "top_right" + bottom_left = "bottom_left" + bottom_right = "bottom_right" + + +class Filter(str, Enum): + """A filter effect to apply to the Clip.""" + + blur = "blur" + boost = "boost" + contrast = "contrast" + darken = "darken" + greyscale = "greyscale" + lighten = "lighten" + muted = "muted" + negative = "negative" + + +class TextAlignment(str, Enum): + """Place the text in one of nine predefined positions of the background.""" + + top = "top" + top_right = "top_right" + right = "right" + bottom_right = "bottom_right" + bottom = "bottom" + bottom_left = "bottom_left" + left = "left" + top_left = "top_left" + center = "center" + + +class HorizontalAlignment(str, Enum): + """Horizontal text alignment options.""" + + left = "left" + center = "center" + right = "right" + + +class CaptionBorderStyle(str, Enum): + """Border style properties for caption assets.""" + + outline_and_shadow = "outline_and_shadow" + opaque_box = "opaque_box" + + +class CaptionAlignment(str, Enum): + """Caption alignment properties for caption assets.""" + + bottom_left = "bottom_left" + bottom_center = "bottom_center" + bottom_right = "bottom_right" + middle_left = "middle_left" + middle_center = "middle_center" + middle_right = "middle_right" + top_left = "top_left" + top_center = "top_center" + top_right = "top_right" + + +class CaptionAnimation(str, Enum): + """Caption animation properties for caption assets.""" + + # float_in_bottom = "float_in_bottom" + box_highlight = "box_highlight" + color_highlight = "color_highlight" + reveal = "reveal" + karioke = "karioke" + impact = "impact" + supersize = "supersize" + + +class VerticalAlignment(str, Enum): + """Vertical text alignment options.""" + + top = "top" + center = "center" + bottom = "bottom" + + +class Offset: + def __init__(self, x: float = 0, y: float = 0): + self.x = x + self.y = y + + def to_json(self): + return { + "x": self.x, + "y": self.y, + } + + +class Crop: + def __init__(self, top: int = 0, right: int = 0, bottom: int = 0, left: int = 0): + self.top = top + self.right = right + self.bottom = bottom + self.left = left + + def to_json(self): + return { + "top": self.top, + "right": self.right, + "bottom": self.bottom, + "left": self.left, + } + + +class Transition: + def __init__(self, in_: str = None, out: str = None): + self.in_ = in_ + self.out = out + + def to_json(self): + return { + "in": self.in_, + "out": self.out, + } + + +class BaseAsset: + """The type of asset to display for the duration of the Clip.""" + + type: AssetType + + +class VideoAsset(BaseAsset): + """The VideoAsset is used to create video sequences from video files. The src must be a publicly accessible URL to a video resource""" + + type = AssetType.video + + def __init__( + self, + id: str, + trim: int = 0, + volume: float = 1, + crop: Optional[Crop] = None, + ): + if trim < 0: + raise ValueError("trim must be non-negative") + if not (0 <= volume <= 5): + raise ValueError("volume must be between 0 and 5") + + self.id = id + self.trim = trim + self.volume = volume + self.crop = crop if crop is not None else Crop() + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "volume": self.volume, + "crop": self.crop.to_json(), + } + + +class ImageAsset(BaseAsset): + """The ImageAsset is used to create video from images to compose an image. The src must be a publicly accessible URL to an image resource such as a jpg or png file.""" + + type = AssetType.image + + def __init__(self, id: str, trim: int = 0, crop: Optional[Crop] = None): + if trim < 0: + raise ValueError("trim must be non-negative") + + self.id = id + self.trim = trim + self.crop = crop if crop is not None else Crop() + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "crop": self.crop.to_json(), + } + + +class AudioAsset(BaseAsset): + """The AudioAsset is used to create audio sequences from audio files. The src must be a publicly accessible URL to an audio resource""" + + type = AssetType.audio + + def __init__(self, id: str, trim: int = 0, volume: float = 1): + self.id = id + self.trim = trim + self.volume = volume + + def to_json(self): + return { + "type": self.type, + "id": self.id, + "trim": self.trim, + "volume": self.volume, + } + + +class Font: + """Font styling properties for text assets.""" + + def __init__( + self, + family: str = "Clear Sans", + size: int = 48, + color: str = "#FFFFFF", + opacity: float = 1.0, + weight: Optional[int] = None, + ): + if size < 1: + raise ValueError("size must be at least 1") + if not (0.0 <= opacity <= 1.0): + raise ValueError("opacity must be between 0.0 and 1.0") + if weight is not None and not (100 <= weight <= 900): + raise ValueError("weight must be between 100 and 900") + + self.family = family + self.size = size + self.color = color + self.opacity = opacity + self.weight = weight + + def to_json(self): + data = { + "family": self.family, + "size": self.size, + "color": self.color, + "opacity": self.opacity, + } + if self.weight is not None: + data["weight"] = self.weight + return data + + +class Border: + """Text border properties.""" + + def __init__(self, color: str = "#000000", width: float = 0.0): + if width < 0.0: + raise ValueError("width must be non-negative") + self.color = color + self.width = width + + def to_json(self): + return { + "color": self.color, + "width": self.width, + } + + +class Shadow: + """Text shadow properties.""" + + def __init__(self, color: str = "#000000", x: float = 0.0, y: float = 0.0): + if x < 0.0: + raise ValueError("x must be non-negative") + if y < 0.0: + raise ValueError("y must be non-negative") + self.color = color + self.x = x + self.y = y + + def to_json(self): + return { + "color": self.color, + "x": self.x, + "y": self.y, + } + + +class Background: + """Text background styling properties.""" + + def __init__( + self, + width: float = 0.0, + height: float = 0.0, + color: str = "#000000", + border_width: float = 0.0, + opacity: float = 1.0, + text_alignment: TextAlignment = TextAlignment.center, + ): + if width < 0.0: + raise ValueError("width must be non-negative") + if height < 0.0: + raise ValueError("height must be non-negative") + if border_width < 0.0: + raise ValueError("border_width must be non-negative") + if not (0.0 <= opacity <= 1.0): + raise ValueError("opacity must be between 0.0 and 1.0") + + self.width = width + self.height = height + self.color = color + self.border_width = border_width + self.opacity = opacity + self.text_alignment = text_alignment + + def to_json(self): + return { + "width": self.width, + "height": self.height, + "color": self.color, + "border_width": self.border_width, + "opacity": self.opacity, + "text_alignment": self.text_alignment.value, + } + + +class Alignment: + """Text alignment properties.""" + + def __init__( + self, + horizontal: HorizontalAlignment = HorizontalAlignment.center, + vertical: VerticalAlignment = VerticalAlignment.center, + ): + self.horizontal = horizontal + self.vertical = vertical + + def to_json(self): + return { + "horizontal": self.horizontal.value, + "vertical": self.vertical.value, + } + + +class TextAsset(BaseAsset): + """The TextAsset is used to create text sequences from text strings with full control over the text styling and positioning.""" + + type = AssetType.text + + def __init__( + self, + text: str, + font: Optional[Font] = None, + border: Optional[Border] = None, + shadow: Optional[Shadow] = None, + background: Optional[Background] = None, + alignment: Optional[Alignment] = None, + tabsize: int = 4, + line_spacing: float = 0, + width: Optional[int] = None, + height: Optional[int] = None, + ): + if tabsize < 1: + raise ValueError("tabsize must be at least 1") + if line_spacing < 0.0: + raise ValueError("line_spacing must be non-negative") + if width is not None and width < 1: + raise ValueError("width must be at least 1") + if height is not None and height < 1: + raise ValueError("height must be at least 1") + + self.text = text + self.font = font if font is not None else Font() + self.border = border + self.shadow = shadow + self.background = background + self.alignment = alignment if alignment is not None else Alignment() + self.tabsize = tabsize + self.line_spacing = line_spacing + self.width = width + self.height = height + + def to_json(self): + data = { + "type": self.type, + "text": self.text, + "font": self.font.to_json(), + "alignment": self.alignment.to_json(), + "tabsize": self.tabsize, + "line_spacing": self.line_spacing, + } + if self.border: + data["border"] = self.border.to_json() + if self.shadow: + data["shadow"] = self.shadow.to_json() + if self.background: + data["background"] = self.background.to_json() + if self.width is not None: + data["width"] = self.width + if self.height is not None: + data["height"] = self.height + + return data + + +class FontStyling: + """Font styling properties for caption assets.""" + + def __init__( + self, + name: str = "Clear Sans", + size: int = 30, + bold: bool = False, + italic: bool = False, + underline: bool = False, + strikeout: bool = False, + scale_x: float = 100, + scale_y: float = 100, + spacing: float = 0.0, + angle: float = 0.0, + ): + self.name = name + self.size = size + self.bold = bold + self.italic = italic + self.underline = underline + self.strikeout = strikeout + self.scale_x = scale_x + self.scale_y = scale_y + self.spacing = spacing + self.angle = angle + + def to_json(self): + return { + "font_name": self.name, + "font_size": self.size, + "bold": self.bold, + "italic": self.italic, + "underline": self.underline, + "strikeout": self.strikeout, + "scale_x": self.scale_x, + "scale_y": self.scale_y, + "spacing": self.spacing, + "angle": self.angle, + } + + +class BorderAndShadow: + """Border and shadow properties for caption assets.""" + + def __init__( + self, + style: CaptionBorderStyle = CaptionBorderStyle.outline_and_shadow, + outline: int = 1, + outline_color: str = "&H00000000", + shadow: int = 0, + ): + self.style = style + self.outline = outline + self.outline_color = outline_color + self.shadow = shadow + + def to_json(self): + return { + "style": self.style.value, + "outline": self.outline, + "outline_color": self.outline_color, + "shadow": self.shadow, + } + + +class Positioning: + """Positioning properties for caption assets.""" + + def __init__( + self, + alignment: CaptionAlignment = CaptionAlignment.bottom_center, + margin_l: int = 30, + margin_r: int = 30, + margin_v: int = 30, + ): + self.alignment = alignment + self.margin_l = margin_l + self.margin_r = margin_r + self.margin_v = margin_v + + def to_json(self): + return { + "alignment": self.alignment.value, + "margin_l": self.margin_l, + "margin_r": self.margin_r, + "margin_v": self.margin_v, + } + + +class CaptionAsset(BaseAsset): + """The CaptionAsset is used to create captions from text strings with full styling and ass support.""" + + type = AssetType.caption + + def __init__( + self, + src: str = "auto", + font: Optional[FontStyling] = None, + primary_color: str = "&H00FFFFFF", + secondary_color: str = "&H000000FF", + back_color: str = "&H00000000", + border: Optional[BorderAndShadow] = None, + position: Optional[Positioning] = None, + animation: Optional[CaptionAnimation] = None, + ): + self.src = src + self.font = font if font is not None else FontStyling() + self.primary_color = primary_color + self.secondary_color = secondary_color + self.back_color = back_color + self.border = border if border is not None else BorderAndShadow() + self.position = position if position is not None else Positioning() + self.animation = animation + + def to_json(self): + data = { + "type": self.type, + "src": self.src, + "font": self.font.to_json(), + "primary_color": self.primary_color, + "secondary_color": self.secondary_color, + "back_color": self.back_color, + "border": self.border.to_json(), + "position": self.position.to_json(), + } + if self.animation: + data["animation"] = self.animation.value + return data + + +AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset, CaptionAsset] + + +class Clip: + """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or caption. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" + + def __init__( + self, + asset: AnyAsset, + start: Union[float, int], + length: Union[float, int], + transition: Optional[Transition] = None, + effect: Optional[str] = None, + filter: Optional[Filter] = None, + scale: float = 1, + opacity: float = 1, + fit: Optional[Fit] = Fit.crop, + position: Position = Position.center, + offset: Optional[Offset] = None, + ): + if start < 0: + raise ValueError("start must be non-negative") + if length <= 0: + raise ValueError("length must be positive") + if not (0 <= scale <= 10): + raise ValueError("scale must be between 0 and 10") + if not (0 <= opacity <= 1): + raise ValueError("opacity must be between 0 and 1") + + self.asset = asset + self.start = start + self.length = length + self.transition = transition + self.effect = effect + self.filter = filter + self.scale = scale + self.opacity = opacity + self.fit = fit + self.position = position + self.offset = offset if offset is not None else Offset() + + def to_json(self): + json = { + "asset": self.asset.to_json(), + "start": self.start, + "length": self.length, + "effect": self.effect, + "scale": self.scale, + "opacity": self.opacity, + "fit": self.fit, + "position": self.position, + "offset": self.offset.to_json(), + } + + if self.transition: + json["transition"] = self.transition.to_json() + if self.filter: + json["filter"] = self.filter.value + + return json + + +class Track: + def __init__(self, clips: List[Clip] = []): + self.clips = clips + + def add_clip(self, clip: Clip): + self.clips.append(clip) + + def to_json(self): + return { + "clips": [clip.to_json() for clip in self.clips], + } + + +class TimelineV2: + def __init__(self, connection): + self.connection = connection + self.background: str = "#000000" + self.resolution: str = "1280x720" + self.tracks: List[Track] = [] + self.stream_url = None + self.player_url = None + + def add_track(self, track: Track): + self.tracks.append(track) + + def add_clip(self, track_index: int, clip: Clip): + self.tracks[track_index].clips.append(clip) + + def to_json(self): + return { + "timeline": { + "background": self.background, + "resolution": self.resolution, + "tracks": [track.to_json() for track in self.tracks], + } + } + + def generate_stream(self): + stream_data = self.connection.post( + path="editor", + data=self.to_json(), + ) + self.stream_url = stream_data.get("stream_url") + self.player_url = stream_data.get("player_url") + return stream_data.get("stream_url", None) + + def download_stream(self, stream_url: str): + return self.connection.post( + path="timeline_v2/download", data={"stream_url": stream_url} + ) From 2c3d1527c5eab03b188fd42b193c65f398583736 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:18:46 +0530 Subject: [PATCH 18/26] fix: asset start --- videodb/_constants.py | 1 + videodb/editor.py | 80 ++++++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/videodb/_constants.py b/videodb/_constants.py index 10b3f10..a8d2830 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -81,6 +81,7 @@ class ApiPath: translate = "translate" dub = "dub" transcode = "transcode" + editor = "editor" class Status: diff --git a/videodb/editor.py b/videodb/editor.py index 764e018..cd16537 100644 --- a/videodb/editor.py +++ b/videodb/editor.py @@ -1,6 +1,8 @@ from typing import List, Optional, Union from enum import Enum +from videodb._constants import ApiPath + class AssetType(str, Enum): """The type of asset to display for the duration of the Clip.""" @@ -93,11 +95,10 @@ class CaptionAlignment(str, Enum): class CaptionAnimation(str, Enum): """Caption animation properties for caption assets.""" - # float_in_bottom = "float_in_bottom" box_highlight = "box_highlight" color_highlight = "color_highlight" reveal = "reveal" - karioke = "karioke" + karaoke = "karaoke" impact = "impact" supersize = "supersize" @@ -139,14 +140,16 @@ def to_json(self): class Transition: - def __init__(self, in_: str = None, out: str = None): + def __init__(self, in_: str = None, out: str = None, duration: int = 0.5): self.in_ = in_ self.out = out + self.duration = duration def to_json(self): return { "in": self.in_, "out": self.out, + "duration": self.duration, } @@ -164,17 +167,17 @@ class VideoAsset(BaseAsset): def __init__( self, id: str, - trim: int = 0, + start: int = 0, volume: float = 1, crop: Optional[Crop] = None, ): - if trim < 0: - raise ValueError("trim must be non-negative") + if start < 0: + raise ValueError("start must be non-negative") if not (0 <= volume <= 5): raise ValueError("volume must be between 0 and 5") self.id = id - self.trim = trim + self.start = start self.volume = volume self.crop = crop if crop is not None else Crop() @@ -182,7 +185,7 @@ def to_json(self): return { "type": self.type, "id": self.id, - "trim": self.trim, + "start": self.start, "volume": self.volume, "crop": self.crop.to_json(), } @@ -193,12 +196,8 @@ class ImageAsset(BaseAsset): type = AssetType.image - def __init__(self, id: str, trim: int = 0, crop: Optional[Crop] = None): - if trim < 0: - raise ValueError("trim must be non-negative") - + def __init__(self, id: str, crop: Optional[Crop] = None): self.id = id - self.trim = trim self.crop = crop if crop is not None else Crop() def to_json(self): @@ -214,16 +213,16 @@ class AudioAsset(BaseAsset): type = AssetType.audio - def __init__(self, id: str, trim: int = 0, volume: float = 1): + def __init__(self, id: str, start: int = 0, volume: float = 1): self.id = id - self.trim = trim + self.start = start self.volume = volume def to_json(self): return { "type": self.type, "id": self.id, - "trim": self.trim, + "start": self.start, "volume": self.volume, } @@ -558,8 +557,7 @@ class Clip: def __init__( self, asset: AnyAsset, - start: Union[float, int], - length: Union[float, int], + duration: Union[float, int], transition: Optional[Transition] = None, effect: Optional[str] = None, filter: Optional[Filter] = None, @@ -568,19 +566,15 @@ def __init__( fit: Optional[Fit] = Fit.crop, position: Position = Position.center, offset: Optional[Offset] = None, + z_index: int = 0, ): - if start < 0: - raise ValueError("start must be non-negative") - if length <= 0: - raise ValueError("length must be positive") if not (0 <= scale <= 10): raise ValueError("scale must be between 0 and 10") if not (0 <= opacity <= 1): raise ValueError("opacity must be between 0 and 1") self.asset = asset - self.start = start - self.length = length + self.duration = duration self.transition = transition self.effect = effect self.filter = filter @@ -589,18 +583,19 @@ def __init__( self.fit = fit self.position = position self.offset = offset if offset is not None else Offset() + self.z_index = z_index def to_json(self): json = { "asset": self.asset.to_json(), - "start": self.start, - "length": self.length, + "duration": self.duration, "effect": self.effect, "scale": self.scale, "opacity": self.opacity, "fit": self.fit, "position": self.position, "offset": self.offset.to_json(), + "z_index": self.z_index, } if self.transition: @@ -611,16 +606,30 @@ def to_json(self): return json +class TrackItem: + def __init__(self, start: int, clip: Clip): + self.start = start + self.clip = clip + + def to_json(self): + return { + "start": self.start, + "clip": self.clip.to_json(), + } + + class Track: - def __init__(self, clips: List[Clip] = []): - self.clips = clips + def __init__(self, z_index: int = 0): + self.clips: List[TrackItem] = [] + self.z_index: int = z_index - def add_clip(self, clip: Clip): - self.clips.append(clip) + def add_clip(self, start: int, clip: Clip): + self.clips.append(TrackItem(start, clip)) def to_json(self): return { "clips": [clip.to_json() for clip in self.clips], + "z_index": self.z_index, } @@ -636,9 +645,6 @@ def __init__(self, connection): def add_track(self, track: Track): self.tracks.append(track) - def add_clip(self, track_index: int, clip: Clip): - self.tracks[track_index].clips.append(clip) - def to_json(self): return { "timeline": { @@ -649,8 +655,10 @@ def to_json(self): } def generate_stream(self): + """Generate a stream from the timeline.""" + stream_data = self.connection.post( - path="editor", + path=ApiPath.editor, data=self.to_json(), ) self.stream_url = stream_data.get("stream_url") @@ -658,6 +666,8 @@ def generate_stream(self): return stream_data.get("stream_url", None) def download_stream(self, stream_url: str): + """Download a stream from the timeline.""" + return self.connection.post( - path="timeline_v2/download", data={"stream_url": stream_url} + path=f"{ApiPath.editor}/{ApiPath.download}", data={"stream_url": stream_url} ) From 092100dcf5630c80d59fd8faec562c10e6003dbe Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:56:28 +0530 Subject: [PATCH 19/26] docs: add docstrings --- videodb/_constants.py | 2 +- videodb/editor.py | 21 +- videodb/timeline_v2.py | 663 ----------------------------------------- 3 files changed, 18 insertions(+), 668 deletions(-) delete mode 100644 videodb/timeline_v2.py diff --git a/videodb/_constants.py b/videodb/_constants.py index a8d2830..e3e240f 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -91,7 +91,7 @@ class Status: class HttpClientDefaultValues: max_retries = 1 - timeout = 59 + timeout = 30 backoff_factor = 0.1 status_forcelist = [502, 503, 504] diff --git a/videodb/editor.py b/videodb/editor.py index cd16537..5293c6a 100644 --- a/videodb/editor.py +++ b/videodb/editor.py @@ -15,7 +15,12 @@ class AssetType(str, Enum): class Fit(str, Enum): - """The fit mode to apply to the asset.""" + """Set how the asset should be scaled to fit the viewport using one of the following options: + crop (default) - scale the asset to fill the viewport while maintaining the aspect ratio. The asset will be cropped if it exceeds the bounds of the viewport. + + cover - stretch the asset to fill the viewport without maintaining the aspect ratio. + contain - fit the entire asset within the viewport while maintaining the original aspect ratio. + none - preserves the original asset dimensions and does not apply any scaling.""" crop = "crop" cover = "cover" @@ -23,7 +28,7 @@ class Fit(str, Enum): class Position(str, Enum): - """The position of the asset on the timeline.""" + """Place the asset in one of nine predefined positions of the viewport. This is most effective for when the asset is scaled and you want to position the element to a specific position.""" top = "top" bottom = "bottom" @@ -607,6 +612,8 @@ def to_json(self): class TrackItem: + """Clip wrapper""" + def __init__(self, start: int, clip: Clip): self.start = start self.clip = clip @@ -619,11 +626,15 @@ def to_json(self): class Track: + """A track contains an array of clips. Tracks are layered on top of each other in the order in the array. The top most track will render on top of those below it.""" + def __init__(self, z_index: int = 0): self.clips: List[TrackItem] = [] self.z_index: int = z_index def add_clip(self, start: int, clip: Clip): + """Add a clip to the track.""" + self.clips.append(TrackItem(start, clip)) def to_json(self): @@ -633,7 +644,9 @@ def to_json(self): } -class TimelineV2: +class Timeline: + """A timeline represents the contents of a video edit over time, an audio edit over time, in seconds, or an image layout. A timeline consists of layers called tracks. Tracks are composed of titles, images, audio, html or video segments referred to as clips which are placed along the track at specific starting point and lasting for a specific amount of time.""" + def __init__(self, connection): self.connection = connection self.background: str = "#000000" @@ -667,7 +680,7 @@ def generate_stream(self): def download_stream(self, stream_url: str): """Download a stream from the timeline.""" - + return self.connection.post( path=f"{ApiPath.editor}/{ApiPath.download}", data={"stream_url": stream_url} ) diff --git a/videodb/timeline_v2.py b/videodb/timeline_v2.py deleted file mode 100644 index f832554..0000000 --- a/videodb/timeline_v2.py +++ /dev/null @@ -1,663 +0,0 @@ -from typing import List, Optional, Union -from enum import Enum - - -class AssetType(str, Enum): - """The type of asset to display for the duration of the Clip.""" - - video = "video" - image = "image" - audio = "audio" - text = "text" - caption = "caption" - - -class Fit(str, Enum): - """The fit mode to apply to the asset.""" - - crop = "crop" - cover = "cover" - contain = "contain" - - -class Position(str, Enum): - """The position of the asset on the timeline.""" - - top = "top" - bottom = "bottom" - left = "left" - right = "right" - center = "center" - top_left = "top_left" - top_right = "top_right" - bottom_left = "bottom_left" - bottom_right = "bottom_right" - - -class Filter(str, Enum): - """A filter effect to apply to the Clip.""" - - blur = "blur" - boost = "boost" - contrast = "contrast" - darken = "darken" - greyscale = "greyscale" - lighten = "lighten" - muted = "muted" - negative = "negative" - - -class TextAlignment(str, Enum): - """Place the text in one of nine predefined positions of the background.""" - - top = "top" - top_right = "top_right" - right = "right" - bottom_right = "bottom_right" - bottom = "bottom" - bottom_left = "bottom_left" - left = "left" - top_left = "top_left" - center = "center" - - -class HorizontalAlignment(str, Enum): - """Horizontal text alignment options.""" - - left = "left" - center = "center" - right = "right" - - -class CaptionBorderStyle(str, Enum): - """Border style properties for caption assets.""" - - outline_and_shadow = "outline_and_shadow" - opaque_box = "opaque_box" - - -class CaptionAlignment(str, Enum): - """Caption alignment properties for caption assets.""" - - bottom_left = "bottom_left" - bottom_center = "bottom_center" - bottom_right = "bottom_right" - middle_left = "middle_left" - middle_center = "middle_center" - middle_right = "middle_right" - top_left = "top_left" - top_center = "top_center" - top_right = "top_right" - - -class CaptionAnimation(str, Enum): - """Caption animation properties for caption assets.""" - - # float_in_bottom = "float_in_bottom" - box_highlight = "box_highlight" - color_highlight = "color_highlight" - reveal = "reveal" - karioke = "karioke" - impact = "impact" - supersize = "supersize" - - -class VerticalAlignment(str, Enum): - """Vertical text alignment options.""" - - top = "top" - center = "center" - bottom = "bottom" - - -class Offset: - def __init__(self, x: float = 0, y: float = 0): - self.x = x - self.y = y - - def to_json(self): - return { - "x": self.x, - "y": self.y, - } - - -class Crop: - def __init__(self, top: int = 0, right: int = 0, bottom: int = 0, left: int = 0): - self.top = top - self.right = right - self.bottom = bottom - self.left = left - - def to_json(self): - return { - "top": self.top, - "right": self.right, - "bottom": self.bottom, - "left": self.left, - } - - -class Transition: - def __init__(self, in_: str = None, out: str = None): - self.in_ = in_ - self.out = out - - def to_json(self): - return { - "in": self.in_, - "out": self.out, - } - - -class BaseAsset: - """The type of asset to display for the duration of the Clip.""" - - type: AssetType - - -class VideoAsset(BaseAsset): - """The VideoAsset is used to create video sequences from video files. The src must be a publicly accessible URL to a video resource""" - - type = AssetType.video - - def __init__( - self, - id: str, - trim: int = 0, - volume: float = 1, - crop: Optional[Crop] = None, - ): - if trim < 0: - raise ValueError("trim must be non-negative") - if not (0 <= volume <= 5): - raise ValueError("volume must be between 0 and 5") - - self.id = id - self.trim = trim - self.volume = volume - self.crop = crop if crop is not None else Crop() - - def to_json(self): - return { - "type": self.type, - "id": self.id, - "trim": self.trim, - "volume": self.volume, - "crop": self.crop.to_json(), - } - - -class ImageAsset(BaseAsset): - """The ImageAsset is used to create video from images to compose an image. The src must be a publicly accessible URL to an image resource such as a jpg or png file.""" - - type = AssetType.image - - def __init__(self, id: str, trim: int = 0, crop: Optional[Crop] = None): - if trim < 0: - raise ValueError("trim must be non-negative") - - self.id = id - self.trim = trim - self.crop = crop if crop is not None else Crop() - - def to_json(self): - return { - "type": self.type, - "id": self.id, - "crop": self.crop.to_json(), - } - - -class AudioAsset(BaseAsset): - """The AudioAsset is used to create audio sequences from audio files. The src must be a publicly accessible URL to an audio resource""" - - type = AssetType.audio - - def __init__(self, id: str, trim: int = 0, volume: float = 1): - self.id = id - self.trim = trim - self.volume = volume - - def to_json(self): - return { - "type": self.type, - "id": self.id, - "trim": self.trim, - "volume": self.volume, - } - - -class Font: - """Font styling properties for text assets.""" - - def __init__( - self, - family: str = "Clear Sans", - size: int = 48, - color: str = "#FFFFFF", - opacity: float = 1.0, - weight: Optional[int] = None, - ): - if size < 1: - raise ValueError("size must be at least 1") - if not (0.0 <= opacity <= 1.0): - raise ValueError("opacity must be between 0.0 and 1.0") - if weight is not None and not (100 <= weight <= 900): - raise ValueError("weight must be between 100 and 900") - - self.family = family - self.size = size - self.color = color - self.opacity = opacity - self.weight = weight - - def to_json(self): - data = { - "family": self.family, - "size": self.size, - "color": self.color, - "opacity": self.opacity, - } - if self.weight is not None: - data["weight"] = self.weight - return data - - -class Border: - """Text border properties.""" - - def __init__(self, color: str = "#000000", width: float = 0.0): - if width < 0.0: - raise ValueError("width must be non-negative") - self.color = color - self.width = width - - def to_json(self): - return { - "color": self.color, - "width": self.width, - } - - -class Shadow: - """Text shadow properties.""" - - def __init__(self, color: str = "#000000", x: float = 0.0, y: float = 0.0): - if x < 0.0: - raise ValueError("x must be non-negative") - if y < 0.0: - raise ValueError("y must be non-negative") - self.color = color - self.x = x - self.y = y - - def to_json(self): - return { - "color": self.color, - "x": self.x, - "y": self.y, - } - - -class Background: - """Text background styling properties.""" - - def __init__( - self, - width: float = 0.0, - height: float = 0.0, - color: str = "#000000", - border_width: float = 0.0, - opacity: float = 1.0, - text_alignment: TextAlignment = TextAlignment.center, - ): - if width < 0.0: - raise ValueError("width must be non-negative") - if height < 0.0: - raise ValueError("height must be non-negative") - if border_width < 0.0: - raise ValueError("border_width must be non-negative") - if not (0.0 <= opacity <= 1.0): - raise ValueError("opacity must be between 0.0 and 1.0") - - self.width = width - self.height = height - self.color = color - self.border_width = border_width - self.opacity = opacity - self.text_alignment = text_alignment - - def to_json(self): - return { - "width": self.width, - "height": self.height, - "color": self.color, - "border_width": self.border_width, - "opacity": self.opacity, - "text_alignment": self.text_alignment.value, - } - - -class Alignment: - """Text alignment properties.""" - - def __init__( - self, - horizontal: HorizontalAlignment = HorizontalAlignment.center, - vertical: VerticalAlignment = VerticalAlignment.center, - ): - self.horizontal = horizontal - self.vertical = vertical - - def to_json(self): - return { - "horizontal": self.horizontal.value, - "vertical": self.vertical.value, - } - - -class TextAsset(BaseAsset): - """The TextAsset is used to create text sequences from text strings with full control over the text styling and positioning.""" - - type = AssetType.text - - def __init__( - self, - text: str, - font: Optional[Font] = None, - border: Optional[Border] = None, - shadow: Optional[Shadow] = None, - background: Optional[Background] = None, - alignment: Optional[Alignment] = None, - tabsize: int = 4, - line_spacing: float = 0, - width: Optional[int] = None, - height: Optional[int] = None, - ): - if tabsize < 1: - raise ValueError("tabsize must be at least 1") - if line_spacing < 0.0: - raise ValueError("line_spacing must be non-negative") - if width is not None and width < 1: - raise ValueError("width must be at least 1") - if height is not None and height < 1: - raise ValueError("height must be at least 1") - - self.text = text - self.font = font if font is not None else Font() - self.border = border - self.shadow = shadow - self.background = background - self.alignment = alignment if alignment is not None else Alignment() - self.tabsize = tabsize - self.line_spacing = line_spacing - self.width = width - self.height = height - - def to_json(self): - data = { - "type": self.type, - "text": self.text, - "font": self.font.to_json(), - "alignment": self.alignment.to_json(), - "tabsize": self.tabsize, - "line_spacing": self.line_spacing, - } - if self.border: - data["border"] = self.border.to_json() - if self.shadow: - data["shadow"] = self.shadow.to_json() - if self.background: - data["background"] = self.background.to_json() - if self.width is not None: - data["width"] = self.width - if self.height is not None: - data["height"] = self.height - - return data - - -class FontStyling: - """Font styling properties for caption assets.""" - - def __init__( - self, - name: str = "Clear Sans", - size: int = 30, - bold: bool = False, - italic: bool = False, - underline: bool = False, - strikeout: bool = False, - scale_x: float = 100, - scale_y: float = 100, - spacing: float = 0.0, - angle: float = 0.0, - ): - self.name = name - self.size = size - self.bold = bold - self.italic = italic - self.underline = underline - self.strikeout = strikeout - self.scale_x = scale_x - self.scale_y = scale_y - self.spacing = spacing - self.angle = angle - - def to_json(self): - return { - "font_name": self.name, - "font_size": self.size, - "bold": self.bold, - "italic": self.italic, - "underline": self.underline, - "strikeout": self.strikeout, - "scale_x": self.scale_x, - "scale_y": self.scale_y, - "spacing": self.spacing, - "angle": self.angle, - } - - -class BorderAndShadow: - """Border and shadow properties for caption assets.""" - - def __init__( - self, - style: CaptionBorderStyle = CaptionBorderStyle.outline_and_shadow, - outline: int = 1, - outline_color: str = "&H00000000", - shadow: int = 0, - ): - self.style = style - self.outline = outline - self.outline_color = outline_color - self.shadow = shadow - - def to_json(self): - return { - "style": self.style.value, - "outline": self.outline, - "outline_color": self.outline_color, - "shadow": self.shadow, - } - - -class Positioning: - """Positioning properties for caption assets.""" - - def __init__( - self, - alignment: CaptionAlignment = CaptionAlignment.bottom_center, - margin_l: int = 30, - margin_r: int = 30, - margin_v: int = 30, - ): - self.alignment = alignment - self.margin_l = margin_l - self.margin_r = margin_r - self.margin_v = margin_v - - def to_json(self): - return { - "alignment": self.alignment.value, - "margin_l": self.margin_l, - "margin_r": self.margin_r, - "margin_v": self.margin_v, - } - - -class CaptionAsset(BaseAsset): - """The CaptionAsset is used to create captions from text strings with full styling and ass support.""" - - type = AssetType.caption - - def __init__( - self, - src: str = "auto", - font: Optional[FontStyling] = None, - primary_color: str = "&H00FFFFFF", - secondary_color: str = "&H000000FF", - back_color: str = "&H00000000", - border: Optional[BorderAndShadow] = None, - position: Optional[Positioning] = None, - animation: Optional[CaptionAnimation] = None, - ): - self.src = src - self.font = font if font is not None else FontStyling() - self.primary_color = primary_color - self.secondary_color = secondary_color - self.back_color = back_color - self.border = border if border is not None else BorderAndShadow() - self.position = position if position is not None else Positioning() - self.animation = animation - - def to_json(self): - data = { - "type": self.type, - "src": self.src, - "font": self.font.to_json(), - "primary_color": self.primary_color, - "secondary_color": self.secondary_color, - "back_color": self.back_color, - "border": self.border.to_json(), - "position": self.position.to_json(), - } - if self.animation: - data["animation"] = self.animation.value - return data - - -AnyAsset = Union[VideoAsset, ImageAsset, AudioAsset, TextAsset, CaptionAsset] - - -class Clip: - """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or caption. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" - - def __init__( - self, - asset: AnyAsset, - start: Union[float, int], - length: Union[float, int], - transition: Optional[Transition] = None, - effect: Optional[str] = None, - filter: Optional[Filter] = None, - scale: float = 1, - opacity: float = 1, - fit: Optional[Fit] = Fit.crop, - position: Position = Position.center, - offset: Optional[Offset] = None, - ): - if start < 0: - raise ValueError("start must be non-negative") - if length <= 0: - raise ValueError("length must be positive") - if not (0 <= scale <= 10): - raise ValueError("scale must be between 0 and 10") - if not (0 <= opacity <= 1): - raise ValueError("opacity must be between 0 and 1") - - self.asset = asset - self.start = start - self.length = length - self.transition = transition - self.effect = effect - self.filter = filter - self.scale = scale - self.opacity = opacity - self.fit = fit - self.position = position - self.offset = offset if offset is not None else Offset() - - def to_json(self): - json = { - "asset": self.asset.to_json(), - "start": self.start, - "length": self.length, - "effect": self.effect, - "scale": self.scale, - "opacity": self.opacity, - "fit": self.fit, - "position": self.position, - "offset": self.offset.to_json(), - } - - if self.transition: - json["transition"] = self.transition.to_json() - if self.filter: - json["filter"] = self.filter.value - - return json - - -class Track: - def __init__(self, clips: List[Clip] = []): - self.clips = clips - - def add_clip(self, clip: Clip): - self.clips.append(clip) - - def to_json(self): - return { - "clips": [clip.to_json() for clip in self.clips], - } - - -class TimelineV2: - def __init__(self, connection): - self.connection = connection - self.background: str = "#000000" - self.resolution: str = "1280x720" - self.tracks: List[Track] = [] - self.stream_url = None - self.player_url = None - - def add_track(self, track: Track): - self.tracks.append(track) - - def add_clip(self, track_index: int, clip: Clip): - self.tracks[track_index].clips.append(clip) - - def to_json(self): - return { - "timeline": { - "background": self.background, - "resolution": self.resolution, - "tracks": [track.to_json() for track in self.tracks], - } - } - - def generate_stream(self): - stream_data = self.connection.post( - path="timeline_v2", - data=self.to_json(), - ) - self.stream_url = stream_data.get("stream_url") - self.player_url = stream_data.get("player_url") - return stream_data.get("stream_url", None) - - def download_stream(self, stream_url: str): - return self.connection.post( - path="timeline_v2/download", data={"stream_url": stream_url} - ) From 828246083e57c1ebb7c307a6c095870d1497d60d Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:46:14 +0530 Subject: [PATCH 20/26] fix: ci/cd --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 07fee5c..a99de3f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ ruff==0.1.7 pytest==7.4.3 twine==5.1.1 wheel==0.42.0 +build==1.2.2 From d3403010a0ee1323f526b0a5664b85e5a9ace853 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:55:10 +0530 Subject: [PATCH 21/26] fix: ci/cd --- requirements-dev.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a99de3f..26bbe4e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ ruff==0.1.7 pytest==7.4.3 twine==5.1.1 -wheel==0.42.0 -build==1.2.2 +wheel==0.45.1 From f3af108be1492f0dcf1cb3e2e8fe9f4c934ddc3d Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:58:48 +0530 Subject: [PATCH 22/26] fix: ci/cd --- .github/workflows/ci.yaml | 1 + requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 316dcd3..8939c13 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,6 +62,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install setuptools==68.0.0 pip install -r requirements.txt pip install -r requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt index 26bbe4e..1a14866 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ ruff==0.1.7 pytest==7.4.3 -twine==5.1.1 +twine==4.0.2 wheel==0.45.1 From cc7e2b9e8259cf4fbb982d60130028de724c6515 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:09:39 +0530 Subject: [PATCH 23/26] fix: ci/cd --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1a14866..c572374 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ ruff==0.1.7 pytest==7.4.3 -twine==4.0.2 +twine==5.0.0 wheel==0.45.1 From 9bb1f56505fcba4b6d1817f481060b377d253d12 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:36:39 +0530 Subject: [PATCH 24/26] docs: add docstings --- videodb/editor.py | 524 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 480 insertions(+), 44 deletions(-) diff --git a/videodb/editor.py b/videodb/editor.py index 5293c6a..30df4ed 100644 --- a/videodb/editor.py +++ b/videodb/editor.py @@ -117,11 +117,27 @@ class VerticalAlignment(str, Enum): class Offset: + """Offset position for positioning elements on the viewport. + + :ivar float x: Horizontal offset value + :ivar float y: Vertical offset value + """ + def __init__(self, x: float = 0, y: float = 0): + """Initialize an Offset instance. + + :param float x: Horizontal offset value (default: 0) + :param float y: Vertical offset value (default: 0) + """ self.x = x self.y = y - def to_json(self): + def to_json(self) -> dict: + """Convert the offset to a JSON-serializable dictionary. + + :return: Dictionary containing offset properties + :rtype: dict + """ return { "x": self.x, "y": self.y, @@ -129,13 +145,33 @@ def to_json(self): class Crop: + """Crop settings for trimming edges of an asset. + + :ivar int top: Number of pixels to crop from the top edge + :ivar int right: Number of pixels to crop from the right edge + :ivar int bottom: Number of pixels to crop from the bottom edge + :ivar int left: Number of pixels to crop from the left edge + """ + def __init__(self, top: int = 0, right: int = 0, bottom: int = 0, left: int = 0): + """Initialize a Crop instance. + + :param int top: Pixels to crop from top (default: 0) + :param int right: Pixels to crop from right (default: 0) + :param int bottom: Pixels to crop from bottom (default: 0) + :param int left: Pixels to crop from left (default: 0) + """ self.top = top self.right = right self.bottom = bottom self.left = left - def to_json(self): + def to_json(self) -> dict: + """Convert the crop settings to a JSON-serializable dictionary. + + :return: Dictionary containing crop properties + :rtype: dict + """ return { "top": self.top, "right": self.right, @@ -145,12 +181,30 @@ def to_json(self): class Transition: + """Transition effect settings for clip entry and exit animations. + + :ivar str in_: The transition effect to apply when the clip enters + :ivar str out: The transition effect to apply when the clip exits + :ivar float duration: Duration of the transition effect in seconds + """ + def __init__(self, in_: str = None, out: str = None, duration: int = 0.5): + """Initialize a Transition instance. + + :param str in_: Entry transition effect name (default: None) + :param str out: Exit transition effect name (default: None) + :param float duration: Transition duration in seconds (default: 0.5) + """ self.in_ = in_ self.out = out self.duration = duration - def to_json(self): + def to_json(self) -> dict: + """Convert the transition settings to a JSON-serializable dictionary. + + :return: Dictionary containing transition properties + :rtype: dict + """ return { "in": self.in_, "out": self.out, @@ -165,7 +219,15 @@ class BaseAsset: class VideoAsset(BaseAsset): - """The VideoAsset is used to create video sequences from video files. The src must be a publicly accessible URL to a video resource""" + """The VideoAsset is used to create video sequences from video files. + + The src must be a publicly accessible URL to a video resource. + + :ivar str id: Unique identifier for the video asset + :ivar int start: Start time offset in seconds + :ivar float volume: Audio volume level (0 to 5) + :ivar Crop crop: Crop settings for the video + """ type = AssetType.video @@ -176,6 +238,14 @@ def __init__( volume: float = 1, crop: Optional[Crop] = None, ): + """Initialize a VideoAsset instance. + + :param str id: Unique identifier for the video asset + :param int start: Start time offset in seconds (default: 0) + :param float volume: Audio volume level between 0 and 5 (default: 1) + :param Crop crop: (optional) Crop settings for the video + :raises ValueError: If start is negative or volume is not between 0 and 5 + """ if start < 0: raise ValueError("start must be non-negative") if not (0 <= volume <= 5): @@ -186,7 +256,12 @@ def __init__( self.volume = volume self.crop = crop if crop is not None else Crop() - def to_json(self): + def to_json(self) -> dict: + """Convert the video asset to a JSON-serializable dictionary. + + :return: Dictionary containing video asset properties + :rtype: dict + """ return { "type": self.type, "id": self.id, @@ -197,15 +272,31 @@ def to_json(self): class ImageAsset(BaseAsset): - """The ImageAsset is used to create video from images to compose an image. The src must be a publicly accessible URL to an image resource such as a jpg or png file.""" + """The ImageAsset is used to create video from images. + + The src must be a publicly accessible URL to an image resource such as a jpg or png file. + + :ivar str id: Unique identifier for the image asset + :ivar Crop crop: Crop settings for the image + """ type = AssetType.image def __init__(self, id: str, crop: Optional[Crop] = None): + """Initialize an ImageAsset instance. + + :param str id: Unique identifier for the image asset + :param Crop crop: (optional) Crop settings for the image + """ self.id = id self.crop = crop if crop is not None else Crop() - def to_json(self): + def to_json(self) -> dict: + """Convert the image asset to a JSON-serializable dictionary. + + :return: Dictionary containing image asset properties + :rtype: dict + """ return { "type": self.type, "id": self.id, @@ -214,16 +305,34 @@ def to_json(self): class AudioAsset(BaseAsset): - """The AudioAsset is used to create audio sequences from audio files. The src must be a publicly accessible URL to an audio resource""" + """The AudioAsset is used to create audio sequences from audio files. + + The src must be a publicly accessible URL to an audio resource. + + :ivar str id: Unique identifier for the audio asset + :ivar int start: Start time offset in seconds + :ivar float volume: Audio volume level + """ type = AssetType.audio def __init__(self, id: str, start: int = 0, volume: float = 1): + """Initialize an AudioAsset instance. + + :param str id: Unique identifier for the audio asset + :param int start: Start time offset in seconds (default: 0) + :param float volume: Audio volume level (default: 1) + """ self.id = id self.start = start self.volume = volume - def to_json(self): + def to_json(self) -> dict: + """Convert the audio asset to a JSON-serializable dictionary. + + :return: Dictionary containing audio asset properties + :rtype: dict + """ return { "type": self.type, "id": self.id, @@ -233,7 +342,14 @@ def to_json(self): class Font: - """Font styling properties for text assets.""" + """Font styling properties for text assets. + + :ivar str family: Font family name + :ivar int size: Font size in pixels + :ivar str color: Font color in hex format (e.g., "#FFFFFF") + :ivar float opacity: Font opacity (0.0 to 1.0) + :ivar int weight: (optional) Font weight (100 to 900) + """ def __init__( self, @@ -243,6 +359,15 @@ def __init__( opacity: float = 1.0, weight: Optional[int] = None, ): + """Initialize a Font instance. + + :param str family: Font family name (default: "Clear Sans") + :param int size: Font size in pixels (default: 48) + :param str color: Font color in hex format (default: "#FFFFFF") + :param float opacity: Font opacity between 0.0 and 1.0 (default: 1.0) + :param int weight: (optional) Font weight between 100 and 900 + :raises ValueError: If size < 1, opacity not in [0.0, 1.0], or weight not in [100, 900] + """ if size < 1: raise ValueError("size must be at least 1") if not (0.0 <= opacity <= 1.0): @@ -256,7 +381,12 @@ def __init__( self.opacity = opacity self.weight = weight - def to_json(self): + def to_json(self) -> dict: + """Convert the font settings to a JSON-serializable dictionary. + + :return: Dictionary containing font properties + :rtype: dict + """ data = { "family": self.family, "size": self.size, @@ -269,15 +399,30 @@ def to_json(self): class Border: - """Text border properties.""" + """Text border properties. + + :ivar str color: Border color in hex format (e.g., "#000000") + :ivar float width: Border width in pixels + """ def __init__(self, color: str = "#000000", width: float = 0.0): + """Initialize a Border instance. + + :param str color: Border color in hex format (default: "#000000") + :param float width: Border width in pixels (default: 0.0) + :raises ValueError: If width is negative + """ if width < 0.0: raise ValueError("width must be non-negative") self.color = color self.width = width - def to_json(self): + def to_json(self) -> dict: + """Convert the border settings to a JSON-serializable dictionary. + + :return: Dictionary containing border properties + :rtype: dict + """ return { "color": self.color, "width": self.width, @@ -285,9 +430,21 @@ def to_json(self): class Shadow: - """Text shadow properties.""" + """Text shadow properties. + + :ivar str color: Shadow color in hex format (e.g., "#000000") + :ivar float x: Horizontal shadow offset in pixels + :ivar float y: Vertical shadow offset in pixels + """ def __init__(self, color: str = "#000000", x: float = 0.0, y: float = 0.0): + """Initialize a Shadow instance. + + :param str color: Shadow color in hex format (default: "#000000") + :param float x: Horizontal shadow offset in pixels (default: 0.0) + :param float y: Vertical shadow offset in pixels (default: 0.0) + :raises ValueError: If x or y is negative + """ if x < 0.0: raise ValueError("x must be non-negative") if y < 0.0: @@ -296,7 +453,12 @@ def __init__(self, color: str = "#000000", x: float = 0.0, y: float = 0.0): self.x = x self.y = y - def to_json(self): + def to_json(self) -> dict: + """Convert the shadow settings to a JSON-serializable dictionary. + + :return: Dictionary containing shadow properties + :rtype: dict + """ return { "color": self.color, "x": self.x, @@ -305,7 +467,15 @@ def to_json(self): class Background: - """Text background styling properties.""" + """Text background styling properties. + + :ivar float width: Background width in pixels + :ivar float height: Background height in pixels + :ivar str color: Background color in hex format (e.g., "#000000") + :ivar float border_width: Background border width in pixels + :ivar float opacity: Background opacity (0.0 to 1.0) + :ivar TextAlignment text_alignment: Text alignment within the background + """ def __init__( self, @@ -316,6 +486,16 @@ def __init__( opacity: float = 1.0, text_alignment: TextAlignment = TextAlignment.center, ): + """Initialize a Background instance. + + :param float width: Background width in pixels (default: 0.0) + :param float height: Background height in pixels (default: 0.0) + :param str color: Background color in hex format (default: "#000000") + :param float border_width: Border width in pixels (default: 0.0) + :param float opacity: Background opacity between 0.0 and 1.0 (default: 1.0) + :param TextAlignment text_alignment: Text alignment position (default: TextAlignment.center) + :raises ValueError: If width, height, or border_width is negative, or opacity not in [0.0, 1.0] + """ if width < 0.0: raise ValueError("width must be non-negative") if height < 0.0: @@ -332,7 +512,12 @@ def __init__( self.opacity = opacity self.text_alignment = text_alignment - def to_json(self): + def to_json(self) -> dict: + """Convert the background settings to a JSON-serializable dictionary. + + :return: Dictionary containing background properties + :rtype: dict + """ return { "width": self.width, "height": self.height, @@ -344,17 +529,31 @@ def to_json(self): class Alignment: - """Text alignment properties.""" + """Text alignment properties. + + :ivar HorizontalAlignment horizontal: Horizontal text alignment + :ivar VerticalAlignment vertical: Vertical text alignment + """ def __init__( self, horizontal: HorizontalAlignment = HorizontalAlignment.center, vertical: VerticalAlignment = VerticalAlignment.center, ): + """Initialize an Alignment instance. + + :param HorizontalAlignment horizontal: Horizontal alignment (default: HorizontalAlignment.center) + :param VerticalAlignment vertical: Vertical alignment (default: VerticalAlignment.center) + """ self.horizontal = horizontal self.vertical = vertical - def to_json(self): + def to_json(self) -> dict: + """Convert the alignment settings to a JSON-serializable dictionary. + + :return: Dictionary containing alignment properties + :rtype: dict + """ return { "horizontal": self.horizontal.value, "vertical": self.vertical.value, @@ -362,7 +561,21 @@ def to_json(self): class TextAsset(BaseAsset): - """The TextAsset is used to create text sequences from text strings with full control over the text styling and positioning.""" + """The TextAsset is used to create text sequences from text strings. + + Provides full control over the text styling and positioning. + + :ivar str text: The text content to display + :ivar Font font: Font styling properties + :ivar Border border: (optional) Text border properties + :ivar Shadow shadow: (optional) Text shadow properties + :ivar Background background: (optional) Text background properties + :ivar Alignment alignment: Text alignment properties + :ivar int tabsize: Tab character size in spaces + :ivar float line_spacing: Space between lines + :ivar int width: (optional) Text box width in pixels + :ivar int height: (optional) Text box height in pixels + """ type = AssetType.text @@ -379,6 +592,20 @@ def __init__( width: Optional[int] = None, height: Optional[int] = None, ): + """Initialize a TextAsset instance. + + :param str text: The text content to display + :param Font font: (optional) Font styling properties + :param Border border: (optional) Text border properties + :param Shadow shadow: (optional) Text shadow properties + :param Background background: (optional) Text background properties + :param Alignment alignment: (optional) Text alignment properties + :param int tabsize: Tab character size in spaces (default: 4) + :param float line_spacing: Space between lines (default: 0) + :param int width: (optional) Text box width in pixels + :param int height: (optional) Text box height in pixels + :raises ValueError: If tabsize < 1, line_spacing < 0, width < 1, or height < 1 + """ if tabsize < 1: raise ValueError("tabsize must be at least 1") if line_spacing < 0.0: @@ -399,7 +626,12 @@ def __init__( self.width = width self.height = height - def to_json(self): + def to_json(self) -> dict: + """Convert the text asset to a JSON-serializable dictionary. + + :return: Dictionary containing text asset properties + :rtype: dict + """ data = { "type": self.type, "text": self.text, @@ -423,7 +655,19 @@ def to_json(self): class FontStyling: - """Font styling properties for caption assets.""" + """Font styling properties for caption assets. + + :ivar str name: Font family name + :ivar int size: Font size in pixels + :ivar bool bold: Whether text is bold + :ivar bool italic: Whether text is italic + :ivar bool underline: Whether text is underlined + :ivar bool strikeout: Whether text has strikethrough + :ivar float scale_x: Horizontal scale percentage + :ivar float scale_y: Vertical scale percentage + :ivar float spacing: Character spacing + :ivar float angle: Text rotation angle in degrees + """ def __init__( self, @@ -438,6 +682,19 @@ def __init__( spacing: float = 0.0, angle: float = 0.0, ): + """Initialize a FontStyling instance. + + :param str name: Font family name (default: "Clear Sans") + :param int size: Font size in pixels (default: 30) + :param bool bold: Enable bold text (default: False) + :param bool italic: Enable italic text (default: False) + :param bool underline: Enable underlined text (default: False) + :param bool strikeout: Enable strikethrough text (default: False) + :param float scale_x: Horizontal scale percentage (default: 100) + :param float scale_y: Vertical scale percentage (default: 100) + :param float spacing: Character spacing (default: 0.0) + :param float angle: Text rotation angle in degrees (default: 0.0) + """ self.name = name self.size = size self.bold = bold @@ -449,7 +706,12 @@ def __init__( self.spacing = spacing self.angle = angle - def to_json(self): + def to_json(self) -> dict: + """Convert the font styling to a JSON-serializable dictionary. + + :return: Dictionary containing font styling properties + :rtype: dict + """ return { "font_name": self.name, "font_size": self.size, @@ -465,7 +727,13 @@ def to_json(self): class BorderAndShadow: - """Border and shadow properties for caption assets.""" + """Border and shadow properties for caption assets. + + :ivar CaptionBorderStyle style: Border style type + :ivar int outline: Outline thickness in pixels + :ivar str outline_color: Outline color in ASS format (e.g., "&H00000000") + :ivar int shadow: Shadow depth in pixels + """ def __init__( self, @@ -474,12 +742,24 @@ def __init__( outline_color: str = "&H00000000", shadow: int = 0, ): + """Initialize a BorderAndShadow instance. + + :param CaptionBorderStyle style: Border style type (default: CaptionBorderStyle.outline_and_shadow) + :param int outline: Outline thickness in pixels (default: 1) + :param str outline_color: Outline color in ASS format (default: "&H00000000") + :param int shadow: Shadow depth in pixels (default: 0) + """ self.style = style self.outline = outline self.outline_color = outline_color self.shadow = shadow - def to_json(self): + def to_json(self) -> dict: + """Convert the border and shadow settings to a JSON-serializable dictionary. + + :return: Dictionary containing border and shadow properties + :rtype: dict + """ return { "style": self.style.value, "outline": self.outline, @@ -489,7 +769,13 @@ def to_json(self): class Positioning: - """Positioning properties for caption assets.""" + """Positioning properties for caption assets. + + :ivar CaptionAlignment alignment: Caption alignment position + :ivar int margin_l: Left margin in pixels + :ivar int margin_r: Right margin in pixels + :ivar int margin_v: Vertical margin in pixels + """ def __init__( self, @@ -498,12 +784,24 @@ def __init__( margin_r: int = 30, margin_v: int = 30, ): + """Initialize a Positioning instance. + + :param CaptionAlignment alignment: Caption alignment position (default: CaptionAlignment.bottom_center) + :param int margin_l: Left margin in pixels (default: 30) + :param int margin_r: Right margin in pixels (default: 30) + :param int margin_v: Vertical margin in pixels (default: 30) + """ self.alignment = alignment self.margin_l = margin_l self.margin_r = margin_r self.margin_v = margin_v - def to_json(self): + def to_json(self) -> dict: + """Convert the positioning settings to a JSON-serializable dictionary. + + :return: Dictionary containing positioning properties + :rtype: dict + """ return { "alignment": self.alignment.value, "margin_l": self.margin_l, @@ -513,7 +811,19 @@ def to_json(self): class CaptionAsset(BaseAsset): - """The CaptionAsset is used to create captions from text strings with full styling and ass support.""" + """The CaptionAsset is used to create captions from text strings. + + Provides full styling and ASS (Advanced SubStation Alpha) subtitle format support. + + :ivar str src: Caption source ("auto" for auto-generated or custom text) + :ivar FontStyling font: Font styling properties + :ivar str primary_color: Primary text color in ASS format (e.g., "&H00FFFFFF") + :ivar str secondary_color: Secondary text color in ASS format + :ivar str back_color: Background color in ASS format + :ivar BorderAndShadow border: Border and shadow properties + :ivar Positioning position: Caption positioning properties + :ivar CaptionAnimation animation: (optional) Caption animation effect + """ type = AssetType.caption @@ -528,6 +838,17 @@ def __init__( position: Optional[Positioning] = None, animation: Optional[CaptionAnimation] = None, ): + """Initialize a CaptionAsset instance. + + :param str src: Caption source (default: "auto") + :param FontStyling font: (optional) Font styling properties + :param str primary_color: Primary text color in ASS format (default: "&H00FFFFFF") + :param str secondary_color: Secondary text color in ASS format (default: "&H000000FF") + :param str back_color: Background color in ASS format (default: "&H00000000") + :param BorderAndShadow border: (optional) Border and shadow properties + :param Positioning position: (optional) Caption positioning properties + :param CaptionAnimation animation: (optional) Caption animation effect + """ self.src = src self.font = font if font is not None else FontStyling() self.primary_color = primary_color @@ -537,7 +858,12 @@ def __init__( self.position = position if position is not None else Positioning() self.animation = animation - def to_json(self): + def to_json(self) -> dict: + """Convert the caption asset to a JSON-serializable dictionary. + + :return: Dictionary containing caption asset properties + :rtype: dict + """ data = { "type": self.type, "src": self.src, @@ -557,7 +883,24 @@ def to_json(self): class Clip: - """A clip is a container for a specific type of asset, i.e. a title, image, video, audio or caption. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.""" + """A clip is a container for a specific type of asset. + + Assets can be title, image, video, audio, or caption. Use a Clip to define + when an asset will display on the timeline, how long it will play for, + and transitions, filters and effects to apply to it. + + :ivar AnyAsset asset: The asset contained in this clip + :ivar Union[float, int] duration: Duration of the clip in seconds + :ivar Transition transition: (optional) Transition effects for the clip + :ivar str effect: (optional) Effect to apply to the clip + :ivar Filter filter: (optional) Filter to apply to the clip + :ivar float scale: Scale factor (0 to 10) + :ivar float opacity: Opacity level (0 to 1) + :ivar Fit fit: How the asset should be scaled to fit the viewport + :ivar Position position: Position of the asset in the viewport + :ivar Offset offset: Offset position for fine-tuning placement + :ivar int z_index: Z-index for layering order + """ def __init__( self, @@ -573,6 +916,21 @@ def __init__( offset: Optional[Offset] = None, z_index: int = 0, ): + """Initialize a Clip instance. + + :param AnyAsset asset: The asset to display (VideoAsset, ImageAsset, AudioAsset, TextAsset, or CaptionAsset) + :param Union[float, int] duration: Duration of the clip in seconds + :param Transition transition: (optional) Transition effects for entry/exit + :param str effect: (optional) Effect name to apply + :param Filter filter: (optional) Filter to apply to the clip + :param float scale: Scale factor between 0 and 10 (default: 1) + :param float opacity: Opacity level between 0 and 1 (default: 1) + :param Fit fit: Asset scaling mode (default: Fit.crop) + :param Position position: Asset position in viewport (default: Position.center) + :param Offset offset: (optional) Fine-tune position offset + :param int z_index: Layering order (default: 0) + :raises ValueError: If scale not in [0, 10] or opacity not in [0, 1] + """ if not (0 <= scale <= 10): raise ValueError("scale must be between 0 and 10") if not (0 <= opacity <= 1): @@ -590,7 +948,12 @@ def __init__( self.offset = offset if offset is not None else Offset() self.z_index = z_index - def to_json(self): + def to_json(self) -> dict: + """Convert the clip to a JSON-serializable dictionary. + + :return: Dictionary containing clip properties + :rtype: dict + """ json = { "asset": self.asset.to_json(), "duration": self.duration, @@ -612,13 +975,27 @@ def to_json(self): class TrackItem: - """Clip wrapper""" + """Wrapper class that positions a clip at a specific start time on a track. + + :ivar int start: Start time in seconds when the clip begins on the track + :ivar Clip clip: The clip to be placed on the track + """ def __init__(self, start: int, clip: Clip): + """Initialize a TrackItem instance. + + :param int start: Start time in seconds when the clip begins + :param Clip clip: The clip to be placed on the track + """ self.start = start self.clip = clip - def to_json(self): + def to_json(self) -> dict: + """Convert the track item to a JSON-serializable dictionary. + + :return: Dictionary containing track item properties + :rtype: dict + """ return { "start": self.start, "clip": self.clip.to_json(), @@ -626,18 +1003,39 @@ def to_json(self): class Track: - """A track contains an array of clips. Tracks are layered on top of each other in the order in the array. The top most track will render on top of those below it.""" + """A track contains an array of clips. + + Tracks are layered on top of each other in the order in the array. + The top most track will render on top of those below it. + + :ivar List[TrackItem] clips: List of clips on this track + :ivar int z_index: Z-index for track layering order + """ def __init__(self, z_index: int = 0): + """Initialize a Track instance. + + :param int z_index: Z-index for track layering order (default: 0) + """ self.clips: List[TrackItem] = [] self.z_index: int = z_index - def add_clip(self, start: int, clip: Clip): - """Add a clip to the track.""" + def add_clip(self, start: int, clip: Clip) -> None: + """Add a clip to the track at a specific start time. + :param int start: Start time in seconds when the clip begins + :param Clip clip: The clip to add to the track + :return: None + :rtype: None + """ self.clips.append(TrackItem(start, clip)) - def to_json(self): + def to_json(self) -> dict: + """Convert the track to a JSON-serializable dictionary. + + :return: Dictionary containing track properties and clips + :rtype: dict + """ return { "clips": [clip.to_json() for clip in self.clips], "z_index": self.z_index, @@ -645,9 +1043,26 @@ def to_json(self): class Timeline: - """A timeline represents the contents of a video edit over time, an audio edit over time, in seconds, or an image layout. A timeline consists of layers called tracks. Tracks are composed of titles, images, audio, html or video segments referred to as clips which are placed along the track at specific starting point and lasting for a specific amount of time.""" + """A timeline represents the contents of a video edit over time. + + A timeline consists of layers called tracks. Tracks are composed of titles, + images, audio, html or video segments referred to as clips which are placed + along the track at specific starting points and lasting for a specific + amount of time. + + :ivar connection: API connection instance for making requests + :ivar str background: Background color in hex format (e.g., "#000000") + :ivar str resolution: Video resolution (e.g., "1280x720") + :ivar List[Track] tracks: List of tracks in the timeline + :ivar str stream_url: URL of the generated stream (populated after generate_stream) + :ivar str player_url: URL of the video player (populated after generate_stream) + """ def __init__(self, connection): + """Initialize a Timeline instance. + + :param connection: API connection instance for making requests + """ self.connection = connection self.background: str = "#000000" self.resolution: str = "1280x720" @@ -655,10 +1070,21 @@ def __init__(self, connection): self.stream_url = None self.player_url = None - def add_track(self, track: Track): + def add_track(self, track: Track) -> None: + """Add a track to the timeline. + + :param Track track: The track to add to the timeline + :return: None + :rtype: None + """ self.tracks.append(track) - def to_json(self): + def to_json(self) -> dict: + """Convert the timeline to a JSON-serializable dictionary. + + :return: Dictionary containing timeline properties and tracks + :rtype: dict + """ return { "timeline": { "background": self.background, @@ -667,9 +1093,15 @@ def to_json(self): } } - def generate_stream(self): - """Generate a stream from the timeline.""" + def generate_stream(self) -> str: + """Generate a stream from the timeline. + + Makes an API request to render the timeline and generate streaming URLs. + Updates the stream_url and player_url instance variables. + :return: The stream URL of the generated video + :rtype: str + """ stream_data = self.connection.post( path=ApiPath.editor, data=self.to_json(), @@ -678,9 +1110,13 @@ def generate_stream(self): self.player_url = stream_data.get("player_url") return stream_data.get("stream_url", None) - def download_stream(self, stream_url: str): - """Download a stream from the timeline.""" + def download_stream(self, stream_url: str) -> dict: + """Download a stream from the timeline. + :param str stream_url: The URL of the stream to download + :return: Dictionary containing download information + :rtype: dict + """ return self.connection.post( path=f"{ApiPath.editor}/{ApiPath.download}", data={"stream_url": stream_url} ) From c1518c0dd749e9616afe92d93e97eb8bb3984445 Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:49:40 +0530 Subject: [PATCH 25/26] fix: ci/cd --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c572374..07fee5c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ ruff==0.1.7 pytest==7.4.3 -twine==5.0.0 -wheel==0.45.1 +twine==5.1.1 +wheel==0.42.0 From 0d3a5463e38c4981f53402897b68c1905411989f Mon Sep 17 00:00:00 2001 From: Ankit raj <113342181+ankit-v2-3@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:32:44 +0530 Subject: [PATCH 26/26] feat: add base64 ass string --- videodb/editor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/videodb/editor.py b/videodb/editor.py index 30df4ed..9efc793 100644 --- a/videodb/editor.py +++ b/videodb/editor.py @@ -811,11 +811,11 @@ def to_json(self) -> dict: class CaptionAsset(BaseAsset): - """The CaptionAsset is used to create captions from text strings. + """The CaptionAsset is used to create auto-generated or custom captions. Provides full styling and ASS (Advanced SubStation Alpha) subtitle format support. - :ivar str src: Caption source ("auto" for auto-generated or custom text) + :ivar str src: Caption source ("auto" for auto-generated or base64 encoded ass string) :ivar FontStyling font: Font styling properties :ivar str primary_color: Primary text color in ASS format (e.g., "&H00FFFFFF") :ivar str secondary_color: Secondary text color in ASS format @@ -840,7 +840,7 @@ def __init__( ): """Initialize a CaptionAsset instance. - :param str src: Caption source (default: "auto") + :param str src: Caption source ("auto" for auto-generated or base64 encoded ass string) :param FontStyling font: (optional) Font styling properties :param str primary_color: Primary text color in ASS format (default: "&H00FFFFFF") :param str secondary_color: Secondary text color in ASS format (default: "&H000000FF")