capcut-edit CLI for editing CapCut / JianYing drafts through a version-aware store. v0.11 detects and synchronizes readable timelines across , , , and . Project locations - macOS : - Windows : - Windows (JianYing / 剪映) : Core principle — deterministic scripts Any recipe involving more than one call, any arithmetic, or any branching ships as a parameterised shell script in . Claude invokes the script with inputs; the script runs a fixed sequence. If a recipe can be a script, it must be a script — not a narrative in . documents which script to call and why , never a reconstructable sequence. Pr…

1\\n00:00:00,000 --> 00:00:01,500\\nFirst caption\\n\\n2\\n00:00:01,500 --> 00:00:03,000\\nSecond caption\\n\\n3\\n00:00:03,000 --> 00:00:04,500\\nThird caption\\n'\nif printf '%s' \"$srt\" | node \"$cli\" import-srt \"$draft\" - >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const t=d.tracks.find(x=>x.type==='text' && x.name==='subtitle'); process.exit((t && t.segments.length===3 && t.segments[0].target_timerange.start===0 && t.segments[1].target_timerange.start===1500000) ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut import-srt (3 cues, stdin)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut import-srt (3 cues, stdin)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# import-srt with --time-offset shifts all cues\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif printf '%s' \"$srt\" | node \"$cli\" import-srt \"$draft\" - --time-offset 500ms >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const t=d.tracks.find(x=>x.type==='text' && x.name==='subtitle'); process.exit((t && t.segments[0].target_timerange.start===500000 && t.segments[2].target_timerange.start===3500000) ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut import-srt --time-offset 500ms\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut import-srt --time-offset 500ms\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# import-srt --style-ref mirrors font_size from an existing text segment.\n# Pre-seed a big-font segment via text-style, then --style-ref it.\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif node \"$cli\" text-style \"$draft\" cccccc01 --shadow --border-width 0.12 >/dev/null 2>&1 \\\n && printf '%s' \"$srt\" | node \"$cli\" import-srt \"$draft\" - --track-name subs --style-ref cccccc01 >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const t=d.tracks.find(x=>x.type==='text' && x.name==='subs'); if(!t||t.segments.length!==3)process.exit(1); const mats=d.materials.texts||[]; const newMats=t.segments.map(s=>mats.find(m=>m.id===s.material_id)); process.exit(newMats.every(m=>m && m.has_shadow===true && m.border_width===0.12) ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut import-srt --style-ref copies styling\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut import-srt --style-ref copies styling\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# Phase 4 — text-ranges + --jianying\n\n# text-ranges: 2 ranges on \"Hello everyone\" (14 chars) → 3 styles (gold \"Hello\", baseline \" \", red \"everyone\")\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif node \"$cli\" text-ranges \"$draft\" cccccc01 --styles '[{\"start\":0,\"end\":5,\"font_color\":\"#FFD700\",\"font_size\":22,\"bold\":true},{\"start\":6,\"end\":14,\"font_color\":\"#FF0000\"}]' >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const c=JSON.parse(d.materials.texts[0].content); if(c.styles.length!==3)process.exit(1); const s0=c.styles[0], s2=c.styles[2]; const goldOk = s0.range[0]===0 && s0.range[1]===5 && s0.size===22 && s0.bold===true && Math.abs(s0.fill.content.solid.color[0]-1)\u003c1e-6; const redOk = s2.range[0]===6 && s2.range[1]===14 && s2.fill.content.solid.color[0]===1 && s2.fill.content.solid.color[1]===0; process.exit((goldOk && redOk)?0:1)\"; then\n printf ' ok %s\\n' \"capcut text-ranges (2 ranges -> 3 styles)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut text-ranges (2 ranges -> 3 styles)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# text-ranges: overlap error is caught\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif ! node \"$cli\" text-ranges \"$draft\" cccccc01 --styles '[{\"start\":0,\"end\":5},{\"start\":3,\"end\":8}]' >/dev/null 2>&1; then\n printf ' ok %s\\n' \"capcut text-ranges rejects overlap\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut text-ranges rejects overlap\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying transition: JianYing identifiers are Chinese, so look up by Python\n# member name (`_3D空间` is the first entry).\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\njy_member=\"$(node -e \"console.log(require('$repo_root/dist/enums.json').jianying.transitions[0].member)\")\"\nif node \"$cli\" transition \"$draft\" aaaaaa01 \"$jy_member\" --jianying >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); process.exit(((d.materials.transitions||[]).length===1 && d.materials.transitions[0].name==='3D空间') ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut transition --jianying (by member name)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut transition --jianying (by member name)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying rejects a capcut-only slug (rgb-glitch is CapCut-only)\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif ! node \"$cli\" transition \"$draft\" aaaaaa01 rgb-glitch --jianying >/dev/null 2>&1; then\n printf ' ok %s\\n' \"capcut transition --jianying rejects capcut-only slug\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut transition --jianying rejects capcut-only slug\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying mask by Chinese member\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif node \"$cli\" mask \"$draft\" aaaaaa01 \"线性\" --jianying >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); process.exit(((d.materials.common_mask||[]).length===1 && d.materials.common_mask[0].name==='线性') ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut mask --jianying (by member)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut mask --jianying (by member)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying text-anim by Chinese member\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\njy_text_intro=\"$(node -e \"console.log(require('$repo_root/dist/enums.json').jianying.text_intros[0].member)\")\"\nif node \"$cli\" text-anim \"$draft\" cccccc02 --intro \"$jy_text_intro\" --jianying >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const mat=(d.materials.material_animations||[]).find(m=>(m.animations||[]).some(a=>a.material_type==='text')); process.exit((mat && mat.animations.length===1 && mat.animations[0].type==='in') ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut text-anim --jianying (by member)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut text-anim --jianying (by member)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying image-anim by Chinese member\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\njy_img_intro=\"$(node -e \"console.log(require('$repo_root/dist/enums.json').jianying.image_intros[0].member)\")\"\nif node \"$cli\" image-anim \"$draft\" aaaaaa01 --intro \"$jy_img_intro\" --jianying >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const mat=(d.materials.material_animations||[]).find(m=>(m.animations||[]).some(a=>a.material_type==='video')); process.exit((mat && mat.animations.length===1 && mat.animations[0].type==='in') ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut image-anim --jianying (by member)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut image-anim --jianying (by member)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying add-effect (bypasses inline knossos catalogue)\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif node \"$cli\" add-effect \"$draft\" 1998 0s 3s --jianying >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const e=(d.materials.video_effects||[]).find(m=>m.name==='1998'); process.exit((e && e.effect_id && e.resource_id) ? 0 : 1)\"; then\n printf ' ok %s\\n' \"capcut add-effect --jianying (ascii jianying slug)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut add-effect --jianying (ascii jianying slug)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# --jianying add-effect: same slug 'shake' should NOT resolve to the CapCut inline effect_id\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\ncc_shake_effect_id=\"$(node -e \"const v = Object.entries(require('$repo_root/dist/enums.json').capcut.scene_effects||[]).find(([,e])=>e.slug==='shake'); console.log(v?v[1].effect_id:'')\" 2>/dev/null || echo \"\")\"\n# In capcut namespace we have an inline shake with effect_id 7061205058364788270.\nnode \"$cli\" add-effect \"$draft\" shake 0s 3s >/dev/null 2>&1\ncc_written=\"$(node -e \"console.log((require('$draft').materials.video_effects[0]||{}).effect_id)\")\"\nrm -f \"$draft\" \"$draft.bak\"\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\n# With --jianying, inline bypass is active; 'shake' has no slug in jianying scene_effects → should error.\nif ! node \"$cli\" add-effect \"$draft\" shake 0s 3s --jianying >/dev/null 2>&1; then\n printf ' ok %s\\n' \"capcut add-effect --jianying bypasses inline catalogue (shake unmapped)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"capcut add-effect --jianying bypasses inline catalogue (shake unmapped)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\necho \" note CapCut inline 'shake' effect_id=$cc_written (expected 7061205058364788270)\"\n\n# Phase 5 — Wikimedia (offline smokes only; live coverage is in tmp scripts)\n\n# Non-Wikimedia HTTPS URL is refused before any network call\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\nif ! node \"$cli\" add-video \"$draft\" 'https://example.com/foo.jpg' 0s 3s >/dev/null 2>&1; then\n printf ' ok %s\\n' \"add-video refuses non-Wikimedia URL\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"add-video refuses non-Wikimedia URL\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\"\n\n# Local file path (non-URL) still works — regression check for the async refactor\ndraft=\"$tmp/capcut-edit-test-$RANDOM.json\"; cp \"$fixture\" \"$draft\"\n# Write a tiny fake jpg-named file — addVideo type-detects by extension only\nfake_img=\"$tmp/fake-$RANDOM.jpg\"; printf 'not really a jpg' > \"$fake_img\"\nif node \"$cli\" add-video \"$draft\" \"$fake_img\" 0s 3s >/dev/null 2>&1 \\\n && node -e \"const d=require('$draft'); const v=d.tracks.find(x=>x.type==='video' && x.name==='video'); const seg=v && v.segments[0]; const mat=seg && d.materials.videos.find(m=>m.id===seg.material_id); process.exit(mat && mat.type==='photo' ? 0 : 1)\"; then\n printf ' ok %s\\n' \"add-video local path still works (async refactor)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"add-video local path still works (async refactor)\"; fail=$((fail+1))\nfi\nrm -f \"$draft\" \"$draft.bak\" \"$fake_img\"\n\n# License classifier (unit-level, no network)\nif node -e \"\nimport('$repo_root/dist/wikimedia.js').then(({classifyLicense})=>{\n const cases = [['CC BY 4.0','permissive'],['CC0','permissive'],['Public domain','permissive'],['CC BY-NC 4.0','restrictive'],['Fair use','fair-use'],[null,'unknown']];\n for (const [v,w] of cases) if (classifyLicense(v)!==w) { console.error('fail',v); process.exit(1); }\n process.exit(0);\n});\n\" 2>/dev/null; then\n printf ' ok %s\\n' \"wikimedia license classifier (6 cases)\"; pass=$((pass+1))\nelse\n printf ' FAIL %s\\n' \"wikimedia license classifier (6 cases)\"; fail=$((fail+1))\nfi\n\necho \"\"\necho \"results: $pass passed, $fail failed\"\nif (( fail > 0 )); then exit 1; fi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":21220,"content_sha256":"7b6e5d4bb6338a93e6c0a1937fbf6b6e650593baec5b93b128c5efbe9896e378"},{"filename":"scripts/anim.sh","content":"#!/usr/bin/env bash\n# anim.sh — attach a CapCut intro/outro animation to a video/image segment.\n#\n# Usage: anim.sh \u003cproject> \u003csegment-id> \u003cslug> [\u003cduration>]\n#\n# \u003cproject> CapCut project directory OR draft JSON file.\n# \u003csegment-id> 6+ char prefix of the segment UUID.\n# \u003cslug> Animation slug from ../assets/animations.json\n# intros: fade-in, flash-in, pulsing-zooms, scroll-up,\n# stripe-merge, zoom-out\n# outros: fade-out, blur-out, smoke\n# \u003cduration> Optional time string (0.5s, 500ms, 1:00). Defaults to the\n# slug's catalogue default (see animations.json).\n#\n# Writes a `material_animations` entry matching the shape CapCut writes\n# natively (seen on knossos-recon segments), then appends its id to the\n# segment's `extra_material_refs`. Creates a `.bak` before writing.\n#\n# For intros the animation is placed at `start: 0` (segment-relative).\n# For outros the animation is placed at `start: target_duration - duration`\n# so it finishes exactly at the segment cut (matches the Blur Out pattern\n# on `b11d518e` at 0:09 in knossos-recon).\n\nset -euo pipefail\n\nif [[ $# -lt 3 || $# -gt 4 ]]; then\n echo \"Usage: $0 \u003cproject> \u003csegment-id> \u003cslug> [\u003cduration>]\" >&2\n exit 2\nfi\n\nproject=\"$1\"\nsegment_id=\"$2\"\nslug=\"$3\"\nduration=\"${4:-}\"\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncatalogue=\"$script_dir/../assets/animations.json\"\n\nif [[ ! -f \"$catalogue\" ]]; then\n echo \"Animation catalogue not found at $catalogue\" >&2\n exit 1\nfi\n\nexec node -e '\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst crypto = require(\"crypto\");\n\nconst [, projectArg, segPrefix, slug, durOverride, cataloguePath] = process.argv;\n\n// The effect cache lives in a per-platform app data directory. Resolve it at\n// runtime instead of hardcoding the macOS container path, which on Windows\n// wrote a path the app cannot resolve into the animation material.\n// CAPCUT_EFFECT_CACHE overrides both.\nfunction defaultEffectCacheBase() {\n const override = process.env.CAPCUT_EFFECT_CACHE;\n if (override) return override;\n if (process.platform === \"win32\") {\n const local =\n process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.env.HOME || \"\", \"AppData\", \"Local\");\n for (const app of [\"CapCut\", \"JianyingPro\"]) {\n const candidate = path.join(local, app, \"User Data\", \"Cache\", \"effect\");\n if (fs.existsSync(candidate)) return candidate;\n }\n return path.join(local, \"CapCut\", \"User Data\", \"Cache\", \"effect\");\n }\n if (process.platform === \"darwin\") {\n return path.join(\n process.env.HOME,\n \"Library/Containers/com.lemon.lvoverseas/Data/Movies/CapCut/User Data/Cache/effect\"\n );\n }\n return path.join(process.env.HOME || \"\", \".local/share/CapCut/Cache/effect\");\n}\nconst EFFECT_CACHE_BASE = defaultEffectCacheBase();\n\nfunction parseTime(input) {\n const neg = input.startsWith(\"-\");\n const clean = input.replace(/^[+-]/, \"\");\n if (clean.endsWith(\"ms\")) return (neg ? -1 : 1) * Math.round(parseFloat(clean.slice(0, -2)) * 1000);\n if (clean.endsWith(\"s\")) return (neg ? -1 : 1) * Math.round(parseFloat(clean.slice(0, -1)) * 1e6);\n if (clean.includes(\":\")) {\n const parts = clean.split(\":\");\n const sec = parts.length === 3\n ? parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2])\n : parseInt(parts[0]) * 60 + parseFloat(parts[1]);\n return (neg ? -1 : 1) * Math.round(sec * 1e6);\n }\n const n = parseFloat(clean);\n if (isNaN(n)) throw new Error(\"invalid time: \" + input);\n return (neg ? -1 : 1) * Math.round(n * 1e6);\n}\n\nfunction findDraft(p) {\n const stat = fs.statSync(p);\n if (stat.isFile()) return p;\n for (const name of [\"draft_info.json\", \"draft_content.json\"]) {\n const candidate = path.join(p, name);\n if (fs.existsSync(candidate)) return candidate;\n }\n throw new Error(\"no draft_info.json / draft_content.json under \" + p);\n}\n\nfunction uuidUpper() {\n const b = crypto.randomBytes(16);\n b[6] = (b[6] & 0x0f) | 0x40;\n b[8] = (b[8] & 0x3f) | 0x80;\n const h = b.toString(\"hex\").toUpperCase();\n return h.slice(0,8)+\"-\"+h.slice(8,12)+\"-\"+h.slice(12,16)+\"-\"+h.slice(16,20)+\"-\"+h.slice(20);\n}\n\nconst catalogue = JSON.parse(fs.readFileSync(cataloguePath, \"utf8\"));\nconst spec = catalogue[slug];\nif (!spec) {\n console.error(\"Unknown animation slug: \" + slug);\n console.error(\"Available: \" + Object.keys(catalogue).join(\", \"));\n process.exit(1);\n}\n\nconst durUs = durOverride ? parseTime(durOverride) : spec.default_duration_us;\nif (durUs \u003c= 0) { console.error(\"duration must be > 0\"); process.exit(2); }\n\nconst file = findDraft(projectArg);\nconst raw = fs.readFileSync(file, \"utf8\");\nfs.writeFileSync(file + \".bak\", raw, \"utf8\");\nconst d = JSON.parse(raw);\n\nlet targetSeg = null;\nfor (const t of d.tracks || []) for (const s of t.segments || []) {\n if (s.id && s.id.startsWith(segPrefix)) targetSeg = s;\n}\nif (!targetSeg) { console.error(\"segment not found: \" + segPrefix); process.exit(1); }\n\n// Refuse to overwrite an existing animation of the same type.\nconst animsById = Object.fromEntries((d.materials.material_animations || []).map(a => [a.id, a]));\nfor (const ref of targetSeg.extra_material_refs || []) {\n const existing = animsById[ref];\n if (!existing) continue;\n for (const a of existing.animations || []) {\n if (a.type === spec.type) {\n console.error(\"segment already has a \" + spec.type + \" animation (\" + a.name + \"). Remove it first.\");\n process.exit(1);\n }\n }\n}\n\nconst targetDur = targetSeg.target_timerange.duration;\nif (durUs > targetDur) {\n console.error(\"duration (\" + durUs + \"us) exceeds segment duration (\" + targetDur + \"us)\");\n process.exit(1);\n}\n\nconst start = spec.type === \"out\" ? (targetDur - durUs) : 0;\nconst cachePath = path.join(EFFECT_CACHE_BASE, spec.effect_id, spec.md5);\nconst newId = uuidUpper();\n\nconst animMat = {\n animations: [{\n anim_adjust_params: null,\n category_id: spec.category_id,\n category_name: spec.category_id,\n duration: durUs,\n id: spec.effect_id,\n material_type: spec.material_type,\n name: spec.name,\n panel: spec.material_type,\n path: cachePath,\n platform: \"all\",\n request_id: \"\",\n resource_id: spec.resource_id,\n source_platform: 1,\n start: start,\n third_resource_id: spec.third_resource_id,\n type: spec.type\n }],\n id: newId,\n multi_language_current: \"none\",\n type: \"sticker_animation\"\n};\n\nif (!Array.isArray(d.materials.material_animations)) d.materials.material_animations = [];\nd.materials.material_animations.push(animMat);\n\ntargetSeg.extra_material_refs = targetSeg.extra_material_refs || [];\ntargetSeg.extra_material_refs.push(newId);\n\nfs.writeFileSync(file, JSON.stringify(d), \"utf8\");\n\nconsole.log(JSON.stringify({\n ok: true,\n segment_id: targetSeg.id,\n slug: slug,\n name: spec.name,\n type: spec.type,\n material_id: newId,\n start_us: start,\n duration_us: durUs,\n cache_file_exists: fs.existsSync(cachePath)\n}));\n' -- \"$project\" \"$segment_id\" \"$slug\" \"$duration\" \"$catalogue\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7039,"content_sha256":"fc8afcbe64b05694fab7f850d4b84b85bd9eccfc0642334d40492cf5bbf2464c"},{"filename":"scripts/fade-in.sh","content":"#!/usr/bin/env bash\n# fade-in.sh — thin wrapper around anim.sh with slug \"fade-in\".\n#\n# Usage: fade-in.sh \u003cproject> \u003csegment-id> [\u003cduration>]\n#\n# Default duration: 0.5s (CapCut default for Fade In).\n#\n# All logic lives in anim.sh + animations.json. Keep this wrapper so the\n# PLAN.md Phase 0 deliverable name stays stable.\n\nset -euo pipefail\n\nif [[ $# -lt 2 || $# -gt 3 ]]; then\n echo \"Usage: $0 \u003cproject> \u003csegment-id> [\u003cduration>]\" >&2\n exit 2\nfi\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nproject=\"$1\"; seg=\"$2\"; dur=\"${3:-}\"\n\nif [[ -n \"$dur\" ]]; then\n exec \"$script_dir/anim.sh\" \"$project\" \"$seg\" fade-in \"$dur\"\nelse\n exec \"$script_dir/anim.sh\" \"$project\" \"$seg\" fade-in\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":700,"content_sha256":"5aaf52ebc744e3c783b85e5006f0e3b83cf05755cab40b7d20e5637425a23548"},{"filename":"scripts/fade-out.sh","content":"#!/usr/bin/env bash\n# fade-out.sh — thin wrapper around anim.sh with slug \"fade-out\".\n#\n# Usage: fade-out.sh \u003cproject> \u003csegment-id> [\u003cduration>]\n#\n# Default duration: 0.5s (CapCut default for Fade Out).\n# The outro is anchored at the end of the segment — anim.sh computes\n# start = target_duration - duration so the fade finishes at the cut.\n#\n# All logic lives in anim.sh + animations.json.\n\nset -euo pipefail\n\nif [[ $# -lt 2 || $# -gt 3 ]]; then\n echo \"Usage: $0 \u003cproject> \u003csegment-id> [\u003cduration>]\" >&2\n exit 2\nfi\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nproject=\"$1\"; seg=\"$2\"; dur=\"${3:-}\"\n\nif [[ -n \"$dur\" ]]; then\n exec \"$script_dir/anim.sh\" \"$project\" \"$seg\" fade-out \"$dur\"\nelse\n exec \"$script_dir/anim.sh\" \"$project\" \"$seg\" fade-out\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":773,"content_sha256":"6443a8d591d8c116212c0802f0e5afcd2b15baa09b7acdc05062bdacfff130f6"},{"filename":"scripts/ken-burns.sh","content":"#!/usr/bin/env bash\n# ken-burns.sh — animate uniform scale + position on a video/image segment.\n#\n# Usage:\n# ken-burns.sh \u003cproject> \u003csegment-id> \\\n# \u003cstart-scale> \u003cend-scale> \\\n# \u003ctx-start> \u003ctx-end> \\\n# \u003cty-start> \u003cty-end> \\\n# \u003cduration> [easing]\n#\n# \u003cstart/end-scale> uniform scale values (1.0 = no zoom, 1.2 = 20% zoom-in)\n# \u003ctx-start/tx-end> horizontal position in half-canvas units (-1..1, 0 = center)\n# \u003cty-start/ty-end> vertical position in half-canvas units (-1..1)\n# \u003cduration> time string (e.g. 3s, 500ms)\n# [easing] linear | ease-in | ease-out | ease-in-out\n# default ease-out — the easing CapCut's own UI applies to\n# a ken-burns zoom (\"Cubic Out\")\n#\n# Writes 6 keyframes total via `capcut keyframe --batch`: uniform_scale,\n# position_x, position_y — each with a start keyframe at t=0 and an end\n# keyframe at t=duration. Keyframes for motion properties render correctly\n# in CapCut (unlike alpha — see pitfalls.md).\n\nset -euo pipefail\n\nif [[ $# -lt 9 || $# -gt 10 ]]; then\n echo \"Usage: $0 \u003cproject> \u003csegment-id> \u003cstart-scale> \u003cend-scale> \u003ctx-start> \u003ctx-end> \u003cty-start> \u003cty-end> \u003cduration> [easing]\" >&2\n exit 2\nfi\n\nproject=\"$1\"\nsegment_id=\"$2\"\nstart_scale=\"$3\"\nend_scale=\"$4\"\ntx_start=\"$5\"\ntx_end=\"$6\"\nty_start=\"$7\"\nty_end=\"$8\"\nduration=\"$9\"\neasing=\"${10:-ease-out}\"\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# Resolve how to launch the CLI, so this script also works when the skill is\n# used outside a capcut-cli checkout. A JavaScript entry runs under node; a\n# launcher (an npm shim, a .cmd wrapper, a shell script) runs as it is, since\n# node cannot execute it. Priority:\n# 1. $CAPCUT_CLI (explicit override: a .js entry or a launcher)\n# 2. \u003crepo>/dist/index.js (this checkout)\n# 3. \u003cdir>/node_modules/capcut-cli/dist/index.js, walking up from here\n# 4. capcut-cli / capcut on PATH (run as a launcher)\ncli_cmd=()\nresolve_cli() {\n local entry\n if [[ -n \"${CAPCUT_CLI:-}\" && -f \"${CAPCUT_CLI}\" ]]; then\n case \"$CAPCUT_CLI\" in\n *.js | *.mjs | *.cjs) cli_cmd=(node \"$CAPCUT_CLI\") ;;\n *) cli_cmd=(\"$CAPCUT_CLI\") ;;\n esac\n return 0\n fi\n entry=\"$script_dir/../../../dist/index.js\"\n if [[ -f \"$entry\" ]]; then\n cli_cmd=(node \"$entry\")\n return 0\n fi\n local dir=\"$script_dir\" i\n for i in 1 2 3 4 5 6; do\n dir=\"$(dirname \"$dir\")\"\n entry=\"$dir/node_modules/capcut-cli/dist/index.js\"\n if [[ -f \"$entry\" ]]; then\n cli_cmd=(node \"$entry\")\n return 0\n fi\n done\n for entry in capcut-cli capcut; do\n if command -v \"$entry\" >/dev/null 2>&1; then\n cli_cmd=(\"$(command -v \"$entry\")\")\n return 0\n fi\n done\n return 1\n}\n\nif ! resolve_cli; then\n echo \"capcut CLI not found — set \\$CAPCUT_CLI to its dist/index.js or a launcher, install it (npm install -g capcut-cli), or run \\`npm run build\\` in capcut-cli/\" >&2\n exit 1\nfi\n\n\"${cli_cmd[@]}\" keyframe \"$project\" \"$segment_id\" --batch --easing \"$easing\" \u003c\u003cEOF\n{\"property\":\"uniform_scale\",\"time\":\"0\",\"value\":\"$start_scale\"}\n{\"property\":\"uniform_scale\",\"time\":\"$duration\",\"value\":\"$end_scale\"}\n{\"property\":\"position_x\",\"time\":\"0\",\"value\":\"$tx_start\"}\n{\"property\":\"position_x\",\"time\":\"$duration\",\"value\":\"$tx_end\"}\n{\"property\":\"position_y\",\"time\":\"0\",\"value\":\"$ty_start\"}\n{\"property\":\"position_y\",\"time\":\"$duration\",\"value\":\"$ty_end\"}\nEOF\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3424,"content_sha256":"e9696500c7c84821b85d4713e4fd8b518e4ff68c4c28c701efdc05122275dba2"},{"filename":"scripts/long-to-short.sh","content":"#!/usr/bin/env bash\n# long-to-short.sh — cut a range from a long-form project and stamp a title + CTA.\n#\n# Usage:\n# long-to-short.sh \u003cproject> \u003cstart> \u003cend> \u003cout-path> \u003ctitle-text> \u003ccta-text>\n#\n# \u003cproject> long-form CapCut project directory or draft JSON\n# \u003cstart> start time (e.g. 2:30, 150s)\n# \u003cend> end time\n# \u003cout-path> output draft JSON path (new file; source is not modified)\n# \u003ctitle-text> text stamped at the top for the first 3 seconds\n# \u003ccta-text> text stamped at the bottom for the last 3 seconds\n#\n# Pipeline:\n# 1. capcut cut — extract the range to \u003cout-path>\n# 2. capcut add-text — title, centred-upper, first 3s\n# 3. capcut add-text — CTA, centred-lower, last 3s\n\nset -euo pipefail\n\nif [[ $# -ne 6 ]]; then\n echo \"Usage: $0 \u003cproject> \u003cstart> \u003cend> \u003cout-path> \u003ctitle-text> \u003ccta-text>\" >&2\n exit 2\nfi\n\nproject=\"$1\"\nstart=\"$2\"\nend=\"$3\"\nout=\"$4\"\ntitle=\"$5\"\ncta=\"$6\"\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# Resolve how to launch the CLI, so this script also works when the skill is\n# used outside a capcut-cli checkout. A JavaScript entry runs under node; a\n# launcher (an npm shim, a .cmd wrapper, a shell script) runs as it is, since\n# node cannot execute it. Priority:\n# 1. $CAPCUT_CLI (explicit override: a .js entry or a launcher)\n# 2. \u003crepo>/dist/index.js (this checkout)\n# 3. \u003cdir>/node_modules/capcut-cli/dist/index.js, walking up from here\n# 4. capcut-cli / capcut on PATH (run as a launcher)\ncli_cmd=()\nresolve_cli() {\n local entry\n if [[ -n \"${CAPCUT_CLI:-}\" && -f \"${CAPCUT_CLI}\" ]]; then\n case \"$CAPCUT_CLI\" in\n *.js | *.mjs | *.cjs) cli_cmd=(node \"$CAPCUT_CLI\") ;;\n *) cli_cmd=(\"$CAPCUT_CLI\") ;;\n esac\n return 0\n fi\n entry=\"$script_dir/../../../dist/index.js\"\n if [[ -f \"$entry\" ]]; then\n cli_cmd=(node \"$entry\")\n return 0\n fi\n local dir=\"$script_dir\" i\n for i in 1 2 3 4 5 6; do\n dir=\"$(dirname \"$dir\")\"\n entry=\"$dir/node_modules/capcut-cli/dist/index.js\"\n if [[ -f \"$entry\" ]]; then\n cli_cmd=(node \"$entry\")\n return 0\n fi\n done\n for entry in capcut-cli capcut; do\n if command -v \"$entry\" >/dev/null 2>&1; then\n cli_cmd=(\"$(command -v \"$entry\")\")\n return 0\n fi\n done\n return 1\n}\n\nif ! resolve_cli; then\n echo \"capcut CLI not found — set \\$CAPCUT_CLI to its dist/index.js or a launcher, install it (npm install -g capcut-cli), or run \\`npm run build\\` in capcut-cli/\" >&2\n exit 1\nfi\n\n# 1. Cut the range\n\"${cli_cmd[@]}\" cut \"$project\" \"$start\" \"$end\" --out \"$out\" >/dev/null\n\n# Compute total duration in seconds for positioning the CTA at the tail.\n# parseTimeInput accepts the same formats on both ends, so we trust capcut\n# to have placed the cut accurately and work in seconds here via the cli.\ntotal_us=$(node -e \"\nconst fs=require('fs');\nconst d=JSON.parse(fs.readFileSync('$out','utf8'));\nconsole.log(d.duration);\n\")\n\n# 2. Title — first 3 seconds, upper-centre\n\"${cli_cmd[@]}\" add-text \"$out\" 0 3s \"$title\" --font-size 18 --y -0.6 >/dev/null\n\n# 3. CTA — last 3 seconds, lower-centre\ncta_start_ms=$(( (total_us - 3000000) / 1000 ))\nif (( cta_start_ms \u003c 0 )); then cta_start_ms=0; fi\n\"${cli_cmd[@]}\" add-text \"$out\" \"${cta_start_ms}ms\" 3s \"$cta\" --font-size 18 --y 0.6 >/dev/null\n\necho \"{\\\"ok\\\":true,\\\"out\\\":\\\"$out\\\",\\\"duration_us\\\":$total_us,\\\"title\\\":\\\"$title\\\",\\\"cta\\\":\\\"$cta\\\"}\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3417,"content_sha256":"5e5b18ab4a4644587081b67e53a3419eb71dcfcb134d825a31e2a43e462e76d7"},{"filename":"scripts/stamp-cta.sh","content":"#!/usr/bin/env bash\n# stamp-cta.sh — apply a saved CTA template into a project at a given time.\n#\n# Usage:\n# stamp-cta.sh \u003cproject> \u003ctemplate.json> \u003cstart> \u003cduration> [\u003ctext>]\n#\n# \u003cproject> CapCut project directory or draft JSON\n# \u003ctemplate.json> saved template (e.g. ../assets/examples/subscribe-cta.json)\n# \u003cstart> start time (e.g. 0:27, 2:30.5)\n# \u003cduration> duration (e.g. 3s, 5000ms)\n# \u003ctext> optional: override the template's text body\n#\n# Thin wrapper around `capcut apply-template` so the CTA stamping recipe is\n# a one-line script call rather than a remembered CLI invocation.\n\nset -euo pipefail\n\nif [[ $# -lt 4 || $# -gt 5 ]]; then\n echo \"Usage: $0 \u003cproject> \u003ctemplate.json> \u003cstart> \u003cduration> [\u003ctext>]\" >&2\n exit 2\nfi\n\nproject=\"$1\"\ntemplate=\"$2\"\nstart=\"$3\"\nduration=\"$4\"\ntext=\"${5:-}\"\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# Resolve how to launch the CLI, so this script also works when the skill is\n# used outside a capcut-cli checkout. A JavaScript entry runs under node; a\n# launcher (an npm shim, a .cmd wrapper, a shell script) runs as it is, since\n# node cannot execute it. Priority:\n# 1. $CAPCUT_CLI (explicit override: a .js entry or a launcher)\n# 2. \u003crepo>/dist/index.js (this checkout)\n# 3. \u003cdir>/node_modules/capcut-cli/dist/index.js, walking up from here\n# 4. capcut-cli / capcut on PATH (run as a launcher)\ncli_cmd=()\nresolve_cli() {\n local entry\n if [[ -n \"${CAPCUT_CLI:-}\" && -f \"${CAPCUT_CLI}\" ]]; then\n case \"$CAPCUT_CLI\" in\n *.js | *.mjs | *.cjs) cli_cmd=(node \"$CAPCUT_CLI\") ;;\n *) cli_cmd=(\"$CAPCUT_CLI\") ;;\n esac\n return 0\n fi\n entry=\"$script_dir/../../../dist/index.js\"\n if [[ -f \"$entry\" ]]; then\n cli_cmd=(node \"$entry\")\n return 0\n fi\n local dir=\"$script_dir\" i\n for i in 1 2 3 4 5 6; do\n dir=\"$(dirname \"$dir\")\"\n entry=\"$dir/node_modules/capcut-cli/dist/index.js\"\n if [[ -f \"$entry\" ]]; then\n cli_cmd=(node \"$entry\")\n return 0\n fi\n done\n for entry in capcut-cli capcut; do\n if command -v \"$entry\" >/dev/null 2>&1; then\n cli_cmd=(\"$(command -v \"$entry\")\")\n return 0\n fi\n done\n return 1\n}\n\nif ! resolve_cli; then\n echo \"capcut CLI not found — set \\$CAPCUT_CLI to its dist/index.js or a launcher, install it (npm install -g capcut-cli), or run \\`npm run build\\` in capcut-cli/\" >&2\n exit 1\nfi\n\nif [[ -n \"$text\" ]]; then\n \"${cli_cmd[@]}\" apply-template \"$project\" \"$template\" \"$start\" \"$duration\" \"$text\"\nelse\n \"${cli_cmd[@]}\" apply-template \"$project\" \"$template\" \"$start\" \"$duration\"\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2619,"content_sha256":"4368ba889d5fcecb15735ad38ae2a150d36b6c5cbe32a9fc283f02d63d123ec0"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"capcut-edit","type":"text"}]},{"type":"paragraph","content":[{"text":"CLI for editing CapCut / JianYing drafts through a version-aware store. v0.11 detects and synchronizes readable timelines across ","type":"text"},{"text":"draft_content.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"draft_info.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"draft_meta_info.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"template-2.tmp","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Project locations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"macOS","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"~/Movies/CapCut/User Data/Projects/com.lveditor.draft/\u003cproject>/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Windows","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"%LOCALAPPDATA%\\CapCut\\User Data\\Projects\\com.lveditor.draft\\\u003cproject>\\","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Windows (JianYing / 剪映)","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"%LOCALAPPDATA%\\JianyingPro\\User Data\\Projects\\com.lveditor.draft\\\u003cproject>\\","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core principle — deterministic scripts","type":"text"}]},{"type":"paragraph","content":[{"text":"Any recipe involving more than one ","type":"text"},{"text":"capcut","type":"text","marks":[{"type":"code_inline"}]},{"text":" call, any arithmetic, or any branching ships as a parameterised shell script in ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":". Claude invokes the script with inputs; the script runs a fixed sequence. If a recipe can be a script, it ","type":"text"},{"text":"must","type":"text","marks":[{"type":"strong"}]},{"text":" be a script — not a narrative in ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"references/workflows.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" documents ","type":"text"},{"text":"which script to call and why","type":"text","marks":[{"type":"em"}]},{"text":", never a reconstructable sequence.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Progressive disclosure","type":"text"}]},{"type":"paragraph","content":[{"text":"Start broad, drill into what you need. Never dump full project JSON.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"capcut info \u003cproject> -H # overview\ncapcut doctor -H # installed media/transcription capabilities\ncapcut diagnose \u003cproject> -H # canonical files, divergence, editor safety\ncapcut tracks \u003cproject> -H # all tracks\ncapcut materials \u003cproject> -H # material summary\ncapcut segments \u003cproject> -H # segments with timing\ncapcut segment \u003cproject> \u003cid> # one item, full detail","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output modes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JSON","type":"text","marks":[{"type":"strong"}]},{"text":" (default): pipe to ","type":"text"},{"text":"jq","type":"text","marks":[{"type":"code_inline"}]},{"text":", feed to scripts.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-H","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":": human-readable tables.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-q","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":": quiet, exit code only — for write commands in scripts.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Batch writes","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"echo '{\"cmd\":\"set-text\",\"id\":\"a1b2c3\",\"text\":\"Fixed\"}\n{\"cmd\":\"volume\",\"id\":\"d4e5f6\",\"volume\":0.5}' | capcut batch \u003cproject>","type":"text"}]},{"type":"paragraph","content":[{"text":"Operations: ","type":"text"},{"text":"set-text","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"shift","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"shift-all","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"speed","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"volume","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"opacity","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"trim","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Batch is transactional: one failing operation writes nothing. Use ","type":"text"},{"text":"--continue-on-error","type":"text","marks":[{"type":"code_inline"}]},{"text":" only when the user explicitly accepts a partial commit; it still exits non-zero.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Where to look","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/api-reference.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — every command, every flag, every value format (time, percentages, degrees), the ID-prefix rule, the ","type":"text"},{"text":".bak","type":"text","marks":[{"type":"code_inline"}]},{"text":" invariant.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"../../docs/command-reference.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — generated command-contract v2 with typed positionals/options, mutability, prerequisites, output form, and exit codes.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/workflows.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — recipes = which ","type":"text"},{"text":"scripts/X.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" to run and why.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/pitfalls.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — close-project-first, ","type":"text"},{"text":".bak","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"clip=null","type":"text","marks":[{"type":"code_inline"}]},{"text":" on audio, ","type":"text"},{"text":"source_timerange","type":"text","marks":[{"type":"code_inline"}]},{"text":" math with ","type":"text"},{"text":"speed","type":"text","marks":[{"type":"code_inline"}]},{"text":", alpha-keyframes-don't-render-use-animation-materials, etc.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enum discovery","type":"text","marks":[{"type":"strong"}]},{"text":" — call ","type":"text"},{"text":"capcut enums --\u003ccategory>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"-H","type":"text","marks":[{"type":"code_inline"}]},{"text":" for a table) to list every CapCut slug. Categories: ","type":"text"},{"text":"--transitions","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--masks","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--image-intros","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--image-outros","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--image-combos","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--text-intros","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--text-outros","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--text-loop-anims","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--scene-effects","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--character-effects","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--audio-effects","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--fonts","type":"text","marks":[{"type":"code_inline"}]},{"text":". Add ","type":"text"},{"text":"--jianying","type":"text","marks":[{"type":"code_inline"}]},{"text":" to switch namespace. The ","type":"text"},{"text":"enums.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" bundle is generated once via ","type":"text"},{"text":"python3 scripts/extract-enums.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"; runtime is Python-free.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — the client library. Current:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"anim.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — attach any of 9 CapCut intro/outro animations; ","type":"text"},{"text":"fade-in.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"fade-out.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" are thin wrappers.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ken-burns.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — scale + pan keyframes via ","type":"text"},{"text":"capcut keyframe --batch","type":"text","marks":[{"type":"code_inline"}]},{"text":" (motion properties render; alpha does not — use ","type":"text"},{"text":"anim.sh fade-in","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"long-to-short.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — cut a range, stamp title + CTA text.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"stamp-cta.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — apply a saved text template (see ","type":"text"},{"text":"assets/examples/subscribe-cta.json","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"_test.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — run every wrapper against the fixture; run after any change here.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/examples/","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — raw JSON snippets for hand-editing.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Scripts resolve the ","type":"text"},{"text":"capcut","type":"text","marks":[{"type":"code_inline"}]},{"text":" entry point themselves, so the skill also works when it is installed outside a ","type":"text"},{"text":"capcut-cli","type":"text","marks":[{"type":"code_inline"}]},{"text":" checkout: ","type":"text"},{"text":"$CAPCUT_CLI","type":"text","marks":[{"type":"code_inline"}]},{"text":" (explicit path to ","type":"text"},{"text":"dist/index.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" or a launcher) wins, then the repo build, then an installed ","type":"text"},{"text":"node_modules/capcut-cli","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"capcut-cli","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"capcut","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":". A ","type":"text"},{"text":".js","type":"text","marks":[{"type":"code_inline"}]},{"text":" entry runs under ","type":"text"},{"text":"node","type":"text","marks":[{"type":"code_inline"}]},{"text":"; a launcher (from ","type":"text"},{"text":"PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"$CAPCUT_CLI","type":"text","marks":[{"type":"code_inline"}]},{"text":" pointing at one) runs directly. ","type":"text"},{"text":"anim.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" resolves the CapCut effect-cache directory per platform; ","type":"text"},{"text":"$CAPCUT_EFFECT_CACHE","type":"text","marks":[{"type":"code_inline"}]},{"text":" overrides it.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Invariants","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Close the project in CapCut before editing; reopen after.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Every write is conflict-checked, prepared atomically, and synchronized to every readable timeline target. Every target receives ","type":"text"},{"text":".bak","type":"text","marks":[{"type":"code_inline"}]},{"text":" and rolling history snapshots.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never pass ","type":"text"},{"text":"--force-write","type":"text","marks":[{"type":"code_inline"}]},{"text":" unless the user explicitly accepts overwriting changed-on-disk state or an open editor.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"clip","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]},{"text":" on audio segments (no opacity/scale).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"capcut cut","type":"text","marks":[{"type":"code_inline"}]},{"text":" writes to ","type":"text"},{"text":"--out","type":"text","marks":[{"type":"code_inline"}]},{"text":", never modifies the source.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"IDs: first 6+ chars of the UUID match as a prefix.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Time formats: ","type":"text"},{"text":"1.5s","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"500ms","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"+0.5s","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"-1s","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"1:30","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"0:05.5","type":"text","marks":[{"type":"code_inline"}]},{"text":", bare number = seconds.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"v0.11 high-value paths","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Whole draft: ","type":"text"},{"text":"capcut compile spec.json --check","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"capcut compile spec.json --out \u003cdir>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Specs support refs, transforms, decorators, templates, captions, keyframes, and fades.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Karaoke: ","type":"text"},{"text":"capcut caption \u003cproject> --audio \u003cpath> --whisper-engine \u003copenai|whisper-cpp|faster-whisper> --karaoke","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Proxy: ","type":"text"},{"text":"capcut render \u003cproject> --all-video-tracks --burn-captions","type":"text","marks":[{"type":"code_inline"}]},{"text":"; run ","type":"text"},{"text":"doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":" first for FFmpeg capability flags.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Automation: ","type":"text"},{"text":"capcut serve --workers N --retries N --timeout MS","type":"text","marks":[{"type":"code_inline"}]},{"text":"; give jobs stable ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" fields and let the runner serialize writes per project.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent schema: on CLI v0.28.0 or newer, start with ","type":"text"},{"text":"capcut describe --compact","type":"text","marks":[{"type":"code_inline"}]},{"text":" to discover commands, then fetch complete contracts with ","type":"text"},{"text":"capcut describe --command \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (repeat for several names). Plain ","type":"text"},{"text":"capcut describe","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns the full registry and works on older versions. Use these contracts as the source of truth; do not scrape help text or invent flags.","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"capcut-edit","author":"@skillopedia","source":{"stars":1093,"repo_name":"capcut-cli","origin_url":"https://github.com/renezander030/capcut-cli/blob/HEAD/skills/capcut-edit/SKILL.md","repo_owner":"renezander030","body_sha256":"7eba75d77e4dc37ab9c041c6b47c39881a792e315d8d4ce297e1442e17eafe27","cluster_key":"3b5f333529578226c7e6f41bfa4095cdd105046e0935cb9b80fa3dd5349cbe0a","clean_bundle":{"format":"clean-skill-bundle-v1","source":"renezander030/capcut-cli/skills/capcut-edit/SKILL.md","text_bytes":81731,"source_commit":"5d4220a8e1845642cebb89d188e63713c1f0ccfc","skills_sh_rank":14209,"attachment_count":12,"text_attachments":12,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/capcut-edit/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"media-content","category_label":"Media"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"media-content","import_tag":"skills-sh-top20k-2026-11","description":"Edit CapCut / JianYing video projects — read and write subtitles, timing, speed, volume, templates, animations (fade/ken-burns), and cut long-form to shorts. Use when the user mentions capcut, jianying, subtitles, video editing, draft_content.json, draft_info.json, or cutting videos — in English or Chinese (剪映, 字幕, 草稿, 剪辑, 切片, 视频编辑)."}},"renderedAt":1793661329916}

capcut-edit CLI for editing CapCut / JianYing drafts through a version-aware store. v0.11 detects and synchronizes readable timelines across , , , and . Project locations - macOS : - Windows : - Windows (JianYing / 剪映) : Core principle — deterministic scripts Any recipe involving more than one call, any arithmetic, or any branching ships as a parameterised shell script in . Claude invokes the script with inputs; the script runs a fixed sequence. If a recipe can be a script, it must be a script — not a narrative in . documents which script to call and why , never a reconstructable sequence. Pr…