"""Geração — tool schemas and handlers. Extracted from server.py; see Engine/docs/03_SERVER_TOOLS.md for the tool catalog. """ from __future__ import annotations from typing import Sequence from mcp.types import TextContent, Tool from fcpxml.models import SegmentSpec from fcpxml.templates import ClipSpec, apply_template, list_templates from server_tools._shared import ( PROJECTS_DIR, _setup_generator, _text_result, _validate_output_path, format_duration, ) TOOLS = [ Tool( name="auto_rough_cut", description="Generate a rough cut from source clips based on keywords, duration, and pacing", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Source FCPXML with clips"}, "output_path": {"type": "string", "description": "Where to save rough cut"}, "target_duration": {"type": "string", "description": "Target length (3m, 00:03:00:00)"}, "pacing": {"type": "string", "enum": ["slow", "medium", "fast", "dynamic"], "default": "medium"}, "keywords": {"type": "array", "items": {"type": "string"}, "description": "Filter clips by keywords"}, "segments": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "keywords": {"type": "array", "items": {"type": "string"}}, "duration": {"type": "number"} } }, "description": "Segment structure [{name, keywords, duration_seconds}]" }, "priority": {"type": "string", "enum": ["best", "favorites", "longest", "shortest", "random"], "default": "best"}, "favorites_only": {"type": "boolean", "default": False}, "add_transitions": {"type": "boolean", "default": False} }, "required": ["filepath", "output_path", "target_duration"] } ), Tool( name="generate_montage", description="Create rapid-fire montages with pacing curves (accelerating, decelerating, pyramid)", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Source FCPXML with clips"}, "output_path": {"type": "string", "description": "Where to save montage"}, "target_duration": {"type": "string", "description": "Total montage length (e.g., '30s', '00:00:30:00')"}, "pacing_curve": {"type": "string", "enum": ["accelerating", "decelerating", "pyramid", "constant"], "default": "accelerating", "description": "How clip duration changes over time"}, "start_duration": {"type": "number", "default": 2.0, "description": "Clip duration at start (seconds)"}, "end_duration": {"type": "number", "default": 0.5, "description": "Clip duration at end (seconds)"}, "keywords": {"type": "array", "items": {"type": "string"}, "description": "Filter clips by keywords"}, "add_transitions": {"type": "boolean", "default": False, "description": "Add quick dissolves"} }, "required": ["filepath", "output_path", "target_duration"] } ), Tool( name="generate_ab_roll", description="Create documentary-style A/B roll edits alternating between main content and cutaways", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Source FCPXML with clips"}, "output_path": {"type": "string", "description": "Where to save A/B roll edit"}, "target_duration": {"type": "string", "description": "Total duration (e.g., '3m', '00:03:00:00')"}, "a_keywords": {"type": "array", "items": {"type": "string"}, "description": "Keywords for A-roll (main content, interviews)"}, "b_keywords": {"type": "array", "items": {"type": "string"}, "description": "Keywords for B-roll (cutaways, visuals)"}, "a_duration": {"type": "string", "default": "5s", "description": "Duration of each A-roll segment"}, "b_duration": {"type": "string", "default": "3s", "description": "Duration of each B-roll cutaway"}, "start_with": {"type": "string", "enum": ["a", "b"], "default": "a", "description": "Which roll to start with"}, "add_transitions": {"type": "boolean", "default": True, "description": "Add cross-dissolves"} }, "required": ["filepath", "output_path", "target_duration", "a_keywords", "b_keywords"] } ), Tool( name="list_templates", description="List available timeline templates with slot definitions", inputSchema={ "type": "object", "properties": {}, } ), Tool( name="apply_template", description="Fill a timeline template with clips and generate FCPXML", inputSchema={ "type": "object", "properties": { "template_name": {"type": "string", "description": "Template name (intro_outro, lower_thirds, music_video)"}, "clips": {"type": "object", "description": "Map of slot_name -> {src, name, duration} or {asset_id, name, duration}"}, "output_path": {"type": "string", "description": "Output FCPXML path"}, "fps": {"type": "number", "description": "Frame rate", "default": 24}, }, "required": ["template_name", "clips", "output_path"] } ), ] async def handle_auto_rough_cut(arguments: dict) -> Sequence[TextContent]: filepath, output_path, generator = _setup_generator(arguments, "_roughcut") segments = None if arguments.get("segments"): segments = [ SegmentSpec( name=s.get("name", "Segment"), keywords=s.get("keywords", []), duration_seconds=s.get("duration", 0), priority=s.get("priority", "best"), ) for s in arguments["segments"] ] result = generator.generate( output_path=output_path, target_duration=arguments["target_duration"], pacing=arguments.get("pacing", "medium"), keywords=arguments.get("keywords"), segments=segments, priority=arguments.get("priority", "best"), favorites_only=arguments.get("favorites_only", False), add_transitions=arguments.get("add_transitions", False), ) return _text_result(f"""# Rough Cut Generated ## Summary - **Clips Used**: {result.clips_used} of {result.clips_available} available - **Target Duration**: {format_duration(result.target_duration)} - **Actual Duration**: {format_duration(result.actual_duration)} - **Average Clip**: {format_duration(result.average_clip_duration)} ## Output Saved to: `{result.output_path}` **Next step**: Import this FCPXML into Final Cut Pro (File > Import > XML) """) async def handle_generate_montage(arguments: dict) -> Sequence[TextContent]: filepath, output_path, generator = _setup_generator(arguments, "_montage") result = generator.generate_montage( output_path=output_path, target_duration=arguments["target_duration"], pacing_curve=arguments.get("pacing_curve", "accelerating"), start_duration=arguments.get("start_duration", 2.0), end_duration=arguments.get("end_duration", 0.5), keywords=arguments.get("keywords"), add_transitions=arguments.get("add_transitions", False), ) curve_desc = { 'accelerating': 'slow to fast (builds energy)', 'decelerating': 'fast to slow (winds down)', 'pyramid': 'slow to fast to slow (dramatic arc)', 'constant': 'same duration throughout', } return _text_result(f"""# Montage Generated ## Summary - **Clips Used**: {result['clips_used']} of {result['clips_available']} available - **Target Duration**: {format_duration(result['target_duration'])} - **Actual Duration**: {format_duration(result['actual_duration'])} - **Pacing Curve**: {result['pacing_curve']} - {curve_desc.get(result['pacing_curve'], '')} ## Pacing - **Start Clip Duration**: {format_duration(result['start_clip_duration'])} - **End Clip Duration**: {format_duration(result['end_clip_duration'])} ## Output Saved to: `{result['output_path']}` """) async def handle_generate_ab_roll(arguments: dict) -> Sequence[TextContent]: filepath, output_path, generator = _setup_generator(arguments, "_ab_roll") result = generator.generate_ab_roll( output_path=output_path, target_duration=arguments["target_duration"], a_keywords=arguments["a_keywords"], b_keywords=arguments["b_keywords"], a_duration=arguments.get("a_duration", "5s"), b_duration=arguments.get("b_duration", "3s"), start_with=arguments.get("start_with", "a"), add_transitions=arguments.get("add_transitions", True), ) return _text_result(f"""# A/B Roll Edit Generated ## Summary - **A-Roll Segments**: {result['a_segments']} (from {result['a_clips_available']} available) - **B-Roll Segments**: {result['b_segments']} (from {result['b_clips_available']} available) - **Total Clips**: {result['clips_used']} ## Timing - **Target Duration**: {format_duration(result['target_duration'])} - **Actual Duration**: {format_duration(result['actual_duration'])} - **A-Roll Duration**: {result['a_duration_setting']} per segment - **B-Roll Duration**: {result['b_duration_setting']} per cutaway ## Output Saved to: `{result['output_path']}` **Next step**: Import this FCPXML into Final Cut Pro (File > Import > XML) """) async def handle_list_templates(arguments: dict) -> Sequence[TextContent]: templates = list_templates() lines = ["# Available Timeline Templates\n"] for tmpl in templates: lines.append(f"## {tmpl['name']}") lines.append(f"{tmpl['description']}\n") lines.append("| Slot | Type | Default Duration | Lane | Required |") lines.append("|------|------|-----------------|------|----------|") for s in tmpl['slots']: lines.append( f"| {s['name']} | {s['slot_type']} | {s['default_duration']}s " f"| {s['lane']} | {'Yes' if s['required'] else 'No'} |" ) lines.append("") return _text_result("\n".join(lines)) async def handle_apply_template(arguments: dict) -> Sequence[TextContent]: template_name = arguments["template_name"] clips_raw = arguments["clips"] output_path = _validate_output_path(arguments["output_path"], anchor_dir=PROJECTS_DIR) fps = arguments.get("fps", 24) # Convert raw clips dict to ClipSpec objects clips_map = {} for slot_name, spec_data in clips_raw.items(): if isinstance(spec_data, dict): clips_map[slot_name] = ClipSpec( asset_id=spec_data.get("asset_id"), src=spec_data.get("src"), name=spec_data.get("name", slot_name), duration=spec_data.get("duration"), ) result_path = apply_template(template_name, clips_map, output_path, fps) return _text_result(( f"Applied template '{template_name}' with {len(clips_map)} clips.\n" f"Saved to: `{result_path}`" )) HANDLERS = { "auto_rough_cut": handle_auto_rough_cut, "generate_montage": handle_generate_montage, "generate_ab_roll": handle_generate_ab_roll, "list_templates": handle_list_templates, "apply_template": handle_apply_template, }