134 lines
5.0 KiB
Python
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,
|
|
}
|