@aicopycoders/exodus 2026.9.2000-beta.3 → 2026.9.2000-beta.5
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 +5 -0
- package/dist/commands/image.js +1 -1
- package/dist/commands/template.js +11 -4
- package/dist/commands/video.d.ts +17 -0
- package/dist/commands/video.js +116 -8
- package/dist/commands/workflow.js +41 -3
- package/package.json +1 -1
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -152,7 +152,8 @@ run options:
|
|
|
152
152
|
--mode auto|manual|hybrid Generation mode (default: auto)
|
|
153
153
|
--render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
|
|
154
154
|
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
155
|
-
--model gpt-image-2|
|
|
155
|
+
--model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
|
|
156
|
+
Kie.ai model (default: gpt-image-2.5)
|
|
156
157
|
--realism off|realistic Realism enforcement (default: off)
|
|
157
158
|
--quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
158
159
|
--pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
|
|
@@ -244,6 +244,7 @@ internals into chat.
|
|
|
244
244
|
- The rig's child image runs are still `creativeSuiteRuns` with engine `meme`,
|
|
245
245
|
so finished memes appear in the creative-suite library alongside other engines
|
|
246
246
|
and `exodus browse --agent meme` still covers them.
|
|
247
|
-
- `run` accepts only the
|
|
248
|
-
`nano-banana-pro`,
|
|
249
|
-
|
|
247
|
+
- `run` accepts only the models the rig renders (`gpt-image-2.5`, the house
|
|
248
|
+
default, plus `gpt-image-2.5-sunburst`, `gpt-image-2` and `nano-banana-pro`,
|
|
249
|
+
or their Kie slugs); anything else is a 400 listing the accepted values. It
|
|
250
|
+
used to accept any value and silently ignore it.
|
|
@@ -17,7 +17,8 @@ run options:
|
|
|
17
17
|
--mode auto|manual|hybrid Generation mode (default: auto)
|
|
18
18
|
--render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
|
|
19
19
|
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
20
|
-
--model gpt-image-2|
|
|
20
|
+
--model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
|
|
21
|
+
Kie.ai model (default: gpt-image-2.5)
|
|
21
22
|
--realism off|realistic Realism enforcement (default: off)
|
|
22
23
|
--quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
23
24
|
--pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
|
|
@@ -115,6 +115,11 @@ under it:
|
|
|
115
115
|
`exodus workflow checkpoint <runId>` is the verb for that one.
|
|
116
116
|
- failed: `status` shows how far it got, and a `--wait` that was running exits
|
|
117
117
|
1 with the error. Tell the user; do not start another run on your own.
|
|
118
|
+
When the storyboard is the step that failed, `status` adds a "What happened
|
|
119
|
+
to the storyboard" block: which parts of the plan were accepted, which part
|
|
120
|
+
was turned down, and why (#1902). Relay it as written. On an admin key it may
|
|
121
|
+
also offer `status <runId> --rejected-draft`. That prints the draft the system
|
|
122
|
+
threw away. It is a record only: never build on it, edit it, or re-run it.
|
|
118
123
|
- finished: pull what it made.
|
|
119
124
|
|
|
120
125
|
**`--wait` stops at both video parks** (#1818). `workflow run --wait` ends at
|
package/dist/commands/image.js
CHANGED
|
@@ -184,7 +184,7 @@ async function runTemplate(text, flags, who) {
|
|
|
184
184
|
renderMode: "images",
|
|
185
185
|
inputAds: text,
|
|
186
186
|
aspectRatio: aspect,
|
|
187
|
-
model: "gpt-image-2",
|
|
187
|
+
model: "gpt-image-2.5",
|
|
188
188
|
realismMode: realisticFlag ? "realistic" : "off",
|
|
189
189
|
...(steering ? { steering } : {}),
|
|
190
190
|
...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
|
|
@@ -17,7 +17,8 @@ Run options:
|
|
|
17
17
|
--mode auto|manual Generation mode (default: auto)
|
|
18
18
|
--render-mode images|prompts Skip render and return prompts only (default: images)
|
|
19
19
|
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
20
|
-
--model gpt-image-2|
|
|
20
|
+
--model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
|
|
21
|
+
Kie.ai model (default: gpt-image-2.5)
|
|
21
22
|
--realism off|realistic Realism enforcement (default: off)
|
|
22
23
|
--quantities <slug:N,slug:N> Manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
23
24
|
--requested-count N Auto-mode total render target (optional)
|
|
@@ -103,9 +104,15 @@ async function runTemplate(flags) {
|
|
|
103
104
|
process.exit(1);
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const TEMPLATE_MODELS = [
|
|
108
|
+
"gpt-image-2.5",
|
|
109
|
+
"gpt-image-2.5-sunburst",
|
|
110
|
+
"gpt-image-2",
|
|
111
|
+
"nano-banana-pro",
|
|
112
|
+
];
|
|
113
|
+
const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2.5";
|
|
114
|
+
if (!TEMPLATE_MODELS.includes(model)) {
|
|
115
|
+
console.error(`Error: --model must be one of ${TEMPLATE_MODELS.join(" | ")} (got "${model}").`);
|
|
109
116
|
process.exit(1);
|
|
110
117
|
return;
|
|
111
118
|
}
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -69,12 +69,23 @@ export type ArtifactSubset = {
|
|
|
69
69
|
} | {
|
|
70
70
|
type: "text" | "primer" | "session" | "document";
|
|
71
71
|
};
|
|
72
|
+
export interface PlanFailureRecord {
|
|
73
|
+
kind: string;
|
|
74
|
+
partsTotal?: number;
|
|
75
|
+
acceptedParts?: number;
|
|
76
|
+
acceptedScenes?: number;
|
|
77
|
+
failedPart?: number;
|
|
78
|
+
reasons?: string[];
|
|
79
|
+
lines: string[];
|
|
80
|
+
}
|
|
72
81
|
export interface VideoRunNode {
|
|
73
82
|
nodeId: string;
|
|
74
83
|
kind: string;
|
|
75
84
|
status: "idle" | "running" | "done" | "failed" | "skipped" | "out-of-scope";
|
|
76
85
|
error?: string;
|
|
77
86
|
warning?: string;
|
|
87
|
+
planFailure?: PlanFailureRecord;
|
|
88
|
+
hasRejectedDraft?: boolean;
|
|
78
89
|
outputs?: ArtifactSubset[];
|
|
79
90
|
}
|
|
80
91
|
export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
|
|
@@ -173,6 +184,7 @@ export type RunStop = {
|
|
|
173
184
|
at: "storyboard-gate";
|
|
174
185
|
nodeId?: string;
|
|
175
186
|
framesNodeId?: string;
|
|
187
|
+
showAd?: true;
|
|
176
188
|
} | {
|
|
177
189
|
at: "final-watch";
|
|
178
190
|
} | {
|
|
@@ -201,8 +213,12 @@ export declare function resolveStop(stop: RunStop, cutAttached: boolean | null):
|
|
|
201
213
|
export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
|
|
202
214
|
export declare function stageWord(stage: string): string;
|
|
203
215
|
export declare function stepName(kind: string | undefined): string;
|
|
216
|
+
export declare function isShowAd(run: Pick<VideoRun, "moduleOwned">): boolean;
|
|
204
217
|
export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
|
|
205
218
|
export declare function stopLines(stop: ResolvedStop, runId: string, runUrl: string): string[];
|
|
219
|
+
export declare function failedStoryboardNode(run: VideoRun): VideoRunNode | undefined;
|
|
220
|
+
export declare function errorEchoesReasons(error: string | undefined, record: PlanFailureRecord | undefined): boolean;
|
|
221
|
+
export declare function planFailureLines(node: VideoRunNode | undefined, runId: string): string[];
|
|
206
222
|
export interface PullDownload {
|
|
207
223
|
file: string;
|
|
208
224
|
url: string;
|
|
@@ -292,6 +308,7 @@ export declare function waitFlow(runId: string, opts: {
|
|
|
292
308
|
maxPolls?: number;
|
|
293
309
|
}, deps: VideoDeps): Promise<FlowResult>;
|
|
294
310
|
export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
311
|
+
export declare function rejectedDraftFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
295
312
|
export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
296
313
|
export declare function approveFlow(runId: string, opts: {
|
|
297
314
|
json: boolean;
|
package/dist/commands/video.js
CHANGED
|
@@ -41,7 +41,13 @@ The whole loop, in order:
|
|
|
41
41
|
Redo ONE still at the pixel gate. Neighbours stay. The gate holds.
|
|
42
42
|
|
|
43
43
|
7. exodus video status <runId>
|
|
44
|
-
Where the run is and how each scene's clip turned out.
|
|
44
|
+
Where the run is and how each scene's clip turned out. If the storyboard
|
|
45
|
+
failed, it also says which parts of it were accepted and why the next one
|
|
46
|
+
was turned down.
|
|
47
|
+
|
|
48
|
+
exodus video status <runId> --rejected-draft
|
|
49
|
+
Prints the storyboard draft that was turned down. A record of what
|
|
50
|
+
happened, thrown away by the system — never something to act on.
|
|
45
51
|
|
|
46
52
|
8. exodus video pull <runId> --out ./ad
|
|
47
53
|
Writes every piece to that folder plus a manifest.json index.
|
|
@@ -67,7 +73,7 @@ The whole loop, in order:
|
|
|
67
73
|
or add --approve-stale-cut to deliver the cut you already uploaded.
|
|
68
74
|
|
|
69
75
|
Usage:
|
|
70
|
-
exodus video status <runId> [--json]
|
|
76
|
+
exodus video status <runId> [--rejected-draft] [--json]
|
|
71
77
|
exodus video storyboard <runId> [--json]
|
|
72
78
|
exodus video approve <runId> [--approve-stale-cut] [--json]
|
|
73
79
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
@@ -99,6 +105,8 @@ Options:
|
|
|
99
105
|
--clear <who> Take a character's voice off again (voices). Repeatable
|
|
100
106
|
--from <file.json> A file of characters and voice IDs (voices). --set and
|
|
101
107
|
--clear win over the same name in the file
|
|
108
|
+
--rejected-draft Print the storyboard draft the system turned down
|
|
109
|
+
(status). It was thrown away — a record only
|
|
102
110
|
--json Machine-readable output
|
|
103
111
|
--help, -h Print this help
|
|
104
112
|
|
|
@@ -124,6 +132,9 @@ const SHOWS_PATH = "/api/v2/shows";
|
|
|
124
132
|
const RUNS_PATH = "/api/v2/video/runs";
|
|
125
133
|
const RUN_PATH = "/api/v2/workflow";
|
|
126
134
|
const ITEMS_PATH = "/api/v2/workflow/items";
|
|
135
|
+
const REJECTED_DRAFT_PATH = "/api/v2/workflow/rejected-draft";
|
|
136
|
+
const REJECTED_DRAFT_NOT_ON_THIS_SERVER = "This Exodus server does not keep rejected drafts yet, so there is nothing to read. It arrives " +
|
|
137
|
+
"with the next server update.";
|
|
127
138
|
const STORYBOARD_PATH = "/api/v2/video/storyboard";
|
|
128
139
|
const FLAG_PATH = "/api/v2/video/storyboard/flag";
|
|
129
140
|
const APPROVE_PATH = "/api/v2/workflow/approve";
|
|
@@ -250,6 +261,7 @@ export function classifyRun(run) {
|
|
|
250
261
|
...(pausedNode.kind === "scene-frames"
|
|
251
262
|
? { framesNodeId: pausedNode.nodeId }
|
|
252
263
|
: {}),
|
|
264
|
+
...(isShowAd(run) ? { showAd: true } : {}),
|
|
253
265
|
};
|
|
254
266
|
}
|
|
255
267
|
if (parkedAtFinalWatch(run))
|
|
@@ -300,8 +312,11 @@ export function stepName(kind) {
|
|
|
300
312
|
return "A step in this run";
|
|
301
313
|
return STEP_NAMES[kind] ?? `The ${kind} step`;
|
|
302
314
|
}
|
|
315
|
+
export function isShowAd(run) {
|
|
316
|
+
return run.moduleOwned === true;
|
|
317
|
+
}
|
|
303
318
|
export function reviewUrl(dashboardUrl, run) {
|
|
304
|
-
if (run
|
|
319
|
+
if (isShowAd(run))
|
|
305
320
|
return `${dashboardUrl}/video?ad=${run._id}`;
|
|
306
321
|
if (run.workflowId)
|
|
307
322
|
return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
|
|
@@ -313,8 +328,10 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
313
328
|
"Parked: the storyboard is waiting for your yes.",
|
|
314
329
|
`Read it: exodus video storyboard ${runId}`,
|
|
315
330
|
`Approve it: exodus video approve ${runId}`,
|
|
316
|
-
`Send back: exodus video flag ${runId} --note "what's wrong"`,
|
|
317
331
|
];
|
|
332
|
+
if (stop.showAd) {
|
|
333
|
+
lines.push(`Send back: exodus video flag ${runId} --note "what's wrong"`);
|
|
334
|
+
}
|
|
318
335
|
if (stop.framesNodeId) {
|
|
319
336
|
lines.push(`Redo one frame: exodus video retry-frame ${runId} --node ${stop.framesNodeId} --scene <n>`);
|
|
320
337
|
}
|
|
@@ -371,6 +388,30 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
371
388
|
}
|
|
372
389
|
return [`Working: ${stageWord(stop.stage)}.`];
|
|
373
390
|
}
|
|
391
|
+
export function failedStoryboardNode(run) {
|
|
392
|
+
return run.nodes.find((n) => n.kind === "storyboard" && n.status === "failed");
|
|
393
|
+
}
|
|
394
|
+
export function errorEchoesReasons(error, record) {
|
|
395
|
+
if (!error || record?.kind !== "rejected")
|
|
396
|
+
return false;
|
|
397
|
+
const reasons = record.reasons ?? [];
|
|
398
|
+
return reasons.length > 0 && reasons.every((reason) => error.includes(reason));
|
|
399
|
+
}
|
|
400
|
+
export function planFailureLines(node, runId) {
|
|
401
|
+
const record = node?.planFailure;
|
|
402
|
+
if (!record || !Array.isArray(record.lines) || !node)
|
|
403
|
+
return [];
|
|
404
|
+
const reasons = record.kind === "rejected" ? (record.reasons ?? []) : [];
|
|
405
|
+
const bodyCount = Math.max(0, record.lines.length - reasons.length);
|
|
406
|
+
const lines = [`What happened to the storyboard step (${node.nodeId}):`];
|
|
407
|
+
record.lines.forEach((line, i) => {
|
|
408
|
+
lines.push(i < bodyCount ? ` ${line}` : ` - ${line}`);
|
|
409
|
+
});
|
|
410
|
+
if (node?.hasRejectedDraft === true) {
|
|
411
|
+
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`);
|
|
412
|
+
}
|
|
413
|
+
return lines;
|
|
414
|
+
}
|
|
374
415
|
export const CAST_LEDGER_BASE = 910000;
|
|
375
416
|
const FALLBACK_EXT = {
|
|
376
417
|
image: "png",
|
|
@@ -977,7 +1018,10 @@ export async function statusFlow(runId, json, deps) {
|
|
|
977
1018
|
const items = itemsRes.data.items ?? [];
|
|
978
1019
|
const hasFinal = hasAttachedCut(items);
|
|
979
1020
|
const stop = resolveStop(classifyRun(run), hasFinal);
|
|
980
|
-
const
|
|
1021
|
+
const failedStoryboard = failedStoryboardNode(run);
|
|
1022
|
+
const guidance = stopLines(stop.at === "failed" && errorEchoesReasons(stop.error, failedStoryboard?.planFailure)
|
|
1023
|
+
? { ...stop, error: undefined }
|
|
1024
|
+
: stop, runId, reviewUrl(deps.dashboardUrl, run));
|
|
981
1025
|
const warnings = run.nodes
|
|
982
1026
|
.filter((n) => n.warning)
|
|
983
1027
|
.map((n) => ({ nodeId: n.nodeId, step: n.kind, warning: n.warning }));
|
|
@@ -985,7 +1029,20 @@ export async function statusFlow(runId, json, deps) {
|
|
|
985
1029
|
return {
|
|
986
1030
|
code: 0,
|
|
987
1031
|
lines: [
|
|
988
|
-
JSON.stringify({
|
|
1032
|
+
JSON.stringify({
|
|
1033
|
+
runId,
|
|
1034
|
+
status: run.status,
|
|
1035
|
+
stop,
|
|
1036
|
+
items,
|
|
1037
|
+
hasFinal,
|
|
1038
|
+
warnings,
|
|
1039
|
+
guidance,
|
|
1040
|
+
...(failedStoryboard?.planFailure ? { planFailure: failedStoryboard.planFailure } : {}),
|
|
1041
|
+
...(failedStoryboard?.planFailure
|
|
1042
|
+
? { storyboardNodeId: failedStoryboard.nodeId }
|
|
1043
|
+
: {}),
|
|
1044
|
+
...(failedStoryboard?.hasRejectedDraft === true ? { hasRejectedDraft: true } : {}),
|
|
1045
|
+
}),
|
|
989
1046
|
],
|
|
990
1047
|
};
|
|
991
1048
|
}
|
|
@@ -1020,6 +1077,7 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1020
1077
|
...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
|
|
1021
1078
|
...guidance,
|
|
1022
1079
|
...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
|
|
1080
|
+
...planFailureLines(failedStoryboard, runId),
|
|
1023
1081
|
...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
|
|
1024
1082
|
];
|
|
1025
1083
|
if (byScene.size === 0) {
|
|
@@ -1068,6 +1126,45 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1068
1126
|
: "Final cut: not uploaded yet.");
|
|
1069
1127
|
return { code: 0, lines };
|
|
1070
1128
|
}
|
|
1129
|
+
export async function rejectedDraftFlow(runId, json, deps) {
|
|
1130
|
+
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1131
|
+
if (!runRes.ok)
|
|
1132
|
+
return errorResult(runRes, json);
|
|
1133
|
+
const node = failedStoryboardNode(asVideoRun(runRes.data));
|
|
1134
|
+
if (!node) {
|
|
1135
|
+
const note = "The storyboard on this run didn't fail, so there is no rejected draft to read.";
|
|
1136
|
+
return {
|
|
1137
|
+
code: 0,
|
|
1138
|
+
lines: json ? [JSON.stringify({ runId, nodeId: null, draft: null })] : [note],
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
const res = await deps.get(`${REJECTED_DRAFT_PATH}?runId=${encodeURIComponent(runId)}&nodeId=${encodeURIComponent(node.nodeId)}`);
|
|
1142
|
+
if (missingRouteLine(res, "exodus video status --rejected-draft")) {
|
|
1143
|
+
return {
|
|
1144
|
+
code: 1,
|
|
1145
|
+
lines: json
|
|
1146
|
+
? [JSON.stringify({ ok: false, status: 404, error: REJECTED_DRAFT_NOT_ON_THIS_SERVER })]
|
|
1147
|
+
: [REJECTED_DRAFT_NOT_ON_THIS_SERVER],
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (!res.ok)
|
|
1151
|
+
return errorResult(res, json);
|
|
1152
|
+
const draft = res.data.draft ?? null;
|
|
1153
|
+
if (json)
|
|
1154
|
+
return { code: 0, lines: [JSON.stringify({ runId, nodeId: node.nodeId, draft })] };
|
|
1155
|
+
if (!draft)
|
|
1156
|
+
return { code: 0, lines: ["No rejected draft was kept for this attempt."] };
|
|
1157
|
+
return {
|
|
1158
|
+
code: 0,
|
|
1159
|
+
lines: [
|
|
1160
|
+
`REJECTED DRAFT — ad run ${runId}, step ${node.nodeId}`,
|
|
1161
|
+
"The system turned this draft down and threw it away. It is kept only as a record of what happened.",
|
|
1162
|
+
"Nothing in it is part of your ad. There is nothing here to act on or edit.",
|
|
1163
|
+
"",
|
|
1164
|
+
...draft.split("\n"),
|
|
1165
|
+
],
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1071
1168
|
export async function storyboardFlow(runId, json, deps) {
|
|
1072
1169
|
const res = await deps.get(`${STORYBOARD_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1073
1170
|
if (!res.ok)
|
|
@@ -1119,7 +1216,14 @@ export async function storyboardFlow(runId, json, deps) {
|
|
|
1119
1216
|
}
|
|
1120
1217
|
}
|
|
1121
1218
|
}
|
|
1122
|
-
|
|
1219
|
+
const runRes = await deps
|
|
1220
|
+
.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`)
|
|
1221
|
+
.catch(() => null);
|
|
1222
|
+
const showAd = runRes?.ok === true && isShowAd(asVideoRun(runRes.data));
|
|
1223
|
+
lines.push("", `approve with: exodus video approve ${runId}`);
|
|
1224
|
+
if (showAd) {
|
|
1225
|
+
lines.push(`send it back: exodus video flag ${runId} --note "what's wrong"`);
|
|
1226
|
+
}
|
|
1123
1227
|
if (cards.framesNodeId) {
|
|
1124
1228
|
lines.push(`redo one frame: exodus video retry-frame ${runId} --node ${cards.framesNodeId} --scene <n>`);
|
|
1125
1229
|
}
|
|
@@ -2053,8 +2157,12 @@ export async function run(flags, occurrences) {
|
|
|
2053
2157
|
if (needsRunId.includes(sub) && !runId) {
|
|
2054
2158
|
usage(`video ${sub} needs a run id: exodus video ${sub} <runId>`);
|
|
2055
2159
|
}
|
|
2056
|
-
if (sub === "status")
|
|
2160
|
+
if (sub === "status") {
|
|
2161
|
+
if (flags["rejected-draft"] !== undefined && flags["rejected-draft"] !== false) {
|
|
2162
|
+
return printResult(await rejectedDraftFlow(runId, json, defaultDeps));
|
|
2163
|
+
}
|
|
2057
2164
|
return printResult(await statusFlow(runId, json, defaultDeps));
|
|
2165
|
+
}
|
|
2058
2166
|
if (sub === "storyboard")
|
|
2059
2167
|
return printResult(await storyboardFlow(runId, json, defaultDeps));
|
|
2060
2168
|
if (sub === "approve") {
|
|
@@ -10,7 +10,7 @@ import { runDisplayName, runHeadingWord, runTypeWord } from "../lib/runNames.js"
|
|
|
10
10
|
import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
|
|
11
11
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
12
12
|
import { getChannel } from "../lib/channel.js";
|
|
13
|
-
import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, stopLines, } from "./video.js";
|
|
13
|
+
import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, scenePrefix, stopLines, } from "./video.js";
|
|
14
14
|
export const helpText = `
|
|
15
15
|
exodus workflow — List, describe, run, inspect, import, and export saved workflows
|
|
16
16
|
|
|
@@ -1382,6 +1382,43 @@ function planArtifactFile(artifact) {
|
|
|
1382
1382
|
url,
|
|
1383
1383
|
};
|
|
1384
1384
|
}
|
|
1385
|
+
function artifactFileSuffixes(artifacts) {
|
|
1386
|
+
const sceneBearing = artifacts.some((a) => typeof a.sceneIndex === "number" || a.final === true);
|
|
1387
|
+
if (!sceneBearing) {
|
|
1388
|
+
return artifacts.map((_, i) => ({
|
|
1389
|
+
suffix: i === 0 ? "" : `-${i + 1}`,
|
|
1390
|
+
renamed: false,
|
|
1391
|
+
}));
|
|
1392
|
+
}
|
|
1393
|
+
const taken = new Set();
|
|
1394
|
+
let fullCount = 0;
|
|
1395
|
+
return artifacts.map((a, i) => {
|
|
1396
|
+
let suffix;
|
|
1397
|
+
if (typeof a.sceneIndex === "number") {
|
|
1398
|
+
suffix = `-${scenePrefix(a.sceneIndex)}`;
|
|
1399
|
+
}
|
|
1400
|
+
else if (a.final === true) {
|
|
1401
|
+
suffix = "-final";
|
|
1402
|
+
}
|
|
1403
|
+
else if (a.type === "audio") {
|
|
1404
|
+
fullCount += 1;
|
|
1405
|
+
suffix = fullCount === 1 ? "-full" : `-full-${fullCount}`;
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
suffix = i === 0 ? "" : `-${i + 1}`;
|
|
1409
|
+
}
|
|
1410
|
+
if (taken.has(suffix)) {
|
|
1411
|
+
let fallback = `${suffix}-${i + 1}`;
|
|
1412
|
+
while (taken.has(fallback))
|
|
1413
|
+
fallback = `${fallback}-${i + 1}`;
|
|
1414
|
+
suffix = fallback;
|
|
1415
|
+
taken.add(suffix);
|
|
1416
|
+
return { suffix, renamed: true };
|
|
1417
|
+
}
|
|
1418
|
+
taken.add(suffix);
|
|
1419
|
+
return { suffix, renamed: false };
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1385
1422
|
export async function saveDeliveries(run, dir, deps) {
|
|
1386
1423
|
const paths = [];
|
|
1387
1424
|
if (!run.isTerminal) {
|
|
@@ -1416,9 +1453,10 @@ export async function saveDeliveries(run, dir, deps) {
|
|
|
1416
1453
|
lines.push(` skipped ${delivery.label} (${delivery.key}) — ${why}`);
|
|
1417
1454
|
continue;
|
|
1418
1455
|
}
|
|
1456
|
+
const suffixes = artifactFileSuffixes(delivery.artifacts);
|
|
1419
1457
|
for (let i = 0; i < delivery.artifacts.length; i++) {
|
|
1420
1458
|
const plan = planArtifactFile(delivery.artifacts[i]);
|
|
1421
|
-
const suffix
|
|
1459
|
+
const { suffix, renamed } = suffixes[i];
|
|
1422
1460
|
if (plan.kind === "none") {
|
|
1423
1461
|
lines.push(` skipped ${delivery.label} (${delivery.key})${suffix} — ${plan.reason}`);
|
|
1424
1462
|
continue;
|
|
@@ -1429,7 +1467,7 @@ export async function saveDeliveries(run, dir, deps) {
|
|
|
1429
1467
|
deps.writeFile(file, plan.body);
|
|
1430
1468
|
else
|
|
1431
1469
|
await downloadToFile(plan.url, file);
|
|
1432
|
-
lines.push(` wrote ${file}`);
|
|
1470
|
+
lines.push(` wrote ${file}${renamed ? " — renamed to avoid a collision" : ""}`);
|
|
1433
1471
|
paths.push(file);
|
|
1434
1472
|
}
|
|
1435
1473
|
catch (e) {
|