Skip to main content
Glama
project_manager.py27.6 kB
""" Project Manager - Creates and manages Remotion projects. Handles project scaffolding, file generation, and project state. """ import logging import shutil from pathlib import Path from typing import Any from jinja2 import Template from ..generator.component_builder import ComponentBuilder from ..generator.composition_builder import ComponentInstance from ..generator.timeline import Timeline logger = logging.getLogger(__name__) class ProjectManager: """Manages Remotion video projects.""" def __init__(self, workspace_dir: Path | None = None): """ Initialize project manager. Args: workspace_dir: Directory for projects (default: ./remotion-projects) """ if workspace_dir is None: workspace_dir = Path.cwd() / "remotion-projects" self.workspace_dir = Path(workspace_dir) self.workspace_dir.mkdir(exist_ok=True, parents=True) self.component_builder = ComponentBuilder() self.current_project: str | None = None self.current_timeline: Timeline | None = None self.current_composition = None def create_project( self, name: str, theme: str = "tech", fps: int = 30, width: int = 1920, height: int = 1080 ) -> dict[str, str]: """ Create a new Remotion project. Args: name: Project name theme: Theme to use fps: Frames per second width: Video width height: Video height Returns: Dictionary with project info """ project_dir = self.workspace_dir / name if project_dir.exists(): raise ValueError(f"Project '{name}' already exists") # Create project structure project_dir.mkdir(parents=True) (project_dir / "src").mkdir() (project_dir / "src" / "components").mkdir() # Copy template files template_dir = Path(__file__).parent.parent.parent.parent / "remotion-templates" # Copy package.json self._copy_template( template_dir / "package.json", project_dir / "package.json", {"project_name": name} ) # Copy config files shutil.copy(template_dir / "remotion.config.ts", project_dir / "remotion.config.ts") shutil.copy(template_dir / "tsconfig.json", project_dir / "tsconfig.json") shutil.copy(template_dir / ".gitignore", project_dir / ".gitignore") # Copy source files # Remotion composition IDs can only contain a-z, A-Z, 0-9, and hyphens composition_id = name.replace("_", "-") self._copy_template( template_dir / "src" / "Root.tsx", project_dir / "src" / "Root.tsx", { "composition_id": composition_id, "duration_in_frames": 300, # 10 seconds at 30fps "fps": fps, "width": width, "height": height, "theme": theme, }, ) shutil.copy(template_dir / "src" / "index.ts", project_dir / "src" / "index.ts") # Create timeline (track-based system) self.current_project = name self.current_timeline = Timeline(fps=fps, width=width, height=height, theme=theme) return { "name": name, "path": str(project_dir), "theme": theme, "fps": str(fps), "resolution": f"{width}x{height}", } def _copy_template(self, src: Path, dest: Path, variables: dict[str, Any]): """Copy a template file and replace variables.""" if not src.exists(): # Create empty file if template doesn't exist dest.write_text("") return content = src.read_text() # Use custom delimiters [[ ]] to avoid JSX {} conflicts template = Template( content, variable_start_string="[[", variable_end_string="]]", block_start_string="[%", block_end_string="%]", ) rendered = template.render(**variables) dest.write_text(rendered) def add_component_to_project( self, component_type: str, config: dict, theme: str = "tech" ) -> str: """ Generate and add a component to the current project. Args: component_type: Type of component (TitleScene, LowerThird, etc.) config: Component configuration theme: Theme to use Returns: Path to generated component file """ if not self.current_project: raise ValueError("No active project. Create a project first.") project_dir = self.workspace_dir / self.current_project components_dir = project_dir / "src" / "components" # Generate component code tsx_code = self.component_builder.build_component(component_type, config, theme) # Write component file component_file = components_dir / f"{component_type}.tsx" component_file.write_text(tsx_code) return str(component_file) def generate_composition(self) -> str: """ Generate the complete video composition from the timeline or composition builder. Returns: Path to generated VideoComposition.tsx file """ if not self.current_project: raise ValueError("No active project") # Support both Timeline and CompositionBuilder # Prefer CompositionBuilder if it exists and has components if ( self.current_composition and hasattr(self.current_composition, "components") and self.current_composition.components ): builder = self.current_composition elif self.current_timeline: builder = self.current_timeline else: raise ValueError("No timeline or composition created") composition_tsx = builder.generate_composition_tsx() duration_frames = builder.get_total_duration_frames() # Ensure minimum duration of 10 seconds (300 frames at 30fps) for empty timelines if duration_frames == 0: duration_frames = 300 fps = builder.fps width = builder.width height = builder.height theme = getattr(builder, "theme", "tech") project_dir = self.workspace_dir / self.current_project components_dir = project_dir / "src" / "components" # Generate component TSX files for all unique component types # Handle both Timeline (tracks-based) and CompositionBuilder (components list) if hasattr(builder, "get_all_components"): # Timeline has get_all_components() method all_components = builder.get_all_components() # Recursively find nested component types component_types = self._find_all_component_types_recursive(all_components) elif hasattr(builder, "components"): # CompositionBuilder has components attribute component_types = builder._find_all_component_types(builder.components) # type: ignore[attr-defined] else: component_types = set() for component_type in component_types: try: # Generate component code using the component builder tsx_code = self.component_builder.build_component( component_type, {}, # Empty config - templates handle props from VideoComposition theme, ) # Write component file component_file = components_dir / f"{component_type}.tsx" component_file.write_text(tsx_code) logger.debug(f"Generated {component_type}.tsx") except Exception as e: logger.warning(f"Could not generate {component_type}: {e}") # Write composition file composition_file = project_dir / "src" / "VideoComposition.tsx" composition_file.write_text(composition_tsx) # Update Root.tsx with correct duration root_file = project_dir / "src" / "Root.tsx" # Remotion composition IDs can only contain a-z, A-Z, 0-9, and hyphens composition_id = self.current_project.replace("_", "-") self._copy_template( Path(__file__).parent.parent.parent.parent / "remotion-templates" / "src" / "Root.tsx", root_file, { "composition_id": composition_id, "duration_in_frames": duration_frames, "fps": fps, "width": width, "height": height, "theme": theme, }, ) return str(composition_file) def _find_all_component_types_recursive(self, components: list) -> set: """Recursively find all component types including nested children.""" types = set() def collect_types(comp): types.add(comp.component_type) # Check for nested children in props for key, value in comp.props.items(): if isinstance(value, ComponentInstance): collect_types(value) elif isinstance(value, list): for item in value: if isinstance(item, ComponentInstance): collect_types(item) for comp in components: collect_types(comp) return types def get_project_info(self) -> dict: """Get information about the current project.""" if not self.current_project: return {"error": "No active project"} if not self.current_timeline: return {"error": "No timeline"} return { "name": self.current_project, "path": str(self.workspace_dir / self.current_project), "composition": self.current_timeline.to_dict(), } def list_projects(self) -> list: """List all projects in the workspace.""" if not self.workspace_dir.exists(): return [] projects = [] for project_dir in self.workspace_dir.iterdir(): if project_dir.is_dir() and (project_dir / "package.json").exists(): projects.append({"name": project_dir.name, "path": str(project_dir)}) return projects def build_composition_from_scenes(self, scenes: list, theme: str = "tech") -> dict[str, Any]: """ Build a complete composition from scene configurations. This method takes a list of scene dictionaries and: 1. Converts them to ComponentInstance objects 2. Generates TSX files for each component type 3. Builds the final VideoComposition.tsx Args: scenes: List of scene dictionaries with type, config, startFrame, durationInFrames theme: Theme to use for generation Returns: Dictionary with paths to generated files Example scene format: { "type": "TitleScene", "config": {"title": "Hello", "subtitle": "World"}, "startFrame": 0, "durationInFrames": 90 } """ if not self.current_project or not self.current_timeline: raise ValueError("No active project. Create a project first.") project_dir = self.workspace_dir / self.current_project components_dir = project_dir / "src" / "components" # Track unique component types that need TSX files component_types_needed = set() generated_files = [] # Process each scene for scene in scenes: scene_type = scene.get("type") scene_config = scene.get("config", {}) start_frame = scene.get("startFrame", 0) duration_frames = scene.get("durationInFrames", 90) # Track this component type component_types_needed.add(scene_type) # Create ComponentInstance and add to composition component_instance = ComponentInstance( component_type=scene_type, start_frame=start_frame, duration_frames=duration_frames, props=scene_config, layer=0, # Main content layer ) # Handle nested children recursively self._process_nested_children(scene, component_instance, component_types_needed) # Add to main track (legacy scenes don't specify tracks) self.current_timeline.tracks["main"].components.append(component_instance) # Generate TSX files for all unique component types for component_type in component_types_needed: try: # Generate component code tsx_code = self.component_builder.build_component( component_type, {}, # Empty config - templates handle props from VideoComposition theme, ) # Write component file component_file = components_dir / f"{component_type}.tsx" component_file.write_text(tsx_code) generated_files.append(str(component_file)) except Exception as e: logger.warning(f"Could not generate {component_type}: {e}") # Generate the main VideoComposition.tsx composition_file = self.generate_composition() generated_files.append(composition_file) return { "project": self.current_project, "composition_file": composition_file, "component_files": generated_files, "component_types": list(component_types_needed), "total_frames": self.current_timeline.get_total_duration_frames(), } def _process_nested_children( self, scene: dict, component_instance: "ComponentInstance", component_types_needed: set ): """ Process nested children in layout components. Args: scene: Scene dictionary that may contain nested children component_instance: ComponentInstance to update with child components component_types_needed: Set to track component types for TSX generation """ from ..generator.composition_builder import ComponentInstance # Handle different types of nested structures # Special handling for SplitScreen children - map array to left/right or top/bottom if "children" in scene and scene.get("type") == "SplitScreen": children = scene["children"] if isinstance(children, list) and len(children) >= 2: orientation = scene.get("config", {}).get("orientation", "horizontal") # Map children to left/right or top/bottom based on orientation keys = ["left", "right"] if orientation == "horizontal" else ["top", "bottom"] for i, key in enumerate(keys): if i < len(children): child = children[i] if isinstance(child, dict) and "type" in child: component_types_needed.add(child["type"]) child_instance = ComponentInstance( component_type=child["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child.get("config", {}), layer=5, ) component_instance.props[key] = child_instance self._process_nested_children( child, child_instance, component_types_needed ) # Handle specialized layout children with specific prop keys elif "children" in scene and scene.get("type") in [ "ThreeColumnLayout", "ThreeRowLayout", "AsymmetricLayout", "PiP", "Mosaic", "OverTheShoulder", "DialogueFrame", "StackedReaction", "HUDStyle", "PerformanceMultiCam", "FocusStrip", "ThreeByThreeGrid", "Vertical", "Timeline", "BrowserFrame", "DeviceFrame", "Terminal", ]: # Map layout types to their prop keys layout_prop_keys = { "ThreeColumnLayout": ["left", "center", "right"], "ThreeRowLayout": ["top", "middle", "bottom"], "AsymmetricLayout": ["main", "top_side", "bottom_side"], "PiP": ["mainContent", "pipContent"], "Mosaic": ["clips"], "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"], "ThreeByThreeGrid": ["children"], "Vertical": ["top", "bottom"], "Timeline": ["main_content"], "BrowserFrame": ["content"], "DeviceFrame": ["content"], "Terminal": ["content"], } children = scene["children"] layout_type = scene.get("type", "") prop_keys = layout_prop_keys.get(layout_type, []) if isinstance(children, list): # For array props like clips, secondary_cams, children - collect all into a list if len(prop_keys) == 1 and prop_keys[0] in ["clips", "secondary_cams", "children"]: child_instances = [] for child in children: if isinstance(child, dict) and "type" in child: component_types_needed.add(child["type"]) child_instance = ComponentInstance( component_type=child["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child.get("config", {}), layer=5, ) child_instances.append(child_instance) self._process_nested_children( child, child_instance, component_types_needed ) if child_instances: # Only set if we have children component_instance.props[prop_keys[0]] = child_instances else: # For named props like left/center/right - map by index for i, key in enumerate(prop_keys): if i < len(children): child = children[i] if isinstance(child, dict) and "type" in child: component_types_needed.add(child["type"]) child_instance = ComponentInstance( component_type=child["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child.get("config", {}), layer=5, ) component_instance.props[key] = child_instance self._process_nested_children( child, child_instance, component_types_needed ) # Grid and Container children (array or single) elif "children" in scene: children = scene["children"] if isinstance(children, list): child_instances = [] for child in children: if isinstance(child, dict) and "type" in child: component_types_needed.add(child["type"]) child_instance = ComponentInstance( component_type=child["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child.get("config", {}), layer=5, ) child_instances.append(child_instance) # Recursively process nested children self._process_nested_children(child, child_instance, component_types_needed) component_instance.props["children"] = child_instances elif isinstance(children, dict) and "type" in children: component_types_needed.add(children["type"]) child_instance = ComponentInstance( component_type=children["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=children.get("config", {}), layer=5, ) component_instance.props["children"] = child_instance self._process_nested_children(children, child_instance, component_types_needed) # SplitScreen left/right/top/bottom for key in ["left", "right", "top", "bottom"]: if key in scene: child = scene[key] if isinstance(child, dict) and "type" in child: component_types_needed.add(child["type"]) child_instance = ComponentInstance( component_type=child["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child.get("config", {}), layer=5, ) component_instance.props[key] = child_instance self._process_nested_children(child, child_instance, component_types_needed) # Specialized layout components (using snake_case names to match templates) specialized_keys = [ # AsymmetricLayout "main", "top_side", "bottom_side", # ThreeColumnLayout / ThreeRowLayout "center", "middle", # 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 "milestones", "clips", # Container "content", # PixelTransition "firstContent", "secondContent", ] for key in specialized_keys: # Check both scene level and config level (for components like PixelTransition) child = None if key in scene: child = scene[key] elif key in scene.get("config", {}): child = scene["config"][key] if child is not None: # Handle single child component if isinstance(child, dict) and "type" in child: component_types_needed.add(child["type"]) child_instance = ComponentInstance( component_type=child["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child.get("config", {}), layer=10 if key == "overlay" else 5, ) component_instance.props[key] = child_instance self._process_nested_children(child, child_instance, component_types_needed) # Handle array of child components (e.g., secondary_cams, clips) elif isinstance(child, list): # Skip if already processed as ComponentInstances by the specialized layouts branch above if child and isinstance(child[0], ComponentInstance): continue child_instances = [] for child_item in child: # Handle direct component: {type: "DemoBox", config: {...}} if isinstance(child_item, dict) and "type" in child_item: component_types_needed.add(child_item["type"]) child_inst = ComponentInstance( component_type=child_item["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=child_item.get("config", {}), layer=5, ) child_instances.append(child_inst) self._process_nested_children( child_item, child_inst, component_types_needed ) # Handle Mosaic clips structure: {content: {type: "DemoBox", config: {...}}} elif isinstance(child_item, dict) and "content" in child_item: content = child_item["content"] if isinstance(content, dict) and "type" in content: component_types_needed.add(content["type"]) child_inst = ComponentInstance( component_type=content["type"], start_frame=scene.get("startFrame", 0), duration_frames=scene.get("durationInFrames", 90), props=content.get("config", {}), layer=5, ) child_instances.append(child_inst) self._process_nested_children( content, child_inst, component_types_needed ) component_instance.props[key] = child_instances

Implementation Reference

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