CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/reels-producer-skill

help quicky produce instagram reels and youtube shorts

80

1.95x
Quality

93%

Does it follow best practices?

Impact

47%

1.95x

Average score across 1 eval scenario

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

export_timeline.pyskills/reel-builder/scripts/

#!/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()

tile.json