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