Turn a finished layered edit into a CapCut desktop draft where an editor can grab any element — nudge a caption, swap a card, mute one SFX hit — instead of receiving a baked MP4.
Honest status up front: the draft generation works and is well understood. The "modern CapCut opens it" step is a documented work-in-progress. pyJianYingDraft writes JianYing-era drafts; current CapCut desktop expects a newer schema. Before the patch described below, CapCut refused the draft with "Couldn't use project: unusual path". The patch is implemented in the script but has not been field-verified end to end on a current CapCut build. Treat the last mile as debugging work, not a guaranteed one-shot (see Debugging — the method is a diff against a native draft).
Prerequisites
- macOS with CapCut desktop installed (drafts live in
~/Movies/CapCut/User Data/Projects/com.lveditor.draft/). python3with pyJianYingDraft in a venv (pip install pyJianYingDraft). Do not install globally; the library moves fast and its API varies between versions.ffmpegon PATH (orFFMPEGenv var) — used to split the cards render pass and grab a cover thumbnail.- Optional: PIL (
pip install pillow) if you want the procedural vignette PNG. - The finished edit's render passes (see next section). Rendering those is the edit skill's job, not this skill's.
Inputs — what the edit pipeline must hand you
| Input | What it is |
|---|---|
| Base footage file(s) | The talking-head / footage video (already reframed to canvas aspect) |
| Cards-only pass | A render of the composition with only the graphic cards visible (opaque background is fine — cards are stacked as opaque clips) |
| Alpha fx pass | Everything that must composite over footage with transparency (progress bar, light leaks). Render with --format mov → true-alpha ProRes, yuva444p. An MP4 cannot carry alpha |
| Word-level transcript | JSON [{"text","start","end"},...] for native captions |
| VO / music / SFX files | The exact audio used in the edit, one file per SFX hit name |
| The edit's timing data | Zoom boundaries, card in/out times, SFX hit list, caption style beats — copied into the script's CONFIG block |
Workflow
- Copy the template
scripts/export-capcut-template.pyinto the edit's project folder and fill in the CONFIG block (durations, cards, hits, caption windows). - Stage media to the local disk. The script does this automatically into
~/Movies/capcut-export-media(override withCAPCUT_MEDIA_DIR). This is not optional tidiness: media referenced by a draft from an external volume can trip macOS permission (TCC) prompts inside CapCut and break media resolution. Keep every referenced file on the boot disk. - Build the draft:
.venv/bin/python export-capcut-template.py— writes the draft to~/Movies/capcut-draft-staging/<name>/. - Patch the schema (see below): rerun with
--skip-build --patch-native <path-to-a-native-draft-folder>. - Quit CapCut completely (Cmd+Q), then
--skip-build --install --register. - Open CapCut. If the draft opens: verify tracks, done. If not: Debugging.
The schema gap (read this before blaming your CONFIG)
pyJianYingDraft targets the JianYing (Chinese CapCut) draft format:
- writes
draft_content.jsonwithnew_version: "110.0.0".
Current CapCut desktop (international) loads draft_info.json with
new_version ≈ "181.0.0" and 8 extra top-level keys that the old schema
lacks:
draft_type, function_assistant_info, is_drop_frame_timecode, lyrics_effects,
mixed_track_mode_on, path, smart_ads_info, uneven_animation_template_info
The fix implemented in --patch-native:
- On the target machine, create any throwaway project in CapCut natively (this gives you a draft written by that exact CapCut build).
- Clone those 8 keys from the native draft into the generated draft; copy the native
new_version/versionfields; setpathto the draft's final installed folder. - Write the result as BOTH filenames —
draft_content.jsonanddraft_info.json— so either loader path finds it.
Status, plainly: before this patch CapCut showed "Couldn't use project: unusual path"; the patched draft was not yet verified to open on the CapCut build that produced the error. Expect to iterate via the diff method below.
Draft installation and the registry
Copying the folder into com.lveditor.draft/ is not enough — CapCut only lists drafts
that are registered in root_meta_info.json (top-level key all_draft_store).
Rules the script enforces / you must respect:
- Back up
root_meta_info.jsonfirst (script writes a timestamped.bak-*). - Only edit the registry while CapCut is fully quit — CapCut rewrites it on exit
and will clobber (or crash on) concurrent edits. The script refuses to run
--install/--registerwhile a CapCut process exists. - New entries are made by cloning a native entry and fixing:
draft_name, every path field containing the old name, a fresh uppercase-UUIDdraft_id,tm_duration(microseconds), timestamps, anddraft_cover(the script grabs a frame with ffmpeg asdraft_cover.jpg).
Track layout — mapping a layered edit onto CapCut tracks
Bottom → top:
| Track | Content | Technique |
|---|---|---|
footage |
Base video | Split at every zoom boundary; each segment gets its own scale_x/scale_y in ClipSettings — that's how a punch-in stays editable |
overlay |
Positioned second video (e.g. a head-card band during split-screen beats) | ClipSettings(scale_x, scale_y, transform_y). Negative transform_y = down, positive = up (e.g. -0.5625 parks a 0.9556-scale band in the lower half of 9:16) |
cards |
One opaque clip per graphic card | Render a cards-only pass of the composition, then ffmpeg-split it at card boundaries (-crf 16 -g 15 keeps cuts frame-accurate). Editors can retime/swap individual cards |
fx |
ONE alpha overlay for the whole timeline | The true-alpha ProRes .mov. Don't split it — it's a single ambient layer |
image-N |
Static PNGs (vignette etc.) | PNG placed as image segments over specific windows |
captions |
Native TextSegments |
TextStyle(size, bold, italic, color), TextBorder(width, color), position via ClipSettings(transform_y=...). Fully editable text in CapCut |
text-N |
CTAs / keyword pops | Same as captions, on extra tracks |
vo, music, sfx-N |
Audio | One clip per SFX hit so each hit is individually mutable/movable |
Text-track hard rules
- A track cannot hold overlapping segments. Two texts on screen at once must go on
separate tracks — the script packs extra texts greedily onto
text-1,text-2, … - Clamp caption windows strictly monotonic. Word-level timestamps from a
transcriber routinely overlap; the script forces
start ≥ prev_end + 2msandend ≤ next_start − 40msbefore creating segments. Skip this andadd_segmentthrows (or worse, CapCut mis-renders).
pyJianYingDraft API gotchas
- Clamp every segment duration to the material duration —
d = min(d, material.duration/1e6 - source_start)— or the library throws.material.durationis in microseconds. VideoSegment(material, target_timerange, source_timerange=…, volume=…, clip_settings=…)—source_timerangeselects which slice of the file plays; omit it for stills/full clips.TextStyle(italic=…)doesn't exist in every version — wrap intry/except TypeErrorand degrade to non-italic (the script does).- Time ranges are built with
trange("12.5s", "0.8s")(start, duration).
Debugging
When CapCut refuses or mangles the draft, the method is a diff against a native draft made by that exact CapCut install:
- In CapCut, create a minimal native project: one video clip, one text, one audio. Quit CapCut.
- Pretty-print both drafts and diff:
python3 -m json.tool <native>/draft_info.json > a.json(same for yours), then diff top-level keys first, then per-track/per-segment structure. - Fix the highest-level differences first (missing keys, version strings,
path), reinstall, retest. Iterate downward into track/material structure only if the draft opens but elements are broken. - Registry problems look different from schema problems: a draft that never appears in the project list = registry; a draft that appears but errors on open = schema.
- If CapCut "repairs" your draft on open, immediately copy the repaired
draft_info.jsonout and diff it against what you wrote — CapCut just told you the correct schema.
Scripts
scripts/export-capcut-template.py— the full exporter template. CONFIG block at top;--patch-native,--install,--register,--skip-buildflags. Read its docstring before running.