@aicopycoders/exodus 2026.9.1800-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 +4 -0
- package/assets/skills/exodus-video/SKILL.md +191 -17
- package/dist/bin/exodus.js +3 -36
- package/dist/commands/video.d.ts +36 -3
- package/dist/commands/video.js +458 -39
- package/dist/commands/workflow.d.ts +11 -7
- package/dist/commands/workflow.js +144 -84
- 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
|
@@ -256,10 +256,14 @@ Start (Show route):
|
|
|
256
256
|
exodus video start --show <id> --script <file> [--wait] [--json] start an ad run; --wait stops at the storyboard gate
|
|
257
257
|
Then, on either route:
|
|
258
258
|
exodus video storyboard <runId> [--json] the scene cards
|
|
259
|
+
exodus video voices <runId> [--set <who>=<voiceId>] [--clear <who>] [--from <f>] [--json] who speaks, with whose voice
|
|
259
260
|
exodus video approve <runId> [--json] approve the storyboard, or the uploaded cut
|
|
260
261
|
exodus video flag <runId> --note "<what is wrong>" [--json] send the storyboard back — SHOW RUNS ONLY
|
|
262
|
+
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "<t>"] [--json] redo one still at the pixel gate
|
|
261
263
|
exodus video status <runId> [--json] the park and each scene's clip/voice/picture
|
|
262
264
|
exodus video pull <runId> --out <dir> [--json] every piece + manifest.json
|
|
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
|
|
263
267
|
exodus video upload <runId> --file <cut.mp4> [--duration <s>] [--json] attach the cut (MP4/MOV/WebM, 200 MB)
|
|
264
268
|
Cut: your choice of tool — Exodus does not stitch or mix. One option ships with the skill:
|
|
265
269
|
node .claude/skills/exodus-video/scripts/first-cut.mjs <dir> [--skip n,m] [--no-music] [--out f]
|
|
@@ -75,6 +75,8 @@ exodus workflow list the brand's workflows
|
|
|
75
75
|
exodus workflow describe <workflowId|name> what inputs it needs
|
|
76
76
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --wait
|
|
77
77
|
start, stop at the storyboard gate
|
|
78
|
+
exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voices.json --wait
|
|
79
|
+
same, with the brand's voices already chosen
|
|
78
80
|
exodus video status <runId> where it is, per scene
|
|
79
81
|
```
|
|
80
82
|
|
|
@@ -96,6 +98,9 @@ exodus video flag <runId> --note "what's wrong" send it back — SHOW
|
|
|
96
98
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
97
99
|
exodus video status <runId> where it is, per scene
|
|
98
100
|
exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
|
|
101
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
|
|
102
|
+
redo one clip at the final watch
|
|
103
|
+
exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
|
|
99
104
|
… make the cut (your choice of tool)
|
|
100
105
|
exodus video upload <runId> --file ./ad-<runId>/cut.mp4 attach it
|
|
101
106
|
exodus video approve <runId> make it the ad
|
|
@@ -115,7 +120,8 @@ per-scene detail, and the checkpoint verbs cannot resolve a video park — see
|
|
|
115
120
|
under it:
|
|
116
121
|
|
|
117
122
|
- storyboard gate: needs `approve`, or `flag` on a Show run.
|
|
118
|
-
- final watch: every piece is made; needs `pull`, a cut, and `upload`.
|
|
123
|
+
- final watch: every piece is made; needs `pull`, a cut, and `upload`. It is also
|
|
124
|
+
the one park where a finished clip can be redone, with `retry-clip`. A
|
|
119
125
|
workflow run only reaches this park if its video node sets `finalWatch: true`
|
|
120
126
|
— otherwise it skips straight to finished.
|
|
121
127
|
- paused: a builder checkpoint neither video verb resolves —
|
|
@@ -170,6 +176,8 @@ Verified against the routes, not assumed. Everything here is admin-gated on top.
|
|
|
170
176
|
| `video flag --note` | **refused** | works |
|
|
171
177
|
| script-turn edits at the gate | **refused** | dashboard only |
|
|
172
178
|
| `video retry-frame` | works | works |
|
|
179
|
+
| `video retry-clip` | works | works |
|
|
180
|
+
| `video revoice` | works | works |
|
|
173
181
|
| `video pull` | works | works |
|
|
174
182
|
| `video upload` | only if the video node sets `finalWatch: true` | works |
|
|
175
183
|
| `workflow checkpoint approve` | **refused at a video gate** | **refused at a video gate** |
|
|
@@ -218,11 +226,14 @@ Five consequences worth knowing before you promise a user anything:
|
|
|
218
226
|
slots park is still not a landing: it prints its notice once and the loop
|
|
219
227
|
keeps polling, so a resolve from another shell carries the run on. Do not read
|
|
220
228
|
a park banner mid-poll as the wait having finished.
|
|
221
|
-
- **The run's page link differs
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
- **The run's page link differs, and every link the CLI prints follows the run.**
|
|
230
|
+
A Show ad opens on the `/video` page. A showless workflow run opens on
|
|
231
|
+
`/workflows/<workflowId>/runs/<runId>`, because `/video` answers "That ad isn't
|
|
232
|
+
here" for a run with no Show (`getAdDetail` rejects it;
|
|
233
|
+
`convex/http.ts:9204-9211`). Since #1851 the manifest's `dashboardUrl`, the
|
|
234
|
+
link in every park block and the link `upload` prints are all minted that way,
|
|
235
|
+
so they agree on both routes. When all the CLI holds is a run id it mints
|
|
236
|
+
`/runs/<runId>`, the canonical forwarder, which lands on the same page.
|
|
226
237
|
|
|
227
238
|
## The script file
|
|
228
239
|
|
|
@@ -248,8 +259,10 @@ THE SOFTGEL: Tap below and give your eyes ClearBlink.
|
|
|
248
259
|
## Reading the storyboard gate
|
|
249
260
|
|
|
250
261
|
`storyboard <runId>` prints one card per scene on both routes: who speaks, the
|
|
251
|
-
line, the planned duration, and whether its picture is drawn.
|
|
252
|
-
|
|
262
|
+
line, the planned duration, and whether its picture is drawn. Under the cards it
|
|
263
|
+
also lists the voices — one line per character who speaks or already has a voice
|
|
264
|
+
— so the user can see who sounds like whom before paying for the clips. Read it
|
|
265
|
+
for the user, then ask one question.
|
|
253
266
|
|
|
254
267
|
On a **workflow run** the question is approve or don't — there is no flag. If
|
|
255
268
|
the storyboard is wrong, say so plainly and let the user decide between
|
|
@@ -266,9 +279,11 @@ whole loop unattended.
|
|
|
266
279
|
|
|
267
280
|
## Giving the characters voices
|
|
268
281
|
|
|
269
|
-
By default a workflow run's characters have no voice
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
272
287
|
it refuses.
|
|
273
288
|
|
|
274
289
|
```
|
|
@@ -302,6 +317,17 @@ on that account. Each refusal says the plain reason and saves nothing. If
|
|
|
302
317
|
ElevenLabs simply cannot be reached, the choice IS saved and the output says the
|
|
303
318
|
check could not be made — a provider outage never blocks the run.
|
|
304
319
|
|
|
320
|
+
**Approving checks the voices one last time.** `video approve` re-checks every
|
|
321
|
+
chosen voice before the clips are paid for. If ElevenLabs is certain one of them
|
|
322
|
+
is gone — deleted from the account, or the account changed — the approve is
|
|
323
|
+
refused in one plain sentence, nothing is approved and the command exits 1. Fix
|
|
324
|
+
it by choosing another voice (`video voices <runId> --set <character>=<voiceId>`)
|
|
325
|
+
or clearing that character (`--clear <character>`, which lets the clip keep the
|
|
326
|
+
voice the video model makes), then approve again. When the approve goes through,
|
|
327
|
+
the receipt lists each speaking character and the voice they got, plus any
|
|
328
|
+
`Heads-up:` lines worth reading — that is the last moment a wrong voice is cheap
|
|
329
|
+
to fix.
|
|
330
|
+
|
|
305
331
|
**The voices file.** A plain JSON object, keyed by the names the script uses:
|
|
306
332
|
|
|
307
333
|
```json
|
|
@@ -319,6 +345,18 @@ flag says `C2`, that is one character named two ways, and the whole request is
|
|
|
319
345
|
refused with a sentence saying so. Voice IDs belong to the user's own ElevenLabs account and to that brand's
|
|
320
346
|
folder — never put a brand's voice IDs in a shared workflow or template.
|
|
321
347
|
|
|
348
|
+
**One file, two moments.** A brand keeps a single `voices.json` in its own
|
|
349
|
+
folder. Hand it to the run at the start with `exodus workflow run <workflow>
|
|
350
|
+
--voices @voices.json` when the script is one the user supplied (it names its
|
|
351
|
+
speakers, so `HOST 1` means something before the planner has run), or hand it in
|
|
352
|
+
at the storyboard review with `exodus video voices <runId> --from voices.json`.
|
|
353
|
+
Launching with voices refuses before the run is created — nothing is spent — if
|
|
354
|
+
a speaker is not in the script, the same speaker is named twice, or ElevenLabs
|
|
355
|
+
is certain it does not have one of the voices; a workflow whose script is
|
|
356
|
+
written by a bot is refused with a pointer at the review-time command, because
|
|
357
|
+
nobody knows the speaker names yet. When the storyboard arrives, the voices are
|
|
358
|
+
already set and `exodus video voices <runId>` shows them.
|
|
359
|
+
|
|
322
360
|
**The JSON receipt** (`--json`), the same shape for reading and for setting:
|
|
323
361
|
|
|
324
362
|
```
|
|
@@ -329,7 +367,10 @@ canChange false once the run has left the storyboard review
|
|
|
329
367
|
whyNot why, in one sentence, when canChange is false
|
|
330
368
|
cast[] one per character
|
|
331
369
|
characterId, name
|
|
332
|
-
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
|
|
333
374
|
availability { state: "available" | "missing" | "not-checked", … }
|
|
334
375
|
spokenScenes how many scenes this character speaks in
|
|
335
376
|
spokenSeconds those scenes' planned seconds — what ElevenLabs bills on
|
|
@@ -344,8 +385,8 @@ A failure is the same shape every `video` verb uses:
|
|
|
344
385
|
`{ "ok": false, "status": 400, "error": "<one plain sentence>" }`, exit 1.
|
|
345
386
|
|
|
346
387
|
**Before you approve, read `notices`.** The ones that cost the user something:
|
|
347
|
-
a character who speaks but has no voice
|
|
348
|
-
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
|
|
349
390
|
a voice ElevenLabs no longer has.
|
|
350
391
|
|
|
351
392
|
`estimatedCostUsd` is always null. Exodus cannot price a voice conversion yet;
|
|
@@ -378,18 +419,28 @@ manifest.json the index below
|
|
|
378
419
|
`manifest.json`:
|
|
379
420
|
|
|
380
421
|
```
|
|
381
|
-
runId, pulledAt, dashboardUrl the run and
|
|
422
|
+
runId, pulledAt, dashboardUrl the run and the page that opens it:
|
|
423
|
+
/video?ad=<runId> for a Show ad,
|
|
424
|
+
/workflows/<workflowId>/runs/<runId> for a
|
|
425
|
+
workflow run
|
|
382
426
|
storyboard, reference, music filenames, or null when not delivered
|
|
383
427
|
narration { file, timing }, or null on a per-scene run
|
|
384
428
|
cast[] one per identity still
|
|
385
429
|
characterId, name from the run's cast lock; null for an
|
|
386
430
|
unclaimed ledger still
|
|
387
431
|
file, status, error
|
|
432
|
+
voiceId, voiceLabel the ElevenLabs voice this character was
|
|
433
|
+
given before the clips were made; null when
|
|
434
|
+
nobody chose one
|
|
388
435
|
scenes[] one per scene, in order
|
|
389
436
|
sceneIndex, durationSec the run's number and the clip's real length
|
|
390
437
|
clip, voice, keyframe, words filenames, or null
|
|
391
438
|
wordsFrom clip | voice — which file the word times are
|
|
392
439
|
measured against; null when none came
|
|
440
|
+
wordsDescribe this-file | original-performance — whether
|
|
441
|
+
those times were measured on the file you
|
|
442
|
+
have, or on the take a cast voice replaced;
|
|
443
|
+
null when no words came
|
|
393
444
|
clipStatus done | failed | running | pending | missing
|
|
394
445
|
error why the clip failed, when it did
|
|
395
446
|
flagged, findings[] QC verdict: {check, code, severity: fail|warn, detail}
|
|
@@ -401,6 +452,10 @@ scenes[] one per scene, in order
|
|
|
401
452
|
speechTrimmed the clip was re-cut to its spoken words
|
|
402
453
|
(0.5 s before the first, 0.8 s after the
|
|
403
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
|
|
404
459
|
failed[] files that did not download: {file, url, error}
|
|
405
460
|
```
|
|
406
461
|
|
|
@@ -466,6 +521,18 @@ re-based to the start of that scene's cut; on a per-scene run it is instead a
|
|
|
466
521
|
blind Scribe pass over that scene's own voice track. Nothing is aligned to the
|
|
467
522
|
finished ad — that timeline does not exist until you make it.
|
|
468
523
|
|
|
524
|
+
**How far to trust them** (`wordsDescribe`). `"this-file"` means the times were
|
|
525
|
+
measured on the very file `wordsFrom` names, so they hold to the frame: cut
|
|
526
|
+
captions and cue points straight off them. `"original-performance"` means the
|
|
527
|
+
scene's clip was re-voiced *after* its words were timed — the times describe the
|
|
528
|
+
take the cast voice replaced. The words and their order are right and the timing
|
|
529
|
+
is close, but nothing re-measures it, so check any cut that has to land on a
|
|
530
|
+
single frame. A clip carrying a `voice-timing-shifted` warning is the same thing
|
|
531
|
+
with a measured gap: the swapped audio came back a different length, so treat
|
|
532
|
+
that scene's times as a rough guide and watch it before you trust it. Nothing in
|
|
533
|
+
the pipeline speeds audio or video up or slows it down to make the two agree —
|
|
534
|
+
the timings are reported honestly instead.
|
|
535
|
+
|
|
469
536
|
**Cast identity stills** (`cast-*.<ext>`). A full-body identity still per cast
|
|
470
537
|
member, on a plain neutral background in the run's style, minted once and
|
|
471
538
|
frozen onto the run as its cast lock. Every scene picture was anchored to these,
|
|
@@ -487,14 +554,19 @@ music failure degrades to a warning rather than blocking the run.
|
|
|
487
554
|
- `wordsFrom: "voice"`, or a scene whose storyboard entry has `voText` and no
|
|
488
555
|
on-camera line: the clip is picture only and the voice track is the sound.
|
|
489
556
|
Do not use that clip's own audio.
|
|
557
|
+
- `wordsDescribe: "original-performance"`: the word times were measured before
|
|
558
|
+
that clip's cast voice was swapped in. Close, not frame-exact — say so if the
|
|
559
|
+
user is cutting captions to the frame.
|
|
490
560
|
- `manifest.narration` present: listen to `narration.mp3` end to end, then the
|
|
491
561
|
joins between adjacent `scene-NN.voice` files, before you trust the cut.
|
|
492
562
|
Matching voice IDs and a one-speaker diarization result do not prove
|
|
493
563
|
continuity (#1802).
|
|
494
564
|
- `flagged: true` is a warning, not a block. The clip was delivered anyway; the
|
|
495
565
|
findings say what the QC model saw (`wrong-character`, `eyeline-off`,
|
|
496
|
-
`set-drift`, `speech-cutoff`, `framing-off`). Tell the
|
|
497
|
-
flagged and why, and let them decide whether to keep,
|
|
566
|
+
`set-drift`, `speech-cutoff`, `framing-off`, `voice-timing-shifted`). Tell the
|
|
567
|
+
user which scenes are flagged and why, and let them decide whether to keep,
|
|
568
|
+
trim, drop or redo each one (`retry-clip`, see "Redoing one clip at the final
|
|
569
|
+
watch").
|
|
498
570
|
A `speech-cutoff` on the CTA scene is the one to worry about; the ad's last
|
|
499
571
|
words are missing. `status` shows the checker's own wording behind each
|
|
500
572
|
finding; the manifest carries only the verdict.
|
|
@@ -506,6 +578,108 @@ music failure degrades to a warning rather than blocking the run.
|
|
|
506
578
|
the run parks at the final watch, which means nothing is still rendering.
|
|
507
579
|
- `failed[]` non-empty: run the same `pull` again.
|
|
508
580
|
|
|
581
|
+
## Redoing one clip at the final watch
|
|
582
|
+
|
|
583
|
+
`retry-clip` re-renders ONE scene's clip on the run that already made it. There
|
|
584
|
+
is no second run, no new storyboard and no re-render of anything else.
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
exodus video retry-clip <runId> --scene 3
|
|
588
|
+
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
589
|
+
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
593
|
+
the CLI asks for it by name when it does.
|
|
594
|
+
|
|
595
|
+
**When it is allowed.** A failed clip, once the step that made it has finished. A
|
|
596
|
+
finished clip, only while the run is parked at the final watch, or when QC
|
|
597
|
+
flagged it (`flagged: true` in `status` and in the manifest). Everything else is
|
|
598
|
+
refused in one plain sentence before any task is queued: a step still rendering
|
|
599
|
+
its other scenes or one that failed outright, a scene already being redone, a
|
|
600
|
+
finished clip on a run that is not at its final watch. An ad that is already
|
|
601
|
+
delivered is refused by the server, in its own sentence.
|
|
602
|
+
|
|
603
|
+
**What stays untouched.** Every other scene's clip, every keyframe, the cast
|
|
604
|
+
pins, the voices and the approved script. The redone clip re-renders from the
|
|
605
|
+
same keyframe with the same voice path the first take used, so it comes back
|
|
606
|
+
revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
607
|
+
|
|
608
|
+
**A `--note` steers the motion, never the words.** It is folded into that
|
|
609
|
+
scene's motion prompt only (`withRedoDirection`,
|
|
610
|
+
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
611
|
+
the approved storyboard. Asking for different words in a note will not change
|
|
612
|
+
them. If the words are wrong, that is a storyboard flag or a new run.
|
|
613
|
+
|
|
614
|
+
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
615
|
+
clip the row stays `done` with the ORIGINAL take and only its findings are
|
|
616
|
+
refreshed, because a worse take must never replace a delivered one. So never
|
|
617
|
+
report that the clip changed. Read `status` again for the new findings, then pull
|
|
618
|
+
and compare the file before and after.
|
|
619
|
+
|
|
620
|
+
**Re-cut afterwards.** A cut you already uploaded still holds the old clip, and
|
|
621
|
+
the CLI says so when that is the case. Pull again, re-cut and upload again before
|
|
622
|
+
anyone approves.
|
|
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
|
+
|
|
509
683
|
## Cutting the ad
|
|
510
684
|
|
|
511
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;
|
|
@@ -70,6 +73,7 @@ export interface VideoRunNode {
|
|
|
70
73
|
kind: string;
|
|
71
74
|
status: "idle" | "running" | "done" | "failed" | "skipped";
|
|
72
75
|
error?: string;
|
|
76
|
+
warning?: string;
|
|
73
77
|
outputs?: ArtifactSubset[];
|
|
74
78
|
}
|
|
75
79
|
export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
|
|
@@ -96,6 +100,8 @@ export interface VideoRun {
|
|
|
96
100
|
pausedNodeId?: string;
|
|
97
101
|
nodes: VideoRunNode[];
|
|
98
102
|
castLock?: PullCastLock | null;
|
|
103
|
+
workflowId?: string;
|
|
104
|
+
moduleOwned?: boolean;
|
|
99
105
|
}
|
|
100
106
|
export declare function asVideoRun(data: unknown): VideoRun;
|
|
101
107
|
export interface NodeItem {
|
|
@@ -177,7 +183,8 @@ export type RunStop = {
|
|
|
177
183
|
export declare function classifyRun(run: VideoRun): RunStop;
|
|
178
184
|
export declare function stageWord(stage: string): string;
|
|
179
185
|
export declare function stepName(kind: string | undefined): string;
|
|
180
|
-
export declare function
|
|
186
|
+
export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
|
|
187
|
+
export declare function stopLines(stop: RunStop, runId: string, runUrl: string): string[];
|
|
181
188
|
export interface PullDownload {
|
|
182
189
|
file: string;
|
|
183
190
|
url: string;
|
|
@@ -192,11 +199,13 @@ export interface ManifestScene {
|
|
|
192
199
|
clip: string | null;
|
|
193
200
|
words: string | null;
|
|
194
201
|
wordsFrom: "clip" | "voice" | null;
|
|
202
|
+
wordsDescribe: "original-performance" | "this-file" | null;
|
|
195
203
|
voice: string | null;
|
|
196
204
|
keyframe: string | null;
|
|
197
205
|
qc: ClipQc | null;
|
|
198
206
|
revoiced: boolean | null;
|
|
199
207
|
speechTrimmed: boolean | null;
|
|
208
|
+
rawStorageId: string | null;
|
|
200
209
|
clipStatus: string;
|
|
201
210
|
error: string | null;
|
|
202
211
|
flagged: boolean;
|
|
@@ -214,6 +223,8 @@ export interface ManifestCastRef {
|
|
|
214
223
|
file: string | null;
|
|
215
224
|
status: string;
|
|
216
225
|
error: string | null;
|
|
226
|
+
voiceId: string | null;
|
|
227
|
+
voiceLabel: string | null;
|
|
217
228
|
}
|
|
218
229
|
export interface VideoManifest {
|
|
219
230
|
runId: string;
|
|
@@ -265,6 +276,27 @@ export declare function storyboardFlow(runId: string, json: boolean, deps: Video
|
|
|
265
276
|
export declare function approveFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
266
277
|
export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
267
278
|
export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
279
|
+
export type ClipRedoPlan = {
|
|
280
|
+
ok: true;
|
|
281
|
+
nodeId: string;
|
|
282
|
+
sceneIndex: number;
|
|
283
|
+
attempt: number;
|
|
284
|
+
warnings: string[];
|
|
285
|
+
} | {
|
|
286
|
+
ok: false;
|
|
287
|
+
reason: string;
|
|
288
|
+
};
|
|
289
|
+
export interface ClipRedoTarget {
|
|
290
|
+
sceneIndex: number;
|
|
291
|
+
nodeId?: string;
|
|
292
|
+
}
|
|
293
|
+
export declare function planClipRedo(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
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>;
|
|
268
300
|
export interface CastVoiceRow {
|
|
269
301
|
characterId: string;
|
|
270
302
|
name: string;
|
|
@@ -272,6 +304,7 @@ export interface CastVoiceRow {
|
|
|
272
304
|
voiceId: string;
|
|
273
305
|
label?: string;
|
|
274
306
|
} | null;
|
|
307
|
+
voiceFrom?: "own-pin" | "run-default";
|
|
275
308
|
availability?: {
|
|
276
309
|
state: "available";
|
|
277
310
|
providerName: string;
|
|
@@ -311,7 +344,7 @@ export type VoiceMap = Record<string, string | {
|
|
|
311
344
|
voiceId: string;
|
|
312
345
|
label?: string;
|
|
313
346
|
} | null>;
|
|
314
|
-
export declare function parseVoiceFlags(
|
|
347
|
+
export declare function parseVoiceFlags(occurrences: FlagOccurrence[], readFile: (path: string) => string): VoiceMap | null;
|
|
315
348
|
export declare function voicesFlow(runId: string, voices: VoiceMap | null, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
316
349
|
export declare function voiceSheetLines(sheet: CastVoiceSheet): string[];
|
|
317
350
|
export declare function pullFlow(runId: string, dir: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
@@ -320,4 +353,4 @@ export declare const NO_DURATION_MESSAGE: string;
|
|
|
320
353
|
export declare function describeFetchFailure(err: unknown): string;
|
|
321
354
|
export declare function uploadFlow(runId: string, filePath: string, durationFlag: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
322
355
|
export declare function parsePositional(args?: string[]): string[];
|
|
323
|
-
export declare function run(flags: Record<string, string | boolean
|
|
356
|
+
export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
|