Skip to main content
Glama
composition_builder.py40.1 kB
""" Composition Builder - Combines components into complete video compositions. Manages the timeline, layering, and sequencing of video components. ## Dynamic Builder Methods All component builder methods (add_line_chart, add_code_block, etc.) are dynamically registered at runtime by discover_components() and register_all_builders() in src/chuk_motion/components/__init__.py. Each component directory (e.g., src/chuk_motion/components/charts/LineChart/) contains a builder.py file with an add_to_composition() function. These functions are automatically discovered and converted into methods on the CompositionBuilder class. This design ensures: - Single source of truth: Component logic lives only in component/builder.py - Automatic discovery: New components are automatically available - Consistency: All components follow the same pattern - Maintainability: Update one place, not multiple files Usage: from chuk_motion.generator.composition_builder import CompositionBuilder from chuk_motion.components import register_all_builders # Register all component builder methods register_all_builders(CompositionBuilder) # Create composition and use dynamically added methods builder = CompositionBuilder(fps=30) builder.add_title_scene(text="Hello", duration_seconds=3.0) builder.add_line_chart(data=[[0,10],[1,20]], start_time=3.0) """ import logging from dataclasses import dataclass, field from typing import Any logger = logging.getLogger(__name__) def snake_to_camel(snake_str: str) -> str: """Convert snake_case to camelCase.""" components = snake_str.split("_") return components[0] + "".join(x.title() for x in components[1:]) @dataclass class ComponentInstance: """Represents an instance of a component in the timeline.""" component_type: str # TitleScene, LowerThird, etc. start_frame: int duration_frames: int props: dict[str, Any] = field(default_factory=dict) layer: int = 0 # Higher layers render on top class CompositionBuilder: """Builds complete video compositions from components.""" # Class-level registry for component-specific renderers _component_renderers: dict = {} def __init__( self, fps: int = 30, width: int = 1920, height: int = 1080, transparent: bool = False ): """ Initialize composition builder. Args: fps: Frames per second (default: 30) width: Video width in pixels (default: 1920) height: Video height in pixels (default: 1080) transparent: Use transparent background (default: False) """ self.fps = fps self.width = width self.height = height self.components: list[ComponentInstance] = [] self.theme = "tech" self.transparent = transparent def seconds_to_frames(self, seconds: float) -> int: """Convert seconds to frames.""" return int(seconds * self.fps) def frames_to_seconds(self, frames: int) -> float: """Convert frames to seconds.""" return frames / self.fps # ========================================================================= # COMPONENT BUILDER METHODS ARE DYNAMICALLY ADDED # ========================================================================= # # All add_* methods (add_line_chart, add_code_block, etc.) are dynamically # registered at runtime by the register_all_builders() function in # src/chuk_motion/components/__init__.py # # This function discovers all components and automatically creates builder # methods from each component's builder.py file, ensuring: # - Single source of truth (no duplication) # - Automatic discovery of new components # - Consistency across all components # # See: src/chuk_motion/components/__init__.py:138-164 # ========================================================================= def _get_next_start_frame(self) -> int: """Get the start frame for the next sequential component.""" if not self.components: return 0 # Find the last component on layer 0 (main content) layer_0_components = [c for c in self.components if c.layer == 0] if not layer_0_components: return 0 last = max(layer_0_components, key=lambda c: c.start_frame + c.duration_frames) return last.start_frame + last.duration_frames def get_total_duration_frames(self) -> int: """Get total duration of the composition in frames.""" if not self.components: return 0 return max(c.start_frame + c.duration_frames for c in self.components) def get_total_duration_seconds(self) -> float: """Get total duration of the composition in seconds.""" return self.frames_to_seconds(self.get_total_duration_frames()) def generate_composition_tsx(self) -> str: """ Generate the main VideoComposition.tsx component. Returns: TSX code for the complete composition """ # Sort components by layer (lower layers first) sorted_components = sorted(self.components, key=lambda c: c.layer) # Find all nested children to exclude from top-level rendering nested_children = self._find_nested_children(sorted_components) # Generate import statements (recursively find all component types) unique_types = self._find_all_component_types(sorted_components) imports = "\n".join( [ f"import {{ {comp_type} }} from './components/{comp_type}';" for comp_type in sorted(unique_types) ] ) # Generate component JSX (only top-level components) components_jsx = [] for comp in sorted_components: # Skip if this component is a child of another component if id(comp) in nested_children: continue jsx = self._render_component_jsx(comp, indent=6) components_jsx.append(jsx) components_jsx_str = "\n".join(components_jsx) # Background color: transparent or black background_color = "transparent" if self.transparent else "#000" # Generate complete composition tsx = f"""import React from 'react'; import {{ AbsoluteFill }} from 'remotion'; {imports} interface VideoCompositionProps {{ theme: string; }} export const VideoComposition: React.FC<VideoCompositionProps> = ({{ theme }}) => {{ return ( <AbsoluteFill style={{{{ backgroundColor: '{background_color}' }}}}> {components_jsx_str} </AbsoluteFill> ); }}; """ return tsx def _find_all_component_types(self, components: list[ComponentInstance]) -> set: """Recursively find all component types including nested children.""" types = set() def collect_types(comp): types.add(comp.component_type) # Check for nested children layout_types = [ "Grid", "Container", "SplitScreen", "ThreeColumnLayout", "ThreeRowLayout", "ThreeByThreeGrid", "AsymmetricLayout", "OverTheShoulder", "DialogueFrame", "StackedReaction", "HUDStyle", "PerformanceMultiCam", "FocusStrip", "PiP", "Vertical", "Timeline", "Mosaic", # Frame components with content prop "BrowserFrame", "DeviceFrame", "Terminal", # Transition components "PixelTransition", "LayoutTransition", # Animation wrapper components "LayoutEntrance", "PanelCascade", # Overlay components with content "TextOverlay", "LowerThird", "SubscribeButton", ] if comp.component_type in layout_types: children = comp.props.get("children") if isinstance(children, list): for child in children: if isinstance(child, ComponentInstance): collect_types(child) elif isinstance(children, ComponentInstance): collect_types(children) # For SplitScreen and ThreeColumn/ThreeRow layouts for key in ["left", "right", "top", "bottom", "center", "middle"]: child = comp.props.get(key) if isinstance(child, ComponentInstance): collect_types(child) # For specialized layouts specialized_keys = [ "mainFeed", "demo1", "demo2", "overlay", # AsymmetricLayout (old) "main", "top_side", "bottom_side", # AsymmetricLayout (new) "hostView", "screenContent", # OverTheShoulder "screen_content", "shoulder_overlay", # OverTheShoulder (new) "characterA", "characterB", # DialogueFrame "left_speaker", "center_content", "right_speaker", # DialogueFrame (new) "originalClip", "reactorFace", # StackedReaction "original_content", "reaction_content", # StackedReaction (new) "gameplay", "webcam", "chatOverlay", # HUDStyle "main_content", "top_left", "top_right", "bottom_left", "bottom_right", # HUDStyle (new) "frontCam", "overheadCam", "handCam", "detailCam", # PerformanceMultiCam "primary_cam", "secondary_cams", # PerformanceMultiCam (new) "hostStrip", "backgroundContent", # FocusStrip "focus_content", # FocusStrip (new) "mainContent", "pipContent", # PiPLayout "topContent", "bottomContent", "captionBar", # VerticalLayout "milestones", "clips", # TimelineLayout, MosaicLayout "content", # Container, Frames, Overlays, Animations "items", # PanelCascade - array of ComponentInstance "panels", # Legacy "leftPanel", "rightPanel", "topPanel", "bottomPanel", # SplitScreen "firstContent", "secondContent", # PixelTransition "before", "after", # LayoutTransition ] for key in specialized_keys: child = comp.props.get(key) if isinstance(child, ComponentInstance): collect_types(child) # Also handle arrays of ComponentInstances elif isinstance(child, list): for item in child: if isinstance(item, ComponentInstance): collect_types(item) for comp in components: collect_types(comp) return types def _find_nested_children(self, components: list[ComponentInstance]) -> set: """Find all components that are children of layout components.""" nested = set() layout_types = [ "Grid", "Container", "SplitScreen", "ThreeColumnLayout", "ThreeRowLayout", "ThreeByThreeGrid", "AsymmetricLayout", "OverTheShoulder", "DialogueFrame", "StackedReaction", "PixelTransition", "LayoutTransition", "HUDStyle", "PerformanceMultiCam", "FocusStrip", "PiP", "Vertical", "Timeline", "Mosaic", # Animation wrappers "LayoutEntrance", "PanelCascade", ] for comp in components: if comp.component_type in layout_types: # Get children from props children = comp.props.get("children") if isinstance(children, list): for child in children: if isinstance(child, ComponentInstance): nested.add(id(child)) elif isinstance(children, ComponentInstance): nested.add(id(children)) # For SplitScreen and ThreeColumn/ThreeRow layouts for key in ["left", "right", "top", "bottom", "center", "middle"]: child = comp.props.get(key) if isinstance(child, ComponentInstance): nested.add(id(child)) # For specialized layouts specialized_keys = [ "mainFeed", "demo1", "demo2", "overlay", # AsymmetricLayout (old) "main", "top_side", "bottom_side", # AsymmetricLayout (new) "hostView", "screenContent", # OverTheShoulder "screen_content", "shoulder_overlay", # OverTheShoulder (new) "characterA", "characterB", # DialogueFrame "left_speaker", "center_content", "right_speaker", # DialogueFrame (new) "originalClip", "reactorFace", # StackedReaction "original_content", "reaction_content", # StackedReaction (new) "gameplay", "webcam", "chatOverlay", # HUDStyle "main_content", "top_left", "top_right", "bottom_left", "bottom_right", # HUDStyle (new) "frontCam", "overheadCam", "handCam", "detailCam", # PerformanceMultiCam "primary_cam", "secondary_cams", # PerformanceMultiCam (new) "hostStrip", "backgroundContent", # FocusStrip "focus_content", # FocusStrip (new) "mainContent", "pipContent", # PiPLayout "topContent", "bottomContent", "captionBar", # VerticalLayout "milestones", "clips", # TimelineLayout, MosaicLayout "content", # Container, Frames, Overlays, Animations "panels", # PanelCascade "items", # PanelCascade (new) "leftPanel", "rightPanel", "topPanel", "bottomPanel", # SplitScreen "firstContent", "secondContent", # PixelTransition "before", "after", # LayoutTransition ] for key in specialized_keys: child = comp.props.get(key) if isinstance(child, ComponentInstance): nested.add(id(child)) # Also handle arrays elif isinstance(child, list): for item in child: if isinstance(item, ComponentInstance): nested.add(id(item)) return nested def _render_component_jsx(self, comp: ComponentInstance, indent: int = 0) -> str: """Render a component as JSX, including nested children.""" # Try component-specific custom renderer FIRST (highest priority) custom_jsx = self._try_custom_renderer(comp, indent) if custom_jsx is not None: return custom_jsx # Check if this is a layout component with children layout_types = [ "Grid", "Container", "SplitScreen", "ThreeColumnLayout", "ThreeRowLayout", "ThreeByThreeGrid", "AsymmetricLayout", "OverTheShoulder", "DialogueFrame", "StackedReaction", "HUDStyle", "PerformanceMultiCam", "FocusStrip", "PiP", "Vertical", "Timeline", "Mosaic", # Frame components with content prop "BrowserFrame", "DeviceFrame", "Terminal", # Transition components "PixelTransition", "LayoutTransition", # Animation components with content "LayoutEntrance", "PanelCascade", # Overlay components with content "TextOverlay", "LowerThird", "SubscribeButton", ] has_children = comp.component_type in layout_types if has_children: return self._render_layout_component(comp, indent) else: return self._render_simple_component(comp, indent) def _render_simple_component(self, comp: ComponentInstance, indent: int) -> str: """Render a simple component without children.""" spaces = " " * indent # Format props (exclude children-related props) # Convert snake_case keys to camelCase for TypeScript props_lines = [] for key, value in comp.props.items(): if key not in ["children", "left", "right", "top", "bottom"] and value is not None: camel_key = snake_to_camel(key) props_lines.append(f"{spaces} {camel_key}={self._format_prop_value(value)}") props_str = "\n".join(props_lines) if props_lines else "" if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces}/>""" def _render_layout_component(self, comp: ComponentInstance, indent: int) -> str: """Render a layout component with nested children.""" spaces = " " * indent # Format non-children props # Exclude child component props from regular props (using snake_case names to match templates) exclude_keys = [ "children", "left", "right", "top", "bottom", "center", "middle", # AsymmetricLayout "main", "top_side", "bottom_side", # OverTheShoulder "screen_content", "shoulder_overlay", # DialogueFrame "left_speaker", "right_speaker", "center_content", # StackedReaction "original_content", "reaction_content", # HUDStyle "main_content", "top_left", "top_right", "bottom_left", "bottom_right", # PerformanceMultiCam "primary_cam", "secondary_cams", # FocusStrip "focus_content", # PiP "mainContent", "pipContent", # Vertical "topContent", "bottomContent", "captionBar", # Timeline / Mosaic "clips", # Container, Overlays, Animations "content", # PanelCascade "panels", # PixelTransition / LayoutTransition "firstContent", "secondContent", "before", "after", ] props_lines = [] for key, value in comp.props.items(): if key not in exclude_keys and value is not None: camel_key = snake_to_camel(key) props_lines.append(f"{spaces} {camel_key}={self._format_prop_value(value)}") props_str = "\n".join(props_lines) if props_lines else "" # Render children based on component type if comp.component_type == "Grid": children = comp.props.get("children", []) if isinstance(children, list): children_jsx = [] for child in children: if isinstance(child, ComponentInstance): child_jsx = self._render_component_jsx(child, indent + 4) children_jsx.append(child_jsx) # Join with commas for JSX array children_str = ",\n".join(children_jsx) else: children_str = "" if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces}> {spaces} {{[ {children_str} {spaces} ]}} {spaces}</{comp.component_type}>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces}> {spaces} {{[ {children_str} {spaces} ]}} {spaces}</{comp.component_type}>""" elif comp.component_type == "Container": children = comp.props.get("children") # Handle single child or array of children if isinstance(children, ComponentInstance): child_jsx = self._render_component_jsx(children, indent + 4) elif isinstance(children, list) and len(children) > 0: # If array with single item, render it directly if len(children) == 1 and isinstance(children[0], ComponentInstance): child_jsx = self._render_component_jsx(children[0], indent + 4) else: # Multiple children - render them all children_jsxs = [] for child_item in children: if isinstance(child_item, ComponentInstance): children_jsxs.append(self._render_component_jsx(child_item, indent + 4)) child_jsx = "\n".join(children_jsxs) else: child_jsx = "" if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces}> {child_jsx} {spaces}</{comp.component_type}>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces}> {child_jsx} {spaces}</{comp.component_type}>""" elif comp.component_type == "SplitScreen": # Render left/right or top/bottom based on orientation orientation = comp.props.get("orientation", "horizontal") if orientation == "horizontal": left = comp.props.get("left") right = comp.props.get("right") left_jsx = ( self._render_component_jsx(left, indent + 4) if isinstance(left, ComponentInstance) else "" ) right_jsx = ( self._render_component_jsx(right, indent + 4) if isinstance(right, ComponentInstance) else "" ) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces} left={{ {left_jsx} {spaces} }} {spaces} right={{ {right_jsx} {spaces} }} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces} left={{ {left_jsx} {spaces} }} {spaces} right={{ {right_jsx} {spaces} }} {spaces}/>""" else: # vertical top = comp.props.get("top") bottom = comp.props.get("bottom") top_jsx = ( self._render_component_jsx(top, indent + 4) if isinstance(top, ComponentInstance) else "" ) bottom_jsx = ( self._render_component_jsx(bottom, indent + 4) if isinstance(bottom, ComponentInstance) else "" ) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces} top={{ {top_jsx} {spaces} }} {spaces} bottom={{ {bottom_jsx} {spaces} }} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces} top={{ {top_jsx} {spaces} }} {spaces} bottom={{ {bottom_jsx} {spaces} }} {spaces}/>""" # Handle specialized layouts (OverTheShoulder, DialogueFrame, ThreeColumn, ThreeRow, Asymmetric, etc.) elif comp.component_type in [ "OverTheShoulder", "DialogueFrame", "StackedReaction", "HUDStyle", "PerformanceMultiCam", "FocusStrip", "ThreeColumnLayout", "ThreeRowLayout", "AsymmetricLayout", "ThreeByThreeGrid", "PiP", "Vertical", "Timeline", "Mosaic", ]: # Map layout types to their prop keys (using snake_case names to match templates) layout_prop_keys = { "OverTheShoulder": ["screen_content", "shoulder_overlay"], "DialogueFrame": ["left_speaker", "center_content", "right_speaker"], "StackedReaction": ["original_content", "reaction_content"], "HUDStyle": [ "main_content", "top_left", "top_right", "bottom_left", "bottom_right", ], "PerformanceMultiCam": ["primary_cam", "secondary_cams"], "FocusStrip": ["main_content", "focus_content"], "ThreeColumnLayout": ["left", "center", "right"], "ThreeRowLayout": ["top", "middle", "bottom"], "AsymmetricLayout": ["main", "top_side", "bottom_side"], "ThreeByThreeGrid": ["children"], "PiP": ["mainContent", "pipContent"], "Vertical": ["top", "bottom"], "Timeline": ["main_content"], "Mosaic": ["clips"], } prop_keys = layout_prop_keys.get(comp.component_type, []) child_props = [] for key in prop_keys: child = comp.props.get(key) if isinstance(child, ComponentInstance): child_jsx = self._render_component_jsx(child, indent + 4) child_props.append(f"{spaces} {key}={{\n{child_jsx}\n{spaces} }}") elif isinstance(child, list): # Handle array of children (e.g., ThreeByThreeGrid, secondary_cams, clips) children_jsx = [] for child_item in child: if isinstance(child_item, ComponentInstance): # Special handling for Mosaic clips - wrap in {content: ...} object if comp.component_type == "Mosaic" and key == "clips": # Render child inline within the content object child_jsx = self._render_component_jsx(child_item, indent + 6).strip() children_jsx.append(f"{spaces} {{\n{spaces} content: {child_jsx}\n{spaces} }}") else: child_jsx = self._render_component_jsx(child_item, indent + 4) children_jsx.append(child_jsx) children_str = ",\n".join(children_jsx) child_props.append(f"{spaces} {key}={{[\n{children_str}\n{spaces} ]}}") elif child is None: # Only add undefined for optional child props pass # Don't render undefined props children_str = "\n".join(child_props) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {children_str} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {children_str} {spaces}/>""" # Handle frame components (BrowserFrame, DeviceFrame, Terminal) elif comp.component_type in ["BrowserFrame", "DeviceFrame", "Terminal"]: content = comp.props.get("content") if isinstance(content, ComponentInstance): content_jsx = self._render_component_jsx(content, indent + 4) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces} content={{ {content_jsx} {spaces} }} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces} content={{ {content_jsx} {spaces} }} {spaces}/>""" else: # No content, render as simple component return self._render_simple_component(comp, indent) # Handle overlay and animation components with content elif comp.component_type in ["TextOverlay", "LowerThird", "SubscribeButton", "LayoutEntrance"]: content = comp.props.get("content") if isinstance(content, ComponentInstance): content_jsx = self._render_component_jsx(content, indent + 4) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces}> {content_jsx} {spaces}</{comp.component_type}>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces}> {content_jsx} {spaces}</{comp.component_type}>""" else: # No content, render as simple component return self._render_simple_component(comp, indent) # Handle PanelCascade with panels array elif comp.component_type == "PanelCascade": panels = comp.props.get("panels", []) if isinstance(panels, list): panels_jsx = [] for panel in panels: if isinstance(panel, ComponentInstance): panel_jsx = self._render_component_jsx(panel, indent + 4) panels_jsx.append(panel_jsx) panels_str = ",\n".join(panels_jsx) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces} panels={{[ {panels_str} {spaces} ]}} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces} panels={{[ {panels_str} {spaces} ]}} {spaces}/>""" else: return self._render_simple_component(comp, indent) # Handle LayoutTransition with before/after elif comp.component_type == "LayoutTransition": before = comp.props.get("before") after = comp.props.get("after") before_jsx = ( self._render_component_jsx(before, indent + 4) if isinstance(before, ComponentInstance) else "" ) after_jsx = ( self._render_component_jsx(after, indent + 4) if isinstance(after, ComponentInstance) else "" ) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces} before={{ {before_jsx} {spaces} }} {spaces} after={{ {after_jsx} {spaces} }} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces} before={{ {before_jsx} {spaces} }} {spaces} after={{ {after_jsx} {spaces} }} {spaces}/>""" # Handle transition components (PixelTransition) elif comp.component_type == "PixelTransition": first_content = comp.props.get("firstContent") second_content = comp.props.get("secondContent") first_jsx = ( self._render_component_jsx(first_content, indent + 4) if isinstance(first_content, ComponentInstance) else "" ) second_jsx = ( self._render_component_jsx(second_content, indent + 4) if isinstance(second_content, ComponentInstance) else "" ) if props_str: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {props_str} {spaces} firstContent={{ {first_jsx} {spaces} }} {spaces} secondContent={{ {second_jsx} {spaces} }} {spaces}/>""" else: return f"""{spaces}<{comp.component_type} {spaces} startFrame={{{comp.start_frame}}} {spaces} durationInFrames={{{comp.duration_frames}}} {spaces} firstContent={{ {first_jsx} {spaces} }} {spaces} secondContent={{ {second_jsx} {spaces} }} {spaces}/>""" # Fallback to default rendering return self._render_simple_component(comp, indent) def _try_custom_renderer(self, comp: ComponentInstance, indent: int) -> str | None: """Try to use a component-specific custom renderer if available.""" # Check if this component has a custom renderer renderer = self._component_renderers.get(comp.component_type) if renderer: try: return renderer( comp, self._render_component_jsx, indent, snake_to_camel, self._format_prop_value, ) except Exception as e: logger.warning(f"Custom renderer for {comp.component_type} failed: {e}") return None return None def _serialize_value(self, value: Any) -> Any: """Recursively serialize values to JSON-compatible types.""" from dataclasses import asdict, is_dataclass from pydantic import BaseModel if isinstance(value, BaseModel): # Recursively serialize Pydantic models using model_dump try: dumped = value.model_dump(mode="python") # Recursively process the dumped dict in case it contains more BaseModels return self._serialize_value(dumped) except Exception as e: logger.warning(f"Could not serialize {type(value).__name__}: {e}") # Fallback: try to convert to dict manually return str(value) elif is_dataclass(value) and not isinstance(value, type): # Handle dataclasses (like ComponentInstance) dumped = asdict(value) # Recursively process the dumped dict return self._serialize_value(dumped) elif isinstance(value, dict): # Recursively serialize dict values return {k: self._serialize_value(v) for k, v in value.items()} elif isinstance(value, list): # Recursively serialize list items return [self._serialize_value(item) for item in value] elif isinstance(value, tuple): # Convert tuples to lists return [self._serialize_value(item) for item in value] else: # Return primitive types as-is return value def _format_prop_value(self, value: Any) -> str: """Format a prop value for JSX.""" import json if isinstance(value, str): # Use template literals for strings (supports multiline and quotes) # Escape backticks and ${} in the string escaped = value.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${") return "{`" + escaped + "`}" elif isinstance(value, bool): return "{" + str(value).lower() + "}" elif isinstance(value, (int, float)): return "{" + str(value) + "}" elif isinstance(value, (dict, list)) or hasattr(value, "model_dump"): # Serialize complex types (dicts, lists, Pydantic models) serialized = self._serialize_value(value) try: return "{" + json.dumps(serialized) + "}" except TypeError: # Debug: log what failed logger.debug(f"Failed to serialize value of type {type(value)}") logger.debug(f"Serialized type: {type(serialized)}") logger.debug(f"Serialized value: {serialized}") raise else: return f"{{{value}}}" def to_dict(self) -> dict[str, Any]: """ Export composition as dictionary. Returns: Dictionary representation of the composition """ return { "fps": self.fps, "width": self.width, "height": self.height, "theme": self.theme, "duration_frames": self.get_total_duration_frames(), "duration_seconds": self.get_total_duration_seconds(), "components": [ { "type": c.component_type, "start_frame": c.start_frame, "duration_frames": c.duration_frames, "start_time": self.frames_to_seconds(c.start_frame), "duration": self.frames_to_seconds(c.duration_frames), "layer": c.layer, "props": c.props, } for c in self.components ], }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server