capcut-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/draft.js +116 -0
- package/dist/factory.js +370 -0
- package/dist/index.js +705 -0
- package/dist/time.js +74 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rene Zander
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# capcut-cli
|
|
2
|
+
|
|
3
|
+
Stop reverse-engineering CapCut's JSON schema every time you need to change a subtitle.
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
CapCut stores projects as `draft_content.json` -- deeply nested, undocumented, with timing in microseconds and text buried inside escaped JSON-in-JSON. Every manual edit means: find the right segment ID, trace it to the material, figure out the content format, convert your timestamp, edit, pray you didn't break the structure. **15 seconds per change**, minimum.
|
|
8
|
+
|
|
9
|
+
`capcut-cli` already knows the schema. One command, one change, **5 seconds**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
$ capcut texts ./project
|
|
13
|
+
[{"id":"a1b2c3d4-...","start_us":500000,"duration_us":2500000,"text":"Welcome to the video"}]
|
|
14
|
+
|
|
15
|
+
$ capcut set-text ./project a1b2c3 "Fixed subtitle"
|
|
16
|
+
{"ok":true,"id":"a1b2c3d4-...","old":"Welcome to the video","new":"Fixed subtitle"}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Zero dependencies. JSON output by default. Pipeable. Works with CapCut and JianYing.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g capcut-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or run directly:
|
|
28
|
+
```bash
|
|
29
|
+
npx capcut-cli info ./my-project/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Output modes
|
|
33
|
+
|
|
34
|
+
**JSON (default)** -- pipe to `jq`, feed to scripts, consume from agents:
|
|
35
|
+
```bash
|
|
36
|
+
capcut texts ./project | jq '.[].text'
|
|
37
|
+
capcut info ./project | jq '.duration_us'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Human-readable** (`-H` / `--human`):
|
|
41
|
+
```bash
|
|
42
|
+
capcut texts ./project -H
|
|
43
|
+
ID Start -End Text
|
|
44
|
+
a1b2c3d4 0:00.50- 0:03.00 Welcome to the video
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Quiet** (`-q` / `--quiet`) -- exit code only, zero stdout on writes:
|
|
48
|
+
```bash
|
|
49
|
+
capcut set-text ./project a1b2c3 "New text" -q && echo "done"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
### Overview (start here)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
capcut info ./project # Project overview + material summary
|
|
58
|
+
capcut tracks ./project # List all tracks
|
|
59
|
+
capcut materials ./project # List all material types + counts
|
|
60
|
+
capcut materials ./project --type audios # List items of one material type
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Browse
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
capcut segments ./project # List all segments with timing
|
|
67
|
+
capcut segments ./project --track text # Filter by track type
|
|
68
|
+
capcut texts ./project # List all text/subtitle content
|
|
69
|
+
capcut export-srt ./project > subs.srt # Export subtitles to SRT
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Detail (drill into one item)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
capcut segment ./project a1b2c3 # Full detail for one segment + its material
|
|
76
|
+
capcut material ./project a1b2c3 # Full detail for one material
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Progressive disclosure: `info` shows the shape, `materials` shows what's available, `segment`/`material` shows everything about one item. An AI agent navigates overview → list → detail, never gets more data than it needs.
|
|
80
|
+
|
|
81
|
+
### Add
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
capcut add-text ./project 0s 5s "Title" --font-size 24 --color "#FFD700" --y -0.4
|
|
85
|
+
capcut add-text ./project 55s 5s "Subscribe!" --font-size 14 --align 1
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Options: `--font-size <n>`, `--color <hex>`, `--align <0|1|2>` (left/center/right), `--x <n> --y <n>` (position, -1 to 1), `--track-name <name>`.
|
|
89
|
+
|
|
90
|
+
### Edit
|
|
91
|
+
|
|
92
|
+
Every write command creates a `.bak` backup before modifying the file.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
capcut set-text ./project a1b2c3 "New subtitle"
|
|
96
|
+
capcut shift ./project a1b2c3 +0.5s
|
|
97
|
+
capcut shift ./project a1b2c3 -200ms
|
|
98
|
+
capcut shift-all ./project +1s
|
|
99
|
+
capcut shift-all ./project -0.5s --track text
|
|
100
|
+
capcut speed ./project a1b2c3 1.5
|
|
101
|
+
capcut volume ./project a1b2c3 0.8
|
|
102
|
+
capcut opacity ./project a1b2c3 0.5
|
|
103
|
+
capcut trim ./project a1b2c3 2s 5s
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Templates
|
|
107
|
+
|
|
108
|
+
Extract any element from a project as a reusable template, then stamp it into other projects. Works with text, stickers, shapes, video, audio -- anything that exists as a segment.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Save a styled text element as a template
|
|
112
|
+
capcut save-template ./project a1b2c3 "gold-title" --out gold-title.json
|
|
113
|
+
|
|
114
|
+
# Apply it to another project with new timing
|
|
115
|
+
capcut apply-template ./other-project gold-title.json 0s 5s
|
|
116
|
+
|
|
117
|
+
# Override the text content (keeps all styling -- font, color, size)
|
|
118
|
+
capcut apply-template ./project gold-title.json 5:00 4s "Chapter 3: The Forge"
|
|
119
|
+
|
|
120
|
+
# Save a sticker and reuse it
|
|
121
|
+
capcut save-template ./project d4e5f6 "subscribe-btn" --out subscribe.json
|
|
122
|
+
capcut apply-template ./project subscribe.json 9:50 5s --x 0.35 --y -0.35
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Templates preserve everything: styling, colors, font size, scale, resource IDs, shadow settings, shape params. Only the ID, timing, and optionally position/text get changed on apply.
|
|
126
|
+
|
|
127
|
+
**Workflow: build a template library**
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Create elements in CapCut, then extract them
|
|
131
|
+
mkdir -p ~/.capcut-templates
|
|
132
|
+
capcut save-template ./project abc123 "lower-third" --out ~/.capcut-templates/lower-third.json
|
|
133
|
+
capcut save-template ./project def456 "end-card" --out ~/.capcut-templates/end-card.json
|
|
134
|
+
capcut save-template ./project ghi789 "subscribe-cta" --out ~/.capcut-templates/subscribe-cta.json
|
|
135
|
+
|
|
136
|
+
# Stamp them into every new project
|
|
137
|
+
capcut apply-template ./new-project ~/.capcut-templates/lower-third.json 0s 5s "New Episode"
|
|
138
|
+
capcut apply-template ./new-project ~/.capcut-templates/end-card.json 9:55 5s
|
|
139
|
+
capcut apply-template ./new-project ~/.capcut-templates/subscribe-cta.json 9:50 5s
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Cut (long-form → short)
|
|
143
|
+
|
|
144
|
+
Extract a time range from a project into a new file. Clips edge segments, rebases timing to zero, removes empty tracks, cleans up orphaned materials.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# 60-second teaser from a 10-minute video
|
|
148
|
+
capcut cut ./project 1:00 2:00 --out ./teaser.json
|
|
149
|
+
|
|
150
|
+
# 30-second highlight
|
|
151
|
+
capcut cut ./project 3:00 3:30 --out ./highlight.json
|
|
152
|
+
|
|
153
|
+
# Then add titles to the short
|
|
154
|
+
capcut add-text ./teaser.json 0s 5s "MYCENAE" --font-size 24 --color "#FFD700"
|
|
155
|
+
capcut add-text ./teaser.json 55s 5s "Full video in description" --font-size 14
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Batch
|
|
159
|
+
|
|
160
|
+
Multiple edits, one JSON parse, one file write:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
echo '{"cmd":"set-text","id":"a1b2c3","text":"Line one"}
|
|
164
|
+
{"cmd":"set-text","id":"d4e5f6","text":"Line two"}
|
|
165
|
+
{"cmd":"shift","id":"a1b2c3","offset":"+0.3s"}
|
|
166
|
+
{"cmd":"volume","id":"g7h8i9","volume":0.5}' | capcut batch ./project
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Output: `{"ok":true,"succeeded":4,"failed":0}`
|
|
170
|
+
|
|
171
|
+
Batch tolerates per-operation errors and continues processing. Operations: `set-text`, `shift`, `shift-all`, `speed`, `volume`, `opacity`, `trim`.
|
|
172
|
+
|
|
173
|
+
### IDs
|
|
174
|
+
|
|
175
|
+
Segment and material IDs are UUIDs. The first 6-8 characters work as prefix match:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
$ capcut texts ./project | jq '.[0].id'
|
|
179
|
+
"a1b2c3d4-0000-0000-0000-000000000001"
|
|
180
|
+
|
|
181
|
+
$ capcut set-text ./project a1b2c3 "Hey everyone"
|
|
182
|
+
{"ok":true,"id":"a1b2c3d4-0000-0000-0000-000000000001","old":"Welcome","new":"Hey everyone"}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Time formats
|
|
186
|
+
|
|
187
|
+
- `1.5s` -- 1.5 seconds
|
|
188
|
+
- `500ms` -- 500 milliseconds
|
|
189
|
+
- `+0.5s` / `-1s` -- relative offset
|
|
190
|
+
- `1:30` -- 1 minute 30 seconds
|
|
191
|
+
- `0:05.5` -- 5.5 seconds
|
|
192
|
+
|
|
193
|
+
## How it works
|
|
194
|
+
|
|
195
|
+
CapCut stores projects as JSON (`draft_content.json` on Windows, `draft_info.json` on macOS). This CLI reads and modifies that JSON directly. It preserves the original file's indentation style on save.
|
|
196
|
+
|
|
197
|
+
Typical project location:
|
|
198
|
+
- **Windows**: `C:\Users\<you>\AppData\Local\CapCut\User Data\Projects\com.lveditor.draft\<id>\`
|
|
199
|
+
- **macOS**: `/Users/<you>/Movies/CapCut/User Data/Projects/com.lveditor.draft/<id>/`
|
|
200
|
+
|
|
201
|
+
Close the project in CapCut before editing, reopen after. CapCut reads the JSON on project open.
|
|
202
|
+
|
|
203
|
+
## Workflow: batch subtitle correction
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Get all subtitle IDs and text
|
|
207
|
+
capcut texts ./project | jq '.[] | "\(.id) \(.text)"'
|
|
208
|
+
|
|
209
|
+
# Fix 3 typos + sync timing in one shot
|
|
210
|
+
echo '{"cmd":"set-text","id":"a1b2c3","text":"Corrected line one"}
|
|
211
|
+
{"cmd":"set-text","id":"d4e5f6","text":"Corrected line two"}
|
|
212
|
+
{"cmd":"set-text","id":"g7h8i9","text":"Corrected line three"}
|
|
213
|
+
{"cmd":"shift-all","offset":"+0.3s","track":"text"}' | capcut batch ./project
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Four changes, one file write. Done in under 5 seconds.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
package/dist/draft.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export function findDraft(input) {
|
|
4
|
+
const resolved = resolve(input);
|
|
5
|
+
if (existsSync(resolved) && statSync(resolved).isFile())
|
|
6
|
+
return resolved;
|
|
7
|
+
const candidates = [
|
|
8
|
+
resolve(resolved, "draft_content.json"),
|
|
9
|
+
resolve(resolved, "draft_info.json"),
|
|
10
|
+
];
|
|
11
|
+
for (const p of candidates) {
|
|
12
|
+
if (existsSync(p) && statSync(p).isFile())
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`No draft found at: ${input}\nExpected draft_content.json or draft_info.json`);
|
|
16
|
+
}
|
|
17
|
+
let rawOriginal = null;
|
|
18
|
+
export function loadDraft(path) {
|
|
19
|
+
const filePath = findDraft(path);
|
|
20
|
+
rawOriginal = readFileSync(filePath, "utf-8");
|
|
21
|
+
const draft = JSON.parse(rawOriginal);
|
|
22
|
+
return { draft, filePath };
|
|
23
|
+
}
|
|
24
|
+
export function saveDraft(filePath, draft) {
|
|
25
|
+
const bakPath = filePath + ".bak";
|
|
26
|
+
if (existsSync(filePath)) {
|
|
27
|
+
const original = rawOriginal ?? readFileSync(filePath, "utf-8");
|
|
28
|
+
writeFileSync(bakPath, original, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
// Detect original indent: if first line after { starts with tab use tab, else count spaces
|
|
31
|
+
const indent = detectIndent(rawOriginal);
|
|
32
|
+
writeFileSync(filePath, JSON.stringify(draft, null, indent), "utf-8");
|
|
33
|
+
}
|
|
34
|
+
function detectIndent(raw) {
|
|
35
|
+
if (!raw)
|
|
36
|
+
return 0;
|
|
37
|
+
const match = raw.match(/\n(\s+)/);
|
|
38
|
+
if (!match)
|
|
39
|
+
return 0;
|
|
40
|
+
const ws = match[1];
|
|
41
|
+
if (ws.includes("\t"))
|
|
42
|
+
return "\t";
|
|
43
|
+
return ws.length;
|
|
44
|
+
}
|
|
45
|
+
export function extractText(content) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(content);
|
|
48
|
+
if (parsed.text)
|
|
49
|
+
return parsed.text;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return content.replace(/<[^>]*>/g, "").replace(/\[|\]/g, "").trim();
|
|
53
|
+
}
|
|
54
|
+
return content;
|
|
55
|
+
}
|
|
56
|
+
export function updateTextContent(content, newText) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(content);
|
|
59
|
+
if (parsed.text !== undefined) {
|
|
60
|
+
parsed.text = newText;
|
|
61
|
+
if (parsed.styles && parsed.styles.length > 0) {
|
|
62
|
+
const encoded = Buffer.from(newText, "utf16le");
|
|
63
|
+
parsed.styles[0].range = [0, encoded.length];
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(parsed);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
const match = content.match(/^(.*\])?(.*?)(\[.*)?$/s);
|
|
70
|
+
if (match) {
|
|
71
|
+
return content.replace(/\[[^\]]*\]/, `[${newText}]`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return newText;
|
|
75
|
+
}
|
|
76
|
+
export function findSegment(draft, id) {
|
|
77
|
+
const shortId = id.toLowerCase();
|
|
78
|
+
for (const track of draft.tracks) {
|
|
79
|
+
for (let i = 0; i < track.segments.length; i++) {
|
|
80
|
+
const seg = track.segments[i];
|
|
81
|
+
if (seg.id === id || seg.id.toLowerCase().startsWith(shortId)) {
|
|
82
|
+
return { track, segment: seg, index: i };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
export function findMaterial(arr, id) {
|
|
89
|
+
return arr.find(m => m.id === id);
|
|
90
|
+
}
|
|
91
|
+
export function getTracksByType(draft, type) {
|
|
92
|
+
return draft.tracks.filter(t => t.type === type);
|
|
93
|
+
}
|
|
94
|
+
export function getMaterialTypes(draft) {
|
|
95
|
+
return Object.entries(draft.materials)
|
|
96
|
+
.filter(([, v]) => Array.isArray(v))
|
|
97
|
+
.map(([type, arr]) => ({ type, count: arr.length }))
|
|
98
|
+
.sort((a, b) => b.count - a.count);
|
|
99
|
+
}
|
|
100
|
+
export function findMaterialGlobal(draft, id) {
|
|
101
|
+
const shortId = id.toLowerCase();
|
|
102
|
+
for (const [type, arr] of Object.entries(draft.materials)) {
|
|
103
|
+
if (!Array.isArray(arr))
|
|
104
|
+
continue;
|
|
105
|
+
for (const mat of arr) {
|
|
106
|
+
if (mat && typeof mat === "object" && typeof mat.id === "string") {
|
|
107
|
+
const m = mat;
|
|
108
|
+
const matId = m.id;
|
|
109
|
+
if (matId === id || matId.toLowerCase().startsWith(shortId)) {
|
|
110
|
+
return { type, material: m };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|