@aicopycoders/exodus 2026.9.1900-beta.0 → 2026.9.1900-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/docs/PIPELINES.md +1 -0
- package/assets/skills/exodus-video/SKILL.md +76 -6
- package/dist/bin/exodus.js +3 -36
- package/dist/commands/video.d.ts +12 -2
- package/dist/commands/video.js +236 -56
- package/dist/commands/workflow.d.ts +9 -8
- package/dist/commands/workflow.js +43 -95
- package/dist/lib/args.d.ts +10 -0
- package/dist/lib/args.js +39 -0
- package/package.json +1 -1
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -263,6 +263,7 @@ Then, on either route:
|
|
|
263
263
|
exodus video status <runId> [--json] the park and each scene's clip/voice/picture
|
|
264
264
|
exodus video pull <runId> --out <dir> [--json] every piece + manifest.json
|
|
265
265
|
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "<t>"] [--json] redo one clip at the final watch
|
|
266
|
+
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json] redo only the voice on finished clips; no new video
|
|
266
267
|
exodus video upload <runId> --file <cut.mp4> [--duration <s>] [--json] attach the cut (MP4/MOV/WebM, 200 MB)
|
|
267
268
|
Cut: your choice of tool — Exodus does not stitch or mix. One option ships with the skill:
|
|
268
269
|
node .claude/skills/exodus-video/scripts/first-cut.mjs <dir> [--skip n,m] [--no-music] [--out f]
|
|
@@ -100,6 +100,7 @@ exodus video status <runId> where it is, per scene
|
|
|
100
100
|
exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
|
|
101
101
|
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
|
|
102
102
|
redo one clip at the final watch
|
|
103
|
+
exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
|
|
103
104
|
… make the cut (your choice of tool)
|
|
104
105
|
exodus video upload <runId> --file ./ad-<runId>/cut.mp4 attach it
|
|
105
106
|
exodus video approve <runId> make it the ad
|
|
@@ -176,6 +177,7 @@ Verified against the routes, not assumed. Everything here is admin-gated on top.
|
|
|
176
177
|
| script-turn edits at the gate | **refused** | dashboard only |
|
|
177
178
|
| `video retry-frame` | works | works |
|
|
178
179
|
| `video retry-clip` | works | works |
|
|
180
|
+
| `video revoice` | works | works |
|
|
179
181
|
| `video pull` | works | works |
|
|
180
182
|
| `video upload` | only if the video node sets `finalWatch: true` | works |
|
|
181
183
|
| `workflow checkpoint approve` | **refused at a video gate** | **refused at a video gate** |
|
|
@@ -277,9 +279,11 @@ whole loop unattended.
|
|
|
277
279
|
|
|
278
280
|
## Giving the characters voices
|
|
279
281
|
|
|
280
|
-
By default a workflow run's characters have no voice
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
By default a workflow run's characters have no voice of their own. A character
|
|
283
|
+
without one gets the run's default voice (the workflow's narrator voice) when the
|
|
284
|
+
run has one; with no default either, the clip keeps the voice the video model
|
|
285
|
+
invents. `exodus video voices` shows which of the three applies and is where it
|
|
286
|
+
gets fixed, and it only works **before** the storyboard is approved — once clips start
|
|
283
287
|
it refuses.
|
|
284
288
|
|
|
285
289
|
```
|
|
@@ -363,7 +367,10 @@ canChange false once the run has left the storyboard review
|
|
|
363
367
|
whyNot why, in one sentence, when canChange is false
|
|
364
368
|
cast[] one per character
|
|
365
369
|
characterId, name
|
|
366
|
-
voice { voiceId, label }
|
|
370
|
+
voice { voiceId, label }: the voice this character's clips are
|
|
371
|
+
converted to. null only when they have no voice of their
|
|
372
|
+
own AND the run has no default voice
|
|
373
|
+
voiceFrom "own-pin" or "run-default"; absent when voice is null
|
|
367
374
|
availability { state: "available" | "missing" | "not-checked", … }
|
|
368
375
|
spokenScenes how many scenes this character speaks in
|
|
369
376
|
spokenSeconds those scenes' planned seconds — what ElevenLabs bills on
|
|
@@ -378,8 +385,8 @@ A failure is the same shape every `video` verb uses:
|
|
|
378
385
|
`{ "ok": false, "status": 400, "error": "<one plain sentence>" }`, exit 1.
|
|
379
386
|
|
|
380
387
|
**Before you approve, read `notices`.** The ones that cost the user something:
|
|
381
|
-
a character who speaks but has no voice
|
|
382
|
-
voice), no ElevenLabs key saved on the run (no voice can be applied at all), and
|
|
388
|
+
a character who speaks but has no voice of their own and no run default to fall
|
|
389
|
+
back on (those clips keep their generated voice), no ElevenLabs key saved on the run (no voice can be applied at all), and
|
|
383
390
|
a voice ElevenLabs no longer has.
|
|
384
391
|
|
|
385
392
|
`estimatedCostUsd` is always null. Exodus cannot price a voice conversion yet;
|
|
@@ -445,6 +452,10 @@ scenes[] one per scene, in order
|
|
|
445
452
|
speechTrimmed the clip was re-cut to its spoken words
|
|
446
453
|
(0.5 s before the first, 0.8 s after the
|
|
447
454
|
last); null when there is no clip
|
|
455
|
+
rawStorageId the stored file the video model made, kept
|
|
456
|
+
on record when the voice pass replaced the
|
|
457
|
+
clip with a new file; null when the clip IS
|
|
458
|
+
that file, or there is no clip
|
|
448
459
|
failed[] files that did not download: {file, url, error}
|
|
449
460
|
```
|
|
450
461
|
|
|
@@ -610,6 +621,65 @@ and compare the file before and after.
|
|
|
610
621
|
the CLI says so when that is the case. Pull again, re-cut and upload again before
|
|
611
622
|
anyone approves.
|
|
612
623
|
|
|
624
|
+
## Redoing only the voice on finished clips
|
|
625
|
+
|
|
626
|
+
`revoice` puts clips that are already paid for back through the voice pass: the
|
|
627
|
+
spoken words are checked against the script, the clip is trimmed to the speech,
|
|
628
|
+
and the chosen voice is applied. It makes no new video, so the picture cannot
|
|
629
|
+
change and no video credits are spent. ElevenLabs bills the voice work to the
|
|
630
|
+
member's own key.
|
|
631
|
+
|
|
632
|
+
```
|
|
633
|
+
exodus video revoice <runId> --all
|
|
634
|
+
exodus video revoice <runId> --scene 3
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**When to reach for it.** `status` shows talking clips with `voice-unpinned` or
|
|
638
|
+
`voice-not-applied` (usually next to `speech-check-skipped`), and the manifest
|
|
639
|
+
shows `revoiced: false` with no words file. The usual cause is a run that had no ElevenLabs key when its
|
|
640
|
+
clips were made. `voices <runId>` says so: "This run has no ElevenLabs key saved".
|
|
641
|
+
|
|
642
|
+
**It finds the key by itself.** Before queuing anything, the server gives the run
|
|
643
|
+
the ElevenLabs key saved on the run owner's account, if the run has none. When
|
|
644
|
+
there is no saved key either, nothing is queued and the refusal says where to add
|
|
645
|
+
one (Settings → Keys). Approving a storyboard does the same top-up, so an older
|
|
646
|
+
run parked at its storyboard is fixed before any clip is paid for.
|
|
647
|
+
|
|
648
|
+
**`--all` picks the clips that need it.** Every finished clip whose file is still
|
|
649
|
+
the video model's own and whose findings carry `voice-unpinned` or
|
|
650
|
+
`voice-not-applied`, the two codes that mean a voice change should have happened
|
|
651
|
+
and did not. `speech-check-skipped` alone does not qualify: it also lands on
|
|
652
|
+
clips whose sound the video model or a later step owns, where there is no voice
|
|
653
|
+
to change and a pass would bill a transcription for nothing. `--scene <n>` is
|
|
654
|
+
not filtered this way; the server decides. It sends one request per scene, tries
|
|
655
|
+
every one, and reports each. Scenes refused with the same sentence share one
|
|
656
|
+
line (a run with no key prints that sentence once); `--json` keeps one entry per
|
|
657
|
+
scene.
|
|
658
|
+
|
|
659
|
+
**What is refused.** A clip that already went through the voice pass (`revoiced`,
|
|
660
|
+
`speechTrimmed` or a trimmed tail): the pass would trim or convert it twice. Use
|
|
661
|
+
`retry-clip` to make that clip again from scratch. A failed clip, a scene already
|
|
662
|
+
being redone, a step still rendering, and an ad already delivered.
|
|
663
|
+
|
|
664
|
+
**An older server starts nothing.** The voice redo has its own server route and
|
|
665
|
+
its own worker task. A server or worker from before it existed cannot run it, and
|
|
666
|
+
the CLI never falls back to `retry-clip`'s route, which would make new paid video.
|
|
667
|
+
You get one sentence saying the server does not have the voice redo yet, and
|
|
668
|
+
nothing is queued or spent.
|
|
669
|
+
|
|
670
|
+
**A clip is never lost.** The clip stays on its row the whole time. If the voice
|
|
671
|
+
pass cannot finish, the clip, its findings and its cost are left as they were and
|
|
672
|
+
one warning, `revoice-not-finished`, is added. Run it again. If ElevenLabs refuses
|
|
673
|
+
the key, the clip comes back unchanged with `voice-not-applied`.
|
|
674
|
+
|
|
675
|
+
**If the word check now fails.** The voiced clip is still delivered, and the scene
|
|
676
|
+
is flagged with the finding, because the picture is the same one the ad already
|
|
677
|
+
had. Decide from `status`: keep it, or `retry-clip` it.
|
|
678
|
+
|
|
679
|
+
**Afterwards.** Pull again. Each voiced scene now has `revoiced: true`, a words
|
|
680
|
+
file, and `rawStorageId` naming the original file. Re-cut and upload again if a
|
|
681
|
+
cut was already uploaded; the CLI says so when that is the case.
|
|
682
|
+
|
|
613
683
|
## Cutting the ad
|
|
614
684
|
|
|
615
685
|
Editing is the part every user does differently, and the team is deliberately
|
package/dist/bin/exodus.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readdirSync } from "node:fs";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { getVersion } from "../lib/version.js";
|
|
8
|
+
import { parseArgs } from "../lib/args.js";
|
|
8
9
|
const OTHER_COMMANDS = new Set([
|
|
9
10
|
"browse",
|
|
10
11
|
"status",
|
|
@@ -113,42 +114,8 @@ async function buildHelp() {
|
|
|
113
114
|
];
|
|
114
115
|
return sections.filter((s) => s.trim() !== "").join("\n\n");
|
|
115
116
|
}
|
|
116
|
-
function parseArgs(argv) {
|
|
117
|
-
const [, , rawCommand = "help", ...rest] = argv;
|
|
118
|
-
const command = ["--help", "-h", "-help"].includes(rawCommand) ? "help" : rawCommand;
|
|
119
|
-
const flags = {};
|
|
120
|
-
let i = 0;
|
|
121
|
-
while (i < rest.length) {
|
|
122
|
-
const arg = rest[i];
|
|
123
|
-
if (arg === "--help" || arg === "-h") {
|
|
124
|
-
flags["help"] = true;
|
|
125
|
-
i++;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (arg.startsWith("--")) {
|
|
129
|
-
const key = arg.slice(2);
|
|
130
|
-
const next = rest[i + 1];
|
|
131
|
-
if (key.startsWith("no-")) {
|
|
132
|
-
flags[key.slice(3)] = false;
|
|
133
|
-
i++;
|
|
134
|
-
}
|
|
135
|
-
else if (next !== undefined && !next.startsWith("--")) {
|
|
136
|
-
flags[key] = next;
|
|
137
|
-
i += 2;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
flags[key] = true;
|
|
141
|
-
i++;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
i++;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return { command, flags };
|
|
149
|
-
}
|
|
150
117
|
async function main() {
|
|
151
|
-
const { command, flags } = parseArgs(process.argv);
|
|
118
|
+
const { command, flags, occurrences } = parseArgs(process.argv);
|
|
152
119
|
if (["--version", "-v", "-V", "version"].includes(command)) {
|
|
153
120
|
console.log(getVersion());
|
|
154
121
|
process.exit(0);
|
|
@@ -180,7 +147,7 @@ async function main() {
|
|
|
180
147
|
console.error(`Command "${command}" does not export a run() function.`);
|
|
181
148
|
process.exit(1);
|
|
182
149
|
}
|
|
183
|
-
await commandModule.run(flags);
|
|
150
|
+
await commandModule.run(flags, occurrences);
|
|
184
151
|
}
|
|
185
152
|
main().catch((err) => {
|
|
186
153
|
console.error("Fatal error:", err instanceof Error ? err.message : String(err));
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
|
+
import type { FlagOccurrence } from "../lib/args.js";
|
|
2
3
|
export declare const helpText: string;
|
|
3
4
|
export declare const VOICES_PATH = "/api/v2/video/voices";
|
|
4
5
|
export interface ClipWord {
|
|
@@ -43,6 +44,8 @@ export type ArtifactSubset = {
|
|
|
43
44
|
final?: boolean;
|
|
44
45
|
revoiced?: boolean;
|
|
45
46
|
speechTrimmed?: boolean;
|
|
47
|
+
tailTrimmed?: boolean;
|
|
48
|
+
rawStorageId?: string;
|
|
46
49
|
} | {
|
|
47
50
|
type: "audio";
|
|
48
51
|
sceneIndex?: number;
|
|
@@ -202,6 +205,7 @@ export interface ManifestScene {
|
|
|
202
205
|
qc: ClipQc | null;
|
|
203
206
|
revoiced: boolean | null;
|
|
204
207
|
speechTrimmed: boolean | null;
|
|
208
|
+
rawStorageId: string | null;
|
|
205
209
|
clipStatus: string;
|
|
206
210
|
error: string | null;
|
|
207
211
|
flagged: boolean;
|
|
@@ -288,6 +292,11 @@ export interface ClipRedoTarget {
|
|
|
288
292
|
}
|
|
289
293
|
export declare function planClipRedo(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
290
294
|
export declare function retryClipFlow(runId: string, target: ClipRedoTarget, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
295
|
+
export declare function planClipRevoice(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
296
|
+
export type RevoiceTarget = ClipRedoTarget | {
|
|
297
|
+
all: true;
|
|
298
|
+
};
|
|
299
|
+
export declare function revoiceFlow(runId: string, target: RevoiceTarget, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
291
300
|
export interface CastVoiceRow {
|
|
292
301
|
characterId: string;
|
|
293
302
|
name: string;
|
|
@@ -295,6 +304,7 @@ export interface CastVoiceRow {
|
|
|
295
304
|
voiceId: string;
|
|
296
305
|
label?: string;
|
|
297
306
|
} | null;
|
|
307
|
+
voiceFrom?: "own-pin" | "run-default";
|
|
298
308
|
availability?: {
|
|
299
309
|
state: "available";
|
|
300
310
|
providerName: string;
|
|
@@ -334,7 +344,7 @@ export type VoiceMap = Record<string, string | {
|
|
|
334
344
|
voiceId: string;
|
|
335
345
|
label?: string;
|
|
336
346
|
} | null>;
|
|
337
|
-
export declare function parseVoiceFlags(
|
|
347
|
+
export declare function parseVoiceFlags(occurrences: FlagOccurrence[], readFile: (path: string) => string): VoiceMap | null;
|
|
338
348
|
export declare function voicesFlow(runId: string, voices: VoiceMap | null, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
339
349
|
export declare function voiceSheetLines(sheet: CastVoiceSheet): string[];
|
|
340
350
|
export declare function pullFlow(runId: string, dir: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
@@ -343,4 +353,4 @@ export declare const NO_DURATION_MESSAGE: string;
|
|
|
343
353
|
export declare function describeFetchFailure(err: unknown): string;
|
|
344
354
|
export declare function uploadFlow(runId: string, filePath: string, durationFlag: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
345
355
|
export declare function parsePositional(args?: string[]): string[];
|
|
346
|
-
export declare function run(flags: Record<string, string | boolean
|
|
356
|
+
export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
|
package/dist/commands/video.js
CHANGED
|
@@ -44,6 +44,12 @@ The whole loop, in order:
|
|
|
44
44
|
Redo ONE finished clip while the run waits for the cut. Everything
|
|
45
45
|
else stays. A --note steers the motion, never the words.
|
|
46
46
|
|
|
47
|
+
exodus video revoice <runId> --all
|
|
48
|
+
Redo only the VOICE on clips that kept the video model's own voice
|
|
49
|
+
(for example, the run had no ElevenLabs key when they were made). The
|
|
50
|
+
picture stays, no new video is made, and a clip whose voice pass
|
|
51
|
+
cannot finish is kept as it was. Use --scene <n> for one clip.
|
|
52
|
+
|
|
47
53
|
8. Make your cut from those files.
|
|
48
54
|
|
|
49
55
|
9. exodus video upload <runId> --file cut.mp4
|
|
@@ -61,6 +67,7 @@ Usage:
|
|
|
61
67
|
exodus video flag <runId> --note "<what is wrong>" [--json]
|
|
62
68
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
63
69
|
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--json]
|
|
70
|
+
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
|
|
64
71
|
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
65
72
|
exodus video pull <runId> --out <dir> [--json]
|
|
66
73
|
exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
|
|
@@ -80,9 +87,11 @@ Options:
|
|
|
80
87
|
steer one redo (retry-frame, retry-clip)
|
|
81
88
|
--node <nodeId> Which scene-frames node holds the still (retry-frame).
|
|
82
89
|
Which video step holds the clip, when a scene has one on
|
|
83
|
-
more than one step (retry-clip)
|
|
90
|
+
more than one step (retry-clip, revoice)
|
|
84
91
|
--scene <n> Which scene to redo. One scene only, a whole number
|
|
85
|
-
(retry-frame, retry-clip)
|
|
92
|
+
(retry-frame, retry-clip, revoice)
|
|
93
|
+
--all Every finished clip that kept the video model's voice
|
|
94
|
+
(revoice)
|
|
86
95
|
--set <who>=<id> Give a character an ElevenLabs voice (voices). Name the
|
|
87
96
|
character by its ID or by the name your script uses.
|
|
88
97
|
Repeat it once per character
|
|
@@ -102,6 +111,7 @@ Examples:
|
|
|
102
111
|
exodus video approve run_123
|
|
103
112
|
exodus video retry-frame run_123 --node frames-1 --scene 2
|
|
104
113
|
exodus video retry-clip run_123 --scene 3 --note "keep the handshake in frame"
|
|
114
|
+
exodus video revoice run_123 --all
|
|
105
115
|
exodus video voices run_123
|
|
106
116
|
exodus video voices run_123 --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
107
117
|
exodus video voices run_123 --from voices.json
|
|
@@ -116,6 +126,9 @@ const STORYBOARD_PATH = "/api/v2/video/storyboard";
|
|
|
116
126
|
const FLAG_PATH = "/api/v2/video/storyboard/flag";
|
|
117
127
|
const APPROVE_PATH = "/api/v2/workflow/approve";
|
|
118
128
|
const SCENE_RETRY_PATH = "/api/v2/workflow/scene/retry";
|
|
129
|
+
const SCENE_REVOICE_PATH = "/api/v2/workflow/scene/revoice";
|
|
130
|
+
const REVOICE_NOT_ON_THIS_SERVER = "This Exodus server does not have the voice redo yet, so nothing was started and nothing " +
|
|
131
|
+
"was spent. It arrives with the next server update.";
|
|
119
132
|
const FINAL_PATH = "/api/v2/video/final";
|
|
120
133
|
export const VOICES_PATH = "/api/v2/video/voices";
|
|
121
134
|
const ASSET_UPLOAD_URL_PATH = "/api/v2/workflows/asset-upload-url";
|
|
@@ -359,6 +372,7 @@ function clipFromArtifact(sceneIndex, artifact) {
|
|
|
359
372
|
words: artifact.words ?? null,
|
|
360
373
|
revoiced: artifact.revoiced === true,
|
|
361
374
|
speechTrimmed: artifact.speechTrimmed === true,
|
|
375
|
+
rawStorageId: artifact.rawStorageId ?? null,
|
|
362
376
|
};
|
|
363
377
|
}
|
|
364
378
|
function keyframeDownload(sceneIndex, imageUrl) {
|
|
@@ -653,6 +667,7 @@ export function planPull(run, items, opts) {
|
|
|
653
667
|
qc: clip?.qc ?? null,
|
|
654
668
|
revoiced: clip ? clip.revoiced : null,
|
|
655
669
|
speechTrimmed: clip ? clip.speechTrimmed : null,
|
|
670
|
+
rawStorageId: clip ? clip.rawStorageId : null,
|
|
656
671
|
clipStatus: item?.status ?? "missing",
|
|
657
672
|
error: item?.error ?? null,
|
|
658
673
|
flagged: item?.flagged === true,
|
|
@@ -710,6 +725,7 @@ export function markPullFailure(manifest, failure) {
|
|
|
710
725
|
scene.clip = null;
|
|
711
726
|
scene.revoiced = null;
|
|
712
727
|
scene.speechTrimmed = null;
|
|
728
|
+
scene.rawStorageId = null;
|
|
713
729
|
}
|
|
714
730
|
if (scene.voice === failure.file)
|
|
715
731
|
scene.voice = null;
|
|
@@ -1146,17 +1162,45 @@ const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the
|
|
|
1146
1162
|
"re-cut, and upload again.";
|
|
1147
1163
|
export function planClipRedo(run, items, target) {
|
|
1148
1164
|
const stop = classifyRun(run);
|
|
1165
|
+
const found = findClipRow(run, items, target);
|
|
1166
|
+
if ("reason" in found)
|
|
1167
|
+
return { ok: false, reason: found.reason };
|
|
1168
|
+
const { row, uploadedCut } = found;
|
|
1169
|
+
const redoable = row.status === "failed" ||
|
|
1170
|
+
(row.status === "done" && (row.flagged === true || stop.at === "final-watch"));
|
|
1171
|
+
if (!redoable) {
|
|
1172
|
+
if (row.status === "done") {
|
|
1173
|
+
return {
|
|
1174
|
+
ok: false,
|
|
1175
|
+
reason: `Scene ${target.sceneIndex}'s clip is finished and nothing flagged it. A finished clip ` +
|
|
1176
|
+
"can only be redone while the run is waiting for the final cut.",
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const word = ITEM_STATUS_WORD[row.status] ?? row.status;
|
|
1180
|
+
return {
|
|
1181
|
+
ok: false,
|
|
1182
|
+
reason: `Scene ${target.sceneIndex}'s clip is ${word}, so there is nothing to redo yet.`,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
ok: true,
|
|
1187
|
+
nodeId: row.nodeId,
|
|
1188
|
+
sceneIndex: target.sceneIndex,
|
|
1189
|
+
attempt: row.attempt ?? 0,
|
|
1190
|
+
warnings: uploadedCut ? [UPLOADED_CUT_WARNING] : [],
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function findClipRow(run, items, target) {
|
|
1149
1194
|
const uploadedCut = items.some((i) => i.itemKind === "final" && i.status === "done");
|
|
1150
1195
|
const rows = items.filter((i) => i.itemKind === "clip" &&
|
|
1151
1196
|
i.sceneIndex === target.sceneIndex &&
|
|
1152
1197
|
(target.nodeId === undefined || i.nodeId === target.nodeId));
|
|
1153
1198
|
if (rows.length === 0) {
|
|
1154
|
-
return {
|
|
1199
|
+
return { reason: `Scene ${target.sceneIndex} has no clip to redo.` };
|
|
1155
1200
|
}
|
|
1156
1201
|
const nodeIds = [...new Set(rows.map((r) => r.nodeId))];
|
|
1157
1202
|
if (nodeIds.length > 1) {
|
|
1158
1203
|
return {
|
|
1159
|
-
ok: false,
|
|
1160
1204
|
reason: `Scene ${target.sceneIndex} has a clip on more than one step (${nodeIds.join(", ")}), ` +
|
|
1161
1205
|
"so say which one: --node <nodeId>.",
|
|
1162
1206
|
};
|
|
@@ -1165,44 +1209,20 @@ export function planClipRedo(run, items, target) {
|
|
|
1165
1209
|
const node = run.nodes.find((n) => n.nodeId === row.nodeId);
|
|
1166
1210
|
if (node?.status === "running") {
|
|
1167
1211
|
return {
|
|
1168
|
-
ok: false,
|
|
1169
1212
|
reason: `The "${row.nodeId}" step is still making other scenes. Try again once it has finished ` +
|
|
1170
1213
|
`(exodus video status ${run._id}).`,
|
|
1171
1214
|
};
|
|
1172
1215
|
}
|
|
1173
1216
|
if (node && node.status !== "done") {
|
|
1174
1217
|
return {
|
|
1175
|
-
ok: false,
|
|
1176
1218
|
reason: `The "${row.nodeId}" step did not finish (it is "${node.status}"), ` +
|
|
1177
1219
|
"and one clip can only be redone on a step that finished.",
|
|
1178
1220
|
};
|
|
1179
1221
|
}
|
|
1180
1222
|
if (row.status === "running") {
|
|
1181
|
-
return {
|
|
1223
|
+
return { reason: `Scene ${target.sceneIndex} is already being redone.` };
|
|
1182
1224
|
}
|
|
1183
|
-
|
|
1184
|
-
(row.status === "done" && (row.flagged === true || stop.at === "final-watch"));
|
|
1185
|
-
if (!redoable) {
|
|
1186
|
-
if (row.status === "done") {
|
|
1187
|
-
return {
|
|
1188
|
-
ok: false,
|
|
1189
|
-
reason: `Scene ${target.sceneIndex}'s clip is finished and nothing flagged it. A finished clip ` +
|
|
1190
|
-
"can only be redone while the run is waiting for the final cut.",
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
const word = ITEM_STATUS_WORD[row.status] ?? row.status;
|
|
1194
|
-
return {
|
|
1195
|
-
ok: false,
|
|
1196
|
-
reason: `Scene ${target.sceneIndex}'s clip is ${word}, so there is nothing to redo yet.`,
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
return {
|
|
1200
|
-
ok: true,
|
|
1201
|
-
nodeId: row.nodeId,
|
|
1202
|
-
sceneIndex: target.sceneIndex,
|
|
1203
|
-
attempt: row.attempt ?? 0,
|
|
1204
|
-
warnings: uploadedCut ? [UPLOADED_CUT_WARNING] : [],
|
|
1205
|
-
};
|
|
1225
|
+
return { row, uploadedCut };
|
|
1206
1226
|
}
|
|
1207
1227
|
export async function retryClipFlow(runId, target, note, json, deps) {
|
|
1208
1228
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
@@ -1263,24 +1283,169 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1263
1283
|
],
|
|
1264
1284
|
};
|
|
1265
1285
|
}
|
|
1266
|
-
|
|
1286
|
+
function clipIsStillRaw(item) {
|
|
1287
|
+
const a = item.artifact;
|
|
1288
|
+
return (item.status === "done" &&
|
|
1289
|
+
a?.type === "video" &&
|
|
1290
|
+
a.revoiced !== true &&
|
|
1291
|
+
a.speechTrimmed !== true &&
|
|
1292
|
+
a.tailTrimmed !== true);
|
|
1293
|
+
}
|
|
1294
|
+
const VOICE_NOT_CHANGED_CODES = new Set(["voice-unpinned", "voice-not-applied"]);
|
|
1295
|
+
export function planClipRevoice(run, items, target) {
|
|
1296
|
+
const found = findClipRow(run, items, target);
|
|
1297
|
+
if ("reason" in found)
|
|
1298
|
+
return { ok: false, reason: found.reason };
|
|
1299
|
+
const { row, uploadedCut } = found;
|
|
1300
|
+
const remake = `To make the clip again from scratch: exodus video retry-clip ${run._id} --scene ${target.sceneIndex}`;
|
|
1301
|
+
if (row.status !== "done" || row.artifact?.type !== "video") {
|
|
1302
|
+
return {
|
|
1303
|
+
ok: false,
|
|
1304
|
+
reason: `Scene ${target.sceneIndex} has no finished clip, so there is no voice to redo. ${remake}`,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
if (!clipIsStillRaw(row)) {
|
|
1308
|
+
return {
|
|
1309
|
+
ok: false,
|
|
1310
|
+
reason: `Scene ${target.sceneIndex}'s clip has already been through the voice pass, ` +
|
|
1311
|
+
`so it cannot go through it again. ${remake}`,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
ok: true,
|
|
1316
|
+
nodeId: row.nodeId,
|
|
1317
|
+
sceneIndex: target.sceneIndex,
|
|
1318
|
+
attempt: row.attempt ?? 0,
|
|
1319
|
+
warnings: uploadedCut ? [UPLOADED_CUT_WARNING] : [],
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function sceneListWords(sceneIndexes) {
|
|
1323
|
+
if (sceneIndexes.length === 1)
|
|
1324
|
+
return `Scene ${sceneIndexes[0]}`;
|
|
1325
|
+
return `Scenes ${sceneIndexes.slice(0, -1).join(", ")} and ${sceneIndexes[sceneIndexes.length - 1]}`;
|
|
1326
|
+
}
|
|
1327
|
+
export async function revoiceFlow(runId, target, json, deps) {
|
|
1328
|
+
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1329
|
+
if (!runRes.ok)
|
|
1330
|
+
return errorResult(runRes, json);
|
|
1331
|
+
const run = asVideoRun(runRes.data);
|
|
1332
|
+
const itemsRes = await deps.get(`${ITEMS_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1333
|
+
if (!itemsRes.ok)
|
|
1334
|
+
return errorResult(itemsRes, json);
|
|
1335
|
+
const items = itemsRes.data.items ?? [];
|
|
1336
|
+
const targets = "all" in target
|
|
1337
|
+
? items
|
|
1338
|
+
.filter((i) => i.itemKind === "clip" &&
|
|
1339
|
+
clipIsStillRaw(i) &&
|
|
1340
|
+
(i.findings ?? []).some((f) => VOICE_NOT_CHANGED_CODES.has(f.code)))
|
|
1341
|
+
.sort((a, b) => a.sceneIndex - b.sceneIndex)
|
|
1342
|
+
.map((i) => ({ sceneIndex: i.sceneIndex, nodeId: i.nodeId }))
|
|
1343
|
+
: [target];
|
|
1344
|
+
if (targets.length === 0) {
|
|
1345
|
+
const nothing = "No clip on this run is waiting for a voice pass, so nothing was started.";
|
|
1346
|
+
return {
|
|
1347
|
+
code: 0,
|
|
1348
|
+
lines: json ? [JSON.stringify({ ok: true, runId, scenes: [], note: nothing })] : [nothing],
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
const scenes = [];
|
|
1352
|
+
const warnings = new Set();
|
|
1353
|
+
for (const one of targets) {
|
|
1354
|
+
const plan = planClipRevoice(run, items, one);
|
|
1355
|
+
if (!plan.ok) {
|
|
1356
|
+
scenes.push({ sceneIndex: one.sceneIndex, nodeId: one.nodeId, ok: false, error: plan.reason });
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const res = await deps.post(SCENE_REVOICE_PATH, {
|
|
1360
|
+
runId,
|
|
1361
|
+
nodeId: plan.nodeId,
|
|
1362
|
+
sceneIndex: plan.sceneIndex,
|
|
1363
|
+
});
|
|
1364
|
+
if (missingRouteLine(res, "exodus video revoice")) {
|
|
1365
|
+
return {
|
|
1366
|
+
code: 1,
|
|
1367
|
+
lines: json
|
|
1368
|
+
? [JSON.stringify({ ok: false, status: 404, error: REVOICE_NOT_ON_THIS_SERVER })]
|
|
1369
|
+
: [REVOICE_NOT_ON_THIS_SERVER],
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
if (res.ok) {
|
|
1373
|
+
plan.warnings.forEach((w) => warnings.add(w));
|
|
1374
|
+
scenes.push({
|
|
1375
|
+
sceneIndex: plan.sceneIndex,
|
|
1376
|
+
nodeId: plan.nodeId,
|
|
1377
|
+
ok: true,
|
|
1378
|
+
triggerRunId: res.data.triggerRunId,
|
|
1379
|
+
});
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
scenes.push({
|
|
1383
|
+
sceneIndex: plan.sceneIndex,
|
|
1384
|
+
nodeId: plan.nodeId,
|
|
1385
|
+
ok: false,
|
|
1386
|
+
error: videoApiError(res),
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
const queued = scenes.filter((r) => r.ok).length;
|
|
1390
|
+
const code = scenes.every((r) => r.ok) ? 0 : 1;
|
|
1391
|
+
const review = reviewUrl(deps.dashboardUrl, run);
|
|
1392
|
+
if (json) {
|
|
1393
|
+
return {
|
|
1394
|
+
code,
|
|
1395
|
+
lines: [
|
|
1396
|
+
JSON.stringify({
|
|
1397
|
+
ok: code === 0,
|
|
1398
|
+
runId,
|
|
1399
|
+
scenes,
|
|
1400
|
+
reviewUrl: review,
|
|
1401
|
+
warnings: [...warnings],
|
|
1402
|
+
}),
|
|
1403
|
+
],
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
const refusedWith = new Map();
|
|
1407
|
+
for (const r of scenes) {
|
|
1408
|
+
if (!r.ok)
|
|
1409
|
+
refusedWith.set(r.error, [...(refusedWith.get(r.error) ?? []), r.sceneIndex]);
|
|
1410
|
+
}
|
|
1411
|
+
const sceneLines = scenes.flatMap((r) => {
|
|
1412
|
+
if (r.ok) {
|
|
1413
|
+
return [
|
|
1414
|
+
`Scene ${r.sceneIndex}: redoing the voice on ${r.nodeId}. triggerRunId: ${r.triggerRunId ?? "-"}`,
|
|
1415
|
+
];
|
|
1416
|
+
}
|
|
1417
|
+
const sharing = refusedWith.get(r.error) ?? [];
|
|
1418
|
+
if (sharing[0] !== r.sceneIndex)
|
|
1419
|
+
return [];
|
|
1420
|
+
return [`${sceneListWords(sharing)}: not started. ${r.error}`];
|
|
1421
|
+
});
|
|
1422
|
+
if (queued === 0)
|
|
1423
|
+
return { code, lines: sceneLines };
|
|
1424
|
+
return {
|
|
1425
|
+
code,
|
|
1426
|
+
lines: [
|
|
1427
|
+
...sceneLines,
|
|
1428
|
+
"The picture in each clip stays exactly as it is. No new video is made.",
|
|
1429
|
+
"The words are checked against the script, the clip is trimmed to the speech, and the chosen voice is applied. ElevenLabs bills that to your own key.",
|
|
1430
|
+
"If the voice pass cannot finish on a clip, that clip is kept as it was.",
|
|
1431
|
+
`Watch them land: exodus video status ${runId}`,
|
|
1432
|
+
`Review the run: ${review}`,
|
|
1433
|
+
...warnings,
|
|
1434
|
+
],
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
export function parseVoiceFlags(occurrences, readFile) {
|
|
1267
1438
|
const fromFile = {};
|
|
1268
1439
|
const typed = {};
|
|
1269
1440
|
let asked = false;
|
|
1270
|
-
const valueFor = (
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const next = argv[i + 1];
|
|
1275
|
-
if (next === undefined)
|
|
1276
|
-
throw new Error(`${flag} needs a value.`);
|
|
1277
|
-
return [next, i + 1];
|
|
1441
|
+
const valueFor = (occurrence) => {
|
|
1442
|
+
if (occurrence.value === undefined)
|
|
1443
|
+
throw new Error(`--${occurrence.flag} needs a value.`);
|
|
1444
|
+
return occurrence.value;
|
|
1278
1445
|
};
|
|
1279
|
-
for (
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const [raw, next] = valueFor(i, "--set");
|
|
1283
|
-
i = next;
|
|
1446
|
+
for (const occurrence of occurrences) {
|
|
1447
|
+
if (occurrence.flag === "set") {
|
|
1448
|
+
const raw = valueFor(occurrence);
|
|
1284
1449
|
const eq = raw.lastIndexOf("=");
|
|
1285
1450
|
if (eq <= 0 || eq === raw.length - 1) {
|
|
1286
1451
|
throw new Error("--set must look like <character>=<voice id>, for example " +
|
|
@@ -1289,17 +1454,15 @@ export function parseVoiceFlags(argv, readFile) {
|
|
|
1289
1454
|
typed[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
|
|
1290
1455
|
asked = true;
|
|
1291
1456
|
}
|
|
1292
|
-
else if (
|
|
1293
|
-
const
|
|
1294
|
-
i = next;
|
|
1457
|
+
else if (occurrence.flag === "clear") {
|
|
1458
|
+
const raw = valueFor(occurrence);
|
|
1295
1459
|
if (!raw.trim())
|
|
1296
1460
|
throw new Error("--clear needs a character, for example --clear C1.");
|
|
1297
1461
|
typed[raw.trim()] = null;
|
|
1298
1462
|
asked = true;
|
|
1299
1463
|
}
|
|
1300
|
-
else if (
|
|
1301
|
-
const
|
|
1302
|
-
i = next;
|
|
1464
|
+
else if (occurrence.flag === "from") {
|
|
1465
|
+
const file = valueFor(occurrence);
|
|
1303
1466
|
let parsed;
|
|
1304
1467
|
try {
|
|
1305
1468
|
parsed = JSON.parse(readFile(file));
|
|
@@ -1349,10 +1512,12 @@ function availabilityWords(row) {
|
|
|
1349
1512
|
return "not checked";
|
|
1350
1513
|
}
|
|
1351
1514
|
}
|
|
1352
|
-
function voiceWords(
|
|
1515
|
+
function voiceWords(row) {
|
|
1516
|
+
const voice = row.voice;
|
|
1353
1517
|
if (!voice)
|
|
1354
1518
|
return "—";
|
|
1355
|
-
|
|
1519
|
+
const words = voice.label ? `${voice.label} (${voice.voiceId})` : voice.voiceId;
|
|
1520
|
+
return row.voiceFrom === "run-default" ? `${words}, the run's default voice` : words;
|
|
1356
1521
|
}
|
|
1357
1522
|
function voiceSummaryLines(cast, notices) {
|
|
1358
1523
|
const rows = cast.filter((row) => row.spokenScenes > 0 || row.voice !== null);
|
|
@@ -1360,7 +1525,7 @@ function voiceSummaryLines(cast, notices) {
|
|
|
1360
1525
|
const lines = rows.length > 0 ? ["Voices"] : [];
|
|
1361
1526
|
for (const row of rows) {
|
|
1362
1527
|
lines.push(row.voice
|
|
1363
|
-
? ` ${row.name.padEnd(width)} → ${voiceWords(row
|
|
1528
|
+
? ` ${row.name.padEnd(width)} → ${voiceWords(row)} ${availabilityWords(row)}`
|
|
1364
1529
|
: ` ${row.name.padEnd(width)} → no voice chosen (keeps the generated voice)`);
|
|
1365
1530
|
}
|
|
1366
1531
|
for (const notice of notices)
|
|
@@ -1372,7 +1537,7 @@ export function voiceSheetLines(sheet) {
|
|
|
1372
1537
|
if (sheet.cast.length === 0)
|
|
1373
1538
|
lines.push(" Nobody is in this ad yet.");
|
|
1374
1539
|
for (const row of sheet.cast) {
|
|
1375
|
-
const voice = voiceWords(row
|
|
1540
|
+
const voice = voiceWords(row);
|
|
1376
1541
|
lines.push(` ${row.characterId} ${row.name} ${voice} ${availabilityWords(row)} ` +
|
|
1377
1542
|
`speaks in ${row.spokenScenes} scenes, about ${row.spokenSeconds}s`);
|
|
1378
1543
|
}
|
|
@@ -1738,7 +1903,7 @@ function usage(line) {
|
|
|
1738
1903
|
console.error(`Error: ${line}`);
|
|
1739
1904
|
process.exit(1);
|
|
1740
1905
|
}
|
|
1741
|
-
export async function run(flags) {
|
|
1906
|
+
export async function run(flags, occurrences) {
|
|
1742
1907
|
const [sub, ...rest] = parsePositional();
|
|
1743
1908
|
const json = flags["json"] === true;
|
|
1744
1909
|
if (!sub) {
|
|
@@ -1771,6 +1936,7 @@ export async function run(flags) {
|
|
|
1771
1936
|
"flag",
|
|
1772
1937
|
"retry-frame",
|
|
1773
1938
|
"retry-clip",
|
|
1939
|
+
"revoice",
|
|
1774
1940
|
"voices",
|
|
1775
1941
|
"pull",
|
|
1776
1942
|
"upload",
|
|
@@ -1815,10 +1981,24 @@ export async function run(flags) {
|
|
|
1815
1981
|
}
|
|
1816
1982
|
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, flagString(flags, "note"), json, defaultDeps));
|
|
1817
1983
|
}
|
|
1984
|
+
if (sub === "revoice") {
|
|
1985
|
+
const sceneRaw = flagString(flags, "scene");
|
|
1986
|
+
const all = flags["all"] === true;
|
|
1987
|
+
if (all === (sceneRaw !== undefined)) {
|
|
1988
|
+
usage("video revoice needs one of --scene <n> (one clip) or --all (every clip that kept the video model's voice).");
|
|
1989
|
+
}
|
|
1990
|
+
if (all)
|
|
1991
|
+
return printResult(await revoiceFlow(runId, { all: true }, json, defaultDeps));
|
|
1992
|
+
const sceneIndex = Number(sceneRaw);
|
|
1993
|
+
if (!Number.isInteger(sceneIndex)) {
|
|
1994
|
+
usage(`video revoice --scene must be a whole scene number, not "${sceneRaw}".`);
|
|
1995
|
+
}
|
|
1996
|
+
return printResult(await revoiceFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, json, defaultDeps));
|
|
1997
|
+
}
|
|
1818
1998
|
if (sub === "voices") {
|
|
1819
1999
|
let voices;
|
|
1820
2000
|
try {
|
|
1821
|
-
voices = parseVoiceFlags(
|
|
2001
|
+
voices = parseVoiceFlags(occurrences, defaultDeps.readFile);
|
|
1822
2002
|
}
|
|
1823
2003
|
catch (err) {
|
|
1824
2004
|
usage(err.message);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
|
+
import type { FlagOccurrence } from "../lib/args.js";
|
|
2
3
|
import { type PollOptions, type PollResult } from "../lib/poll.js";
|
|
3
4
|
import { LEGACY_WORKFLOW_RUN_STATUS_VALUES, type RunStatus } from "../lib/runStatus.js";
|
|
4
5
|
import { type RunDeliverySummary } from "../lib/runVerdict.js";
|
|
@@ -416,8 +417,8 @@ interface RunFlowOptions {
|
|
|
416
417
|
onWarningLine?: (line: string) => void;
|
|
417
418
|
}
|
|
418
419
|
export declare function formatPauseNotice(pauseReason: WorkflowPauseReason | undefined, runId: string, dashboardUrl: string): string[];
|
|
419
|
-
export declare function parseRawInputFlags(
|
|
420
|
-
export declare function parseInputFlags(
|
|
420
|
+
export declare function parseRawInputFlags(occurrences: FlagOccurrence[]): Record<string, string>;
|
|
421
|
+
export declare function parseInputFlags(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, string>;
|
|
421
422
|
export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
|
|
422
423
|
mimeByExtension: Record<string, string>;
|
|
423
424
|
maxBytes: number;
|
|
@@ -426,11 +427,11 @@ export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
|
|
|
426
427
|
export declare const NO_DELIVERIES_WARNING: string;
|
|
427
428
|
export declare function noDeliveriesWarning(described: unknown): string | undefined;
|
|
428
429
|
export declare function serverWarningsToPrint(serverWarnings: unknown, alreadyPrinted: string[]): string[];
|
|
429
|
-
export declare function rejectTerminalFlag(
|
|
430
|
+
export declare function rejectTerminalFlag(occurrences: FlagOccurrence[]): void;
|
|
430
431
|
export declare function parseAutoApproveFlag(args: string[]): boolean;
|
|
431
|
-
export declare function parseRigOverridesFlag(
|
|
432
|
-
export declare function parseVoicesFlag(
|
|
433
|
-
export declare function parseFillFlag(
|
|
432
|
+
export declare function parseRigOverridesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
|
|
433
|
+
export declare function parseVoicesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceMap | undefined;
|
|
434
|
+
export declare function parseFillFlag(occurrences: FlagOccurrence[]): string | undefined;
|
|
434
435
|
export declare function formatWorkflowList(workflows: WorkflowListItem[]): string;
|
|
435
436
|
export declare function formatRecentRuns(runs: WorkflowRunProjection[]): string;
|
|
436
437
|
export declare function formatWorkflowVersions(versions: WorkflowVersion[]): string;
|
|
@@ -586,9 +587,9 @@ export declare function repairFlow(runId: string, action: "retry" | "skip" | "ki
|
|
|
586
587
|
json: boolean;
|
|
587
588
|
onProgressLine?: (line: string) => void;
|
|
588
589
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
589
|
-
export declare function parseSlotFlags(
|
|
590
|
+
export declare function parseSlotFlags(occurrences: FlagOccurrence[]): Record<string, string>;
|
|
590
591
|
export declare function answerFlow(runId: string, values: Record<string, string>, opts: {
|
|
591
592
|
json: boolean;
|
|
592
593
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
593
|
-
export declare function run(flags: Record<string, string | boolean
|
|
594
|
+
export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
|
|
594
595
|
export {};
|
|
@@ -552,35 +552,25 @@ function normalizeMultiValue(value) {
|
|
|
552
552
|
.filter((p) => p.length > 0)
|
|
553
553
|
.join(", ");
|
|
554
554
|
}
|
|
555
|
-
export function parseRawInputFlags(
|
|
555
|
+
export function parseRawInputFlags(occurrences) {
|
|
556
556
|
const inputs = {};
|
|
557
|
-
for (
|
|
558
|
-
|
|
559
|
-
let raw;
|
|
560
|
-
if (arg === "--input") {
|
|
561
|
-
raw = args[i + 1];
|
|
562
|
-
i++;
|
|
563
|
-
}
|
|
564
|
-
else if (arg.startsWith("--input=")) {
|
|
565
|
-
raw = arg.slice("--input=".length);
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
557
|
+
for (const { flag, value } of occurrences) {
|
|
558
|
+
if (flag !== "input")
|
|
568
559
|
continue;
|
|
569
|
-
|
|
570
|
-
if (!raw)
|
|
560
|
+
if (!value)
|
|
571
561
|
throw new Error("--input requires key=value");
|
|
572
|
-
const eq =
|
|
562
|
+
const eq = value.indexOf("=");
|
|
573
563
|
if (eq <= 0)
|
|
574
|
-
throw new Error(`--input must be key=value (got "${
|
|
575
|
-
const key =
|
|
564
|
+
throw new Error(`--input must be key=value (got "${value}")`);
|
|
565
|
+
const key = value.slice(0, eq).trim();
|
|
576
566
|
if (!key)
|
|
577
|
-
throw new Error(`--input must include a key (got "${
|
|
578
|
-
inputs[key] =
|
|
567
|
+
throw new Error(`--input must include a key (got "${value}")`);
|
|
568
|
+
inputs[key] = value.slice(eq + 1);
|
|
579
569
|
}
|
|
580
570
|
return inputs;
|
|
581
571
|
}
|
|
582
|
-
export function parseInputFlags(
|
|
583
|
-
const inputs = parseRawInputFlags(
|
|
572
|
+
export function parseInputFlags(occurrences, readFile) {
|
|
573
|
+
const inputs = parseRawInputFlags(occurrences);
|
|
584
574
|
for (const [key, value] of Object.entries(inputs)) {
|
|
585
575
|
inputs[key] = expandInputValue(key, value, readFile);
|
|
586
576
|
}
|
|
@@ -876,12 +866,10 @@ async function prepareRunInputs(workflowId, raw, deps, note) {
|
|
|
876
866
|
}
|
|
877
867
|
return { inputs: prepared, warnings };
|
|
878
868
|
}
|
|
879
|
-
export function rejectTerminalFlag(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
"workflow or it doesn't start. Re-run without --terminal.");
|
|
884
|
-
}
|
|
869
|
+
export function rejectTerminalFlag(occurrences) {
|
|
870
|
+
if (occurrences.some((o) => o.flag === "terminal")) {
|
|
871
|
+
throw new Error("--terminal is no longer supported: a run now executes the whole " +
|
|
872
|
+
"workflow or it doesn't start. Re-run without --terminal.");
|
|
885
873
|
}
|
|
886
874
|
}
|
|
887
875
|
export function parseAutoApproveFlag(args) {
|
|
@@ -894,21 +882,11 @@ export function parseAutoApproveFlag(args) {
|
|
|
894
882
|
}
|
|
895
883
|
return false;
|
|
896
884
|
}
|
|
897
|
-
export function parseRigOverridesFlag(
|
|
885
|
+
export function parseRigOverridesFlag(occurrences, readFile) {
|
|
898
886
|
let raw;
|
|
899
|
-
for (
|
|
900
|
-
|
|
901
|
-
let value;
|
|
902
|
-
if (arg === "--rig-overrides") {
|
|
903
|
-
value = args[i + 1];
|
|
904
|
-
i++;
|
|
905
|
-
}
|
|
906
|
-
else if (arg.startsWith("--rig-overrides=")) {
|
|
907
|
-
value = arg.slice("--rig-overrides=".length);
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
887
|
+
for (const { flag, value } of occurrences) {
|
|
888
|
+
if (flag !== "rig-overrides")
|
|
910
889
|
continue;
|
|
911
|
-
}
|
|
912
890
|
if (value === undefined || value.startsWith("--")) {
|
|
913
891
|
throw new Error("--rig-overrides requires JSON or @path/to/file.json");
|
|
914
892
|
}
|
|
@@ -951,21 +929,11 @@ export function parseRigOverridesFlag(args, readFile) {
|
|
|
951
929
|
}
|
|
952
930
|
return parsed;
|
|
953
931
|
}
|
|
954
|
-
export function parseVoicesFlag(
|
|
932
|
+
export function parseVoicesFlag(occurrences, readFile) {
|
|
955
933
|
let raw;
|
|
956
|
-
for (
|
|
957
|
-
|
|
958
|
-
let value;
|
|
959
|
-
if (arg === "--voices") {
|
|
960
|
-
value = args[i + 1];
|
|
961
|
-
i++;
|
|
962
|
-
}
|
|
963
|
-
else if (arg.startsWith("--voices=")) {
|
|
964
|
-
value = arg.slice("--voices=".length);
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
934
|
+
for (const { flag, value } of occurrences) {
|
|
935
|
+
if (flag !== "voices")
|
|
967
936
|
continue;
|
|
968
|
-
}
|
|
969
937
|
if (value === undefined || value.startsWith("--")) {
|
|
970
938
|
throw new Error("--voices requires JSON or @path/to/voices.json");
|
|
971
939
|
}
|
|
@@ -1008,25 +976,15 @@ export function parseVoicesFlag(args, readFile) {
|
|
|
1008
976
|
}
|
|
1009
977
|
return parsed;
|
|
1010
978
|
}
|
|
1011
|
-
export function parseFillFlag(
|
|
979
|
+
export function parseFillFlag(occurrences) {
|
|
1012
980
|
let name;
|
|
1013
|
-
for (
|
|
1014
|
-
|
|
1015
|
-
let raw;
|
|
1016
|
-
if (arg === "--fill") {
|
|
1017
|
-
raw = args[i + 1];
|
|
1018
|
-
i++;
|
|
1019
|
-
}
|
|
1020
|
-
else if (arg.startsWith("--fill=")) {
|
|
1021
|
-
raw = arg.slice("--fill=".length);
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
981
|
+
for (const { flag, value } of occurrences) {
|
|
982
|
+
if (flag !== "fill")
|
|
1024
983
|
continue;
|
|
1025
|
-
|
|
1026
|
-
if (raw === undefined || raw.startsWith("--")) {
|
|
984
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1027
985
|
throw new Error("--fill requires a saved fill's name");
|
|
1028
986
|
}
|
|
1029
|
-
const trimmed =
|
|
987
|
+
const trimmed = value.trim();
|
|
1030
988
|
if (!trimmed)
|
|
1031
989
|
throw new Error("--fill requires a saved fill's name");
|
|
1032
990
|
name = trimmed;
|
|
@@ -2858,30 +2816,20 @@ async function resumeAndMaybeWait(runId, triggerRunId, headline, opts, deps, lan
|
|
|
2858
2816
|
const prefix = opts.onProgressLine ? [] : startLines;
|
|
2859
2817
|
return { code: waited.code, lines: [...prefix, ...waited.lines] };
|
|
2860
2818
|
}
|
|
2861
|
-
export function parseSlotFlags(
|
|
2819
|
+
export function parseSlotFlags(occurrences) {
|
|
2862
2820
|
const values = {};
|
|
2863
|
-
for (
|
|
2864
|
-
|
|
2865
|
-
let raw;
|
|
2866
|
-
if (arg === "--slot") {
|
|
2867
|
-
raw = args[i + 1];
|
|
2868
|
-
i++;
|
|
2869
|
-
}
|
|
2870
|
-
else if (arg.startsWith("--slot=")) {
|
|
2871
|
-
raw = arg.slice("--slot=".length);
|
|
2872
|
-
}
|
|
2873
|
-
else {
|
|
2821
|
+
for (const { flag, value } of occurrences) {
|
|
2822
|
+
if (flag !== "slot")
|
|
2874
2823
|
continue;
|
|
2875
|
-
|
|
2876
|
-
if (!raw)
|
|
2824
|
+
if (!value)
|
|
2877
2825
|
throw new Error("--slot requires key=value");
|
|
2878
|
-
const eq =
|
|
2826
|
+
const eq = value.indexOf("=");
|
|
2879
2827
|
if (eq <= 0)
|
|
2880
|
-
throw new Error(`--slot must be key=value (got "${
|
|
2881
|
-
const key =
|
|
2828
|
+
throw new Error(`--slot must be key=value (got "${value}")`);
|
|
2829
|
+
const key = value.slice(0, eq).trim();
|
|
2882
2830
|
if (!key)
|
|
2883
|
-
throw new Error(`--slot must include a key (got "${
|
|
2884
|
-
values[key] =
|
|
2831
|
+
throw new Error(`--slot must include a key (got "${value}")`);
|
|
2832
|
+
values[key] = value.slice(eq + 1);
|
|
2885
2833
|
}
|
|
2886
2834
|
return values;
|
|
2887
2835
|
}
|
|
@@ -2953,7 +2901,7 @@ async function maybeReadStdin(flags) {
|
|
|
2953
2901
|
return undefined;
|
|
2954
2902
|
return raw.endsWith("\n") ? raw.slice(0, -1) : raw;
|
|
2955
2903
|
}
|
|
2956
|
-
export async function run(flags) {
|
|
2904
|
+
export async function run(flags, occurrences) {
|
|
2957
2905
|
const positional = parsePositional();
|
|
2958
2906
|
const [sub, ...rest] = positional;
|
|
2959
2907
|
const json = !!flags["json"];
|
|
@@ -3018,12 +2966,12 @@ export async function run(flags) {
|
|
|
3018
2966
|
let imageRigOverrides;
|
|
3019
2967
|
let voices;
|
|
3020
2968
|
try {
|
|
3021
|
-
inputs = parseRawInputFlags(
|
|
3022
|
-
rejectTerminalFlag(
|
|
3023
|
-
fill = parseFillFlag(
|
|
2969
|
+
inputs = parseRawInputFlags(occurrences);
|
|
2970
|
+
rejectTerminalFlag(occurrences);
|
|
2971
|
+
fill = parseFillFlag(occurrences);
|
|
3024
2972
|
autoApprove = parseAutoApproveFlag(process.argv.slice(3));
|
|
3025
|
-
imageRigOverrides = parseRigOverridesFlag(
|
|
3026
|
-
voices = parseVoicesFlag(
|
|
2973
|
+
imageRigOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
|
|
2974
|
+
voices = parseVoicesFlag(occurrences, defaultDeps.readFile);
|
|
3027
2975
|
}
|
|
3028
2976
|
catch (e) {
|
|
3029
2977
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3167,7 +3115,7 @@ export async function run(flags) {
|
|
|
3167
3115
|
}
|
|
3168
3116
|
let values;
|
|
3169
3117
|
try {
|
|
3170
|
-
values = parseSlotFlags(
|
|
3118
|
+
values = parseSlotFlags(occurrences);
|
|
3171
3119
|
}
|
|
3172
3120
|
catch (e) {
|
|
3173
3121
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3205,7 +3153,7 @@ export async function run(flags) {
|
|
|
3205
3153
|
}
|
|
3206
3154
|
let fireOverrides;
|
|
3207
3155
|
try {
|
|
3208
|
-
fireOverrides = parseRigOverridesFlag(
|
|
3156
|
+
fireOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
|
|
3209
3157
|
}
|
|
3210
3158
|
catch (e) {
|
|
3211
3159
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface FlagOccurrence {
|
|
2
|
+
flag: string;
|
|
3
|
+
value: string | undefined;
|
|
4
|
+
}
|
|
5
|
+
export interface ParsedArgs {
|
|
6
|
+
command: string;
|
|
7
|
+
flags: Record<string, string | boolean>;
|
|
8
|
+
occurrences: FlagOccurrence[];
|
|
9
|
+
}
|
|
10
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
package/dist/lib/args.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const [, , rawCommand = "help", ...rest] = argv;
|
|
3
|
+
const command = ["--help", "-h", "-help"].includes(rawCommand) ? "help" : rawCommand;
|
|
4
|
+
const flags = {};
|
|
5
|
+
const occurrences = [];
|
|
6
|
+
let i = 0;
|
|
7
|
+
while (i < rest.length) {
|
|
8
|
+
const arg = rest[i];
|
|
9
|
+
if (arg === "--help" || arg === "-h") {
|
|
10
|
+
flags["help"] = true;
|
|
11
|
+
i++;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg.startsWith("--")) {
|
|
15
|
+
const key = arg.slice(2);
|
|
16
|
+
const next = rest[i + 1];
|
|
17
|
+
const eq = key.indexOf("=");
|
|
18
|
+
occurrences.push(eq === -1
|
|
19
|
+
? { flag: key, value: next }
|
|
20
|
+
: { flag: key.slice(0, eq), value: key.slice(eq + 1) });
|
|
21
|
+
if (key.startsWith("no-")) {
|
|
22
|
+
flags[key.slice(3)] = false;
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
else if (next !== undefined && !next.startsWith("--")) {
|
|
26
|
+
flags[key] = next;
|
|
27
|
+
i += 2;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
flags[key] = true;
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { command, flags, occurrences };
|
|
39
|
+
}
|