blitzreels 0.1.5 → 0.1.7
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 +41 -8
- package/dist/agent-context.d.ts +26 -1
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +45 -1
- package/dist/agent-context.js.map +1 -1
- package/dist/cli-command-routing.d.ts.map +1 -1
- package/dist/cli-command-routing.js +18 -3
- package/dist/cli-command-routing.js.map +1 -1
- package/dist/cli-errors.d.ts +3 -1
- package/dist/cli-errors.d.ts.map +1 -1
- package/dist/cli-errors.js +3 -1
- package/dist/cli-errors.js.map +1 -1
- package/dist/cli-presenter.d.ts +2 -1
- package/dist/cli-presenter.d.ts.map +1 -1
- package/dist/cli-presenter.js +75 -31
- package/dist/cli-presenter.js.map +1 -1
- package/dist/cli-runtime.d.ts +1 -2
- package/dist/cli-runtime.d.ts.map +1 -1
- package/dist/cli-runtime.js +20 -2
- package/dist/cli-runtime.js.map +1 -1
- package/dist/cli-types.d.ts +2 -1
- package/dist/cli-types.d.ts.map +1 -1
- package/dist/cli-value-parsers.d.ts +4 -4
- package/dist/cli-value-parsers.d.ts.map +1 -1
- package/dist/cli-value-parsers.js +11 -6
- package/dist/cli-value-parsers.js.map +1 -1
- package/dist/command-metadata.d.ts +9 -3
- package/dist/command-metadata.d.ts.map +1 -1
- package/dist/command-metadata.js +33 -4
- package/dist/command-metadata.js.map +1 -1
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +10 -0
- package/dist/command-registry.js.map +1 -1
- package/dist/commands/ai-edit.js +7 -5
- package/dist/commands/ai-edit.js.map +1 -1
- package/dist/commands/audio.d.ts +3 -3
- package/dist/commands/audio.d.ts.map +1 -1
- package/dist/commands/audio.js +5 -2
- package/dist/commands/audio.js.map +1 -1
- package/dist/commands/caption-dry-run.js +20 -0
- package/dist/commands/caption-dry-run.js.map +1 -1
- package/dist/commands/caption-selection.d.ts +2 -1
- package/dist/commands/caption-selection.d.ts.map +1 -1
- package/dist/commands/caption-style-flags.d.ts.map +1 -1
- package/dist/commands/caption-style-flags.js +58 -0
- package/dist/commands/caption-style-flags.js.map +1 -1
- package/dist/commands/captions-specs.d.ts.map +1 -1
- package/dist/commands/captions-specs.js +23 -1
- package/dist/commands/captions-specs.js.map +1 -1
- package/dist/commands/captions.js +23 -1
- package/dist/commands/captions.js.map +1 -1
- package/dist/commands/clip-runtime.d.ts +2 -5
- package/dist/commands/clip-runtime.d.ts.map +1 -1
- package/dist/commands/clip-runtime.js +36 -111
- package/dist/commands/clip-runtime.js.map +1 -1
- package/dist/commands/clips.d.ts +12 -5
- package/dist/commands/clips.d.ts.map +1 -1
- package/dist/commands/clips.js +466 -83
- package/dist/commands/clips.js.map +1 -1
- package/dist/commands/effects.d.ts.map +1 -1
- package/dist/commands/effects.js +6 -0
- package/dist/commands/effects.js.map +1 -1
- package/dist/commands/exports.d.ts +1 -8
- package/dist/commands/exports.d.ts.map +1 -1
- package/dist/commands/exports.js +69 -7
- package/dist/commands/exports.js.map +1 -1
- package/dist/commands/faceless.d.ts +44 -0
- package/dist/commands/faceless.d.ts.map +1 -0
- package/dist/commands/faceless.js +587 -0
- package/dist/commands/faceless.js.map +1 -0
- package/dist/commands/formats.d.ts +41 -0
- package/dist/commands/formats.d.ts.map +1 -0
- package/dist/commands/formats.js +140 -0
- package/dist/commands/formats.js.map +1 -0
- package/dist/commands/generation.d.ts +25 -0
- package/dist/commands/generation.d.ts.map +1 -0
- package/dist/commands/generation.js +494 -0
- package/dist/commands/generation.js.map +1 -0
- package/dist/commands/jobs.d.ts +20 -0
- package/dist/commands/jobs.d.ts.map +1 -0
- package/dist/commands/jobs.js +40 -0
- package/dist/commands/jobs.js.map +1 -0
- package/dist/commands/media-assets.d.ts.map +1 -1
- package/dist/commands/media-assets.js +81 -9
- package/dist/commands/media-assets.js.map +1 -1
- package/dist/commands/media-overlays.d.ts.map +1 -1
- package/dist/commands/media-overlays.js +50 -6
- package/dist/commands/media-overlays.js.map +1 -1
- package/dist/commands/media-runtime.js +19 -1
- package/dist/commands/media-runtime.js.map +1 -1
- package/dist/commands/onboarding.d.ts +51 -0
- package/dist/commands/onboarding.d.ts.map +1 -0
- package/dist/commands/onboarding.js +452 -0
- package/dist/commands/onboarding.js.map +1 -0
- package/dist/commands/project-read.d.ts.map +1 -1
- package/dist/commands/project-read.js +32 -3
- package/dist/commands/project-read.js.map +1 -1
- package/dist/commands/projects.d.ts +1 -6
- package/dist/commands/projects.d.ts.map +1 -1
- package/dist/commands/projects.js +24 -4
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/system.d.ts.map +1 -1
- package/dist/commands/system.js +56 -14
- package/dist/commands/system.js.map +1 -1
- package/dist/commands/text-overlay-spec.d.ts +6 -0
- package/dist/commands/text-overlay-spec.d.ts.map +1 -1
- package/dist/commands/text-overlay-spec.js +59 -0
- package/dist/commands/text-overlay-spec.js.map +1 -1
- package/dist/commands/text-overlays.d.ts +6 -0
- package/dist/commands/text-overlays.d.ts.map +1 -1
- package/dist/commands/text-overlays.js +74 -1
- package/dist/commands/text-overlays.js.map +1 -1
- package/dist/commands/timeline-runtime.d.ts +3 -0
- package/dist/commands/timeline-runtime.d.ts.map +1 -1
- package/dist/commands/timeline-runtime.js +102 -9
- package/dist/commands/timeline-runtime.js.map +1 -1
- package/dist/commands/timeline.d.ts +4 -0
- package/dist/commands/timeline.d.ts.map +1 -1
- package/dist/commands/timeline.js +119 -47
- package/dist/commands/timeline.js.map +1 -1
- package/dist/index.js +59 -3
- package/dist/index.js.map +1 -1
- package/dist/preview-first-edit.d.ts +3 -1
- package/dist/preview-first-edit.d.ts.map +1 -1
- package/dist/preview-first-edit.js +2 -2
- package/dist/preview-first-edit.js.map +1 -1
- package/dist/update-notifier.d.ts +2 -1
- package/dist/update-notifier.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/commands/clips.js
CHANGED
|
@@ -1,30 +1,78 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
1
3
|
import { commandSpec } from "../command-metadata.js";
|
|
2
4
|
export const CLIPS_COMMAND_SPECS = [
|
|
3
5
|
commandSpec({
|
|
4
|
-
command: ["clips", "
|
|
5
|
-
usage: "blitzreels clips
|
|
6
|
+
command: ["clips", "list"],
|
|
7
|
+
usage: "blitzreels clips list [--asset-id ID] [--project-id ID] [--status ready|exported|failed|needs_repair] [--limit 20] [--offset 0] [--json]",
|
|
6
8
|
flags: [
|
|
7
|
-
"source-type",
|
|
8
9
|
"asset-id",
|
|
10
|
+
"project-id",
|
|
11
|
+
{
|
|
12
|
+
name: "status",
|
|
13
|
+
enum: [
|
|
14
|
+
"preparing_source",
|
|
15
|
+
"awaiting_suggestions",
|
|
16
|
+
"selecting_clip",
|
|
17
|
+
"assembling",
|
|
18
|
+
"qa_running",
|
|
19
|
+
"needs_reselection",
|
|
20
|
+
"needs_repair",
|
|
21
|
+
"ready",
|
|
22
|
+
"exporting",
|
|
23
|
+
"exported",
|
|
24
|
+
"failed",
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
"limit",
|
|
28
|
+
"offset",
|
|
29
|
+
],
|
|
30
|
+
description: "List managed clip runs with bounded output.",
|
|
31
|
+
}),
|
|
32
|
+
commandSpec({
|
|
33
|
+
command: ["clips", "create"],
|
|
34
|
+
usage: "blitzreels clips create [--asset-id ID|--youtube-url URL] [--output ./clips] [--count 5] [--idempotency-key KEY]",
|
|
35
|
+
flags: [
|
|
36
|
+
{ name: "asset-id", enum: [] },
|
|
9
37
|
"youtube-url",
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
{
|
|
39
|
+
name: "output",
|
|
40
|
+
description: "Directory for exported MP4 downloads.",
|
|
41
|
+
},
|
|
42
|
+
"logo-asset-id",
|
|
43
|
+
{
|
|
44
|
+
name: "count",
|
|
45
|
+
type: "number",
|
|
46
|
+
default: 5,
|
|
47
|
+
description: "Maximum rendered clips to download from the batch.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "preset",
|
|
51
|
+
enum: ["default", "high-retention", "formation-clipperz", "demo-focus"],
|
|
52
|
+
default: "default",
|
|
53
|
+
description: "Override clip preset.",
|
|
54
|
+
hidden: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "clip-preset-id",
|
|
58
|
+
enum: ["default", "high-retention", "formation-clipperz", "demo-focus"],
|
|
59
|
+
default: "default",
|
|
60
|
+
description: "Override clip preset id.",
|
|
61
|
+
hidden: false,
|
|
62
|
+
},
|
|
63
|
+
{ name: "caption-theme-id" },
|
|
64
|
+
{
|
|
65
|
+
name: "idempotency-key",
|
|
66
|
+
description: "Stable retry key; reuse only with identical inputs.",
|
|
67
|
+
},
|
|
68
|
+
{ name: "interval-ms", type: "number", hidden: true },
|
|
69
|
+
{ name: "timeout-ms", type: "number", hidden: true },
|
|
23
70
|
],
|
|
24
|
-
description: "Create
|
|
71
|
+
description: "Create clips from a workspace asset or YouTube URL; with --output it exports 1080p MP4 files.",
|
|
25
72
|
examples: [
|
|
26
|
-
|
|
27
|
-
"blitzreels clips create --
|
|
73
|
+
"blitzreels clips create --asset-id ID --output ./clips",
|
|
74
|
+
"blitzreels clips create --asset-id ID --logo-asset-id LOGO_ID --output ./clips --count 5",
|
|
75
|
+
"blitzreels clips create --youtube-url https://youtube.com/watch?v=...",
|
|
28
76
|
],
|
|
29
77
|
}),
|
|
30
78
|
commandSpec({
|
|
@@ -41,31 +89,59 @@ export const CLIPS_COMMAND_SPECS = [
|
|
|
41
89
|
}),
|
|
42
90
|
commandSpec({
|
|
43
91
|
command: ["clips", "reselect"],
|
|
44
|
-
usage: "blitzreels clips reselect --clip-id ID
|
|
92
|
+
usage: "blitzreels clips reselect --clip-id ID [--suggestion-id ID | --start-seconds 52 --end-seconds 64]",
|
|
45
93
|
flags: [
|
|
46
94
|
"clip-id",
|
|
47
|
-
"selection-mode",
|
|
48
95
|
"suggestion-id",
|
|
49
96
|
"start-seconds",
|
|
50
97
|
"end-seconds",
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
{
|
|
99
|
+
name: "selection-mode",
|
|
100
|
+
enum: ["auto_best", "suggestion", "time_range"],
|
|
101
|
+
hidden: true,
|
|
102
|
+
},
|
|
103
|
+
{ name: "min-duration", hidden: true },
|
|
104
|
+
{ name: "max-duration", hidden: true },
|
|
53
105
|
],
|
|
54
106
|
description: "Choose a different suggestion or time range for a clip.",
|
|
55
107
|
mutates: true,
|
|
56
108
|
}),
|
|
57
109
|
commandSpec({
|
|
58
110
|
command: ["clips", "repair"],
|
|
59
|
-
usage: "blitzreels clips repair --clip-id ID
|
|
60
|
-
flags: [
|
|
111
|
+
usage: "blitzreels clips repair --clip-id ID",
|
|
112
|
+
flags: [
|
|
113
|
+
"clip-id",
|
|
114
|
+
{
|
|
115
|
+
name: "repair-mode",
|
|
116
|
+
enum: [
|
|
117
|
+
"auto",
|
|
118
|
+
"prefer_split",
|
|
119
|
+
"prefer_focus",
|
|
120
|
+
"prefer_tutorial",
|
|
121
|
+
"prefer_demo",
|
|
122
|
+
"move_captions_off_faces",
|
|
123
|
+
],
|
|
124
|
+
default: "auto",
|
|
125
|
+
hidden: true,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
61
128
|
description: "Run a repair pass on an assembled clip.",
|
|
62
129
|
mutates: true,
|
|
63
130
|
}),
|
|
64
131
|
commandSpec({
|
|
65
132
|
command: ["clips", "export"],
|
|
66
|
-
usage: "blitzreels clips export --clip-id ID
|
|
67
|
-
flags: [
|
|
68
|
-
|
|
133
|
+
usage: "blitzreels clips export --clip-id ID",
|
|
134
|
+
flags: [
|
|
135
|
+
"clip-id",
|
|
136
|
+
{
|
|
137
|
+
name: "resolution",
|
|
138
|
+
enum: ["720p", "1080p"],
|
|
139
|
+
default: "1080p",
|
|
140
|
+
hidden: true,
|
|
141
|
+
},
|
|
142
|
+
{ name: "format", enum: ["mp4", "mov"], default: "mp4", hidden: true },
|
|
143
|
+
],
|
|
144
|
+
description: "Export a managed clip that has passed QA.",
|
|
69
145
|
mutates: true,
|
|
70
146
|
}),
|
|
71
147
|
];
|
|
@@ -81,8 +157,40 @@ async function handleClipsCommand({ context, helpers, }) {
|
|
|
81
157
|
const action = context.args.command[1];
|
|
82
158
|
const flags = context.args.flags;
|
|
83
159
|
if (action === "create") {
|
|
84
|
-
const result = await
|
|
85
|
-
helpers.
|
|
160
|
+
const result = await createClipWorkflow({ context, helpers });
|
|
161
|
+
helpers.printResult({
|
|
162
|
+
json: context.json,
|
|
163
|
+
value: result,
|
|
164
|
+
human: result.output_dir === null
|
|
165
|
+
? [
|
|
166
|
+
`Clip batch created: ${result.batch_id}`,
|
|
167
|
+
`Status: ${result.status}`,
|
|
168
|
+
]
|
|
169
|
+
: [
|
|
170
|
+
`Clips created: ${result.summary.completed}/${result.summary.requested}`,
|
|
171
|
+
`Output: ${result.output_dir}`,
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (action === "list") {
|
|
177
|
+
const result = await context.client.clips.list({
|
|
178
|
+
assetId: helpers.getStringFlag({ flags, name: "asset-id" }),
|
|
179
|
+
projectId: helpers.getStringFlag({ flags, name: "project-id" }),
|
|
180
|
+
status: parseClipStatus({
|
|
181
|
+
value: helpers.getStringFlag({ flags, name: "status" }),
|
|
182
|
+
}),
|
|
183
|
+
limit: helpers.getNumberFlag({ flags, name: "limit" }),
|
|
184
|
+
offset: helpers.getNumberFlag({ flags, name: "offset" }),
|
|
185
|
+
});
|
|
186
|
+
helpers.printResult({
|
|
187
|
+
json: context.json,
|
|
188
|
+
value: result,
|
|
189
|
+
human: [
|
|
190
|
+
`Returned ${result.bounds.returned}/${result.bounds.total} clip(s).`,
|
|
191
|
+
...result.clips.map((clip) => formatClipListLine({ clip })),
|
|
192
|
+
],
|
|
193
|
+
});
|
|
86
194
|
return true;
|
|
87
195
|
}
|
|
88
196
|
if (action === "get") {
|
|
@@ -98,23 +206,42 @@ async function handleClipsCommand({ context, helpers, }) {
|
|
|
98
206
|
return true;
|
|
99
207
|
}
|
|
100
208
|
if (action === "reselect") {
|
|
209
|
+
const clipId = helpers.requireStringFlag({ flags, name: "clip-id" });
|
|
210
|
+
const selection = helpers.buildClipSelection({ flags });
|
|
211
|
+
const target = helpers.buildClipTarget({
|
|
212
|
+
flags,
|
|
213
|
+
requireMaxDuration: false,
|
|
214
|
+
});
|
|
215
|
+
const selectionMode = helpers.getStringFlag({
|
|
216
|
+
flags,
|
|
217
|
+
name: "selection-mode",
|
|
218
|
+
});
|
|
101
219
|
const result = await context.client.clips.reselect({
|
|
102
|
-
clipId
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
220
|
+
clipId,
|
|
221
|
+
...(selectionMode === undefined
|
|
222
|
+
? {
|
|
223
|
+
suggestionId: selection.suggestionId,
|
|
224
|
+
startSeconds: selection.startSeconds,
|
|
225
|
+
endSeconds: selection.endSeconds,
|
|
226
|
+
target,
|
|
227
|
+
}
|
|
228
|
+
: {
|
|
229
|
+
selection,
|
|
230
|
+
target,
|
|
231
|
+
}),
|
|
108
232
|
});
|
|
109
233
|
helpers.printClipResult({ context, result });
|
|
110
234
|
return true;
|
|
111
235
|
}
|
|
112
236
|
if (action === "repair") {
|
|
237
|
+
const repairMode = helpers.getStringFlag({ flags, name: "repair-mode" });
|
|
113
238
|
const result = await context.client.clips.repair({
|
|
114
239
|
clipId: helpers.requireStringFlag({ flags, name: "clip-id" }),
|
|
115
|
-
repairMode
|
|
116
|
-
|
|
117
|
-
|
|
240
|
+
...(repairMode === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: {
|
|
243
|
+
repairMode: helpers.parseClipRepairMode({ value: repairMode }),
|
|
244
|
+
}),
|
|
118
245
|
});
|
|
119
246
|
helpers.printClipResult({ context, result });
|
|
120
247
|
return true;
|
|
@@ -128,53 +255,101 @@ async function handleClipsCommand({ context, helpers, }) {
|
|
|
128
255
|
format: helpers.parseClipExportFormat({
|
|
129
256
|
value: helpers.getStringFlag({ flags, name: "format" }),
|
|
130
257
|
}),
|
|
131
|
-
allowBlockingQaBypass:
|
|
132
|
-
flags,
|
|
133
|
-
name: "allow-blocking-qa-bypass",
|
|
134
|
-
}) ?? false,
|
|
258
|
+
allowBlockingQaBypass: false,
|
|
135
259
|
});
|
|
136
260
|
helpers.printClipResult({ context, result });
|
|
137
261
|
return true;
|
|
138
262
|
}
|
|
139
263
|
return false;
|
|
140
264
|
}
|
|
265
|
+
function parseClipStatus({ value, }) {
|
|
266
|
+
if (value === undefined) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
if (value === "preparing_source" ||
|
|
270
|
+
value === "awaiting_suggestions" ||
|
|
271
|
+
value === "selecting_clip" ||
|
|
272
|
+
value === "assembling" ||
|
|
273
|
+
value === "qa_running" ||
|
|
274
|
+
value === "needs_reselection" ||
|
|
275
|
+
value === "needs_repair" ||
|
|
276
|
+
value === "ready" ||
|
|
277
|
+
value === "exporting" ||
|
|
278
|
+
value === "exported" ||
|
|
279
|
+
value === "failed") {
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`Unsupported clip status: ${value}`);
|
|
283
|
+
}
|
|
284
|
+
function formatClipListLine({ clip }) {
|
|
285
|
+
const clipId = String(clip.clip_id ?? "");
|
|
286
|
+
const status = String(clip.status ?? "");
|
|
287
|
+
const nextAction = String(clip.next_action ?? "");
|
|
288
|
+
const projectId = clip.project_id === null ? "-" : String(clip.project_id);
|
|
289
|
+
const exportRecord = clip.export !== null &&
|
|
290
|
+
typeof clip.export === "object" &&
|
|
291
|
+
!Array.isArray(clip.export)
|
|
292
|
+
? clip.export
|
|
293
|
+
: {};
|
|
294
|
+
const exportId = exportRecord.export_id === null || exportRecord.export_id === undefined
|
|
295
|
+
? "-"
|
|
296
|
+
: String(exportRecord.export_id);
|
|
297
|
+
return `${clipId} ${status} next=${nextAction} project=${projectId} export=${exportId}`;
|
|
298
|
+
}
|
|
141
299
|
function buildClipsDryRun({ context, spec, helpers, }) {
|
|
142
300
|
const action = context.args.command[1];
|
|
143
301
|
const flags = context.args.flags;
|
|
302
|
+
if (action === "list") {
|
|
303
|
+
return helpers.dryRunRequest({
|
|
304
|
+
spec,
|
|
305
|
+
method: "GET",
|
|
306
|
+
path: "/clips",
|
|
307
|
+
query: {
|
|
308
|
+
asset_id: helpers.getStringFlag({ flags, name: "asset-id" }),
|
|
309
|
+
project_id: helpers.getStringFlag({ flags, name: "project-id" }),
|
|
310
|
+
status: helpers.getStringFlag({ flags, name: "status" }),
|
|
311
|
+
limit: helpers.getNumberFlag({ flags, name: "limit" }),
|
|
312
|
+
offset: helpers.getNumberFlag({ flags, name: "offset" }),
|
|
313
|
+
},
|
|
314
|
+
body: undefined,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
144
317
|
if (action === "create") {
|
|
145
|
-
const options =
|
|
318
|
+
const options = parseClipWorkflowOptions({ flags, helpers });
|
|
146
319
|
return helpers.dryRunRequest({
|
|
147
320
|
spec,
|
|
148
321
|
method: "POST",
|
|
149
322
|
path: "/clips",
|
|
150
323
|
body: {
|
|
151
|
-
project_name: options.projectName,
|
|
152
324
|
source: {
|
|
153
325
|
source_type: options.source.sourceType,
|
|
154
326
|
asset_id: options.source.assetId,
|
|
155
327
|
youtube_url: options.source.youtubeUrl,
|
|
328
|
+
quality: null,
|
|
156
329
|
},
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
163
|
-
layout: {
|
|
164
|
-
layout_mode: options.layout.mode,
|
|
165
|
-
content_type_hint: options.layout.contentTypeHint,
|
|
166
|
-
},
|
|
167
|
-
captions: {
|
|
168
|
-
enabled: options.captions.enabled,
|
|
169
|
-
style_id: options.captions.styleId,
|
|
170
|
-
},
|
|
171
|
-
qa: {
|
|
172
|
-
qa_mode: options.qa.mode,
|
|
173
|
-
},
|
|
174
|
-
export: {
|
|
175
|
-
auto_export: options.export.autoExport,
|
|
176
|
-
},
|
|
330
|
+
clip_preset_id: options.clipPresetId,
|
|
331
|
+
caption_theme_id: options.captionThemeId,
|
|
332
|
+
create_flow_options: {},
|
|
333
|
+
branding_options: buildClipWorkflowBrandingOptions({
|
|
334
|
+
logoAssetId: options.logoAssetId,
|
|
335
|
+
}),
|
|
177
336
|
},
|
|
337
|
+
followUpRequests: options.outputDir === null
|
|
338
|
+
? []
|
|
339
|
+
: [
|
|
340
|
+
{
|
|
341
|
+
reason: "Poll the batch until rendered clips have download URLs.",
|
|
342
|
+
method: "GET",
|
|
343
|
+
path: "/clip-batches/{batch_id}",
|
|
344
|
+
body: undefined,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
notes: [
|
|
348
|
+
options.outputDir === null
|
|
349
|
+
? "Queues clips through the web app batch pipeline."
|
|
350
|
+
: `Downloads up to ${options.count} rendered clips from the web app batch pipeline.`,
|
|
351
|
+
"Caption themes, AI edit, exports, and batch status follow /dashboard/clip defaults.",
|
|
352
|
+
],
|
|
178
353
|
});
|
|
179
354
|
}
|
|
180
355
|
if (action === "reselect") {
|
|
@@ -183,31 +358,56 @@ function buildClipsDryRun({ context, spec, helpers, }) {
|
|
|
183
358
|
requireMaxDuration: false,
|
|
184
359
|
});
|
|
185
360
|
const selection = helpers.buildClipSelection({ flags });
|
|
361
|
+
const selectionMode = helpers.getStringFlag({
|
|
362
|
+
flags,
|
|
363
|
+
name: "selection-mode",
|
|
364
|
+
});
|
|
186
365
|
return helpers.dryRunRequest({
|
|
187
366
|
spec,
|
|
188
367
|
method: "POST",
|
|
189
368
|
path: `/clips/${encodeURIComponent(helpers.requireStringFlag({ flags, name: "clip-id" }))}/reselect`,
|
|
190
|
-
body:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
369
|
+
body: selectionMode === undefined
|
|
370
|
+
? {
|
|
371
|
+
...(selection.suggestionId === null
|
|
372
|
+
? {}
|
|
373
|
+
: { suggestion_id: selection.suggestionId }),
|
|
374
|
+
...(selection.startSeconds === null
|
|
375
|
+
? {}
|
|
376
|
+
: { start_seconds: selection.startSeconds }),
|
|
377
|
+
...(selection.endSeconds === null
|
|
378
|
+
? {}
|
|
379
|
+
: { end_seconds: selection.endSeconds }),
|
|
380
|
+
...(target === null
|
|
381
|
+
? {}
|
|
382
|
+
: {
|
|
383
|
+
target: {
|
|
384
|
+
min_duration_seconds: target.minDurationSeconds,
|
|
385
|
+
max_duration_seconds: target.maxDurationSeconds,
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
}
|
|
389
|
+
: {
|
|
390
|
+
selection: toApiClipSelection({ selection }),
|
|
391
|
+
target: target === null
|
|
392
|
+
? null
|
|
393
|
+
: {
|
|
394
|
+
min_duration_seconds: target.minDurationSeconds,
|
|
395
|
+
max_duration_seconds: target.maxDurationSeconds,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
199
398
|
});
|
|
200
399
|
}
|
|
201
400
|
if (action === "repair") {
|
|
401
|
+
const repairMode = helpers.getStringFlag({ flags, name: "repair-mode" });
|
|
202
402
|
return helpers.dryRunRequest({
|
|
203
403
|
spec,
|
|
204
404
|
method: "POST",
|
|
205
405
|
path: `/clips/${encodeURIComponent(helpers.requireStringFlag({ flags, name: "clip-id" }))}/repair`,
|
|
206
|
-
body:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
406
|
+
body: repairMode === undefined
|
|
407
|
+
? {}
|
|
408
|
+
: {
|
|
409
|
+
repair_mode: helpers.parseClipRepairMode({ value: repairMode }),
|
|
410
|
+
},
|
|
211
411
|
});
|
|
212
412
|
}
|
|
213
413
|
if (action === "export") {
|
|
@@ -222,15 +422,198 @@ function buildClipsDryRun({ context, spec, helpers, }) {
|
|
|
222
422
|
format: helpers.parseClipExportFormat({
|
|
223
423
|
value: helpers.getStringFlag({ flags, name: "format" }),
|
|
224
424
|
}),
|
|
225
|
-
allow_blocking_qa_bypass:
|
|
226
|
-
flags,
|
|
227
|
-
name: "allow-blocking-qa-bypass",
|
|
228
|
-
}) ?? false,
|
|
425
|
+
allow_blocking_qa_bypass: false,
|
|
229
426
|
},
|
|
230
427
|
});
|
|
231
428
|
}
|
|
232
429
|
return undefined;
|
|
233
430
|
}
|
|
431
|
+
function parseClipWorkflowOptions({ flags, helpers, }) {
|
|
432
|
+
const count = helpers.getNumberFlag({ flags, name: "count" }) ?? 5;
|
|
433
|
+
if (!Number.isInteger(count) || count < 1 || count > 10) {
|
|
434
|
+
throw new Error("--count must be an integer between 1 and 10.");
|
|
435
|
+
}
|
|
436
|
+
const assetId = helpers.getStringFlag({ flags, name: "asset-id" }) ?? null;
|
|
437
|
+
const youtubeUrl = helpers.getStringFlag({ flags, name: "youtube-url" }) ?? null;
|
|
438
|
+
if (assetId === null && youtubeUrl === null) {
|
|
439
|
+
throw new Error("Provide --asset-id or --youtube-url.");
|
|
440
|
+
}
|
|
441
|
+
if (assetId !== null && youtubeUrl !== null) {
|
|
442
|
+
throw new Error("Use only one of --asset-id or --youtube-url.");
|
|
443
|
+
}
|
|
444
|
+
if (helpers.getOptionalBooleanFlag({ flags, name: "no-captions" }) === true) {
|
|
445
|
+
throw new Error("--no-captions is not supported. Clip batches use captions like /dashboard/clip.");
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
source: assetId !== null
|
|
449
|
+
? {
|
|
450
|
+
sourceType: "asset",
|
|
451
|
+
assetId,
|
|
452
|
+
youtubeUrl: null,
|
|
453
|
+
}
|
|
454
|
+
: {
|
|
455
|
+
sourceType: "youtube",
|
|
456
|
+
assetId: null,
|
|
457
|
+
youtubeUrl: youtubeUrl ?? "",
|
|
458
|
+
},
|
|
459
|
+
logoAssetId: helpers.getStringFlag({ flags, name: "logo-asset-id" }) ?? null,
|
|
460
|
+
outputDir: helpers.getStringFlag({ flags, name: "output" }) ?? null,
|
|
461
|
+
count,
|
|
462
|
+
clipPresetId: normalizeClipPresetId({
|
|
463
|
+
value: helpers.getStringFlag({ flags, name: "clip-preset-id" }) ??
|
|
464
|
+
helpers.getStringFlag({ flags, name: "preset" }) ??
|
|
465
|
+
"default",
|
|
466
|
+
}),
|
|
467
|
+
captionThemeId: parseNullableWorkflowString({
|
|
468
|
+
value: helpers.getStringFlag({ flags, name: "caption-theme-id" }),
|
|
469
|
+
}),
|
|
470
|
+
idempotencyKey: helpers.getStringFlag({ flags, name: "idempotency-key" }) ?? null,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function normalizeClipPresetId({ value }) {
|
|
474
|
+
if (value === "high-retention") {
|
|
475
|
+
return "formation-clipperz";
|
|
476
|
+
}
|
|
477
|
+
if (value === "default" ||
|
|
478
|
+
value === "formation-clipperz" ||
|
|
479
|
+
value === "demo-focus") {
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
throw new Error("--clip-preset-id must be default, formation-clipperz, high-retention, or demo-focus.");
|
|
483
|
+
}
|
|
484
|
+
function parseNullableWorkflowString({ value, }) {
|
|
485
|
+
if (value === undefined || value === "null") {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
const trimmed = value.trim();
|
|
489
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
490
|
+
}
|
|
491
|
+
async function createClipWorkflow({ context, helpers, }) {
|
|
492
|
+
const options = parseClipWorkflowOptions({
|
|
493
|
+
flags: context.args.flags,
|
|
494
|
+
helpers,
|
|
495
|
+
});
|
|
496
|
+
const created = await context.client.clips.create({
|
|
497
|
+
source: {
|
|
498
|
+
sourceType: options.source.sourceType,
|
|
499
|
+
assetId: options.source.assetId,
|
|
500
|
+
youtubeUrl: options.source.youtubeUrl,
|
|
501
|
+
quality: null,
|
|
502
|
+
},
|
|
503
|
+
clipPresetId: options.clipPresetId,
|
|
504
|
+
captionThemeId: options.captionThemeId,
|
|
505
|
+
createFlowOptions: {},
|
|
506
|
+
brandingOptions: buildClipWorkflowBrandingOptions({
|
|
507
|
+
logoAssetId: options.logoAssetId,
|
|
508
|
+
}),
|
|
509
|
+
idempotencyKey: options.idempotencyKey ?? undefined,
|
|
510
|
+
});
|
|
511
|
+
if (options.outputDir === null) {
|
|
512
|
+
return {
|
|
513
|
+
operation: "clips.create",
|
|
514
|
+
batch_id: created.batch.batch_id,
|
|
515
|
+
status: created.batch.status,
|
|
516
|
+
preset: options.clipPresetId,
|
|
517
|
+
caption_theme_id: options.captionThemeId,
|
|
518
|
+
output_dir: null,
|
|
519
|
+
summary: {
|
|
520
|
+
requested: options.count,
|
|
521
|
+
completed: created.batch.rendered_count,
|
|
522
|
+
},
|
|
523
|
+
clips: [],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const outputDir = resolve(options.outputDir);
|
|
527
|
+
await mkdir(outputDir, { recursive: true });
|
|
528
|
+
const readyBatch = await waitForClipBatchEnvelope({
|
|
529
|
+
context,
|
|
530
|
+
helpers,
|
|
531
|
+
batchId: created.batch.batch_id,
|
|
532
|
+
});
|
|
533
|
+
const renderedClips = readyBatch.batch.clips
|
|
534
|
+
.filter((clip) => clip.status === "rendered" && clip.download_url !== null)
|
|
535
|
+
.slice(0, options.count);
|
|
536
|
+
if (renderedClips.length === 0) {
|
|
537
|
+
throw new Error("Clip batch completed without rendered clips to download.");
|
|
538
|
+
}
|
|
539
|
+
const receipts = [];
|
|
540
|
+
for (let index = 0; index < renderedClips.length; index++) {
|
|
541
|
+
const clip = renderedClips[index];
|
|
542
|
+
if (clip === undefined || clip.download_url === null) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const bytes = await downloadSignedClip({
|
|
546
|
+
url: clip.download_url,
|
|
547
|
+
});
|
|
548
|
+
const outputPath = join(outputDir, `${String(index + 1).padStart(2, "0")}-${slugifyFileName({
|
|
549
|
+
value: clip.title,
|
|
550
|
+
})}.mp4`);
|
|
551
|
+
await writeFile(outputPath, Buffer.from(bytes));
|
|
552
|
+
receipts.push({
|
|
553
|
+
clip_id: clip.clip_id,
|
|
554
|
+
project_id: clip.project_id,
|
|
555
|
+
suggestion_id: clip.suggestion_id,
|
|
556
|
+
export_id: clip.export_id,
|
|
557
|
+
output_path: outputPath,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
operation: "clips.create",
|
|
562
|
+
batch_id: readyBatch.batch.batch_id,
|
|
563
|
+
status: readyBatch.batch.status,
|
|
564
|
+
preset: options.clipPresetId,
|
|
565
|
+
caption_theme_id: options.captionThemeId,
|
|
566
|
+
output_dir: outputDir,
|
|
567
|
+
summary: {
|
|
568
|
+
requested: options.count,
|
|
569
|
+
completed: receipts.length,
|
|
570
|
+
},
|
|
571
|
+
clips: receipts,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async function waitForClipBatchEnvelope({ context, helpers, batchId, }) {
|
|
575
|
+
const timeoutMs = helpers.getNumberFlag({ flags: context.args.flags, name: "timeout-ms" }) ??
|
|
576
|
+
1_800_000;
|
|
577
|
+
const intervalMs = helpers.getNumberFlag({ flags: context.args.flags, name: "interval-ms" }) ??
|
|
578
|
+
5_000;
|
|
579
|
+
const startedAt = Date.now();
|
|
580
|
+
while (true) {
|
|
581
|
+
const batch = await context.client.clipBatches.get({ batchId });
|
|
582
|
+
if (batch.batch.status === "completed" ||
|
|
583
|
+
batch.batch.status === "completed_with_errors" ||
|
|
584
|
+
batch.batch.status === "failed") {
|
|
585
|
+
return batch;
|
|
586
|
+
}
|
|
587
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
588
|
+
throw new Error(`Timed out waiting for clip batch ${batchId}.`);
|
|
589
|
+
}
|
|
590
|
+
await new Promise((resolvePromise) => {
|
|
591
|
+
setTimeout(resolvePromise, intervalMs);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function buildClipWorkflowBrandingOptions({ logoAssetId, }) {
|
|
596
|
+
return {
|
|
597
|
+
logoOverlayEnabled: logoAssetId !== null,
|
|
598
|
+
logoMediaAssetId: logoAssetId,
|
|
599
|
+
logoPosition: "top-right",
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function downloadSignedClip({ url, }) {
|
|
603
|
+
const response = await fetch(url);
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
throw new Error(`Download failed with ${response.status}.`);
|
|
606
|
+
}
|
|
607
|
+
return response.arrayBuffer();
|
|
608
|
+
}
|
|
609
|
+
function slugifyFileName({ value }) {
|
|
610
|
+
const slug = value
|
|
611
|
+
.toLowerCase()
|
|
612
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
613
|
+
.replace(/^-+|-+$/g, "")
|
|
614
|
+
.slice(0, 56);
|
|
615
|
+
return slug.length > 0 ? slug : "clip";
|
|
616
|
+
}
|
|
234
617
|
function toApiClipSelection({ selection, }) {
|
|
235
618
|
return {
|
|
236
619
|
selection_mode: selection.mode,
|