@aicopycoders/exodus 2026.9.2000-beta.2 → 2026.9.2000-beta.20
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/assets/docs/PIPELINES.md +2 -1
- package/assets/skills/exodus-meme/SKILL.md +4 -3
- package/assets/skills/exodus-template/SKILL.md +2 -1
- package/assets/skills/exodus-video/SKILL.md +165 -27
- package/assets/skills/exodus-video/scripts/first-cut.mjs +538 -38
- package/assets/skills/exodus-workflow/SKILL.md +1 -1
- package/dist/commands/ads.js +1 -1
- package/dist/commands/image.js +1 -1
- package/dist/commands/template.js +11 -4
- package/dist/commands/video.d.ts +126 -7
- package/dist/commands/video.js +686 -58
- package/dist/commands/workflow.d.ts +52 -4
- package/dist/commands/workflow.js +60 -8
- package/dist/lib/helperLedgerBase.d.ts +3 -0
- package/dist/lib/helperLedgerBase.js +3 -0
- package/dist/lib/meta-ads.d.ts +1 -0
- package/package.json +1 -1
package/dist/commands/video.js
CHANGED
|
@@ -6,11 +6,30 @@ import { displayRunStatus, formatApiError } from "../lib/format.js";
|
|
|
6
6
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
7
7
|
import { hasBinary } from "../lib/preflight.js";
|
|
8
8
|
import { ASSET_UPLOAD_POLICY, pauseAheadLine, } from "./workflow.js";
|
|
9
|
+
import { HELPER_LEDGER_BASE, SET_OPTION_LEDGER_BASE } from "../lib/helperLedgerBase.js";
|
|
10
|
+
const CONCEIT_KEYS = ["podcast", "ugc", "stage", "street", "personification"];
|
|
11
|
+
const VOICE_MODES = ["native", "voice-first"];
|
|
12
|
+
const STYLE_SLUGS = [
|
|
13
|
+
"pixar-3d",
|
|
14
|
+
"claymation-stop-motion",
|
|
15
|
+
"lego-toy-brick",
|
|
16
|
+
"balloon",
|
|
17
|
+
"video-game-8bit",
|
|
18
|
+
];
|
|
19
|
+
function joinedOr(items) {
|
|
20
|
+
if (items.length <= 1)
|
|
21
|
+
return items[0] ?? "";
|
|
22
|
+
return `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
|
|
23
|
+
}
|
|
24
|
+
function cantSayNames(names) {
|
|
25
|
+
return `the recorded voice can't say ${joinedOr(names.map((n) => `"${n}"`))}`;
|
|
26
|
+
}
|
|
9
27
|
export const helpText = `
|
|
10
28
|
exodus video — make a video ad from a saved workflow, pull every piece, upload your cut
|
|
11
29
|
|
|
12
30
|
The dashboard makes the PIECES of an ad: the storyboard, one picture per scene,
|
|
13
|
-
one voice track per scene, one video clip per scene,
|
|
31
|
+
one voice track per scene, one video clip per scene, and a music bed if you
|
|
32
|
+
asked for one with --music. It does
|
|
14
33
|
not make the finished ad. You pull the pieces to a folder, cut them together
|
|
15
34
|
with whatever editing tools you like, and upload the cut back.
|
|
16
35
|
|
|
@@ -41,14 +60,22 @@ The whole loop, in order:
|
|
|
41
60
|
Redo ONE still at the pixel gate. Neighbours stay. The gate holds.
|
|
42
61
|
|
|
43
62
|
7. exodus video status <runId>
|
|
44
|
-
Where the run is and how each scene's clip turned out.
|
|
63
|
+
Where the run is and how each scene's clip turned out. If the storyboard
|
|
64
|
+
failed, it also says which parts of it were accepted and why the next one
|
|
65
|
+
was turned down.
|
|
66
|
+
|
|
67
|
+
exodus video status <runId> --rejected-draft
|
|
68
|
+
Prints the storyboard draft that was turned down. A record of what
|
|
69
|
+
happened, thrown away by the system — never something to act on.
|
|
45
70
|
|
|
46
71
|
8. exodus video pull <runId> --out ./ad
|
|
47
72
|
Writes every piece to that folder plus a manifest.json index.
|
|
48
73
|
|
|
49
74
|
exodus video retry-clip <runId> --scene <n>
|
|
50
75
|
Redo ONE finished clip while the run waits for the cut. Everything
|
|
51
|
-
else stays. A --note steers the motion, never the words.
|
|
76
|
+
else stays. A --note steers the motion, never the words. Add
|
|
77
|
+
--voice-first when the words came out wrong: the line is recorded in
|
|
78
|
+
the character's own voice first, then the clip is made to match it.
|
|
52
79
|
|
|
53
80
|
exodus video revoice <runId> --all
|
|
54
81
|
Redo only the VOICE on clips that kept the video model's own voice
|
|
@@ -62,18 +89,21 @@ The whole loop, in order:
|
|
|
62
89
|
Attaches your cut to the run and prints the page to approve it on.
|
|
63
90
|
|
|
64
91
|
11. exodus video approve <runId>
|
|
65
|
-
Or click Approve on the page from step 10.
|
|
92
|
+
Or click Approve on the page from step 10. If you redid a clip after
|
|
93
|
+
uploading, this stops and says which scenes changed. Upload a fresh cut,
|
|
94
|
+
or add --approve-stale-cut to deliver the cut you already uploaded.
|
|
66
95
|
|
|
67
96
|
Usage:
|
|
68
|
-
exodus video status <runId> [--json]
|
|
97
|
+
exodus video status <runId> [--rejected-draft] [--json]
|
|
69
98
|
exodus video storyboard <runId> [--json]
|
|
70
|
-
exodus video approve <runId> [--json]
|
|
99
|
+
exodus video approve <runId> [--approve-stale-cut] [--json]
|
|
71
100
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
72
|
-
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--json]
|
|
101
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--voice-first] [--json]
|
|
73
102
|
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
|
|
74
103
|
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
75
104
|
exodus video pull <runId> --out <dir> [--json]
|
|
76
105
|
exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
|
|
106
|
+
exodus video start --script <file> --conceit <${CONCEIT_KEYS.join("|")}> --style <${STYLE_SLUGS.join("|")}> [--direction "<note>"] [--voice-path <path>] [--video-model <id>] [--voice <native|voice-first>] [--music] [--wait] [--json]
|
|
77
107
|
|
|
78
108
|
Options:
|
|
79
109
|
--out <dir> Folder to write the pulled pieces into (pull)
|
|
@@ -88,12 +118,33 @@ Options:
|
|
|
88
118
|
(retry-frame, retry-clip, revoice)
|
|
89
119
|
--all Every finished clip that kept the video model's voice
|
|
90
120
|
(revoice)
|
|
121
|
+
--voice-first Record the scene's line in the character's own voice,
|
|
122
|
+
then make the clip to match it (retry-clip). For a clip
|
|
123
|
+
whose words came out wrong
|
|
124
|
+
--approve-stale-cut Deliver the cut you uploaded even though a clip changed
|
|
125
|
+
after you uploaded it (approve). Without it, approving
|
|
126
|
+
stops and names the scenes that changed
|
|
91
127
|
--set <who>=<id> Give a character an ElevenLabs voice (voices). Name the
|
|
92
128
|
character by its ID or by the name your script uses.
|
|
93
129
|
Repeat it once per character
|
|
94
130
|
--clear <who> Take a character's voice off again (voices). Repeatable
|
|
95
131
|
--from <file.json> A file of characters and voice IDs (voices). --set and
|
|
96
132
|
--clear win over the same name in the file
|
|
133
|
+
--rejected-draft Print the storyboard draft the system turned down
|
|
134
|
+
(status). It was thrown away — a record only
|
|
135
|
+
--script <file> Text file of what the ad says (start)
|
|
136
|
+
--conceit <key> Kind of ad: ${joinedOr(CONCEIT_KEYS)} (start)
|
|
137
|
+
--style <slug> Look: ${joinedOr(STYLE_SLUGS)} (start)
|
|
138
|
+
--direction "<note>" Note for the whole ad (start)
|
|
139
|
+
--voice-path <path> How the voices are made (start)
|
|
140
|
+
--video-model <id> Which video model films this ad, instead of the kind of
|
|
141
|
+
ad's own default (start, with --conceit)
|
|
142
|
+
--voice <mode> native: the video model speaks the lines. voice-first:
|
|
143
|
+
the lines are recorded first (start, with --conceit)
|
|
144
|
+
--music Put a music bed under the ad (start). An ad has no music
|
|
145
|
+
unless you ask for it here
|
|
146
|
+
--no-music Leave the music bed off (start), which it already is
|
|
147
|
+
--wait Wait until the storyboard needs a yes (start)
|
|
97
148
|
--json Machine-readable output
|
|
98
149
|
--help, -h Print this help
|
|
99
150
|
|
|
@@ -108,6 +159,7 @@ Examples:
|
|
|
108
159
|
exodus video approve run_123
|
|
109
160
|
exodus video retry-frame run_123 --node frames-1 --scene 2
|
|
110
161
|
exodus video retry-clip run_123 --scene 3 --note "keep the handshake in frame"
|
|
162
|
+
exodus video retry-clip run_123 --scene 3 --voice-first
|
|
111
163
|
exodus video revoice run_123 --all
|
|
112
164
|
exodus video voices run_123
|
|
113
165
|
exodus video voices run_123 --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
@@ -117,13 +169,20 @@ Examples:
|
|
|
117
169
|
`.trim();
|
|
118
170
|
const SHOWS_PATH = "/api/v2/shows";
|
|
119
171
|
const RUNS_PATH = "/api/v2/video/runs";
|
|
172
|
+
const SCRIPT_RUNS_PATH = "/api/v2/video/script-runs";
|
|
120
173
|
const RUN_PATH = "/api/v2/workflow";
|
|
121
174
|
const ITEMS_PATH = "/api/v2/workflow/items";
|
|
175
|
+
const REJECTED_DRAFT_PATH = "/api/v2/workflow/rejected-draft";
|
|
176
|
+
const REJECTED_DRAFT_NOT_ON_THIS_SERVER = "This Exodus server does not keep rejected drafts yet, so there is nothing to read. It arrives " +
|
|
177
|
+
"with the next server update.";
|
|
122
178
|
const STORYBOARD_PATH = "/api/v2/video/storyboard";
|
|
123
179
|
const FLAG_PATH = "/api/v2/video/storyboard/flag";
|
|
124
180
|
const APPROVE_PATH = "/api/v2/workflow/approve";
|
|
125
181
|
const SCENE_RETRY_PATH = "/api/v2/workflow/scene/retry";
|
|
126
182
|
const SCENE_REVOICE_PATH = "/api/v2/workflow/scene/revoice";
|
|
183
|
+
const SCENE_VOICE_FIRST_PATH = "/api/v2/workflow/scene/voice-first";
|
|
184
|
+
const VOICE_FIRST_NOT_ON_THIS_SERVER = "This Exodus server cannot redo a clip voice first yet, so nothing was started and nothing " +
|
|
185
|
+
"was spent. It arrives with the next server update.";
|
|
127
186
|
const REVOICE_NOT_ON_THIS_SERVER = "This Exodus server does not have the voice redo yet, so nothing was started and nothing " +
|
|
128
187
|
"was spent. It arrives with the next server update.";
|
|
129
188
|
const FINAL_PATH = "/api/v2/video/final";
|
|
@@ -133,6 +192,51 @@ const ASSETS_PATH = "/api/v2/workflows/assets";
|
|
|
133
192
|
function withoutJudgeFields({ judgeDetail: _d, judgeSeverity: _s, ...rest }) {
|
|
134
193
|
return rest;
|
|
135
194
|
}
|
|
195
|
+
export function qcWithoutJudgeWording(qc) {
|
|
196
|
+
if (!qc.takes)
|
|
197
|
+
return qc;
|
|
198
|
+
return {
|
|
199
|
+
...qc,
|
|
200
|
+
takes: qc.takes.map(({ judgeWording: _w, heardText: _h, ...rest }) => rest),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export const NO_TAKE_HISTORY_LINE = "no history recorded for this clip";
|
|
204
|
+
const TAKE_NEIGHBOUR_SENTENCE = {
|
|
205
|
+
"not-applicable": () => "never compared",
|
|
206
|
+
"not-attached": () => "no neighbours attached",
|
|
207
|
+
"none-accepted": () => "no accepted neighbours yet",
|
|
208
|
+
attached: ({ scenes }) => `compared against scene${scenes.length === 1 ? "" : "s"} ${scenes.join(", ")}`,
|
|
209
|
+
};
|
|
210
|
+
export function renderQcTakeHistory(takes) {
|
|
211
|
+
if (!takes || takes.length === 0)
|
|
212
|
+
return [NO_TAKE_HISTORY_LINE];
|
|
213
|
+
const lines = [];
|
|
214
|
+
takes.forEach((take, index) => {
|
|
215
|
+
const say = TAKE_NEIGHBOUR_SENTENCE[take.neighbours.state];
|
|
216
|
+
const parts = [
|
|
217
|
+
take.failCodes.length ? `failed ${take.failCodes.join(", ")}` : "passed",
|
|
218
|
+
...(take.warnCodes.length ? [`warned ${take.warnCodes.join(", ")}`] : []),
|
|
219
|
+
say(take.neighbours),
|
|
220
|
+
];
|
|
221
|
+
const bonus = take.kind === "soft-retry" ? " (bonus take)" : "";
|
|
222
|
+
lines.push(`take ${index + 1}${bonus}: ${parts.join("; ")}`);
|
|
223
|
+
for (const { code, wording } of take.judgeWording ?? []) {
|
|
224
|
+
lines.push(` checker said (${code}): ${wording}`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
function redoLine(lastRedo) {
|
|
230
|
+
return lastRedo.reason ? `${lastRedo.label}: ${lastRedo.reason}` : lastRedo.label;
|
|
231
|
+
}
|
|
232
|
+
function printDisplacedHistory(lines, lastRedo) {
|
|
233
|
+
const history = lastRedo.displacedHistory;
|
|
234
|
+
if (!history)
|
|
235
|
+
return;
|
|
236
|
+
lines.push(` ${history.title}`);
|
|
237
|
+
for (const line of history.lines)
|
|
238
|
+
lines.push(` ${line}`);
|
|
239
|
+
}
|
|
136
240
|
export function asVideoRun(data) {
|
|
137
241
|
const run = (data ?? {});
|
|
138
242
|
return { ...run, nodes: Array.isArray(run.nodes) ? run.nodes : [] };
|
|
@@ -245,10 +349,20 @@ export function classifyRun(run) {
|
|
|
245
349
|
...(pausedNode.kind === "scene-frames"
|
|
246
350
|
? { framesNodeId: pausedNode.nodeId }
|
|
247
351
|
: {}),
|
|
352
|
+
...(isShowAd(run) ? { showAd: true } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (parkedAtFinalWatch(run)) {
|
|
356
|
+
const missingScenes = run.nodes
|
|
357
|
+
.filter((n) => n.kind === "video")
|
|
358
|
+
.flatMap((n) => n.missingScenes ?? []);
|
|
359
|
+
const framesNodeId = run.nodes.find((n) => n.kind === "scene-frames")?.nodeId;
|
|
360
|
+
return {
|
|
361
|
+
at: "final-watch",
|
|
362
|
+
...(missingScenes.length > 0 ? { missingScenes } : {}),
|
|
363
|
+
...(missingScenes.length > 0 && framesNodeId ? { framesNodeId } : {}),
|
|
248
364
|
};
|
|
249
365
|
}
|
|
250
|
-
if (parkedAtFinalWatch(run))
|
|
251
|
-
return { at: "final-watch" };
|
|
252
366
|
return { at: "paused", nodeId: run.pausedNodeId, reason: run.pauseReason };
|
|
253
367
|
}
|
|
254
368
|
const active = run.nodes.find((n) => n.status === "running");
|
|
@@ -260,7 +374,7 @@ export function hasAttachedCut(items) {
|
|
|
260
374
|
export function resolveStop(stop, cutAttached) {
|
|
261
375
|
if (stop.at !== "final-watch")
|
|
262
376
|
return stop;
|
|
263
|
-
return {
|
|
377
|
+
return { ...stop, cutAttached };
|
|
264
378
|
}
|
|
265
379
|
export async function resolveStopAtPark(stop, runId, deps) {
|
|
266
380
|
if (stop.at !== "final-watch")
|
|
@@ -295,27 +409,57 @@ export function stepName(kind) {
|
|
|
295
409
|
return "A step in this run";
|
|
296
410
|
return STEP_NAMES[kind] ?? `The ${kind} step`;
|
|
297
411
|
}
|
|
412
|
+
export function isShowAd(run) {
|
|
413
|
+
return run.moduleOwned === true;
|
|
414
|
+
}
|
|
298
415
|
export function reviewUrl(dashboardUrl, run) {
|
|
299
|
-
if (run.moduleOwned === true)
|
|
300
|
-
return `${dashboardUrl}/video?ad=${run._id}`;
|
|
301
416
|
if (run.workflowId)
|
|
302
417
|
return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
|
|
303
418
|
return `${dashboardUrl}/runs/${run._id}`;
|
|
304
419
|
}
|
|
420
|
+
function scenesPhrase(scenes) {
|
|
421
|
+
if (scenes.length === 1)
|
|
422
|
+
return `scene ${scenes[0]}`;
|
|
423
|
+
return `scenes ${scenes.slice(0, -1).join(", ")} and ${scenes[scenes.length - 1]}`;
|
|
424
|
+
}
|
|
425
|
+
function missingSceneLines(missing, framesNodeId, runId, runUrl) {
|
|
426
|
+
const scenes = missing.map((m) => m.sceneIndex);
|
|
427
|
+
const lines = [
|
|
428
|
+
`Parked: ${scenesPhrase(scenes)} ${scenes.length === 1 ? "has" : "have"} no clip yet, so the ad isn't complete.`,
|
|
429
|
+
];
|
|
430
|
+
for (const { sceneIndex, pictureRefused } of missing) {
|
|
431
|
+
if (!pictureRefused) {
|
|
432
|
+
lines.push(`Redo clip ${sceneIndex}: exodus video retry-clip ${runId} --scene ${sceneIndex}`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
lines.push(`Scene ${sceneIndex}'s picture was refused by the video service as too close to an existing character, so redraw it before redoing the clip.`);
|
|
436
|
+
lines.push(framesNodeId
|
|
437
|
+
? `Redraw frame ${sceneIndex}: exodus video retry-frame ${runId} --node ${framesNodeId} --scene ${sceneIndex} --note "<what to change>"`
|
|
438
|
+
: `Redraw frame ${sceneIndex}: on the run page, ${runUrl}`);
|
|
439
|
+
lines.push(`Then redo clip ${sceneIndex}: exodus video retry-clip ${runId} --scene ${sceneIndex}`);
|
|
440
|
+
}
|
|
441
|
+
lines.push("The ad can't be approved until every scene has a clip.", `Watch it here: ${runUrl}`);
|
|
442
|
+
return lines;
|
|
443
|
+
}
|
|
305
444
|
export function stopLines(stop, runId, runUrl) {
|
|
306
445
|
if (stop.at === "storyboard-gate") {
|
|
307
446
|
const lines = [
|
|
308
447
|
"Parked: the storyboard is waiting for your yes.",
|
|
309
448
|
`Read it: exodus video storyboard ${runId}`,
|
|
310
449
|
`Approve it: exodus video approve ${runId}`,
|
|
311
|
-
`Send back: exodus video flag ${runId} --note "what's wrong"`,
|
|
312
450
|
];
|
|
451
|
+
if (stop.showAd) {
|
|
452
|
+
lines.push(`Send back: exodus video flag ${runId} --note "what's wrong"`);
|
|
453
|
+
}
|
|
313
454
|
if (stop.framesNodeId) {
|
|
314
455
|
lines.push(`Redo one frame: exodus video retry-frame ${runId} --node ${stop.framesNodeId} --scene <n>`);
|
|
315
456
|
}
|
|
316
457
|
return lines;
|
|
317
458
|
}
|
|
318
459
|
if (stop.at === "final-watch") {
|
|
460
|
+
if (stop.missingScenes?.length) {
|
|
461
|
+
return missingSceneLines(stop.missingScenes, stop.framesNodeId, runId, runUrl);
|
|
462
|
+
}
|
|
319
463
|
if (stop.cutAttached === null) {
|
|
320
464
|
return [
|
|
321
465
|
"Parked: every piece is made.",
|
|
@@ -341,6 +485,13 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
341
485
|
}
|
|
342
486
|
if (stop.at === "failed") {
|
|
343
487
|
if (stop.repair) {
|
|
488
|
+
if (!stop.step) {
|
|
489
|
+
return [
|
|
490
|
+
"This run stopped at a step that can't be picked back up from here.",
|
|
491
|
+
"Start a fresh run to get this ad made.",
|
|
492
|
+
`Open the run: ${runUrl}`,
|
|
493
|
+
];
|
|
494
|
+
}
|
|
344
495
|
return [
|
|
345
496
|
`${stepName(stop.step)} failed${stop.error ? `: ${stop.error}` : "."}`,
|
|
346
497
|
`Try that step again: exodus workflow repair ${runId} retry`,
|
|
@@ -366,7 +517,40 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
366
517
|
}
|
|
367
518
|
return [`Working: ${stageWord(stop.stage)}.`];
|
|
368
519
|
}
|
|
369
|
-
export
|
|
520
|
+
export function failedStoryboardNode(run) {
|
|
521
|
+
return run.nodes.find((n) => n.kind === "storyboard" && n.status === "failed");
|
|
522
|
+
}
|
|
523
|
+
export function errorEchoesReasons(error, record) {
|
|
524
|
+
if (!error || record?.kind !== "rejected")
|
|
525
|
+
return false;
|
|
526
|
+
const reasons = record.reasons ?? [];
|
|
527
|
+
return reasons.length > 0 && reasons.every((reason) => error.includes(reason));
|
|
528
|
+
}
|
|
529
|
+
export function planFailureLines(node, runId) {
|
|
530
|
+
const record = node?.planFailure;
|
|
531
|
+
if (!record || !Array.isArray(record.lines) || !node)
|
|
532
|
+
return [];
|
|
533
|
+
const reasons = record.kind === "rejected" ? (record.reasons ?? []) : [];
|
|
534
|
+
const bodyCount = Math.max(0, record.lines.length - reasons.length);
|
|
535
|
+
const lines = [`What happened to the storyboard step (${node.nodeId}):`];
|
|
536
|
+
record.lines.forEach((line, i) => {
|
|
537
|
+
lines.push(i < bodyCount ? ` ${line}` : ` - ${line}`);
|
|
538
|
+
});
|
|
539
|
+
if (node?.hasRejectedDraft === true) {
|
|
540
|
+
lines.push(" A rejected draft is on file. The system threw it away, so it is a record of what happened and nothing to act on.", ` Read it: exodus video status ${runId} --rejected-draft`);
|
|
541
|
+
}
|
|
542
|
+
return lines;
|
|
543
|
+
}
|
|
544
|
+
export const MUSIC_HEARD_CODE = "music-heard";
|
|
545
|
+
export const CAST_LEDGER_BASE = HELPER_LEDGER_BASE + 10000;
|
|
546
|
+
export function musicBedState(run) {
|
|
547
|
+
if (run.musicBed === undefined)
|
|
548
|
+
return "unknown";
|
|
549
|
+
return run.musicBed ? "on" : "off";
|
|
550
|
+
}
|
|
551
|
+
function isPullSceneIndex(sceneIndex) {
|
|
552
|
+
return sceneIndex >= 0 && sceneIndex < HELPER_LEDGER_BASE;
|
|
553
|
+
}
|
|
370
554
|
const FALLBACK_EXT = {
|
|
371
555
|
image: "png",
|
|
372
556
|
video: "mp4",
|
|
@@ -383,6 +567,10 @@ function extFor(url, family) {
|
|
|
383
567
|
export function scenePrefix(sceneIndex) {
|
|
384
568
|
return `scene-${String(sceneIndex).padStart(2, "0")}`;
|
|
385
569
|
}
|
|
570
|
+
function clipQcOf(item) {
|
|
571
|
+
const artifact = item?.artifact;
|
|
572
|
+
return artifact && artifact.type === "video" ? artifact.qc?.takes : undefined;
|
|
573
|
+
}
|
|
386
574
|
function outputsOfNodeKind(run, kind) {
|
|
387
575
|
return run.nodes.filter((n) => n.kind === kind).flatMap((n) => n.outputs ?? []);
|
|
388
576
|
}
|
|
@@ -401,6 +589,8 @@ function clipFromArtifact(sceneIndex, artifact) {
|
|
|
401
589
|
revoiced: artifact.revoiced === true,
|
|
402
590
|
speechTrimmed: artifact.speechTrimmed === true,
|
|
403
591
|
rawStorageId: artifact.rawStorageId ?? null,
|
|
592
|
+
voiceMode: artifact.voiceMode ?? null,
|
|
593
|
+
voiceFirstBlockedBy: artifact.voiceFirstBlockedBy ?? null,
|
|
404
594
|
};
|
|
405
595
|
}
|
|
406
596
|
function keyframeDownload(sceneIndex, imageUrl) {
|
|
@@ -412,7 +602,9 @@ function keyframeDownload(sceneIndex, imageUrl) {
|
|
|
412
602
|
function reservedCastFrames(items) {
|
|
413
603
|
const byIndex = new Map();
|
|
414
604
|
for (const item of items) {
|
|
415
|
-
if (item.itemKind === "frame" &&
|
|
605
|
+
if (item.itemKind === "frame" &&
|
|
606
|
+
item.sceneIndex >= CAST_LEDGER_BASE &&
|
|
607
|
+
item.sceneIndex < SET_OPTION_LEDGER_BASE) {
|
|
416
608
|
byIndex.set(item.sceneIndex, item);
|
|
417
609
|
}
|
|
418
610
|
}
|
|
@@ -565,6 +757,8 @@ export function planPull(run, items, opts) {
|
|
|
565
757
|
for (const frame of artifact.frames ?? []) {
|
|
566
758
|
if (!frame.imageUrl)
|
|
567
759
|
continue;
|
|
760
|
+
if (!isPullSceneIndex(frame.sceneIndex))
|
|
761
|
+
continue;
|
|
568
762
|
keyframeByScene.set(frame.sceneIndex, keyframeDownload(frame.sceneIndex, frame.imageUrl));
|
|
569
763
|
}
|
|
570
764
|
}
|
|
@@ -582,7 +776,7 @@ export function planPull(run, items, opts) {
|
|
|
582
776
|
};
|
|
583
777
|
continue;
|
|
584
778
|
}
|
|
585
|
-
if (typeof artifact.sceneIndex !== "number" || artifact.sceneIndex
|
|
779
|
+
if (typeof artifact.sceneIndex !== "number" || !isPullSceneIndex(artifact.sceneIndex))
|
|
586
780
|
continue;
|
|
587
781
|
voiceByScene.set(artifact.sceneIndex, {
|
|
588
782
|
download: {
|
|
@@ -623,6 +817,8 @@ export function planPull(run, items, opts) {
|
|
|
623
817
|
}
|
|
624
818
|
if (artifact.type !== "video" || typeof artifact.sceneIndex !== "number")
|
|
625
819
|
continue;
|
|
820
|
+
if (!isPullSceneIndex(artifact.sceneIndex))
|
|
821
|
+
continue;
|
|
626
822
|
const clip = clipFromArtifact(artifact.sceneIndex, artifact);
|
|
627
823
|
if (!clip)
|
|
628
824
|
continue;
|
|
@@ -632,6 +828,8 @@ export function planPull(run, items, opts) {
|
|
|
632
828
|
const frameItemByScene = new Map();
|
|
633
829
|
const sceneFrameNodeIds = new Set(run.nodes.filter((n) => n.kind === "scene-frames").map((n) => n.nodeId));
|
|
634
830
|
for (const item of items) {
|
|
831
|
+
if (!isPullSceneIndex(item.sceneIndex))
|
|
832
|
+
continue;
|
|
635
833
|
if (item.itemKind === "clip")
|
|
636
834
|
clipItemByScene.set(item.sceneIndex, item);
|
|
637
835
|
if (item.itemKind === "frame" && sceneFrameNodeIds.has(item.nodeId)) {
|
|
@@ -663,7 +861,7 @@ export function planPull(run, items, opts) {
|
|
|
663
861
|
...frameItemByScene.keys(),
|
|
664
862
|
]),
|
|
665
863
|
]
|
|
666
|
-
.filter((sceneIndex) => sceneIndex
|
|
864
|
+
.filter((sceneIndex) => isPullSceneIndex(sceneIndex))
|
|
667
865
|
.sort((a, b) => a - b);
|
|
668
866
|
const scenes = sceneIndexes.map((sceneIndex) => {
|
|
669
867
|
const clip = clipByScene.get(sceneIndex);
|
|
@@ -698,10 +896,12 @@ export function planPull(run, items, opts) {
|
|
|
698
896
|
: "this-file",
|
|
699
897
|
voice: voice?.download.file ?? null,
|
|
700
898
|
keyframe: keyframeByScene.get(sceneIndex)?.file ?? null,
|
|
701
|
-
qc: clip?.qc
|
|
899
|
+
qc: clip?.qc ? qcWithoutJudgeWording(clip.qc) : null,
|
|
702
900
|
revoiced: clip ? clip.revoiced : null,
|
|
703
901
|
speechTrimmed: clip ? clip.speechTrimmed : null,
|
|
704
902
|
rawStorageId: clip ? clip.rawStorageId : null,
|
|
903
|
+
voiceMode: clip ? clip.voiceMode : null,
|
|
904
|
+
voiceFirstBlockedBy: clip ? clip.voiceFirstBlockedBy : null,
|
|
705
905
|
clipStatus: item?.status ?? "missing",
|
|
706
906
|
error: item?.error ?? null,
|
|
707
907
|
flagged: item?.flagged === true,
|
|
@@ -729,6 +929,10 @@ export function planPull(run, items, opts) {
|
|
|
729
929
|
storyboard,
|
|
730
930
|
reference: reference?.file ?? null,
|
|
731
931
|
music: music?.file ?? null,
|
|
932
|
+
musicBed: musicBedState(run),
|
|
933
|
+
musicHeardScenes: scenes
|
|
934
|
+
.filter((scene) => scene.findings.some((f) => f.code === MUSIC_HEARD_CODE))
|
|
935
|
+
.map((scene) => scene.sceneIndex),
|
|
732
936
|
cast,
|
|
733
937
|
narration: narrationDownload
|
|
734
938
|
? { file: narrationDownload.file, timing: "narration.json" }
|
|
@@ -738,6 +942,14 @@ export function planPull(run, items, opts) {
|
|
|
738
942
|
...(run.provenance?.format || run.provenance?.voice
|
|
739
943
|
? { provenance: run.provenance }
|
|
740
944
|
: {}),
|
|
945
|
+
...(run.videoChoice
|
|
946
|
+
? {
|
|
947
|
+
videoChoice: {
|
|
948
|
+
videoModel: run.videoChoice.videoModel,
|
|
949
|
+
voiceMode: run.videoChoice.voiceMode,
|
|
950
|
+
},
|
|
951
|
+
}
|
|
952
|
+
: {}),
|
|
741
953
|
},
|
|
742
954
|
};
|
|
743
955
|
}
|
|
@@ -763,6 +975,8 @@ export function markPullFailure(manifest, failure) {
|
|
|
763
975
|
scene.revoiced = null;
|
|
764
976
|
scene.speechTrimmed = null;
|
|
765
977
|
scene.rawStorageId = null;
|
|
978
|
+
scene.voiceMode = null;
|
|
979
|
+
scene.voiceFirstBlockedBy = null;
|
|
766
980
|
}
|
|
767
981
|
if (scene.voice === failure.file)
|
|
768
982
|
scene.voice = null;
|
|
@@ -868,7 +1082,7 @@ export async function startFlow(opts, deps) {
|
|
|
868
1082
|
showId: opts.showId,
|
|
869
1083
|
script,
|
|
870
1084
|
...(opts.voicePath ? { voicePath: opts.voicePath } : {}),
|
|
871
|
-
...(opts.music ===
|
|
1085
|
+
...(opts.music === undefined ? {} : { music: opts.music }),
|
|
872
1086
|
});
|
|
873
1087
|
if (!res.ok)
|
|
874
1088
|
return errorResult(res, opts.json);
|
|
@@ -897,6 +1111,264 @@ export async function startFlow(opts, deps) {
|
|
|
897
1111
|
lines: opts.json ? waited.lines : [`Started ad run ${runId}`, `Watch it: ${url}`, "", ...waited.lines],
|
|
898
1112
|
};
|
|
899
1113
|
}
|
|
1114
|
+
function nonBlank(value) {
|
|
1115
|
+
if (value === undefined || value.trim().length === 0)
|
|
1116
|
+
return undefined;
|
|
1117
|
+
return value;
|
|
1118
|
+
}
|
|
1119
|
+
function isVoiceMode(value) {
|
|
1120
|
+
return VOICE_MODES.some((mode) => mode === value);
|
|
1121
|
+
}
|
|
1122
|
+
function isConceit(value) {
|
|
1123
|
+
return CONCEIT_KEYS.some((key) => key === value);
|
|
1124
|
+
}
|
|
1125
|
+
export function planMusicChoice(occurrences) {
|
|
1126
|
+
let music;
|
|
1127
|
+
for (const { flag, value } of occurrences) {
|
|
1128
|
+
if (flag === "no-music") {
|
|
1129
|
+
music = false;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (flag !== "music")
|
|
1133
|
+
continue;
|
|
1134
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1135
|
+
music = true;
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
const word = value.trim().toLowerCase();
|
|
1139
|
+
const literal = word === "true" ? true : word === "false" ? false : undefined;
|
|
1140
|
+
if (literal === undefined) {
|
|
1141
|
+
return {
|
|
1142
|
+
ok: false,
|
|
1143
|
+
line: `video start --music takes true or false, or no value at all (got "${value}").`,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
music = literal;
|
|
1147
|
+
}
|
|
1148
|
+
return { ok: true, music };
|
|
1149
|
+
}
|
|
1150
|
+
function planValueFlag(occurrences, name) {
|
|
1151
|
+
let value;
|
|
1152
|
+
for (const occurrence of occurrences) {
|
|
1153
|
+
if (occurrence.flag !== name)
|
|
1154
|
+
continue;
|
|
1155
|
+
const given = occurrence.value;
|
|
1156
|
+
if (given === undefined || given.startsWith("--") || given.trim().length === 0) {
|
|
1157
|
+
return { ok: false, line: `video start --${name} needs a value.` };
|
|
1158
|
+
}
|
|
1159
|
+
value = given.trim();
|
|
1160
|
+
}
|
|
1161
|
+
return { ok: true, value };
|
|
1162
|
+
}
|
|
1163
|
+
export function planVideoStart(flags, occurrences) {
|
|
1164
|
+
const showId = nonBlank(flagString(flags, "show"));
|
|
1165
|
+
const scriptFile = nonBlank(flagString(flags, "script"));
|
|
1166
|
+
const conceit = nonBlank(flagString(flags, "conceit"));
|
|
1167
|
+
const style = nonBlank(flagString(flags, "style"));
|
|
1168
|
+
if (showId && (conceit || style)) {
|
|
1169
|
+
return {
|
|
1170
|
+
kind: "usage",
|
|
1171
|
+
line: "Pass either --show <id>, or --conceit and --style. Not both.",
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (!scriptFile) {
|
|
1175
|
+
return {
|
|
1176
|
+
kind: "usage",
|
|
1177
|
+
line: "video start needs --script <file>, a text file of what the ad says.",
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
const musicChoice = planMusicChoice(occurrences);
|
|
1181
|
+
if (!musicChoice.ok)
|
|
1182
|
+
return { kind: "usage", line: musicChoice.line };
|
|
1183
|
+
const videoModelFlag = planValueFlag(occurrences, "video-model");
|
|
1184
|
+
if (!videoModelFlag.ok)
|
|
1185
|
+
return { kind: "usage", line: videoModelFlag.line };
|
|
1186
|
+
const voiceFlag = planValueFlag(occurrences, "voice");
|
|
1187
|
+
if (!voiceFlag.ok)
|
|
1188
|
+
return { kind: "usage", line: voiceFlag.line };
|
|
1189
|
+
const videoModel = videoModelFlag.value;
|
|
1190
|
+
const voiceMode = voiceFlag.value;
|
|
1191
|
+
if (showId && (videoModel || voiceMode)) {
|
|
1192
|
+
return {
|
|
1193
|
+
kind: "usage",
|
|
1194
|
+
line: "--video-model and --voice work with --conceit, not --show. A Show films on its own model.",
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
if (voiceMode && !isVoiceMode(voiceMode)) {
|
|
1198
|
+
return {
|
|
1199
|
+
kind: "usage",
|
|
1200
|
+
line: `video start --voice must be native or voice-first (got "${voiceMode}").`,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
if (showId) {
|
|
1204
|
+
return {
|
|
1205
|
+
kind: "show",
|
|
1206
|
+
opts: {
|
|
1207
|
+
showId,
|
|
1208
|
+
scriptFile,
|
|
1209
|
+
voicePath: flagString(flags, "voice-path"),
|
|
1210
|
+
music: musicChoice.music,
|
|
1211
|
+
wait: flags["wait"] === true,
|
|
1212
|
+
json: flags["json"] === true,
|
|
1213
|
+
},
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
if (!conceit) {
|
|
1217
|
+
return {
|
|
1218
|
+
kind: "usage",
|
|
1219
|
+
line: `video start needs --conceit <${CONCEIT_KEYS.join("|")}>.`,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
if (!isConceit(conceit)) {
|
|
1223
|
+
return {
|
|
1224
|
+
kind: "usage",
|
|
1225
|
+
line: `video start --conceit must be one of ${joinedOr(CONCEIT_KEYS)}.`,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
if (!style) {
|
|
1229
|
+
return {
|
|
1230
|
+
kind: "usage",
|
|
1231
|
+
line: `video start needs --style <${STYLE_SLUGS.join("|")}>.`,
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const direction = nonBlank(flagString(flags, "direction"));
|
|
1235
|
+
const voicePath = nonBlank(flagString(flags, "voice-path"));
|
|
1236
|
+
const opts = {
|
|
1237
|
+
scriptFile,
|
|
1238
|
+
conceit,
|
|
1239
|
+
style,
|
|
1240
|
+
wait: flags["wait"] === true,
|
|
1241
|
+
json: flags["json"] === true,
|
|
1242
|
+
};
|
|
1243
|
+
if (direction)
|
|
1244
|
+
opts.direction = direction;
|
|
1245
|
+
if (voicePath)
|
|
1246
|
+
opts.voicePath = voicePath;
|
|
1247
|
+
if (videoModel)
|
|
1248
|
+
opts.videoModel = videoModel;
|
|
1249
|
+
if (voiceMode && isVoiceMode(voiceMode))
|
|
1250
|
+
opts.voiceMode = voiceMode;
|
|
1251
|
+
if (musicChoice.music !== undefined)
|
|
1252
|
+
opts.music = musicChoice.music;
|
|
1253
|
+
return { kind: "script", opts };
|
|
1254
|
+
}
|
|
1255
|
+
function isRecord(value) {
|
|
1256
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1257
|
+
}
|
|
1258
|
+
function isResolvedCard(value) {
|
|
1259
|
+
if (!isRecord(value))
|
|
1260
|
+
return false;
|
|
1261
|
+
if (!isRecord(value.conceit) || !isRecord(value.style))
|
|
1262
|
+
return false;
|
|
1263
|
+
if (typeof value.conceit.key !== "string" || typeof value.conceit.label !== "string")
|
|
1264
|
+
return false;
|
|
1265
|
+
if (typeof value.family !== "string")
|
|
1266
|
+
return false;
|
|
1267
|
+
if (!("wrapper" in value))
|
|
1268
|
+
return false;
|
|
1269
|
+
if (value.wrapper !== null && typeof value.wrapper !== "string")
|
|
1270
|
+
return false;
|
|
1271
|
+
if (typeof value.style.slug !== "string" || typeof value.style.label !== "string")
|
|
1272
|
+
return false;
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
function startedVideoChoice(value) {
|
|
1276
|
+
if (!isRecord(value))
|
|
1277
|
+
return null;
|
|
1278
|
+
if (typeof value.videoModel !== "string" || typeof value.voiceMode !== "string")
|
|
1279
|
+
return null;
|
|
1280
|
+
return { videoModel: value.videoModel, voiceMode: value.voiceMode };
|
|
1281
|
+
}
|
|
1282
|
+
function scriptStartedLines(runId, url, card, video) {
|
|
1283
|
+
return [
|
|
1284
|
+
`Started ad run ${runId}`,
|
|
1285
|
+
...(card
|
|
1286
|
+
? [
|
|
1287
|
+
`Conceit: ${card.conceit.label}`,
|
|
1288
|
+
`Family: ${card.family}`,
|
|
1289
|
+
`Wrapper: ${card.wrapper ?? ""}`,
|
|
1290
|
+
`Style: ${card.style.label}`,
|
|
1291
|
+
]
|
|
1292
|
+
: ["The server did not include a resolved card."]),
|
|
1293
|
+
...(video ? [`Video model: ${video.videoModel}`, `Voice: ${video.voiceMode}`] : []),
|
|
1294
|
+
`Watch it: ${url}`,
|
|
1295
|
+
];
|
|
1296
|
+
}
|
|
1297
|
+
function waitJsonWithCard(waited, card, video) {
|
|
1298
|
+
const first = waited.lines[0];
|
|
1299
|
+
if (typeof first !== "string")
|
|
1300
|
+
return waited;
|
|
1301
|
+
let parsed;
|
|
1302
|
+
try {
|
|
1303
|
+
parsed = JSON.parse(first);
|
|
1304
|
+
}
|
|
1305
|
+
catch {
|
|
1306
|
+
return waited;
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
1309
|
+
return waited;
|
|
1310
|
+
return {
|
|
1311
|
+
code: waited.code,
|
|
1312
|
+
lines: [JSON.stringify({ ...parsed, card, ...(video ? { videoChoice: video } : {}) })],
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
export async function startScriptFlow(opts, deps) {
|
|
1316
|
+
let script;
|
|
1317
|
+
try {
|
|
1318
|
+
script = deps.readFile(opts.scriptFile);
|
|
1319
|
+
}
|
|
1320
|
+
catch (e) {
|
|
1321
|
+
return {
|
|
1322
|
+
code: 1,
|
|
1323
|
+
lines: [
|
|
1324
|
+
`Can't read the script file "${opts.scriptFile}": ${e instanceof Error ? e.message : String(e)}`,
|
|
1325
|
+
],
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (script.trim().length === 0) {
|
|
1329
|
+
return { code: 1, lines: [`The script file "${opts.scriptFile}" is empty.`] };
|
|
1330
|
+
}
|
|
1331
|
+
const res = await deps.post(SCRIPT_RUNS_PATH, {
|
|
1332
|
+
script,
|
|
1333
|
+
conceit: opts.conceit,
|
|
1334
|
+
style: opts.style,
|
|
1335
|
+
...(opts.direction ? { direction: opts.direction } : {}),
|
|
1336
|
+
...(opts.voicePath ? { voicePath: opts.voicePath } : {}),
|
|
1337
|
+
...(opts.videoModel ? { videoModel: opts.videoModel } : {}),
|
|
1338
|
+
...(opts.voiceMode ? { voiceMode: opts.voiceMode } : {}),
|
|
1339
|
+
...(opts.music === true ? { music: true } : {}),
|
|
1340
|
+
});
|
|
1341
|
+
if (!res.ok)
|
|
1342
|
+
return errorResult(res, opts.json);
|
|
1343
|
+
const started = res.data;
|
|
1344
|
+
if (!started.runId) {
|
|
1345
|
+
return { code: 1, lines: ["The server started the ad but did not say which run it is."] };
|
|
1346
|
+
}
|
|
1347
|
+
const runId = started.runId;
|
|
1348
|
+
const url = typeof started.url === "string" ? started.url : "";
|
|
1349
|
+
const card = isResolvedCard(started.card) ? started.card : null;
|
|
1350
|
+
const video = startedVideoChoice(started.videoChoice);
|
|
1351
|
+
if (!opts.wait) {
|
|
1352
|
+
return {
|
|
1353
|
+
code: 0,
|
|
1354
|
+
lines: opts.json
|
|
1355
|
+
? [JSON.stringify({ runId, url, card, ...(video ? { videoChoice: video } : {}) })]
|
|
1356
|
+
: [
|
|
1357
|
+
...scriptStartedLines(runId, url, card, video),
|
|
1358
|
+
"",
|
|
1359
|
+
"Wait for the storyboard here instead: exodus video start … --wait",
|
|
1360
|
+
`Or check in whenever: exodus video status ${runId}`,
|
|
1361
|
+
],
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
const waited = await waitFlow(runId, { json: opts.json, url }, deps);
|
|
1365
|
+
if (opts.json)
|
|
1366
|
+
return waitJsonWithCard(waited, card, video);
|
|
1367
|
+
return {
|
|
1368
|
+
code: waited.code,
|
|
1369
|
+
lines: [...scriptStartedLines(runId, url, card, video), "", ...waited.lines],
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
900
1372
|
const POLL_INTERVAL_MS = 5000;
|
|
901
1373
|
const MAX_POLLS = 720;
|
|
902
1374
|
export async function waitFlow(runId, opts, deps) {
|
|
@@ -956,6 +1428,11 @@ function itemWord(item) {
|
|
|
956
1428
|
return "flagged";
|
|
957
1429
|
return ITEM_STATUS_WORD[item.status] ?? item.status;
|
|
958
1430
|
}
|
|
1431
|
+
const CARRYING_ON_LINE = "Waiting for a worker to carry on. Everything finished so far is kept — the run picks up from there by itself.";
|
|
1432
|
+
function carryingOn(run) {
|
|
1433
|
+
return (run.status === "queued" &&
|
|
1434
|
+
run.nodes.some((n) => n.status !== "idle" && n.status !== "out-of-scope"));
|
|
1435
|
+
}
|
|
959
1436
|
export async function statusFlow(runId, json, deps) {
|
|
960
1437
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
961
1438
|
if (!runRes.ok)
|
|
@@ -967,7 +1444,10 @@ export async function statusFlow(runId, json, deps) {
|
|
|
967
1444
|
const items = itemsRes.data.items ?? [];
|
|
968
1445
|
const hasFinal = hasAttachedCut(items);
|
|
969
1446
|
const stop = resolveStop(classifyRun(run), hasFinal);
|
|
970
|
-
const
|
|
1447
|
+
const failedStoryboard = failedStoryboardNode(run);
|
|
1448
|
+
const guidance = stopLines(stop.at === "failed" && errorEchoesReasons(stop.error, failedStoryboard?.planFailure)
|
|
1449
|
+
? { ...stop, error: undefined }
|
|
1450
|
+
: stop, runId, reviewUrl(deps.dashboardUrl, run));
|
|
971
1451
|
const warnings = run.nodes
|
|
972
1452
|
.filter((n) => n.warning)
|
|
973
1453
|
.map((n) => ({ nodeId: n.nodeId, step: n.kind, warning: n.warning }));
|
|
@@ -975,13 +1455,39 @@ export async function statusFlow(runId, json, deps) {
|
|
|
975
1455
|
return {
|
|
976
1456
|
code: 0,
|
|
977
1457
|
lines: [
|
|
978
|
-
JSON.stringify({
|
|
1458
|
+
JSON.stringify({
|
|
1459
|
+
runId,
|
|
1460
|
+
status: run.status,
|
|
1461
|
+
stop,
|
|
1462
|
+
items,
|
|
1463
|
+
hasFinal,
|
|
1464
|
+
warnings,
|
|
1465
|
+
guidance,
|
|
1466
|
+
...(run.musicBed === undefined ? {} : { musicBed: run.musicBed }),
|
|
1467
|
+
...(failedStoryboard?.planFailure ? { planFailure: failedStoryboard.planFailure } : {}),
|
|
1468
|
+
...(failedStoryboard?.planFailure
|
|
1469
|
+
? { storyboardNodeId: failedStoryboard.nodeId }
|
|
1470
|
+
: {}),
|
|
1471
|
+
...(failedStoryboard?.hasRejectedDraft === true ? { hasRejectedDraft: true } : {}),
|
|
1472
|
+
}),
|
|
979
1473
|
],
|
|
980
1474
|
};
|
|
981
1475
|
}
|
|
982
1476
|
const revoicedByScene = new Set();
|
|
1477
|
+
const voiceFirstByScene = new Set();
|
|
1478
|
+
const voiceFirstBlockedByScene = new Map();
|
|
983
1479
|
const timedOnOriginalVoice = new Set();
|
|
984
1480
|
for (const artifact of outputsOfNodeKind(run, "video")) {
|
|
1481
|
+
if (artifact.type === "video" &&
|
|
1482
|
+
artifact.voiceMode === "voice-first" &&
|
|
1483
|
+
typeof artifact.sceneIndex === "number") {
|
|
1484
|
+
voiceFirstByScene.add(artifact.sceneIndex);
|
|
1485
|
+
}
|
|
1486
|
+
if (artifact.type === "video" &&
|
|
1487
|
+
artifact.voiceFirstBlockedBy?.length &&
|
|
1488
|
+
typeof artifact.sceneIndex === "number") {
|
|
1489
|
+
voiceFirstBlockedByScene.set(artifact.sceneIndex, artifact.voiceFirstBlockedBy);
|
|
1490
|
+
}
|
|
985
1491
|
if (artifact.type === "video" && artifact.revoiced === true && typeof artifact.sceneIndex === "number") {
|
|
986
1492
|
revoicedByScene.add(artifact.sceneIndex);
|
|
987
1493
|
if ((artifact.words?.length ?? 0) > 0)
|
|
@@ -1007,8 +1513,10 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1007
1513
|
: `Ad run ${runId} — ${displayRunStatus(run.status)}`;
|
|
1008
1514
|
const lines = [
|
|
1009
1515
|
headline,
|
|
1516
|
+
...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
|
|
1010
1517
|
...guidance,
|
|
1011
1518
|
...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
|
|
1519
|
+
...planFailureLines(failedStoryboard, runId),
|
|
1012
1520
|
...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
|
|
1013
1521
|
];
|
|
1014
1522
|
if (byScene.size === 0) {
|
|
@@ -1019,10 +1527,19 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1019
1527
|
for (const sceneIndex of [...byScene.keys()].sort((a, b) => a - b)) {
|
|
1020
1528
|
const row = byScene.get(sceneIndex);
|
|
1021
1529
|
lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
|
|
1022
|
-
if (row.clip?.lastRedo)
|
|
1023
|
-
lines.push(` clip: ${row.clip.lastRedo
|
|
1024
|
-
|
|
1025
|
-
|
|
1530
|
+
if (row.clip?.lastRedo) {
|
|
1531
|
+
lines.push(` clip: ${redoLine(row.clip.lastRedo)}`);
|
|
1532
|
+
printDisplacedHistory(lines, row.clip.lastRedo);
|
|
1533
|
+
}
|
|
1534
|
+
if (row.clip?.lastRedo || row.clip?.flagged) {
|
|
1535
|
+
for (const line of renderQcTakeHistory(clipQcOf(row.clip))) {
|
|
1536
|
+
lines.push(` ${line}`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (row.voiceover?.lastRedo) {
|
|
1540
|
+
lines.push(` voice: ${redoLine(row.voiceover.lastRedo)}`);
|
|
1541
|
+
printDisplacedHistory(lines, row.voiceover.lastRedo);
|
|
1542
|
+
}
|
|
1026
1543
|
if (row.frame?.lastRedo)
|
|
1027
1544
|
lines.push(` picture: ${row.frame.lastRedo.label}`);
|
|
1028
1545
|
for (const finding of row.clip?.findings ?? []) {
|
|
@@ -1039,6 +1556,12 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1039
1556
|
lines.push(` ${row.clip.error}`);
|
|
1040
1557
|
if (revoicedByScene.has(sceneIndex))
|
|
1041
1558
|
lines.push(" voice: cast voice applied");
|
|
1559
|
+
if (voiceFirstByScene.has(sceneIndex))
|
|
1560
|
+
lines.push(" clip: redone voice first");
|
|
1561
|
+
const blockedBy = voiceFirstBlockedByScene.get(sceneIndex);
|
|
1562
|
+
if (blockedBy) {
|
|
1563
|
+
lines.push(` clip: redone the usual way — ${cantSayNames(blockedBy)}`);
|
|
1564
|
+
}
|
|
1042
1565
|
if (timedOnOriginalVoice.has(sceneIndex)) {
|
|
1043
1566
|
lines.push(" word timings: measured on the original voice (close, not frame-exact)");
|
|
1044
1567
|
}
|
|
@@ -1057,6 +1580,45 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1057
1580
|
: "Final cut: not uploaded yet.");
|
|
1058
1581
|
return { code: 0, lines };
|
|
1059
1582
|
}
|
|
1583
|
+
export async function rejectedDraftFlow(runId, json, deps) {
|
|
1584
|
+
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1585
|
+
if (!runRes.ok)
|
|
1586
|
+
return errorResult(runRes, json);
|
|
1587
|
+
const node = failedStoryboardNode(asVideoRun(runRes.data));
|
|
1588
|
+
if (!node) {
|
|
1589
|
+
const note = "The storyboard on this run didn't fail, so there is no rejected draft to read.";
|
|
1590
|
+
return {
|
|
1591
|
+
code: 0,
|
|
1592
|
+
lines: json ? [JSON.stringify({ runId, nodeId: null, draft: null })] : [note],
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
const res = await deps.get(`${REJECTED_DRAFT_PATH}?runId=${encodeURIComponent(runId)}&nodeId=${encodeURIComponent(node.nodeId)}`);
|
|
1596
|
+
if (missingRouteLine(res, "exodus video status --rejected-draft")) {
|
|
1597
|
+
return {
|
|
1598
|
+
code: 1,
|
|
1599
|
+
lines: json
|
|
1600
|
+
? [JSON.stringify({ ok: false, status: 404, error: REJECTED_DRAFT_NOT_ON_THIS_SERVER })]
|
|
1601
|
+
: [REJECTED_DRAFT_NOT_ON_THIS_SERVER],
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (!res.ok)
|
|
1605
|
+
return errorResult(res, json);
|
|
1606
|
+
const draft = res.data.draft ?? null;
|
|
1607
|
+
if (json)
|
|
1608
|
+
return { code: 0, lines: [JSON.stringify({ runId, nodeId: node.nodeId, draft })] };
|
|
1609
|
+
if (!draft)
|
|
1610
|
+
return { code: 0, lines: ["No rejected draft was kept for this attempt."] };
|
|
1611
|
+
return {
|
|
1612
|
+
code: 0,
|
|
1613
|
+
lines: [
|
|
1614
|
+
`REJECTED DRAFT — ad run ${runId}, step ${node.nodeId}`,
|
|
1615
|
+
"The system turned this draft down and threw it away. It is kept only as a record of what happened.",
|
|
1616
|
+
"Nothing in it is part of your ad. There is nothing here to act on or edit.",
|
|
1617
|
+
"",
|
|
1618
|
+
...draft.split("\n"),
|
|
1619
|
+
],
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1060
1622
|
export async function storyboardFlow(runId, json, deps) {
|
|
1061
1623
|
const res = await deps.get(`${STORYBOARD_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1062
1624
|
if (!res.ok)
|
|
@@ -1108,7 +1670,14 @@ export async function storyboardFlow(runId, json, deps) {
|
|
|
1108
1670
|
}
|
|
1109
1671
|
}
|
|
1110
1672
|
}
|
|
1111
|
-
|
|
1673
|
+
const runRes = await deps
|
|
1674
|
+
.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`)
|
|
1675
|
+
.catch(() => null);
|
|
1676
|
+
const showAd = runRes?.ok === true && isShowAd(asVideoRun(runRes.data));
|
|
1677
|
+
lines.push("", `approve with: exodus video approve ${runId}`);
|
|
1678
|
+
if (showAd) {
|
|
1679
|
+
lines.push(`send it back: exodus video flag ${runId} --note "what's wrong"`);
|
|
1680
|
+
}
|
|
1112
1681
|
if (cards.framesNodeId) {
|
|
1113
1682
|
lines.push(`redo one frame: exodus video retry-frame ${runId} --node ${cards.framesNodeId} --scene <n>`);
|
|
1114
1683
|
}
|
|
@@ -1136,14 +1705,30 @@ function sceneCardLines(scene) {
|
|
|
1136
1705
|
lines.push(` picture: ${scene.frame.status}`);
|
|
1137
1706
|
return lines;
|
|
1138
1707
|
}
|
|
1139
|
-
export async function approveFlow(runId,
|
|
1140
|
-
const res = await deps.post(APPROVE_PATH, {
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1708
|
+
export async function approveFlow(runId, opts, deps) {
|
|
1709
|
+
const res = await deps.post(APPROVE_PATH, {
|
|
1710
|
+
runId,
|
|
1711
|
+
...(opts.approveStaleCut ? { approveStaleCut: true } : {}),
|
|
1712
|
+
});
|
|
1713
|
+
if (!res.ok) {
|
|
1714
|
+
const refusal = errorResult(res, opts.json);
|
|
1715
|
+
const staleCut = !opts.json && videoApiError(res).includes("approve this cut anyway");
|
|
1716
|
+
return staleCut
|
|
1717
|
+
? {
|
|
1718
|
+
...refusal,
|
|
1719
|
+
lines: [
|
|
1720
|
+
...refusal.lines,
|
|
1721
|
+
`To deliver it anyway: exodus video approve ${runId} --approve-stale-cut`,
|
|
1722
|
+
],
|
|
1723
|
+
}
|
|
1724
|
+
: refusal;
|
|
1725
|
+
}
|
|
1726
|
+
const data = res.data;
|
|
1727
|
+
const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
|
|
1728
|
+
if (opts.json) {
|
|
1729
|
+
return { code: 0, lines: [JSON.stringify({ ok: true, runId, warnings, data: res.data })] };
|
|
1730
|
+
}
|
|
1731
|
+
const voices = data?.voices;
|
|
1147
1732
|
const summary = voices
|
|
1148
1733
|
? voiceSummaryLines(Array.isArray(voices.cast) ? voices.cast : [], Array.isArray(voices.notices) ? voices.notices : [])
|
|
1149
1734
|
: [];
|
|
@@ -1151,6 +1736,7 @@ export async function approveFlow(runId, json, deps) {
|
|
|
1151
1736
|
code: 0,
|
|
1152
1737
|
lines: [
|
|
1153
1738
|
"Approved.",
|
|
1739
|
+
...warnings,
|
|
1154
1740
|
...(summary.length > 0 ? ["", ...summary, ""] : []),
|
|
1155
1741
|
`See what happens next: exodus video status ${runId}`,
|
|
1156
1742
|
],
|
|
@@ -1208,8 +1794,9 @@ export async function retryFrameFlow(runId, nodeId, sceneIndex, note, json, deps
|
|
|
1208
1794
|
],
|
|
1209
1795
|
};
|
|
1210
1796
|
}
|
|
1211
|
-
const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip.
|
|
1212
|
-
"re-cut
|
|
1797
|
+
const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip. If this redo replaces the clip, " +
|
|
1798
|
+
"exodus video approve stops until you pull the pieces again, re-cut and upload again, " +
|
|
1799
|
+
"or pass --approve-stale-cut to deliver the cut you uploaded as it is.";
|
|
1213
1800
|
export function planClipRedo(run, items, target) {
|
|
1214
1801
|
const stop = classifyRun(run);
|
|
1215
1802
|
const found = findClipRow(run, items, target);
|
|
@@ -1275,7 +1862,7 @@ function findClipRow(run, items, target) {
|
|
|
1275
1862
|
}
|
|
1276
1863
|
return { row, uploadedCut };
|
|
1277
1864
|
}
|
|
1278
|
-
export async function retryClipFlow(runId, target, note, json, deps) {
|
|
1865
|
+
export async function retryClipFlow(runId, target, { note, voiceFirst = false }, json, deps) {
|
|
1279
1866
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1280
1867
|
if (!runRes.ok)
|
|
1281
1868
|
return errorResult(runRes, json);
|
|
@@ -1291,12 +1878,20 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1291
1878
|
lines: json ? [JSON.stringify({ ok: false, error: plan.reason })] : [plan.reason],
|
|
1292
1879
|
};
|
|
1293
1880
|
}
|
|
1294
|
-
const res = await deps.post(SCENE_RETRY_PATH, {
|
|
1881
|
+
const res = await deps.post(voiceFirst ? SCENE_VOICE_FIRST_PATH : SCENE_RETRY_PATH, {
|
|
1295
1882
|
runId,
|
|
1296
1883
|
nodeId: plan.nodeId,
|
|
1297
1884
|
sceneIndex: plan.sceneIndex,
|
|
1298
1885
|
...(note ? { note } : {}),
|
|
1299
1886
|
});
|
|
1887
|
+
if (voiceFirst && missingRouteLine(res, "exodus video retry-clip --voice-first")) {
|
|
1888
|
+
return {
|
|
1889
|
+
code: 1,
|
|
1890
|
+
lines: json
|
|
1891
|
+
? [JSON.stringify({ ok: false, status: 404, error: VOICE_FIRST_NOT_ON_THIS_SERVER })]
|
|
1892
|
+
: [VOICE_FIRST_NOT_ON_THIS_SERVER],
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1300
1895
|
if (!res.ok)
|
|
1301
1896
|
return errorResult(res, json);
|
|
1302
1897
|
const triggerRunId = res.data.triggerRunId;
|
|
@@ -1315,6 +1910,7 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1315
1910
|
reviewUrl: review,
|
|
1316
1911
|
warnings: plan.warnings,
|
|
1317
1912
|
...(note ? { note } : {}),
|
|
1913
|
+
...(voiceFirst ? { voiceMode: "voice-first" } : {}),
|
|
1318
1914
|
}),
|
|
1319
1915
|
],
|
|
1320
1916
|
};
|
|
@@ -1322,9 +1918,19 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1322
1918
|
return {
|
|
1323
1919
|
code: 0,
|
|
1324
1920
|
lines: [
|
|
1325
|
-
|
|
1921
|
+
voiceFirst
|
|
1922
|
+
? `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}, voice first: the line is ` +
|
|
1923
|
+
"recorded in the character's own voice, then the clip is made to match it. " +
|
|
1924
|
+
"If the line has a made-up name the recorded voice can't say, the clip may be made " +
|
|
1925
|
+
`the usual way instead, and exodus video status ${runId} will say so.`
|
|
1926
|
+
: `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}.`,
|
|
1326
1927
|
`triggerRunId: ${triggerRunId ?? "-"}`,
|
|
1327
|
-
|
|
1928
|
+
...(voiceFirst
|
|
1929
|
+
? [
|
|
1930
|
+
"Every other scene stays as it is, and so do the pictures and the script.",
|
|
1931
|
+
"A speaking character with no voice yet gets one picked and saved for the whole run.",
|
|
1932
|
+
]
|
|
1933
|
+
: ["Every other scene stays as it is, and so do the pictures, the voices and the script."]),
|
|
1328
1934
|
...(note
|
|
1329
1935
|
? ["Your note steers how this clip moves. It never changes the words that are spoken."]
|
|
1330
1936
|
: []),
|
|
@@ -1641,6 +2247,13 @@ export function voiceSheetLines(sheet) {
|
|
|
1641
2247
|
: (sheet.whyNot ?? "Voices can no longer be changed on this run."));
|
|
1642
2248
|
return lines;
|
|
1643
2249
|
}
|
|
2250
|
+
function musicLines(manifest) {
|
|
2251
|
+
const lines = [`music: ${manifest.musicBed}`];
|
|
2252
|
+
if (manifest.musicHeardScenes.length > 0) {
|
|
2253
|
+
lines.push(`music heard in scene ${manifest.musicHeardScenes.join(", ")}`);
|
|
2254
|
+
}
|
|
2255
|
+
return lines;
|
|
2256
|
+
}
|
|
1644
2257
|
export async function pullFlow(runId, dir, json, deps) {
|
|
1645
2258
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1646
2259
|
if (!runRes.ok)
|
|
@@ -1684,7 +2297,20 @@ export async function pullFlow(runId, dir, json, deps) {
|
|
|
1684
2297
|
const lines = [
|
|
1685
2298
|
`Pulled ${wrote} files into ${dir}`,
|
|
1686
2299
|
`Every piece is indexed in ${path.join(dir, "manifest.json")} — scene numbers there are the run's own.`,
|
|
2300
|
+
...musicLines(plan.manifest),
|
|
1687
2301
|
];
|
|
2302
|
+
const voiceFirst = plan.manifest.scenes.filter((s) => s.voiceMode === "voice-first");
|
|
2303
|
+
if (voiceFirst.length > 0) {
|
|
2304
|
+
lines.push("", `Redone voice first (the line recorded, then the clip made to match it): ` +
|
|
2305
|
+
`scene${voiceFirst.length === 1 ? "" : "s"} ${voiceFirst.map((s) => s.sceneIndex).join(", ")}`);
|
|
2306
|
+
}
|
|
2307
|
+
const redoneUsualWay = plan.manifest.scenes.filter((s) => s.voiceFirstBlockedBy?.length);
|
|
2308
|
+
if (redoneUsualWay.length > 0) {
|
|
2309
|
+
lines.push("", "Asked for voice first, but redone the usual way:");
|
|
2310
|
+
for (const scene of redoneUsualWay) {
|
|
2311
|
+
lines.push(` scene ${scene.sceneIndex}: ${cantSayNames(scene.voiceFirstBlockedBy ?? [])}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
1688
2314
|
const flagged = plan.manifest.scenes.filter((s) => s.flagged);
|
|
1689
2315
|
if (flagged.length > 0) {
|
|
1690
2316
|
lines.push("", `${flagged.length} clip${flagged.length === 1 ? "" : "s"} came back flagged — usable, but look before you cut:`);
|
|
@@ -1940,7 +2566,12 @@ export async function uploadFlow(runId, filePath, durationFlag, json, deps) {
|
|
|
1940
2566
|
const VALUE_FLAGS = new Set([
|
|
1941
2567
|
"show",
|
|
1942
2568
|
"script",
|
|
2569
|
+
"conceit",
|
|
2570
|
+
"style",
|
|
2571
|
+
"direction",
|
|
1943
2572
|
"voice-path",
|
|
2573
|
+
"video-model",
|
|
2574
|
+
"voice",
|
|
1944
2575
|
"note",
|
|
1945
2576
|
"out",
|
|
1946
2577
|
"file",
|
|
@@ -1993,20 +2624,12 @@ export async function run(flags, occurrences) {
|
|
|
1993
2624
|
if (sub === "shows")
|
|
1994
2625
|
return printResult(await showsFlow(json, defaultDeps));
|
|
1995
2626
|
if (sub === "start") {
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
return printResult(await startFlow({
|
|
2003
|
-
showId,
|
|
2004
|
-
scriptFile,
|
|
2005
|
-
voicePath: flagString(flags, "voice-path"),
|
|
2006
|
-
music: flags["music"] === false ? false : undefined,
|
|
2007
|
-
wait: flags["wait"] === true,
|
|
2008
|
-
json,
|
|
2009
|
-
}, defaultDeps));
|
|
2627
|
+
const plan = planVideoStart(flags, occurrences);
|
|
2628
|
+
if (plan.kind === "usage")
|
|
2629
|
+
usage(plan.line);
|
|
2630
|
+
if (plan.kind === "show")
|
|
2631
|
+
return printResult(await startFlow(plan.opts, defaultDeps));
|
|
2632
|
+
return printResult(await startScriptFlow(plan.opts, defaultDeps));
|
|
2010
2633
|
}
|
|
2011
2634
|
const runId = rest[0];
|
|
2012
2635
|
const needsRunId = [
|
|
@@ -2024,12 +2647,17 @@ export async function run(flags, occurrences) {
|
|
|
2024
2647
|
if (needsRunId.includes(sub) && !runId) {
|
|
2025
2648
|
usage(`video ${sub} needs a run id: exodus video ${sub} <runId>`);
|
|
2026
2649
|
}
|
|
2027
|
-
if (sub === "status")
|
|
2650
|
+
if (sub === "status") {
|
|
2651
|
+
if (flags["rejected-draft"] !== undefined && flags["rejected-draft"] !== false) {
|
|
2652
|
+
return printResult(await rejectedDraftFlow(runId, json, defaultDeps));
|
|
2653
|
+
}
|
|
2028
2654
|
return printResult(await statusFlow(runId, json, defaultDeps));
|
|
2655
|
+
}
|
|
2029
2656
|
if (sub === "storyboard")
|
|
2030
2657
|
return printResult(await storyboardFlow(runId, json, defaultDeps));
|
|
2031
|
-
if (sub === "approve")
|
|
2032
|
-
return printResult(await approveFlow(runId, json, defaultDeps));
|
|
2658
|
+
if (sub === "approve") {
|
|
2659
|
+
return printResult(await approveFlow(runId, { json, approveStaleCut: flags["approve-stale-cut"] === true }, defaultDeps));
|
|
2660
|
+
}
|
|
2033
2661
|
if (sub === "flag") {
|
|
2034
2662
|
const note = flagString(flags, "note");
|
|
2035
2663
|
if (!note)
|
|
@@ -2059,7 +2687,7 @@ export async function run(flags, occurrences) {
|
|
|
2059
2687
|
if (!Number.isInteger(sceneIndex)) {
|
|
2060
2688
|
usage(`video retry-clip --scene must be a whole scene number, not "${sceneRaw}".`);
|
|
2061
2689
|
}
|
|
2062
|
-
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, flagString(flags, "note"), json, defaultDeps));
|
|
2690
|
+
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, { note: flagString(flags, "note"), voiceFirst: flags["voice-first"] === true }, json, defaultDeps));
|
|
2063
2691
|
}
|
|
2064
2692
|
if (sub === "revoice") {
|
|
2065
2693
|
const sceneRaw = flagString(flags, "scene");
|