@aicopycoders/exodus 2026.9.2000-beta.20 → 2026.9.2000-beta.21
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.
|
@@ -32,7 +32,8 @@ workflow run`. From there the same nodes render, `video status` reads the run,
|
|
|
32
32
|
`video pull` writes the folder, and `video upload` and `approve` finish it.
|
|
33
33
|
|
|
34
34
|
A Show is no longer required. `exodus video start` starts from a script, a
|
|
35
|
-
conceit, and a style
|
|
35
|
+
conceit, and a style. The same parks follow, except that the app approves the
|
|
36
|
+
storyboard itself when its checks pass (see "What costs money"). The full line is under
|
|
36
37
|
"Starting a run". An optional `--direction` note is whole-ad rules, and that
|
|
37
38
|
note cannot contain a colon. "The script file" shows the right form and the
|
|
38
39
|
wrong one.
|
|
@@ -84,9 +85,14 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
84
85
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
85
86
|
same, with the video model speaking voices you WROTE
|
|
86
87
|
exodus video status <runId> where it is, per scene
|
|
87
|
-
exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--wait]
|
|
88
|
+
exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--review-storyboard] [--wait]
|
|
88
89
|
start from a script, a conceit and a look.
|
|
89
90
|
No music bed unless you pass --music.
|
|
91
|
+
The app approves its own storyboard when its
|
|
92
|
+
checks pass and goes straight on to the clips,
|
|
93
|
+
which is the part that costs. It stops at the
|
|
94
|
+
gate only when a check finds a problem, and says
|
|
95
|
+
why. --review-storyboard always stops there.
|
|
90
96
|
Each conceit has its own video model and voice;
|
|
91
97
|
--video-model / --voice override them for one run
|
|
92
98
|
```
|
|
@@ -169,18 +175,31 @@ minutes with "Still running after 60 minutes" and exit 0; that is not done, so
|
|
|
169
175
|
check `status` before treating it as finished. Do not poll `status` in a tight
|
|
170
176
|
loop; a full run is several minutes.
|
|
171
177
|
|
|
172
|
-
**What costs money.**
|
|
173
|
-
reference, cast and scene pictures
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
**What costs money.** Every run writes the storyboard first and then draws the
|
|
179
|
+
reference, cast and scene pictures. Each picture is a few cents and each scene
|
|
180
|
+
draws a few to choose from: about 25 cents on a small test, and it can approach
|
|
181
|
+
a dollar on a longer ad. The expensive part comes after the storyboard: the
|
|
182
|
+
voices and the clips. What happens at that point depends on how the run
|
|
183
|
+
started.
|
|
184
|
+
|
|
185
|
+
- A paste-a-script run (`exodus video start --conceit …`) checks its own
|
|
186
|
+
storyboard once the pictures are drawn. When every check passes, the app
|
|
187
|
+
approves the storyboard itself and goes straight on to the voices and the
|
|
188
|
+
clips, with no stop. It stops for the user only when a check finds a problem,
|
|
189
|
+
and the stop block says why in plain words. Started with
|
|
190
|
+
`--review-storyboard`, it always stops at the storyboard instead. `status`
|
|
191
|
+
prints "the app checks the storyboard and approves it itself if the checks
|
|
192
|
+
pass" on a run headed that way.
|
|
193
|
+
- A Show run (`video start --show`) and a saved workflow run (`workflow run`)
|
|
194
|
+
still pause at the storyboard for approval. Approving is what releases the
|
|
195
|
+
voices and the clips. `status` prints "This run will pause for your approval
|
|
196
|
+
once the frames are ready" on a run headed for that pause.
|
|
197
|
+
|
|
198
|
+
Either way, a run still drawing pictures is healthy and already spending a
|
|
199
|
+
little. Do not cancel it because nothing has happened yet. `workflow list`,
|
|
200
|
+
`workflow describe`, `video status`, `storyboard` and `pull` are free reads; run
|
|
201
|
+
them as often as you like. Never start a second run to "retry" without the user
|
|
202
|
+
asking; tell them what failed instead.
|
|
184
203
|
|
|
185
204
|
**`--auto-approve` does not cover either video gate.** It releases Checkpoint
|
|
186
205
|
boxes only (`convex/workflows.ts:7266` keys on `pauseReason === "checkpoint"`),
|
|
@@ -574,6 +593,11 @@ musicHeardScenes[] scene numbers whose clip came back with
|
|
|
574
593
|
music the video model baked in. A run with
|
|
575
594
|
the bed off can still list scenes here, and
|
|
576
595
|
those are the ones to listen to
|
|
596
|
+
pendingRedo[] scene numbers the app is redoing right
|
|
597
|
+
now. Their clip and words are left out,
|
|
598
|
+
because the file on the run is the take
|
|
599
|
+
being replaced: run `exodus video wait`,
|
|
600
|
+
then pull again
|
|
577
601
|
narration { file, timing }, or null on a per-scene run
|
|
578
602
|
cast[] one per identity still
|
|
579
603
|
characterId, name from the run's cast lock; null for an
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -118,6 +118,7 @@ export interface VideoRunNode {
|
|
|
118
118
|
hasRejectedDraft?: boolean;
|
|
119
119
|
outputs?: ArtifactSubset[];
|
|
120
120
|
missingScenes?: MissingScene[];
|
|
121
|
+
autoRedoScenes?: number[];
|
|
121
122
|
}
|
|
122
123
|
export interface MissingScene {
|
|
123
124
|
sceneIndex: number;
|
|
@@ -151,6 +152,11 @@ export interface VideoRun {
|
|
|
151
152
|
provenance?: RunProvenance;
|
|
152
153
|
musicBed?: boolean;
|
|
153
154
|
videoChoice?: RunVideoChoice;
|
|
155
|
+
storyboardAutoApproval?: {
|
|
156
|
+
outcome: "approved" | "held";
|
|
157
|
+
at: number;
|
|
158
|
+
reason?: string;
|
|
159
|
+
};
|
|
154
160
|
workflowId?: string;
|
|
155
161
|
moduleOwned?: boolean;
|
|
156
162
|
}
|
|
@@ -222,11 +228,13 @@ export declare function videoApiError(res: ApiResponse<unknown>): string;
|
|
|
222
228
|
export type RunStop = {
|
|
223
229
|
at: "running";
|
|
224
230
|
stage: string;
|
|
231
|
+
autoRedoScenes?: number[];
|
|
225
232
|
} | {
|
|
226
233
|
at: "storyboard-gate";
|
|
227
234
|
nodeId?: string;
|
|
228
235
|
framesNodeId?: string;
|
|
229
236
|
showAd?: true;
|
|
237
|
+
heldReason?: string;
|
|
230
238
|
} | {
|
|
231
239
|
at: "final-watch";
|
|
232
240
|
missingScenes?: MissingScene[];
|
|
@@ -254,6 +262,7 @@ export type ResolvedStop = Exclude<RunStop, {
|
|
|
254
262
|
});
|
|
255
263
|
export declare function classifyRun(run: VideoRun): RunStop;
|
|
256
264
|
export declare function hasAttachedCut(items: NodeItem[]): boolean;
|
|
265
|
+
export declare function scenesBeingRedone(run: VideoRun, items: NodeItem[]): number[];
|
|
257
266
|
export declare function resolveStop(stop: RunStop, cutAttached: boolean | null): ResolvedStop;
|
|
258
267
|
export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
|
|
259
268
|
export declare function stageWord(stage: string): string;
|
|
@@ -325,6 +334,7 @@ export interface VideoManifest {
|
|
|
325
334
|
music: string | null;
|
|
326
335
|
musicBed: MusicBedState;
|
|
327
336
|
musicHeardScenes: number[];
|
|
337
|
+
pendingRedo: number[];
|
|
328
338
|
cast: ManifestCastRef[];
|
|
329
339
|
narration: {
|
|
330
340
|
file: string;
|
|
@@ -371,6 +381,7 @@ export interface ScriptStartOptions {
|
|
|
371
381
|
videoModel?: string;
|
|
372
382
|
voiceMode?: VoiceMode;
|
|
373
383
|
music?: boolean;
|
|
384
|
+
reviewStoryboard?: true;
|
|
374
385
|
wait: boolean;
|
|
375
386
|
json: boolean;
|
|
376
387
|
}
|
package/dist/commands/video.js
CHANGED
|
@@ -103,7 +103,7 @@ Usage:
|
|
|
103
103
|
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
104
104
|
exodus video pull <runId> --out <dir> [--json]
|
|
105
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]
|
|
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] [--review-storyboard] [--wait] [--json]
|
|
107
107
|
|
|
108
108
|
Options:
|
|
109
109
|
--out <dir> Folder to write the pulled pieces into (pull)
|
|
@@ -144,7 +144,11 @@ Options:
|
|
|
144
144
|
--music Put a music bed under the ad (start). An ad has no music
|
|
145
145
|
unless you ask for it here
|
|
146
146
|
--no-music Leave the music bed off (start), which it already is
|
|
147
|
-
--
|
|
147
|
+
--review-storyboard Stop at the storyboard so you can approve it yourself
|
|
148
|
+
(start, with --conceit). By default the app approves its
|
|
149
|
+
own storyboard when its checks pass
|
|
150
|
+
--wait Wait until the run needs you (start): at the
|
|
151
|
+
storyboard, or once every piece is made
|
|
148
152
|
--json Machine-readable output
|
|
149
153
|
--help, -h Print this help
|
|
150
154
|
|
|
@@ -324,6 +328,14 @@ function parkedAtFinalWatch(run) {
|
|
|
324
328
|
return (!parkedByBuilderCheckpoint(run) &&
|
|
325
329
|
run.nodes.some((n) => n.kind === "video" && n.status === "done"));
|
|
326
330
|
}
|
|
331
|
+
function heldReason(run) {
|
|
332
|
+
const verdict = run.storyboardAutoApproval;
|
|
333
|
+
if (verdict?.outcome !== "held")
|
|
334
|
+
return undefined;
|
|
335
|
+
const reason = verdict.reason?.trim() ||
|
|
336
|
+
"The app didn't approve this storyboard by itself because its checks found a problem.";
|
|
337
|
+
return `${reason} (checked ${new Date(verdict.at).toISOString()})`;
|
|
338
|
+
}
|
|
327
339
|
export function classifyRun(run) {
|
|
328
340
|
if (run.status === "failed")
|
|
329
341
|
return { at: "failed", error: run.error };
|
|
@@ -343,6 +355,7 @@ export function classifyRun(run) {
|
|
|
343
355
|
}
|
|
344
356
|
const pausedNode = run.nodes.find((n) => n.nodeId === run.pausedNodeId);
|
|
345
357
|
if (pausedNode && parkedAtStoryboardGate(run, pausedNode)) {
|
|
358
|
+
const held = heldReason(run);
|
|
346
359
|
return {
|
|
347
360
|
at: "storyboard-gate",
|
|
348
361
|
nodeId: pausedNode.nodeId,
|
|
@@ -350,9 +363,15 @@ export function classifyRun(run) {
|
|
|
350
363
|
? { framesNodeId: pausedNode.nodeId }
|
|
351
364
|
: {}),
|
|
352
365
|
...(isShowAd(run) ? { showAd: true } : {}),
|
|
366
|
+
...(held ? { heldReason: held } : {}),
|
|
353
367
|
};
|
|
354
368
|
}
|
|
355
369
|
if (parkedAtFinalWatch(run)) {
|
|
370
|
+
const autoRedoScenes = run.nodes
|
|
371
|
+
.filter((n) => n.kind === "video")
|
|
372
|
+
.flatMap((n) => n.autoRedoScenes ?? []);
|
|
373
|
+
if (autoRedoScenes.length > 0)
|
|
374
|
+
return { at: "running", stage: "video", autoRedoScenes };
|
|
356
375
|
const missingScenes = run.nodes
|
|
357
376
|
.filter((n) => n.kind === "video")
|
|
358
377
|
.flatMap((n) => n.missingScenes ?? []);
|
|
@@ -371,6 +390,18 @@ export function classifyRun(run) {
|
|
|
371
390
|
export function hasAttachedCut(items) {
|
|
372
391
|
return items.some((i) => i.itemKind === "final" && i.status === "done");
|
|
373
392
|
}
|
|
393
|
+
export function scenesBeingRedone(run, items) {
|
|
394
|
+
const scenes = new Set(run.nodes.filter((n) => n.kind === "video").flatMap((n) => n.autoRedoScenes ?? []));
|
|
395
|
+
for (const i of items) {
|
|
396
|
+
if (i.itemKind === "clip" &&
|
|
397
|
+
i.status === "running" &&
|
|
398
|
+
(i.attempt ?? 1) > 1 &&
|
|
399
|
+
i.staleClaim !== true) {
|
|
400
|
+
scenes.add(i.sceneIndex);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return [...scenes].sort((a, b) => a - b);
|
|
404
|
+
}
|
|
374
405
|
export function resolveStop(stop, cutAttached) {
|
|
375
406
|
if (stop.at !== "final-watch")
|
|
376
407
|
return stop;
|
|
@@ -444,6 +475,7 @@ function missingSceneLines(missing, framesNodeId, runId, runUrl) {
|
|
|
444
475
|
export function stopLines(stop, runId, runUrl) {
|
|
445
476
|
if (stop.at === "storyboard-gate") {
|
|
446
477
|
const lines = [
|
|
478
|
+
...(stop.heldReason ? [stop.heldReason] : []),
|
|
447
479
|
"Parked: the storyboard is waiting for your yes.",
|
|
448
480
|
`Read it: exodus video storyboard ${runId}`,
|
|
449
481
|
`Approve it: exodus video approve ${runId}`,
|
|
@@ -515,8 +547,15 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
515
547
|
`Open it: ${runUrl}`,
|
|
516
548
|
];
|
|
517
549
|
}
|
|
550
|
+
if (stop.autoRedoScenes?.length)
|
|
551
|
+
return [autoRedoLine(stop.autoRedoScenes)];
|
|
518
552
|
return [`Working: ${stageWord(stop.stage)}.`];
|
|
519
553
|
}
|
|
554
|
+
function autoRedoLine(scenes) {
|
|
555
|
+
const one = scenes.length === 1;
|
|
556
|
+
return (`Working: the app is redoing ${scenesPhrase(scenes)} on ${one ? "its" : "their"} own because ` +
|
|
557
|
+
`${one ? "it" : "they"} repeated words. The ad isn't ready to watch until ${one ? "it lands" : "they land"}.`);
|
|
558
|
+
}
|
|
520
559
|
export function failedStoryboardNode(run) {
|
|
521
560
|
return run.nodes.find((n) => n.kind === "storyboard" && n.status === "failed");
|
|
522
561
|
}
|
|
@@ -852,6 +891,9 @@ export function planPull(run, items, opts) {
|
|
|
852
891
|
continue;
|
|
853
892
|
keyframeByScene.set(sceneIndex, keyframeDownload(sceneIndex, artifact.imageUrl));
|
|
854
893
|
}
|
|
894
|
+
const pendingRedo = scenesBeingRedone(run, items);
|
|
895
|
+
for (const sceneIndex of pendingRedo)
|
|
896
|
+
clipByScene.delete(sceneIndex);
|
|
855
897
|
const sceneIndexes = [
|
|
856
898
|
...new Set([
|
|
857
899
|
...clipByScene.keys(),
|
|
@@ -933,6 +975,7 @@ export function planPull(run, items, opts) {
|
|
|
933
975
|
musicHeardScenes: scenes
|
|
934
976
|
.filter((scene) => scene.findings.some((f) => f.code === MUSIC_HEARD_CODE))
|
|
935
977
|
.map((scene) => scene.sceneIndex),
|
|
978
|
+
pendingRedo,
|
|
936
979
|
cast,
|
|
937
980
|
narration: narrationDownload
|
|
938
981
|
? { file: narrationDownload.file, timing: "narration.json" }
|
|
@@ -1188,6 +1231,13 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1188
1231
|
return { kind: "usage", line: voiceFlag.line };
|
|
1189
1232
|
const videoModel = videoModelFlag.value;
|
|
1190
1233
|
const voiceMode = voiceFlag.value;
|
|
1234
|
+
const reviewStoryboard = occurrences.some((o) => o.flag === "review-storyboard");
|
|
1235
|
+
if (showId && reviewStoryboard) {
|
|
1236
|
+
return {
|
|
1237
|
+
kind: "usage",
|
|
1238
|
+
line: "--review-storyboard works with --conceit, not --show. A Show's ad always stops at the storyboard for you.",
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1191
1241
|
if (showId && (videoModel || voiceMode)) {
|
|
1192
1242
|
return {
|
|
1193
1243
|
kind: "usage",
|
|
@@ -1250,6 +1300,8 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1250
1300
|
opts.voiceMode = voiceMode;
|
|
1251
1301
|
if (musicChoice.music !== undefined)
|
|
1252
1302
|
opts.music = musicChoice.music;
|
|
1303
|
+
if (reviewStoryboard)
|
|
1304
|
+
opts.reviewStoryboard = true;
|
|
1253
1305
|
return { kind: "script", opts };
|
|
1254
1306
|
}
|
|
1255
1307
|
function isRecord(value) {
|
|
@@ -1337,6 +1389,7 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1337
1389
|
...(opts.videoModel ? { videoModel: opts.videoModel } : {}),
|
|
1338
1390
|
...(opts.voiceMode ? { voiceMode: opts.voiceMode } : {}),
|
|
1339
1391
|
...(opts.music === true ? { music: true } : {}),
|
|
1392
|
+
...(opts.reviewStoryboard ? { reviewStoryboard: true } : {}),
|
|
1340
1393
|
});
|
|
1341
1394
|
if (!res.ok)
|
|
1342
1395
|
return errorResult(res, opts.json);
|
|
@@ -1356,7 +1409,12 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1356
1409
|
: [
|
|
1357
1410
|
...scriptStartedLines(runId, url, card, video),
|
|
1358
1411
|
"",
|
|
1359
|
-
|
|
1412
|
+
...(opts.reviewStoryboard
|
|
1413
|
+
? ["Wait for the storyboard here instead: exodus video start … --wait"]
|
|
1414
|
+
: [
|
|
1415
|
+
"The app approves the storyboard itself if its checks pass. It only stops for you if they find a problem.",
|
|
1416
|
+
"Wait here until it needs you: exodus video start … --wait",
|
|
1417
|
+
]),
|
|
1360
1418
|
`Or check in whenever: exodus video status ${runId}`,
|
|
1361
1419
|
],
|
|
1362
1420
|
};
|
|
@@ -1375,7 +1433,8 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
1375
1433
|
const interval = opts.intervalMs ?? POLL_INTERVAL_MS;
|
|
1376
1434
|
const maxPolls = opts.maxPolls ?? MAX_POLLS;
|
|
1377
1435
|
const lines = [];
|
|
1378
|
-
let
|
|
1436
|
+
let lastProgress = null;
|
|
1437
|
+
let redoSeen = false;
|
|
1379
1438
|
for (let poll = 0; poll < maxPolls; poll++) {
|
|
1380
1439
|
if (poll > 0)
|
|
1381
1440
|
await deps.sleep(interval);
|
|
@@ -1387,14 +1446,35 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
1387
1446
|
const run = asVideoRun(res.data);
|
|
1388
1447
|
const stop = classifyRun(run);
|
|
1389
1448
|
if (stop.at === "running") {
|
|
1390
|
-
|
|
1391
|
-
|
|
1449
|
+
const progress = stop.autoRedoScenes?.length
|
|
1450
|
+
? autoRedoLine(stop.autoRedoScenes)
|
|
1451
|
+
: `Working: ${stageWord(stop.stage)}…`;
|
|
1452
|
+
if (progress !== lastProgress) {
|
|
1453
|
+
lastProgress = progress;
|
|
1392
1454
|
if (!opts.json)
|
|
1393
|
-
lines.push(
|
|
1455
|
+
lines.push(progress);
|
|
1394
1456
|
}
|
|
1395
1457
|
continue;
|
|
1396
1458
|
}
|
|
1397
|
-
|
|
1459
|
+
let items = null;
|
|
1460
|
+
if (stop.at === "final-watch") {
|
|
1461
|
+
const itemsRes = await deps.get(`${ITEMS_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1462
|
+
items = itemsRes.ok ? (itemsRes.data.items ?? []) : null;
|
|
1463
|
+
if (items === null && redoSeen)
|
|
1464
|
+
continue;
|
|
1465
|
+
const redoing = items ? scenesBeingRedone(run, items) : [];
|
|
1466
|
+
redoSeen = redoing.length > 0;
|
|
1467
|
+
if (redoSeen) {
|
|
1468
|
+
const progress = `Working: redoing scene${redoing.length === 1 ? "" : "s"} ${redoing.join(", ")}…`;
|
|
1469
|
+
if (progress !== lastProgress) {
|
|
1470
|
+
lastProgress = progress;
|
|
1471
|
+
if (!opts.json)
|
|
1472
|
+
lines.push(progress);
|
|
1473
|
+
}
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const resolved = resolveStop(stop, items ? hasAttachedCut(items) : null);
|
|
1398
1478
|
if (opts.json) {
|
|
1399
1479
|
return {
|
|
1400
1480
|
code: stop.at === "failed" ? 1 : 0,
|
|
@@ -1805,7 +1885,10 @@ export function planClipRedo(run, items, target) {
|
|
|
1805
1885
|
const { row, uploadedCut } = found;
|
|
1806
1886
|
const redoable = row.status === "failed" ||
|
|
1807
1887
|
row.staleClaim === true ||
|
|
1808
|
-
(row.status === "done" &&
|
|
1888
|
+
(row.status === "done" &&
|
|
1889
|
+
(row.flagged === true ||
|
|
1890
|
+
stop.at === "final-watch" ||
|
|
1891
|
+
(stop.at === "running" && stop.autoRedoScenes !== undefined)));
|
|
1809
1892
|
if (!redoable) {
|
|
1810
1893
|
if (row.status === "done") {
|
|
1811
1894
|
return {
|
|
@@ -2299,6 +2382,9 @@ export async function pullFlow(runId, dir, json, deps) {
|
|
|
2299
2382
|
`Every piece is indexed in ${path.join(dir, "manifest.json")} — scene numbers there are the run's own.`,
|
|
2300
2383
|
...musicLines(plan.manifest),
|
|
2301
2384
|
];
|
|
2385
|
+
for (const sceneIndex of plan.manifest.pendingRedo) {
|
|
2386
|
+
lines.push("", `Scene ${sceneIndex} is being redone right now; its file will change. Run \`exodus video wait ${runId}\`, then pull again.`);
|
|
2387
|
+
}
|
|
2302
2388
|
const voiceFirst = plan.manifest.scenes.filter((s) => s.voiceMode === "voice-first");
|
|
2303
2389
|
if (voiceFirst.length > 0) {
|
|
2304
2390
|
lines.push("", `Redone voice first (the line recorded, then the clip made to match it): ` +
|
|
@@ -378,6 +378,9 @@ export function formatRulesLine(provenance) {
|
|
|
378
378
|
return `Format rules: ${rigName}${version}${where}.${borrowed}`;
|
|
379
379
|
}
|
|
380
380
|
export function pauseAheadLine(ahead) {
|
|
381
|
+
if (ahead.after === "frames" && ahead.selfApproves) {
|
|
382
|
+
return "When the frames are ready, the app checks the storyboard and approves it itself if the checks pass, then goes on to the voices and clips. It only stops for your approval if a check finds a problem. The reference, cast and scene pictures are made first (a few cents each).";
|
|
383
|
+
}
|
|
381
384
|
return ahead.after === "frames"
|
|
382
385
|
? "This run will pause for your approval once the frames are ready. The reference, cast and scene pictures are made first (a few cents each). Nothing bigger is spent until you approve."
|
|
383
386
|
: "This run will pause for your approval once the storyboard is written. Nothing is spent on pictures, voices or clips until you approve.";
|
|
@@ -2088,6 +2091,7 @@ function videoParkStop(raw) {
|
|
|
2088
2091
|
async function waitForRun(runId, opts, deps) {
|
|
2089
2092
|
const seen = new Map();
|
|
2090
2093
|
let pausedNotified = false;
|
|
2094
|
+
let autoRedoNotified;
|
|
2091
2095
|
const landOnPark = opts.landOnPark;
|
|
2092
2096
|
const isLandingPark = (raw) => (landOnPark !== undefined && raw["pauseReason"] === landOnPark.pauseReason) ||
|
|
2093
2097
|
videoParkStop(raw) !== undefined;
|
|
@@ -2105,7 +2109,15 @@ async function waitForRun(runId, opts, deps) {
|
|
|
2105
2109
|
return;
|
|
2106
2110
|
const parked = isParkedSnapshot(raw);
|
|
2107
2111
|
const isLanding = isLandingPark(raw);
|
|
2108
|
-
|
|
2112
|
+
const autoRedo = parked ? classifyRun(asVideoRun(raw)) : undefined;
|
|
2113
|
+
if (autoRedo?.at === "running" && autoRedo.autoRedoScenes) {
|
|
2114
|
+
const line = ` ${stopLines(autoRedo, runId, "")[0]}`;
|
|
2115
|
+
if (line !== autoRedoNotified) {
|
|
2116
|
+
autoRedoNotified = line;
|
|
2117
|
+
opts.onProgressLine(line);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
else if (parked && !pausedNotified && !isLanding) {
|
|
2109
2121
|
pausedNotified = true;
|
|
2110
2122
|
const dashboardUrl = deps.dashboardUrl ?? getDashboardUrl();
|
|
2111
2123
|
const pauseReason = raw["pauseReason"];
|
|
@@ -2113,7 +2125,9 @@ async function waitForRun(runId, opts, deps) {
|
|
|
2113
2125
|
opts.onProgressLine(line);
|
|
2114
2126
|
}
|
|
2115
2127
|
}
|
|
2116
|
-
const parkedNodeId =
|
|
2128
|
+
const parkedNodeId = autoRedo?.at === "running"
|
|
2129
|
+
? undefined
|
|
2130
|
+
: gateParkedNodeId(raw["status"], raw["pauseReason"], raw["pausedNodeId"]);
|
|
2117
2131
|
const nodes = Array.isArray(raw["nodes"]) ? raw["nodes"] : [];
|
|
2118
2132
|
for (const node of nodes) {
|
|
2119
2133
|
const isParked = parkedNodeId !== undefined && node.nodeId === parkedNodeId;
|