186 lines
7.6 KiB
Python
186 lines
7.6 KiB
Python
"""Export / relink — 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.export import DaVinciExporter
|
|
from fcpxml.writer import FCPXMLModifier
|
|
from server_tools._shared import (
|
|
_require_timeline,
|
|
_resolve_io_paths,
|
|
_setup_modifier,
|
|
_text_result,
|
|
_validate_filepath,
|
|
format_timecode,
|
|
)
|
|
|
|
TOOLS = [
|
|
Tool(
|
|
name="export_edl",
|
|
description="Generate EDL (Edit Decision List) from timeline",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {"filepath": {"type": "string"}},
|
|
"required": ["filepath"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="export_csv",
|
|
description="Export timeline data to CSV format",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"filepath": {"type": "string"},
|
|
"include": {"type": "array", "items": {"type": "string"}}
|
|
},
|
|
"required": ["filepath"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="export_resolve_xml",
|
|
description="Export timeline as DaVinci Resolve compatible FCPXML (simplified v1.9)",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"filepath": {"type": "string", "description": "Path to FCPXML file"},
|
|
"flatten_compounds": {"type": "boolean", "default": True, "description": "Flatten compound clips for compatibility"},
|
|
"output_path": {"type": "string", "description": "Output path (default: adds _resolve suffix)"},
|
|
},
|
|
"required": ["filepath"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="export_fcp7_xml",
|
|
description="Export timeline as FCP7 XML (XMEML) for Premiere Pro, DaVinci Resolve, and Avid compatibility",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"filepath": {"type": "string", "description": "Path to FCPXML file"},
|
|
"output_path": {"type": "string", "description": "Output path (default: adds _fcp7.xml suffix)"},
|
|
},
|
|
"required": ["filepath"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="relink_media",
|
|
description="Bulk-rewrite media source paths (asset/media-rep src URLs) to relink moved or renamed media folders without opening FCP. Prefix-based: find='/Volumes/OldDrive/Media' replace='/Volumes/NewDrive/Media'. Use dry_run to preview.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"filepath": {"type": "string", "description": "Path to FCPXML file or .fcpxmld bundle"},
|
|
"find": {"type": "string", "description": "Old path prefix to match (plain path or file:// URL)"},
|
|
"replace": {"type": "string", "description": "New path prefix to substitute"},
|
|
"dry_run": {"type": "boolean", "description": "Preview changes without writing", "default": False},
|
|
"output_path": {"type": "string", "description": "Output path (default: adds _relinked suffix)"},
|
|
},
|
|
"required": ["filepath", "find", "replace"]
|
|
}
|
|
),
|
|
]
|
|
|
|
|
|
async def handle_export_edl(arguments: dict) -> Sequence[TextContent]:
|
|
project, tl = _require_timeline(arguments["filepath"])
|
|
edl = f"TITLE: {tl.name}\nFCM: NON-DROP FRAME\n\n"
|
|
for i, c in enumerate(tl.clips, 1):
|
|
edl += f"{i:03d} AX V C {format_timecode(c.source_start)} {format_timecode(c.end)} {format_timecode(c.start)} {format_timecode(c.end)}\n"
|
|
edl += f"* FROM CLIP NAME: {c.name}\n\n"
|
|
return _text_result(f"```edl\n{edl}```")
|
|
|
|
|
|
async def handle_export_csv(arguments: dict) -> Sequence[TextContent]:
|
|
project, tl = _require_timeline(arguments["filepath"])
|
|
csv = "Name,Start,End,Duration,Keywords\n"
|
|
for c in tl.clips:
|
|
kws = "|".join(k.value for k in c.keywords)
|
|
csv += f'"{c.name}",{format_timecode(c.start)},{format_timecode(c.end)},{c.duration_seconds:.3f},"{kws}"\n'
|
|
return _text_result(f"```csv\n{csv}```")
|
|
|
|
|
|
async def handle_export_resolve_xml(arguments: dict) -> Sequence[TextContent]:
|
|
filepath, output_path = _resolve_io_paths(arguments, "_resolve")
|
|
exporter = DaVinciExporter(filepath)
|
|
exporter.export_simplified_fcpxml(
|
|
output_path,
|
|
flatten_compounds=arguments.get("flatten_compounds", True),
|
|
)
|
|
return _text_result((
|
|
f"# Exported for DaVinci Resolve\n\n"
|
|
f"- **Format**: Simplified FCPXML v1.9\n"
|
|
f"- **Compound clips flattened**: {arguments.get('flatten_compounds', True)}\n\n"
|
|
f"Saved to: `{output_path}`\n\n"
|
|
f"**Next step**: In DaVinci Resolve, go to File > Import > Timeline > Import AAF/EDL/XML"
|
|
))
|
|
|
|
|
|
async def handle_export_fcp7_xml(arguments: dict) -> Sequence[TextContent]:
|
|
filepath, output_path = _resolve_io_paths(arguments, "_fcp7")
|
|
exporter = DaVinciExporter(filepath)
|
|
exporter.export_xmeml(output_path)
|
|
return _text_result((
|
|
f"# Exported as FCP7 XML (XMEML)\n\n"
|
|
f"- **Format**: XMEML v5\n"
|
|
f"- **Compatible with**: Premiere Pro, DaVinci Resolve, Avid Media Composer\n\n"
|
|
f"Saved to: `{output_path}`\n\n"
|
|
f"**Next step**: Import via File > Import in your target NLE"
|
|
))
|
|
|
|
|
|
async def handle_relink_media(arguments: dict) -> Sequence[TextContent]:
|
|
dry_run = arguments.get("dry_run", False)
|
|
if dry_run:
|
|
filepath = _validate_filepath(arguments["filepath"], ('.fcpxml', '.fcpxmld'))
|
|
modifier = FCPXMLModifier(filepath)
|
|
result = modifier.relink_media(
|
|
arguments["find"], arguments["replace"], dry_run=True
|
|
)
|
|
footer = "Dry run — no file written."
|
|
else:
|
|
filepath, output_path, modifier = _setup_modifier(arguments, "_relinked")
|
|
result = modifier.relink_media(arguments["find"], arguments["replace"])
|
|
saved = modifier.save(output_path)
|
|
footer = f"Saved to: {saved}"
|
|
|
|
if not result["relinked"]:
|
|
return _text_result(
|
|
f"No media paths matched prefix '{arguments['find']}' "
|
|
f"({result['total_assets']} assets scanned). Nothing to relink."
|
|
)
|
|
|
|
lines = [
|
|
f"{'Would relink' if dry_run else 'Relinked'} "
|
|
f"{result['relinked']} media reference(s) "
|
|
f"across {result['total_assets']} asset(s):",
|
|
"",
|
|
]
|
|
missing = 0
|
|
for change in result["changes"]:
|
|
mark = "✓" if change["target_exists"] else "⚠ target missing"
|
|
if not change["target_exists"]:
|
|
missing += 1
|
|
lines.append(f" {change['asset']}: {change['new']} [{mark}]")
|
|
if missing:
|
|
lines.append("")
|
|
lines.append(
|
|
f"⚠ {missing} new path(s) do not exist on this machine — "
|
|
f"FCP will show those clips as missing until the media is present."
|
|
)
|
|
lines.append("")
|
|
lines.append(footer)
|
|
return _text_result("\n".join(lines))
|
|
|
|
|
|
HANDLERS = {
|
|
"export_edl": handle_export_edl,
|
|
"export_csv": handle_export_csv,
|
|
"export_resolve_xml": handle_export_resolve_xml,
|
|
"export_fcp7_xml": handle_export_fcp7_xml,
|
|
"relink_media": handle_relink_media,
|
|
}
|