272 lines
12 KiB
Python
272 lines
12 KiB
Python
"""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,
|
|
}
|