@aicopycoders/exodus 2026.9.2000-beta.2 → 2026.9.2000-beta.4
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/dist/commands/image.js +1 -1
- package/dist/commands/template.js +11 -4
- package/dist/commands/video.d.ts +7 -2
- package/dist/commands/video.js +60 -17
- package/dist/commands/workflow.d.ts +51 -3
- package/dist/commands/workflow.js +60 -8
- 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"}]'
|
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
|
@@ -72,7 +72,7 @@ export type ArtifactSubset = {
|
|
|
72
72
|
export interface VideoRunNode {
|
|
73
73
|
nodeId: string;
|
|
74
74
|
kind: string;
|
|
75
|
-
status: "idle" | "running" | "done" | "failed" | "skipped";
|
|
75
|
+
status: "idle" | "running" | "done" | "failed" | "skipped" | "out-of-scope";
|
|
76
76
|
error?: string;
|
|
77
77
|
warning?: string;
|
|
78
78
|
outputs?: ArtifactSubset[];
|
|
@@ -173,6 +173,7 @@ export type RunStop = {
|
|
|
173
173
|
at: "storyboard-gate";
|
|
174
174
|
nodeId?: string;
|
|
175
175
|
framesNodeId?: string;
|
|
176
|
+
showAd?: true;
|
|
176
177
|
} | {
|
|
177
178
|
at: "final-watch";
|
|
178
179
|
} | {
|
|
@@ -201,6 +202,7 @@ export declare function resolveStop(stop: RunStop, cutAttached: boolean | null):
|
|
|
201
202
|
export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
|
|
202
203
|
export declare function stageWord(stage: string): string;
|
|
203
204
|
export declare function stepName(kind: string | undefined): string;
|
|
205
|
+
export declare function isShowAd(run: Pick<VideoRun, "moduleOwned">): boolean;
|
|
204
206
|
export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
|
|
205
207
|
export declare function stopLines(stop: ResolvedStop, runId: string, runUrl: string): string[];
|
|
206
208
|
export interface PullDownload {
|
|
@@ -293,7 +295,10 @@ export declare function waitFlow(runId: string, opts: {
|
|
|
293
295
|
}, deps: VideoDeps): Promise<FlowResult>;
|
|
294
296
|
export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
295
297
|
export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
296
|
-
export declare function approveFlow(runId: string,
|
|
298
|
+
export declare function approveFlow(runId: string, opts: {
|
|
299
|
+
json: boolean;
|
|
300
|
+
approveStaleCut: boolean;
|
|
301
|
+
}, deps: VideoDeps): Promise<FlowResult>;
|
|
297
302
|
export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
298
303
|
export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
299
304
|
export type ClipRedoPlan = {
|
package/dist/commands/video.js
CHANGED
|
@@ -62,12 +62,14 @@ The whole loop, in order:
|
|
|
62
62
|
Attaches your cut to the run and prints the page to approve it on.
|
|
63
63
|
|
|
64
64
|
11. exodus video approve <runId>
|
|
65
|
-
Or click Approve on the page from step 10.
|
|
65
|
+
Or click Approve on the page from step 10. If you redid a clip after
|
|
66
|
+
uploading, this stops and says which scenes changed. Upload a fresh cut,
|
|
67
|
+
or add --approve-stale-cut to deliver the cut you already uploaded.
|
|
66
68
|
|
|
67
69
|
Usage:
|
|
68
70
|
exodus video status <runId> [--json]
|
|
69
71
|
exodus video storyboard <runId> [--json]
|
|
70
|
-
exodus video approve <runId> [--json]
|
|
72
|
+
exodus video approve <runId> [--approve-stale-cut] [--json]
|
|
71
73
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
72
74
|
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--json]
|
|
73
75
|
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
|
|
@@ -88,6 +90,9 @@ Options:
|
|
|
88
90
|
(retry-frame, retry-clip, revoice)
|
|
89
91
|
--all Every finished clip that kept the video model's voice
|
|
90
92
|
(revoice)
|
|
93
|
+
--approve-stale-cut Deliver the cut you uploaded even though a clip changed
|
|
94
|
+
after you uploaded it (approve). Without it, approving
|
|
95
|
+
stops and names the scenes that changed
|
|
91
96
|
--set <who>=<id> Give a character an ElevenLabs voice (voices). Name the
|
|
92
97
|
character by its ID or by the name your script uses.
|
|
93
98
|
Repeat it once per character
|
|
@@ -245,6 +250,7 @@ export function classifyRun(run) {
|
|
|
245
250
|
...(pausedNode.kind === "scene-frames"
|
|
246
251
|
? { framesNodeId: pausedNode.nodeId }
|
|
247
252
|
: {}),
|
|
253
|
+
...(isShowAd(run) ? { showAd: true } : {}),
|
|
248
254
|
};
|
|
249
255
|
}
|
|
250
256
|
if (parkedAtFinalWatch(run))
|
|
@@ -295,8 +301,11 @@ export function stepName(kind) {
|
|
|
295
301
|
return "A step in this run";
|
|
296
302
|
return STEP_NAMES[kind] ?? `The ${kind} step`;
|
|
297
303
|
}
|
|
304
|
+
export function isShowAd(run) {
|
|
305
|
+
return run.moduleOwned === true;
|
|
306
|
+
}
|
|
298
307
|
export function reviewUrl(dashboardUrl, run) {
|
|
299
|
-
if (run
|
|
308
|
+
if (isShowAd(run))
|
|
300
309
|
return `${dashboardUrl}/video?ad=${run._id}`;
|
|
301
310
|
if (run.workflowId)
|
|
302
311
|
return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
|
|
@@ -308,8 +317,10 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
308
317
|
"Parked: the storyboard is waiting for your yes.",
|
|
309
318
|
`Read it: exodus video storyboard ${runId}`,
|
|
310
319
|
`Approve it: exodus video approve ${runId}`,
|
|
311
|
-
`Send back: exodus video flag ${runId} --note "what's wrong"`,
|
|
312
320
|
];
|
|
321
|
+
if (stop.showAd) {
|
|
322
|
+
lines.push(`Send back: exodus video flag ${runId} --note "what's wrong"`);
|
|
323
|
+
}
|
|
313
324
|
if (stop.framesNodeId) {
|
|
314
325
|
lines.push(`Redo one frame: exodus video retry-frame ${runId} --node ${stop.framesNodeId} --scene <n>`);
|
|
315
326
|
}
|
|
@@ -956,6 +967,11 @@ function itemWord(item) {
|
|
|
956
967
|
return "flagged";
|
|
957
968
|
return ITEM_STATUS_WORD[item.status] ?? item.status;
|
|
958
969
|
}
|
|
970
|
+
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.";
|
|
971
|
+
function carryingOn(run) {
|
|
972
|
+
return (run.status === "queued" &&
|
|
973
|
+
run.nodes.some((n) => n.status !== "idle" && n.status !== "out-of-scope"));
|
|
974
|
+
}
|
|
959
975
|
export async function statusFlow(runId, json, deps) {
|
|
960
976
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
961
977
|
if (!runRes.ok)
|
|
@@ -1007,6 +1023,7 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1007
1023
|
: `Ad run ${runId} — ${displayRunStatus(run.status)}`;
|
|
1008
1024
|
const lines = [
|
|
1009
1025
|
headline,
|
|
1026
|
+
...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
|
|
1010
1027
|
...guidance,
|
|
1011
1028
|
...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
|
|
1012
1029
|
...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
|
|
@@ -1108,7 +1125,14 @@ export async function storyboardFlow(runId, json, deps) {
|
|
|
1108
1125
|
}
|
|
1109
1126
|
}
|
|
1110
1127
|
}
|
|
1111
|
-
|
|
1128
|
+
const runRes = await deps
|
|
1129
|
+
.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`)
|
|
1130
|
+
.catch(() => null);
|
|
1131
|
+
const showAd = runRes?.ok === true && isShowAd(asVideoRun(runRes.data));
|
|
1132
|
+
lines.push("", `approve with: exodus video approve ${runId}`);
|
|
1133
|
+
if (showAd) {
|
|
1134
|
+
lines.push(`send it back: exodus video flag ${runId} --note "what's wrong"`);
|
|
1135
|
+
}
|
|
1112
1136
|
if (cards.framesNodeId) {
|
|
1113
1137
|
lines.push(`redo one frame: exodus video retry-frame ${runId} --node ${cards.framesNodeId} --scene <n>`);
|
|
1114
1138
|
}
|
|
@@ -1136,14 +1160,30 @@ function sceneCardLines(scene) {
|
|
|
1136
1160
|
lines.push(` picture: ${scene.frame.status}`);
|
|
1137
1161
|
return lines;
|
|
1138
1162
|
}
|
|
1139
|
-
export async function approveFlow(runId,
|
|
1140
|
-
const res = await deps.post(APPROVE_PATH, {
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1163
|
+
export async function approveFlow(runId, opts, deps) {
|
|
1164
|
+
const res = await deps.post(APPROVE_PATH, {
|
|
1165
|
+
runId,
|
|
1166
|
+
...(opts.approveStaleCut ? { approveStaleCut: true } : {}),
|
|
1167
|
+
});
|
|
1168
|
+
if (!res.ok) {
|
|
1169
|
+
const refusal = errorResult(res, opts.json);
|
|
1170
|
+
const staleCut = !opts.json && videoApiError(res).includes("approve this cut anyway");
|
|
1171
|
+
return staleCut
|
|
1172
|
+
? {
|
|
1173
|
+
...refusal,
|
|
1174
|
+
lines: [
|
|
1175
|
+
...refusal.lines,
|
|
1176
|
+
`To deliver it anyway: exodus video approve ${runId} --approve-stale-cut`,
|
|
1177
|
+
],
|
|
1178
|
+
}
|
|
1179
|
+
: refusal;
|
|
1180
|
+
}
|
|
1181
|
+
const data = res.data;
|
|
1182
|
+
const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
|
|
1183
|
+
if (opts.json) {
|
|
1184
|
+
return { code: 0, lines: [JSON.stringify({ ok: true, runId, warnings, data: res.data })] };
|
|
1185
|
+
}
|
|
1186
|
+
const voices = data?.voices;
|
|
1147
1187
|
const summary = voices
|
|
1148
1188
|
? voiceSummaryLines(Array.isArray(voices.cast) ? voices.cast : [], Array.isArray(voices.notices) ? voices.notices : [])
|
|
1149
1189
|
: [];
|
|
@@ -1151,6 +1191,7 @@ export async function approveFlow(runId, json, deps) {
|
|
|
1151
1191
|
code: 0,
|
|
1152
1192
|
lines: [
|
|
1153
1193
|
"Approved.",
|
|
1194
|
+
...warnings,
|
|
1154
1195
|
...(summary.length > 0 ? ["", ...summary, ""] : []),
|
|
1155
1196
|
`See what happens next: exodus video status ${runId}`,
|
|
1156
1197
|
],
|
|
@@ -1208,8 +1249,9 @@ export async function retryFrameFlow(runId, nodeId, sceneIndex, note, json, deps
|
|
|
1208
1249
|
],
|
|
1209
1250
|
};
|
|
1210
1251
|
}
|
|
1211
|
-
const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip.
|
|
1212
|
-
"re-cut
|
|
1252
|
+
const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip. If this redo replaces the clip, " +
|
|
1253
|
+
"exodus video approve stops until you pull the pieces again, re-cut and upload again, " +
|
|
1254
|
+
"or pass --approve-stale-cut to deliver the cut you uploaded as it is.";
|
|
1213
1255
|
export function planClipRedo(run, items, target) {
|
|
1214
1256
|
const stop = classifyRun(run);
|
|
1215
1257
|
const found = findClipRow(run, items, target);
|
|
@@ -2028,8 +2070,9 @@ export async function run(flags, occurrences) {
|
|
|
2028
2070
|
return printResult(await statusFlow(runId, json, defaultDeps));
|
|
2029
2071
|
if (sub === "storyboard")
|
|
2030
2072
|
return printResult(await storyboardFlow(runId, json, defaultDeps));
|
|
2031
|
-
if (sub === "approve")
|
|
2032
|
-
return printResult(await approveFlow(runId, json, defaultDeps));
|
|
2073
|
+
if (sub === "approve") {
|
|
2074
|
+
return printResult(await approveFlow(runId, { json, approveStaleCut: flags["approve-stale-cut"] === true }, defaultDeps));
|
|
2075
|
+
}
|
|
2033
2076
|
if (sub === "flag") {
|
|
2034
2077
|
const note = flagString(flags, "note");
|
|
2035
2078
|
if (!note)
|
|
@@ -34,14 +34,60 @@ export interface WorkflowEdge {
|
|
|
34
34
|
targetHandle: string;
|
|
35
35
|
}
|
|
36
36
|
export type WorkflowTriggerEvent = "winner-promoted";
|
|
37
|
+
export interface ImageRigLineOverride {
|
|
38
|
+
count?: number;
|
|
39
|
+
aspects?: string[];
|
|
40
|
+
adType?: string;
|
|
41
|
+
style?: string;
|
|
42
|
+
memeFormat?: string;
|
|
43
|
+
steering?: string;
|
|
44
|
+
copyPin?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ImageRigPlanOverride {
|
|
47
|
+
lines?: unknown[];
|
|
48
|
+
steering?: string;
|
|
49
|
+
defaultAspects?: string[];
|
|
50
|
+
defaultStyle?: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
confirmLargeRun?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface ImageRigNodeOverride {
|
|
55
|
+
plan?: ImageRigPlanOverride;
|
|
56
|
+
lines?: Record<string, ImageRigLineOverride>;
|
|
57
|
+
confirmLargeRun?: boolean;
|
|
58
|
+
model?: string;
|
|
59
|
+
}
|
|
60
|
+
export type ImageRigOverrides = Record<string, ImageRigNodeOverride>;
|
|
61
|
+
export interface VoiceRequest {
|
|
62
|
+
who: string;
|
|
63
|
+
choice: {
|
|
64
|
+
kind: "pin";
|
|
65
|
+
voiceId: string;
|
|
66
|
+
label?: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "clear";
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export type TriggerVoiceMap = Record<string, string | {
|
|
72
|
+
voiceId: string;
|
|
73
|
+
label?: string;
|
|
74
|
+
} | null>;
|
|
37
75
|
export type WorkflowTrigger = {
|
|
38
76
|
type: "event";
|
|
39
77
|
event: WorkflowTriggerEvent;
|
|
40
78
|
enabled: boolean;
|
|
79
|
+
imageRigOverrides?: ImageRigOverrides;
|
|
80
|
+
voices?: VoiceRequest[];
|
|
41
81
|
} | {
|
|
42
82
|
type: "cron";
|
|
43
83
|
cron: string;
|
|
44
84
|
enabled: boolean;
|
|
85
|
+
imageRigOverrides?: ImageRigOverrides;
|
|
86
|
+
voices?: VoiceRequest[];
|
|
87
|
+
};
|
|
88
|
+
type WithoutVoices<T> = T extends unknown ? Omit<T, "voices"> : never;
|
|
89
|
+
export type WorkflowContractTrigger = WithoutVoices<WorkflowTrigger> & {
|
|
90
|
+
voices?: TriggerVoiceMap;
|
|
45
91
|
};
|
|
46
92
|
export interface WorkflowContractJson {
|
|
47
93
|
contract: "exodus-workflow";
|
|
@@ -51,7 +97,7 @@ export interface WorkflowContractJson {
|
|
|
51
97
|
name: string;
|
|
52
98
|
description?: string;
|
|
53
99
|
slots?: WorkflowSlot[];
|
|
54
|
-
triggers?:
|
|
100
|
+
triggers?: WorkflowContractTrigger[];
|
|
55
101
|
nodes: WorkflowNode[];
|
|
56
102
|
edges: WorkflowEdge[];
|
|
57
103
|
}
|
|
@@ -530,12 +576,13 @@ export declare function parseVersionFlag(flags: Record<string, string | boolean>
|
|
|
530
576
|
export declare function versionsFlow(workflowRef: string, opts: {
|
|
531
577
|
json: boolean;
|
|
532
578
|
}, deps: WorkflowRunDeps, channel?: Channel): Promise<FlowResult>;
|
|
533
|
-
|
|
579
|
+
type AnyTrigger = WorkflowTrigger | WorkflowContractTrigger;
|
|
580
|
+
export declare function triggerExpect(t: AnyTrigger): {
|
|
534
581
|
type: string;
|
|
535
582
|
event?: string;
|
|
536
583
|
cron?: string;
|
|
537
584
|
};
|
|
538
|
-
export declare function formatTriggers(triggers:
|
|
585
|
+
export declare function formatTriggers(triggers: AnyTrigger[]): string;
|
|
539
586
|
export declare function triggersListFlow(workflowRef: string, opts: {
|
|
540
587
|
json: boolean;
|
|
541
588
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
@@ -546,6 +593,7 @@ export declare function triggersFireFlow(workflowRef: string, opts: {
|
|
|
546
593
|
n?: number;
|
|
547
594
|
text?: string;
|
|
548
595
|
imageRigOverrides?: Record<string, unknown>;
|
|
596
|
+
voices?: VoiceMap;
|
|
549
597
|
wait: boolean;
|
|
550
598
|
json: boolean;
|
|
551
599
|
onProgressLine?: (line: string) => void;
|
|
@@ -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
|
|
|
@@ -32,7 +32,7 @@ Usage:
|
|
|
32
32
|
exodus workflow triggers <workflowId|name> [--json]
|
|
33
33
|
exodus workflow triggers <workflowId|name> enable <n> [--json]
|
|
34
34
|
exodus workflow triggers <workflowId|name> disable <n> [--json]
|
|
35
|
-
exodus workflow triggers <workflowId|name> fire [<n>] [--text "..."] [--rig-overrides <json|@file>] [--wait] [--json]
|
|
35
|
+
exodus workflow triggers <workflowId|name> fire [<n>] [--text "..."] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--wait] [--json]
|
|
36
36
|
exodus workflow inbox [--json]
|
|
37
37
|
exodus workflow checkpoint <runId> [show] [--json]
|
|
38
38
|
exodus workflow checkpoint <runId> edit <n> [--text "..." | --file <path> | (stdin)] [--json]
|
|
@@ -84,9 +84,9 @@ Flags:
|
|
|
84
84
|
before anything runs, and the error names it. On
|
|
85
85
|
"triggers fire" it REPLACES the schedule's own
|
|
86
86
|
overrides for that one test fire.
|
|
87
|
-
--voices <json> (run) Give the ad's characters their
|
|
88
|
-
moment the run starts, instead of at the
|
|
89
|
-
review. Keyed by the names the SCRIPT uses for its
|
|
87
|
+
--voices <json> (run, triggers fire) Give the ad's characters their
|
|
88
|
+
voices at the moment the run starts, instead of at the
|
|
89
|
+
storyboard review. Keyed by the names the SCRIPT uses for its
|
|
90
90
|
speakers ("HOST 1") — the planner's character ids
|
|
91
91
|
(C1, C2) do not exist yet when a run starts. Takes a
|
|
92
92
|
JSON object, or @path to a .json file holding one:
|
|
@@ -103,6 +103,14 @@ Flags:
|
|
|
103
103
|
<run>" at the storyboard review instead. Voice ids
|
|
104
104
|
belong to the brand's own folder — never put them in a
|
|
105
105
|
shared workflow or template.
|
|
106
|
+
A schedule can carry the same map in its YAML
|
|
107
|
+
("voices:" beside the schedule), so a weekly ad arrives
|
|
108
|
+
already voiced. On "triggers fire" this flag REPLACES
|
|
109
|
+
the schedule's own voices for that one test fire. A
|
|
110
|
+
schedule whose voice has been deleted, or whose
|
|
111
|
+
ElevenLabs key is missing or refused, does not start at
|
|
112
|
+
all: it leaves a failed run saying which speaker and
|
|
113
|
+
what to fix, and spends nothing.
|
|
106
114
|
--voice-treatment <v> (run) Say HOW this run makes its voices. Two kinds of
|
|
107
115
|
answer. A name on its own picks a way of working that
|
|
108
116
|
uses voices you already have:
|
|
@@ -279,7 +287,9 @@ Notes:
|
|
|
279
287
|
panel in the app); anything you name that isn't there stops the launch and the
|
|
280
288
|
message says which key was wrong. A trigger can carry the same payload in its
|
|
281
289
|
YAML ("imageRigOverrides:" beside its schedule), so a Monday schedule and a
|
|
282
|
-
Friday one can fire the same workflow at different sizes.
|
|
290
|
+
Friday one can fire the same workflow at different sizes. A trigger can carry
|
|
291
|
+
"voices:" the same way — the same map "workflow run --voices" takes — so a run
|
|
292
|
+
that starts on a schedule arrives at the storyboard already voiced.
|
|
283
293
|
"workflow run --voices" hands a video run its voices at the start, so a brand
|
|
284
294
|
that always uses the same hosts stops choosing them by hand on every run. The
|
|
285
295
|
file is keyed by the names the script uses for its speakers, because that is
|
|
@@ -1372,6 +1382,43 @@ function planArtifactFile(artifact) {
|
|
|
1372
1382
|
url,
|
|
1373
1383
|
};
|
|
1374
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
|
+
}
|
|
1375
1422
|
export async function saveDeliveries(run, dir, deps) {
|
|
1376
1423
|
const paths = [];
|
|
1377
1424
|
if (!run.isTerminal) {
|
|
@@ -1406,9 +1453,10 @@ export async function saveDeliveries(run, dir, deps) {
|
|
|
1406
1453
|
lines.push(` skipped ${delivery.label} (${delivery.key}) — ${why}`);
|
|
1407
1454
|
continue;
|
|
1408
1455
|
}
|
|
1456
|
+
const suffixes = artifactFileSuffixes(delivery.artifacts);
|
|
1409
1457
|
for (let i = 0; i < delivery.artifacts.length; i++) {
|
|
1410
1458
|
const plan = planArtifactFile(delivery.artifacts[i]);
|
|
1411
|
-
const suffix
|
|
1459
|
+
const { suffix, renamed } = suffixes[i];
|
|
1412
1460
|
if (plan.kind === "none") {
|
|
1413
1461
|
lines.push(` skipped ${delivery.label} (${delivery.key})${suffix} — ${plan.reason}`);
|
|
1414
1462
|
continue;
|
|
@@ -1419,7 +1467,7 @@ export async function saveDeliveries(run, dir, deps) {
|
|
|
1419
1467
|
deps.writeFile(file, plan.body);
|
|
1420
1468
|
else
|
|
1421
1469
|
await downloadToFile(plan.url, file);
|
|
1422
|
-
lines.push(` wrote ${file}`);
|
|
1470
|
+
lines.push(` wrote ${file}${renamed ? " — renamed to avoid a collision" : ""}`);
|
|
1423
1471
|
paths.push(file);
|
|
1424
1472
|
}
|
|
1425
1473
|
catch (e) {
|
|
@@ -2450,6 +2498,7 @@ export async function triggersFireFlow(workflowRef, opts, deps) {
|
|
|
2450
2498
|
...(opts.imageRigOverrides
|
|
2451
2499
|
? { imageRigOverrides: opts.imageRigOverrides }
|
|
2452
2500
|
: {}),
|
|
2501
|
+
...(opts.voices ? { voices: opts.voices } : {}),
|
|
2453
2502
|
});
|
|
2454
2503
|
if (!res.ok)
|
|
2455
2504
|
return triggerErrorResult(res, verb, opts.json);
|
|
@@ -3313,8 +3362,10 @@ export async function run(flags, occurrences) {
|
|
|
3313
3362
|
}
|
|
3314
3363
|
}
|
|
3315
3364
|
let fireOverrides;
|
|
3365
|
+
let fireVoices;
|
|
3316
3366
|
try {
|
|
3317
3367
|
fireOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
|
|
3368
|
+
fireVoices = parseVoicesFlag(occurrences, defaultDeps.readFile);
|
|
3318
3369
|
}
|
|
3319
3370
|
catch (e) {
|
|
3320
3371
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3324,6 +3375,7 @@ export async function run(flags, occurrences) {
|
|
|
3324
3375
|
n,
|
|
3325
3376
|
text: flagString(flags, "text"),
|
|
3326
3377
|
imageRigOverrides: fireOverrides,
|
|
3378
|
+
voices: fireVoices,
|
|
3327
3379
|
wait: flags["wait"] === true,
|
|
3328
3380
|
json,
|
|
3329
3381
|
onProgressLine: (line) => console.log(line),
|