@aicopycoders/exodus 2026.9.1700-beta.1 → 2026.9.1900-beta.0
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 +3 -0
- package/assets/skills/exodus-video/SKILL.md +217 -18
- package/dist/commands/video.d.ts +74 -1
- package/dist/commands/video.js +415 -22
- package/dist/commands/workflow.d.ts +3 -0
- package/dist/commands/workflow.js +116 -4
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import { runVerdict } from "../lib/runVerdict.js";
|
|
|
9
9
|
import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
|
|
10
10
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
11
11
|
import { getChannel } from "../lib/channel.js";
|
|
12
|
-
import { asVideoRun, classifyRun, stopLines } from "./video.js";
|
|
12
|
+
import { asVideoRun, classifyRun, reviewUrl, stopLines, } from "./video.js";
|
|
13
13
|
export const helpText = `
|
|
14
14
|
exodus workflow — List, describe, run, inspect, import, and export saved workflows
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ Usage:
|
|
|
20
20
|
exodus workflow templates [list] [--json]
|
|
21
21
|
exodus workflow templates export <key> [--out <file>] [--json]
|
|
22
22
|
exodus workflow schema [--kind <kind>] [--face <face>] [--json]
|
|
23
|
-
exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json]
|
|
23
|
+
exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--auto-approve] [--wait] [--out <dir>] [--json]
|
|
24
24
|
exodus workflow status [--json]
|
|
25
25
|
exodus workflow status --id <runId> [--out <dir>] [--json]
|
|
26
26
|
exodus workflow cancel <runId> [--reason "..."] [--json]
|
|
@@ -83,6 +83,25 @@ Flags:
|
|
|
83
83
|
before anything runs, and the error names it. On
|
|
84
84
|
"triggers fire" it REPLACES the schedule's own
|
|
85
85
|
overrides for that one test fire.
|
|
86
|
+
--voices <json> (run) Give the ad's characters their voices at the
|
|
87
|
+
moment the run starts, instead of at the storyboard
|
|
88
|
+
review. Keyed by the names the SCRIPT uses for its
|
|
89
|
+
speakers ("HOST 1") — the planner's character ids
|
|
90
|
+
(C1, C2) do not exist yet when a run starts. Takes a
|
|
91
|
+
JSON object, or @path to a .json file holding one:
|
|
92
|
+
--voices @voices.json
|
|
93
|
+
--voices '{"HOST 1":"<voiceId>"}'
|
|
94
|
+
Same file "exodus video voices <run> --from" reads at
|
|
95
|
+
the review, so a brand keeps ONE voices.json for both.
|
|
96
|
+
A speaker the script does not have, the same speaker
|
|
97
|
+
named twice, or a voice ElevenLabs does not have stops
|
|
98
|
+
the launch before the run is created — nothing is
|
|
99
|
+
spent, and the message says which one. A workflow whose
|
|
100
|
+
script is written by a bot (so nobody knows the speaker
|
|
101
|
+
names at the start) is told to use "exodus video voices
|
|
102
|
+
<run>" at the storyboard review instead. Voice ids
|
|
103
|
+
belong to the brand's own folder — never put them in a
|
|
104
|
+
shared workflow or template.
|
|
86
105
|
--auto-approve (run) Deliberately unattended launch: every Checkpoint
|
|
87
106
|
box this run stops at is approved automatically, with
|
|
88
107
|
whatever it was holding left exactly as it is, and the
|
|
@@ -174,6 +193,7 @@ Examples:
|
|
|
174
193
|
exodus workflow run "Launch Flow" --auto-approve --wait
|
|
175
194
|
exodus workflow run "Product Shots" --rig-overrides '{"rig_1":{"lines":{"line_1":{"count":3}}}}'
|
|
176
195
|
exodus workflow run "Product Shots" --rig-overrides @rig.json --wait
|
|
196
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt --voices @voices.json --wait
|
|
177
197
|
exodus workflow run "Launch Flow" --wait --out ./deliverables
|
|
178
198
|
exodus workflow status # recent runs + their ids
|
|
179
199
|
exodus workflow status --id wr_123
|
|
@@ -235,6 +255,19 @@ Notes:
|
|
|
235
255
|
message says which key was wrong. A trigger can carry the same payload in its
|
|
236
256
|
YAML ("imageRigOverrides:" beside its schedule), so a Monday schedule and a
|
|
237
257
|
Friday one can fire the same workflow at different sizes.
|
|
258
|
+
"workflow run --voices" hands a video run its voices at the start, so a brand
|
|
259
|
+
that always uses the same hosts stops choosing them by hand on every run. The
|
|
260
|
+
file is keyed by the names the script uses for its speakers, because that is
|
|
261
|
+
all that exists before the planner has run — it is the SAME voices.json
|
|
262
|
+
"exodus video voices <run> --from" reads at the storyboard review, so one file
|
|
263
|
+
per brand serves both moments. A speaker the script does not have, the same
|
|
264
|
+
speaker named twice, or a voice ElevenLabs does not have stops the launch
|
|
265
|
+
before the run exists and nothing is spent. When the storyboard is planned,
|
|
266
|
+
each named speaker's character already has their voice, and "exodus video
|
|
267
|
+
voices <run>" shows them set. A workflow whose script is written by a bot has
|
|
268
|
+
no speaker names to key on at the start, so it is refused with a pointer at
|
|
269
|
+
the review-time command. Keep voice ids in the brand's own folder, never in a
|
|
270
|
+
shared workflow or template. (#1846)
|
|
238
271
|
workflow triggers are addressed by 1-based position — a trigger carries no id,
|
|
239
272
|
so the CLI reads the live list from the export contract and sends a fingerprint
|
|
240
273
|
of the trigger's fields as the guard; a concurrent edit fails loud rather than
|
|
@@ -320,6 +353,7 @@ const VALUE_FLAGS = new Set([
|
|
|
320
353
|
"slot",
|
|
321
354
|
"reject",
|
|
322
355
|
"rig-overrides",
|
|
356
|
+
"voices",
|
|
323
357
|
"note",
|
|
324
358
|
]);
|
|
325
359
|
function defaultMkdirp(dir) {
|
|
@@ -917,6 +951,63 @@ export function parseRigOverridesFlag(args, readFile) {
|
|
|
917
951
|
}
|
|
918
952
|
return parsed;
|
|
919
953
|
}
|
|
954
|
+
export function parseVoicesFlag(args, readFile) {
|
|
955
|
+
let raw;
|
|
956
|
+
for (let i = 0; i < args.length; i++) {
|
|
957
|
+
const arg = args[i];
|
|
958
|
+
let value;
|
|
959
|
+
if (arg === "--voices") {
|
|
960
|
+
value = args[i + 1];
|
|
961
|
+
i++;
|
|
962
|
+
}
|
|
963
|
+
else if (arg.startsWith("--voices=")) {
|
|
964
|
+
value = arg.slice("--voices=".length);
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (value === undefined || value.startsWith("--")) {
|
|
970
|
+
throw new Error("--voices requires JSON or @path/to/voices.json");
|
|
971
|
+
}
|
|
972
|
+
raw = value;
|
|
973
|
+
}
|
|
974
|
+
if (raw === undefined)
|
|
975
|
+
return undefined;
|
|
976
|
+
let text = raw.trim();
|
|
977
|
+
if (!text)
|
|
978
|
+
throw new Error("--voices requires JSON or @path/to/voices.json");
|
|
979
|
+
if (text.startsWith("@@")) {
|
|
980
|
+
text = text.slice(1);
|
|
981
|
+
}
|
|
982
|
+
else if (text.startsWith("@")) {
|
|
983
|
+
const filePath = text.slice(1);
|
|
984
|
+
if (!filePath) {
|
|
985
|
+
throw new Error("--voices @<file> needs a file path after \"@\"");
|
|
986
|
+
}
|
|
987
|
+
if (!readFile) {
|
|
988
|
+
throw new Error(`--voices: cannot load @${filePath} here (no file access)`);
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
text = readFile(filePath);
|
|
992
|
+
}
|
|
993
|
+
catch (e) {
|
|
994
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
995
|
+
throw new Error(`--voices: could not read file "${filePath}": ${msg}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
let parsed;
|
|
999
|
+
try {
|
|
1000
|
+
parsed = JSON.parse(text);
|
|
1001
|
+
}
|
|
1002
|
+
catch (e) {
|
|
1003
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1004
|
+
throw new Error(`--voices is not valid JSON: ${msg}`);
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1007
|
+
throw new Error("--voices must be a JSON object keyed by the script's speaker names, e.g. {\"HOST 1\":\"<voiceId>\"}");
|
|
1008
|
+
}
|
|
1009
|
+
return parsed;
|
|
1010
|
+
}
|
|
920
1011
|
export function parseFillFlag(args) {
|
|
921
1012
|
let name;
|
|
922
1013
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1723,6 +1814,20 @@ export async function botsFlow(opts, deps) {
|
|
|
1723
1814
|
return { code: 0, lines: [JSON.stringify(catalog)] };
|
|
1724
1815
|
return { code: 0, lines: [formatBotsList(catalog, opts.category)] };
|
|
1725
1816
|
}
|
|
1817
|
+
function launchVoicesLine(attached) {
|
|
1818
|
+
if (!Array.isArray(attached))
|
|
1819
|
+
return undefined;
|
|
1820
|
+
const said = [];
|
|
1821
|
+
for (const raw of attached) {
|
|
1822
|
+
if (!raw || typeof raw !== "object")
|
|
1823
|
+
continue;
|
|
1824
|
+
const { speaker, voiceId, label } = raw;
|
|
1825
|
+
if (typeof speaker !== "string" || typeof voiceId !== "string")
|
|
1826
|
+
continue;
|
|
1827
|
+
said.push(`${speaker} → ${typeof label === "string" && label !== "" ? label : voiceId}`);
|
|
1828
|
+
}
|
|
1829
|
+
return said.length > 0 ? `Voices: ${said.join(", ")}` : undefined;
|
|
1830
|
+
}
|
|
1726
1831
|
export async function runFlow(workflowRef, opts, deps) {
|
|
1727
1832
|
let workflowId;
|
|
1728
1833
|
try {
|
|
@@ -1765,6 +1870,7 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1765
1870
|
...(opts.fill && opts.fill.trim() !== "" ? { fill: opts.fill.trim() } : {}),
|
|
1766
1871
|
...(opts.autoApprove === true ? { autoApprove: true } : {}),
|
|
1767
1872
|
...(opts.imageRigOverrides ? { imageRigOverrides: opts.imageRigOverrides } : {}),
|
|
1873
|
+
...(opts.voices ? { voices: opts.voices } : {}),
|
|
1768
1874
|
launchedVia: "cli",
|
|
1769
1875
|
};
|
|
1770
1876
|
const start = await deps.post(RUN_PATH, body);
|
|
@@ -1778,6 +1884,7 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1778
1884
|
const base = { ...data, workflowId };
|
|
1779
1885
|
if (opts.json && !opts.wait)
|
|
1780
1886
|
return { code: 0, lines: [JSON.stringify(base)] };
|
|
1887
|
+
const voicesLine = launchVoicesLine(data?.launchVoices);
|
|
1781
1888
|
const lines = opts.json
|
|
1782
1889
|
? []
|
|
1783
1890
|
: [
|
|
@@ -1785,6 +1892,7 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1785
1892
|
"Workflow run started.",
|
|
1786
1893
|
`runId: ${data.runId}`,
|
|
1787
1894
|
`triggerRunId: ${data.triggerRunId}`,
|
|
1895
|
+
...(voicesLine ? [voicesLine] : []),
|
|
1788
1896
|
`Poll: exodus workflow status --id ${data.runId}`,
|
|
1789
1897
|
];
|
|
1790
1898
|
if (!opts.wait) {
|
|
@@ -1903,7 +2011,8 @@ async function waitForRun(runId, opts, deps) {
|
|
|
1903
2011
|
lines.push("", landOnPark.headline, ...formatPauseNotice(landOnPark.pauseReason, runId, dashboardUrl).slice(1));
|
|
1904
2012
|
}
|
|
1905
2013
|
if (videoStop) {
|
|
1906
|
-
|
|
2014
|
+
const review = reviewUrl(deps.dashboardUrl ?? getDashboardUrl(), asVideoRun(pollResult.data));
|
|
2015
|
+
lines.push("", ...stopLines(videoStop, runId, review));
|
|
1907
2016
|
}
|
|
1908
2017
|
return { code: pollResult.ok ? 0 : 1, lines };
|
|
1909
2018
|
}
|
|
@@ -2900,19 +3009,21 @@ export async function run(flags) {
|
|
|
2900
3009
|
const workflowRef = rest[0];
|
|
2901
3010
|
if (!workflowRef) {
|
|
2902
3011
|
console.error("Error: workflow run requires <workflowId|name>.");
|
|
2903
|
-
console.log("Usage: exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=./path/to/file ...] [--auto-approve] [--wait] [--json]");
|
|
3012
|
+
console.log("Usage: exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=./path/to/file ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--auto-approve] [--wait] [--json]");
|
|
2904
3013
|
process.exit(1);
|
|
2905
3014
|
}
|
|
2906
3015
|
let inputs;
|
|
2907
3016
|
let fill;
|
|
2908
3017
|
let autoApprove;
|
|
2909
3018
|
let imageRigOverrides;
|
|
3019
|
+
let voices;
|
|
2910
3020
|
try {
|
|
2911
3021
|
inputs = parseRawInputFlags(process.argv.slice(3));
|
|
2912
3022
|
rejectTerminalFlag(process.argv.slice(3));
|
|
2913
3023
|
fill = parseFillFlag(process.argv.slice(3));
|
|
2914
3024
|
autoApprove = parseAutoApproveFlag(process.argv.slice(3));
|
|
2915
3025
|
imageRigOverrides = parseRigOverridesFlag(process.argv.slice(3), defaultDeps.readFile);
|
|
3026
|
+
voices = parseVoicesFlag(process.argv.slice(3), defaultDeps.readFile);
|
|
2916
3027
|
}
|
|
2917
3028
|
catch (e) {
|
|
2918
3029
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -2923,6 +3034,7 @@ export async function run(flags) {
|
|
|
2923
3034
|
fill,
|
|
2924
3035
|
autoApprove,
|
|
2925
3036
|
imageRigOverrides,
|
|
3037
|
+
voices,
|
|
2926
3038
|
wait: flags["wait"] === true,
|
|
2927
3039
|
json,
|
|
2928
3040
|
out: flagString(flags, "out"),
|