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

import_timeline.pyskills/reel-builder/scripts/

#!/usr/bin/env python3
"""Import an FCPXML timeline back into cut_plan.json (round trip from an NLE).

Reads fcpxml exported by Final Cut Pro or DaVinci Resolve after manual edits
and rebuilds the plan: spine clip order/in-points/durations become segments,
a connected audio clip (lane != 0) becomes the music track. Accepts flat
.fcpxml files and .fcpxmld bundles (FCP 11's default export).

Only cuts survive the round trip: clip order, source in-points, durations,
music offset. NLE-side effects, transitions, retimes, and titles are ignored
with a warning — apply looks via --grade at render time instead.

Usage:
  import_timeline.py plan/reel_edited.fcpxml --out plan/cut_plan.json \
      [--base-plan plan/cut_plan.json]   # inherit music/bpm if timeline lacks them
"""
import argparse, json, sys
import xml.etree.ElementTree as ET
from pathlib import Path
from urllib.parse import unquote, urlparse

IGNORED = {"transition", "title", "video", "audio", "spine"}  # non asset-clip spine items we warn on


def secs(v):
    """Parse fcpxml rational time: '2800/3000s' | '8s' | '1.5s' -> float seconds."""
    if not v:
        return 0.0
    v = v.rstrip("s")
    if "/" in v:
        n, d = v.split("/")
        return float(n) / float(d)
    return float(v)


def uri_to_path(src):
    if src.startswith("file:"):
        p = unquote(urlparse(src).path)
        return p
    return src


def relativize(path):
    try:
        return str(Path(path).resolve().relative_to(Path.cwd()))
    except ValueError:
        return path


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("fcpxml", help=".fcpxml file or .fcpxmld bundle")
    ap.add_argument("--out", default="plan/cut_plan.json")
    ap.add_argument("--base-plan",
                    help="Existing plan to inherit music/music_start/bpm from "
                         "when the timeline doesn't carry them")
    args = ap.parse_args()

    src = Path(args.fcpxml)
    if src.is_dir():  # .fcpxmld bundle
        inner = next(iter(src.glob("*.fcpxml")), None)
        if not inner:
            sys.exit(f"No .fcpxml inside bundle {src}")
        src = inner
    root = ET.parse(src).getroot()

    # asset id -> media path (media-rep child, or src attr in older exports)
    asset_path = {}
    for a in root.iter("asset"):
        rep = a.find("media-rep")
        loc = (rep.get("src") if rep is not None else None) or a.get("src")
        if loc:
            asset_path[a.get("id")] = uri_to_path(loc)

    spine = next(root.iter("spine"), None)
    if spine is None:
        sys.exit("No <spine> found — is this a timeline export?")

    segments, music, music_start, warnings = [], None, 0.0, []
    for el in spine:
        tag = el.tag
        if tag == "gap":
            warnings.append(f"gap at offset {el.get('offset')} dropped — the "
                            "renderer butts segments together; re-time in the plan if intended")
            continue
        if tag not in ("asset-clip", "clip"):
            warnings.append(f"<{tag}> in spine ignored (only cuts survive the round trip)")
            continue
        ref = el.get("ref")
        if ref is None and tag == "clip":  # FCP sometimes nests the ref
            inner = next((c for c in el.iter() if c.get("ref")), None)
            ref = inner.get("ref") if inner is not None else None
        path = asset_path.get(ref)
        if not path:
            warnings.append(f"clip '{el.get('name')}' has no resolvable media — skipped")
            continue
        segments.append({
            "clip": relativize(path),
            "in": round(secs(el.get("start")), 3),
            "duration": round(secs(el.get("duration")), 3),
            "note": el.get("name", ""),
        })
        # connected clips: first audio-only lane is the music
        for child in el:
            if child.tag == "asset-clip" and child.get("lane") and music is None:
                mp = asset_path.get(child.get("ref"))
                if mp:
                    music = relativize(mp)
                    music_start = round(secs(child.get("start")), 3)

    if not segments:
        sys.exit("No clips found in the spine.")

    base = {}
    if args.base_plan and Path(args.base_plan).exists():
        base = json.loads(Path(args.base_plan).read_text())
    plan = {
        "music": music or base.get("music", ""),
        "music_start": music_start if music else base.get("music_start", 0.0),
        **({"bpm": base["bpm"]} if "bpm" in base else {}),
        "total_duration": round(sum(s["duration"] for s in segments), 2),
        "segments": segments,
    }
    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    if out.exists():
        backup = out.with_suffix(".json.bak")
        backup.write_text(out.read_text())
        print(f"Existing plan backed up to {backup}")
    out.write_text(json.dumps(plan, indent=2))

    print(f"Imported {len(segments)} segments, {plan['total_duration']:.1f}s -> {out}")
    if not plan["music"]:
        print("  NOTE: no music track found in the timeline — set \"music\" in the "
              "plan or pass --base-plan.")
    for w in warnings:
        print(f"  WARNING: {w}")
    print("Render a preview + check_cuts.py before showing the human.")


if __name__ == "__main__":
    main()

tile.json