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 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
+ }