Skip to content

feat(publish): FcpxmlExport — editorial timeline hand-off to DaVinci Resolve - #451

Open
prasanthsasikumar wants to merge 2 commits into
calesthio:mainfrom
prasanthsasikumar:feat/fcpxml-editorial-export
Open

feat(publish): FcpxmlExport — editorial timeline hand-off to DaVinci Resolve#451
prasanthsasikumar wants to merge 2 commits into
calesthio:mainfrom
prasanthsasikumar:feat/fcpxml-editorial-export

Conversation

@prasanthsasikumar

Copy link
Copy Markdown
Contributor

Summary

Adds fcpxml_export, a second publish provider alongside export_bundle. Where export_bundle packages a finished render for platform upload, this emits an editable editorial timeline (FCPXML) so a cut can be reopened, graded, and finished in DaVinci Resolve or Final Cut Pro instead of being locked to a flat file.

Resolve imports FCPXML reliably; plain EDL (CMX3600) has no multi-track or transform support, so side-by-side grid scenes can't be represented at all. FCPXML's connected clips (the lane attribute) let simultaneous videos stack on one anchor clip, which is how grid scenes survive the hand-off as separately editable cells.

On the grid transform constants: two attempts at generic Motion-style <adjust-transform> (position + scale) failed to composite in Resolve — a non-uniform scale skewed the video, and a corrected uniform scale sat on an opaque background hiding the layers beneath. The working recipe (<adjust-crop> trim + <adjust-conform type="fit"> + <adjust-transform> with scale always "1 1") was reverse-engineered from a timeline positioned by hand in Resolve and exported back out. Resolve's internal position unit space isn't the 1920x1080 canvas and its conversion isn't publicly documented, so those numbers cannot be re-derived analytically — hence the tests that pin them.

Related issue

No linked issue.

Changes

  • tools/publishers/fcpxml_export.py — the tool, plus a CLI entry point (python -m tools.publishers.fcpxml_export <project_dir>).
  • Sequence frame rate and resolution are probed from the first source clip, with fps/width/height overrides on the tool. They were previously hardcoded to 30 and 1920x1080, which silently retimed 24/25/60fps footage on import and mislabelled vertical sequences.
  • Grid widths beyond the verified 2-up/3-up now raise. Previously a 4-up got no transform at all, so every cell rendered full-frame stacked and only the top one was visible — a timeline that imports cleanly and is silently wrong.
  • All ffprobe failure modes return ToolResult(success=False): missing binary, timeout, and non-zero exit. The last one mattered most — ffprobe still prints {} on a bad file, so the tool used to build a zero-duration asset and report success on a broken timeline.
  • Dolby Vision sources redirected to transcoded SDR copies are reported in ToolResult.data["sdr_substitutions"] (and by the CLI) instead of being swapped invisibly, so an editor knows to relink before grading.
  • Declared in cinematic.yaml's publish stage and documented in the publish-director skill — the registry already discovered the tool, but no manifest offered it, so the documented agent path couldn't reach it.

Testing

  • 15 new tests in tests/tools/test_fcpxml_export.py, written test-first — each fix had a test that failed for the right reason before the code existed.
  • Tests run against real ffmpeg-generated clips (skipped when ffmpeg/ffprobe are unavailable, matching test_video_compose_vertical.py), so the ffprobe paths exercise the real binary rather than a mock. The missing-binary case strips PATH rather than patching.
  • Golden assertions pin the reverse-engineered Resolve constants — positions, crop percentages, scale="1 1", lane structure, and the connected-clip-offset-is-anchor-relative regression.
  • Verified end to end through the CLI: a 24fps 720x1280 source produces frameDuration="1/24s" width="720" height="1280" and duration="48/24s", and the SDR substitution note prints.
  • Full suite: 914 passed, 11 skipped. The 12 failures in tests/contracts/test_phase3_contracts.py are pre-existing and environmental (No module named 'google.genai' locally); they reproduce identically on unmodified main, and CI installs the package via make install-dev.

Known limits, deliberately not hidden: only 2-up and 3-up grids have measured Resolve transforms (wider raises rather than emitting a wrong timeline), and the tool is marked BETA. The publish stage in the other 9 pipeline manifests still has tools_available: [] — that predates this change and felt like a separate concern, so this PR only wires cinematic, where grid scenes live.

Checklist

  • The change is focused on a single logical concern.
  • I ran the relevant tests locally (make test-contracts / make test) where applicable.
  • I updated docs/README if behavior or usage changed.
  • No unrelated files (build artifacts, local config) are included in the diff.

🤖 Generated with Claude Code

prasanthsasikumar and others added 2 commits July 29, 2026 00:50
Promote the standalone tools/video/fcpxml_export.py script to a registered
publish-capability BaseTool (provider "fcpxml") alongside export_bundle, so
it's registry-discoverable, appears in preflight, and is routable. Emits an
FCPXML timeline (grid scenes -> multi-track connected clips) for reopening a
cut in Resolve/FCP. build_fcpxml logic unchanged; CLI preserved via a shared
export_project_to_fcpxml() used by both execute() and __main__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tool worked for the project it was built against but carried
assumptions from it. Six fixes, each with a test written first:

- fps/resolution were hardcoded to 30 and 1920x1080, silently retiming
  24/25/60fps footage and mislabelling vertical sequences. They are now
  probed from the first source clip, with explicit overrides on the tool.
- Grid widths beyond 3 had no verified Resolve transform, so every cell
  got zero adjustments and rendered full-frame stacked — a timeline that
  imports cleanly and is silently wrong. Now raises instead.
- ffprobe failures escaped execute(): a missing binary or a hang raised
  through the contract, and a non-zero exit was worse than that — ffprobe
  still prints '{}', so a bad file produced a zero-duration asset and a
  broken timeline reported as success. All three are ToolResult errors now.
- The Dolby Vision SDR path swap was invisible; substituted filenames are
  reported in ToolResult.data['sdr_substitutions'] and by the CLI.
- Added tests pinning the reverse-engineered Resolve grid constants, which
  cannot be re-derived analytically if someone "simplifies" them.
- Declared the tool in cinematic.yaml's publish stage and documented the
  editorial hand-off in the publish-director skill; the registry already
  discovered it, but no manifest offered it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant