@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.
@@ -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`. A
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.** A Show run's `dashboardUrl` opens
222
- `/video?show=…&ad=…`; a showless run gets `/runs/<runId>`, because the `/video`
223
- page cannot render one (`convex/http.ts:9204-9211`). The manifest's
224
- `dashboardUrl` is always `/video?ad=<runId>`, which is the Show-route shape —
225
- on a workflow run, hand the user the link `upload` prints, not the manifest's.
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. Read it for the
252
- user, then ask one question.
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 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
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 } or null
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 (those clips keep their generated
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 its page (/video?ad=<runId>)
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 user which scenes are
497
- flagged and why, and let them decide whether to keep, trim, or drop each one.
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
@@ -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));
@@ -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 stopLines(stop: RunStop, runId: string, dashboardUrl: string): string[];
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(argv: readonly string[], readFile: (path: string) => string): VoiceMap | null;
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>): Promise<void>;
356
+ export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;