Files
gart/code/server_tools/export.py
T

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