CapCut / JianYing draft_content.json schema cheat sheet: every top-level key, what it does, version differences
A field-by-field reference for the CapCut / JianYing project format — what's in materials, what's in tracks, which version added which field, and the jq one-liners that pull data out cleanly.
Last tested: May 2026 (capcut-cli v0.5.0; CapCut 6.x–9.x, JianYing 5.9.0). See Changelog at the bottom.
If this saves you a reverse-engineering session, follow @renezander030 — more notes on the CapCut / JianYing draft format and the CLI that edits it.
Source for every field below: github.com/renezander030/capcut-cli —
docs/draft-schema/(~3,700 lines), distilled here.
| Key | What it stores | Which capcut command touches it |
|---|---|---|
id |
draft UUID | init (sets it once) |
name |
user-visible project name | init, info (read) |
duration |
timeline length in microseconds | every add-/cut-/import- command recomputes it |
fps |
24 / 25 / 30 / 50 / 60 | init |
canvas_config.{width,height,ratio} |
pixel canvas + "9:16" / "16:9" / "1:1" / "4:5" | init |
platform.app_source |
"cc" (CapCut) or "lv" (JianYing) — drives enum namespace |
read by version, enums |
platform.app_version |
exact app version that wrote the draft | read by version, decrypt, lint |
tracks[] |
ordered list of timeline lanes; array order = z-order | tracks, segments, add-* |
tracks[].segments[] |
the clips themselves; reference materials by material_id |
segments, segment <id>, set-text, mix-mode, audio-fade, etc. |
materials.videos[] |
video clips AND images (discriminator: type: "video" vs "photo") |
add-video, materials --type videos, material <id> |
materials.audios[] |
VO, music, SFX, recordings | add-audio, add-sfx, audio-fade |
materials.texts[] |
text bodies + per-range styling (content field is JSON-in-JSON) | add-text, set-text, text-style, text-ranges, bubble-text, import-srt, import-ass, caption |
materials.stickers[] |
sticker references (resource_id from CapCut library) | add-sticker |
materials.video_effects[] |
scene effects AND filter chain | add-effect, add-filter, chroma |
materials.material_animations[] |
intro/outro/combo animations on video/image segments | (companion of video segment via extra_material_refs) |
materials.transitions[] |
transition between segments | (companion of segment) |
materials.masks[] (legacy) / materials.common_masks[] (new) |
shape masks; field name depends on app version | mask, migrate |
materials.canvases[] |
background blur / background colour | (companion of video segment) |
materials.speeds[] |
playback speed envelopes | (companion of video segment) |
materials.audio_fades[] |
fade-in / fade-out objects on audio segments | audio-fade (v0.5) |
materials.placeholder_infos[] |
missing-asset placeholders | written by CapCut when source file is gone |
materials.vocal_separations[] |
VO/music split metadata | written by CapCut's separator |
materials.sound_channel_mappings[] |
stereo/mono routing | usually CapCut-set defaults |
materials.smart_crops[] / manual_deformations[] |
aspect-ratio crops + manual deformations | CapCut-set; rarely edited |
cover (a.k.a. cover_info) |
the thumbnail frame for the draft | add-cover (v0.5) |
extra_info |
app-specific scratchpad | preserve, don't edit |
last_modified_platform |
last app that touched the draft | informational |
new_version |
non-null on some CapCut International builds | informational |
free_render_index_mode_on |
always false for CLI-created drafts |
preserve, don't edit |
The format is flat and decoupled: a segment in a track does not contain its material inline — it carries a material_id UUID that points into one of the materials.<category>[] arrays. To answer "what is this clip?", you do segment.material_id → materials.<type> → find by id.
You don't need to memorise the schema. You need three things:
capcut info <project>for the read overview (track counts, material counts, duration).jqone-liners for ad-hoc extraction (see Useful jq one-liners below).capcut <command>for any write — the CLI knows which schema fields go together (e.g.audio-fadewrites bothmaterials.audio_fades[]and asegment.extra_material_refs[]pointer).
Hand-editing draft_content.json is reasonable for info-level inspection. It is not reasonable for writes — text content lives in a JSON-encoded content string with UTF-16 byte-offset ranges, and getting it wrong silently breaks the draft.
{
"id": "test-project-001",
"name": "Test Project",
"duration": 10000000, // microseconds total timeline
"fps": 30,
"canvas_config": {
"width": 1080, "height": 1920,
"ratio": "9:16"
},
"platform": {
"app_source": "cc", // "cc" = CapCut International; "lv" = JianYing (CN)
"app_version": "9.0.0",
"os": "mac"
},
"tracks": [ // top of array = bottom of z-stack
{ "id": "...", "type": "video", "segments": [...] },
{ "id": "...", "type": "text", "segments": [...] },
{ "id": "...", "type": "audio", "segments": [...] }
],
"materials": {
"videos": [...], // video files AND images
"audios": [...],
"texts": [...], // one per text segment, JSON-in-JSON content
"stickers": [...],
"video_effects": [...],
"material_animations":[...],
"transitions": [...],
"masks": [...], // legacy
"common_masks": [...], // new — see version table below
"canvases": [...],
"speeds": [...],
"audio_fades": [...], // v0.5 ships writes here
"placeholder_infos": [...],
"vocal_separations": [...]
},
"extra_info": { ... },
"free_render_index_mode_on": false
}Every segment in every track type has roughly this shape:
{
"id": "...-segment-uuid",
"material_id": "...-material-uuid", // (1) what this segment IS
"target_timerange": { // (2) where it plays on the timeline
"start": 0, "duration": 5000000
},
"source_timerange": { // (3) what slice of the source
"start": 0, "duration": 5000000
},
"extra_material_refs": [ // (4) companion materials: speeds, masks,
"uuid1", "uuid2" // animations, transitions, audio_fades
],
"clip": { "rotation": 0, "alpha": 1.0, "scale": {...}, "transform": {...}, "flip": {...} },
"speed": 1.0, "volume": 1.0, "visible": true,
"render_index": 0, "render_uniform_index": -1
}material_id→ primarymaterials.<type>[].id— defines what the segment is.target_timerange→ microseconds; position on the timeline.source_timerange→ microseconds; trim into the source material.extra_material_refs[]→ array of UUIDs into the companion materials (speeds, masks, audio_fades, transitions). The companion pattern is how CapCut keeps the schema "flat enough" to merge across versions.
type |
Used by | Notes |
|---|---|---|
video |
add-video (video files + still images) |
most segments live here |
audio |
add-audio, add-sfx |
seg.clip must be null |
text |
add-text, set-text, text-style, text-anim, text-ranges, bubble-text, import-srt, import-ass, caption |
each segment has exactly one text material |
image |
rare — JianYing-only static image lane | CapCut puts images on video track |
sticker |
add-sticker |
references CapCut library resource_id |
effect |
add-effect, add-filter |
scene effect / filter applied to the frame |
subtitle |
reserved for CapCut auto-subtitle | import-srt uses text by default |
filter |
filter chain | not exposed directly yet |
Tracks created by capcut-cli get sensible defaults. Rename via --track-name <name> on add-* commands. Subsequent add-* with the same name reuses the existing track.
This is the trickiest material. The actual text and per-range styling live inside a JSON-encoded string called content:
{
"id": "text-mat-uuid",
"type": "text",
"content": "{\"text\":\"Hello\",\"styles\":[...],\"layer_weight\":1,\"effect\":[]}",
"font_name": "", "font_size": 8.0,
"text_color": "#FFFFFFFF",
"border_color": "#000000FF", "border_width": 0.0,
"has_shadow": false, "shadow_color": "#000000FF", "shadow_distance": 8.0,
"background_color": "#00000000",
"text_alignment": 1, // 0=left, 1=centre, 2=right
"vertical": false
}And the parsed content:
{
"text": "Hello world",
"styles": [
{
"range": [0, 10], // UTF-16 BYTE offsets, not chars
"fill": { "content": { "solid": { "color": [1.0, 0.84, 0.0] } } },
"font": { "id": "...", "path": "..." },
"size": 18, "bold": true, "italic": false
}
],
"layer_weight": 1,
"effect": []
}The range array is in UTF-16 little-endian byte offsets. For ASCII, byte index = character index. For Chinese / emoji, each char is 2 bytes (BMP) or 4 bytes (non-BMP). capcut set-text and capcut text-ranges handle the conversion — hand-editing content is the source of most CapCut "text disappeared" bugs.
| Field | CapCut 6.x–9.x (app_source: cc) |
JianYing 5.9 (app_source: lv) |
JianYing 6.0+ (app_source: lv) |
Notes |
|---|---|---|---|---|
draft_content.json file format |
plain JSON | plain JSON | encrypted blob | See encryption gist |
materials.masks[] (legacy mask_field) |
yes (≤9.5) | yes | encrypted | capcut migrate for ≥9.6 |
materials.common_masks[] (new mask_field) |
yes (≥9.6) | — | encrypted | capcut mask writes legacy; migrate converts |
materials.audio_fades[] |
yes (all versions) | yes (5.9) | encrypted | capcut audio-fade writes here (v0.5) |
materials.texts[].content.styles[].range[] |
byte-offset (UTF-16 LE) | byte-offset | byte-offset | text-ranges (v0.3+) |
new_version (top-level) |
sometimes non-null on Intl. builds | null | encrypted | informational |
cover / cover-image validation |
lenient ≤9.x, stricter on 10.x+ | n/a | n/a + 10.3 mac "内容已损坏" bug | add-cover (v0.5) |
| transitions enum namespace | CapCut slugs | JianYing slugs | encrypted | enums --jianying switches |
Run capcut version <project> to get an exact verdict:
capcut version ./project
# → {"app":"cc","app_version":"9.0.0",
# "support":{"status":"tested"},
# "schema":{"mask_field":"common_masks","has_text_ranges":true,
# "has_audio_fades":true,"new_version_field":null,
# "last_modified_platform":"mac"}}These run against any unencrypted draft_content.json (or draft_info.json on macOS):
# project overview — name, duration in seconds, fps, canvas
jq '{name, duration_s: (.duration/1e6), fps, canvas: .canvas_config}' draft_content.json
# every text segment with its rendered string
jq '.materials.texts[] | {id, content: (.content | fromjson | .text)}' draft_content.json
# every track with type and segment count
jq '.tracks[] | {id, type, name, segments: (.segments | length)}' draft_content.json
# every segment on the video track with start + duration in seconds
jq '.tracks[] | select(.type=="video") | .segments[] |
{id, start_s: (.target_timerange.start/1e6),
dur_s: (.target_timerange.duration/1e6),
material_id}' draft_content.json
# join segment → material to get the source video filename
jq '.materials.videos as $v | .tracks[] | select(.type=="video") | .segments[] |
{seg: .id, mat: .material_id,
path: ($v[] | select(.id==(. // ""))) }' draft_content.json # see capcut-cli `segment <id>` for proper join
# detect encryption stance + version
jq '{app: .platform.app_source, ver: .platform.app_version,
has_common_masks: (.materials.common_masks != null),
has_audio_fades: (.materials.audio_fades != null)}' draft_content.json
# every audio segment with fade durations
jq '.materials.audio_fades[]?' draft_content.jsonFor the joins (segment → material), prefer capcut segment <id> and capcut material <id> — they walk extra_material_refs[] for you and return the resolved companion bundle.
| Command | What it reads | Output shape |
|---|---|---|
capcut info <project> |
top-level fields + track counts + per-material counts | summary JSON |
capcut tracks <project> |
tracks[] |
array of {id, type, name, segment_count} |
capcut materials <project> |
every key under materials.* |
per-category counts |
capcut materials <project> --type texts |
materials.texts[] |
array of materials |
capcut segments <project> [--track <type>] |
tracks[].segments[] filtered by track type |
array of timed segments |
capcut texts <project> |
text segments joined with text materials | array of {id, start_us, duration_us, text} |
capcut segment <project> <id> |
one segment + its primary material + all companions | resolved bundle |
capcut material <project> <id> |
one material in full | raw material object |
capcut version <project> |
platform.* + schema flags |
version verdict + support status |
capcut decrypt <project> |
heuristic on file shape | {encrypted, app, app_version, workaround} |
Reading is what the schema is for. Writing is what the CLI is for.
The schema cheat sheet above is a distillation. The full reference is in capcut-cli/docs/draft-schema/ — seven files, ~3,700 lines, covering every field including the ones I left out (animations sub-structure, clip transform math, keyframe shape, mask geometry, vocal separation params). Grep that directory for any field name you've seen in a draft.
- Driving CapCut / JianYing video drafts from an LLM agent (PAAN #4) — agent-style edit pipeline on top of this schema
- JianYing 6.0+ encryption: detection and workarounds — what to do when you can't read the draft at all
- pyJianYingDraft — the Python alternative; their docstrings are another good schema source
If you find a field that's not in this cheat sheet (or in the full docs/draft-schema/), comment with:
- The field path (e.g.
materials.video_effects[].adjust_params[]) - A sample value
- The app + version it appeared in
- What it controls in the CapCut / JianYing UI
Particularly wanted: full anatomy of clip.transform for non-trivial keyframed motion, and the vocal_separations[] payload shape on JianYing 5.9.
- Initial publication. Distilled from
capcut-cli/docs/draft-schema/for capcut-cli v0.5.0. - Covers CapCut 6.x–9.x, JianYing 5.9.0; encryption-blocked versions noted but not field-resolved.
Independent project — not affiliated with or endorsed by ByteDance. "CapCut"/"JianYing" are trademarks of their owner; used nominatively.
Hi @vhly, thanks, this matches my findings. The encrypt-on-every-save behavior in 6.0+ is covered in the companion encryption gist (https://gist.github.com/renezander030/521e6c6e8590a2a6e917009d9313bc55): once 6.0+ re-saves a draft there is no way back, so the only clean pattern is generate, don't roundtrip.
The online templates being encrypted already on 5.9 is new to me. My 5.9.0 fixtures are all local drafts, which are plain JSON. If templates pulled from the online library arrive encrypted even on 5.9, that deserves its own row in the version table. How did you spot it, which folder or endpoint?
And yes, please share the link to the DLL repo. Using the app's own DLL is the first approach I've heard of that wouldn't go stale on every release, unlike the community decrypt scripts. Expected catches: Windows only, and the legal posture is murky since it invokes proprietary code, which is why capcut-cli stays on the detection side. I'd still like to study it.