Skip to main content
Glama
create_layouts.py•12.7 kB
#!/usr/bin/env python3 """ Script to generate layout component boilerplate following the new pattern. """ import os import json # Layout definitions LAYOUTS = { "ThreeRowLayout": { "description": "Header + Main + Footer arrangements with configurable heights", "tool_name": "remotion_add_three_row_layout", "props": { "top": {"type": "component", "default": None, "description": "Content for top row"}, "middle": {"type": "component", "default": None, "description": "Content for middle row"}, "bottom": {"type": "component", "default": None, "description": "Content for bottom row"}, "top_height": {"type": "number", "default": 25, "description": "Top row height (percentage)"}, "middle_height": {"type": "number", "default": 50, "description": "Middle row height (percentage)"}, "bottom_height": {"type": "number", "default": 25, "description": "Bottom row height (percentage)"}, "gap": {"type": "number", "default": 20, "description": "Gap between rows (pixels)"}, "padding": {"type": "number", "default": 40, "description": "Padding around layout (pixels)"}, }, "template_type": "three_row" }, "AsymmetricLayout": { "description": "Main feed (2/3) + two demo panels (1/3 stacked) - perfect for tutorials", "tool_name": "remotion_add_asymmetric_layout", "props": { "main": {"type": "component", "default": None, "description": "Main content (left 2/3)"}, "top_side": {"type": "component", "default": None, "description": "Top sidebar content"}, "bottom_side": {"type": "component", "default": None, "description": "Bottom sidebar content"}, "main_width": {"type": "number", "default": 66.67, "description": "Main content width (percentage)"}, "gap": {"type": "number", "default": 20, "description": "Gap between panels (pixels)"}, "padding": {"type": "number", "default": 40, "description": "Padding around layout (pixels)"}, }, "template_type": "asymmetric" }, "PiP": { "description": "Picture-in-Picture webcam overlay with customizable positions", "tool_name": "remotion_add_pip", "props": { "main": {"type": "component", "default": None, "description": "Main content"}, "overlay": {"type": "component", "default": None, "description": "PiP overlay content"}, "position": {"type": "enum", "default": "bottom-right", "values": ["bottom-right", "bottom-left", "top-right", "top-left"], "description": "Overlay position"}, "overlay_size": {"type": "number", "default": 20, "description": "Overlay size (percentage of screen)"}, "margin": {"type": "number", "default": 40, "description": "Margin from edges (pixels)"}, }, "template_type": "pip" }, "Vertical": { "description": "9:16 optimized for Shorts/TikTok/Reels with multiple layout styles", "tool_name": "remotion_add_vertical", "props": { "top": {"type": "component", "default": None, "description": "Top content"}, "bottom": {"type": "component", "default": None, "description": "Bottom content"}, "layout_style": {"type": "enum", "default": "top-bottom", "values": ["top-bottom", "caption-content", "content-caption", "split-vertical"], "description": "Vertical layout style"}, "gap": {"type": "number", "default": 20, "description": "Gap between sections (pixels)"}, "padding": {"type": "number", "default": 40, "description": "Padding around layout (pixels)"}, }, "template_type": "vertical" }, } def create_schema_file(component_name, config): """Generate schema.py file.""" props_class = f"{component_name}Props" # Build Pydantic fields pydantic_fields = [] for prop_name, prop_config in config["props"].items(): prop_type = prop_config["type"] default = prop_config.get("default") description = prop_config["description"] if prop_type == "component": type_hint = "Any | None" field_default = "None" elif prop_type == "number": type_hint = "float | None" field_default = str(default) if default is not None else "None" elif prop_type == "enum": type_hint = "str | None" field_default = f'"{default}"' if default else "None" else: type_hint = "Any | None" field_default = "None" pydantic_fields.append( f' {prop_name}: {type_hint} = Field({field_default}, description="{description}")' ) # Add standard timing fields pydantic_fields.append(' start_time: float = Field(description="When to show (seconds)")') pydantic_fields.append(' duration: float | None = Field(5.0, description="How long to show (seconds)")') content = f'''# chuk-motion/src/chuk_motion/components/layouts/{component_name}/schema.py """{component_name} component schema and Pydantic models.""" from typing import Any from pydantic import BaseModel, Field from ...base import ComponentMetadata class {props_class}(BaseModel): """Properties for {component_name} component.""" {chr(10).join(pydantic_fields)} class Config: extra = "forbid" # Component metadata METADATA = ComponentMetadata( name="{component_name}", description="{config['description']}", category="layout", ) # MCP schema (for backward compatibility with MCP tools list) MCP_SCHEMA = {{ "description": "{config['description']}", "category": "layout", "schema": {{ # Add schema details here }}, }} ''' return content def create_builder_file(component_name, config): """Generate builder.py file.""" # Build parameter list params = ["builder: \"CompositionBuilder\""] for prop_name, prop_config in config["props"].items(): default = prop_config.get("default") if prop_config["type"] == "component": params.append(f'{prop_name}: Any | None = None') elif prop_config["type"] == "number": params.append(f'{prop_name}: float = {default}') elif prop_config["type"] == "enum": params.append(f'{prop_name}: str = "{default}"') params.append("start_time: float") params.append("duration: float = 5.0") # Build props dict props_items = [f'"{k}": {k}' for k in config["props"].keys()] props_items.extend(['"start_time": start_time', '"duration": duration']) content = f'''# chuk-motion/src/chuk_motion/components/layouts/{component_name}/builder.py """{component_name} composition builder method.""" from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from ....generator.composition_builder import CompositionBuilder def add_to_composition( {(",\n ").join(params)} ) -> "CompositionBuilder": """ Add {component_name} to the composition. Returns: CompositionBuilder instance for chaining """ from ....generator.composition_builder import ComponentInstance # Calculate frames start_frame = builder.seconds_to_frames(start_time) duration_frames = builder.seconds_to_frames(duration) component = ComponentInstance( component_type="{component_name}", start_frame=start_frame, duration_frames=duration_frames, props={{ {", ".join(props_items)} }}, layer=0, ) builder.components.append(component) return builder ''' return content def create_tool_file(component_name, config): """Generate tool.py file.""" tool_name = config["tool_name"] # Build parameters params = [] for prop_name, prop_config in config["props"].items(): if prop_config["type"] == "component": params.append(f'{prop_name}: str | None = None') elif prop_config["type"] == "number": default = prop_config.get("default") params.append(f'{prop_name}: float = {default}') elif prop_config["type"] == "enum": default = prop_config.get("default") params.append(f'{prop_name}: str = "{default}"') params.extend([ 'duration: float | str = 5.0', 'track: str = "main"', 'gap_before: float | str | None = None' ]) # Build JSON parsing json_parsing = [] for prop_name, prop_config in config["props"].items(): if prop_config["type"] == "component": json_parsing.append(f' {prop_name}_parsed = json.loads({prop_name}) if {prop_name} else None') # Build props dict props_items = [] for prop_name, prop_config in config["props"].items(): if prop_config["type"] == "component": props_items.append(f' "{prop_name}": {prop_name}_parsed') else: props_items.append(f' "{prop_name}": {prop_name}') content = f'''# chuk-motion/src/chuk_motion/components/layouts/{component_name}/tool.py """{component_name} MCP tool.""" import asyncio import json from chuk_motion.generator.composition_builder import ComponentInstance from chuk_motion.models import ErrorResponse, LayoutComponentResponse def register_tool(mcp, project_manager): """Register the {component_name} tool with the MCP server.""" @mcp.tool async def {tool_name}( {(",\n ").join(params)} ) -> str: """ Add {component_name} to the composition. {config['description']} Returns: JSON with component info """ def _add(): if not project_manager.current_timeline: return ErrorResponse( error="No active project. Create a project first." ).model_dump_json() try: {chr(10).join(json_parsing)} except json.JSONDecodeError as e: return ErrorResponse(error=f"Invalid component JSON: {{str(e)}}").model_dump_json() try: component = ComponentInstance( component_type="{component_name}", start_frame=0, duration_frames=0, props={{ {(",\n").join(props_items)} }}, layer=0, ) component = project_manager.current_timeline.add_component( component, duration=duration, track=track, gap_before=gap_before ) return LayoutComponentResponse( component="{component_name}", layout="custom", start_time=project_manager.current_timeline.frames_to_seconds( component.start_frame ), duration=duration, ).model_dump_json() except Exception as e: return ErrorResponse(error=str(e)).model_dump_json() return await asyncio.get_event_loop().run_in_executor(None, _add) ''' return content def create_init_file(component_name): """Generate __init__.py file.""" props_class = f"{component_name}Props" content = f'''"""{component_name} component.""" from .builder import add_to_composition from .schema import MCP_SCHEMA, METADATA, {props_class} from .tool import register_tool __all__ = [ "METADATA", "MCP_SCHEMA", "{props_class}", "register_tool", "add_to_composition", ] ''' return content def main(): base_path = "src/chuk_motion/components/layouts" for component_name, config in LAYOUTS.items(): component_path = os.path.join(base_path, component_name) os.makedirs(component_path, exist_ok=True) print(f"Creating {component_name}...") # Create schema.py with open(os.path.join(component_path, "schema.py"), "w") as f: f.write(create_schema_file(component_name, config)) # Create builder.py with open(os.path.join(component_path, "builder.py"), "w") as f: f.write(create_builder_file(component_name, config)) # Create tool.py with open(os.path.join(component_path, "builder.py"), "w") as f: f.write(create_tool_file(component_name, config)) # Create __init__.py with open(os.path.join(component_path, "__init__.py"), "w") as f: f.write(create_init_file(component_name)) print(f" āœ“ Created {component_name}") print("\nAll layouts created successfully!") if __name__ == "__main__": main()

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