@aicopycoders/exodus 2026.9.2000-beta.1 → 2026.9.2000-beta.3
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/skills/exodus-video/SKILL.md +9 -3
- package/dist/commands/genesis.js +12 -133
- package/dist/commands/status.js +8 -18
- package/dist/commands/video.d.ts +20 -3
- package/dist/commands/video.js +92 -21
- package/dist/commands/workflow.d.ts +51 -3
- package/dist/commands/workflow.js +22 -7
- package/package.json +1 -1
|
@@ -104,7 +104,10 @@ under it:
|
|
|
104
104
|
- storyboard gate: needs `approve`. When the stop block prints a `Redo one
|
|
105
105
|
frame:` line with a node id, one still can be redone first with `retry-frame`;
|
|
106
106
|
a gate parked before the stills are drawn has nothing to redraw.
|
|
107
|
-
- final watch: every piece is made; needs `pull`, a cut, and `upload`.
|
|
107
|
+
- final watch: every piece is made; needs `pull`, a cut, and `upload`. Once a
|
|
108
|
+
cut is uploaded the stop block leads with `approve` instead, and `status
|
|
109
|
+
--json` says which state it is in (`stop.cutAttached`, plus the same next-step
|
|
110
|
+
lines under `guidance`). It is also
|
|
108
111
|
the one park where a finished clip can be redone, with `retry-clip`. A run
|
|
109
112
|
only reaches this park if its video node sets `finalWatch: true` — otherwise
|
|
110
113
|
it skips straight to finished.
|
|
@@ -677,8 +680,11 @@ them. If the words are wrong, that is a new run.
|
|
|
677
680
|
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
678
681
|
clip the row stays `done` with the ORIGINAL take and only its findings are
|
|
679
682
|
refreshed, because a worse take must never replace a delivered one. So never
|
|
680
|
-
report that the clip changed
|
|
681
|
-
|
|
683
|
+
report that the clip changed without reading `status` again: under the scene it
|
|
684
|
+
prints what the redo did, `new take accepted (take N)` or `new take rejected,
|
|
685
|
+
original kept (take N)`, and `status --json` carries the same sentence on the
|
|
686
|
+
row as `lastRedo.label`. A row with no `lastRedo` was redone before outcomes
|
|
687
|
+
were recorded; for those, pull and compare the file before and after.
|
|
682
688
|
|
|
683
689
|
**Re-cut afterwards.** A cut you already uploaded still holds the old clip, and
|
|
684
690
|
the CLI says so when that is the case. Pull again, re-cut and upload again before
|
package/dist/commands/genesis.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
3
|
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
5
4
|
import { pollUntilDone } from "../lib/poll.js";
|
|
6
5
|
import { displayRunStatus, formatGenesisRun, formatError, tickerRunStatus } from "../lib/format.js";
|
|
@@ -278,52 +277,13 @@ export async function run(flags) {
|
|
|
278
277
|
return submitGenesis(action.body, { noWait, cc });
|
|
279
278
|
}
|
|
280
279
|
}
|
|
281
|
-
async function listBank(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
console.log(formatError(res));
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
const ideas = Array.isArray(res.data.ideas) ? res.data.ideas : [];
|
|
288
|
-
if (ideas.length === 0) {
|
|
289
|
-
console.log("Bank is empty.");
|
|
290
|
-
console.log('Fill it with: exodus genesis scrape (or paste a reel: exodus genesis --reel "<url>")');
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
console.log(`## Idea Bank (${ideas.length} reel${ideas.length === 1 ? "" : "s"})`);
|
|
294
|
-
console.log("");
|
|
295
|
-
for (const i of ideas) {
|
|
296
|
-
const hook = i.hook ?? "(no hook)";
|
|
297
|
-
const user = typeof i.sourceUsername === "string" && i.sourceUsername.length > 0
|
|
298
|
-
? ` @${i.sourceUsername.replace(/^@+/, "")}`
|
|
299
|
-
: "";
|
|
300
|
-
const score = typeof i.relevanceScore === "number" ? ` score=${i.relevanceScore.toFixed(2)}` : "";
|
|
301
|
-
const used = typeof i.useCount === "number" && i.useCount > 0 ? " · used" : "";
|
|
302
|
-
console.log(` • ${hook}${user}${score}`);
|
|
303
|
-
console.log(` id: ${i._id ?? "?"} [${i.status ?? "new"}${used}]`);
|
|
304
|
-
if (i.sourceUrl)
|
|
305
|
-
console.log(` ${i.sourceUrl}`);
|
|
306
|
-
}
|
|
307
|
-
console.log("");
|
|
308
|
-
console.log("Write from one: exodus genesis --from-bank --idea <id>");
|
|
280
|
+
async function listBank(_limit) {
|
|
281
|
+
console.error(`This command has been retired. View the idea bank in the dashboard: ${getDashboardUrl()}/idea-bank`);
|
|
282
|
+
process.exit(1);
|
|
309
283
|
}
|
|
310
|
-
async function runFromBank(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
console.log(formatError(res));
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
const idea = (res.data.ideas ?? [])[0];
|
|
317
|
-
if (!idea) {
|
|
318
|
-
console.error(`No banked idea found for id "${ideaId}". List the bank: exodus genesis --from-bank`);
|
|
319
|
-
process.exit(1);
|
|
320
|
-
}
|
|
321
|
-
if (!idea.sourceUrl) {
|
|
322
|
-
console.error(`Banked idea "${ideaId}" has no source reel URL to write from.`);
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
console.log(`Writing from banked reel: ${idea.sourceUrl}`);
|
|
326
|
-
return runReel([idea.sourceUrl], opts, rt);
|
|
284
|
+
async function runFromBank(_ideaId, _opts, _rt) {
|
|
285
|
+
console.error(`This command has been retired. Write from banked ideas in the dashboard: ${getDashboardUrl()}/idea-bank`);
|
|
286
|
+
process.exit(1);
|
|
327
287
|
}
|
|
328
288
|
async function runReel(urls, opts, rt) {
|
|
329
289
|
console.log(`Transcribing ${urls.length === 1 ? "the reel" : `${urls.length} reels`} → idea → Genesis…`);
|
|
@@ -613,92 +573,11 @@ function requireActiveBrand() {
|
|
|
613
573
|
}
|
|
614
574
|
return slug;
|
|
615
575
|
}
|
|
616
|
-
async function runConnectInstagram(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (!start.ok || !start.data.liveViewUrl) {
|
|
620
|
-
console.log(formatError(start));
|
|
621
|
-
process.exit(1);
|
|
622
|
-
}
|
|
623
|
-
console.log(`\nConnecting an Instagram account to brand "${client}".`);
|
|
624
|
-
console.log("\n1. Open this link in your browser:\n");
|
|
625
|
-
console.log(` ${start.data.liveViewUrl}\n`);
|
|
626
|
-
console.log('2. Go to instagram.com and log into the account for THIS brand.');
|
|
627
|
-
console.log("3. Scroll your feed for a bit so the algorithm learns the niche.");
|
|
628
|
-
console.log("4. Come back here and press Enter.\n");
|
|
629
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
630
|
-
try {
|
|
631
|
-
await rl.question("Press Enter once you're logged in and done grooming... ");
|
|
632
|
-
let username = typeof flags["username"] === "string" ? flags["username"].trim() : "";
|
|
633
|
-
while (!username) {
|
|
634
|
-
username = (await rl.question("Instagram handle you just logged in as (e.g. mybrand): ")).trim();
|
|
635
|
-
}
|
|
636
|
-
const finish = await apiPost("/api/v2/genesis/instagram/connect/finish", { client, igUsername: username }, { ccCommand: cc });
|
|
637
|
-
if (!finish.ok) {
|
|
638
|
-
console.log(formatError(finish));
|
|
639
|
-
process.exit(1);
|
|
640
|
-
}
|
|
641
|
-
console.log(`\n✓ Instagram @${username.replace(/^@+/, "")} connected to "${client}".`);
|
|
642
|
-
console.log("Fill the idea bank now: exodus genesis scrape --organic");
|
|
643
|
-
}
|
|
644
|
-
finally {
|
|
645
|
-
rl.close();
|
|
646
|
-
}
|
|
576
|
+
async function runConnectInstagram(_flags, _cc) {
|
|
577
|
+
console.error(`This command has been retired. Connect Instagram accounts in the dashboard: ${getDashboardUrl()}/settings`);
|
|
578
|
+
process.exit(1);
|
|
647
579
|
}
|
|
648
|
-
async function runScrape(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (flags["organic"] === true) {
|
|
652
|
-
const smoke = flags["smoke"] === true;
|
|
653
|
-
const res = await apiPost("/api/v2/scout/organic", { client, ...(smoke ? { smoke: true } : {}) }, { ccCommand: cc });
|
|
654
|
-
if (!res.ok || !res.data.runId) {
|
|
655
|
-
console.log(formatError(res));
|
|
656
|
-
process.exit(1);
|
|
657
|
-
}
|
|
658
|
-
console.log(`Organic scrape started for "${client}": ${res.data.runId}${smoke ? " (smoke)" : ""}`);
|
|
659
|
-
return pollScrape(res.data.runId, noWait, 1_800_000);
|
|
660
|
-
}
|
|
661
|
-
const term = typeof flags["term"] === "string" ? flags["term"] : undefined;
|
|
662
|
-
const body = { sourceMode: "fresh", client };
|
|
663
|
-
if (term)
|
|
664
|
-
body.term = term;
|
|
665
|
-
const res = await apiPost("/api/v2/scout", body, { ccCommand: cc });
|
|
666
|
-
if (!res.ok || !res.data.runId) {
|
|
667
|
-
console.log(formatError(res));
|
|
668
|
-
process.exit(1);
|
|
669
|
-
}
|
|
670
|
-
console.log(`Discovery scrape started for "${client}": ${res.data.runId}`);
|
|
671
|
-
return pollScrape(res.data.runId, noWait, 1_200_000);
|
|
672
|
-
}
|
|
673
|
-
async function pollScrape(runId, noWait, timeoutMs) {
|
|
674
|
-
if (noWait) {
|
|
675
|
-
console.log(`Run ID: ${runId}`);
|
|
676
|
-
console.log(`Check status: exodus status --type scout --id ${runId}`);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
console.log("Polling for completion...");
|
|
680
|
-
const result = await pollUntilDone({
|
|
681
|
-
path: `/api/v2/scout?runId=${runId}`,
|
|
682
|
-
intervalMs: 15_000,
|
|
683
|
-
timeoutMs,
|
|
684
|
-
onProgress: (data) => {
|
|
685
|
-
const status = data["status"];
|
|
686
|
-
const captured = data["organicCaptured"];
|
|
687
|
-
const qualified = data["organicQualified"];
|
|
688
|
-
const counts = captured !== undefined || qualified !== undefined
|
|
689
|
-
? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
|
|
690
|
-
: "";
|
|
691
|
-
if (status)
|
|
692
|
-
process.stdout.write(`\r status: ${tickerRunStatus(status)}${counts} `);
|
|
693
|
-
},
|
|
694
|
-
});
|
|
695
|
-
console.log();
|
|
696
|
-
if (result.timedOut) {
|
|
697
|
-
console.log(`Timed out waiting. Check later: exodus status --type scout --id ${runId}`);
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
console.log(`Done. Banked reels are ready to write: exodus genesis --from-bank`);
|
|
701
|
-
}
|
|
702
|
-
if (!result.ok && !result.timedOut)
|
|
703
|
-
process.exit(1);
|
|
580
|
+
async function runScrape(_flags, _cc) {
|
|
581
|
+
console.error(`This command has been retired. Fill the idea bank from the Mining page in the dashboard: ${getDashboardUrl()}/mining`);
|
|
582
|
+
process.exit(1);
|
|
704
583
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { apiGet } from "../lib/client.js";
|
|
1
|
+
import { apiGet, getDashboardUrl } from "../lib/client.js";
|
|
2
2
|
import { formatGeneration, formatGenesisRun, formatError, displayRunStatus } from "../lib/format.js";
|
|
3
|
-
import { hydrateScoutIdeasCount } from "../lib/scout-hydrate.js";
|
|
4
3
|
import { formatIntelResult as formatIntelResultRich } from "../lib/intel-format.js";
|
|
5
4
|
export const helpText = `
|
|
6
5
|
exodus status — Check the status of a pipeline run
|
|
@@ -12,19 +11,13 @@ Required:
|
|
|
12
11
|
--id <runId> The generation or run ID returned by a prior exodus command
|
|
13
12
|
|
|
14
13
|
Options:
|
|
15
|
-
--type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse
|
|
14
|
+
--type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse
|
|
16
15
|
| creative | template
|
|
17
16
|
Spark, Viral, Mirror, Remix runs use --type generation (default).
|
|
18
|
-
Intel, Pulse
|
|
17
|
+
Intel, Pulse require --type intel|pulse.
|
|
19
18
|
Creative-suite (native/copy-derived/ref-match/meme)
|
|
20
19
|
use --type creative; Template uses --type template.
|
|
21
20
|
`.trim();
|
|
22
|
-
async function fetchScoutIdeasCount(runId) {
|
|
23
|
-
const res = await apiGet(`/api/v2/scout/ideas?runId=${runId}&limit=1`);
|
|
24
|
-
if (res.ok && typeof res.data.count === "number")
|
|
25
|
-
return res.data.count;
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
21
|
export function formatIntelResult(data) {
|
|
29
22
|
return formatIntelResultRich(data).replace("## Intel Analysis Result", "## Intel Status");
|
|
30
23
|
}
|
|
@@ -157,18 +150,21 @@ export async function run(flags) {
|
|
|
157
150
|
process.exit(1);
|
|
158
151
|
}
|
|
159
152
|
const type = flags["type"] ?? "generation";
|
|
153
|
+
if (type === "scout") {
|
|
154
|
+
console.error(`--type scout has been retired. Check run status in the dashboard: ${getDashboardUrl()}/runs`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
160
157
|
const pathMap = {
|
|
161
158
|
generation: `/api/v2/generations?id=${id}`,
|
|
162
159
|
genesis: `/api/v2/genesis?id=${id}`,
|
|
163
160
|
intel: `/api/v2/intel?id=${id}`,
|
|
164
161
|
pulse: `/api/v2/pulse?id=${id}`,
|
|
165
|
-
scout: `/api/v2/scout?runId=${id}`,
|
|
166
162
|
creative: `/api/v2/creative?runId=${id}`,
|
|
167
163
|
template: `/api/v2/template?runId=${id}`,
|
|
168
164
|
};
|
|
169
165
|
const path = pathMap[type];
|
|
170
166
|
if (!path) {
|
|
171
|
-
console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse,
|
|
167
|
+
console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse, creative, template`);
|
|
172
168
|
process.exit(1);
|
|
173
169
|
}
|
|
174
170
|
const res = await apiGet(path);
|
|
@@ -176,9 +172,6 @@ export async function run(flags) {
|
|
|
176
172
|
console.log(formatError(res));
|
|
177
173
|
process.exit(1);
|
|
178
174
|
}
|
|
179
|
-
if (type === "scout") {
|
|
180
|
-
await hydrateScoutIdeasCount(id, res.data, fetchScoutIdeasCount);
|
|
181
|
-
}
|
|
182
175
|
switch (type) {
|
|
183
176
|
case "generation":
|
|
184
177
|
console.log(formatGeneration(res.data));
|
|
@@ -192,9 +185,6 @@ export async function run(flags) {
|
|
|
192
185
|
case "pulse":
|
|
193
186
|
console.log(formatPulseResult(res.data));
|
|
194
187
|
break;
|
|
195
|
-
case "scout":
|
|
196
|
-
console.log(formatScoutResult(res.data));
|
|
197
|
-
break;
|
|
198
188
|
case "creative":
|
|
199
189
|
console.log(formatCreativeSuiteStatus("Creative Suite Status", res.data));
|
|
200
190
|
break;
|
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[];
|
|
@@ -117,6 +117,11 @@ export interface NodeItem {
|
|
|
117
117
|
flagged?: boolean;
|
|
118
118
|
findings?: ClipFinding[];
|
|
119
119
|
staleClaim?: boolean;
|
|
120
|
+
lastRedo?: {
|
|
121
|
+
take: number;
|
|
122
|
+
outcome: string;
|
|
123
|
+
label: string;
|
|
124
|
+
};
|
|
120
125
|
artifact?: ArtifactSubset;
|
|
121
126
|
}
|
|
122
127
|
export interface ShowRow {
|
|
@@ -184,11 +189,20 @@ export type RunStop = {
|
|
|
184
189
|
at: "finished";
|
|
185
190
|
status: string;
|
|
186
191
|
};
|
|
192
|
+
export type ResolvedStop = Exclude<RunStop, {
|
|
193
|
+
at: "final-watch";
|
|
194
|
+
}> | {
|
|
195
|
+
at: "final-watch";
|
|
196
|
+
cutAttached: boolean | null;
|
|
197
|
+
};
|
|
187
198
|
export declare function classifyRun(run: VideoRun): RunStop;
|
|
199
|
+
export declare function hasAttachedCut(items: NodeItem[]): boolean;
|
|
200
|
+
export declare function resolveStop(stop: RunStop, cutAttached: boolean | null): ResolvedStop;
|
|
201
|
+
export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
|
|
188
202
|
export declare function stageWord(stage: string): string;
|
|
189
203
|
export declare function stepName(kind: string | undefined): string;
|
|
190
204
|
export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
|
|
191
|
-
export declare function stopLines(stop:
|
|
205
|
+
export declare function stopLines(stop: ResolvedStop, runId: string, runUrl: string): string[];
|
|
192
206
|
export interface PullDownload {
|
|
193
207
|
file: string;
|
|
194
208
|
url: string;
|
|
@@ -279,7 +293,10 @@ export declare function waitFlow(runId: string, opts: {
|
|
|
279
293
|
}, deps: VideoDeps): Promise<FlowResult>;
|
|
280
294
|
export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
281
295
|
export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
282
|
-
export declare function approveFlow(runId: string,
|
|
296
|
+
export declare function approveFlow(runId: string, opts: {
|
|
297
|
+
json: boolean;
|
|
298
|
+
approveStaleCut: boolean;
|
|
299
|
+
}, deps: VideoDeps): Promise<FlowResult>;
|
|
283
300
|
export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
284
301
|
export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
285
302
|
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
|
|
@@ -254,6 +259,22 @@ export function classifyRun(run) {
|
|
|
254
259
|
const active = run.nodes.find((n) => n.status === "running");
|
|
255
260
|
return { at: "running", stage: active?.kind ?? "starting" };
|
|
256
261
|
}
|
|
262
|
+
export function hasAttachedCut(items) {
|
|
263
|
+
return items.some((i) => i.itemKind === "final" && i.status === "done");
|
|
264
|
+
}
|
|
265
|
+
export function resolveStop(stop, cutAttached) {
|
|
266
|
+
if (stop.at !== "final-watch")
|
|
267
|
+
return stop;
|
|
268
|
+
return { at: "final-watch", cutAttached };
|
|
269
|
+
}
|
|
270
|
+
export async function resolveStopAtPark(stop, runId, deps) {
|
|
271
|
+
if (stop.at !== "final-watch")
|
|
272
|
+
return stop;
|
|
273
|
+
const res = await deps.get(`${ITEMS_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
274
|
+
if (!res.ok)
|
|
275
|
+
return resolveStop(stop, null);
|
|
276
|
+
return resolveStop(stop, hasAttachedCut(res.data.items ?? []));
|
|
277
|
+
}
|
|
257
278
|
const STAGE_WORDS = {
|
|
258
279
|
brief: "reading the script",
|
|
259
280
|
storyboard: "writing the storyboard",
|
|
@@ -300,6 +321,21 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
300
321
|
return lines;
|
|
301
322
|
}
|
|
302
323
|
if (stop.at === "final-watch") {
|
|
324
|
+
if (stop.cutAttached === null) {
|
|
325
|
+
return [
|
|
326
|
+
"Parked: every piece is made.",
|
|
327
|
+
`Couldn't check whether a cut is already uploaded. See where it stands: exodus video status ${runId}`,
|
|
328
|
+
`Watch it here: ${runUrl}`,
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
if (stop.cutAttached) {
|
|
332
|
+
return [
|
|
333
|
+
"Parked: your cut is uploaded and waiting for your approval.",
|
|
334
|
+
`Approve it: exodus video approve ${runId}`,
|
|
335
|
+
`Watch it here: ${runUrl}`,
|
|
336
|
+
"Changed your mind? You can still redo a clip and upload a new cut.",
|
|
337
|
+
];
|
|
338
|
+
}
|
|
303
339
|
return [
|
|
304
340
|
"Parked: every piece is made and the run is waiting for a cut.",
|
|
305
341
|
`Pull the pieces: exodus video pull ${runId} --out ./ad-${runId}`,
|
|
@@ -891,15 +927,16 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
891
927
|
}
|
|
892
928
|
continue;
|
|
893
929
|
}
|
|
930
|
+
const resolved = await resolveStopAtPark(stop, runId, deps);
|
|
894
931
|
if (opts.json) {
|
|
895
932
|
return {
|
|
896
933
|
code: stop.at === "failed" ? 1 : 0,
|
|
897
|
-
lines: [JSON.stringify({ runId, stop, status: run.status, url: opts.url })],
|
|
934
|
+
lines: [JSON.stringify({ runId, stop: resolved, status: run.status, url: opts.url })],
|
|
898
935
|
};
|
|
899
936
|
}
|
|
900
937
|
return {
|
|
901
938
|
code: stop.at === "failed" ? 1 : 0,
|
|
902
|
-
lines: [...lines, ...stopLines(
|
|
939
|
+
lines: [...lines, ...stopLines(resolved, runId, reviewUrl(deps.dashboardUrl, run))],
|
|
903
940
|
};
|
|
904
941
|
}
|
|
905
942
|
const timeoutLine = `Still running after ${Math.round((maxPolls * interval) / 60000)} minutes. Check in with: exodus video status ${runId}`;
|
|
@@ -924,6 +961,11 @@ function itemWord(item) {
|
|
|
924
961
|
return "flagged";
|
|
925
962
|
return ITEM_STATUS_WORD[item.status] ?? item.status;
|
|
926
963
|
}
|
|
964
|
+
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.";
|
|
965
|
+
function carryingOn(run) {
|
|
966
|
+
return (run.status === "queued" &&
|
|
967
|
+
run.nodes.some((n) => n.status !== "idle" && n.status !== "out-of-scope"));
|
|
968
|
+
}
|
|
927
969
|
export async function statusFlow(runId, json, deps) {
|
|
928
970
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
929
971
|
if (!runRes.ok)
|
|
@@ -933,15 +975,18 @@ export async function statusFlow(runId, json, deps) {
|
|
|
933
975
|
if (!itemsRes.ok)
|
|
934
976
|
return errorResult(itemsRes, json);
|
|
935
977
|
const items = itemsRes.data.items ?? [];
|
|
936
|
-
const
|
|
937
|
-
const
|
|
978
|
+
const hasFinal = hasAttachedCut(items);
|
|
979
|
+
const stop = resolveStop(classifyRun(run), hasFinal);
|
|
980
|
+
const guidance = stopLines(stop, runId, reviewUrl(deps.dashboardUrl, run));
|
|
938
981
|
const warnings = run.nodes
|
|
939
982
|
.filter((n) => n.warning)
|
|
940
983
|
.map((n) => ({ nodeId: n.nodeId, step: n.kind, warning: n.warning }));
|
|
941
984
|
if (json) {
|
|
942
985
|
return {
|
|
943
986
|
code: 0,
|
|
944
|
-
lines: [
|
|
987
|
+
lines: [
|
|
988
|
+
JSON.stringify({ runId, status: run.status, stop, items, hasFinal, warnings, guidance }),
|
|
989
|
+
],
|
|
945
990
|
};
|
|
946
991
|
}
|
|
947
992
|
const revoicedByScene = new Set();
|
|
@@ -972,7 +1017,8 @@ export async function statusFlow(runId, json, deps) {
|
|
|
972
1017
|
: `Ad run ${runId} — ${displayRunStatus(run.status)}`;
|
|
973
1018
|
const lines = [
|
|
974
1019
|
headline,
|
|
975
|
-
...
|
|
1020
|
+
...(carryingOn(run) ? [CARRYING_ON_LINE] : []),
|
|
1021
|
+
...guidance,
|
|
976
1022
|
...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
|
|
977
1023
|
...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
|
|
978
1024
|
];
|
|
@@ -984,6 +1030,12 @@ export async function statusFlow(runId, json, deps) {
|
|
|
984
1030
|
for (const sceneIndex of [...byScene.keys()].sort((a, b) => a - b)) {
|
|
985
1031
|
const row = byScene.get(sceneIndex);
|
|
986
1032
|
lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
|
|
1033
|
+
if (row.clip?.lastRedo)
|
|
1034
|
+
lines.push(` clip: ${row.clip.lastRedo.label}`);
|
|
1035
|
+
if (row.voiceover?.lastRedo)
|
|
1036
|
+
lines.push(` voice: ${row.voiceover.lastRedo.label}`);
|
|
1037
|
+
if (row.frame?.lastRedo)
|
|
1038
|
+
lines.push(` picture: ${row.frame.lastRedo.label}`);
|
|
987
1039
|
for (const finding of row.clip?.findings ?? []) {
|
|
988
1040
|
lines.push(` ${finding.code} (${finding.severity}): ${finding.detail}`);
|
|
989
1041
|
const judgeSeverity = finding.judgeSeverity ?? finding.severity;
|
|
@@ -1095,14 +1147,30 @@ function sceneCardLines(scene) {
|
|
|
1095
1147
|
lines.push(` picture: ${scene.frame.status}`);
|
|
1096
1148
|
return lines;
|
|
1097
1149
|
}
|
|
1098
|
-
export async function approveFlow(runId,
|
|
1099
|
-
const res = await deps.post(APPROVE_PATH, {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1150
|
+
export async function approveFlow(runId, opts, deps) {
|
|
1151
|
+
const res = await deps.post(APPROVE_PATH, {
|
|
1152
|
+
runId,
|
|
1153
|
+
...(opts.approveStaleCut ? { approveStaleCut: true } : {}),
|
|
1154
|
+
});
|
|
1155
|
+
if (!res.ok) {
|
|
1156
|
+
const refusal = errorResult(res, opts.json);
|
|
1157
|
+
const staleCut = !opts.json && videoApiError(res).includes("approve this cut anyway");
|
|
1158
|
+
return staleCut
|
|
1159
|
+
? {
|
|
1160
|
+
...refusal,
|
|
1161
|
+
lines: [
|
|
1162
|
+
...refusal.lines,
|
|
1163
|
+
`To deliver it anyway: exodus video approve ${runId} --approve-stale-cut`,
|
|
1164
|
+
],
|
|
1165
|
+
}
|
|
1166
|
+
: refusal;
|
|
1167
|
+
}
|
|
1168
|
+
const data = res.data;
|
|
1169
|
+
const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
|
|
1170
|
+
if (opts.json) {
|
|
1171
|
+
return { code: 0, lines: [JSON.stringify({ ok: true, runId, warnings, data: res.data })] };
|
|
1172
|
+
}
|
|
1173
|
+
const voices = data?.voices;
|
|
1106
1174
|
const summary = voices
|
|
1107
1175
|
? voiceSummaryLines(Array.isArray(voices.cast) ? voices.cast : [], Array.isArray(voices.notices) ? voices.notices : [])
|
|
1108
1176
|
: [];
|
|
@@ -1110,6 +1178,7 @@ export async function approveFlow(runId, json, deps) {
|
|
|
1110
1178
|
code: 0,
|
|
1111
1179
|
lines: [
|
|
1112
1180
|
"Approved.",
|
|
1181
|
+
...warnings,
|
|
1113
1182
|
...(summary.length > 0 ? ["", ...summary, ""] : []),
|
|
1114
1183
|
`See what happens next: exodus video status ${runId}`,
|
|
1115
1184
|
],
|
|
@@ -1167,8 +1236,9 @@ export async function retryFrameFlow(runId, nodeId, sceneIndex, note, json, deps
|
|
|
1167
1236
|
],
|
|
1168
1237
|
};
|
|
1169
1238
|
}
|
|
1170
|
-
const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip.
|
|
1171
|
-
"re-cut
|
|
1239
|
+
const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the new clip. If this redo replaces the clip, " +
|
|
1240
|
+
"exodus video approve stops until you pull the pieces again, re-cut and upload again, " +
|
|
1241
|
+
"or pass --approve-stale-cut to deliver the cut you uploaded as it is.";
|
|
1172
1242
|
export function planClipRedo(run, items, target) {
|
|
1173
1243
|
const stop = classifyRun(run);
|
|
1174
1244
|
const found = findClipRow(run, items, target);
|
|
@@ -1201,7 +1271,7 @@ export function planClipRedo(run, items, target) {
|
|
|
1201
1271
|
};
|
|
1202
1272
|
}
|
|
1203
1273
|
function findClipRow(run, items, target) {
|
|
1204
|
-
const uploadedCut = items
|
|
1274
|
+
const uploadedCut = hasAttachedCut(items);
|
|
1205
1275
|
const rows = items.filter((i) => i.itemKind === "clip" &&
|
|
1206
1276
|
i.sceneIndex === target.sceneIndex &&
|
|
1207
1277
|
(target.nodeId === undefined || i.nodeId === target.nodeId));
|
|
@@ -1987,8 +2057,9 @@ export async function run(flags, occurrences) {
|
|
|
1987
2057
|
return printResult(await statusFlow(runId, json, defaultDeps));
|
|
1988
2058
|
if (sub === "storyboard")
|
|
1989
2059
|
return printResult(await storyboardFlow(runId, json, defaultDeps));
|
|
1990
|
-
if (sub === "approve")
|
|
1991
|
-
return printResult(await approveFlow(runId, json, defaultDeps));
|
|
2060
|
+
if (sub === "approve") {
|
|
2061
|
+
return printResult(await approveFlow(runId, { json, approveStaleCut: flags["approve-stale-cut"] === true }, defaultDeps));
|
|
2062
|
+
}
|
|
1992
2063
|
if (sub === "flag") {
|
|
1993
2064
|
const note = flagString(flags, "note");
|
|
1994
2065
|
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, reviewUrl, stopLines, } from "./video.js";
|
|
13
|
+
import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, 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
|
|
@@ -2085,9 +2095,10 @@ async function waitForRun(runId, opts, deps) {
|
|
|
2085
2095
|
const saved = opts.out !== undefined && terminalRun
|
|
2086
2096
|
? await saveDeliveries(terminalRun, opts.out, deps)
|
|
2087
2097
|
: undefined;
|
|
2088
|
-
const
|
|
2098
|
+
const parkStop = !pollResult.timedOut && isRecord(pollResult.data)
|
|
2089
2099
|
? videoParkStop(pollResult.data)
|
|
2090
2100
|
: undefined;
|
|
2101
|
+
const videoStop = parkStop ? await resolveStopAtPark(parkStop, runId, deps) : undefined;
|
|
2091
2102
|
if (opts.json) {
|
|
2092
2103
|
return {
|
|
2093
2104
|
code: pollResult.ok ? 0 : 1,
|
|
@@ -2449,6 +2460,7 @@ export async function triggersFireFlow(workflowRef, opts, deps) {
|
|
|
2449
2460
|
...(opts.imageRigOverrides
|
|
2450
2461
|
? { imageRigOverrides: opts.imageRigOverrides }
|
|
2451
2462
|
: {}),
|
|
2463
|
+
...(opts.voices ? { voices: opts.voices } : {}),
|
|
2452
2464
|
});
|
|
2453
2465
|
if (!res.ok)
|
|
2454
2466
|
return triggerErrorResult(res, verb, opts.json);
|
|
@@ -3312,8 +3324,10 @@ export async function run(flags, occurrences) {
|
|
|
3312
3324
|
}
|
|
3313
3325
|
}
|
|
3314
3326
|
let fireOverrides;
|
|
3327
|
+
let fireVoices;
|
|
3315
3328
|
try {
|
|
3316
3329
|
fireOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
|
|
3330
|
+
fireVoices = parseVoicesFlag(occurrences, defaultDeps.readFile);
|
|
3317
3331
|
}
|
|
3318
3332
|
catch (e) {
|
|
3319
3333
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3323,6 +3337,7 @@ export async function run(flags, occurrences) {
|
|
|
3323
3337
|
n,
|
|
3324
3338
|
text: flagString(flags, "text"),
|
|
3325
3339
|
imageRigOverrides: fireOverrides,
|
|
3340
|
+
voices: fireVoices,
|
|
3326
3341
|
wait: flags["wait"] === true,
|
|
3327
3342
|
json,
|
|
3328
3343
|
onProgressLine: (line) => console.log(line),
|