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/dist/index.js
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { loadDraft, saveDraft, extractText, updateTextContent, findSegment, findMaterial, findMaterialGlobal, getMaterialTypes, getTracksByType } from "./draft.js";
|
|
4
|
+
import { formatTime, formatDuration, parseTimeInput, srtTime } from "./time.js";
|
|
5
|
+
import { addText, cutProject, saveTemplate, applyTemplate } from "./factory.js";
|
|
6
|
+
const HELP = `capcut-cli -- fast edits to CapCut projects
|
|
7
|
+
|
|
8
|
+
Usage: capcut <command> <project> [options]
|
|
9
|
+
|
|
10
|
+
<project> = path to draft_content.json, draft_info.json, or their parent directory
|
|
11
|
+
|
|
12
|
+
Global flags:
|
|
13
|
+
-H, --human Human-readable table output (default: JSON)
|
|
14
|
+
-q, --quiet No output on success, exit code only (write commands)
|
|
15
|
+
|
|
16
|
+
Overview (start here):
|
|
17
|
+
info <project> Project overview + material summary
|
|
18
|
+
tracks <project> List all tracks
|
|
19
|
+
materials <project> List all material types + counts
|
|
20
|
+
materials <project> --type <type> List items of one material type
|
|
21
|
+
|
|
22
|
+
Browse:
|
|
23
|
+
segments <project> [--track <type>] List segments with timing
|
|
24
|
+
texts <project> List all text/subtitle content
|
|
25
|
+
|
|
26
|
+
Detail (drill into one item):
|
|
27
|
+
segment <project> <id> Full detail for one segment + its material
|
|
28
|
+
material <project> <id> Full detail for one material
|
|
29
|
+
|
|
30
|
+
Add:
|
|
31
|
+
add-text <project> <start> <duration> <text> [options]
|
|
32
|
+
Add a text segment. Options:
|
|
33
|
+
--font-size <n> Font size (default: 15)
|
|
34
|
+
--color <hex> Text color (default: #FFFFFF)
|
|
35
|
+
--align <0|1|2> Left/center/right (default: 1)
|
|
36
|
+
--x <n> --y <n> Position (-1 to 1, default: 0,0)
|
|
37
|
+
--track-name <s> Track name (default: "text")
|
|
38
|
+
|
|
39
|
+
Edit:
|
|
40
|
+
set-text <project> <id> <text> Change text content
|
|
41
|
+
shift <project> <id> <offset> Shift segment timing (e.g. +0.5s, -1s)
|
|
42
|
+
shift-all <project> <offset> [--track <type>] Shift all segments on a track
|
|
43
|
+
speed <project> <id> <multiplier> Set playback speed
|
|
44
|
+
volume <project> <id> <level> Set volume (0.0-1.0)
|
|
45
|
+
trim <project> <id> <start> <duration> Trim segment (times in seconds)
|
|
46
|
+
opacity <project> <id> <alpha> Set opacity (0.0-1.0)
|
|
47
|
+
export-srt <project> Export subtitles to SRT
|
|
48
|
+
batch <project> Run multiple edits from stdin (JSONL)
|
|
49
|
+
|
|
50
|
+
Templates:
|
|
51
|
+
save-template <project> <id> <name> --out <path>
|
|
52
|
+
Extract any segment as a reusable template (text, sticker, video, audio)
|
|
53
|
+
apply-template <project> <template.json> <start> <duration> [text override]
|
|
54
|
+
Stamp a template into a project at the given time
|
|
55
|
+
Options: --x <n> --y <n> (override position)
|
|
56
|
+
|
|
57
|
+
Project:
|
|
58
|
+
cut <project> <start> <end> --out <path>
|
|
59
|
+
Extract a time range into a new project (long-form → short)
|
|
60
|
+
|
|
61
|
+
Navigation: info → tracks/materials → segments → segment <id>
|
|
62
|
+
info → materials --type X → material <id>
|
|
63
|
+
Time formats: 1.5s, 500ms, 1:30, +0.5s, -200ms
|
|
64
|
+
IDs: first 6+ chars of segment/material ID (prefix match)`;
|
|
65
|
+
function parseFlags(args) {
|
|
66
|
+
const positional = [];
|
|
67
|
+
const flags = { human: false, quiet: false };
|
|
68
|
+
for (let i = 0; i < args.length; i++) {
|
|
69
|
+
const a = args[i];
|
|
70
|
+
if (a === "-H" || a === "--human")
|
|
71
|
+
flags.human = true;
|
|
72
|
+
else if (a === "-q" || a === "--quiet")
|
|
73
|
+
flags.quiet = true;
|
|
74
|
+
else if ((a === "--track" || a === "--type") && i + 1 < args.length) {
|
|
75
|
+
flags.track = args[++i];
|
|
76
|
+
}
|
|
77
|
+
else if (a === "--out" && i + 1 < args.length) {
|
|
78
|
+
flags.out = args[++i];
|
|
79
|
+
}
|
|
80
|
+
else if (a === "--font-size" && i + 1 < args.length) {
|
|
81
|
+
flags.fontSize = parseFloat(args[++i]);
|
|
82
|
+
}
|
|
83
|
+
else if (a === "--color" && i + 1 < args.length) {
|
|
84
|
+
flags.color = args[++i];
|
|
85
|
+
}
|
|
86
|
+
else if (a === "--align" && i + 1 < args.length) {
|
|
87
|
+
flags.align = parseInt(args[++i]);
|
|
88
|
+
}
|
|
89
|
+
else if (a === "--x" && i + 1 < args.length) {
|
|
90
|
+
flags.x = parseFloat(args[++i]);
|
|
91
|
+
}
|
|
92
|
+
else if (a === "--y" && i + 1 < args.length) {
|
|
93
|
+
flags.y = parseFloat(args[++i]);
|
|
94
|
+
}
|
|
95
|
+
else if (a === "--track-name" && i + 1 < args.length) {
|
|
96
|
+
flags.trackName = args[++i];
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
positional.push(a);
|
|
100
|
+
}
|
|
101
|
+
return { positional, flags };
|
|
102
|
+
}
|
|
103
|
+
// --- Output ---
|
|
104
|
+
function out(data, flags) {
|
|
105
|
+
if (flags.quiet)
|
|
106
|
+
return;
|
|
107
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
108
|
+
}
|
|
109
|
+
class CliError extends Error {
|
|
110
|
+
constructor(msg) { super(msg); this.name = "CliError"; }
|
|
111
|
+
}
|
|
112
|
+
function die(msg) {
|
|
113
|
+
throw new CliError(msg);
|
|
114
|
+
}
|
|
115
|
+
function requireArgs(args, min, usage) {
|
|
116
|
+
if (args.length < min)
|
|
117
|
+
die(`Missing arguments. Usage: ${usage}`);
|
|
118
|
+
}
|
|
119
|
+
// --- Commands ---
|
|
120
|
+
function cmdInfo(draft, flags) {
|
|
121
|
+
const totalSegments = draft.tracks.reduce((n, t) => n + t.segments.length, 0);
|
|
122
|
+
const matTypes = getMaterialTypes(draft);
|
|
123
|
+
const matWithItems = matTypes.filter(m => m.count > 0);
|
|
124
|
+
const data = {
|
|
125
|
+
id: draft.id,
|
|
126
|
+
name: draft.name || draft.id,
|
|
127
|
+
duration_us: draft.duration,
|
|
128
|
+
fps: draft.fps,
|
|
129
|
+
width: draft.canvas_config.width,
|
|
130
|
+
height: draft.canvas_config.height,
|
|
131
|
+
ratio: draft.canvas_config.ratio,
|
|
132
|
+
tracks: draft.tracks.length,
|
|
133
|
+
segments: totalSegments,
|
|
134
|
+
platform: draft.platform ? `${draft.platform.app_source === "cc" ? "CapCut" : "JianYing"} ${draft.platform.app_version}` : null,
|
|
135
|
+
material_types: matTypes.length,
|
|
136
|
+
materials_with_items: matWithItems.length,
|
|
137
|
+
material_summary: matWithItems.map(m => ({ type: m.type, count: m.count })),
|
|
138
|
+
};
|
|
139
|
+
if (flags.human) {
|
|
140
|
+
const d = data;
|
|
141
|
+
console.log(`Project: ${d.name}`);
|
|
142
|
+
console.log(`Duration: ${formatDuration(d.duration_us)}`);
|
|
143
|
+
console.log(`Resolution: ${d.width}x${d.height} (${d.ratio})`);
|
|
144
|
+
console.log(`FPS: ${d.fps}`);
|
|
145
|
+
console.log(`Tracks: ${d.tracks}`);
|
|
146
|
+
console.log(`Segments: ${d.segments}`);
|
|
147
|
+
if (d.platform)
|
|
148
|
+
console.log(`Platform: ${d.platform}`);
|
|
149
|
+
console.log(`Materials: ${d.materials_with_items} types with data (${d.material_types} total)`);
|
|
150
|
+
for (const m of d.material_summary) {
|
|
151
|
+
console.log(` ${m.type.padEnd(28)} ${m.count}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
out(data, flags);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function cmdTracks(draft, flags) {
|
|
159
|
+
const data = draft.tracks.map((t, i) => {
|
|
160
|
+
const end = t.segments.reduce((max, s) => {
|
|
161
|
+
const e = s.target_timerange.start + s.target_timerange.duration;
|
|
162
|
+
return e > max ? e : max;
|
|
163
|
+
}, 0);
|
|
164
|
+
return {
|
|
165
|
+
index: i,
|
|
166
|
+
id: t.id,
|
|
167
|
+
type: t.type,
|
|
168
|
+
name: t.name,
|
|
169
|
+
segments: t.segments.length,
|
|
170
|
+
duration_us: end,
|
|
171
|
+
muted: !!(t.attribute & 1),
|
|
172
|
+
hidden: !!(t.attribute & 2),
|
|
173
|
+
locked: !!(t.attribute & 4),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
if (flags.human) {
|
|
177
|
+
console.log(`# Type Name Segs Duration`);
|
|
178
|
+
for (const t of data) {
|
|
179
|
+
const fl = [];
|
|
180
|
+
if (t.muted)
|
|
181
|
+
fl.push("muted");
|
|
182
|
+
if (t.hidden)
|
|
183
|
+
fl.push("hidden");
|
|
184
|
+
if (t.locked)
|
|
185
|
+
fl.push("locked");
|
|
186
|
+
console.log(`${String(t.index).padStart(2)} ${t.type.padEnd(8)} ${t.name.padEnd(14)} ${String(t.segments).padStart(4)} segs ${formatDuration(t.duration_us).padStart(10)}${fl.length ? " [" + fl.join(",") + "]" : ""}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
out(data, flags);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function segmentData(draft, track, seg) {
|
|
194
|
+
const t = seg.target_timerange;
|
|
195
|
+
let label = "";
|
|
196
|
+
if (track.type === "text") {
|
|
197
|
+
const mat = findMaterial(draft.materials.texts, seg.material_id);
|
|
198
|
+
if (mat)
|
|
199
|
+
label = extractText(mat.content);
|
|
200
|
+
}
|
|
201
|
+
else if (track.type === "video") {
|
|
202
|
+
const mat = findMaterial(draft.materials.videos, seg.material_id);
|
|
203
|
+
if (mat)
|
|
204
|
+
label = mat.material_name;
|
|
205
|
+
}
|
|
206
|
+
else if (track.type === "audio") {
|
|
207
|
+
const mat = findMaterial(draft.materials.audios, seg.material_id);
|
|
208
|
+
if (mat)
|
|
209
|
+
label = mat.name || "";
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
id: seg.id,
|
|
213
|
+
type: track.type,
|
|
214
|
+
start_us: t.start,
|
|
215
|
+
duration_us: t.duration,
|
|
216
|
+
speed: seg.speed,
|
|
217
|
+
volume: seg.volume,
|
|
218
|
+
opacity: seg.clip?.alpha ?? 1,
|
|
219
|
+
label,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function cmdSegments(draft, flags) {
|
|
223
|
+
const tracks = flags.track ? getTracksByType(draft, flags.track) : draft.tracks;
|
|
224
|
+
if (tracks.length === 0)
|
|
225
|
+
die(`No tracks of type "${flags.track}"`);
|
|
226
|
+
const data = tracks.flatMap(track => track.segments.map(seg => segmentData(draft, track, seg)));
|
|
227
|
+
if (flags.human) {
|
|
228
|
+
console.log(`ID Type Start -End Dur Spd Label`);
|
|
229
|
+
for (const s of data) {
|
|
230
|
+
const end = s.start_us + s.duration_us;
|
|
231
|
+
console.log(`${s.id.slice(0, 8)} ${s.type.padEnd(6)} ${formatTime(s.start_us).padStart(8)}-${formatTime(end).padStart(8)} ${formatDuration(s.duration_us).padStart(8)} ${s.speed !== 1 ? s.speed + "x" : " "} ${s.label.slice(0, 40)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
out(data, flags);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function cmdTexts(draft, flags) {
|
|
239
|
+
const textTracks = getTracksByType(draft, "text");
|
|
240
|
+
const data = textTracks.flatMap(track => track.segments.map(seg => {
|
|
241
|
+
const mat = findMaterial(draft.materials.texts, seg.material_id);
|
|
242
|
+
const t = seg.target_timerange;
|
|
243
|
+
return {
|
|
244
|
+
id: seg.id,
|
|
245
|
+
start_us: t.start,
|
|
246
|
+
duration_us: t.duration,
|
|
247
|
+
text: mat ? extractText(mat.content) : "",
|
|
248
|
+
};
|
|
249
|
+
}));
|
|
250
|
+
if (flags.human) {
|
|
251
|
+
if (data.length === 0) {
|
|
252
|
+
console.log("No text segments found.");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
console.log(`ID Start -End Text`);
|
|
256
|
+
for (const s of data) {
|
|
257
|
+
console.log(`${s.id.slice(0, 8)} ${formatTime(s.start_us).padStart(8)}-${formatTime(s.start_us + s.duration_us).padStart(8)} ${s.text}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
out(data, flags);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function cmdSetText(draft, filePath, segId, newText, flags, save = true) {
|
|
265
|
+
const result = findSegment(draft, segId);
|
|
266
|
+
if (!result)
|
|
267
|
+
die(`Segment not found: ${segId}`);
|
|
268
|
+
const mat = findMaterial(draft.materials.texts, result.segment.material_id);
|
|
269
|
+
if (!mat)
|
|
270
|
+
die(`Text material not found for segment ${segId}`);
|
|
271
|
+
const oldText = extractText(mat.content);
|
|
272
|
+
mat.content = updateTextContent(mat.content, newText);
|
|
273
|
+
if (save)
|
|
274
|
+
saveDraft(filePath, draft);
|
|
275
|
+
out({ ok: true, id: result.segment.id, old: oldText, new: newText }, flags);
|
|
276
|
+
}
|
|
277
|
+
function cmdShift(draft, filePath, segId, offsetStr, flags, save = true) {
|
|
278
|
+
const result = findSegment(draft, segId);
|
|
279
|
+
if (!result)
|
|
280
|
+
die(`Segment not found: ${segId}`);
|
|
281
|
+
const offset = parseTimeInput(offsetStr);
|
|
282
|
+
const seg = result.segment;
|
|
283
|
+
const oldStart = seg.target_timerange.start;
|
|
284
|
+
seg.target_timerange.start = Math.max(0, oldStart + offset);
|
|
285
|
+
if (save)
|
|
286
|
+
saveDraft(filePath, draft);
|
|
287
|
+
out({ ok: true, id: seg.id, old_start_us: oldStart, new_start_us: seg.target_timerange.start }, flags);
|
|
288
|
+
}
|
|
289
|
+
function cmdShiftAll(draft, filePath, offsetStr, flags, save = true) {
|
|
290
|
+
const offset = parseTimeInput(offsetStr);
|
|
291
|
+
const tracks = flags.track ? getTracksByType(draft, flags.track) : draft.tracks;
|
|
292
|
+
let count = 0;
|
|
293
|
+
for (const track of tracks) {
|
|
294
|
+
for (const seg of track.segments) {
|
|
295
|
+
seg.target_timerange.start = Math.max(0, seg.target_timerange.start + offset);
|
|
296
|
+
count++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (save)
|
|
300
|
+
saveDraft(filePath, draft);
|
|
301
|
+
out({ ok: true, shifted: count, offset_us: offset }, flags);
|
|
302
|
+
}
|
|
303
|
+
function cmdSpeed(draft, filePath, segId, multiplier, flags, save = true) {
|
|
304
|
+
const result = findSegment(draft, segId);
|
|
305
|
+
if (!result)
|
|
306
|
+
die(`Segment not found: ${segId}`);
|
|
307
|
+
const speed = parseFloat(multiplier);
|
|
308
|
+
if (isNaN(speed) || speed <= 0)
|
|
309
|
+
die("Speed must be a positive number");
|
|
310
|
+
const seg = result.segment;
|
|
311
|
+
const oldSpeed = seg.speed;
|
|
312
|
+
seg.speed = speed;
|
|
313
|
+
seg.source_timerange.duration = Math.round(seg.target_timerange.duration * speed);
|
|
314
|
+
for (const refId of seg.extra_material_refs) {
|
|
315
|
+
const speedMat = findMaterial(draft.materials.speeds, refId);
|
|
316
|
+
if (speedMat)
|
|
317
|
+
speedMat.speed = speed;
|
|
318
|
+
}
|
|
319
|
+
if (save)
|
|
320
|
+
saveDraft(filePath, draft);
|
|
321
|
+
out({ ok: true, id: seg.id, old_speed: oldSpeed, new_speed: speed }, flags);
|
|
322
|
+
}
|
|
323
|
+
function cmdVolume(draft, filePath, segId, levelStr, flags, save = true) {
|
|
324
|
+
const result = findSegment(draft, segId);
|
|
325
|
+
if (!result)
|
|
326
|
+
die(`Segment not found: ${segId}`);
|
|
327
|
+
const level = parseFloat(levelStr);
|
|
328
|
+
if (isNaN(level) || level < 0)
|
|
329
|
+
die("Volume must be >= 0");
|
|
330
|
+
const old = result.segment.volume;
|
|
331
|
+
result.segment.volume = level;
|
|
332
|
+
if (save)
|
|
333
|
+
saveDraft(filePath, draft);
|
|
334
|
+
out({ ok: true, id: result.segment.id, old_volume: old, new_volume: level }, flags);
|
|
335
|
+
}
|
|
336
|
+
function cmdTrim(draft, filePath, segId, startStr, durationStr, flags, save = true) {
|
|
337
|
+
const result = findSegment(draft, segId);
|
|
338
|
+
if (!result)
|
|
339
|
+
die(`Segment not found: ${segId}`);
|
|
340
|
+
const start = parseTimeInput(startStr);
|
|
341
|
+
const duration = parseTimeInput(durationStr);
|
|
342
|
+
const seg = result.segment;
|
|
343
|
+
seg.source_timerange.start = start;
|
|
344
|
+
seg.source_timerange.duration = duration;
|
|
345
|
+
seg.target_timerange.duration = Math.round(duration / seg.speed);
|
|
346
|
+
if (save)
|
|
347
|
+
saveDraft(filePath, draft);
|
|
348
|
+
out({ ok: true, id: seg.id, source_start_us: start, source_duration_us: duration, target_duration_us: seg.target_timerange.duration }, flags);
|
|
349
|
+
}
|
|
350
|
+
function cmdOpacity(draft, filePath, segId, alphaStr, flags, save = true) {
|
|
351
|
+
const result = findSegment(draft, segId);
|
|
352
|
+
if (!result)
|
|
353
|
+
die(`Segment not found: ${segId}`);
|
|
354
|
+
const alpha = parseFloat(alphaStr);
|
|
355
|
+
if (isNaN(alpha) || alpha < 0 || alpha > 1)
|
|
356
|
+
die("Opacity must be 0.0-1.0");
|
|
357
|
+
if (!result.segment.clip)
|
|
358
|
+
die(`Segment ${segId} has no clip (audio segment?)`);
|
|
359
|
+
const old = result.segment.clip.alpha;
|
|
360
|
+
result.segment.clip.alpha = alpha;
|
|
361
|
+
if (save)
|
|
362
|
+
saveDraft(filePath, draft);
|
|
363
|
+
out({ ok: true, id: result.segment.id, old_opacity: old, new_opacity: alpha }, flags);
|
|
364
|
+
}
|
|
365
|
+
function cmdExportSrt(draft) {
|
|
366
|
+
const textTracks = getTracksByType(draft, "text");
|
|
367
|
+
const entries = [];
|
|
368
|
+
for (const track of textTracks) {
|
|
369
|
+
for (const seg of track.segments) {
|
|
370
|
+
const mat = findMaterial(draft.materials.texts, seg.material_id);
|
|
371
|
+
if (!mat)
|
|
372
|
+
continue;
|
|
373
|
+
const t = seg.target_timerange;
|
|
374
|
+
entries.push({ start: t.start, end: t.start + t.duration, text: extractText(mat.content) });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
entries.sort((a, b) => a.start - b.start);
|
|
378
|
+
const srt = entries.map((e, i) => `${i + 1}\n${srtTime(e.start)} --> ${srtTime(e.end)}\n${e.text}\n`).join("\n");
|
|
379
|
+
process.stdout.write(srt);
|
|
380
|
+
}
|
|
381
|
+
// --- Discovery & drill-down ---
|
|
382
|
+
function cmdMaterials(draft, flags) {
|
|
383
|
+
const matTypes = getMaterialTypes(draft);
|
|
384
|
+
if (flags.track) {
|
|
385
|
+
// --type filter: list items of that material type
|
|
386
|
+
const key = flags.track; // reuse --track flag as --type
|
|
387
|
+
const arr = draft.materials[key];
|
|
388
|
+
if (!arr || !Array.isArray(arr))
|
|
389
|
+
die(`Unknown material type: ${key}`);
|
|
390
|
+
const items = arr.map((m) => {
|
|
391
|
+
const summary = { id: m.id };
|
|
392
|
+
if (m.name !== undefined)
|
|
393
|
+
summary.name = m.name;
|
|
394
|
+
if (m.material_name !== undefined)
|
|
395
|
+
summary.name = m.material_name;
|
|
396
|
+
if (m.path !== undefined)
|
|
397
|
+
summary.path = m.path;
|
|
398
|
+
if (m.duration !== undefined)
|
|
399
|
+
summary.duration_us = m.duration;
|
|
400
|
+
if (m.type !== undefined)
|
|
401
|
+
summary.type = m.type;
|
|
402
|
+
summary.fields = Object.keys(m).length;
|
|
403
|
+
return summary;
|
|
404
|
+
});
|
|
405
|
+
if (flags.human) {
|
|
406
|
+
if (items.length === 0) {
|
|
407
|
+
console.log(`No ${key} materials.`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
console.log(`ID Name/Path Fields`);
|
|
411
|
+
for (const item of items) {
|
|
412
|
+
const label = (item.name || item.path || "");
|
|
413
|
+
console.log(`${item.id.slice(0, 8)} ${label.slice(0, 44).padEnd(44)} ${String(item.fields).padStart(3)}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
out(items, flags);
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (flags.human) {
|
|
422
|
+
console.log(`Type Count`);
|
|
423
|
+
for (const m of matTypes) {
|
|
424
|
+
console.log(`${m.type.padEnd(28)} ${String(m.count).padStart(5)}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
out(matTypes, flags);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function cmdSegmentDetail(draft, segId, flags) {
|
|
432
|
+
const result = findSegment(draft, segId);
|
|
433
|
+
if (!result)
|
|
434
|
+
die(`Segment not found: ${segId}`);
|
|
435
|
+
const seg = result.segment;
|
|
436
|
+
// Resolve the primary material
|
|
437
|
+
const mat = findMaterialGlobal(draft, seg.material_id);
|
|
438
|
+
const detail = {
|
|
439
|
+
...seg,
|
|
440
|
+
_track_type: result.track.type,
|
|
441
|
+
_track_name: result.track.name,
|
|
442
|
+
_track_id: result.track.id,
|
|
443
|
+
_material: mat ? { _type: mat.type, ...mat.material } : null,
|
|
444
|
+
};
|
|
445
|
+
if (flags.human) {
|
|
446
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
out(detail, flags);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function cmdMaterialDetail(draft, matId, flags) {
|
|
453
|
+
const result = findMaterialGlobal(draft, matId);
|
|
454
|
+
if (!result)
|
|
455
|
+
die(`Material not found: ${matId}`);
|
|
456
|
+
const detail = { _type: result.type, ...result.material };
|
|
457
|
+
if (flags.human) {
|
|
458
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
out(detail, flags);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// --- Add commands ---
|
|
465
|
+
function cmdAddText(draft, filePath, positional, flags) {
|
|
466
|
+
const startStr = positional[2];
|
|
467
|
+
const durationStr = positional[3];
|
|
468
|
+
const text = positional.slice(4).join(" ");
|
|
469
|
+
if (!text)
|
|
470
|
+
die("Missing text. Usage: capcut add-text <project> <start> <duration> <text>");
|
|
471
|
+
const start = parseTimeInput(startStr);
|
|
472
|
+
const duration = parseTimeInput(durationStr);
|
|
473
|
+
const opts = {
|
|
474
|
+
text,
|
|
475
|
+
start,
|
|
476
|
+
duration,
|
|
477
|
+
fontSize: flags.fontSize,
|
|
478
|
+
color: flags.color,
|
|
479
|
+
alignment: flags.align,
|
|
480
|
+
x: flags.x,
|
|
481
|
+
y: flags.y,
|
|
482
|
+
trackName: flags.trackName,
|
|
483
|
+
};
|
|
484
|
+
const result = addText(draft, filePath, opts);
|
|
485
|
+
saveDraft(filePath, draft);
|
|
486
|
+
out({ ok: true, segment_id: result.segmentId, material_id: result.materialId, track_id: result.trackId, text, start_us: start, duration_us: duration }, flags);
|
|
487
|
+
}
|
|
488
|
+
function cmdCut(draft, filePath, positional, flags) {
|
|
489
|
+
if (!flags.out)
|
|
490
|
+
die("Missing --out <path>. Usage: capcut cut <project> <start> <end> --out <path>");
|
|
491
|
+
const start = parseTimeInput(positional[2]);
|
|
492
|
+
const end = parseTimeInput(positional[3]);
|
|
493
|
+
if (end <= start)
|
|
494
|
+
die("End time must be after start time");
|
|
495
|
+
const opts = { start, end };
|
|
496
|
+
const result = cutProject(draft, opts);
|
|
497
|
+
// Write to new file (not in-place)
|
|
498
|
+
const indent = 0;
|
|
499
|
+
writeFileSync(flags.out, JSON.stringify(draft, null, indent), "utf-8");
|
|
500
|
+
out({ ok: true, kept: result.kept, removed: result.removed, duration_us: end - start, out: flags.out }, flags);
|
|
501
|
+
}
|
|
502
|
+
// --- Templates ---
|
|
503
|
+
function cmdSaveTemplate(draft, positional, flags) {
|
|
504
|
+
const segId = positional[2];
|
|
505
|
+
const name = positional[3];
|
|
506
|
+
if (!flags.out)
|
|
507
|
+
die("Missing --out <path>. Usage: capcut save-template <project> <id> <name> --out <path>");
|
|
508
|
+
const template = saveTemplate(draft, segId, name, flags.out);
|
|
509
|
+
out({
|
|
510
|
+
ok: true,
|
|
511
|
+
name: template.name,
|
|
512
|
+
type: template.type,
|
|
513
|
+
material_type: template.material.type,
|
|
514
|
+
extra_materials: template.extra_materials.length,
|
|
515
|
+
out: flags.out,
|
|
516
|
+
}, flags);
|
|
517
|
+
}
|
|
518
|
+
function cmdApplyTemplate(draft, filePath, positional, flags) {
|
|
519
|
+
const templatePath = positional[2];
|
|
520
|
+
const startStr = positional[3];
|
|
521
|
+
const durationStr = positional[4];
|
|
522
|
+
const start = parseTimeInput(startStr);
|
|
523
|
+
const duration = parseTimeInput(durationStr);
|
|
524
|
+
const textOverride = positional.length > 5 ? positional.slice(5).join(" ") : undefined;
|
|
525
|
+
const result = applyTemplate(draft, templatePath, start, duration, {
|
|
526
|
+
x: flags.x,
|
|
527
|
+
y: flags.y,
|
|
528
|
+
text: textOverride,
|
|
529
|
+
});
|
|
530
|
+
saveDraft(filePath, draft);
|
|
531
|
+
out({
|
|
532
|
+
ok: true,
|
|
533
|
+
segment_id: result.segmentId,
|
|
534
|
+
material_id: result.materialId,
|
|
535
|
+
track_id: result.trackId,
|
|
536
|
+
start_us: start,
|
|
537
|
+
duration_us: duration,
|
|
538
|
+
}, flags);
|
|
539
|
+
}
|
|
540
|
+
function execBatchOp(draft, filePath, op, flags) {
|
|
541
|
+
const silent = { ...flags, quiet: true };
|
|
542
|
+
switch (op.cmd) {
|
|
543
|
+
case "set-text":
|
|
544
|
+
if (!op.id || op.text === undefined)
|
|
545
|
+
die(`batch set-text requires id and text`);
|
|
546
|
+
cmdSetText(draft, filePath, op.id, op.text, silent, false);
|
|
547
|
+
break;
|
|
548
|
+
case "shift":
|
|
549
|
+
if (!op.id || !op.offset)
|
|
550
|
+
die(`batch shift requires id and offset`);
|
|
551
|
+
cmdShift(draft, filePath, op.id, op.offset, silent, false);
|
|
552
|
+
break;
|
|
553
|
+
case "shift-all":
|
|
554
|
+
if (!op.offset)
|
|
555
|
+
die(`batch shift-all requires offset`);
|
|
556
|
+
cmdShiftAll(draft, filePath, op.offset, { ...silent, track: op.track }, false);
|
|
557
|
+
break;
|
|
558
|
+
case "speed":
|
|
559
|
+
if (!op.id || op.speed === undefined)
|
|
560
|
+
die(`batch speed requires id and speed`);
|
|
561
|
+
cmdSpeed(draft, filePath, op.id, String(op.speed), silent, false);
|
|
562
|
+
break;
|
|
563
|
+
case "volume":
|
|
564
|
+
if (!op.id || op.volume === undefined)
|
|
565
|
+
die(`batch volume requires id and volume`);
|
|
566
|
+
cmdVolume(draft, filePath, op.id, String(op.volume), silent, false);
|
|
567
|
+
break;
|
|
568
|
+
case "opacity":
|
|
569
|
+
if (!op.id || op.opacity === undefined)
|
|
570
|
+
die(`batch opacity requires id and opacity`);
|
|
571
|
+
cmdOpacity(draft, filePath, op.id, String(op.opacity), silent, false);
|
|
572
|
+
break;
|
|
573
|
+
case "trim":
|
|
574
|
+
if (!op.id || !op.start || !op.duration)
|
|
575
|
+
die(`batch trim requires id, start, duration`);
|
|
576
|
+
cmdTrim(draft, filePath, op.id, op.start, op.duration, silent, false);
|
|
577
|
+
break;
|
|
578
|
+
default:
|
|
579
|
+
die(`Unknown batch command: ${op.cmd}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function cmdBatch(draft, filePath, flags) {
|
|
583
|
+
const input = readFileSync("/dev/stdin", "utf-8").trim();
|
|
584
|
+
if (!input)
|
|
585
|
+
die("No input on stdin");
|
|
586
|
+
const lines = input.split("\n");
|
|
587
|
+
let ok = 0;
|
|
588
|
+
let fail = 0;
|
|
589
|
+
for (const line of lines) {
|
|
590
|
+
const trimmed = line.trim();
|
|
591
|
+
if (!trimmed)
|
|
592
|
+
continue;
|
|
593
|
+
try {
|
|
594
|
+
const op = JSON.parse(trimmed);
|
|
595
|
+
execBatchOp(draft, filePath, op, flags);
|
|
596
|
+
ok++;
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
fail++;
|
|
600
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
601
|
+
process.stderr.write(JSON.stringify({ error: msg, line: trimmed }) + "\n");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
saveDraft(filePath, draft);
|
|
605
|
+
out({ ok: true, succeeded: ok, failed: fail }, flags);
|
|
606
|
+
}
|
|
607
|
+
// --- Main ---
|
|
608
|
+
function main() {
|
|
609
|
+
const raw = process.argv.slice(2);
|
|
610
|
+
if (raw.length === 0 || raw[0] === "--help" || raw[0] === "-h") {
|
|
611
|
+
console.log(HELP);
|
|
612
|
+
process.exit(0);
|
|
613
|
+
}
|
|
614
|
+
const { positional, flags } = parseFlags(raw);
|
|
615
|
+
const cmd = positional[0];
|
|
616
|
+
const projectPath = positional[1];
|
|
617
|
+
if (!projectPath)
|
|
618
|
+
die("Missing project path. Run 'capcut --help' for usage.");
|
|
619
|
+
const { draft, filePath } = loadDraft(projectPath);
|
|
620
|
+
switch (cmd) {
|
|
621
|
+
case "info":
|
|
622
|
+
cmdInfo(draft, flags);
|
|
623
|
+
break;
|
|
624
|
+
case "tracks":
|
|
625
|
+
cmdTracks(draft, flags);
|
|
626
|
+
break;
|
|
627
|
+
case "segments":
|
|
628
|
+
cmdSegments(draft, flags);
|
|
629
|
+
break;
|
|
630
|
+
case "texts":
|
|
631
|
+
cmdTexts(draft, flags);
|
|
632
|
+
break;
|
|
633
|
+
case "set-text":
|
|
634
|
+
requireArgs(positional, 4, "capcut set-text <project> <id> <text>");
|
|
635
|
+
cmdSetText(draft, filePath, positional[2], positional.slice(3).join(" "), flags);
|
|
636
|
+
break;
|
|
637
|
+
case "shift":
|
|
638
|
+
requireArgs(positional, 4, "capcut shift <project> <id> <offset>");
|
|
639
|
+
cmdShift(draft, filePath, positional[2], positional[3], flags);
|
|
640
|
+
break;
|
|
641
|
+
case "shift-all":
|
|
642
|
+
requireArgs(positional, 3, "capcut shift-all <project> <offset> [--track <type>]");
|
|
643
|
+
cmdShiftAll(draft, filePath, positional[2], flags);
|
|
644
|
+
break;
|
|
645
|
+
case "speed":
|
|
646
|
+
requireArgs(positional, 4, "capcut speed <project> <id> <multiplier>");
|
|
647
|
+
cmdSpeed(draft, filePath, positional[2], positional[3], flags);
|
|
648
|
+
break;
|
|
649
|
+
case "volume":
|
|
650
|
+
requireArgs(positional, 4, "capcut volume <project> <id> <level>");
|
|
651
|
+
cmdVolume(draft, filePath, positional[2], positional[3], flags);
|
|
652
|
+
break;
|
|
653
|
+
case "trim":
|
|
654
|
+
requireArgs(positional, 5, "capcut trim <project> <id> <start> <duration>");
|
|
655
|
+
cmdTrim(draft, filePath, positional[2], positional[3], positional[4], flags);
|
|
656
|
+
break;
|
|
657
|
+
case "opacity":
|
|
658
|
+
requireArgs(positional, 4, "capcut opacity <project> <id> <alpha>");
|
|
659
|
+
cmdOpacity(draft, filePath, positional[2], positional[3], flags);
|
|
660
|
+
break;
|
|
661
|
+
case "export-srt":
|
|
662
|
+
cmdExportSrt(draft);
|
|
663
|
+
break;
|
|
664
|
+
case "materials":
|
|
665
|
+
cmdMaterials(draft, flags);
|
|
666
|
+
break;
|
|
667
|
+
case "segment":
|
|
668
|
+
requireArgs(positional, 3, "capcut segment <project> <id>");
|
|
669
|
+
cmdSegmentDetail(draft, positional[2], flags);
|
|
670
|
+
break;
|
|
671
|
+
case "material":
|
|
672
|
+
requireArgs(positional, 3, "capcut material <project> <id>");
|
|
673
|
+
cmdMaterialDetail(draft, positional[2], flags);
|
|
674
|
+
break;
|
|
675
|
+
case "add-text":
|
|
676
|
+
requireArgs(positional, 5, "capcut add-text <project> <start> <duration> <text>");
|
|
677
|
+
cmdAddText(draft, filePath, positional, flags);
|
|
678
|
+
break;
|
|
679
|
+
case "cut":
|
|
680
|
+
requireArgs(positional, 4, "capcut cut <project> <start> <end> --out <path>");
|
|
681
|
+
cmdCut(draft, filePath, positional, flags);
|
|
682
|
+
break;
|
|
683
|
+
case "save-template":
|
|
684
|
+
requireArgs(positional, 4, "capcut save-template <project> <id> <name> --out <path>");
|
|
685
|
+
cmdSaveTemplate(draft, positional, flags);
|
|
686
|
+
break;
|
|
687
|
+
case "apply-template":
|
|
688
|
+
requireArgs(positional, 5, "capcut apply-template <project> <template.json> <start> <duration>");
|
|
689
|
+
cmdApplyTemplate(draft, filePath, positional, flags);
|
|
690
|
+
break;
|
|
691
|
+
case "batch":
|
|
692
|
+
cmdBatch(draft, filePath, flags);
|
|
693
|
+
break;
|
|
694
|
+
default:
|
|
695
|
+
die(`Unknown command: ${cmd}. Run 'capcut --help' for usage.`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
main();
|
|
700
|
+
}
|
|
701
|
+
catch (e) {
|
|
702
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
703
|
+
process.stderr.write(JSON.stringify({ error: msg }) + "\n");
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|