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.
Files changed (129) hide show
  1. package/README.md +41 -8
  2. package/dist/agent-context.d.ts +26 -1
  3. package/dist/agent-context.d.ts.map +1 -1
  4. package/dist/agent-context.js +45 -1
  5. package/dist/agent-context.js.map +1 -1
  6. package/dist/cli-command-routing.d.ts.map +1 -1
  7. package/dist/cli-command-routing.js +18 -3
  8. package/dist/cli-command-routing.js.map +1 -1
  9. package/dist/cli-errors.d.ts +3 -1
  10. package/dist/cli-errors.d.ts.map +1 -1
  11. package/dist/cli-errors.js +3 -1
  12. package/dist/cli-errors.js.map +1 -1
  13. package/dist/cli-presenter.d.ts +2 -1
  14. package/dist/cli-presenter.d.ts.map +1 -1
  15. package/dist/cli-presenter.js +75 -31
  16. package/dist/cli-presenter.js.map +1 -1
  17. package/dist/cli-runtime.d.ts +1 -2
  18. package/dist/cli-runtime.d.ts.map +1 -1
  19. package/dist/cli-runtime.js +20 -2
  20. package/dist/cli-runtime.js.map +1 -1
  21. package/dist/cli-types.d.ts +2 -1
  22. package/dist/cli-types.d.ts.map +1 -1
  23. package/dist/cli-value-parsers.d.ts +4 -4
  24. package/dist/cli-value-parsers.d.ts.map +1 -1
  25. package/dist/cli-value-parsers.js +11 -6
  26. package/dist/cli-value-parsers.js.map +1 -1
  27. package/dist/command-metadata.d.ts +9 -3
  28. package/dist/command-metadata.d.ts.map +1 -1
  29. package/dist/command-metadata.js +33 -4
  30. package/dist/command-metadata.js.map +1 -1
  31. package/dist/command-registry.d.ts.map +1 -1
  32. package/dist/command-registry.js +10 -0
  33. package/dist/command-registry.js.map +1 -1
  34. package/dist/commands/ai-edit.js +7 -5
  35. package/dist/commands/ai-edit.js.map +1 -1
  36. package/dist/commands/audio.d.ts +3 -3
  37. package/dist/commands/audio.d.ts.map +1 -1
  38. package/dist/commands/audio.js +5 -2
  39. package/dist/commands/audio.js.map +1 -1
  40. package/dist/commands/caption-dry-run.js +20 -0
  41. package/dist/commands/caption-dry-run.js.map +1 -1
  42. package/dist/commands/caption-selection.d.ts +2 -1
  43. package/dist/commands/caption-selection.d.ts.map +1 -1
  44. package/dist/commands/caption-style-flags.d.ts.map +1 -1
  45. package/dist/commands/caption-style-flags.js +58 -0
  46. package/dist/commands/caption-style-flags.js.map +1 -1
  47. package/dist/commands/captions-specs.d.ts.map +1 -1
  48. package/dist/commands/captions-specs.js +23 -1
  49. package/dist/commands/captions-specs.js.map +1 -1
  50. package/dist/commands/captions.js +23 -1
  51. package/dist/commands/captions.js.map +1 -1
  52. package/dist/commands/clip-runtime.d.ts +2 -5
  53. package/dist/commands/clip-runtime.d.ts.map +1 -1
  54. package/dist/commands/clip-runtime.js +36 -111
  55. package/dist/commands/clip-runtime.js.map +1 -1
  56. package/dist/commands/clips.d.ts +12 -5
  57. package/dist/commands/clips.d.ts.map +1 -1
  58. package/dist/commands/clips.js +466 -83
  59. package/dist/commands/clips.js.map +1 -1
  60. package/dist/commands/effects.d.ts.map +1 -1
  61. package/dist/commands/effects.js +6 -0
  62. package/dist/commands/effects.js.map +1 -1
  63. package/dist/commands/exports.d.ts +1 -8
  64. package/dist/commands/exports.d.ts.map +1 -1
  65. package/dist/commands/exports.js +69 -7
  66. package/dist/commands/exports.js.map +1 -1
  67. package/dist/commands/faceless.d.ts +44 -0
  68. package/dist/commands/faceless.d.ts.map +1 -0
  69. package/dist/commands/faceless.js +587 -0
  70. package/dist/commands/faceless.js.map +1 -0
  71. package/dist/commands/formats.d.ts +41 -0
  72. package/dist/commands/formats.d.ts.map +1 -0
  73. package/dist/commands/formats.js +140 -0
  74. package/dist/commands/formats.js.map +1 -0
  75. package/dist/commands/generation.d.ts +25 -0
  76. package/dist/commands/generation.d.ts.map +1 -0
  77. package/dist/commands/generation.js +494 -0
  78. package/dist/commands/generation.js.map +1 -0
  79. package/dist/commands/jobs.d.ts +20 -0
  80. package/dist/commands/jobs.d.ts.map +1 -0
  81. package/dist/commands/jobs.js +40 -0
  82. package/dist/commands/jobs.js.map +1 -0
  83. package/dist/commands/media-assets.d.ts.map +1 -1
  84. package/dist/commands/media-assets.js +81 -9
  85. package/dist/commands/media-assets.js.map +1 -1
  86. package/dist/commands/media-overlays.d.ts.map +1 -1
  87. package/dist/commands/media-overlays.js +50 -6
  88. package/dist/commands/media-overlays.js.map +1 -1
  89. package/dist/commands/media-runtime.js +19 -1
  90. package/dist/commands/media-runtime.js.map +1 -1
  91. package/dist/commands/onboarding.d.ts +51 -0
  92. package/dist/commands/onboarding.d.ts.map +1 -0
  93. package/dist/commands/onboarding.js +452 -0
  94. package/dist/commands/onboarding.js.map +1 -0
  95. package/dist/commands/project-read.d.ts.map +1 -1
  96. package/dist/commands/project-read.js +32 -3
  97. package/dist/commands/project-read.js.map +1 -1
  98. package/dist/commands/projects.d.ts +1 -6
  99. package/dist/commands/projects.d.ts.map +1 -1
  100. package/dist/commands/projects.js +24 -4
  101. package/dist/commands/projects.js.map +1 -1
  102. package/dist/commands/system.d.ts.map +1 -1
  103. package/dist/commands/system.js +56 -14
  104. package/dist/commands/system.js.map +1 -1
  105. package/dist/commands/text-overlay-spec.d.ts +6 -0
  106. package/dist/commands/text-overlay-spec.d.ts.map +1 -1
  107. package/dist/commands/text-overlay-spec.js +59 -0
  108. package/dist/commands/text-overlay-spec.js.map +1 -1
  109. package/dist/commands/text-overlays.d.ts +6 -0
  110. package/dist/commands/text-overlays.d.ts.map +1 -1
  111. package/dist/commands/text-overlays.js +74 -1
  112. package/dist/commands/text-overlays.js.map +1 -1
  113. package/dist/commands/timeline-runtime.d.ts +3 -0
  114. package/dist/commands/timeline-runtime.d.ts.map +1 -1
  115. package/dist/commands/timeline-runtime.js +102 -9
  116. package/dist/commands/timeline-runtime.js.map +1 -1
  117. package/dist/commands/timeline.d.ts +4 -0
  118. package/dist/commands/timeline.d.ts.map +1 -1
  119. package/dist/commands/timeline.js +119 -47
  120. package/dist/commands/timeline.js.map +1 -1
  121. package/dist/index.js +59 -3
  122. package/dist/index.js.map +1 -1
  123. package/dist/preview-first-edit.d.ts +3 -1
  124. package/dist/preview-first-edit.d.ts.map +1 -1
  125. package/dist/preview-first-edit.js +2 -2
  126. package/dist/preview-first-edit.js.map +1 -1
  127. package/dist/update-notifier.d.ts +2 -1
  128. package/dist/update-notifier.d.ts.map +1 -1
  129. package/package.json +2 -2
@@ -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", "create"],
5
- usage: "blitzreels clips create --source-type asset|youtube [--asset-id ID|--youtube-url URL] [--name NAME] [--selection-mode auto_best|suggestion|time_range] [--suggestion-id ID] [--start-seconds 52] [--end-seconds 64] [--min-duration 8] --max-duration 12 [--layout-mode auto|prefer_split|prefer_focus] [--content-type-hint auto|podcast|tutorial|generic] [--captions true] [--caption-style-id ID|null] [--qa-mode required|permissive] [--auto-export false]",
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
- "name",
11
- "selection-mode",
12
- "suggestion-id",
13
- "start-seconds",
14
- "end-seconds",
15
- "min-duration",
16
- "max-duration",
17
- "layout-mode",
18
- "content-type-hint",
19
- "captions",
20
- "caption-style-id",
21
- "qa-mode",
22
- "auto-export",
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 a managed clip job from a workspace asset or YouTube URL.",
71
+ description: "Create clips from a workspace asset or YouTube URL; with --output it exports 1080p MP4 files.",
25
72
  examples: [
26
- 'blitzreels clips create --source-type youtube --youtube-url https://youtube.com/watch?v=... --name "MKBHD hero loop" --min-duration 8 --max-duration 12 --qa-mode permissive',
27
- "blitzreels clips create --source-type asset --asset-id ID --selection-mode time_range --start-seconds 52 --end-seconds 64 --max-duration 12",
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 --selection-mode auto_best|suggestion|time_range [--suggestion-id ID] [--start-seconds 52] [--end-seconds 64] [--min-duration 8] [--max-duration 12]",
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
- "min-duration",
52
- "max-duration",
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 [--repair-mode auto|prefer_split|prefer_focus|prefer_tutorial|move_captions_off_faces]",
60
- flags: ["clip-id", "repair-mode"],
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 [--resolution 1080p] [--format mp4] [--allow-blocking-qa-bypass false]",
67
- flags: ["clip-id", "resolution", "format", "allow-blocking-qa-bypass"],
68
- description: "Export a managed clip, optionally accepting QA warnings.",
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 context.client.clips.create(helpers.toCreateClipOptions({ flags }));
85
- helpers.printClipResult({ context, result });
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: helpers.requireStringFlag({ flags, name: "clip-id" }),
103
- selection: helpers.buildClipSelection({ flags }),
104
- target: helpers.buildClipTarget({
105
- flags,
106
- requireMaxDuration: false,
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: helpers.parseClipRepairMode({
116
- value: helpers.getStringFlag({ flags, name: "repair-mode" }),
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: helpers.getOptionalBooleanFlag({
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 = helpers.toCreateClipOptions({ flags });
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
- selection: toApiClipSelection({ selection: options.selection }),
158
- target: {
159
- aspect_ratio: options.target.aspectRatio,
160
- min_duration_seconds: options.target.minDurationSeconds,
161
- max_duration_seconds: options.target.maxDurationSeconds,
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
- selection: toApiClipSelection({ selection }),
192
- target: target === null
193
- ? null
194
- : {
195
- min_duration_seconds: target.minDurationSeconds,
196
- max_duration_seconds: target.maxDurationSeconds,
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
- repair_mode: helpers.parseClipRepairMode({
208
- value: helpers.getStringFlag({ flags, name: "repair-mode" }),
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: helpers.getOptionalBooleanFlag({
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,