help quicky produce instagram reels and youtube shorts
80
93%
Does it follow best practices?
Impact
47%
1.95xAverage score across 1 eval scenario
Passed
No known issues
#!/usr/bin/env python3
"""Export cut_plan.json as an FCPXML timeline for manual editing in an NLE.
Opens in both Final Cut Pro (File > Import > XML) and DaVinci Resolve
(File > Import Timeline). Every segment lands as a clip pre-positioned at the
right source timecode; segment notes become clip names so the pacing phases
stay visible on the timeline. Music is attached as a connected audio clip.
Version notes (why the default is 1.10):
- Resolve rejects fcpxml versions newer than it understands (1.12+ from
modern FCP is the classic failure). Resolve 18+ imports 1.10; use
--fcpxml-version 1.9 for Resolve 17.
- Final Cut imports older versions and upgrades them silently.
- Flat .fcpxml only — Resolve cannot read .fcpxmld bundles.
All times are frame-quantized (cut boundaries rounded cumulatively, so error
never exceeds half a frame) — NLEs require times on frame boundaries, and the
renderer quantizes to the same 30fps grid anyway.
Usage:
export_timeline.py plan/cut_plan.json --out plan/reel.fcpxml [--name my-reel]
Round trip: edit in the NLE, export fcpxml (from Resolve: version 1.9/1.10),
then import_timeline.py it back into a cut plan.
"""
import argparse, json, subprocess, sys
import xml.etree.ElementTree as ET
from pathlib import Path
FPS = 30 # mezzanine grid — everything upstream is CFR 30
W, H = 1080, 1920
def rt(frames): # rational time on the frame grid, e.g. 28 -> "2800/3000s"
return f"{frames * 100}/{FPS * 100}s"
def media_duration(path):
r = subprocess.run(["ffprobe", "-v", "quiet", "-show_entries",
"format=duration", "-of", "csv=p=0", str(path)],
capture_output=True, text=True)
if r.returncode != 0 or not r.stdout.strip():
sys.exit(f"ffprobe failed on {path} — do the mezzanine files exist?")
return float(r.stdout.strip())
def main():
ap = argparse.ArgumentParser()
ap.add_argument("plan")
ap.add_argument("--out", default="plan/reel.fcpxml")
ap.add_argument("--name", help="Project name in the NLE (default: plan folder name)")
ap.add_argument("--fcpxml-version", default="1.10", choices=["1.9", "1.10"],
help="1.10 for Resolve 18+/FCP (default); 1.9 for Resolve 17")
args = ap.parse_args()
plan = json.loads(Path(args.plan).read_text())
segs = plan["segments"]
name = args.name or Path(args.plan).resolve().parent.parent.name
# frame-quantize cut boundaries cumulatively (no drift accumulation)
bounds, acc, prev = [0], 0.0, 0
for s in segs:
acc += s["duration"]
prev = max(prev + 1, round(acc * FPS))
bounds.append(prev)
frame_durs = [b - a for a, b in zip(bounds, bounds[1:])]
total_frames = bounds[-1]
root = ET.Element("fcpxml", version=args.fcpxml_version)
res = ET.SubElement(root, "resources")
ET.SubElement(res, "format", id="r1", frameDuration=rt(1),
width=str(W), height=str(H),
colorSpace="1-1-1 (Rec. 709)")
# one asset per unique clip; in-points clamped to real media length
assets, next_id = {}, 2
def asset_for(path, has_video):
nonlocal next_id
p = Path(path).resolve()
if p in assets:
return assets[p]
dur = media_duration(p)
rid = f"r{next_id}"
next_id += 1
a = ET.SubElement(res, "asset", id=rid, name=p.stem, start="0s",
duration=rt(int(dur * FPS)),
hasVideo="1" if has_video else "0", hasAudio="1",
audioSources="1", audioChannels="2", audioRate="48000")
if has_video:
a.set("format", "r1")
ET.SubElement(a, "media-rep", kind="original-media", src=p.as_uri())
assets[p] = (rid, dur)
return assets[p]
lib = ET.SubElement(root, "library")
event = ET.SubElement(lib, "event", name="Reel Builder")
project = ET.SubElement(event, "project", name=name)
seq = ET.SubElement(project, "sequence", format="r1",
duration=rt(total_frames), tcStart="0s", tcFormat="NDF",
audioLayout="stereo", audioRate="48k")
spine = ET.SubElement(seq, "spine")
first_clip_el, first_in_frames = None, 0
for k, s in enumerate(segs):
rid, media_dur = asset_for(s["clip"], has_video=True)
max_in = max(0, int(media_dur * FPS) - frame_durs[k])
in_frames = min(max(0, round(s["in"] * FPS)), max_in)
el = ET.SubElement(spine, "asset-clip", ref=rid,
name=s.get("note") or Path(s["clip"]).stem,
offset=rt(bounds[k]), start=rt(in_frames),
duration=rt(frame_durs[k]),
format="r1", tcFormat="NDF")
if k == 0:
first_clip_el, first_in_frames = el, in_frames
# music: connected clip on lane -1, anchored to the head of the first clip
# (offset of a connected clip is in the parent's source time, so anchoring
# at the parent's start places it at timeline zero)
if plan.get("music"):
rid, music_dur = asset_for(plan["music"], has_video=False)
m_start = round(plan.get("music_start", 0.0) * FPS)
m_len = min(total_frames, int(music_dur * FPS) - m_start)
ET.SubElement(first_clip_el, "asset-clip", ref=rid, lane="-1",
name=Path(plan["music"]).stem,
offset=rt(first_in_frames), start=rt(m_start),
duration=rt(max(1, m_len)))
ET.indent(root)
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text('<?xml version="1.0" encoding="UTF-8"?>\n'
"<!DOCTYPE fcpxml>\n" + ET.tostring(root, encoding="unicode") + "\n")
print(f"Timeline: {len(segs)} clips, {total_frames / FPS:.2f}s -> {out} "
f"(fcpxml {args.fcpxml_version})")
print(" Final Cut Pro: File > Import > XML")
print(" DaVinci Resolve: File > Import Timeline (check 'import source clips')")
print("After manual edits, export fcpxml from the NLE (Resolve: v1.9/1.10) "
"and run import_timeline.py to bring the cuts back into the plan.")
if __name__ == "__main__":
main()