Capcut editor
π¦ Ducky's agent skills collection
npx -y skills add duckyman-ai/agent-skills --skill capcut-editorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Read, analyze, and create CapCut desktop (macOS) video editing projects programmatically. Use this skill whenever the user mentions CapCut, video editing with CapCut, reading CapCut projects, creating CapCut-compatible drafts, adding video/audio/text tracks to CapCut, or wants to automate any CapCut desktop workflow. Also use when the user wants to batch-create video projects, insert media into CapCut, or manage CapCut project files.
SKILL.md
22.4 KB, as published. Nobody here has run it
CapCut Desktop Project Skill
Programmatically read, create, and modify CapCut desktop (macOS) project files.
Important Limitation
Only locally-created projects can be read/written. Projects downloaded from CapCut's
template library have encrypted draft_info.json files (base64-encoded with proprietary
ByteDance encryption) and cannot be modified. Always check by attempting json.loads() β
if it fails, the project is encrypted.
Project Location
BASE = ~/Movies/CapCut/User Data/Projects/com.lveditor.draft/
Quick Reference
Read references/draft-schema.md for the complete JSON schema with field descriptions
and time conversion rules.
Commands
List all projects
python3 -c "
import json, os
base = os.path.expanduser('~/Movies/CapCut/User Data/Projects/com.lveditor.draft')
with open(os.path.join(base, 'root_meta_info.json')) as f:
data = json.load(f)
for d in data['all_draft_store']:
name = d.get('draft_name', d['draft_fold_path'].split('/')[-1])
dur = d.get('tm_duration', 0)
secs = dur / 1000000 if dur else 0
print(f'{name:30s} {secs:>6.1f}s {d[\"draft_fold_path\"]}')
"
Check if a project is readable (not encrypted)
python3 -c "
import json, os
base = os.path.expanduser('~/Movies/CapCut/User Data/Projects/com.lveditor.draft')
path = os.path.join(base, 'PROJECT_NAME', 'draft_info.json')
try:
with open(path) as f: json.load(f)
print('READABLE (plain JSON)')
except:
print('ENCRYPTED (template project β cannot modify)')
"
Read project summary
python3 -c "
import json, os
base = os.path.expanduser('~/Movies/CapCut/User Data/Projects/com.lveditor.draft')
with open(os.path.join(base, 'PROJECT_NAME', 'draft_info.json')) as f:
d = json.load(f)
print(f'ID: {d[\"id\"]}')
print(f'Duration: {d[\"duration\"]/1000000:.1f}s')
print(f'FPS: {d[\"fps\"]}')
print(f'Canvas: {d[\"canvas_config\"][\"width\"]}x{d[\"canvas_config\"][\"height\"]}')
tracks = d.get('tracks', [])
print(f'Tracks: {len(tracks)}')
for i, t in enumerate(tracks):
segs = t.get('segments', [])
print(f' Track {i}: {t[\"type\"]} ({len(segs)} segments)')
mats = d.get('materials', {})
for k, v in mats.items():
if isinstance(v, list) and len(v) > 0:
print(f' materials.{k}: {len(v)}')
"
Create a new project from scratch
python3 scripts/create-project.py <name> [--width 1920] [--height 1080] [--fps 30]
View and edit clips
# Show full timeline overview (tracks, segments, timing, transforms)
python3 scripts/edit-clips.py <project> --info
# Scale clips (1.0 = 100%, 1.5 = 150%)
python3 scripts/edit-clips.py <project> --scale 1.2 --clips 0 5 10
python3 scripts/edit-clips.py <project> --scale 1.1 --range 3-19
# Speed (0.5 = half speed, 2.0 = double)
python3 scripts/edit-clips.py <project> --speed 2.0 --clips 5
python3 scripts/edit-clips.py <project> --speed 0.5 --all
# Volume (0.0 = mute, 1.0 = normal, 2.0 = double)
python3 scripts/edit-clips.py <project> --volume 0 --range 0-2
python3 scripts/edit-clips.py <project> --mute --all
# Rotate/flip/opacity
python3 scripts/edit-clips.py <project> --rotate 90 --clips 2
python3 scripts/edit-clips.py <project> --flip-h --clips 3
python3 scripts/edit-clips.py <project> --opacity 0.8 --clips 0
# Trim clip to first N seconds
python3 scripts/edit-clips.py <project> --trim 3.5 --clips 0
# Split clip at position (seconds from clip start)
python3 scripts/edit-clips.py <project> --split 2.5 --clips 0
# Remove clips
python3 scripts/edit-clips.py <project> --remove --clips 10 11 12
# Move clip from index to index
python3 scripts/edit-clips.py <project> --move 5 --to 10
# Reset transforms
python3 scripts/edit-clips.py <project> --reset --clips 0 1 2
Edit audio tracks
# Audio tracks shown in --info output
python3 scripts/edit-clips.py <project> --info
# Volume (0.0=mute, 1.0=normal)
python3 scripts/edit-clips.py <project> --audio-track --volume 0.5 --all
python3 scripts/edit-clips.py <project> --audio-track --mute --clips 0
# Move audio segment to align with video (seconds from timeline start)
python3 scripts/edit-clips.py <project> --audio-track --position 10.5 --clips 0
# Select which audio track (0=first, 1=second, etc.)
python3 scripts/edit-clips.py <project> --audio-track --track-index 2 --volume 0.8 --all
# Speed up/slow down audio
python3 scripts/edit-clips.py <project> --audio-track --speed 1.5 --clips 0
# Trim audio to first N seconds
python3 scripts/edit-clips.py <project> --audio-track --trim 5.0 --clips 0
# Remove audio segment
python3 scripts/edit-clips.py <project> --audio-track --remove --clips 2
# Fade in/out (seconds)
python3 scripts/edit-clips.py <project> --audio-track --fade-in 0.5 --clips 0
python3 scripts/edit-clips.py <project> --audio-track --fade-out 1.0 --clips 0
Add video effects (zoom, blur, sparkle, etc.)
# List built-in effect shortcuts (20 effects)
python3 scripts/add-effect.py --list-effects
# Scan ALL cached effects (404+ downloaded effects)
python3 scripts/add-effect.py --scan-cache
# Add Zoom Lens to all clips
python3 scripts/add-effect.py <project> --effect zoom-lens --all
# Add Sparkle to specific clips
python3 scripts/add-effect.py <project> --effect sparkle --clips 0 5 10
# Add any cached effect by ID (from --scan-cache)
python3 scripts/add-effect.py <project> --effect-id 7399469087174233349 --all
# Add Blur to a range with custom parameters
python3 scripts/add-effect.py <project> --effect blur --range 3-19 --speed 0.02 --param-range 0.06
# Remove all effects
python3 scripts/add-effect.py <project> --clear
Add transitions between clips
# List available transitions
python3 scripts/add-transition.py --list
# Add transition between two adjacent clips
python3 scripts/add-transition.py <project> --between 19 20
# Add transitions between all consecutive clips
python3 scripts/add-transition.py <project> --all-gaps
# Add at specific gaps with custom duration
python3 scripts/add-transition.py <project> --gaps 2 5 10 --duration 0.5
# Remove all transitions
python3 scripts/add-transition.py <project> --clear
Add color grading (HSL)
# Show current color settings per clip
python3 scripts/add-color.py <project> --info
# Custom HSL values (hue: -180~180, sat: -100~100, light: -100~100)
python3 scripts/add-color.py <project> --hsl --hue 10 --saturation 20 --lightness -5 --clips 0 1 2
python3 scripts/add-color.py <project> --hsl --saturation 30 --all
# Presets
python3 scripts/add-color.py <project> --preset warm --all
python3 scripts/add-color.py <project> --preset vivid --range 0-10
python3 scripts/add-color.py <project> --preset moody --all
# Available presets: warm, cool, vivid, moody, vintage, fade
# Remove all color grading
python3 scripts/add-color.py <project> --clear
Add text overlays
# Single text
python3 scripts/add-text.py <project> --text "Hello" --start 0 --duration 5 --y -0.65 --size 14
# With a specific font
python3 scripts/add-text.py <project> --text "ΰΈͺΰΈ§ΰΈ±ΰΈͺΰΈΰΈ΅" --font sarabun-bold --size 14
python3 scripts/add-text.py <project> --text "Hello" --font /full/path/to/font.ttf
# List available font aliases
python3 scripts/add-text.py --list-fonts
# From a JSON file
python3 scripts/add-text.py <project> --from-file texts.json
# Remove all text
python3 scripts/add-text.py <project> --clear
JSON file format (supports font field):
[
{"text": "Opening title", "start": 0, "duration": 4.5, "y": -0.65, "size": 14, "font": "thai"},
{"text": "Subtitle", "start": 5.4, "duration": 2, "y": 0.7, "size": 10, "font": "sarabun-bold"}
]
Built-in font aliases: default, en, th/thai, noto, noto-thai, capcut/capcut-bold/capcut-medium,
sarabun/sarabun-bold/sarabun-light/sarabun-med/sarabun-thin, thsarabun/thsarabun-new,
db-helv-med/db-helv-bold. Or pass any full path to a .ttf/.otf file.
Create project manually
The create-project.py script handles everything. Alternatively, create manually:
- Generate 3 separate UUIDs:
project_json_id,timeline_id,draft_id - Create the folder structure (22 files total β see below)
- CapCut MUST be closed β it overwrites
root_meta_info.jsonon launch
Required file structure (22 files)
PROJECT_NAME/
βββ draft_info.json # Main project data
βββ draft_info.json.bak # Backup (identical to draft_info.json)
βββ draft_meta_info.json # Project metadata (plain JSON)
βββ draft_settings # INI format settings
βββ draft_agency_config.json # Resolution/agency config
βββ draft_biz_config.json # Empty file (0 bytes)
βββ performance_opt_info.json # Performance optimization
βββ timeline_layout.json # UI layout with timeline UUID
βββ attachment_pc_common.json # PC-specific attachments
βββ template-2.tmp # Full draft_info copy
βββ common_attachment/
β βββ attachment_pc_timeline.json # Timeline reference lines config
βββ Timelines/
βββ project.json # Timeline registry (separate id!)
βββ project.json.bak # Backup
βββ {timeline_id}/ # Named with timeline_id UUID
βββ draft_info.json # Same content as root draft_info.json
βββ draft_info.json.bak
βββ attachment_editing.json # Edit state
βββ attachment_pc_common.json # Same as root level
βββ template.tmp # Draft info with canvas w/h = 0
βββ template-2.tmp # Full draft_info copy
βββ common_attachment/
βββ attachment_action_scene.json
βββ attachment_gen_ai_info.json
βββ attachment_pc_timeline.json
βββ attachment_script_video.json
UUID scheme
CapCut uses 3 separate UUIDs for each project:
| UUID | Used in | Notes |
|---|---|---|
project_json_id | Timelines/project.json β id | Separate from timeline |
timeline_id | Timelines/project.json β main_timeline_id, draft_info.json β id, timeline folder name | The primary project UUID |
draft_id | root_meta_info.json β draft_id, draft_meta_info.json β draft_id | Registry identifier |
Key parameters
canvas_config.width/heightβ 1080Γ1920 for 9:16 portrait, 1920Γ1080 for 16:9 landscapefpsβ 30.0 (standard)create_time/update_timeβ microseconds (0 for blank project)tm_draft_create/tm_draft_modifiedβ microseconds (Unix timestamp Γ 1,000,000)
Register in root_meta_info.json
When creating a project, add an entry to all_draft_store in root_meta_info.json:
import json, os, uuid, time
BASE = os.path.expanduser("~/Movies/CapCut/User Data/Projects/com.lveditor.draft")
dst = os.path.join(BASE, "PROJECT_NAME")
meta_path = os.path.join(BASE, "root_meta_info.json")
with open(meta_path) as f:
meta = json.load(f)
meta["all_draft_store"].insert(0, {
"cloud_draft_cover": False,
"cloud_draft_sync": False,
"draft_cloud_last_action_download": False,
"draft_cloud_purchase_info": "",
"draft_cloud_template_id": "",
"draft_cloud_tutorial_info": "",
"draft_cloud_videocut_purchase_info": "",
"draft_cover": os.path.join(dst, "draft_cover.jpg"),
"draft_fold_path": dst,
"draft_id": str(uuid.uuid4()).upper(),
"draft_is_ai_shorts": False,
"draft_is_cloud_temp_draft": False,
"draft_is_invisible": False,
"draft_is_web_article_video": False,
"draft_json_file": os.path.join(dst, "draft_info.json"),
"draft_name": "PROJECT_NAME",
"draft_new_version": "",
"draft_root_path": BASE,
"draft_timeline_materials_size": 4106,
"draft_type": "",
"draft_web_article_video_enter_from": "",
"streaming_edit_draft_ready": True,
"tm_draft_cloud_completed": "",
"tm_draft_cloud_entry_id": -1,
"tm_draft_cloud_modified": 0,
"tm_draft_cloud_parent_entry_id": -1,
"tm_draft_cloud_space_id": -1,
"tm_draft_cloud_user_id": -1,
"tm_draft_create": int(time.time() * 1_000_000),
"tm_draft_modified": int(time.time() * 1_000_000),
"tm_draft_removed": 0,
"tm_duration": 0,
})
if isinstance(meta.get("draft_ids"), int):
meta["draft_ids"] += 1
with open(meta_path, "w") as f:
json.dump(meta, f, separators=(",", ":"))
Add a video clip to a project
- Generate UUIDs for: segment, video material, canvas material, speed material, placeholder_info, sound_channel_mapping, vocal_separation, material_color
- Add a
videomaterial tomaterials.videoswith the video file path and metadata - Add a
canvas_colormaterial tomaterials.canvases - Add entries to
materials.speeds,materials.placeholder_infos,materials.sound_channel_mappings,materials.vocal_separations,materials.material_colors - Add a segment to the target video track with:
material_idβ video material UUIDextra_material_refsβ [speed_uuid, canvas_uuid, placeholder_uuid, sound_ch_uuid, vocal_sep_uuid, material_color_uuid]target_timerangeβ { start: cumulative position, duration: clip length in ΞΌs }source_timerangeβ { start: 0, duration: clip length in ΞΌs }
- Update
durationat the project root level
Add an audio track
- Generate UUIDs for: segment, audio material, audio_fade, beats, speed, placeholder_info, sound_channel_mapping, vocal_separation
- Add
audiomaterial tomaterials.audios - Add entries to
materials.audio_fades,materials.beats,materials.speeds,materials.placeholder_infos,materials.sound_channel_mappings,materials.vocal_separations - Either add a new track of
type: "audio"or add segment to existing audio track - Segment
clipshould benullfor audio
Add a text overlay
- Generate UUID for: segment and text material
- Add text material to
materials.textswith:contentβ JSON-encoded string withtextandstylesarrayfont_size,text_color,alignment,line_max_width
- Add segment to a
type: "text"track (create one if needed)source_timerangeshould benullfor textclip.transform.ycontrols vertical position (e.g., 0.7 = lower third)
Text content field format
import json
content = json.dumps({
"text": "Your text here",
"styles": [{
"fill": {
"content": {
"solid": {"color": [1.0, 1.0, 1.0]},
"render_type": "solid"
}
},
"range": [0, len("Your text here")],
"strokes": [{
"width": 0.06,
"content": {
"solid": {"color": [0, 0, 0]},
"render_type": "solid"
}
}],
"size": 15,
"font": {
"path": "/Applications/CapCut.app/Contents/Resources/Font/SystemFont/en.ttf",
"id": ""
}
}]
})
Colors in styles are [R, G, B] normalized 0.0-1.0 (not hex).
Add video effects (zoom, blur, sparkle, etc.)
Video effects are placed on type: "effect" tracks and reference entries in
materials.video_effects. Each segment on an effect track matches the timing of
a video clip it applies to. Multiple effects can share one track via separate segments.
Structure:
- Create a video_effect material in
materials.video_effects - Create a segment on an
effecttrack withtarget_timerangematching the video clip - The segment's
material_idpoints to the video_effect entry
import uuid, os
CACHE_BASE = "/Users/admin/Library/Containers/com.lemon.lvoverseas/Data/Movies/CapCut/User Data/Cache/effect"
def gen_uuid():
return str(uuid.uuid4()).upper()
# Create a video effect material
effect_id = "7399465441057328389" # Zoom Lens
effect_hash = "60a68556b7df52cc36d20d1f565b4569"
effect_path = os.path.join(CACHE_BASE, effect_id, effect_hash)
mat_id = gen_uuid()
video_effect = {
"id": mat_id,
"effect_id": effect_id,
"resource_id": effect_id,
"name": "Zoom Lens",
"type": "video_effect",
"path": effect_path,
"adjust_params": [
{"name": "effects_adjust_speed", "value": 0.01, "default_value": 0.33},
{"name": "effects_adjust_range", "value": 0.07, "default_value": 0.3}
],
"category_id": "27296",
"category_name": "Trending",
"apply_target_type": 2,
"platform": "all",
"source_platform": 1,
"effect_type": 0,
"is_ai_generate_effect": False,
"is_third_party": False,
"meta": "",
"request_id": "",
"resource_name": "",
"sub_effects": [],
"track_id": "",
}
# Add to materials
draft["materials"]["video_effects"].append(video_effect)
# Create effect segment (timing matches video clip)
effect_segment = {
"id": gen_uuid(),
"material_id": mat_id,
"target_timerange": {"start": clip_start_us, "duration": clip_duration_us},
"source_timerange": None,
"render_timerange": None,
"clip": None,
"speed": 1.0,
"volume": 1.0,
"visible": True,
"extra_material_refs": [],
"render_index": 11001,
"keyframe_refs": {},
"common_keyframes": {},
"group_id": "",
"track_render_index": 2,
"is_placeholder": False,
}
# Add to an existing effect track, or create one
effect_track = {
"id": gen_uuid(),
"type": "effect",
"flag": 0,
"attribute": 0,
"name": "",
"is_default_name": True,
"segments": [effect_segment],
}
draft["tracks"].append(effect_track)
Key notes:
- Effect segments have
clip: null,source_timerange: null,extra_material_refs: [] - Multiple effect segments for the same effect can go on a single effect track
render_index: 11001andtrack_render_index: 2are typical values (from observed projects)- Each effect needs its own video_effect material (separate UUID) even if using the same effect_id
- Effect files must exist in the CapCut cache (user must have used the effect at least once)
Discover available effects from cache
# Scan all 404+ cached video effects
python3 scripts/add-effect.py --scan-cache
Or manually scan the cache:
python3 -c "
import os, json
CACHE = '/Users/admin/Library/Containers/com.lemon.lvoverseas/Data/Movies/CapCut/User Data/Cache/effect'
for eid in sorted(os.listdir(CACHE)):
epath = os.path.join(CACHE, eid)
if not os.path.isdir(epath): continue
for h in os.listdir(epath):
hpath = os.path.join(epath, h)
config = os.path.join(hpath, 'config.json')
if os.path.exists(config):
try:
with open(config) as f: data = json.load(f)
name = data.get('name', '')
if name: print(f'{eid:>20s} {h} {name}')
except: pass
"
Get effect info from an existing project
If a user has applied an effect in CapCut, extract the full structure:
python3 -c "
import json, os
base = os.path.expanduser('~/Movies/CapCut/User Data/Projects/com.lveditor.draft')
with open(os.path.join(base, 'PROJECT_NAME', 'draft_info.json')) as f:
d = json.load(f)
for ve in d['materials'].get('video_effects', []):
print(f' {ve[\"name\"]:30s} effect_id={ve[\"effect_id\"]}')
print(f' adjust_params={ve.get(\"adjust_params\", [])}')
print(f' path={ve.get(\"path\", \"\")}')
"
Built-in effect reference
These effects are commonly cached after using CapCut. The effect_id is the identifier
used in video_effect.effect_id and the cache directory name. The hash is the
subdirectory inside the effect's cache folder.
| Effect | effect_id | hash | adjust_params |
|---|---|---|---|
| Zoom Lens | 7399465441057328389 | 60a68556b7df52cc36d20d1f565b4569 | speed, range |
| Slow Zoom | 7399468961949125894 | 56b46f1ebc45c47730d3f7c2569200fc | β |
| Slight Zoom | 7399463624906984709 | c09004507723569a3e762494d4ffda7d | β |
| Full Zoom | 7399470808759815429 | 24749b428adbacfa9712b8a249912905 | β |
| Zoom Far | 6724226338418332167 | 8d97f1c1a60d9393c97ff4e9da0669ae | β |
| Blur | 7399464929830423813 | 2db7bf49d9349e308ef0f46c39b14abf | β |
| Blur Opening | 7399468886309162246 | 5dd4bf7e879fe7356e3e27e5105f5af1 | β |
| Soft Light | 7399467970071743749 | 258b5bd7ba1fb94dce800bc496a30ed9 | β |
| Bokeh | 7399470883863088389 | 1c8442102d00628a4958e488251a75e7 | β |
| Dark Corner | 7399463239379209477 | ef7abad9671e2f3da7993b7673ece5fc | β |
| Sparkle | 7399469087174233349 | 816803366dd866837e21380513b81e33 | β |
| Flashing Light | 7399472112223669510 | d7c42c303074967c0cad7c7a6adfe896 | β |
| Color Shift | 7399470160203107589 | 53c8584c8174f887b2802540dd28955b | β |
| Fog | 7399471802361105669 | bdda3043cfa04aa56d2806ada93367ae | β |
| Oil Paint | 11353735 | 118b5e6a07a603581825a0fa8bb08e35 | β |
| Rainbow Bubble | 7399470727121947910 | ae2e32daa7af0fa8f4b61a0c5aacd196 | β |
| Petals Falling | 7399464130664500486 | a772c059e7fb8304292e7ebb870e8eb3 | β |
| Curtain Close | 7399468499044683013 | 05c17ac3298c0521cd91a720850a27de | β |
| Star Shift | 7399470054053547270 | f1c6583c2a7227b6ccf002863fdfdf65 | β |
Note: Effect availability depends on what the user has downloaded/used in CapCut. Always verify the cache path exists before using an effect. If an effect isn't cached, ask the user to apply it once in CapCut, then close CapCut and re-read the project.
Time System
All time values in CapCut are microseconds (ΞΌs).
1 second = 1,000,000 ΞΌs
30fps frame β 33,333 ΞΌs
UUID Generation
import uuid
str(uuid.uuid4()).upper()
# Example: "595F710B-F0C1-44C6-AA60-525CF85705D5"
Safety
- CapCut MUST be closed when creating/modifying projects β it overwrites files on launch
- Always back up
draft_info.jsonbefore modifying - Always back up
root_meta_info.jsonbefore adding/removing projects - To check if CapCut is running:
pgrep -fl "CapCut|lvoverseas|lemon"