@aicopycoders/exodus 2026.9.1700-beta.1 → 2026.9.1800-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/skills/exodus-video/SKILL.md +102 -7
- package/dist/commands/video.d.ts +50 -0
- package/dist/commands/video.js +162 -8
- package/package.json +1 -1
|
@@ -89,6 +89,8 @@ exodus video start --show <id> --script script.txt --wait start, wait for the s
|
|
|
89
89
|
|
|
90
90
|
```operator-guide
|
|
91
91
|
exodus video storyboard <runId> read the scene cards
|
|
92
|
+
exodus video voices <runId> who speaks and with whose voice
|
|
93
|
+
exodus video voices <runId> --set <character>=<voiceId> give a character a voice
|
|
92
94
|
exodus video approve <runId> keep going
|
|
93
95
|
exodus video flag <runId> --note "what's wrong" send it back — SHOW RUNS ONLY
|
|
94
96
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
@@ -162,6 +164,8 @@ Verified against the routes, not assumed. Everything here is admin-gated on top.
|
|
|
162
164
|
| `workflow run` | starts the run | not the entry point |
|
|
163
165
|
| `video status` | works | works |
|
|
164
166
|
| `video storyboard` | works | works |
|
|
167
|
+
| `video voices` (read) | works | works |
|
|
168
|
+
| `video voices --set` | works | **refused** — the Show owns its voices |
|
|
165
169
|
| `video approve` | works, both gates | works, both gates |
|
|
166
170
|
| `video flag --note` | **refused** | works |
|
|
167
171
|
| script-turn edits at the gate | **refused** | dashboard only |
|
|
@@ -196,13 +200,16 @@ Five consequences worth knowing before you promise a user anything:
|
|
|
196
200
|
|
|
197
201
|
- **`flag` is Show-only and fails loudly.** `exodus video flag` on a workflow run
|
|
198
202
|
answers "Flag-with-note is only available on Video-module ads."
|
|
199
|
-
(`convex/videoModule.ts
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
wired only to the dashboard's gate screen, not
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
(`convex/videoModule.ts`, guarded on `!run.showId`). Script-turn edits refuse
|
|
204
|
+
the same way. There is **no** CLI path that sends a workflow run's storyboard
|
|
205
|
+
back with a note, and none that rewrites its scenes, script or prompts — the
|
|
206
|
+
whole-envelope edit mutation is wired only to the dashboard's gate screen, not
|
|
207
|
+
to any API route. The one exception is voices: `exodus video voices --set`
|
|
208
|
+
writes the cast's voice choices in place and nothing else (see "Giving the
|
|
209
|
+
characters voices"). For everything else the storyboard is approve-or-cancel
|
|
210
|
+
from the terminal: `exodus workflow cancel <runId>` and run again with a better
|
|
211
|
+
script, or edit the workflow. Do not offer `flag` to a user on the workflow
|
|
212
|
+
route.
|
|
206
213
|
- **`--wait` lands on both video parks, but only those.** Since #1818 every
|
|
207
214
|
`--wait` caller — `workflow run`, `triggers fire`, `checkpoint approve|retry`,
|
|
208
215
|
`repair retry|skip` — ends at a storyboard gate or a final watch, exits 0, and
|
|
@@ -257,6 +264,94 @@ The note is read by the model that rewrites the storyboard, so make it concrete
|
|
|
257
264
|
Either way, do not approve on the user's behalf unless they told you to run the
|
|
258
265
|
whole loop unattended.
|
|
259
266
|
|
|
267
|
+
## Giving the characters voices
|
|
268
|
+
|
|
269
|
+
By default a workflow run's characters have no voice chosen, so every clip keeps
|
|
270
|
+
the voice the video model invents. `exodus video voices` is where that gets
|
|
271
|
+
fixed, and it only works **before** the storyboard is approved — once clips start
|
|
272
|
+
it refuses.
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
exodus video voices <runId>
|
|
276
|
+
exodus video voices <runId> --json
|
|
277
|
+
exodus video voices <runId> --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
278
|
+
exodus video voices <runId> --from voices.json
|
|
279
|
+
exodus video voices <runId> --clear C2
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Read it first. With no flags it prints one line per character: the character's
|
|
283
|
+
ID, the name the script uses, the voice chosen, whether ElevenLabs actually has
|
|
284
|
+
that voice on the user's account, and how many scenes that character speaks in.
|
|
285
|
+
Under it: how the voices get applied, who pays, and anything worth knowing before
|
|
286
|
+
approving.
|
|
287
|
+
|
|
288
|
+
Name a character either way. `C1` is the ID the planner handed out; `HOST 1` is
|
|
289
|
+
the name the script uses. Both work, and the name is the portable one — the same
|
|
290
|
+
voices file works on every run of that brand because the planner may hand out the
|
|
291
|
+
IDs differently each time.
|
|
292
|
+
|
|
293
|
+
**What `--set` changes.** The voice choices, and nothing else. The script, the
|
|
294
|
+
scene pictures and the run itself are left exactly as they were — no re-boarding,
|
|
295
|
+
no picture is redrawn, nothing is charged. A character whose voice was already
|
|
296
|
+
what you asked for reports "Nothing changed".
|
|
297
|
+
|
|
298
|
+
**What it refuses, and why nothing is written when it does.** A character name
|
|
299
|
+
nobody in the cast answers to, a voice ID with a space in it (people paste the
|
|
300
|
+
voice's *name* by mistake), and a voice ID ElevenLabs is certain it does not have
|
|
301
|
+
on that account. Each refusal says the plain reason and saves nothing. If
|
|
302
|
+
ElevenLabs simply cannot be reached, the choice IS saved and the output says the
|
|
303
|
+
check could not be made — a provider outage never blocks the run.
|
|
304
|
+
|
|
305
|
+
**The voices file.** A plain JSON object, keyed by the names the script uses:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"HOST 1": { "voiceId": "abc123voiceid", "label": "First host" },
|
|
310
|
+
"HOST 2": { "voiceId": "def456voiceid", "label": "Second host" }
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
A bare string works instead of the object when you do not care about the label
|
|
315
|
+
(`"HOST 1": "abc123voiceid"`), and `null` clears that character's voice. `--set`
|
|
316
|
+
and `--clear` win over the same name in the file, so one entry can be overridden
|
|
317
|
+
without editing it. Use the name the file uses: if the file says `HOST 2` and the
|
|
318
|
+
flag says `C2`, that is one character named two ways, and the whole request is
|
|
319
|
+
refused with a sentence saying so. Voice IDs belong to the user's own ElevenLabs account and to that brand's
|
|
320
|
+
folder — never put a brand's voice IDs in a shared workflow or template.
|
|
321
|
+
|
|
322
|
+
**The JSON receipt** (`--json`), the same shape for reading and for setting:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
ok true
|
|
326
|
+
runId
|
|
327
|
+
changed[] the characters whose voice actually moved (set only)
|
|
328
|
+
canChange false once the run has left the storyboard review
|
|
329
|
+
whyNot why, in one sentence, when canChange is false
|
|
330
|
+
cast[] one per character
|
|
331
|
+
characterId, name
|
|
332
|
+
voice { voiceId, label } or null
|
|
333
|
+
availability { state: "available" | "missing" | "not-checked", … }
|
|
334
|
+
spokenScenes how many scenes this character speaks in
|
|
335
|
+
spokenSeconds those scenes' planned seconds — what ElevenLabs bills on
|
|
336
|
+
narrator the one fallback voice, set on the workflow, not here
|
|
337
|
+
treatment how the voices reach the clips: summary, provider, model,
|
|
338
|
+
speedChange, billedTo, estimatedCostUsd, costNote,
|
|
339
|
+
usage { clips, seconds }, keyOnRun
|
|
340
|
+
notices[] plain sentences worth reading before approving
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
A failure is the same shape every `video` verb uses:
|
|
344
|
+
`{ "ok": false, "status": 400, "error": "<one plain sentence>" }`, exit 1.
|
|
345
|
+
|
|
346
|
+
**Before you approve, read `notices`.** The ones that cost the user something:
|
|
347
|
+
a character who speaks but has no voice (those clips keep their generated
|
|
348
|
+
voice), no ElevenLabs key saved on the run (no voice can be applied at all), and
|
|
349
|
+
a voice ElevenLabs no longer has.
|
|
350
|
+
|
|
351
|
+
`estimatedCostUsd` is always null. Exodus cannot price a voice conversion yet;
|
|
352
|
+
ElevenLabs bills the user's own account by audio length, and `usage` gives the
|
|
353
|
+
length. Do not invent a number.
|
|
354
|
+
|
|
260
355
|
## The handover: what `pull` gives you
|
|
261
356
|
|
|
262
357
|
`pull` writes a flat folder and a `manifest.json` that indexes it. The manifest
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
2
|
export declare const helpText: string;
|
|
3
|
+
export declare const VOICES_PATH = "/api/v2/video/voices";
|
|
3
4
|
export interface ClipWord {
|
|
4
5
|
w: string;
|
|
5
6
|
s: number;
|
|
@@ -264,6 +265,55 @@ export declare function storyboardFlow(runId: string, json: boolean, deps: Video
|
|
|
264
265
|
export declare function approveFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
265
266
|
export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
266
267
|
export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
268
|
+
export interface CastVoiceRow {
|
|
269
|
+
characterId: string;
|
|
270
|
+
name: string;
|
|
271
|
+
voice: {
|
|
272
|
+
voiceId: string;
|
|
273
|
+
label?: string;
|
|
274
|
+
} | null;
|
|
275
|
+
availability?: {
|
|
276
|
+
state: "available";
|
|
277
|
+
providerName: string;
|
|
278
|
+
} | {
|
|
279
|
+
state: "missing";
|
|
280
|
+
} | {
|
|
281
|
+
state: "not-checked";
|
|
282
|
+
why: string;
|
|
283
|
+
};
|
|
284
|
+
spokenScenes: number;
|
|
285
|
+
spokenSeconds: number;
|
|
286
|
+
}
|
|
287
|
+
export interface CastVoiceSheet {
|
|
288
|
+
runId: string;
|
|
289
|
+
canChange: boolean;
|
|
290
|
+
whyNot?: string;
|
|
291
|
+
cast: CastVoiceRow[];
|
|
292
|
+
narrator: {
|
|
293
|
+
voiceId: string;
|
|
294
|
+
label?: string;
|
|
295
|
+
} | null;
|
|
296
|
+
treatment: {
|
|
297
|
+
summary: string;
|
|
298
|
+
provider: string;
|
|
299
|
+
model: string;
|
|
300
|
+
speedChange: boolean;
|
|
301
|
+
costNote: string;
|
|
302
|
+
usage: {
|
|
303
|
+
clips: number;
|
|
304
|
+
seconds: number;
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
notices: string[];
|
|
308
|
+
changed?: string[];
|
|
309
|
+
}
|
|
310
|
+
export type VoiceMap = Record<string, string | {
|
|
311
|
+
voiceId: string;
|
|
312
|
+
label?: string;
|
|
313
|
+
} | null>;
|
|
314
|
+
export declare function parseVoiceFlags(argv: readonly string[], readFile: (path: string) => string): VoiceMap | null;
|
|
315
|
+
export declare function voicesFlow(runId: string, voices: VoiceMap | null, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
316
|
+
export declare function voiceSheetLines(sheet: CastVoiceSheet): string[];
|
|
267
317
|
export declare function pullFlow(runId: string, dir: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
268
318
|
export declare function parseMvhdDurationSec(bytes: Uint8Array): number | null;
|
|
269
319
|
export declare const NO_DURATION_MESSAGE: string;
|
package/dist/commands/video.js
CHANGED
|
@@ -25,24 +25,28 @@ The whole loop, in order:
|
|
|
25
25
|
3. exodus video storyboard <runId>
|
|
26
26
|
The scene cards: what each scene says and the picture it will look like.
|
|
27
27
|
|
|
28
|
-
4. exodus video
|
|
28
|
+
4. exodus video voices <runId>
|
|
29
|
+
Who is in the ad, which voice each one has, how the voices get applied
|
|
30
|
+
and who pays. Add --set to give someone a voice, before any clip is made.
|
|
31
|
+
|
|
32
|
+
5. exodus video approve <runId> (looks right — keep going)
|
|
29
33
|
exodus video flag <runId> --note "…" (something is wrong — send it back)
|
|
30
34
|
exodus video retry-frame <runId> --node <nodeId> --scene <n>
|
|
31
35
|
Redo ONE still at the pixel gate. Neighbours stay. The gate holds.
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
6. exodus video status <runId>
|
|
34
38
|
Where the run is and how each scene's clip turned out.
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
7. exodus video pull <runId> --out ./ad
|
|
37
41
|
Writes every piece to that folder plus a manifest.json index.
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
8. Make your cut from those files.
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
9. exodus video upload <runId> --file cut.mp4
|
|
42
46
|
Attaches your cut to the run and prints the page to approve it on.
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
10. exodus video approve <runId>
|
|
49
|
+
Or click Approve on the page from step 9.
|
|
46
50
|
|
|
47
51
|
Usage:
|
|
48
52
|
exodus video shows [--json]
|
|
@@ -52,6 +56,7 @@ Usage:
|
|
|
52
56
|
exodus video approve <runId> [--json]
|
|
53
57
|
exodus video flag <runId> --note "<what is wrong>" [--json]
|
|
54
58
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
59
|
+
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
55
60
|
exodus video pull <runId> --out <dir> [--json]
|
|
56
61
|
exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
|
|
57
62
|
|
|
@@ -70,6 +75,12 @@ Options:
|
|
|
70
75
|
steer one frame redo (retry-frame)
|
|
71
76
|
--node <nodeId> Which scene-frames node holds the still (retry-frame)
|
|
72
77
|
--scene <n> Which scene's frame to redo. One scene only (retry-frame)
|
|
78
|
+
--set <who>=<id> Give a character an ElevenLabs voice (voices). Name the
|
|
79
|
+
character by its ID or by the name your script uses.
|
|
80
|
+
Repeat it once per character
|
|
81
|
+
--clear <who> Take a character's voice off again (voices). Repeatable
|
|
82
|
+
--from <file.json> A file of characters and voice IDs (voices). --set and
|
|
83
|
+
--clear win over the same name in the file
|
|
73
84
|
--json Machine-readable output
|
|
74
85
|
--help, -h Show this help
|
|
75
86
|
|
|
@@ -82,6 +93,9 @@ Examples:
|
|
|
82
93
|
exodus video storyboard run_123
|
|
83
94
|
exodus video approve run_123
|
|
84
95
|
exodus video retry-frame run_123 --node frames-1 --scene 2
|
|
96
|
+
exodus video voices run_123
|
|
97
|
+
exodus video voices run_123 --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
98
|
+
exodus video voices run_123 --from voices.json
|
|
85
99
|
exodus video pull run_123 --out ./ad-run_123
|
|
86
100
|
exodus video upload run_123 --file ./cut.mp4
|
|
87
101
|
`.trim();
|
|
@@ -94,6 +108,7 @@ const FLAG_PATH = "/api/v2/video/storyboard/flag";
|
|
|
94
108
|
const APPROVE_PATH = "/api/v2/workflow/approve";
|
|
95
109
|
const SCENE_RETRY_PATH = "/api/v2/workflow/scene/retry";
|
|
96
110
|
const FINAL_PATH = "/api/v2/video/final";
|
|
111
|
+
export const VOICES_PATH = "/api/v2/video/voices";
|
|
97
112
|
const ASSET_UPLOAD_URL_PATH = "/api/v2/workflows/asset-upload-url";
|
|
98
113
|
const ASSETS_PATH = "/api/v2/workflows/assets";
|
|
99
114
|
function withoutJudgeFields({ judgeDetail: _d, judgeSeverity: _s, ...rest }) {
|
|
@@ -1037,6 +1052,123 @@ export async function retryFrameFlow(runId, nodeId, sceneIndex, note, json, deps
|
|
|
1037
1052
|
],
|
|
1038
1053
|
};
|
|
1039
1054
|
}
|
|
1055
|
+
export function parseVoiceFlags(argv, readFile) {
|
|
1056
|
+
const fromFile = {};
|
|
1057
|
+
const typed = {};
|
|
1058
|
+
let asked = false;
|
|
1059
|
+
const valueFor = (i, flag) => {
|
|
1060
|
+
const arg = argv[i];
|
|
1061
|
+
if (arg.startsWith(`${flag}=`))
|
|
1062
|
+
return [arg.slice(flag.length + 1), i];
|
|
1063
|
+
const next = argv[i + 1];
|
|
1064
|
+
if (next === undefined)
|
|
1065
|
+
throw new Error(`${flag} needs a value.`);
|
|
1066
|
+
return [next, i + 1];
|
|
1067
|
+
};
|
|
1068
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1069
|
+
const arg = argv[i];
|
|
1070
|
+
if (arg === "--set" || arg.startsWith("--set=")) {
|
|
1071
|
+
const [raw, next] = valueFor(i, "--set");
|
|
1072
|
+
i = next;
|
|
1073
|
+
const eq = raw.lastIndexOf("=");
|
|
1074
|
+
if (eq <= 0 || eq === raw.length - 1) {
|
|
1075
|
+
throw new Error("--set must look like <character>=<voice id>, for example " +
|
|
1076
|
+
`--set C1=abc123voiceid (got "${raw}").`);
|
|
1077
|
+
}
|
|
1078
|
+
typed[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
|
|
1079
|
+
asked = true;
|
|
1080
|
+
}
|
|
1081
|
+
else if (arg === "--clear" || arg.startsWith("--clear=")) {
|
|
1082
|
+
const [raw, next] = valueFor(i, "--clear");
|
|
1083
|
+
i = next;
|
|
1084
|
+
if (!raw.trim())
|
|
1085
|
+
throw new Error("--clear needs a character, for example --clear C1.");
|
|
1086
|
+
typed[raw.trim()] = null;
|
|
1087
|
+
asked = true;
|
|
1088
|
+
}
|
|
1089
|
+
else if (arg === "--from" || arg.startsWith("--from=")) {
|
|
1090
|
+
const [file, next] = valueFor(i, "--from");
|
|
1091
|
+
i = next;
|
|
1092
|
+
let parsed;
|
|
1093
|
+
try {
|
|
1094
|
+
parsed = JSON.parse(readFile(file));
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
throw new Error(`Could not read ${file} as a list of characters and voice IDs.`);
|
|
1098
|
+
}
|
|
1099
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1100
|
+
throw new Error(`Could not read ${file} as a list of characters and voice IDs.`);
|
|
1101
|
+
}
|
|
1102
|
+
Object.assign(fromFile, parsed);
|
|
1103
|
+
asked = true;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (!asked)
|
|
1107
|
+
return null;
|
|
1108
|
+
const sameName = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
1109
|
+
const merged = {};
|
|
1110
|
+
for (const [who, choice] of Object.entries(fromFile)) {
|
|
1111
|
+
if (!Object.keys(typed).some((t) => sameName(t, who)))
|
|
1112
|
+
merged[who] = choice;
|
|
1113
|
+
}
|
|
1114
|
+
return { ...merged, ...typed };
|
|
1115
|
+
}
|
|
1116
|
+
export async function voicesFlow(runId, voices, json, deps) {
|
|
1117
|
+
const res = voices
|
|
1118
|
+
? await deps.post(VOICES_PATH, { runId, voices })
|
|
1119
|
+
: await deps.get(`${VOICES_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1120
|
+
if (!res.ok)
|
|
1121
|
+
return errorResult(res, json);
|
|
1122
|
+
const sheet = res.data;
|
|
1123
|
+
if (json)
|
|
1124
|
+
return { code: 0, lines: [JSON.stringify({ ok: true, ...sheet })] };
|
|
1125
|
+
return { code: 0, lines: voiceSheetLines(sheet) };
|
|
1126
|
+
}
|
|
1127
|
+
function availabilityWords(row) {
|
|
1128
|
+
if (!row.voice)
|
|
1129
|
+
return "no voice yet";
|
|
1130
|
+
switch (row.availability?.state) {
|
|
1131
|
+
case "available":
|
|
1132
|
+
return "ElevenLabs has it";
|
|
1133
|
+
case "missing":
|
|
1134
|
+
return "ElevenLabs does not have it";
|
|
1135
|
+
case "not-checked":
|
|
1136
|
+
return "not checked just now";
|
|
1137
|
+
default:
|
|
1138
|
+
return "not checked";
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
export function voiceSheetLines(sheet) {
|
|
1142
|
+
const lines = [`Voices for run ${sheet.runId}`];
|
|
1143
|
+
if (sheet.cast.length === 0)
|
|
1144
|
+
lines.push(" Nobody is in this ad yet.");
|
|
1145
|
+
for (const row of sheet.cast) {
|
|
1146
|
+
const voice = row.voice
|
|
1147
|
+
? `${row.voice.label ? `${row.voice.label} (${row.voice.voiceId})` : row.voice.voiceId}`
|
|
1148
|
+
: "—";
|
|
1149
|
+
lines.push(` ${row.characterId} ${row.name} ${voice} ${availabilityWords(row)} ` +
|
|
1150
|
+
`speaks in ${row.spokenScenes} scenes, about ${row.spokenSeconds}s`);
|
|
1151
|
+
}
|
|
1152
|
+
if (sheet.narrator) {
|
|
1153
|
+
const label = sheet.narrator.label ? `${sheet.narrator.label} ` : "";
|
|
1154
|
+
lines.push(` Narrator ${label}(${sheet.narrator.voiceId}) set on the workflow, not here`);
|
|
1155
|
+
}
|
|
1156
|
+
if (sheet.changed) {
|
|
1157
|
+
lines.push(sheet.changed.length > 0
|
|
1158
|
+
? `Changed: ${sheet.changed.join(", ")}. Pictures and script were not touched.`
|
|
1159
|
+
: "Nothing changed — those voices were already set. Pictures and script were not touched.");
|
|
1160
|
+
}
|
|
1161
|
+
lines.push(`How voices are applied: ${sheet.treatment.summary}${sheet.treatment.speedChange ? "" : " No speed change."}`);
|
|
1162
|
+
lines.push(`Cost: billed to your own ElevenLabs key. ${sheet.treatment.costNote} ` +
|
|
1163
|
+
`About ${sheet.treatment.usage.seconds} seconds of speech across ` +
|
|
1164
|
+
`${sheet.treatment.usage.clips} clips will be converted.`);
|
|
1165
|
+
for (const notice of sheet.notices)
|
|
1166
|
+
lines.push(notice);
|
|
1167
|
+
lines.push(sheet.canChange
|
|
1168
|
+
? "You can still change voices until you approve the storyboard."
|
|
1169
|
+
: (sheet.whyNot ?? "Voices can no longer be changed on this run."));
|
|
1170
|
+
return lines;
|
|
1171
|
+
}
|
|
1040
1172
|
export async function pullFlow(runId, dir, json, deps) {
|
|
1041
1173
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1042
1174
|
if (!runRes.ok)
|
|
@@ -1343,6 +1475,9 @@ const VALUE_FLAGS = new Set([
|
|
|
1343
1475
|
"duration",
|
|
1344
1476
|
"node",
|
|
1345
1477
|
"scene",
|
|
1478
|
+
"set",
|
|
1479
|
+
"clear",
|
|
1480
|
+
"from",
|
|
1346
1481
|
]);
|
|
1347
1482
|
export function parsePositional(args = process.argv.slice(3)) {
|
|
1348
1483
|
const out = [];
|
|
@@ -1402,7 +1537,16 @@ export async function run(flags) {
|
|
|
1402
1537
|
}, defaultDeps));
|
|
1403
1538
|
}
|
|
1404
1539
|
const runId = rest[0];
|
|
1405
|
-
const needsRunId = [
|
|
1540
|
+
const needsRunId = [
|
|
1541
|
+
"status",
|
|
1542
|
+
"storyboard",
|
|
1543
|
+
"approve",
|
|
1544
|
+
"flag",
|
|
1545
|
+
"retry-frame",
|
|
1546
|
+
"voices",
|
|
1547
|
+
"pull",
|
|
1548
|
+
"upload",
|
|
1549
|
+
];
|
|
1406
1550
|
if (needsRunId.includes(sub) && !runId) {
|
|
1407
1551
|
usage(`video ${sub} needs a run id: exodus video ${sub} <runId>`);
|
|
1408
1552
|
}
|
|
@@ -1432,6 +1576,16 @@ export async function run(flags) {
|
|
|
1432
1576
|
}
|
|
1433
1577
|
return printResult(await retryFrameFlow(runId, nodeId, sceneIndex, flagString(flags, "note"), json, defaultDeps));
|
|
1434
1578
|
}
|
|
1579
|
+
if (sub === "voices") {
|
|
1580
|
+
let voices;
|
|
1581
|
+
try {
|
|
1582
|
+
voices = parseVoiceFlags(process.argv.slice(3), defaultDeps.readFile);
|
|
1583
|
+
}
|
|
1584
|
+
catch (err) {
|
|
1585
|
+
usage(err.message);
|
|
1586
|
+
}
|
|
1587
|
+
return printResult(await voicesFlow(runId, voices, json, defaultDeps));
|
|
1588
|
+
}
|
|
1435
1589
|
if (sub === "pull") {
|
|
1436
1590
|
const dir = flagString(flags, "out");
|
|
1437
1591
|
if (!dir)
|