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/factory.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { findMaterialGlobal } from "./draft.js";
|
|
4
|
+
// --- UUID generation ---
|
|
5
|
+
export function uuid() {
|
|
6
|
+
return randomUUID();
|
|
7
|
+
}
|
|
8
|
+
export function createCompanionMaterials(trackType) {
|
|
9
|
+
const speed = { id: uuid(), type: "speed", speed: 1, mode: 0, curve_speed: null };
|
|
10
|
+
const placeholder = {
|
|
11
|
+
id: uuid(), type: "placeholder_info",
|
|
12
|
+
error_path: "", error_text: "", meta_type: "none", res_path: "", res_text: "",
|
|
13
|
+
};
|
|
14
|
+
const scm = {
|
|
15
|
+
id: uuid(), type: "none",
|
|
16
|
+
audio_channel_mapping: 0, is_config_open: false,
|
|
17
|
+
};
|
|
18
|
+
const vocal = {
|
|
19
|
+
id: uuid(), type: "vocal_separation",
|
|
20
|
+
choice: 0, enter_from: "", final_algorithm: "",
|
|
21
|
+
production_path: "", removed_sounds: [], time_range: null,
|
|
22
|
+
};
|
|
23
|
+
const refs = {
|
|
24
|
+
ids: [speed.id, placeholder.id, scm.id, vocal.id],
|
|
25
|
+
materials: [
|
|
26
|
+
{ type: "speeds", data: speed },
|
|
27
|
+
{ type: "placeholder_infos", data: placeholder },
|
|
28
|
+
{ type: "sound_channel_mappings", data: scm },
|
|
29
|
+
{ type: "vocal_separations", data: vocal },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
if (trackType === "video") {
|
|
33
|
+
const canvas = {
|
|
34
|
+
id: uuid(), type: "canvas_color",
|
|
35
|
+
album_image: "", blur: 0, color: "", image: "",
|
|
36
|
+
image_id: "", image_name: "", source_platform: 0, team_id: "",
|
|
37
|
+
};
|
|
38
|
+
const matColor = {
|
|
39
|
+
id: uuid(), type: "material_color",
|
|
40
|
+
gradient_angle: 90, gradient_colors: [], gradient_percents: [],
|
|
41
|
+
height: 0, is_color_clip: false, is_gradient: false, solid_color: "", width: 0,
|
|
42
|
+
};
|
|
43
|
+
refs.ids.push(canvas.id, matColor.id);
|
|
44
|
+
refs.materials.push({ type: "canvases", data: canvas }, { type: "material_colors", data: matColor });
|
|
45
|
+
}
|
|
46
|
+
return refs;
|
|
47
|
+
}
|
|
48
|
+
export function registerCompanions(draft, companions) {
|
|
49
|
+
for (const { type, data } of companions.materials) {
|
|
50
|
+
if (!draft.materials[type])
|
|
51
|
+
draft.materials[type] = [];
|
|
52
|
+
draft.materials[type].push(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// --- Base segment ---
|
|
56
|
+
function baseSegment(id, materialId, trackId, timerange, companionIds, renderIndex) {
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
material_id: materialId,
|
|
60
|
+
raw_segment_id: trackId,
|
|
61
|
+
target_timerange: { ...timerange },
|
|
62
|
+
source_timerange: { start: 0, duration: timerange.duration },
|
|
63
|
+
speed: 1,
|
|
64
|
+
volume: 1,
|
|
65
|
+
visible: true,
|
|
66
|
+
reverse: false,
|
|
67
|
+
clip: {
|
|
68
|
+
alpha: 1,
|
|
69
|
+
rotation: 0,
|
|
70
|
+
scale: { x: 1, y: 1 },
|
|
71
|
+
transform: { x: 0, y: 0 },
|
|
72
|
+
flip: { horizontal: false, vertical: false },
|
|
73
|
+
},
|
|
74
|
+
render_index: renderIndex,
|
|
75
|
+
track_render_index: 0,
|
|
76
|
+
track_attribute: 0,
|
|
77
|
+
extra_material_refs: companionIds,
|
|
78
|
+
common_keyframes: [],
|
|
79
|
+
keyframe_refs: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function hexToRgb(hex) {
|
|
83
|
+
const h = hex.replace("#", "");
|
|
84
|
+
return [
|
|
85
|
+
parseInt(h.slice(0, 2), 16) / 255,
|
|
86
|
+
parseInt(h.slice(2, 4), 16) / 255,
|
|
87
|
+
parseInt(h.slice(4, 6), 16) / 255,
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function buildTextContent(text, fontSize, color) {
|
|
91
|
+
const encoded = Buffer.from(text, "utf16le");
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
styles: [{
|
|
94
|
+
range: [0, encoded.length],
|
|
95
|
+
size: fontSize,
|
|
96
|
+
bold: false,
|
|
97
|
+
italic: false,
|
|
98
|
+
underline: false,
|
|
99
|
+
fill: {
|
|
100
|
+
alpha: 1,
|
|
101
|
+
content: {
|
|
102
|
+
render_type: "solid",
|
|
103
|
+
solid: { alpha: 1, color },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}],
|
|
107
|
+
text,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function addText(draft, filePath, opts) {
|
|
111
|
+
const segId = uuid();
|
|
112
|
+
const matId = uuid();
|
|
113
|
+
const fontSize = opts.fontSize ?? 15;
|
|
114
|
+
const color = opts.color ?? "#FFFFFF";
|
|
115
|
+
const rgb = hexToRgb(color);
|
|
116
|
+
const alignment = opts.alignment ?? 1;
|
|
117
|
+
const trackName = opts.trackName ?? "text";
|
|
118
|
+
// Find or create text track
|
|
119
|
+
let track = draft.tracks.find(t => t.type === "text" && (t.name === trackName || !opts.trackName));
|
|
120
|
+
if (!track) {
|
|
121
|
+
track = {
|
|
122
|
+
id: uuid(),
|
|
123
|
+
type: "text",
|
|
124
|
+
name: trackName,
|
|
125
|
+
attribute: 0,
|
|
126
|
+
segments: [],
|
|
127
|
+
is_default_name: false,
|
|
128
|
+
flag: 0,
|
|
129
|
+
};
|
|
130
|
+
draft.tracks.push(track);
|
|
131
|
+
}
|
|
132
|
+
// Create companion materials
|
|
133
|
+
const companions = createCompanionMaterials("text");
|
|
134
|
+
registerCompanions(draft, companions);
|
|
135
|
+
// Create text material
|
|
136
|
+
const textMaterial = {
|
|
137
|
+
id: matId,
|
|
138
|
+
type: "text",
|
|
139
|
+
content: buildTextContent(opts.text, fontSize, rgb),
|
|
140
|
+
alignment,
|
|
141
|
+
font_size: fontSize,
|
|
142
|
+
text_color: color,
|
|
143
|
+
typesetting: 0,
|
|
144
|
+
letter_spacing: 0,
|
|
145
|
+
line_spacing: 0.02,
|
|
146
|
+
line_feed: 1,
|
|
147
|
+
line_max_width: 0.82,
|
|
148
|
+
force_apply_line_max_width: false,
|
|
149
|
+
check_flag: 7,
|
|
150
|
+
fixed_width: -1,
|
|
151
|
+
fixed_height: -1,
|
|
152
|
+
};
|
|
153
|
+
draft.materials.texts.push(textMaterial);
|
|
154
|
+
// Create segment
|
|
155
|
+
const timerange = { start: opts.start, duration: opts.duration };
|
|
156
|
+
const seg = baseSegment(segId, matId, track.id, timerange, companions.ids, 15000);
|
|
157
|
+
if (opts.x !== undefined || opts.y !== undefined) {
|
|
158
|
+
seg.clip.transform = { x: opts.x ?? 0, y: opts.y ?? 0 };
|
|
159
|
+
}
|
|
160
|
+
track.segments.push(seg);
|
|
161
|
+
return { segmentId: segId, materialId: matId, trackId: track.id };
|
|
162
|
+
}
|
|
163
|
+
export function cutProject(draft, opts) {
|
|
164
|
+
const { start, end } = opts;
|
|
165
|
+
const duration = end - start;
|
|
166
|
+
let kept = 0;
|
|
167
|
+
let removed = 0;
|
|
168
|
+
// Collect material IDs to remove
|
|
169
|
+
const removedMaterialIds = new Set();
|
|
170
|
+
const removedExtraRefs = new Set();
|
|
171
|
+
for (const track of draft.tracks) {
|
|
172
|
+
const surviving = [];
|
|
173
|
+
for (const seg of track.segments) {
|
|
174
|
+
const segStart = seg.target_timerange.start;
|
|
175
|
+
const segEnd = segStart + seg.target_timerange.duration;
|
|
176
|
+
// Skip segments entirely outside the range
|
|
177
|
+
if (segEnd <= start || segStart >= end) {
|
|
178
|
+
removedMaterialIds.add(seg.material_id);
|
|
179
|
+
for (const ref of seg.extra_material_refs)
|
|
180
|
+
removedExtraRefs.add(ref);
|
|
181
|
+
removed++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Clip segment to range
|
|
185
|
+
const clippedStart = Math.max(segStart, start);
|
|
186
|
+
const clippedEnd = Math.min(segEnd, end);
|
|
187
|
+
const trimFromStart = clippedStart - segStart;
|
|
188
|
+
const newDuration = clippedEnd - clippedStart;
|
|
189
|
+
// Adjust source_timerange for the trim
|
|
190
|
+
if (seg.source_timerange) {
|
|
191
|
+
seg.source_timerange.start += Math.round(trimFromStart * seg.speed);
|
|
192
|
+
seg.source_timerange.duration = Math.round(newDuration * seg.speed);
|
|
193
|
+
}
|
|
194
|
+
seg.target_timerange.start = clippedStart - start; // rebase to 0
|
|
195
|
+
seg.target_timerange.duration = newDuration;
|
|
196
|
+
surviving.push(seg);
|
|
197
|
+
kept++;
|
|
198
|
+
}
|
|
199
|
+
track.segments = surviving;
|
|
200
|
+
}
|
|
201
|
+
// Remove empty tracks
|
|
202
|
+
draft.tracks = draft.tracks.filter(t => t.segments.length > 0);
|
|
203
|
+
// Clean up orphaned materials (only if not referenced by surviving segments)
|
|
204
|
+
const survivingMatIds = new Set();
|
|
205
|
+
const survivingExtraRefs = new Set();
|
|
206
|
+
for (const track of draft.tracks) {
|
|
207
|
+
for (const seg of track.segments) {
|
|
208
|
+
survivingMatIds.add(seg.material_id);
|
|
209
|
+
for (const ref of seg.extra_material_refs)
|
|
210
|
+
survivingExtraRefs.add(ref);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [key, arr] of Object.entries(draft.materials)) {
|
|
214
|
+
if (!Array.isArray(arr))
|
|
215
|
+
continue;
|
|
216
|
+
draft.materials[key] = arr.filter((m) => {
|
|
217
|
+
if (!m || typeof m.id !== "string")
|
|
218
|
+
return true;
|
|
219
|
+
const id = m.id;
|
|
220
|
+
// Keep if referenced by any surviving segment
|
|
221
|
+
if (survivingMatIds.has(id) || survivingExtraRefs.has(id))
|
|
222
|
+
return true;
|
|
223
|
+
// Remove if only referenced by removed segments
|
|
224
|
+
if (removedMaterialIds.has(id) || removedExtraRefs.has(id))
|
|
225
|
+
return false;
|
|
226
|
+
// Keep anything not directly tracked (safety)
|
|
227
|
+
return true;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// Update project duration
|
|
231
|
+
draft.duration = duration;
|
|
232
|
+
return { kept, removed };
|
|
233
|
+
}
|
|
234
|
+
export function saveTemplate(draft, segId, name, outPath) {
|
|
235
|
+
const shortId = segId.toLowerCase();
|
|
236
|
+
let foundSeg = null;
|
|
237
|
+
let foundTrack = null;
|
|
238
|
+
for (const track of draft.tracks) {
|
|
239
|
+
for (const seg of track.segments) {
|
|
240
|
+
if (seg.id === segId || seg.id.toLowerCase().startsWith(shortId)) {
|
|
241
|
+
foundSeg = seg;
|
|
242
|
+
foundTrack = track;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (foundSeg)
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
if (!foundSeg || !foundTrack)
|
|
250
|
+
throw new Error(`Segment not found: ${segId}`);
|
|
251
|
+
// Resolve primary material
|
|
252
|
+
const mat = findMaterialGlobal(draft, foundSeg.material_id);
|
|
253
|
+
if (!mat)
|
|
254
|
+
throw new Error(`Material not found for segment: ${segId}`);
|
|
255
|
+
// Resolve extra material refs
|
|
256
|
+
const extras = [];
|
|
257
|
+
for (const refId of foundSeg.extra_material_refs) {
|
|
258
|
+
const extra = findMaterialGlobal(draft, refId);
|
|
259
|
+
if (extra)
|
|
260
|
+
extras.push({ type: extra.type, data: { ...extra.material } });
|
|
261
|
+
}
|
|
262
|
+
const template = {
|
|
263
|
+
name,
|
|
264
|
+
type: foundTrack.type,
|
|
265
|
+
segment: { ...foundSeg },
|
|
266
|
+
material: { type: mat.type, data: { ...mat.material } },
|
|
267
|
+
extra_materials: extras,
|
|
268
|
+
};
|
|
269
|
+
writeFileSync(outPath, JSON.stringify(template, null, 2), "utf-8");
|
|
270
|
+
return template;
|
|
271
|
+
}
|
|
272
|
+
export function applyTemplate(draft, templatePath, start, duration, overrides) {
|
|
273
|
+
const template = JSON.parse(readFileSync(templatePath, "utf-8"));
|
|
274
|
+
// Generate new IDs for everything
|
|
275
|
+
const idMap = new Map();
|
|
276
|
+
function remapId(oldId) {
|
|
277
|
+
if (!idMap.has(oldId))
|
|
278
|
+
idMap.set(oldId, uuid());
|
|
279
|
+
return idMap.get(oldId);
|
|
280
|
+
}
|
|
281
|
+
const newSegId = uuid();
|
|
282
|
+
const newMatId = uuid();
|
|
283
|
+
// Clone and remap the material
|
|
284
|
+
const newMat = deepCloneWithIdRemap(template.material.data, remapId);
|
|
285
|
+
newMat.id = newMatId;
|
|
286
|
+
// If text and override provided, update content
|
|
287
|
+
if (overrides?.text && template.type === "text" && typeof newMat.content === "string") {
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(newMat.content);
|
|
290
|
+
if (parsed.text !== undefined) {
|
|
291
|
+
parsed.text = overrides.text;
|
|
292
|
+
if (parsed.styles && parsed.styles.length > 0) {
|
|
293
|
+
const encoded = Buffer.from(overrides.text, "utf16le");
|
|
294
|
+
parsed.styles[0].range = [0, encoded.length];
|
|
295
|
+
}
|
|
296
|
+
newMat.content = JSON.stringify(parsed);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { /* keep original content */ }
|
|
300
|
+
}
|
|
301
|
+
// Register primary material
|
|
302
|
+
if (!draft.materials[template.material.type])
|
|
303
|
+
draft.materials[template.material.type] = [];
|
|
304
|
+
draft.materials[template.material.type].push(newMat);
|
|
305
|
+
// Clone and register extra materials
|
|
306
|
+
const newExtraIds = [];
|
|
307
|
+
for (const extra of template.extra_materials) {
|
|
308
|
+
const newExtra = deepCloneWithIdRemap(extra.data, remapId);
|
|
309
|
+
newExtraIds.push(newExtra.id);
|
|
310
|
+
if (!draft.materials[extra.type])
|
|
311
|
+
draft.materials[extra.type] = [];
|
|
312
|
+
draft.materials[extra.type].push(newExtra);
|
|
313
|
+
}
|
|
314
|
+
// Also add companion materials if the template didn't have them
|
|
315
|
+
if (newExtraIds.length === 0) {
|
|
316
|
+
const companions = createCompanionMaterials(template.type);
|
|
317
|
+
registerCompanions(draft, companions);
|
|
318
|
+
newExtraIds.push(...companions.ids);
|
|
319
|
+
}
|
|
320
|
+
// Find or create track
|
|
321
|
+
let track = draft.tracks.find(t => t.type === template.type);
|
|
322
|
+
if (!track) {
|
|
323
|
+
track = {
|
|
324
|
+
id: uuid(),
|
|
325
|
+
type: template.type,
|
|
326
|
+
name: template.name || template.type,
|
|
327
|
+
attribute: 0,
|
|
328
|
+
segments: [],
|
|
329
|
+
is_default_name: true,
|
|
330
|
+
flag: 0,
|
|
331
|
+
};
|
|
332
|
+
draft.tracks.push(track);
|
|
333
|
+
}
|
|
334
|
+
// Clone segment with new IDs and timing
|
|
335
|
+
const newSeg = { ...template.segment };
|
|
336
|
+
newSeg.id = newSegId;
|
|
337
|
+
newSeg.material_id = newMatId;
|
|
338
|
+
newSeg.raw_segment_id = track.id;
|
|
339
|
+
newSeg.target_timerange = { start, duration };
|
|
340
|
+
if (template.segment.source_timerange) {
|
|
341
|
+
newSeg.source_timerange = { start: 0, duration };
|
|
342
|
+
}
|
|
343
|
+
newSeg.extra_material_refs = newExtraIds;
|
|
344
|
+
// Apply position/scale overrides
|
|
345
|
+
if (overrides && newSeg.clip && typeof newSeg.clip === "object") {
|
|
346
|
+
const clip = newSeg.clip;
|
|
347
|
+
if (overrides.x !== undefined || overrides.y !== undefined) {
|
|
348
|
+
clip.transform = {
|
|
349
|
+
x: overrides.x ?? clip.transform?.x ?? 0,
|
|
350
|
+
y: overrides.y ?? clip.transform?.y ?? 0,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (overrides.scaleX !== undefined || overrides.scaleY !== undefined) {
|
|
354
|
+
clip.scale = {
|
|
355
|
+
x: overrides.scaleX ?? clip.scale?.x ?? 1,
|
|
356
|
+
y: overrides.scaleY ?? clip.scale?.y ?? 1,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
track.segments.push(newSeg);
|
|
361
|
+
return { segmentId: newSegId, materialId: newMatId, trackId: track.id };
|
|
362
|
+
}
|
|
363
|
+
function deepCloneWithIdRemap(obj, remapId) {
|
|
364
|
+
const clone = JSON.parse(JSON.stringify(obj));
|
|
365
|
+
// Remap the id field
|
|
366
|
+
if (typeof clone.id === "string") {
|
|
367
|
+
clone.id = remapId(clone.id);
|
|
368
|
+
}
|
|
369
|
+
return clone;
|
|
370
|
+
}
|