capcut-cli 0.1.5 → 0.2.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/README.md +22 -1
- package/dist/factory.js +181 -1
- package/dist/index.js +87 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# capcut-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create and edit CapCut projects from the command line. Build drafts from scratch, add media, modify subtitles, cut long-form to shorts.
|
|
4
4
|
|
|
5
5
|
## The problem
|
|
6
6
|
|
|
@@ -89,6 +89,27 @@ capcut material ./project a1b2c3 # Full detail for one material
|
|
|
89
89
|
|
|
90
90
|
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.
|
|
91
91
|
|
|
92
|
+
### Create (build projects from scratch)
|
|
93
|
+
|
|
94
|
+
No need to open CapCut first. Create a draft, add media, then open in CapCut.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Create an empty draft
|
|
98
|
+
capcut init "My Short" --drafts ~/Movies/CapCut/User\ Data/Projects/com.lveditor.draft
|
|
99
|
+
|
|
100
|
+
# Add media
|
|
101
|
+
capcut add-video ./my-short ./clip.mp4 0s 10s
|
|
102
|
+
capcut add-audio ./my-short ./voiceover.wav 0s 10s --volume 0.9
|
|
103
|
+
capcut add-audio ./my-short ./music.mp3 0s 30s --volume 0.3
|
|
104
|
+
|
|
105
|
+
# Add titles
|
|
106
|
+
capcut add-text ./my-short 0s 5s "My Short" --font-size 24 --color "#FFD700"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`init` creates a valid `draft_content.json` from a built-in template. `add-video` and `add-audio` copy the file into the draft's assets directory so CapCut can find it. Open the project in CapCut and everything links up.
|
|
110
|
+
|
|
111
|
+
Options for `add-video` / `add-audio`: `--volume <0-1>`, `--template <path>` (custom draft template).
|
|
112
|
+
|
|
92
113
|
### Add
|
|
93
114
|
|
|
94
115
|
```bash
|
package/dist/factory.js
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, copyFileSync } from "node:fs";
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
3
4
|
import { findMaterialGlobal } from "./draft.js";
|
|
4
5
|
// --- UUID generation ---
|
|
5
6
|
export function uuid() {
|
|
6
7
|
return randomUUID();
|
|
7
8
|
}
|
|
9
|
+
export function initDraft(opts) {
|
|
10
|
+
const draftPath = resolve(opts.draftsDir, opts.name);
|
|
11
|
+
if (existsSync(draftPath)) {
|
|
12
|
+
throw new Error(`Draft already exists: ${draftPath}. Delete it first or use a different name.`);
|
|
13
|
+
}
|
|
14
|
+
cpSync(opts.templateDir, draftPath, { recursive: true });
|
|
15
|
+
// Find the draft file
|
|
16
|
+
const candidates = ["draft_info.json", "draft_content.json"];
|
|
17
|
+
for (const c of candidates) {
|
|
18
|
+
const fp = resolve(draftPath, c);
|
|
19
|
+
if (existsSync(fp)) {
|
|
20
|
+
// Update the draft name
|
|
21
|
+
const raw = readFileSync(fp, "utf-8");
|
|
22
|
+
const draft = JSON.parse(raw);
|
|
23
|
+
draft.name = opts.name;
|
|
24
|
+
draft.id = uuid();
|
|
25
|
+
writeFileSync(fp, JSON.stringify(draft, null, 0), "utf-8");
|
|
26
|
+
return { draftPath, filePath: fp };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`No draft_info.json or draft_content.json found in template: ${opts.templateDir}`);
|
|
30
|
+
}
|
|
8
31
|
export function createCompanionMaterials(trackType) {
|
|
9
32
|
const speed = { id: uuid(), type: "speed", speed: 1, mode: 0, curve_speed: null };
|
|
10
33
|
const placeholder = {
|
|
@@ -160,6 +183,163 @@ export function addText(draft, filePath, opts) {
|
|
|
160
183
|
track.segments.push(seg);
|
|
161
184
|
return { segmentId: segId, materialId: matId, trackId: track.id };
|
|
162
185
|
}
|
|
186
|
+
export function addAudio(draft, filePath, opts) {
|
|
187
|
+
const segId = uuid();
|
|
188
|
+
const matId = uuid();
|
|
189
|
+
const trackName = opts.trackName ?? "audio";
|
|
190
|
+
const volume = opts.volume ?? 1.0;
|
|
191
|
+
// Copy file into draft assets directory
|
|
192
|
+
const draftDir = dirname(filePath);
|
|
193
|
+
const filename = opts.path.split("/").pop() || "audio.mp3";
|
|
194
|
+
const assetsDir = resolve(draftDir, "assets", "audio");
|
|
195
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
196
|
+
const destPath = resolve(assetsDir, filename);
|
|
197
|
+
if (!existsSync(destPath)) {
|
|
198
|
+
copyFileSync(opts.path, destPath);
|
|
199
|
+
}
|
|
200
|
+
// Use the local assets path — CapCut rewrites to placeholder on open
|
|
201
|
+
const localPath = destPath;
|
|
202
|
+
// Find or create audio track
|
|
203
|
+
let track = draft.tracks.find(t => t.type === "audio" && t.name === trackName);
|
|
204
|
+
if (!track) {
|
|
205
|
+
track = {
|
|
206
|
+
id: uuid(),
|
|
207
|
+
type: "audio",
|
|
208
|
+
name: trackName,
|
|
209
|
+
attribute: 0,
|
|
210
|
+
segments: [],
|
|
211
|
+
is_default_name: false,
|
|
212
|
+
flag: 0,
|
|
213
|
+
};
|
|
214
|
+
draft.tracks.push(track);
|
|
215
|
+
}
|
|
216
|
+
// Create companion materials
|
|
217
|
+
const companions = createCompanionMaterials("audio");
|
|
218
|
+
registerCompanions(draft, companions);
|
|
219
|
+
// Create audio material
|
|
220
|
+
const audioMaterial = {
|
|
221
|
+
id: matId,
|
|
222
|
+
path: localPath,
|
|
223
|
+
name: filename,
|
|
224
|
+
duration: opts.duration,
|
|
225
|
+
type: "extract_music",
|
|
226
|
+
category_id: "",
|
|
227
|
+
category_name: "local",
|
|
228
|
+
check_flag: 1,
|
|
229
|
+
music_id: "",
|
|
230
|
+
request_id: "",
|
|
231
|
+
source_platform: 0,
|
|
232
|
+
team_id: "",
|
|
233
|
+
text_id: "",
|
|
234
|
+
tone_category_id: "",
|
|
235
|
+
tone_category_name: "",
|
|
236
|
+
tone_effect_id: "",
|
|
237
|
+
tone_effect_name: "",
|
|
238
|
+
tone_platform: "",
|
|
239
|
+
tone_second_category_id: "",
|
|
240
|
+
tone_second_category_name: "",
|
|
241
|
+
tone_speaker: "",
|
|
242
|
+
tone_type: "",
|
|
243
|
+
wave_points: [],
|
|
244
|
+
};
|
|
245
|
+
draft.materials.audios.push(audioMaterial);
|
|
246
|
+
// Create segment
|
|
247
|
+
const timerange = { start: opts.start, duration: opts.duration };
|
|
248
|
+
const seg = baseSegment(segId, matId, track.id, timerange, companions.ids, 11000);
|
|
249
|
+
seg.volume = volume;
|
|
250
|
+
track.segments.push(seg);
|
|
251
|
+
// Update project duration if needed
|
|
252
|
+
const segEnd = opts.start + opts.duration;
|
|
253
|
+
if (segEnd > draft.duration) {
|
|
254
|
+
draft.duration = segEnd;
|
|
255
|
+
}
|
|
256
|
+
return { segmentId: segId, materialId: matId, trackId: track.id };
|
|
257
|
+
}
|
|
258
|
+
export function addVideo(draft, filePath, opts) {
|
|
259
|
+
const segId = uuid();
|
|
260
|
+
const matId = uuid();
|
|
261
|
+
const trackName = opts.trackName ?? "video";
|
|
262
|
+
const width = opts.width ?? 1920;
|
|
263
|
+
const height = opts.height ?? 1080;
|
|
264
|
+
// Infer type from extension if not provided
|
|
265
|
+
const ext = opts.path.split(".").pop()?.toLowerCase() || "";
|
|
266
|
+
const materialType = opts.type ?? (["jpg", "jpeg", "png", "webp", "bmp", "tiff"].includes(ext) ? "photo" : "video");
|
|
267
|
+
// Copy file into draft assets directory
|
|
268
|
+
const draftDir = dirname(filePath);
|
|
269
|
+
const filename = opts.path.split("/").pop() || "media";
|
|
270
|
+
const assetsDir = resolve(draftDir, "assets", "video");
|
|
271
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
272
|
+
const destPath = resolve(assetsDir, filename);
|
|
273
|
+
if (!existsSync(destPath)) {
|
|
274
|
+
copyFileSync(opts.path, destPath);
|
|
275
|
+
}
|
|
276
|
+
// Use the local assets path — CapCut rewrites to placeholder on open
|
|
277
|
+
const localPath = destPath;
|
|
278
|
+
// Find or create video track
|
|
279
|
+
let track = draft.tracks.find(t => t.type === "video" && t.name === trackName);
|
|
280
|
+
if (!track) {
|
|
281
|
+
track = {
|
|
282
|
+
id: uuid(),
|
|
283
|
+
type: "video",
|
|
284
|
+
name: trackName,
|
|
285
|
+
attribute: 0,
|
|
286
|
+
segments: [],
|
|
287
|
+
is_default_name: false,
|
|
288
|
+
flag: 0,
|
|
289
|
+
};
|
|
290
|
+
draft.tracks.push(track);
|
|
291
|
+
}
|
|
292
|
+
// Create companion materials
|
|
293
|
+
const companions = createCompanionMaterials("video");
|
|
294
|
+
registerCompanions(draft, companions);
|
|
295
|
+
// Create video material
|
|
296
|
+
const videoMaterial = {
|
|
297
|
+
id: matId,
|
|
298
|
+
path: localPath,
|
|
299
|
+
material_name: filename,
|
|
300
|
+
type: materialType,
|
|
301
|
+
duration: opts.duration,
|
|
302
|
+
width,
|
|
303
|
+
height,
|
|
304
|
+
category_id: "",
|
|
305
|
+
category_name: "local",
|
|
306
|
+
check_flag: 7,
|
|
307
|
+
crop: { lower_left_x: 0, lower_left_y: 1, lower_right_x: 1, lower_right_y: 1, upper_left_x: 0, upper_left_y: 0, upper_right_x: 1, upper_right_y: 0 },
|
|
308
|
+
has_audio: materialType === "video",
|
|
309
|
+
extra_type_option: 0,
|
|
310
|
+
formula_id: "",
|
|
311
|
+
freeze: null,
|
|
312
|
+
intensifies_audio_path: "",
|
|
313
|
+
intensifies_path: "",
|
|
314
|
+
is_ai_generate_content: false,
|
|
315
|
+
is_copyright: false,
|
|
316
|
+
is_text_edit_overdub: false,
|
|
317
|
+
is_unified_beauty_mode: false,
|
|
318
|
+
local_id: "",
|
|
319
|
+
local_material_id: "",
|
|
320
|
+
material_url: "",
|
|
321
|
+
media_path: "",
|
|
322
|
+
object_locked: null,
|
|
323
|
+
origin_material_id: "",
|
|
324
|
+
request_id: "",
|
|
325
|
+
reverse_path: "",
|
|
326
|
+
source_platform: 0,
|
|
327
|
+
stable: { matrix_path: "", stable_level: 0, time_range: { duration: 0, start: 0 } },
|
|
328
|
+
team_id: "",
|
|
329
|
+
video_algorithm: { algorithms: [], deflicker: null, motion_blur_config: null, noise_reduction: null, path: "", quality_enhance: null, time_range: null },
|
|
330
|
+
};
|
|
331
|
+
draft.materials.videos.push(videoMaterial);
|
|
332
|
+
// Create segment
|
|
333
|
+
const timerange = { start: opts.start, duration: opts.duration };
|
|
334
|
+
const seg = baseSegment(segId, matId, track.id, timerange, companions.ids, 14000);
|
|
335
|
+
track.segments.push(seg);
|
|
336
|
+
// Update project duration if needed
|
|
337
|
+
const segEnd = opts.start + opts.duration;
|
|
338
|
+
if (segEnd > draft.duration) {
|
|
339
|
+
draft.duration = segEnd;
|
|
340
|
+
}
|
|
341
|
+
return { segmentId: segId, materialId: matId, trackId: track.id };
|
|
342
|
+
}
|
|
163
343
|
export function cutProject(draft, opts) {
|
|
164
344
|
const { start, end } = opts;
|
|
165
345
|
const duration = end - start;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { loadDraft, saveDraft, extractText, updateTextContent, findSegment, findMaterial, findMaterialGlobal, getMaterialTypes, getTracksByType } from "./draft.js";
|
|
4
4
|
import { formatTime, formatDuration, parseTimeInput, srtTime } from "./time.js";
|
|
5
|
-
import { addText, cutProject, saveTemplate, applyTemplate } from "./factory.js";
|
|
5
|
+
import { addText, addAudio, addVideo, cutProject, saveTemplate, applyTemplate, initDraft } from "./factory.js";
|
|
6
6
|
const HELP = `capcut-cli -- fast edits to CapCut projects
|
|
7
7
|
|
|
8
8
|
Usage: capcut <command> <project> [options]
|
|
@@ -27,7 +27,23 @@ Detail (drill into one item):
|
|
|
27
27
|
segment <project> <id> Full detail for one segment + its material
|
|
28
28
|
material <project> <id> Full detail for one material
|
|
29
29
|
|
|
30
|
+
Create:
|
|
31
|
+
init <name> [--template <dir>] [--drafts <dir>]
|
|
32
|
+
Create a new empty draft from template. Defaults:
|
|
33
|
+
--template ../CapCutAPI/template (relative to capcut-cli)
|
|
34
|
+
--drafts ~/Movies/CapCut/User Data/Projects/com.lveditor.draft
|
|
35
|
+
|
|
30
36
|
Add:
|
|
37
|
+
add-audio <project> <file> <start> <duration> [options]
|
|
38
|
+
Add an audio segment (VO, music, SFX). Options:
|
|
39
|
+
--volume <n> Volume 0.0-1.0 (default: 1.0)
|
|
40
|
+
--track-name <s> Track name (default: "audio")
|
|
41
|
+
|
|
42
|
+
add-video <project> <file> <start> <duration> [options]
|
|
43
|
+
Add a video or image segment. Type auto-detected from extension.
|
|
44
|
+
Options:
|
|
45
|
+
--track-name <s> Track name (default: "video")
|
|
46
|
+
|
|
31
47
|
add-text <project> <start> <duration> <text> [options]
|
|
32
48
|
Add a text segment. Options:
|
|
33
49
|
--font-size <n> Font size (default: 15)
|
|
@@ -95,6 +111,15 @@ function parseFlags(args) {
|
|
|
95
111
|
else if (a === "--track-name" && i + 1 < args.length) {
|
|
96
112
|
flags.trackName = args[++i];
|
|
97
113
|
}
|
|
114
|
+
else if (a === "--volume" && i + 1 < args.length) {
|
|
115
|
+
flags.volume = parseFloat(args[++i]);
|
|
116
|
+
}
|
|
117
|
+
else if (a === "--template" && i + 1 < args.length) {
|
|
118
|
+
flags.template = args[++i];
|
|
119
|
+
}
|
|
120
|
+
else if (a === "--drafts" && i + 1 < args.length) {
|
|
121
|
+
flags.drafts = args[++i];
|
|
122
|
+
}
|
|
98
123
|
else
|
|
99
124
|
positional.push(a);
|
|
100
125
|
}
|
|
@@ -462,6 +487,45 @@ function cmdMaterialDetail(draft, matId, flags) {
|
|
|
462
487
|
}
|
|
463
488
|
}
|
|
464
489
|
// --- Add commands ---
|
|
490
|
+
function cmdAddAudio(draft, filePath, positional, flags) {
|
|
491
|
+
const audioPath = positional[2];
|
|
492
|
+
const startStr = positional[3];
|
|
493
|
+
const durationStr = positional[4];
|
|
494
|
+
if (!audioPath || !startStr || !durationStr)
|
|
495
|
+
die("Usage: capcut add-audio <project> <file> <start> <duration>");
|
|
496
|
+
const absPath = audioPath.startsWith("/") ? audioPath : process.cwd() + "/" + audioPath;
|
|
497
|
+
const start = parseTimeInput(startStr);
|
|
498
|
+
const duration = parseTimeInput(durationStr);
|
|
499
|
+
const opts = {
|
|
500
|
+
path: absPath,
|
|
501
|
+
start,
|
|
502
|
+
duration,
|
|
503
|
+
volume: flags.volume,
|
|
504
|
+
trackName: flags.trackName,
|
|
505
|
+
};
|
|
506
|
+
const result = addAudio(draft, filePath, opts);
|
|
507
|
+
saveDraft(filePath, draft);
|
|
508
|
+
out({ ok: true, segment_id: result.segmentId, material_id: result.materialId, track_id: result.trackId, path: absPath, start_us: start, duration_us: duration }, flags);
|
|
509
|
+
}
|
|
510
|
+
function cmdAddVideo(draft, filePath, positional, flags) {
|
|
511
|
+
const videoPath = positional[2];
|
|
512
|
+
const startStr = positional[3];
|
|
513
|
+
const durationStr = positional[4];
|
|
514
|
+
if (!videoPath || !startStr || !durationStr)
|
|
515
|
+
die("Usage: capcut add-video <project> <file> <start> <duration>");
|
|
516
|
+
const absPath = videoPath.startsWith("/") ? videoPath : process.cwd() + "/" + videoPath;
|
|
517
|
+
const start = parseTimeInput(startStr);
|
|
518
|
+
const duration = parseTimeInput(durationStr);
|
|
519
|
+
const opts = {
|
|
520
|
+
path: absPath,
|
|
521
|
+
start,
|
|
522
|
+
duration,
|
|
523
|
+
trackName: flags.trackName,
|
|
524
|
+
};
|
|
525
|
+
const result = addVideo(draft, filePath, opts);
|
|
526
|
+
saveDraft(filePath, draft);
|
|
527
|
+
out({ ok: true, segment_id: result.segmentId, material_id: result.materialId, track_id: result.trackId, path: absPath, start_us: start, duration_us: duration }, flags);
|
|
528
|
+
}
|
|
465
529
|
function cmdAddText(draft, filePath, positional, flags) {
|
|
466
530
|
const startStr = positional[2];
|
|
467
531
|
const durationStr = positional[3];
|
|
@@ -614,6 +678,20 @@ function main() {
|
|
|
614
678
|
const { positional, flags } = parseFlags(raw);
|
|
615
679
|
const cmd = positional[0];
|
|
616
680
|
const projectPath = positional[1];
|
|
681
|
+
// init doesn't need an existing project
|
|
682
|
+
if (cmd === "init") {
|
|
683
|
+
const name = projectPath; // positional[1] is the name for init
|
|
684
|
+
if (!name)
|
|
685
|
+
die("Missing name. Usage: capcut init <name> [--template <dir>] [--drafts <dir>]");
|
|
686
|
+
const cliDir = new URL(".", import.meta.url).pathname.replace(/\/dist\/$/, "");
|
|
687
|
+
const templateDir = flags.template ?? cliDir + "/../CapCutAPI/template";
|
|
688
|
+
const draftsDir = flags.drafts ?? (process.env.HOME || "~") + "/Movies/CapCut/User Data/Projects/com.lveditor.draft";
|
|
689
|
+
const result = initDraft({ name, templateDir, draftsDir });
|
|
690
|
+
out({ ok: true, name, draft_path: result.draftPath, file_path: result.filePath }, flags);
|
|
691
|
+
if (!flags.quiet)
|
|
692
|
+
process.stderr.write(`Created: ${result.draftPath}\n`);
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
617
695
|
if (!projectPath)
|
|
618
696
|
die("Missing project path. Run 'capcut --help' for usage.");
|
|
619
697
|
const { draft, filePath } = loadDraft(projectPath);
|
|
@@ -672,6 +750,14 @@ function main() {
|
|
|
672
750
|
requireArgs(positional, 3, "capcut material <project> <id>");
|
|
673
751
|
cmdMaterialDetail(draft, positional[2], flags);
|
|
674
752
|
break;
|
|
753
|
+
case "add-audio":
|
|
754
|
+
requireArgs(positional, 5, "capcut add-audio <project> <file> <start> <duration>");
|
|
755
|
+
cmdAddAudio(draft, filePath, positional, flags);
|
|
756
|
+
break;
|
|
757
|
+
case "add-video":
|
|
758
|
+
requireArgs(positional, 5, "capcut add-video <project> <file> <start> <duration>");
|
|
759
|
+
cmdAddVideo(draft, filePath, positional, flags);
|
|
760
|
+
break;
|
|
675
761
|
case "add-text":
|
|
676
762
|
requireArgs(positional, 5, "capcut add-text <project> <start> <duration> <text>");
|
|
677
763
|
cmdAddText(draft, filePath, positional, flags);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capcut-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI to edit CapCut
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI to create and edit CapCut projects — build drafts from scratch, add video/audio/text, subtitles, timing, speed, volume, templates, cut long-form to shorts. No API needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"capcut": "dist/index.js",
|