Files
gart/code/server_tools/generation.py
T

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,
}