Files

134 lines
5.0 KiB
Python

"""Roles — 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 server_tools._shared import (
_require_timeline,
_setup_modifier,
_text_result,
format_duration,
)
TOOLS = [
Tool(
name="assign_role",
description="Set the audio or video role on a clip (dialogue, music, effects, titles, etc.)",
inputSchema={
"type": "object",
"properties": {
"filepath": {"type": "string", "description": "Path to FCPXML file"},
"clip_id": {"type": "string", "description": "Clip name or ID"},
"audio_role": {"type": "string", "description": "Audio role (e.g., dialogue, music, effects)"},
"video_role": {"type": "string", "description": "Video role (e.g., video, titles)"},
"output_path": {"type": "string", "description": "Output path (default: adds _modified suffix)"}
},
"required": ["filepath", "clip_id"]
}
),
Tool(
name="filter_by_role",
description="List all clips matching a specific audio or video role",
inputSchema={
"type": "object",
"properties": {
"filepath": {"type": "string", "description": "Path to FCPXML file"},
"role": {"type": "string", "description": "Role name to filter by"},
"role_type": {"type": "string", "enum": ["audio", "video", "any"], "default": "any", "description": "Which role type to search"},
},
"required": ["filepath", "role"]
}
),
Tool(
name="export_role_stems",
description="Export clip list grouped by role for audio mixing stem planning",
inputSchema={
"type": "object",
"properties": {
"filepath": {"type": "string", "description": "Path to FCPXML file"},
},
"required": ["filepath"]
}
),
]
async def handle_assign_role(arguments: dict) -> Sequence[TextContent]:
filepath, output_path, modifier = _setup_modifier(arguments)
modifier.assign_role(
clip_id=arguments["clip_id"],
audio_role=arguments.get("audio_role"),
video_role=arguments.get("video_role"),
)
modifier.save(output_path)
roles_set = []
if arguments.get("audio_role"):
roles_set.append(f"audioRole={arguments['audio_role']}")
if arguments.get("video_role"):
roles_set.append(f"videoRole={arguments['video_role']}")
return _text_result((
f"Set {', '.join(roles_set)} on '{arguments['clip_id']}'\n\n"
f"Saved to: `{output_path}`"
))
async def handle_filter_by_role(arguments: dict) -> Sequence[TextContent]:
project, tl = _require_timeline(arguments["filepath"])
role = arguments["role"].lower()
role_type = arguments.get("role_type", "any")
matches = []
for clip in tl.clips:
if role_type in ("audio", "any") and clip.audio_role.lower() == role:
matches.append((clip.name, "audio", clip.audio_role, format_duration(clip.duration_seconds)))
if role_type in ("video", "any") and clip.video_role.lower() == role:
matches.append((clip.name, "video", clip.video_role, format_duration(clip.duration_seconds)))
if not matches:
return _text_result(f"No clips found with role '{role}'.")
result = f"# Clips with role '{role}'\n\n"
result += "| Clip | Type | Role | Duration |\n|------|------|------|----------|\n"
for name, rtype, rval, dur in matches:
result += f"| {name} | {rtype} | {rval} | {dur} |\n"
return _text_result(result)
async def handle_export_role_stems(arguments: dict) -> Sequence[TextContent]:
project, tl = _require_timeline(arguments["filepath"])
stems: dict[str, list] = {}
for clip in tl.clips:
role = clip.audio_role or "unassigned"
stems.setdefault(role, []).append(clip)
for cc in tl.connected_clips:
role = cc.role or "unassigned"
stems.setdefault(role, []).append(cc)
result = f"# Audio Stem Plan for {tl.name}\n\n"
for role, clips in sorted(stems.items()):
total_dur = sum(c.duration_seconds for c in clips)
result += f"## {role.title()} ({len(clips)} clips, {format_duration(total_dur)})\n\n"
for c in clips:
result += f"- {c.name} ({format_duration(c.duration_seconds)})\n"
result += "\n"
return _text_result(result)
HANDLERS = {
"assign_role": handle_assign_role,
"filter_by_role": handle_filter_by_role,
"export_role_stems": handle_export_role_stems,
}