Historical implementation plan through v0.3. v0.11 additionally ships optional ffprobe metadata/duration detection, a local FFmpeg proxy renderer, compile v2, and reliability features documented in the changelog. “Out of scope” below now means no mandatory runtime/server or cloud final renderer.
Source of reference features: sun-guannan/VectCutAPI (formerly CapCutAPI), Python HTTP/MCP server wrapping pyJianYingDraft. Local clone for reference lives at ../CapCutAPI/ (fork: ashreo/CapCutAPI).
Guiding principles
- capcut-cli stays zero-dep, local-only, JSON-by-default, pipeable.
- Ports are pure JSON writes — no new runtime, no network, no Python.
- Each phase is a self-contained PR. Fixtures under
test/per new command. - CapCut vs JianYing enum namespaces handled via a
--jianyingflag (default: CapCut).
Legend
[ ]not started ·[~]in progress ·[x]done
Current skill is a single SKILL.md (119 lines). Adopt sun-guannan's vectcut-skill directory structure (not its server-coupled content) so each phase's additions land in a predictable place and future Claudes don't miss capabilities.
Meta-rule — pattern-first, one layer at a time. Determinism is the top priority across all videos and all tools. Nail one pattern completely (script + tested + used on a real video) before starting the next. Viewers fall in love with patterns unconsciously; inconsistency breaks that. No parallel half-finished wrappers. See memory: feedback_pattern_first_production.md.
Core principle — deterministic scripts over Claude-chained commands.
Every recipe that involves more than one CLI call, any arithmetic, or any branching gets a parameterised shell script. Claude invokes the script with inputs; the script runs a fixed sequence of capcut commands. No intermediate reasoning. If a recipe can be written as a script, it must be a script — not a "first run X, then run Y" narrative in workflows.md. workflows.md documents which script to call and why, not how to reconstruct it.
Target layout:
capcut-cli/skills/capcut-edit/
├── SKILL.md # orientation + pointers only; short
├── references/
│ ├── api-reference.md # every command, every flag, every value format
│ ├── workflows.md # recipes = "call scripts/X.sh with these args"
│ ├── pitfalls.md # close-project-first, .bak, clip=null on audio, etc.
│ └── enums.md # (populated when Phase 3 ships)
├── scripts/ # deterministic shell wrappers — the "client library"
│ ├── fade-in.sh # <project> <id> <duration> — writes alpha 0→1
│ ├── fade-out.sh # <project> <id> <duration> — writes alpha 1→0
│ ├── ken-burns.sh # <project> <id> <start-scale> <end-scale> <tx-start> <tx-end> <ty-start> <ty-end> <duration>
│ ├── long-to-short.sh # <project> <start> <end> <out> <title> <cta> — cut + titles
│ ├── stamp-cta.sh # <project> <template.json> <start> <duration> <text>
│ └── apply-subtitle-srt.sh # <project> <srt-path> [--style-ref <id>] — once Phase 3 ships
└── assets/examples/ # raw data snippets only — no logic
├── fade-in.jsonl # keyframe batch (for users who want to hand-edit)
└── subscribe-cta.json # reusable text template
Rule: scripts/*.sh are the only moving parts. If Claude needs to do math or a loop, that math/loop lives in the script, not the skill prose.
- Restructure skill directory — create
references/,scripts/,assets/examples/. -
SKILL.mdtrim — keep frontmatter, project locations, progressive-disclosure pattern,-H/-q/--batchconventions, the deterministic-scripts principle, and pointers intoreferences/+scripts/. Drop exhaustive flag lists. -
references/api-reference.md— one section per command; every flag with type, default, example; time-format table; ID-prefix rule;.bakinvariant. -
references/workflows.md— recipes named byscripts/X.sh: when to reach for fade-in vs fade-out, Ken Burns param guidance, long→short guidance. Each recipe starts with "Runscripts/<name>.sh <args>" — not a multi-step narrative. Coversanim.sh(9-slug table),ken-burns.sh,long-to-short.sh,stamp-cta.sh. -
references/pitfalls.md— close-project-first,.bakfiles,clip=nullon audio segments,source_timerangemath when usingspeed, Windows vs macOS filename differences. -
scripts/fade-in.sh— thin wrapper aroundscripts/anim.sh fade-in. Logic consolidated inanim.sh+assets/animations.jsoncatalogue of 9 animation slugs (fade-in, fade-out, flash-in, pulsing-zooms, scroll-up, stripe-merge, zoom-out, blur-out, smoke — all discovered in use on knossos-recon except Fade Out which came from CapCutAPI metadata).anim.shwrites amaterials.material_animationsentry matching CapCut's native shape, attaches its id to the segment'sextra_material_refs, and guards against double-applying an intro or outro. Verified end-to-end on knossos-recon segmentc1ce4d65(lightwell-shaft, 0:16–0:19) and fixture. NOT alpha-keyframe-based — that approach was tried first and CapCut ignored the keyframes on both video and text segments. Follow-up: promoteanim.sh's JSON mutation into a propercapcut animCLI command in Phase 1. -
scripts/fade-out.sh— thin wrapper aroundscripts/anim.sh fade-out.anim.shauto-anchors outros atstart = target_duration - durationso the fade finishes at the cut. Uses CapCut video "Fade Out" effect (effect_id 6798320902548230669, from CapCutAPI metadata — not yet visually verified in CapCut because knossos-recon hasn't used Fade Out; the effect will be fetched on first open). -
scripts/ken-burns.sh—<project> <id> <start-scale> <end-scale> <tx-start> <tx-end> <ty-start> <ty-end> <duration>→ batchcapcut keyframe --batchwith uniform_scale + position_x + position_y. Six keyframes; covered in_test.sh. -
scripts/long-to-short.sh—<project> <start> <end> <out> <title> <cta>→capcut cut→capcut add-textintro →capcut add-textCTA. Covered in_test.sh. -
scripts/stamp-cta.sh—<project> <template.json> <start> <duration> <text>→capcut apply-templatewith text override. Covered in_test.sh. -
assets/examples/ken-burns.jsonl+subscribe-cta.json— raw snippets for users who want to hand-edit; not the primary path. (Replacedfade-in.jsonlwithken-burns.jsonlbecause fade-in no longer uses keyframes — see pitfalls.md.subscribe-cta.jsonproduced by runningcapcut save-templateon a CLI-generated CTA text segment.) - Add keyframe coverage to
SKILL.md— already shipped in Phase 1; skill must name the command + point atscripts/fade-in.shandscripts/ken-burns.sh. SKILL.md scripts block listsken-burns.shwith the note that motion-property keyframes render in CapCut (alpha doesn't — see pitfalls.md). - Test all scripts — each
scripts/*.shmust run end-to-end againsttest/draft_content.jsonwithout error; add ascripts/_test.shrunner that exercises every wrapper. Runner ships atscripts/_test.sh; 7/7 tests pass covering fade-in, fade-out, anim (flash-in + smoke), ken-burns, stamp-cta, long-to-short. - Acceptance rule (documented in this file): every subsequent phase PR must (a) update
references/api-reference.md, (b) if the phase enables a recipe, add ascripts/*.shwrapper (not a workflow narrative), and (c) add the script toscripts/_test.sh.
Reference: ../CapCutAPI/add_video_keyframe_impl.py, add_video_track.py, add_text_impl.py.
-
capcut keyframe— add keyframe(s) to a segment- Single:
capcut keyframe <project> <id> <property> <time> <value> - Batch:
capcut keyframe <project> <id> --batch(JSONL on stdin:{"property","time","value"}per line) - Properties:
position_x,position_y,rotation,scale_x,scale_y,uniform_scale,alpha,saturation,contrast,brightness,volume. - Value parsing rules copied from
add_video_keyframe_impl.py:131("50%","+0.5","45deg", etc.). - Writes to
common_keyframeson the segment. Appends to existing per-property list on re-invocation; sorted bytime_offset. - Implementation:
src/decorators.ts; dispatched fromsrc/index.ts.
- Single:
-
capcut transition— attach transition to a video/image segmentcapcut transition <project> <id> <slug> [--duration <s>]- Default duration comes from the per-slug metadata. Starter catalogue of 8 slugs in
src/decorators.ts(dissolve / rgb-glitch / radial-blur / horizontal-blur / vertical-blur-ii / twinkle-zoom / urban-glitch / shake-3). Covered in_test.sh. Phase 3 will replace the inline catalogue with a generatedenums.json.
-
capcut mask— attach mask to video/image segmentcapcut mask <project> <id> <slug>where slug ∈linear|mirror|circle|rectangle|heart|star- Flags:
--center-x,--center-y,--size,--rotation,--feather,--invert,--rect-width(rect only),--round-corner(rect only, 0–100). capcut mask <project> <id> --offremoves all masks.- Writes to
materials.common_mask. Refuses stacking. Covered in_test.sh.
-
capcut bg-blur— background blur levelcapcut bg-blur <project> <id> <1|2|3|4>(light → maximum).capcut bg-blur <project> <id> --off.- Writes
materials.canvases[].type = canvas_blurwith values0.0625 / 0.375 / 0.75 / 1.0. Replaces any existing canvas reference on the segment. Covered in_test.sh.
-
capcut text-style— rich text styling on an existing text segment- Flags shipped:
--alpha,--vertical,--fixed-width,--fixed-height,--shadow/--no-shadow(+--shadow-alpha/--shadow-angle/--shadow-color/--shadow-distance/--shadow-smoothing),--border-width/--border-color/--border-alpha,--bg-color/--bg-alpha/--bg-style/--bg-round-radius/--bg-width/--bg-height/--bg-h-offset/--bg-v-offset. Covered in_test.sh. - Deferred to Phase 4:
--bubble-effect-id/--bubble-resource-idand--effect-id(花字) require extra text-material types that aren't in the starter catalogue.
- Flags shipped:
-
capcut text-anim— text intro/outro animationscapcut text-anim <project> <id> [--intro <slug>] [--outro <slug>] [--intro-duration <s>] [--outro-duration <s>]- Validated against starter catalogue in
src/decorators.ts(fade-in / fade-out / typewriter / pop-up / throw-out / blur-text-in / zoom-in-text). Extends the segment'ssticker_animationcontainer withmaterial_type: "text"animations. Covered in_test.sh.
-
Implementation scaffolding
src/decorators.ts— addedaddTransition,addMask,setBgBlur,setTextStyle,addTextAnim, plustransitionSlugs/maskSlugs/textAnimSlugs.- New flag parsing in
src/index.ts: 35 Phase 1 flags (mask geometry, shadow, border, bg box, anim slugs/durations). - 5 new subcommand handlers + dispatch cases in
src/index.ts; HELP text updated. - Smoke tests in
scripts/_test.sh— 5 new tests pass. Per the Phase 0 acceptance rule (PLAN:61), every new command has anapi-reference.mdsection and a_test.shentry. No dedicatedscripts/*.shwrappers shipped yet — reconsider per command during real use; wrap only when a recipe emerges that needs locking (e.g. default transition slugs per video series).
Reference: ../CapCutAPI/add_sticker_impl.py, add_effect_impl.py, add_image_impl.py.
-
capcut add-sticker— sticker segment + sticker trackcapcut add-sticker <project> <resource-id> <start> <duration> [--x/--y/--scale/--rotation/--track-name]- Creates sticker track on demand; sticker material at
materials.stickers; transforms applied viaseg.clip. Covered in_test.sh. - Deferred intro/outro flags: use
capcut image-anim(or Phase 4's sticker-anim if slug catalogue adds sticker-specific intros) — the genericimage-animalready supports sticker segments since the animation shape is the same.
-
capcut add-effect— scene / character effect track segmentcapcut add-effect <project> <slug> <start> <duration> [--params <json-array>] [--track-name <name>]- Starter slug catalogue of 7 scene effects (shake / vhs / cinematic / light-leak / film-grain / chromatic / vignette) in
src/factory.ts.apply_target_type=2(global track scope). Creates effect track on demand. Material atmaterials.video_effects. Covered in_test.sh. - Deferred
--jianyingflag → Phase 4 (requires the JianYing enum namespace split).
-
capcut image-anim— image intro/outro/combo animationscapcut image-anim <project> <id> [--intro <slug>] [--outro <slug>] [--combo <slug>] [--intro-duration/--outro-duration/--combo-duration]- Shares the
sticker_animationcontainer shape withtext-animbut writesmaterial_type: "video". Starter slug catalogue insrc/decorators.tsmatchesskills/capcut-edit/assets/animations.json(fade-in / flash-in / pulsing-zooms / scroll-up / stripe-merge / zoom-out / fade-out / blur-out / smoke). Combo slugs not yet in catalogue (deferred to Phase 3 extraction). Covered in_test.sh.
-
Companion-materials update
createCompanionMaterialsinsrc/factory.tsnow accepts"sticker"(shares the video companion set: speeds/placeholder/scm/vocal + canvas + material_color) and"effect"(returns empty — effect segments don't use companions). Sticker + effect round-trips covered in_test.sh(track + material + segment created, verified via JSON asserts).
Reference: ../CapCutAPI/add_subtitle_impl.py and the /get_*_types endpoints in capcut_server.py.
-
capcut import-srt— parse SRT → per-cue text segmentscapcut import-srt <project> <srt-path-or-->(supports-for stdin; file path; not URL — keep local-only).- Flags:
--track-name(defaultsubtitle),--style-ref <segment-id>(copy styling from existing text segment — copies full material style fields +content.styles[0]fill), or explicit text-style flags (--font-size/--color/--align/--x/--y/--alpha/--vertical/--shadow/--border-*/--bg-*) matchingcapcut text-style. - Flag:
--time-offset <s>to shift all cues; fails fast if shift drops cue 1 below 0us. - Zero-dep SRT parser in
src/srt.ts. Accepts.or,as ms separator; index lines optional. - Single
saveDraftfor the whole file (fast on hundreds of cues).
-
capcut enums— list valid enum values for agents- Subcommand flags wired:
--transitions,--masks,--text-intros,--text-outros,--text-loop-anims,--image-intros,--image-outros,--image-combos,--scene-effects,--character-effects,--fonts,--audio-effects. - Respects
--jianyingflag (separate enum set). Namespacecapcutis default. - Output: JSON array by default (
slug,member,name/title, effect/resource ids, md5, durations),-Htable (slug / name / member). - No project argument — pure lookup against the committed JSON.
- Subcommand flags wired:
-
Enum extraction build step
scripts/extract-enums.py— one-shot script: importspyJianYingDraftfrom../CapCutAPI, dumps 13 enum categories × 2 namespaces tosrc/enums.json(committed; ~790KB).npm run builddoestsc && cp src/enums.json dist/enums.jsonso the runtime reads the dist copy viaimport.meta.url.npm run extract-enumsconvenience script.- Inline
TRANSITIONS/MASKS/TEXT_ANIMSstarter maps removed (values matched upstream exactly);IMAGE_ANIMSandVIDEO_EFFECTSkept inline because their empirically-harvested knossos-reconeffect_ids differ from the upstream metadata. Those two categories: inline wins, enums.json is fallback for any unknown slug.
-
capcut text-ranges— multi-style text (different styling per char range)capcut text-ranges <project> <id> --styles @styles.json(also accepts inline JSON).styles.json:[{"start":0,"end":5,"font_color":"#FFD700","font_size":18,"font_alpha":1.0,"bold":true},…].start/endare JS string code-unit indices; translated to UTF-16LE byte offsets to match existingcontent.styles[i].rangeformat.- Writer sorts + validates non-overlap, then emits one style per range plus baseline-style fillers for any gaps so CapCut renders the whole text.
- Unlocks word-level-highlight captions (big win for funshorts VO).
-
--jianyingglobal flag- Threaded through
transition,mask,text-anim,image-anim,add-effect, andenums. Each decorator function takes an optionalnamespaceparam (defaultcapcut). - JianYing transitions / masks mostly carry Chinese Python identifiers → slug is
"". CLI looks up bymembertoo, socapcut transition <project> <id> "_3D空间" --jianyingworks. addEffectskips its inline knossos catalogue when--jianyingis set, since those effect_ids are CapCut-specific.
- Threaded through
-
Skill + docs update
SKILL.md— replaced the "once Phase 3 ships" placeholder with an Enum-discovery bullet listing every--<category>flag.references/api-reference.md— addedtext-rangessection, documented--jianyingin Global flags, noted namespace aliases inside each decorator section.- Top-level
README.md— added Decorators / Enum discovery / Import SRT sections with copyable examples.
-
add-video/add-audioaccept Wikimedia URLs —src/wikimedia.tsresolvescommons.wikimedia.org, any*.wikipedia.org, andupload.wikimedia.orginputs (page URLs, direct CDN URLs, andapi.php?prop=pageimages&piprop=originalqueries) to a canonicalFile:title via the Commons imageinfo API. - License classifier + refusal gate — extracts
LicenseShortNamefrom extmetadata and classifies aspermissive(CC*/PD*/CC0/Public domain/No restrictions),fair-use,restrictive(CC BY-NC/ND, ©), orunknown. Restrictive/unknown require--force-license. Fair-use downloads with a warning. Zero-dep (Node 18+ globalfetch). - Output surfaces attribution —
wikimediablock in the JSON carriesartist,credit,description_url, license raw + class, dimensions, mime. Attribution required by CC-BY goes straight into a YouTube description. - Single on-disk copy — download lands directly in
<draft>/assets/<kind>/so addVideo/addAudio's copyFileSync becomes a no-op. No temp-dir churn. - Test coverage — 37 URL-parse/classifier unit tests + 12 gate-logic tests + 3 CLI smoke tests in
_test.sh(refusal of non-Wikimedia, local-path regression, classifier round-trip). Live fetch verified againsten.wikipedia.org/w/api.php?...titles=Barcelona...piprop=original— returnedCC BY 2.0, 16.6 MB JPEG, artist "dronepicr".
Keep capcut-cli lean. These stay in the upstream Python project:
- HTTP server (port 9001) — upstream covers it.
- MCP server — capcut-cli plugin already provides skill-level integration; a thin MCP shim is only justified if usage demands it.
- Non-Wikimedia remote URL downloading — conflicts with zero-dep ethos and license-check guarantees; user downloads separately.
- Mandatory ffprobe dependency — metadata/duration probing is now optional and gracefully falls back to explicit arguments.
- Letterboxing to 1920×1080 — ffmpeg runtime dep; wrap outside the CLI if needed.
- Cloud/final CapCut rendering — out of scope. The local FFmpeg proxy renderer is intentionally approximate.
Phase 1 → Phase 2 → Phase 3 → Phase 4. Each phase ships as its own PR with fixtures + README update. Phase 3's enum extraction is the only step that touches Python, and only at build time — the committed enums.json is the runtime artifact.