chore: atualização geral
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user