@aicopycoders/exodus 2026.10.1200-beta.2 → 2026.10.1600-beta.1
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 +88 -22
- package/assets/skills/exodus-video/scripts/first-cut.mjs +135 -8
- package/assets/skills/exodus-workflow/SKILL.md +1 -1
- package/dist/commands/video-customer.d.ts +12 -0
- package/dist/commands/video.d.ts +39 -2
- package/dist/commands/video.js +392 -65
- package/dist/commands/whoami.js +3 -1
- package/dist/lib/backend-hint.d.ts +1 -0
- package/dist/lib/backend-hint.js +17 -3
- package/package.json +1 -1
package/dist/commands/video.js
CHANGED
|
@@ -14,6 +14,11 @@ function joinedOr(items) {
|
|
|
14
14
|
return items[0] ?? "";
|
|
15
15
|
return `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
|
|
16
16
|
}
|
|
17
|
+
function joinedAnd(items) {
|
|
18
|
+
if (items.length <= 1)
|
|
19
|
+
return items[0] ?? "";
|
|
20
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
21
|
+
}
|
|
17
22
|
export function scriptRunPickProblem(options, conceit, style) {
|
|
18
23
|
const keys = options.conceits.map((row) => row.key);
|
|
19
24
|
if (!keys.includes(conceit)) {
|
|
@@ -64,6 +69,8 @@ The whole loop, in order:
|
|
|
64
69
|
and who pays. Add --set to give someone a voice, before any clip is made.
|
|
65
70
|
|
|
66
71
|
6. exodus video approve <runId> (looks right — keep going)
|
|
72
|
+
Guided runs only. A One-shot run approves its own storyboard and stops
|
|
73
|
+
here only when its checks find a problem.
|
|
67
74
|
exodus video retry-frame <runId> --node <nodeId> --scene <n>
|
|
68
75
|
Redo ONE still at the pixel gate. Neighbours stay. The gate holds.
|
|
69
76
|
|
|
@@ -95,9 +102,9 @@ The whole loop, in order:
|
|
|
95
102
|
|
|
96
103
|
10. exodus video upload <runId> --file cut.mp4
|
|
97
104
|
Attaches your cut to the run and prints the page to watch it on. On a
|
|
98
|
-
One-shot run the upload delivers the ad
|
|
105
|
+
One-shot run the upload delivers the ad and you are done.
|
|
99
106
|
|
|
100
|
-
11. exodus video approve <runId>
|
|
107
|
+
11. Guided runs only: exodus video approve <runId>
|
|
101
108
|
Or click Approve on the page from step 10. If you redid a clip after
|
|
102
109
|
uploading, this stops and says which scenes changed. Upload a fresh cut,
|
|
103
110
|
or add --approve-stale-cut to deliver the cut you already uploaded.
|
|
@@ -109,10 +116,11 @@ Usage:
|
|
|
109
116
|
exodus video retry-frame <runId> [--node <nodeId>] --scene <n> [--note "..."] [--json]
|
|
110
117
|
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--voice-first] [--json]
|
|
111
118
|
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
|
|
112
|
-
exodus video voices <runId> [--set <character>=<
|
|
119
|
+
exodus video voices <runId> [--set <character>=<voice>] [--clear <character>] [--from <file.json>] [--json]
|
|
120
|
+
exodus video voices [<runId>] --catalog [--json]
|
|
113
121
|
exodus video pull <runId> --out <dir> [--json]
|
|
114
122
|
exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
|
|
115
|
-
exodus video start --script <file> --conceit <kind> --style <look> [--direction "<note>"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice <native|voice-first>] [--music] [--
|
|
123
|
+
exodus video start --script <file> --conceit <kind> --style <look> [--direction "<note>"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice <native|voice-first>] [--music] [--one-shot] [--director] [--wait] [--json]
|
|
116
124
|
exodus video director <runId> <on|off|follow> [--json]
|
|
117
125
|
|
|
118
126
|
Options:
|
|
@@ -135,15 +143,29 @@ Options:
|
|
|
135
143
|
--approve-stale-cut Deliver the cut you uploaded even though a clip changed
|
|
136
144
|
after you uploaded it (approve). Without it, approving
|
|
137
145
|
stops and names the scenes that changed
|
|
138
|
-
--set <who>=<
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
--set <who>=<voice> Give a character a voice (voices). The voice is a
|
|
147
|
+
catalog voice's name, like George, or an ElevenLabs
|
|
148
|
+
voice ID. Name the character by its ID or by the name
|
|
149
|
+
your script uses. Repeat it once per character
|
|
141
150
|
--clear <who> Take a character's voice off again (voices). Repeatable
|
|
142
|
-
--from <file.json> A file of characters and
|
|
143
|
-
--clear win over the same name in the file
|
|
151
|
+
--from <file.json> A file of characters and voices, by name or ID (voices).
|
|
152
|
+
--set and --clear win over the same name in the file
|
|
153
|
+
--catalog List the voices you can pick by name (voices). No run
|
|
154
|
+
needed. With a run, also the best few for each
|
|
155
|
+
character who speaks
|
|
144
156
|
--rejected-draft Print the storyboard draft the system turned down
|
|
145
157
|
(status). It was thrown away — a record only
|
|
146
|
-
--script <file> Text file of what the ad says (start)
|
|
158
|
+
--script <file> Text file of what the ad says (start). Anything works:
|
|
159
|
+
a plain transcript, a rough draft, or a description of
|
|
160
|
+
the ad. With --conceit, the server lays it out in the
|
|
161
|
+
format the video needs without changing a spoken word,
|
|
162
|
+
and says what it did. A description gets a script
|
|
163
|
+
written from it, printed, and the run starts on it. If
|
|
164
|
+
the server can't tell who says a line, it asks and
|
|
165
|
+
starts nothing
|
|
166
|
+
--stop-at-script Print the script the run would start on, and start
|
|
167
|
+
nothing (start, with --conceit). To read a written or
|
|
168
|
+
laid-out script first; send it back with --script
|
|
147
169
|
--conceit <kind> Kind of ad (start)
|
|
148
170
|
--style <look> Look (start)
|
|
149
171
|
The server lists the kinds and looks it offers. An unknown word prints them.
|
|
@@ -163,9 +185,14 @@ Options:
|
|
|
163
185
|
--music Put a music bed under the ad (start). An ad has no music
|
|
164
186
|
unless you ask for it here
|
|
165
187
|
--no-music Leave the music bed off (start), which it already is
|
|
188
|
+
--one-shot Make the whole video with no stop (start, with
|
|
189
|
+
--conceit). The app approves its own storyboard when
|
|
190
|
+
its checks pass and goes straight on to the clips,
|
|
191
|
+
which is the part that costs. Without it the run is
|
|
192
|
+
Guided: it stops at the storyboard for your yes
|
|
166
193
|
--review-storyboard Stop at the storyboard so you can approve it yourself
|
|
167
|
-
(start, with --conceit).
|
|
168
|
-
|
|
194
|
+
(start, with --conceit). This is the default, so the
|
|
195
|
+
flag changes nothing. Kept for scripts that pass it
|
|
169
196
|
--director Turn the trial director on for this ad (start, with
|
|
170
197
|
--conceit). Admins only. To switch it on or off later:
|
|
171
198
|
exodus video director <runId> on|off|follow, where
|
|
@@ -177,7 +204,8 @@ Options:
|
|
|
177
204
|
|
|
178
205
|
Who can use what. Admins get every command here. Beta testers with video
|
|
179
206
|
switched on get these, on their own brands: start (--script, --conceit,
|
|
180
|
-
--style, --direction, --frame, --pacing, --music, --
|
|
207
|
+
--style, --direction, --frame, --pacing, --music, --one-shot,
|
|
208
|
+
--review-storyboard, --stop-at-script),
|
|
181
209
|
status, storyboard, approve (the storyboard), voices, retry-frame --scene <n> (redo one scene's
|
|
182
210
|
picture while the storyboard waits), pull (the finished video, each scene's
|
|
183
211
|
clip and voice, and the music) and upload. These stay admin-only:
|
|
@@ -197,7 +225,9 @@ Examples:
|
|
|
197
225
|
exodus video retry-clip run_123 --scene 3 --voice-first
|
|
198
226
|
exodus video revoice run_123 --all
|
|
199
227
|
exodus video voices run_123
|
|
200
|
-
exodus video voices
|
|
228
|
+
exodus video voices --catalog
|
|
229
|
+
exodus video voices run_123 --catalog
|
|
230
|
+
exodus video voices run_123 --set C1=George --set "HOST 2=def456voiceid"
|
|
201
231
|
exodus video voices run_123 --from voices.json
|
|
202
232
|
exodus video pull run_123 --out ./ad-run_123
|
|
203
233
|
exodus video upload run_123 --file ./cut.mp4
|
|
@@ -639,6 +669,7 @@ export function planFailureLines(node, runId) {
|
|
|
639
669
|
}
|
|
640
670
|
export const MUSIC_HEARD_CODE = "music-heard";
|
|
641
671
|
export const CAST_LEDGER_BASE = HELPER_LEDGER_BASE + 10000;
|
|
672
|
+
export const CAST_VIEW_LEDGER_BASE = CAST_LEDGER_BASE + 1000;
|
|
642
673
|
export function musicBedState(run) {
|
|
643
674
|
if (run.musicBed === undefined)
|
|
644
675
|
return "unknown";
|
|
@@ -737,9 +768,25 @@ function castVoicePins(artifact) {
|
|
|
737
768
|
}
|
|
738
769
|
return pins;
|
|
739
770
|
}
|
|
771
|
+
function lockViews(member, viewRows) {
|
|
772
|
+
return (member.identityRefs ?? []).flatMap((ref) => {
|
|
773
|
+
if (!ref.view)
|
|
774
|
+
return [];
|
|
775
|
+
const row = viewRows.find(({ artifact: art }) => art?.type !== "image"
|
|
776
|
+
? false
|
|
777
|
+
: art.storageId
|
|
778
|
+
? art.storageId === ref.storageId
|
|
779
|
+
: !!art.imageUrl && art.imageUrl === ref.imageUrl);
|
|
780
|
+
const url = (row?.artifact?.type === "image" ? row.artifact.imageUrl : undefined) ?? ref.imageUrl;
|
|
781
|
+
return url ? [{ view: ref.view, url }] : [];
|
|
782
|
+
});
|
|
783
|
+
}
|
|
740
784
|
function planCastRefs(run, items, pins) {
|
|
741
785
|
const ledger = reservedCastFrames(items);
|
|
742
|
-
const
|
|
786
|
+
const isViewRow = ([sceneIndex, row]) => sceneIndex >= CAST_VIEW_LEDGER_BASE || (row.artifact?.type === "image" && row.artifact.view !== undefined);
|
|
787
|
+
const sorted = [...ledger.entries()].sort(([a], [b]) => a - b);
|
|
788
|
+
const ledgerRows = sorted.filter((r) => !isViewRow(r));
|
|
789
|
+
const viewRows = sorted.filter(isViewRow).map(([, row]) => row);
|
|
743
790
|
const locked = run.castLock?.cast;
|
|
744
791
|
const rows = [];
|
|
745
792
|
if (locked !== undefined && locked.length > 0) {
|
|
@@ -765,6 +812,7 @@ function planCastRefs(run, items, pins) {
|
|
|
765
812
|
status: item?.status ?? (url ? "done" : "missing"),
|
|
766
813
|
error: item?.error ?? null,
|
|
767
814
|
stem: `cast-${member.characterId}`,
|
|
815
|
+
views: lockViews(member, viewRows),
|
|
768
816
|
});
|
|
769
817
|
}
|
|
770
818
|
for (const [sceneIndex, item] of ledgerRows) {
|
|
@@ -780,6 +828,7 @@ function planCastRefs(run, items, pins) {
|
|
|
780
828
|
status: item.status,
|
|
781
829
|
error: item.error ?? null,
|
|
782
830
|
stem: `cast-ref-${String(sceneIndex - CAST_LEDGER_BASE).padStart(2, "0")}`,
|
|
831
|
+
views: [],
|
|
783
832
|
});
|
|
784
833
|
}
|
|
785
834
|
}
|
|
@@ -794,6 +843,7 @@ function planCastRefs(run, items, pins) {
|
|
|
794
843
|
status: item.status,
|
|
795
844
|
error: item.error ?? null,
|
|
796
845
|
stem: `cast-ref-${String(sceneIndex - CAST_LEDGER_BASE).padStart(2, "0")}`,
|
|
846
|
+
views: [],
|
|
797
847
|
});
|
|
798
848
|
}
|
|
799
849
|
}
|
|
@@ -802,6 +852,7 @@ function planCastRefs(run, items, pins) {
|
|
|
802
852
|
for (const row of rows) {
|
|
803
853
|
const file = row.url ? `${row.stem}.${extFor(row.url, "image")}` : null;
|
|
804
854
|
const pin = row.characterId ? pins.get(row.characterId) : undefined;
|
|
855
|
+
const views = row.views.map((v) => ({ view: v.view, file: `${row.stem}-${v.view}.${extFor(v.url, "image")}`, url: v.url }));
|
|
805
856
|
cast.push({
|
|
806
857
|
characterId: row.characterId,
|
|
807
858
|
name: row.name,
|
|
@@ -811,9 +862,12 @@ function planCastRefs(run, items, pins) {
|
|
|
811
862
|
voiceId: pin?.voiceId ?? null,
|
|
812
863
|
voiceLabel: pin?.voiceLabel ?? null,
|
|
813
864
|
voiceDescription: pin?.voiceDescription ?? null,
|
|
865
|
+
...(views.length ? { views: views.map(({ view, file: viewFile }) => ({ view, file: viewFile })) } : {}),
|
|
814
866
|
});
|
|
815
867
|
if (file && row.url)
|
|
816
868
|
downloads.push({ file, url: row.url });
|
|
869
|
+
for (const v of views)
|
|
870
|
+
downloads.push({ file: v.file, url: v.url });
|
|
817
871
|
}
|
|
818
872
|
return { cast, downloads };
|
|
819
873
|
}
|
|
@@ -1105,6 +1159,10 @@ export function markPullFailure(manifest, failure) {
|
|
|
1105
1159
|
for (const ref of manifest.cast) {
|
|
1106
1160
|
if (ref.file === failure.file)
|
|
1107
1161
|
ref.file = null;
|
|
1162
|
+
for (const view of ref.views ?? []) {
|
|
1163
|
+
if (view.file === failure.file)
|
|
1164
|
+
view.file = null;
|
|
1165
|
+
}
|
|
1108
1166
|
}
|
|
1109
1167
|
if (manifest.narration?.file === failure.file)
|
|
1110
1168
|
manifest.narration = null;
|
|
@@ -1248,11 +1306,17 @@ export async function startFlow(opts, deps) {
|
|
|
1248
1306
|
lines: opts.json ? [JSON.stringify({ runId, url })] : lines,
|
|
1249
1307
|
};
|
|
1250
1308
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1309
|
+
return startedThenWait(runId, [`Started ad run ${runId}`, `Watch it: ${url}`], { ...opts, url }, deps);
|
|
1310
|
+
}
|
|
1311
|
+
async function startedThenWait(runId, head, opts, deps) {
|
|
1312
|
+
const stream = opts.json ? undefined : opts.onProgressLine;
|
|
1313
|
+
if (stream)
|
|
1314
|
+
for (const line of [...head, ""])
|
|
1315
|
+
stream(line);
|
|
1316
|
+
const waited = await waitFlow(runId, { json: opts.json, url: opts.url, onProgressLine: stream }, deps);
|
|
1317
|
+
if (opts.json || stream)
|
|
1318
|
+
return waited;
|
|
1319
|
+
return { code: waited.code, lines: [...head, "", ...waited.lines] };
|
|
1256
1320
|
}
|
|
1257
1321
|
function nonBlank(value) {
|
|
1258
1322
|
if (value === undefined || value.trim().length === 0)
|
|
@@ -1334,13 +1398,32 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1334
1398
|
return { kind: "usage", line: pacingFlag.line };
|
|
1335
1399
|
const videoModel = videoModelFlag.value;
|
|
1336
1400
|
const voiceMode = voiceFlag.value;
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1401
|
+
const reviewStoryboardFlag = occurrences.some((o) => o.flag === "review-storyboard");
|
|
1402
|
+
const oneShot = occurrences.some((o) => o.flag === "one-shot");
|
|
1403
|
+
if (showId && reviewStoryboardFlag) {
|
|
1339
1404
|
return {
|
|
1340
1405
|
kind: "usage",
|
|
1341
1406
|
line: "--review-storyboard works with --conceit, not --show. A Show's ad always stops at the storyboard for you.",
|
|
1342
1407
|
};
|
|
1343
1408
|
}
|
|
1409
|
+
if (showId && oneShot) {
|
|
1410
|
+
return {
|
|
1411
|
+
kind: "usage",
|
|
1412
|
+
line: "--one-shot works with --conceit, not --show. A Show's ad always stops at the storyboard for you.",
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
if (oneShot && reviewStoryboardFlag) {
|
|
1416
|
+
return {
|
|
1417
|
+
kind: "usage",
|
|
1418
|
+
line: "Pass --one-shot or --review-storyboard, not both.",
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (occurrences.some((o) => o.flag === "one-shot" && o.value !== undefined && !o.value.startsWith("--"))) {
|
|
1422
|
+
return {
|
|
1423
|
+
kind: "usage",
|
|
1424
|
+
line: "--one-shot takes no value. Leave it off to stop at the storyboard.",
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1344
1427
|
const directorFlags = occurrences.filter((o) => o.flag === "director");
|
|
1345
1428
|
if (directorFlags.some((o) => o.value !== undefined && !o.value.startsWith("--"))) {
|
|
1346
1429
|
return {
|
|
@@ -1349,6 +1432,20 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1349
1432
|
};
|
|
1350
1433
|
}
|
|
1351
1434
|
const director = directorFlags.length > 0;
|
|
1435
|
+
const stopFlags = occurrences.filter((o) => o.flag === "stop-at-script");
|
|
1436
|
+
if (stopFlags.some((o) => o.value !== undefined && !o.value.startsWith("--"))) {
|
|
1437
|
+
return {
|
|
1438
|
+
kind: "usage",
|
|
1439
|
+
line: "--stop-at-script takes no value. Leave it off to start the run.",
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
const stopAtScript = stopFlags.length > 0;
|
|
1443
|
+
if (showId && stopAtScript) {
|
|
1444
|
+
return {
|
|
1445
|
+
kind: "usage",
|
|
1446
|
+
line: "--stop-at-script works with --conceit, not --show. A Show's script is read as you wrote it.",
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1352
1449
|
if (showId && director) {
|
|
1353
1450
|
return {
|
|
1354
1451
|
kind: "usage",
|
|
@@ -1417,10 +1514,12 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1417
1514
|
opts.voiceMode = voiceMode;
|
|
1418
1515
|
if (musicChoice.music !== undefined)
|
|
1419
1516
|
opts.music = musicChoice.music;
|
|
1420
|
-
if (
|
|
1517
|
+
if (!oneShot)
|
|
1421
1518
|
opts.reviewStoryboard = true;
|
|
1422
1519
|
if (director)
|
|
1423
1520
|
opts.director = true;
|
|
1521
|
+
if (stopAtScript)
|
|
1522
|
+
opts.stopAtScript = true;
|
|
1424
1523
|
if (frameFlag.value || pacingFlag.value) {
|
|
1425
1524
|
opts.shots = {
|
|
1426
1525
|
...(frameFlag.value ? { frame: frameFlag.value } : {}),
|
|
@@ -1474,6 +1573,135 @@ function startedVideoChoice(value) {
|
|
|
1474
1573
|
return null;
|
|
1475
1574
|
return { videoModel: value.videoModel, voiceMode: value.voiceMode };
|
|
1476
1575
|
}
|
|
1576
|
+
function stringsOf(value) {
|
|
1577
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1578
|
+
}
|
|
1579
|
+
function isCastNote(value) {
|
|
1580
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.description === "string";
|
|
1581
|
+
}
|
|
1582
|
+
function scriptTidiedOf(value) {
|
|
1583
|
+
if (!isRecord(value))
|
|
1584
|
+
return null;
|
|
1585
|
+
const added = isRecord(value.added) ? value.added : {};
|
|
1586
|
+
return {
|
|
1587
|
+
setAside: stringsOf(value.setAside),
|
|
1588
|
+
added: {
|
|
1589
|
+
names: stringsOf(added.names),
|
|
1590
|
+
descriptions: Array.isArray(added.descriptions) ? added.descriptions.filter(isCastNote) : [],
|
|
1591
|
+
},
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function scriptTidiedHead(tidied) {
|
|
1595
|
+
if (!tidied)
|
|
1596
|
+
return [];
|
|
1597
|
+
const { setAside } = tidied;
|
|
1598
|
+
const { names, descriptions } = tidied.added;
|
|
1599
|
+
return [
|
|
1600
|
+
"We laid your script out in the format the video needs. " +
|
|
1601
|
+
(setAside.length === 0
|
|
1602
|
+
? "Every spoken word is unchanged."
|
|
1603
|
+
: "Every spoken line we kept is word for word what you wrote."),
|
|
1604
|
+
...(names.length === 1 ? [`We called the person your script doesn't name ${names[0]}.`] : []),
|
|
1605
|
+
...(names.length > 1 ? [`We called the people your script doesn't name ${joinedAnd(names)}.`] : []),
|
|
1606
|
+
...(descriptions.length > 0
|
|
1607
|
+
? [
|
|
1608
|
+
"We added a short note on who each person is. It's never spoken; it only guides the video:",
|
|
1609
|
+
...descriptions.map(({ name, description }) => ` ${name} — ${description}`),
|
|
1610
|
+
]
|
|
1611
|
+
: []),
|
|
1612
|
+
...(setAside.length > 0
|
|
1613
|
+
? [
|
|
1614
|
+
"We left out these lines because they don't look like part of the ad:",
|
|
1615
|
+
...setAside.map((line) => ` "${line}"`),
|
|
1616
|
+
]
|
|
1617
|
+
: []),
|
|
1618
|
+
"",
|
|
1619
|
+
];
|
|
1620
|
+
}
|
|
1621
|
+
function scriptWrittenHead(written, started) {
|
|
1622
|
+
if (written === null)
|
|
1623
|
+
return [];
|
|
1624
|
+
return [
|
|
1625
|
+
started
|
|
1626
|
+
? "Your file described the ad, so we wrote this script from it and started the run on it:"
|
|
1627
|
+
: "Your file described the ad, so we wrote this script from it:",
|
|
1628
|
+
"",
|
|
1629
|
+
...written.split("\n").map((line) => (line ? ` ${line}` : "")),
|
|
1630
|
+
"",
|
|
1631
|
+
];
|
|
1632
|
+
}
|
|
1633
|
+
function writtenOf(value) {
|
|
1634
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
1635
|
+
}
|
|
1636
|
+
function scriptQuestionResult(res, json) {
|
|
1637
|
+
const data = res.data;
|
|
1638
|
+
if (!isRecord(data) || !isRecord(data.error) || data.error.code !== "SCRIPT_QUESTION")
|
|
1639
|
+
return null;
|
|
1640
|
+
const message = formatApiError(res);
|
|
1641
|
+
const lines = stringsOf(data.lines);
|
|
1642
|
+
if (json) {
|
|
1643
|
+
return {
|
|
1644
|
+
code: 1,
|
|
1645
|
+
lines: [JSON.stringify({ ok: false, status: res.status, code: "SCRIPT_QUESTION", error: message, lines })],
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
return { code: 1, lines: [message] };
|
|
1649
|
+
}
|
|
1650
|
+
function scriptStartRefused(res, json) {
|
|
1651
|
+
const data = isRecord(res.data) ? res.data : {};
|
|
1652
|
+
const tidied = data.tidied === undefined ? null : scriptTidiedOf(data.tidied);
|
|
1653
|
+
const written = writtenOf(data.written);
|
|
1654
|
+
if (!tidied && written === null)
|
|
1655
|
+
return errorResult(res, json);
|
|
1656
|
+
const script = typeof data.script === "string" ? data.script : null;
|
|
1657
|
+
const error = videoApiError(res);
|
|
1658
|
+
return {
|
|
1659
|
+
code: 1,
|
|
1660
|
+
lines: json
|
|
1661
|
+
? [
|
|
1662
|
+
JSON.stringify({
|
|
1663
|
+
ok: false,
|
|
1664
|
+
status: res.status,
|
|
1665
|
+
error,
|
|
1666
|
+
...(tidied ? { tidied } : {}),
|
|
1667
|
+
...(written !== null ? { written } : {}),
|
|
1668
|
+
...(script !== null ? { script } : {}),
|
|
1669
|
+
}),
|
|
1670
|
+
]
|
|
1671
|
+
: [...scriptTidiedHead(tidied), ...scriptWrittenHead(written, false), error],
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
function stoppedAtScriptResult(data, json) {
|
|
1675
|
+
const script = typeof data.script === "string" ? data.script : "";
|
|
1676
|
+
const tidied = data.tidied === undefined ? null : scriptTidiedOf(data.tidied);
|
|
1677
|
+
const written = writtenOf(data.written);
|
|
1678
|
+
if (json) {
|
|
1679
|
+
return {
|
|
1680
|
+
code: 0,
|
|
1681
|
+
lines: [
|
|
1682
|
+
JSON.stringify({
|
|
1683
|
+
stoppedAtScript: true,
|
|
1684
|
+
script,
|
|
1685
|
+
...(tidied ? { tidied } : {}),
|
|
1686
|
+
...(written !== null ? { written } : {}),
|
|
1687
|
+
}),
|
|
1688
|
+
],
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
code: 0,
|
|
1693
|
+
lines: [
|
|
1694
|
+
...scriptTidiedHead(tidied),
|
|
1695
|
+
written !== null
|
|
1696
|
+
? "Your file described the ad, so we wrote this script from it. Nothing has started."
|
|
1697
|
+
: "This is the script the run would start on. Nothing has started.",
|
|
1698
|
+
"",
|
|
1699
|
+
...script.split("\n").map((line) => (line ? ` ${line}` : "")),
|
|
1700
|
+
"",
|
|
1701
|
+
"To start it, save it to a file and run the same command with --script <that file> and without --stop-at-script.",
|
|
1702
|
+
],
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1477
1705
|
function scriptStartedLines(runId, url, card, video, director) {
|
|
1478
1706
|
return [
|
|
1479
1707
|
`Started ad run ${runId}`,
|
|
@@ -1490,7 +1718,7 @@ function scriptStartedLines(runId, url, card, video, director) {
|
|
|
1490
1718
|
`Watch it: ${url}`,
|
|
1491
1719
|
];
|
|
1492
1720
|
}
|
|
1493
|
-
function
|
|
1721
|
+
function waitJsonWith(waited, facts) {
|
|
1494
1722
|
const first = waited.lines[0];
|
|
1495
1723
|
if (typeof first !== "string")
|
|
1496
1724
|
return waited;
|
|
@@ -1505,7 +1733,7 @@ function waitJsonWithCard(waited, card, video) {
|
|
|
1505
1733
|
return waited;
|
|
1506
1734
|
return {
|
|
1507
1735
|
code: waited.code,
|
|
1508
|
-
lines: [JSON.stringify({ ...parsed,
|
|
1736
|
+
lines: [JSON.stringify({ ...parsed, ...facts })],
|
|
1509
1737
|
};
|
|
1510
1738
|
}
|
|
1511
1739
|
export async function startScriptFlow(opts, deps) {
|
|
@@ -1547,9 +1775,13 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1547
1775
|
...(opts.reviewStoryboard ? { reviewStoryboard: true } : {}),
|
|
1548
1776
|
...(opts.director ? { director: true } : {}),
|
|
1549
1777
|
...(opts.shots ? { shots: opts.shots } : {}),
|
|
1778
|
+
...(opts.stopAtScript ? { stopAtScript: true } : {}),
|
|
1550
1779
|
});
|
|
1551
1780
|
if (!res.ok)
|
|
1552
|
-
return
|
|
1781
|
+
return scriptQuestionResult(res, opts.json) ?? scriptStartRefused(res, opts.json);
|
|
1782
|
+
if (isRecord(res.data) && res.data.stoppedAtScript === true) {
|
|
1783
|
+
return stoppedAtScriptResult(res.data, opts.json);
|
|
1784
|
+
}
|
|
1553
1785
|
const customerStart = customerAnswer(res.data);
|
|
1554
1786
|
if (customerStart)
|
|
1555
1787
|
return customerStartedResult(customerStart, opts, deps);
|
|
@@ -1561,12 +1793,22 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1561
1793
|
const url = typeof started.url === "string" ? started.url : "";
|
|
1562
1794
|
const card = isResolvedCard(started.card) ? started.card : null;
|
|
1563
1795
|
const video = startedVideoChoice(started.videoChoice);
|
|
1796
|
+
const tidied = started.tidied === undefined ? null : scriptTidiedOf(started.tidied);
|
|
1797
|
+
const written = writtenOf(started.written);
|
|
1798
|
+
const facts = {
|
|
1799
|
+
card,
|
|
1800
|
+
...(video ? { videoChoice: video } : {}),
|
|
1801
|
+
...(tidied ? { tidied } : {}),
|
|
1802
|
+
...(written !== null ? { written } : {}),
|
|
1803
|
+
};
|
|
1804
|
+
const head = [...scriptTidiedHead(tidied), ...scriptWrittenHead(written, true)];
|
|
1564
1805
|
if (!opts.wait) {
|
|
1565
1806
|
return {
|
|
1566
1807
|
code: 0,
|
|
1567
1808
|
lines: opts.json
|
|
1568
|
-
? [JSON.stringify({ runId, url,
|
|
1809
|
+
? [JSON.stringify({ runId, url, ...facts })]
|
|
1569
1810
|
: [
|
|
1811
|
+
...head,
|
|
1570
1812
|
...scriptStartedLines(runId, url, card, video, opts.director === true),
|
|
1571
1813
|
"",
|
|
1572
1814
|
...(opts.reviewStoryboard
|
|
@@ -1579,20 +1821,23 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1579
1821
|
],
|
|
1580
1822
|
};
|
|
1581
1823
|
}
|
|
1582
|
-
const waited = await
|
|
1583
|
-
|
|
1584
|
-
return waitJsonWithCard(waited, card, video);
|
|
1585
|
-
return {
|
|
1586
|
-
code: waited.code,
|
|
1587
|
-
lines: [...scriptStartedLines(runId, url, card, video, opts.director === true), "", ...waited.lines],
|
|
1588
|
-
};
|
|
1824
|
+
const waited = await startedThenWait(runId, [...head, ...scriptStartedLines(runId, url, card, video, opts.director === true)], { ...opts, url }, deps);
|
|
1825
|
+
return opts.json ? waitJsonWith(waited, facts) : waited;
|
|
1589
1826
|
}
|
|
1590
1827
|
async function customerStartedResult(started, opts, deps) {
|
|
1591
1828
|
const { runId, dashboardUrl } = started;
|
|
1592
|
-
const
|
|
1829
|
+
const tidied = started.tidied === undefined ? null : scriptTidiedOf(started.tidied);
|
|
1830
|
+
const written = writtenOf(started.written);
|
|
1831
|
+
const head = [
|
|
1832
|
+
...scriptTidiedHead(tidied),
|
|
1833
|
+
...scriptWrittenHead(written, true),
|
|
1834
|
+
`Started video ${runId}`,
|
|
1835
|
+
`Watch it: ${dashboardUrl}`,
|
|
1836
|
+
];
|
|
1593
1837
|
if (opts.wait) {
|
|
1594
|
-
const waited = await
|
|
1595
|
-
|
|
1838
|
+
const waited = await startedThenWait(runId, head, { ...opts, url: dashboardUrl }, deps);
|
|
1839
|
+
const facts = { ...(tidied ? { tidied } : {}), ...(written !== null ? { written } : {}) };
|
|
1840
|
+
return opts.json && (tidied || written !== null) ? waitJsonWith(waited, facts) : waited;
|
|
1596
1841
|
}
|
|
1597
1842
|
if (opts.json)
|
|
1598
1843
|
return { code: 0, lines: [JSON.stringify(started)] };
|
|
@@ -1616,6 +1861,17 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
1616
1861
|
const lines = [];
|
|
1617
1862
|
let lastProgress = null;
|
|
1618
1863
|
let redoSeen = false;
|
|
1864
|
+
const progress = (line) => {
|
|
1865
|
+
if (line === lastProgress)
|
|
1866
|
+
return;
|
|
1867
|
+
lastProgress = line;
|
|
1868
|
+
if (opts.json)
|
|
1869
|
+
return;
|
|
1870
|
+
if (opts.onProgressLine)
|
|
1871
|
+
opts.onProgressLine(line);
|
|
1872
|
+
else
|
|
1873
|
+
lines.push(line);
|
|
1874
|
+
};
|
|
1619
1875
|
for (let poll = 0; poll < maxPolls; poll++) {
|
|
1620
1876
|
if (poll > 0)
|
|
1621
1877
|
await deps.sleep(interval);
|
|
@@ -1628,12 +1884,7 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
1628
1884
|
if (customer) {
|
|
1629
1885
|
const page = customerRunPage(deps.dashboardUrl, runId);
|
|
1630
1886
|
if (customerStageIsWorking(customer.stage)) {
|
|
1631
|
-
|
|
1632
|
-
if (progress !== lastProgress) {
|
|
1633
|
-
lastProgress = progress;
|
|
1634
|
-
if (!opts.json)
|
|
1635
|
-
lines.push(progress);
|
|
1636
|
-
}
|
|
1887
|
+
progress(customerStageLines(customer, page)[0]);
|
|
1637
1888
|
continue;
|
|
1638
1889
|
}
|
|
1639
1890
|
return {
|
|
@@ -1646,14 +1897,9 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
1646
1897
|
const run = asVideoRun(res.data);
|
|
1647
1898
|
const stop = classifyRun(run);
|
|
1648
1899
|
if (stop.at === "running") {
|
|
1649
|
-
|
|
1900
|
+
progress(stop.autoRedoScenes?.length
|
|
1650
1901
|
? autoRedoLine(stop.autoRedoScenes, stop.autoRedoReasons)
|
|
1651
|
-
: `Working: ${stageWord(stop.stage)}
|
|
1652
|
-
if (progress !== lastProgress) {
|
|
1653
|
-
lastProgress = progress;
|
|
1654
|
-
if (!opts.json)
|
|
1655
|
-
lines.push(progress);
|
|
1656
|
-
}
|
|
1902
|
+
: `Working: ${stageWord(stop.stage)}…`);
|
|
1657
1903
|
continue;
|
|
1658
1904
|
}
|
|
1659
1905
|
let items = null;
|
|
@@ -1665,12 +1911,7 @@ export async function waitFlow(runId, opts, deps) {
|
|
|
1665
1911
|
const redoing = items ? scenesBeingRedone(run, items) : [];
|
|
1666
1912
|
redoSeen = redoing.length > 0;
|
|
1667
1913
|
if (redoSeen) {
|
|
1668
|
-
|
|
1669
|
-
if (progress !== lastProgress) {
|
|
1670
|
-
lastProgress = progress;
|
|
1671
|
-
if (!opts.json)
|
|
1672
|
-
lines.push(progress);
|
|
1673
|
-
}
|
|
1914
|
+
progress(`Working: redoing scene${redoing.length === 1 ? "" : "s"} ${redoing.join(", ")}…`);
|
|
1674
1915
|
continue;
|
|
1675
1916
|
}
|
|
1676
1917
|
}
|
|
@@ -1818,6 +2059,10 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1818
2059
|
lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
|
|
1819
2060
|
if (row.clip?.lastRedo) {
|
|
1820
2061
|
lines.push(` clip: ${redoLine(row.clip.lastRedo)}`);
|
|
2062
|
+
const blocked = row.clip.lastRedo.voiceFirstBlockedBy;
|
|
2063
|
+
if (blocked?.length) {
|
|
2064
|
+
lines.push(` clip: asked for voice first, but redone the usual way — ${cantSayNames(blocked)}`);
|
|
2065
|
+
}
|
|
1821
2066
|
printDisplacedHistory(lines, row.clip.lastRedo);
|
|
1822
2067
|
}
|
|
1823
2068
|
if (row.clip?.lastRedo || row.clip?.flagged) {
|
|
@@ -1862,6 +2107,9 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1862
2107
|
lines.push("Narration master take");
|
|
1863
2108
|
}
|
|
1864
2109
|
}
|
|
2110
|
+
if (run.nodes.some((n) => n.kind === "storyboard" && n.status === "done")) {
|
|
2111
|
+
lines.push(...(await statusVoiceLines(runId, deps)));
|
|
2112
|
+
}
|
|
1865
2113
|
lines.push("", hasFinal && stop.at === "finished"
|
|
1866
2114
|
? "Final cut: approved and delivered."
|
|
1867
2115
|
: hasFinal
|
|
@@ -2027,7 +2275,7 @@ export async function storyboardFlow(runId, json, deps) {
|
|
|
2027
2275
|
if (summary.length > 0) {
|
|
2028
2276
|
lines.push("", ...summary);
|
|
2029
2277
|
if (canChange) {
|
|
2030
|
-
lines.push(`set voices with: exodus video voices ${runId} --set <character>=<
|
|
2278
|
+
lines.push(`set voices with: exodus video voices ${runId} --set <character>=<voice name or ID>`);
|
|
2031
2279
|
}
|
|
2032
2280
|
}
|
|
2033
2281
|
}
|
|
@@ -2512,8 +2760,8 @@ export function parseVoiceFlags(occurrences, readFile) {
|
|
|
2512
2760
|
const raw = valueFor(occurrence);
|
|
2513
2761
|
const eq = raw.lastIndexOf("=");
|
|
2514
2762
|
if (eq <= 0 || eq === raw.length - 1) {
|
|
2515
|
-
throw new Error("--set must look like <character>=<voice
|
|
2516
|
-
|
|
2763
|
+
throw new Error("--set must look like <character>=<voice>, where the voice is a catalog voice's name " +
|
|
2764
|
+
`or an ElevenLabs voice ID, for example --set C1=George (got "${raw}").`);
|
|
2517
2765
|
}
|
|
2518
2766
|
typed[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
|
|
2519
2767
|
asked = true;
|
|
@@ -2551,6 +2799,57 @@ export function parseVoiceFlags(occurrences, readFile) {
|
|
|
2551
2799
|
}
|
|
2552
2800
|
return { ...merged, ...typed };
|
|
2553
2801
|
}
|
|
2802
|
+
function catalogLines(catalog) {
|
|
2803
|
+
const width = (pick) => Math.max(0, ...catalog.map((voice) => pick(voice).length));
|
|
2804
|
+
const nameWidth = width((voice) => voice.name);
|
|
2805
|
+
const genderWidth = width((voice) => voice.gender);
|
|
2806
|
+
const ageWidth = width((voice) => voice.age);
|
|
2807
|
+
const accentWidth = width((voice) => voice.accent);
|
|
2808
|
+
return [
|
|
2809
|
+
"Voices you can pick by name (ElevenLabs voices every account has)",
|
|
2810
|
+
...catalog.map((voice) => {
|
|
2811
|
+
const qualities = [...voice.qualities, ...(voice.characterVoice ? ["character voice"] : [])];
|
|
2812
|
+
return (` ${voice.name.padEnd(nameWidth)} ${voice.gender.padEnd(genderWidth)} ` +
|
|
2813
|
+
`${voice.age.padEnd(ageWidth)} ${voice.accent.padEnd(accentWidth)} ${qualities.join(", ")}`).trimEnd();
|
|
2814
|
+
}),
|
|
2815
|
+
];
|
|
2816
|
+
}
|
|
2817
|
+
function catalogPickLines(cast) {
|
|
2818
|
+
const lines = [];
|
|
2819
|
+
for (const row of cast) {
|
|
2820
|
+
if (!row.catalogPicks || row.catalogPicks.length === 0)
|
|
2821
|
+
continue;
|
|
2822
|
+
const width = Math.max(...row.catalogPicks.map((pick) => pick.name.length));
|
|
2823
|
+
lines.push("", `Best fits for ${row.name} (${row.characterId})`);
|
|
2824
|
+
for (const pick of row.catalogPicks) {
|
|
2825
|
+
lines.push(` ${pick.name.padEnd(width)}${pick.fit ? ` ${pick.fit.note}` : ""}`.trimEnd());
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return lines;
|
|
2829
|
+
}
|
|
2830
|
+
export async function voiceCatalogFlow(runId, json, deps) {
|
|
2831
|
+
const query = runId ? `runId=${encodeURIComponent(runId)}&check=none&catalog=1` : "catalog=1";
|
|
2832
|
+
const res = await deps.get(`${VOICES_PATH}?${query}`);
|
|
2833
|
+
if (!res.ok)
|
|
2834
|
+
return errorResult(res, json);
|
|
2835
|
+
const data = (res.data ?? {});
|
|
2836
|
+
if (!Array.isArray(data.catalog)) {
|
|
2837
|
+
return { code: 1, lines: ["This server can't list the voice catalog yet. Voice IDs still work with --set."] };
|
|
2838
|
+
}
|
|
2839
|
+
if (json)
|
|
2840
|
+
return { code: 0, lines: [JSON.stringify({ ok: true, ...data })] };
|
|
2841
|
+
const cast = Array.isArray(data.cast) ? data.cast : [];
|
|
2842
|
+
const set = runId ? `exodus video voices ${runId}` : "exodus video voices <runId>";
|
|
2843
|
+
return {
|
|
2844
|
+
code: 0,
|
|
2845
|
+
lines: [
|
|
2846
|
+
...catalogLines(data.catalog),
|
|
2847
|
+
...catalogPickLines(cast),
|
|
2848
|
+
"",
|
|
2849
|
+
`Pick one by name: ${set} --set <character>=<Name>`,
|
|
2850
|
+
],
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2554
2853
|
export async function voicesFlow(runId, voices, json, deps) {
|
|
2555
2854
|
const res = voices
|
|
2556
2855
|
? await deps.post(VOICES_PATH, { runId, voices })
|
|
@@ -2585,7 +2884,30 @@ function voiceWords(row) {
|
|
|
2585
2884
|
if (!voice)
|
|
2586
2885
|
return "—";
|
|
2587
2886
|
const words = voice.label ? `${voice.label} (${voice.voiceId})` : voice.voiceId;
|
|
2588
|
-
|
|
2887
|
+
if (row.voiceFrom === "run-default")
|
|
2888
|
+
return `${words}, the run's default voice`;
|
|
2889
|
+
if (row.voiceFrom === "planner-pick")
|
|
2890
|
+
return `${words}, picked by the planner`;
|
|
2891
|
+
return words;
|
|
2892
|
+
}
|
|
2893
|
+
function fitWords(row) {
|
|
2894
|
+
return row.fit ? ` — ${row.fit.note}` : "";
|
|
2895
|
+
}
|
|
2896
|
+
export const VOICE_CLASH_HINT = 'To choose a voice yourself, write "voice: <Name>" on that character\'s CAST line in the script, ' +
|
|
2897
|
+
"or launch with --review-storyboard and change it at the storyboard stop.";
|
|
2898
|
+
async function statusVoiceLines(runId, deps) {
|
|
2899
|
+
const res = await deps
|
|
2900
|
+
.get(`${VOICES_PATH}?runId=${encodeURIComponent(runId)}&check=none`)
|
|
2901
|
+
.catch(() => null);
|
|
2902
|
+
if (!res?.ok)
|
|
2903
|
+
return [];
|
|
2904
|
+
const sheet = (res.data ?? {});
|
|
2905
|
+
const cast = Array.isArray(sheet.cast) ? sheet.cast : [];
|
|
2906
|
+
const summary = voiceSummaryLines(cast, []);
|
|
2907
|
+
if (summary.length === 0)
|
|
2908
|
+
return [];
|
|
2909
|
+
const clash = cast.some((row) => row.fit?.clash === true);
|
|
2910
|
+
return ["", ...summary, ...(clash ? [VOICE_CLASH_HINT] : [])];
|
|
2589
2911
|
}
|
|
2590
2912
|
function voiceSummaryLines(cast, notices) {
|
|
2591
2913
|
const rows = cast.filter((row) => row.spokenScenes > 0 || row.voice !== null);
|
|
@@ -2593,7 +2915,7 @@ function voiceSummaryLines(cast, notices) {
|
|
|
2593
2915
|
const lines = rows.length > 0 ? ["Voices"] : [];
|
|
2594
2916
|
for (const row of rows) {
|
|
2595
2917
|
lines.push(row.voice
|
|
2596
|
-
? ` ${row.name.padEnd(width)} → ${voiceWords(row)} ${availabilityWords(row)}`
|
|
2918
|
+
? ` ${row.name.padEnd(width)} → ${voiceWords(row)} ${availabilityWords(row)}${fitWords(row)}`
|
|
2597
2919
|
: ` ${row.name.padEnd(width)} → no voice chosen (keeps the generated voice)`);
|
|
2598
2920
|
}
|
|
2599
2921
|
for (const notice of notices)
|
|
@@ -2611,7 +2933,7 @@ export function voiceSheetLines(sheet) {
|
|
|
2611
2933
|
const voice = voiceWords(row);
|
|
2612
2934
|
lines.push(` ${row.characterId} ${row.name} ${voice} ${availabilityWords(row)} ` +
|
|
2613
2935
|
`speaks in ${row.spokenScenes} scene${row.spokenScenes === 1 ? "" : "s"}, ` +
|
|
2614
|
-
`about ${row.spokenSeconds}s`);
|
|
2936
|
+
`about ${row.spokenSeconds}s${fitWords(row)}`);
|
|
2615
2937
|
if (row.description)
|
|
2616
2938
|
lines.push(` written voice: ${row.description}`);
|
|
2617
2939
|
}
|
|
@@ -3186,9 +3508,11 @@ export async function run(flags, occurrences) {
|
|
|
3186
3508
|
const plan = planVideoStart(flags, occurrences);
|
|
3187
3509
|
if (plan.kind === "usage")
|
|
3188
3510
|
usage(plan.line);
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3511
|
+
const onProgressLine = (line) => console.log(line);
|
|
3512
|
+
if (plan.kind === "show") {
|
|
3513
|
+
return printResult(await startFlow({ ...plan.opts, onProgressLine }, defaultDeps));
|
|
3514
|
+
}
|
|
3515
|
+
const started = await startScriptFlow({ ...plan.opts, onProgressLine }, defaultDeps);
|
|
3192
3516
|
if (started.usage)
|
|
3193
3517
|
usage(started.lines[0] ?? "");
|
|
3194
3518
|
return printResult(started);
|
|
@@ -3207,6 +3531,9 @@ export async function run(flags, occurrences) {
|
|
|
3207
3531
|
"upload",
|
|
3208
3532
|
"director",
|
|
3209
3533
|
];
|
|
3534
|
+
if (sub === "voices" && flags["catalog"] !== undefined && flags["catalog"] !== false) {
|
|
3535
|
+
return printResult(await voiceCatalogFlow(runId, json, defaultDeps));
|
|
3536
|
+
}
|
|
3210
3537
|
if (needsRunId.includes(sub) && !runId) {
|
|
3211
3538
|
usage(`video ${sub} needs a run id: exodus video ${sub} <runId>`);
|
|
3212
3539
|
}
|