@aicopycoders/exodus 2026.9.1700-beta.1 → 2026.9.1900-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/docs/PIPELINES.md +3 -0
- package/assets/skills/exodus-video/SKILL.md +217 -18
- package/dist/commands/video.d.ts +74 -1
- package/dist/commands/video.js +415 -22
- package/dist/commands/workflow.d.ts +3 -0
- package/dist/commands/workflow.js +116 -4
- package/package.json +1 -1
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -256,10 +256,13 @@ 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
|
|
263
266
|
exodus video upload <runId> --file <cut.mp4> [--duration <s>] [--json] attach the cut (MP4/MOV/WebM, 200 MB)
|
|
264
267
|
Cut: your choice of tool — Exodus does not stitch or mix. One option ships with the skill:
|
|
265
268
|
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
|
|
|
@@ -89,11 +91,15 @@ exodus video start --show <id> --script script.txt --wait start, wait for the s
|
|
|
89
91
|
|
|
90
92
|
```operator-guide
|
|
91
93
|
exodus video storyboard <runId> read the scene cards
|
|
94
|
+
exodus video voices <runId> who speaks and with whose voice
|
|
95
|
+
exodus video voices <runId> --set <character>=<voiceId> give a character a voice
|
|
92
96
|
exodus video approve <runId> keep going
|
|
93
97
|
exodus video flag <runId> --note "what's wrong" send it back — SHOW RUNS ONLY
|
|
94
98
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
95
99
|
exodus video status <runId> where it is, per scene
|
|
96
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
|
|
97
103
|
… make the cut (your choice of tool)
|
|
98
104
|
exodus video upload <runId> --file ./ad-<runId>/cut.mp4 attach it
|
|
99
105
|
exodus video approve <runId> make it the ad
|
|
@@ -113,7 +119,8 @@ per-scene detail, and the checkpoint verbs cannot resolve a video park — see
|
|
|
113
119
|
under it:
|
|
114
120
|
|
|
115
121
|
- storyboard gate: needs `approve`, or `flag` on a Show run.
|
|
116
|
-
- final watch: every piece is made; needs `pull`, a cut, and `upload`.
|
|
122
|
+
- final watch: every piece is made; needs `pull`, a cut, and `upload`. It is also
|
|
123
|
+
the one park where a finished clip can be redone, with `retry-clip`. A
|
|
117
124
|
workflow run only reaches this park if its video node sets `finalWatch: true`
|
|
118
125
|
— otherwise it skips straight to finished.
|
|
119
126
|
- paused: a builder checkpoint neither video verb resolves —
|
|
@@ -162,10 +169,13 @@ Verified against the routes, not assumed. Everything here is admin-gated on top.
|
|
|
162
169
|
| `workflow run` | starts the run | not the entry point |
|
|
163
170
|
| `video status` | works | works |
|
|
164
171
|
| `video storyboard` | works | works |
|
|
172
|
+
| `video voices` (read) | works | works |
|
|
173
|
+
| `video voices --set` | works | **refused** — the Show owns its voices |
|
|
165
174
|
| `video approve` | works, both gates | works, both gates |
|
|
166
175
|
| `video flag --note` | **refused** | works |
|
|
167
176
|
| script-turn edits at the gate | **refused** | dashboard only |
|
|
168
177
|
| `video retry-frame` | works | works |
|
|
178
|
+
| `video retry-clip` | works | works |
|
|
169
179
|
| `video pull` | works | works |
|
|
170
180
|
| `video upload` | only if the video node sets `finalWatch: true` | works |
|
|
171
181
|
| `workflow checkpoint approve` | **refused at a video gate** | **refused at a video gate** |
|
|
@@ -196,13 +206,16 @@ Five consequences worth knowing before you promise a user anything:
|
|
|
196
206
|
|
|
197
207
|
- **`flag` is Show-only and fails loudly.** `exodus video flag` on a workflow run
|
|
198
208
|
answers "Flag-with-note is only available on Video-module ads."
|
|
199
|
-
(`convex/videoModule.ts
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
wired only to the dashboard's gate screen, not
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
(`convex/videoModule.ts`, guarded on `!run.showId`). Script-turn edits refuse
|
|
210
|
+
the same way. There is **no** CLI path that sends a workflow run's storyboard
|
|
211
|
+
back with a note, and none that rewrites its scenes, script or prompts — the
|
|
212
|
+
whole-envelope edit mutation is wired only to the dashboard's gate screen, not
|
|
213
|
+
to any API route. The one exception is voices: `exodus video voices --set`
|
|
214
|
+
writes the cast's voice choices in place and nothing else (see "Giving the
|
|
215
|
+
characters voices"). For everything else the storyboard is approve-or-cancel
|
|
216
|
+
from the terminal: `exodus workflow cancel <runId>` and run again with a better
|
|
217
|
+
script, or edit the workflow. Do not offer `flag` to a user on the workflow
|
|
218
|
+
route.
|
|
206
219
|
- **`--wait` lands on both video parks, but only those.** Since #1818 every
|
|
207
220
|
`--wait` caller — `workflow run`, `triggers fire`, `checkpoint approve|retry`,
|
|
208
221
|
`repair retry|skip` — ends at a storyboard gate or a final watch, exits 0, and
|
|
@@ -211,11 +224,14 @@ Five consequences worth knowing before you promise a user anything:
|
|
|
211
224
|
slots park is still not a landing: it prints its notice once and the loop
|
|
212
225
|
keeps polling, so a resolve from another shell carries the run on. Do not read
|
|
213
226
|
a park banner mid-poll as the wait having finished.
|
|
214
|
-
- **The run's page link differs
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
227
|
+
- **The run's page link differs, and every link the CLI prints follows the run.**
|
|
228
|
+
A Show ad opens on the `/video` page. A showless workflow run opens on
|
|
229
|
+
`/workflows/<workflowId>/runs/<runId>`, because `/video` answers "That ad isn't
|
|
230
|
+
here" for a run with no Show (`getAdDetail` rejects it;
|
|
231
|
+
`convex/http.ts:9204-9211`). Since #1851 the manifest's `dashboardUrl`, the
|
|
232
|
+
link in every park block and the link `upload` prints are all minted that way,
|
|
233
|
+
so they agree on both routes. When all the CLI holds is a run id it mints
|
|
234
|
+
`/runs/<runId>`, the canonical forwarder, which lands on the same page.
|
|
219
235
|
|
|
220
236
|
## The script file
|
|
221
237
|
|
|
@@ -241,8 +257,10 @@ THE SOFTGEL: Tap below and give your eyes ClearBlink.
|
|
|
241
257
|
## Reading the storyboard gate
|
|
242
258
|
|
|
243
259
|
`storyboard <runId>` prints one card per scene on both routes: who speaks, the
|
|
244
|
-
line, the planned duration, and whether its picture is drawn.
|
|
245
|
-
|
|
260
|
+
line, the planned duration, and whether its picture is drawn. Under the cards it
|
|
261
|
+
also lists the voices — one line per character who speaks or already has a voice
|
|
262
|
+
— so the user can see who sounds like whom before paying for the clips. Read it
|
|
263
|
+
for the user, then ask one question.
|
|
246
264
|
|
|
247
265
|
On a **workflow run** the question is approve or don't — there is no flag. If
|
|
248
266
|
the storyboard is wrong, say so plainly and let the user decide between
|
|
@@ -257,6 +275,117 @@ The note is read by the model that rewrites the storyboard, so make it concrete
|
|
|
257
275
|
Either way, do not approve on the user's behalf unless they told you to run the
|
|
258
276
|
whole loop unattended.
|
|
259
277
|
|
|
278
|
+
## Giving the characters voices
|
|
279
|
+
|
|
280
|
+
By default a workflow run's characters have no voice chosen, so every clip keeps
|
|
281
|
+
the voice the video model invents. `exodus video voices` is where that gets
|
|
282
|
+
fixed, and it only works **before** the storyboard is approved — once clips start
|
|
283
|
+
it refuses.
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
exodus video voices <runId>
|
|
287
|
+
exodus video voices <runId> --json
|
|
288
|
+
exodus video voices <runId> --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
289
|
+
exodus video voices <runId> --from voices.json
|
|
290
|
+
exodus video voices <runId> --clear C2
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Read it first. With no flags it prints one line per character: the character's
|
|
294
|
+
ID, the name the script uses, the voice chosen, whether ElevenLabs actually has
|
|
295
|
+
that voice on the user's account, and how many scenes that character speaks in.
|
|
296
|
+
Under it: how the voices get applied, who pays, and anything worth knowing before
|
|
297
|
+
approving.
|
|
298
|
+
|
|
299
|
+
Name a character either way. `C1` is the ID the planner handed out; `HOST 1` is
|
|
300
|
+
the name the script uses. Both work, and the name is the portable one — the same
|
|
301
|
+
voices file works on every run of that brand because the planner may hand out the
|
|
302
|
+
IDs differently each time.
|
|
303
|
+
|
|
304
|
+
**What `--set` changes.** The voice choices, and nothing else. The script, the
|
|
305
|
+
scene pictures and the run itself are left exactly as they were — no re-boarding,
|
|
306
|
+
no picture is redrawn, nothing is charged. A character whose voice was already
|
|
307
|
+
what you asked for reports "Nothing changed".
|
|
308
|
+
|
|
309
|
+
**What it refuses, and why nothing is written when it does.** A character name
|
|
310
|
+
nobody in the cast answers to, a voice ID with a space in it (people paste the
|
|
311
|
+
voice's *name* by mistake), and a voice ID ElevenLabs is certain it does not have
|
|
312
|
+
on that account. Each refusal says the plain reason and saves nothing. If
|
|
313
|
+
ElevenLabs simply cannot be reached, the choice IS saved and the output says the
|
|
314
|
+
check could not be made — a provider outage never blocks the run.
|
|
315
|
+
|
|
316
|
+
**Approving checks the voices one last time.** `video approve` re-checks every
|
|
317
|
+
chosen voice before the clips are paid for. If ElevenLabs is certain one of them
|
|
318
|
+
is gone — deleted from the account, or the account changed — the approve is
|
|
319
|
+
refused in one plain sentence, nothing is approved and the command exits 1. Fix
|
|
320
|
+
it by choosing another voice (`video voices <runId> --set <character>=<voiceId>`)
|
|
321
|
+
or clearing that character (`--clear <character>`, which lets the clip keep the
|
|
322
|
+
voice the video model makes), then approve again. When the approve goes through,
|
|
323
|
+
the receipt lists each speaking character and the voice they got, plus any
|
|
324
|
+
`Heads-up:` lines worth reading — that is the last moment a wrong voice is cheap
|
|
325
|
+
to fix.
|
|
326
|
+
|
|
327
|
+
**The voices file.** A plain JSON object, keyed by the names the script uses:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"HOST 1": { "voiceId": "abc123voiceid", "label": "First host" },
|
|
332
|
+
"HOST 2": { "voiceId": "def456voiceid", "label": "Second host" }
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
A bare string works instead of the object when you do not care about the label
|
|
337
|
+
(`"HOST 1": "abc123voiceid"`), and `null` clears that character's voice. `--set`
|
|
338
|
+
and `--clear` win over the same name in the file, so one entry can be overridden
|
|
339
|
+
without editing it. Use the name the file uses: if the file says `HOST 2` and the
|
|
340
|
+
flag says `C2`, that is one character named two ways, and the whole request is
|
|
341
|
+
refused with a sentence saying so. Voice IDs belong to the user's own ElevenLabs account and to that brand's
|
|
342
|
+
folder — never put a brand's voice IDs in a shared workflow or template.
|
|
343
|
+
|
|
344
|
+
**One file, two moments.** A brand keeps a single `voices.json` in its own
|
|
345
|
+
folder. Hand it to the run at the start with `exodus workflow run <workflow>
|
|
346
|
+
--voices @voices.json` when the script is one the user supplied (it names its
|
|
347
|
+
speakers, so `HOST 1` means something before the planner has run), or hand it in
|
|
348
|
+
at the storyboard review with `exodus video voices <runId> --from voices.json`.
|
|
349
|
+
Launching with voices refuses before the run is created — nothing is spent — if
|
|
350
|
+
a speaker is not in the script, the same speaker is named twice, or ElevenLabs
|
|
351
|
+
is certain it does not have one of the voices; a workflow whose script is
|
|
352
|
+
written by a bot is refused with a pointer at the review-time command, because
|
|
353
|
+
nobody knows the speaker names yet. When the storyboard arrives, the voices are
|
|
354
|
+
already set and `exodus video voices <runId>` shows them.
|
|
355
|
+
|
|
356
|
+
**The JSON receipt** (`--json`), the same shape for reading and for setting:
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
ok true
|
|
360
|
+
runId
|
|
361
|
+
changed[] the characters whose voice actually moved (set only)
|
|
362
|
+
canChange false once the run has left the storyboard review
|
|
363
|
+
whyNot why, in one sentence, when canChange is false
|
|
364
|
+
cast[] one per character
|
|
365
|
+
characterId, name
|
|
366
|
+
voice { voiceId, label } or null
|
|
367
|
+
availability { state: "available" | "missing" | "not-checked", … }
|
|
368
|
+
spokenScenes how many scenes this character speaks in
|
|
369
|
+
spokenSeconds those scenes' planned seconds — what ElevenLabs bills on
|
|
370
|
+
narrator the one fallback voice, set on the workflow, not here
|
|
371
|
+
treatment how the voices reach the clips: summary, provider, model,
|
|
372
|
+
speedChange, billedTo, estimatedCostUsd, costNote,
|
|
373
|
+
usage { clips, seconds }, keyOnRun
|
|
374
|
+
notices[] plain sentences worth reading before approving
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A failure is the same shape every `video` verb uses:
|
|
378
|
+
`{ "ok": false, "status": 400, "error": "<one plain sentence>" }`, exit 1.
|
|
379
|
+
|
|
380
|
+
**Before you approve, read `notices`.** The ones that cost the user something:
|
|
381
|
+
a character who speaks but has no voice (those clips keep their generated
|
|
382
|
+
voice), no ElevenLabs key saved on the run (no voice can be applied at all), and
|
|
383
|
+
a voice ElevenLabs no longer has.
|
|
384
|
+
|
|
385
|
+
`estimatedCostUsd` is always null. Exodus cannot price a voice conversion yet;
|
|
386
|
+
ElevenLabs bills the user's own account by audio length, and `usage` gives the
|
|
387
|
+
length. Do not invent a number.
|
|
388
|
+
|
|
260
389
|
## The handover: what `pull` gives you
|
|
261
390
|
|
|
262
391
|
`pull` writes a flat folder and a `manifest.json` that indexes it. The manifest
|
|
@@ -283,18 +412,28 @@ manifest.json the index below
|
|
|
283
412
|
`manifest.json`:
|
|
284
413
|
|
|
285
414
|
```
|
|
286
|
-
runId, pulledAt, dashboardUrl the run and
|
|
415
|
+
runId, pulledAt, dashboardUrl the run and the page that opens it:
|
|
416
|
+
/video?ad=<runId> for a Show ad,
|
|
417
|
+
/workflows/<workflowId>/runs/<runId> for a
|
|
418
|
+
workflow run
|
|
287
419
|
storyboard, reference, music filenames, or null when not delivered
|
|
288
420
|
narration { file, timing }, or null on a per-scene run
|
|
289
421
|
cast[] one per identity still
|
|
290
422
|
characterId, name from the run's cast lock; null for an
|
|
291
423
|
unclaimed ledger still
|
|
292
424
|
file, status, error
|
|
425
|
+
voiceId, voiceLabel the ElevenLabs voice this character was
|
|
426
|
+
given before the clips were made; null when
|
|
427
|
+
nobody chose one
|
|
293
428
|
scenes[] one per scene, in order
|
|
294
429
|
sceneIndex, durationSec the run's number and the clip's real length
|
|
295
430
|
clip, voice, keyframe, words filenames, or null
|
|
296
431
|
wordsFrom clip | voice — which file the word times are
|
|
297
432
|
measured against; null when none came
|
|
433
|
+
wordsDescribe this-file | original-performance — whether
|
|
434
|
+
those times were measured on the file you
|
|
435
|
+
have, or on the take a cast voice replaced;
|
|
436
|
+
null when no words came
|
|
298
437
|
clipStatus done | failed | running | pending | missing
|
|
299
438
|
error why the clip failed, when it did
|
|
300
439
|
flagged, findings[] QC verdict: {check, code, severity: fail|warn, detail}
|
|
@@ -371,6 +510,18 @@ re-based to the start of that scene's cut; on a per-scene run it is instead a
|
|
|
371
510
|
blind Scribe pass over that scene's own voice track. Nothing is aligned to the
|
|
372
511
|
finished ad — that timeline does not exist until you make it.
|
|
373
512
|
|
|
513
|
+
**How far to trust them** (`wordsDescribe`). `"this-file"` means the times were
|
|
514
|
+
measured on the very file `wordsFrom` names, so they hold to the frame: cut
|
|
515
|
+
captions and cue points straight off them. `"original-performance"` means the
|
|
516
|
+
scene's clip was re-voiced *after* its words were timed — the times describe the
|
|
517
|
+
take the cast voice replaced. The words and their order are right and the timing
|
|
518
|
+
is close, but nothing re-measures it, so check any cut that has to land on a
|
|
519
|
+
single frame. A clip carrying a `voice-timing-shifted` warning is the same thing
|
|
520
|
+
with a measured gap: the swapped audio came back a different length, so treat
|
|
521
|
+
that scene's times as a rough guide and watch it before you trust it. Nothing in
|
|
522
|
+
the pipeline speeds audio or video up or slows it down to make the two agree —
|
|
523
|
+
the timings are reported honestly instead.
|
|
524
|
+
|
|
374
525
|
**Cast identity stills** (`cast-*.<ext>`). A full-body identity still per cast
|
|
375
526
|
member, on a plain neutral background in the run's style, minted once and
|
|
376
527
|
frozen onto the run as its cast lock. Every scene picture was anchored to these,
|
|
@@ -392,14 +543,19 @@ music failure degrades to a warning rather than blocking the run.
|
|
|
392
543
|
- `wordsFrom: "voice"`, or a scene whose storyboard entry has `voText` and no
|
|
393
544
|
on-camera line: the clip is picture only and the voice track is the sound.
|
|
394
545
|
Do not use that clip's own audio.
|
|
546
|
+
- `wordsDescribe: "original-performance"`: the word times were measured before
|
|
547
|
+
that clip's cast voice was swapped in. Close, not frame-exact — say so if the
|
|
548
|
+
user is cutting captions to the frame.
|
|
395
549
|
- `manifest.narration` present: listen to `narration.mp3` end to end, then the
|
|
396
550
|
joins between adjacent `scene-NN.voice` files, before you trust the cut.
|
|
397
551
|
Matching voice IDs and a one-speaker diarization result do not prove
|
|
398
552
|
continuity (#1802).
|
|
399
553
|
- `flagged: true` is a warning, not a block. The clip was delivered anyway; the
|
|
400
554
|
findings say what the QC model saw (`wrong-character`, `eyeline-off`,
|
|
401
|
-
`set-drift`, `speech-cutoff`, `framing-off`). Tell the
|
|
402
|
-
flagged and why, and let them decide whether to keep,
|
|
555
|
+
`set-drift`, `speech-cutoff`, `framing-off`, `voice-timing-shifted`). Tell the
|
|
556
|
+
user which scenes are flagged and why, and let them decide whether to keep,
|
|
557
|
+
trim, drop or redo each one (`retry-clip`, see "Redoing one clip at the final
|
|
558
|
+
watch").
|
|
403
559
|
A `speech-cutoff` on the CTA scene is the one to worry about; the ad's last
|
|
404
560
|
words are missing. `status` shows the checker's own wording behind each
|
|
405
561
|
finding; the manifest carries only the verdict.
|
|
@@ -411,6 +567,49 @@ music failure degrades to a warning rather than blocking the run.
|
|
|
411
567
|
the run parks at the final watch, which means nothing is still rendering.
|
|
412
568
|
- `failed[]` non-empty: run the same `pull` again.
|
|
413
569
|
|
|
570
|
+
## Redoing one clip at the final watch
|
|
571
|
+
|
|
572
|
+
`retry-clip` re-renders ONE scene's clip on the run that already made it. There
|
|
573
|
+
is no second run, no new storyboard and no re-render of anything else.
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
exodus video retry-clip <runId> --scene 3
|
|
577
|
+
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
578
|
+
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
582
|
+
the CLI asks for it by name when it does.
|
|
583
|
+
|
|
584
|
+
**When it is allowed.** A failed clip, once the step that made it has finished. A
|
|
585
|
+
finished clip, only while the run is parked at the final watch, or when QC
|
|
586
|
+
flagged it (`flagged: true` in `status` and in the manifest). Everything else is
|
|
587
|
+
refused in one plain sentence before any task is queued: a step still rendering
|
|
588
|
+
its other scenes or one that failed outright, a scene already being redone, a
|
|
589
|
+
finished clip on a run that is not at its final watch. An ad that is already
|
|
590
|
+
delivered is refused by the server, in its own sentence.
|
|
591
|
+
|
|
592
|
+
**What stays untouched.** Every other scene's clip, every keyframe, the cast
|
|
593
|
+
pins, the voices and the approved script. The redone clip re-renders from the
|
|
594
|
+
same keyframe with the same voice path the first take used, so it comes back
|
|
595
|
+
revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
596
|
+
|
|
597
|
+
**A `--note` steers the motion, never the words.** It is folded into that
|
|
598
|
+
scene's motion prompt only (`withRedoDirection`,
|
|
599
|
+
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
600
|
+
the approved storyboard. Asking for different words in a note will not change
|
|
601
|
+
them. If the words are wrong, that is a storyboard flag or a new run.
|
|
602
|
+
|
|
603
|
+
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
604
|
+
clip the row stays `done` with the ORIGINAL take and only its findings are
|
|
605
|
+
refreshed, because a worse take must never replace a delivered one. So never
|
|
606
|
+
report that the clip changed. Read `status` again for the new findings, then pull
|
|
607
|
+
and compare the file before and after.
|
|
608
|
+
|
|
609
|
+
**Re-cut afterwards.** A cut you already uploaded still holds the old clip, and
|
|
610
|
+
the CLI says so when that is the case. Pull again, re-cut and upload again before
|
|
611
|
+
anyone approves.
|
|
612
|
+
|
|
414
613
|
## Cutting the ad
|
|
415
614
|
|
|
416
615
|
Editing is the part every user does differently, and the team is deliberately
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
2
|
export declare const helpText: string;
|
|
3
|
+
export declare const VOICES_PATH = "/api/v2/video/voices";
|
|
3
4
|
export interface ClipWord {
|
|
4
5
|
w: string;
|
|
5
6
|
s: number;
|
|
@@ -69,6 +70,7 @@ export interface VideoRunNode {
|
|
|
69
70
|
kind: string;
|
|
70
71
|
status: "idle" | "running" | "done" | "failed" | "skipped";
|
|
71
72
|
error?: string;
|
|
73
|
+
warning?: string;
|
|
72
74
|
outputs?: ArtifactSubset[];
|
|
73
75
|
}
|
|
74
76
|
export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
|
|
@@ -95,6 +97,8 @@ export interface VideoRun {
|
|
|
95
97
|
pausedNodeId?: string;
|
|
96
98
|
nodes: VideoRunNode[];
|
|
97
99
|
castLock?: PullCastLock | null;
|
|
100
|
+
workflowId?: string;
|
|
101
|
+
moduleOwned?: boolean;
|
|
98
102
|
}
|
|
99
103
|
export declare function asVideoRun(data: unknown): VideoRun;
|
|
100
104
|
export interface NodeItem {
|
|
@@ -176,7 +180,8 @@ export type RunStop = {
|
|
|
176
180
|
export declare function classifyRun(run: VideoRun): RunStop;
|
|
177
181
|
export declare function stageWord(stage: string): string;
|
|
178
182
|
export declare function stepName(kind: string | undefined): string;
|
|
179
|
-
export declare function
|
|
183
|
+
export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
|
|
184
|
+
export declare function stopLines(stop: RunStop, runId: string, runUrl: string): string[];
|
|
180
185
|
export interface PullDownload {
|
|
181
186
|
file: string;
|
|
182
187
|
url: string;
|
|
@@ -191,6 +196,7 @@ export interface ManifestScene {
|
|
|
191
196
|
clip: string | null;
|
|
192
197
|
words: string | null;
|
|
193
198
|
wordsFrom: "clip" | "voice" | null;
|
|
199
|
+
wordsDescribe: "original-performance" | "this-file" | null;
|
|
194
200
|
voice: string | null;
|
|
195
201
|
keyframe: string | null;
|
|
196
202
|
qc: ClipQc | null;
|
|
@@ -213,6 +219,8 @@ export interface ManifestCastRef {
|
|
|
213
219
|
file: string | null;
|
|
214
220
|
status: string;
|
|
215
221
|
error: string | null;
|
|
222
|
+
voiceId: string | null;
|
|
223
|
+
voiceLabel: string | null;
|
|
216
224
|
}
|
|
217
225
|
export interface VideoManifest {
|
|
218
226
|
runId: string;
|
|
@@ -264,6 +272,71 @@ export declare function storyboardFlow(runId: string, json: boolean, deps: Video
|
|
|
264
272
|
export declare function approveFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
265
273
|
export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
266
274
|
export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
275
|
+
export type ClipRedoPlan = {
|
|
276
|
+
ok: true;
|
|
277
|
+
nodeId: string;
|
|
278
|
+
sceneIndex: number;
|
|
279
|
+
attempt: number;
|
|
280
|
+
warnings: string[];
|
|
281
|
+
} | {
|
|
282
|
+
ok: false;
|
|
283
|
+
reason: string;
|
|
284
|
+
};
|
|
285
|
+
export interface ClipRedoTarget {
|
|
286
|
+
sceneIndex: number;
|
|
287
|
+
nodeId?: string;
|
|
288
|
+
}
|
|
289
|
+
export declare function planClipRedo(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
290
|
+
export declare function retryClipFlow(runId: string, target: ClipRedoTarget, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
291
|
+
export interface CastVoiceRow {
|
|
292
|
+
characterId: string;
|
|
293
|
+
name: string;
|
|
294
|
+
voice: {
|
|
295
|
+
voiceId: string;
|
|
296
|
+
label?: string;
|
|
297
|
+
} | null;
|
|
298
|
+
availability?: {
|
|
299
|
+
state: "available";
|
|
300
|
+
providerName: string;
|
|
301
|
+
} | {
|
|
302
|
+
state: "missing";
|
|
303
|
+
} | {
|
|
304
|
+
state: "not-checked";
|
|
305
|
+
why: string;
|
|
306
|
+
};
|
|
307
|
+
spokenScenes: number;
|
|
308
|
+
spokenSeconds: number;
|
|
309
|
+
}
|
|
310
|
+
export interface CastVoiceSheet {
|
|
311
|
+
runId: string;
|
|
312
|
+
canChange: boolean;
|
|
313
|
+
whyNot?: string;
|
|
314
|
+
cast: CastVoiceRow[];
|
|
315
|
+
narrator: {
|
|
316
|
+
voiceId: string;
|
|
317
|
+
label?: string;
|
|
318
|
+
} | null;
|
|
319
|
+
treatment: {
|
|
320
|
+
summary: string;
|
|
321
|
+
provider: string;
|
|
322
|
+
model: string;
|
|
323
|
+
speedChange: boolean;
|
|
324
|
+
costNote: string;
|
|
325
|
+
usage: {
|
|
326
|
+
clips: number;
|
|
327
|
+
seconds: number;
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
notices: string[];
|
|
331
|
+
changed?: string[];
|
|
332
|
+
}
|
|
333
|
+
export type VoiceMap = Record<string, string | {
|
|
334
|
+
voiceId: string;
|
|
335
|
+
label?: string;
|
|
336
|
+
} | null>;
|
|
337
|
+
export declare function parseVoiceFlags(argv: readonly string[], readFile: (path: string) => string): VoiceMap | null;
|
|
338
|
+
export declare function voicesFlow(runId: string, voices: VoiceMap | null, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
339
|
+
export declare function voiceSheetLines(sheet: CastVoiceSheet): string[];
|
|
267
340
|
export declare function pullFlow(runId: string, dir: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
268
341
|
export declare function parseMvhdDurationSec(bytes: Uint8Array): number | null;
|
|
269
342
|
export declare const NO_DURATION_MESSAGE: string;
|