@aicopycoders/exodus 2026.9.1900-beta.1 → 2026.9.1900-beta.2
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/CLAUDE.md +1 -1
- package/assets/docs/PIPELINES.md +11 -0
- package/assets/skills/exodus-video/SKILL.md +113 -7
- package/assets/skills/exodus-workflow/SKILL.md +33 -1
- package/dist/commands/video.d.ts +8 -0
- package/dist/commands/video.js +27 -6
- package/dist/commands/workflow.d.ts +43 -0
- package/dist/commands/workflow.js +146 -2
- package/package.json +1 -1
package/assets/docs/CLAUDE.md
CHANGED
|
@@ -61,7 +61,7 @@ These are the **Exodus** pipelines — the automatic system that drives the Gene
|
|
|
61
61
|
| **Meme** | `exodus-meme` | Meme ads — recommends formats for a brief, then one batched run renders AI-image and classic Imgflip-template memes server-side. |
|
|
62
62
|
| **Workflow** | `exodus-workflow` | Run and build saved multi-node workflow automations from the CLI, resolve checkpoint/repair parks from the inbox, continue bot sessions, fire triggers, and read banks or promote winners. |
|
|
63
63
|
| **Hooks** | `exodus-hooks` | Read the Scout library — the hook cards captured from Instagram outliers. Compose filters (score, lane, validation status, language, hook type, recency), inspect one card, find cards sharing a hook pattern, export to CSV. Read-only. |
|
|
64
|
-
| **Video** | `exodus-video` | Make a video ad from a script. Main path: run a saved video workflow (`npx @aicopycoders/exodus workflow run`, no Show); starting from a Show (`video start --show`) is the other way in. Then read and approve the storyboard, `npx @aicopycoders/exodus video pull` every piece (per-scene clips, narration and per-scene voice, keyframes, cast stills, word timings, music bed) plus a manifest, cut them in whatever editor you use (a bundled script is one option), upload the cut and approve it. Admin-only. |
|
|
64
|
+
| **Video** | `exodus-video` | Make a video ad from a script. Main path: run a saved video workflow (`npx @aicopycoders/exodus workflow run`, no Show); starting from a Show (`video start --show`) is the other way in. Choose the voices as the run starts: `--voices` gives each speaker an ElevenLabs voice, `--voice-treatment` says how the voices are made (including having the video model speak a voice you wrote). Then read and approve the storyboard, `npx @aicopycoders/exodus video pull` every piece (per-scene clips, narration and per-scene voice, keyframes, cast stills, word timings, music bed) plus a manifest, cut them in whatever editor you use (a bundled script is one option), upload the cut and approve it. Admin-only. |
|
|
65
65
|
| **Browse** | `exodus-browse` | View history and retrieve past outputs |
|
|
66
66
|
| **Drive** | `exodus-drive` | Read/write Google Docs, Sheets, and Drive files via `npx @aicopycoders/exodus drive` (uses the dashboard's Google OAuth — no local CLI) |
|
|
67
67
|
| **Swipe / Mining** | (no skill; CLI only) | The competitor watch list behind the Mining page: `npx @aicopycoders/exodus swipe brands list`, `swipe brands bulk-import <csv>` (checks each brand and starts collecting its ads), `swipe brands add "<Name>" --fb <pageId>` (adds one brand and starts checking it the same way), `swipe run` / `swipe mine`. There is no `competitors` verb. |
|
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -215,6 +215,13 @@ Representative commands:
|
|
|
215
215
|
exodus session list | show | chat
|
|
216
216
|
exodus bank list | show <key> | promote <key>
|
|
217
217
|
Every workflow/session/bank verb takes --json — that output is the machine API.
|
|
218
|
+
|
|
219
|
+
A video workflow whose Rig box points at a rig with saved format rules follows
|
|
220
|
+
them — how many speakers the script may have, which checks run, how the cast is
|
|
221
|
+
built. The start receipt and `workflow status` name the rulebook and its version;
|
|
222
|
+
`video pull` writes the same facts into manifest.json. Every Rig box in the
|
|
223
|
+
workflow has to point at the same saved rig, or the launch refuses and nothing
|
|
224
|
+
is spent.
|
|
218
225
|
```
|
|
219
226
|
|
|
220
227
|
---
|
|
@@ -247,10 +254,14 @@ Returns: a table, --json data, or an exported CSV/JSON file
|
|
|
247
254
|
|
|
248
255
|
**What it does:** Makes a video ad from a script, in pieces. Main path is a saved video workflow with no Show (`exodus workflow run <name> --input <field>=@script.txt`); a locked Show (`exodus video start --show`) is the other way in and is the only route with `video flag`. On the workflow route the run stops for your cut only if its video node sets `finalWatch: true` (defaults to off — check with `exodus workflow export`, not `describe`), and video gates are approved with `exodus video approve`, not `workflow checkpoint approve`. Either way the run writes the storyboard, draws a picture per scene, records the voice, renders a clip per scene and generates a music bed, then parks for a cut. The CLI pulls the pieces to a folder with a `manifest.json`; you cut them in whatever editor you use; the upload attaches the cut for approval. Admin-only.
|
|
249
256
|
|
|
257
|
+
**Voices, on the workflow route.** Two separate choices, both made when the run starts and both refused before the run is created if anything is wrong, so a mistake costs nothing. `--voices @voices.json` says WHICH ElevenLabs voice each speaker gets, keyed by the names the script uses (`HOST 1`) — the same file `exodus video voices <run> --from` reads at the review, so one file per brand serves both moments. `--voice-treatment` says HOW the voices are made: a name on its own for a way of working that plays a voice you picked, or a `voice-treatment.json` shaped `{"path": "...", "describe": {"HOST 1": "Dry, tired baritone."}}` for the ways where the video model speaks the CAST's lines itself in voices you WROTE. A written voice needs one description per speaker, and it cannot be changed at the storyboard review — that is what the flag at launch is for. The two never combine: written voices plus `--voices` is refused, because those ElevenLabs voices would be paid for and never heard. A written voice covers the CAST's lines only: a narrated scene is still recorded by ElevenLabs on the member's own key, so a script with `NARRATOR:` turns still needs the key and still costs ElevenLabs money. Ways of working beyond the two shipped ones are switched on only on the test stack while they are being tried out.
|
|
258
|
+
|
|
250
259
|
```operator-guide
|
|
251
260
|
Start (workflow route, no Show):
|
|
252
261
|
exodus workflow list / describe <name> find it and see what it needs
|
|
253
262
|
exodus workflow run <workflowId|name> --input <field>=@script.txt [--wait] start it; --wait stops at the storyboard gate
|
|
263
|
+
exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voices.json ... and give each speaker an ElevenLabs voice up front
|
|
264
|
+
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json ... or have the video model speak voices you WROTE
|
|
254
265
|
Start (Show route):
|
|
255
266
|
exodus video shows [--json] Shows and whether each is ready
|
|
256
267
|
exodus video start --show <id> --script <file> [--wait] [--json] start an ad run; --wait stops at the storyboard gate
|
|
@@ -77,6 +77,8 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --wait
|
|
|
77
77
|
start, stop at the storyboard gate
|
|
78
78
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voices.json --wait
|
|
79
79
|
same, with the brand's voices already chosen
|
|
80
|
+
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
81
|
+
same, with the video model speaking voices you WROTE
|
|
80
82
|
exodus video status <runId> where it is, per scene
|
|
81
83
|
```
|
|
82
84
|
|
|
@@ -256,6 +258,13 @@ CTA:
|
|
|
256
258
|
THE SOFTGEL: Tap below and give your eyes ClearBlink.
|
|
257
259
|
```
|
|
258
260
|
|
|
261
|
+
**How many speakers you may have depends on the rig.** Three by default. A rig
|
|
262
|
+
that carries saved format rules sets its own number: an ensemble rig seats more,
|
|
263
|
+
a talking-head rig seats one. Over the number, the launch refuses before
|
|
264
|
+
anything is spent and says both figures ("4 speakers — this format supports 3").
|
|
265
|
+
The rulebook the run followed is on the start receipt, on `workflow status`, and
|
|
266
|
+
in `manifest.json` under `provenance`.
|
|
267
|
+
|
|
259
268
|
## Reading the storyboard gate
|
|
260
269
|
|
|
261
270
|
`storyboard <runId>` prints one card per scene on both routes: who speaks, the
|
|
@@ -357,6 +366,88 @@ written by a bot is refused with a pointer at the review-time command, because
|
|
|
357
366
|
nobody knows the speaker names yet. When the storyboard arrives, the voices are
|
|
358
367
|
already set and `exodus video voices <runId>` shows them.
|
|
359
368
|
|
|
369
|
+
**Choosing how the voices are made.** `--voice-treatment` on `workflow run` says
|
|
370
|
+
HOW a run makes its voices, and it is the only door on the workflow route. Two
|
|
371
|
+
kinds of answer.
|
|
372
|
+
|
|
373
|
+
A name on its own picks a way of working that plays a voice you already have,
|
|
374
|
+
and still pairs with `--voices`:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt \
|
|
378
|
+
--voice-treatment lipsync-retarget --voices @voices.json
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The other kind has the video model speak the CAST's lines itself, in voices you
|
|
382
|
+
WRITE. Those take a `voice-treatment.json`, which is the whole request body:
|
|
383
|
+
|
|
384
|
+
```json
|
|
385
|
+
{
|
|
386
|
+
"path": "native-prompt",
|
|
387
|
+
"describe": {
|
|
388
|
+
"BUDGET": "Dry, tired middle-aged baritone. Slight rasp. Deadpan, unhurried.",
|
|
389
|
+
"PIGGY BANK": "Bright quick alto, a little smug, crisp consonants."
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt \
|
|
396
|
+
--voice-treatment @voice-treatment.json
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Every speaker in the script needs a description. One left out gets a voice the
|
|
400
|
+
model invents afresh on every clip, and that drift only shows up once the clips
|
|
401
|
+
are paid for, so the launch refuses instead.
|
|
402
|
+
|
|
403
|
+
**The narration is not part of this.** A describing treatment changes how the
|
|
404
|
+
CAST's lines are voiced. A scene with narration (`voText`) is still recorded by
|
|
405
|
+
ElevenLabs on the member's own key by the Voiceover step, exactly as it always
|
|
406
|
+
was — so a run with `NARRATOR:` turns still needs an ElevenLabs key, and still
|
|
407
|
+
costs ElevenLabs money. `exodus video voices <runId>` says which of the two
|
|
408
|
+
shapes a run is, and only claims nothing is sent to ElevenLabs when nothing is.
|
|
409
|
+
|
|
410
|
+
**What refuses, all before the run is created, so nothing is spent.**
|
|
411
|
+
|
|
412
|
+
- A treatment name that does not exist. The message lists the ones that do.
|
|
413
|
+
- A describing treatment with a speaker undescribed, a speaker the script does
|
|
414
|
+
not have, the same speaker described twice, or an empty description.
|
|
415
|
+
- Written voices plus `--voices`. The video model owns the voice on those runs,
|
|
416
|
+
so the ElevenLabs voices would be paid for and never heard.
|
|
417
|
+
- Descriptions on a treatment that plays voices you picked instead.
|
|
418
|
+
- A workflow whose script is written by a bot. Nobody knows the speaker names
|
|
419
|
+
yet, so there is nothing to describe.
|
|
420
|
+
- A workflow with no Video step, a Video step whose model cannot do what was
|
|
421
|
+
asked, or a Video step with nothing wired into its voiceover port when the
|
|
422
|
+
treatment needs one.
|
|
423
|
+
- No Google AI key on the account, for the treatment that renders through
|
|
424
|
+
Google directly.
|
|
425
|
+
- A rig that shows every line on screen as text. No clip speaks, so there is no
|
|
426
|
+
voice to treat.
|
|
427
|
+
|
|
428
|
+
**Still being tested.** Everything beyond the two shipped ways of working is
|
|
429
|
+
switched on only on the test stack while it is being tried out. On the live
|
|
430
|
+
stack those names refuse by name, and the message says which two to use
|
|
431
|
+
instead. That is a property of the stack, not of the workflow, so the same
|
|
432
|
+
saved workflow behaves differently on each — and a retry asks again rather than
|
|
433
|
+
copying, so a run retried after a treatment was switched off refuses.
|
|
434
|
+
|
|
435
|
+
**A sub-workflow chooses none of this.** When a Call box runs another workflow
|
|
436
|
+
inside this one, the child run carries no voice treatment: it renders the
|
|
437
|
+
shipped default, whatever the parent was started with. A treatment is a choice a
|
|
438
|
+
person made at a keyboard for one run, and nobody made it for the sub-workflow.
|
|
439
|
+
|
|
440
|
+
**Written voices cannot be changed at the review.** They are chosen when the run
|
|
441
|
+
starts. `exodus video voices <runId>` on a described run names the treatment and
|
|
442
|
+
prints each written voice, and says plainly that `--voice-treatment` at launch is
|
|
443
|
+
what changes them. Do not tell anyone to create a Show for this, and do not set a
|
|
444
|
+
voice path in node config — neither is a door.
|
|
445
|
+
|
|
446
|
+
**Where it shows up afterwards.** The launch receipt and `exodus workflow status`
|
|
447
|
+
both print one line naming the treatment and the speakers who got a written
|
|
448
|
+
voice. `manifest.json` from `exodus video pull` carries the same under
|
|
449
|
+
`provenance.voice`, and each cast entry carries its `voiceDescription`.
|
|
450
|
+
|
|
360
451
|
**The JSON receipt** (`--json`), the same shape for reading and for setting:
|
|
361
452
|
|
|
362
453
|
```
|
|
@@ -365,12 +456,15 @@ runId
|
|
|
365
456
|
changed[] the characters whose voice actually moved (set only)
|
|
366
457
|
canChange false once the run has left the storyboard review
|
|
367
458
|
whyNot why, in one sentence, when canChange is false
|
|
459
|
+
treatment.path which way of making voices this run froze, or null
|
|
460
|
+
treatment.kind "performance-then-conversion" or "render-owns-voice"
|
|
368
461
|
cast[] one per character
|
|
369
462
|
characterId, name
|
|
370
463
|
voice { voiceId, label }: the voice this character's clips are
|
|
371
464
|
converted to. null only when they have no voice of their
|
|
372
465
|
own AND the run has no default voice
|
|
373
466
|
voiceFrom "own-pin" or "run-default"; absent when voice is null
|
|
467
|
+
description the WRITTEN voice, when the run was given one
|
|
374
468
|
availability { state: "available" | "missing" | "not-checked", … }
|
|
375
469
|
spokenScenes how many scenes this character speaks in
|
|
376
470
|
spokenSeconds those scenes' planned seconds — what ElevenLabs bills on
|
|
@@ -456,6 +550,15 @@ scenes[] one per scene, in order
|
|
|
456
550
|
on record when the voice pass replaced the
|
|
457
551
|
clip with a new file; null when the clip IS
|
|
458
552
|
that file, or there is no clip
|
|
553
|
+
provenance which saved rulebook this run followed:
|
|
554
|
+
{format: {via, nodeId, rigId, rigName,
|
|
555
|
+
rulesFrom, specVersion}}. Absent when the
|
|
556
|
+
run followed none. `nodeId` is the Rig box
|
|
557
|
+
that chose it (absent on a Show ad, which
|
|
558
|
+
has no box); `rulesFrom` names the rig the
|
|
559
|
+
rules were borrowed from when the picked
|
|
560
|
+
rig is a copy. Names, ids and a version
|
|
561
|
+
only — never the rules themselves
|
|
459
562
|
failed[] files that did not download: {file, url, error}
|
|
460
563
|
```
|
|
461
564
|
|
|
@@ -504,13 +607,16 @@ own ElevenLabs render.
|
|
|
504
607
|
**Which scenes carry their voice where.** A scene whose storyboard entry has a
|
|
505
608
|
non-empty `voText` is a narrator scene: its clip is rendered silent and the
|
|
506
609
|
voice arrives as a separate track. A scene with on-camera `dialogue[]` has the
|
|
507
|
-
speech inside the clip — under the default voice
|
|
508
|
-
voice is then replaced, in place, with the cast member's pinned
|
|
509
|
-
(that is `revoiced: true`). Other
|
|
510
|
-
audio, e.g. lip-sync retargeting a VO track onto the video. The
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
610
|
+
speech inside the clip — under the default voice treatment the video model's
|
|
611
|
+
invented voice is then replaced, in place, with the cast member's pinned
|
|
612
|
+
ElevenLabs voice (that is `revoiced: true`). Other treatments leave the render
|
|
613
|
+
owning its own audio, e.g. lip-sync retargeting a VO track onto the video. The
|
|
614
|
+
treatment is frozen on the run when it starts, and there are exactly two doors
|
|
615
|
+
that set one: `--voice-path` on `video start` for a Show run, and
|
|
616
|
+
`--voice-treatment` on `workflow run` for a workflow run (see **Choosing how the
|
|
617
|
+
voices are made**). Node config is never a door — nothing on the canvas sets a
|
|
618
|
+
voice treatment, and a run that was given none renders the default. Read
|
|
619
|
+
`wordsFrom` and `revoiced` per scene rather than assuming.
|
|
514
620
|
|
|
515
621
|
**Word timings** (`scene-NN.words.json`). Seconds from the start of the file
|
|
516
622
|
`wordsFrom` names, always. `wordsFrom: "clip"` is an ElevenLabs Scribe
|
|
@@ -8,7 +8,7 @@ Core — discover & run:
|
|
|
8
8
|
exodus workflow list [--json] List the brand's saved workflows
|
|
9
9
|
exodus workflow describe <workflowId|name> [--json] Inputs, prerequisites, outputs
|
|
10
10
|
exodus workflow bots [--category <cat>] [--slug <slug>] [--json] Bot catalog / one bot's spec
|
|
11
|
-
exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--auto-approve] [--wait] [--out <dir>] [--json] Run it
|
|
11
|
+
exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--voices <json|@file>] [--voice-treatment <name|json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json] Run it
|
|
12
12
|
exodus workflow status [--id <runId>] [--out <dir>] [--json] Poll a run / read its outputs (no --id → the brand's 25 newest runs + their ids)
|
|
13
13
|
exodus workflow cancel <runId> [--reason "..."] [--json] Stop a run that hasn't finished — queued, running, or parked
|
|
14
14
|
exodus workflow export <workflowId|name> [--version <n>] [--out <file>] [--json] Dump the contract (YAML)
|
|
@@ -51,6 +51,24 @@ Flag notes that add agent-level guidance (everything else is in --help):
|
|
|
51
51
|
earlier upload is relayed as-is. Accepted: image PNG/JPEG/
|
|
52
52
|
WebP/GIF ≤15MB, video MP4/MOV/WebM ≤200MB, audio MP3/M4A/
|
|
53
53
|
WAV/OGG ≤50MB, document PDF/TXT/MD/DOC/DOCX ≤25MB.
|
|
54
|
+
--voices <v> (run, video workflows) Give each speaker in the script an
|
|
55
|
+
ElevenLabs voice from the moment the run starts, instead
|
|
56
|
+
of at the storyboard review. A JSON object keyed by the
|
|
57
|
+
names the SCRIPT uses ("HOST 1"), or @path to a file
|
|
58
|
+
holding one — the same voices.json "exodus video voices
|
|
59
|
+
<run> --from" reads, so a brand keeps one file for both.
|
|
60
|
+
--voice-treatment <v> (run, video workflows) Say HOW the run makes its voices.
|
|
61
|
+
A name on its own picks a way of working that plays a
|
|
62
|
+
voice you already have (and still takes --voices); a JSON
|
|
63
|
+
object, or @path to one, shaped {"path": "...",
|
|
64
|
+
"describe": {"HOST 1": "..."}} has the video model speak
|
|
65
|
+
the lines itself in a voice you WROTE, one description per
|
|
66
|
+
speaker. Written voices and --voices are refused together:
|
|
67
|
+
the ElevenLabs voices would be paid for and never heard.
|
|
68
|
+
Chosen at launch only — the storyboard review can show a
|
|
69
|
+
written voice but cannot change it. Anything the run could
|
|
70
|
+
not do stops the launch before the run exists. The full
|
|
71
|
+
flow is in the exodus-video skill.
|
|
54
72
|
--auto-approve (run) Send this ONE run off unattended: every Checkpoint it
|
|
55
73
|
reaches is approved for you, exactly as it stands, and the
|
|
56
74
|
run records that nobody looked. Without it a Checkpoint
|
|
@@ -145,6 +163,20 @@ Fill every required input. For a free-text brief you can compose a **bespoke bri
|
|
|
145
163
|
|
|
146
164
|
**A brief wired straight into a Storyboard is a script, not a prompt.** On a video workflow whose Storyboard takes its `script` port from a Brief node, the text you pass for that brief boards VERBATIM, the same way a Show ad's pasted script does: one speaker per scene, every line rendered once in order, long lines split across clips, narrated lines rendered silent under the voice track. So it must be in the script form the `exodus-video` skill documents (an optional `CAST:` block of `NAME — description` lines, then `NAME:` turns, then a `CTA:` block; `NARRATOR:` lines only for Narrated Story and Explainer). A brief that is not a script, or one that does not fit the wired rig's family, is refused at launch with the parser's own message and nothing is spent. Write the script to a file and pass it with `--input script=@script.txt`. A Storyboard fed by a bot is unaffected.
|
|
147
165
|
|
|
166
|
+
**A saved rig can bring a rulebook with it.** Some saved rigs carry saved format rules: how many speakers the script may have, which checks run on the clips, how the cast is built. Point a Rig box at one of those rigs and the run follows its rules — an ensemble rig seats more speakers than the usual three, a talking-head rig seats one. The start receipt says which rulebook ran:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Format rules: Ensemble Talking Head, version ensemble-e1-provisional-2026-07 (from the "rig_1" Rig box).
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`workflow status` says the same line later, and `video pull` writes the same facts into `manifest.json` under `provenance`. Nothing prints the rules themselves — they are the house's, not the brand's — only whose they were and which version ran.
|
|
173
|
+
|
|
174
|
+
Three things to know before you wire one:
|
|
175
|
+
|
|
176
|
+
- **One run, one rulebook.** If any Rig box's rig carries rules, every Rig box in the workflow has to point at that same saved rig. A second box on a different rig, or a box with its knobs set in place, refuses the launch with a sentence naming every box. Nothing is spent.
|
|
177
|
+
- **Some rulebooks aren't released yet.** A rig that is still being built refuses a member's launch and names itself. Only an admin can start a run on one. Duplicating that rig doesn't get around it — the copy still reads the original's rules, so the answer is the same.
|
|
178
|
+
- **The rig's video model is advice, not a change.** When the rig is normally filmed with a different video model than the one your Video step is set to, the receipt says so in one line and the run uses YOUR setting. Change the Video step yourself if you want the rig's.
|
|
179
|
+
|
|
148
180
|
For an **`asset` (file) input, pass the path to a local file** and the CLI does the uploading for you — mint, push the bytes, register, then start the run with the stored asset:
|
|
149
181
|
|
|
150
182
|
```bash
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
2
|
import type { FlagOccurrence } from "../lib/args.js";
|
|
3
|
+
import { type RunProvenance } from "./workflow.js";
|
|
3
4
|
export declare const helpText: string;
|
|
4
5
|
export declare const VOICES_PATH = "/api/v2/video/voices";
|
|
5
6
|
export interface ClipWord {
|
|
@@ -100,6 +101,7 @@ export interface VideoRun {
|
|
|
100
101
|
pausedNodeId?: string;
|
|
101
102
|
nodes: VideoRunNode[];
|
|
102
103
|
castLock?: PullCastLock | null;
|
|
104
|
+
provenance?: RunProvenance;
|
|
103
105
|
workflowId?: string;
|
|
104
106
|
moduleOwned?: boolean;
|
|
105
107
|
}
|
|
@@ -225,6 +227,7 @@ export interface ManifestCastRef {
|
|
|
225
227
|
error: string | null;
|
|
226
228
|
voiceId: string | null;
|
|
227
229
|
voiceLabel: string | null;
|
|
230
|
+
voiceDescription: string | null;
|
|
228
231
|
}
|
|
229
232
|
export interface VideoManifest {
|
|
230
233
|
runId: string;
|
|
@@ -240,6 +243,7 @@ export interface VideoManifest {
|
|
|
240
243
|
} | null;
|
|
241
244
|
scenes: ManifestScene[];
|
|
242
245
|
failed: PullFailure[];
|
|
246
|
+
provenance?: RunProvenance;
|
|
243
247
|
}
|
|
244
248
|
export declare const CAST_LEDGER_BASE = 910000;
|
|
245
249
|
export interface PullPlan {
|
|
@@ -314,6 +318,7 @@ export interface CastVoiceRow {
|
|
|
314
318
|
state: "not-checked";
|
|
315
319
|
why: string;
|
|
316
320
|
};
|
|
321
|
+
description?: string;
|
|
317
322
|
spokenScenes: number;
|
|
318
323
|
spokenSeconds: number;
|
|
319
324
|
}
|
|
@@ -327,6 +332,9 @@ export interface CastVoiceSheet {
|
|
|
327
332
|
label?: string;
|
|
328
333
|
} | null;
|
|
329
334
|
treatment: {
|
|
335
|
+
kind?: string;
|
|
336
|
+
path?: string | null;
|
|
337
|
+
usesElevenLabs?: boolean;
|
|
330
338
|
summary: string;
|
|
331
339
|
provider: string;
|
|
332
340
|
model: string;
|
package/dist/commands/video.js
CHANGED
|
@@ -407,10 +407,15 @@ function castVoicePins(artifact) {
|
|
|
407
407
|
for (const member of cast) {
|
|
408
408
|
const characterId = typeof member.characterId === "string" ? member.characterId.trim() : "";
|
|
409
409
|
const voiceId = typeof member.voiceId === "string" ? member.voiceId.trim() : "";
|
|
410
|
-
|
|
410
|
+
const description = typeof member.voiceDescription === "string" ? member.voiceDescription.trim() : "";
|
|
411
|
+
if (!characterId || (!voiceId && !description))
|
|
411
412
|
continue;
|
|
412
413
|
const label = typeof member.voiceLabel === "string" ? member.voiceLabel.trim() : "";
|
|
413
|
-
pins.set(characterId, {
|
|
414
|
+
pins.set(characterId, {
|
|
415
|
+
voiceId: voiceId ? voiceId : null,
|
|
416
|
+
voiceLabel: voiceId && label ? label : null,
|
|
417
|
+
voiceDescription: description ? description : null,
|
|
418
|
+
});
|
|
414
419
|
}
|
|
415
420
|
return pins;
|
|
416
421
|
}
|
|
@@ -487,6 +492,7 @@ function planCastRefs(run, items, pins) {
|
|
|
487
492
|
error: row.error,
|
|
488
493
|
voiceId: pin?.voiceId ?? null,
|
|
489
494
|
voiceLabel: pin?.voiceLabel ?? null,
|
|
495
|
+
voiceDescription: pin?.voiceDescription ?? null,
|
|
490
496
|
});
|
|
491
497
|
if (file && row.url)
|
|
492
498
|
downloads.push({ file, url: row.url });
|
|
@@ -701,6 +707,9 @@ export function planPull(run, items, opts) {
|
|
|
701
707
|
: null,
|
|
702
708
|
scenes,
|
|
703
709
|
failed: [],
|
|
710
|
+
...(run.provenance?.format || run.provenance?.voice
|
|
711
|
+
? { provenance: run.provenance }
|
|
712
|
+
: {}),
|
|
704
713
|
},
|
|
705
714
|
};
|
|
706
715
|
}
|
|
@@ -1500,7 +1509,7 @@ export async function voicesFlow(runId, voices, json, deps) {
|
|
|
1500
1509
|
}
|
|
1501
1510
|
function availabilityWords(row) {
|
|
1502
1511
|
if (!row.voice)
|
|
1503
|
-
return "no voice yet";
|
|
1512
|
+
return row.description ? "voice written below" : "no voice yet";
|
|
1504
1513
|
switch (row.availability?.state) {
|
|
1505
1514
|
case "available":
|
|
1506
1515
|
return "ElevenLabs has it";
|
|
@@ -1534,12 +1543,17 @@ function voiceSummaryLines(cast, notices) {
|
|
|
1534
1543
|
}
|
|
1535
1544
|
export function voiceSheetLines(sheet) {
|
|
1536
1545
|
const lines = [`Voices for run ${sheet.runId}`];
|
|
1546
|
+
if (typeof sheet.treatment.path === "string" && sheet.treatment.path !== "") {
|
|
1547
|
+
lines.push(` Treatment: ${sheet.treatment.path}`);
|
|
1548
|
+
}
|
|
1537
1549
|
if (sheet.cast.length === 0)
|
|
1538
1550
|
lines.push(" Nobody is in this ad yet.");
|
|
1539
1551
|
for (const row of sheet.cast) {
|
|
1540
1552
|
const voice = voiceWords(row);
|
|
1541
1553
|
lines.push(` ${row.characterId} ${row.name} ${voice} ${availabilityWords(row)} ` +
|
|
1542
1554
|
`speaks in ${row.spokenScenes} scenes, about ${row.spokenSeconds}s`);
|
|
1555
|
+
if (row.description)
|
|
1556
|
+
lines.push(` written voice: ${row.description}`);
|
|
1543
1557
|
}
|
|
1544
1558
|
if (sheet.narrator) {
|
|
1545
1559
|
const label = sheet.narrator.label ? `${sheet.narrator.label} ` : "";
|
|
@@ -1551,9 +1565,16 @@ export function voiceSheetLines(sheet) {
|
|
|
1551
1565
|
: "Nothing changed — those voices were already set. Pictures and script were not touched.");
|
|
1552
1566
|
}
|
|
1553
1567
|
lines.push(`How voices are applied: ${sheet.treatment.summary}${sheet.treatment.speedChange ? "" : " No speed change."}`);
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1568
|
+
const rendersOwnVoice = sheet.treatment.kind === "render-owns-voice";
|
|
1569
|
+
if (!rendersOwnVoice || sheet.treatment.usesElevenLabs === undefined) {
|
|
1570
|
+
lines.push(`Cost: billed to your own ElevenLabs key. ${sheet.treatment.costNote} ` +
|
|
1571
|
+
`About ${sheet.treatment.usage.seconds} seconds of speech across ` +
|
|
1572
|
+
`${sheet.treatment.usage.clips} clips will be converted.`);
|
|
1573
|
+
}
|
|
1574
|
+
else if (sheet.treatment.usesElevenLabs) {
|
|
1575
|
+
lines.push("Cost: the narration on this run is voiced by ElevenLabs and billed to your own key. " +
|
|
1576
|
+
`${sheet.treatment.costNote} The clips themselves are not converted.`);
|
|
1577
|
+
}
|
|
1557
1578
|
for (const notice of sheet.notices)
|
|
1558
1579
|
lines.push(notice);
|
|
1559
1580
|
lines.push(sheet.canChange
|
|
@@ -290,6 +290,11 @@ export interface WorkflowRunOutput {
|
|
|
290
290
|
durationSec?: number;
|
|
291
291
|
sceneIndex?: number;
|
|
292
292
|
final?: boolean;
|
|
293
|
+
words?: Array<{
|
|
294
|
+
w: string;
|
|
295
|
+
s: number;
|
|
296
|
+
e: number;
|
|
297
|
+
}>;
|
|
293
298
|
frames?: Array<{
|
|
294
299
|
sceneIndex: number;
|
|
295
300
|
imageUrl?: string;
|
|
@@ -347,6 +352,36 @@ export interface WorkflowPendingSlot {
|
|
|
347
352
|
value?: string;
|
|
348
353
|
hint?: string;
|
|
349
354
|
}
|
|
355
|
+
export interface RunFormatProvenance {
|
|
356
|
+
via: "show" | "rig-node";
|
|
357
|
+
nodeId?: string;
|
|
358
|
+
rigId: string;
|
|
359
|
+
rigName: string;
|
|
360
|
+
rulesFrom?: {
|
|
361
|
+
rigId: string;
|
|
362
|
+
rigName: string;
|
|
363
|
+
};
|
|
364
|
+
specVersion: string | null;
|
|
365
|
+
}
|
|
366
|
+
export type VoicePath = "sts-revoice" | "lipsync-retarget" | "native-prompt" | "omni-audio-ids" | "gemini-direct";
|
|
367
|
+
export type RunVoiceProvenance = {
|
|
368
|
+
source: "show";
|
|
369
|
+
path: VoicePath;
|
|
370
|
+
} | {
|
|
371
|
+
source: "launch";
|
|
372
|
+
path: VoicePath;
|
|
373
|
+
descriptions: {
|
|
374
|
+
speaker: string;
|
|
375
|
+
description: string;
|
|
376
|
+
}[];
|
|
377
|
+
} | {
|
|
378
|
+
source: "unreadable";
|
|
379
|
+
};
|
|
380
|
+
export interface RunProvenance {
|
|
381
|
+
format: RunFormatProvenance | null;
|
|
382
|
+
voice: RunVoiceProvenance | null;
|
|
383
|
+
}
|
|
384
|
+
export declare function formatRulesLine(provenance: unknown): string | undefined;
|
|
350
385
|
export interface WorkflowRun {
|
|
351
386
|
_id: string;
|
|
352
387
|
workflowId: string;
|
|
@@ -372,6 +407,7 @@ export interface WorkflowRun {
|
|
|
372
407
|
nodeId: string;
|
|
373
408
|
approvedAt: number;
|
|
374
409
|
}[];
|
|
410
|
+
provenance?: RunProvenance;
|
|
375
411
|
}
|
|
376
412
|
export type WorkflowRunProjection = Omit<WorkflowRun, "nodes"> & {
|
|
377
413
|
nodes?: never;
|
|
@@ -410,6 +446,7 @@ interface RunFlowOptions {
|
|
|
410
446
|
autoApprove?: boolean;
|
|
411
447
|
imageRigOverrides?: Record<string, unknown>;
|
|
412
448
|
voices?: VoiceMap;
|
|
449
|
+
voiceTreatment?: VoiceTreatmentBody;
|
|
413
450
|
wait: boolean;
|
|
414
451
|
json: boolean;
|
|
415
452
|
out?: string;
|
|
@@ -431,6 +468,11 @@ export declare function rejectTerminalFlag(occurrences: FlagOccurrence[]): void;
|
|
|
431
468
|
export declare function parseAutoApproveFlag(args: string[]): boolean;
|
|
432
469
|
export declare function parseRigOverridesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
|
|
433
470
|
export declare function parseVoicesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceMap | undefined;
|
|
471
|
+
export interface VoiceTreatmentBody {
|
|
472
|
+
path: string;
|
|
473
|
+
describe?: Record<string, string>;
|
|
474
|
+
}
|
|
475
|
+
export declare function parseVoiceTreatmentFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceTreatmentBody | undefined;
|
|
434
476
|
export declare function parseFillFlag(occurrences: FlagOccurrence[]): string | undefined;
|
|
435
477
|
export declare function formatWorkflowList(workflows: WorkflowListItem[]): string;
|
|
436
478
|
export declare function formatRecentRuns(runs: WorkflowRunProjection[]): string;
|
|
@@ -464,6 +506,7 @@ export declare function botsFlow(opts: {
|
|
|
464
506
|
slug?: string;
|
|
465
507
|
json: boolean;
|
|
466
508
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
509
|
+
export declare function voiceTreatmentLine(provenance: unknown): string | undefined;
|
|
467
510
|
export declare function runFlow(workflowRef: string, opts: RunFlowOptions, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
468
511
|
export declare function statusFlow(opts: {
|
|
469
512
|
id?: string;
|
|
@@ -20,7 +20,7 @@ Usage:
|
|
|
20
20
|
exodus workflow templates [list] [--json]
|
|
21
21
|
exodus workflow templates export <key> [--out <file>] [--json]
|
|
22
22
|
exodus workflow schema [--kind <kind>] [--face <face>] [--json]
|
|
23
|
-
exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--auto-approve] [--wait] [--out <dir>] [--json]
|
|
23
|
+
exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--voice-treatment <name|json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json]
|
|
24
24
|
exodus workflow status [--json]
|
|
25
25
|
exodus workflow status --id <runId> [--out <dir>] [--json]
|
|
26
26
|
exodus workflow cancel <runId> [--reason "..."] [--json]
|
|
@@ -102,6 +102,29 @@ Flags:
|
|
|
102
102
|
<run>" at the storyboard review instead. Voice ids
|
|
103
103
|
belong to the brand's own folder — never put them in a
|
|
104
104
|
shared workflow or template.
|
|
105
|
+
--voice-treatment <v> (run) Say HOW this run makes its voices. Two kinds of
|
|
106
|
+
answer. A name on its own picks a way of working that
|
|
107
|
+
uses voices you already have:
|
|
108
|
+
--voice-treatment sts-revoice
|
|
109
|
+
--voice-treatment lipsync-retarget --voices @voices.json
|
|
110
|
+
The other kind has the video model speak the lines
|
|
111
|
+
itself, in a voice you WRITE instead of one you pick.
|
|
112
|
+
Those need a description for every speaker in your
|
|
113
|
+
script, so they take a JSON object, or @path to a .json
|
|
114
|
+
file holding one:
|
|
115
|
+
--voice-treatment @voice-treatment.json
|
|
116
|
+
--voice-treatment '{"path":"native-prompt","describe":{"HOST 1":"Dry, tired baritone."}}'
|
|
117
|
+
Written voices and --voices are never used together:
|
|
118
|
+
the video model owns the voice on those runs, so
|
|
119
|
+
ElevenLabs voices would be paid for and never heard,
|
|
120
|
+
and asking for both stops the launch. Anything the run
|
|
121
|
+
could not actually do — a treatment this stack has not
|
|
122
|
+
switched on, a Video step on a model that cannot do it,
|
|
123
|
+
a missing key, a rig that shows every line as on-screen
|
|
124
|
+
text — stops the launch before the run is created, and
|
|
125
|
+
the message says which. Descriptions are chosen HERE,
|
|
126
|
+
at the start: "exodus video voices <run>" shows them at
|
|
127
|
+
the review but cannot change them.
|
|
105
128
|
--auto-approve (run) Deliberately unattended launch: every Checkpoint
|
|
106
129
|
box this run stops at is approved automatically, with
|
|
107
130
|
whatever it was holding left exactly as it is, and the
|
|
@@ -194,6 +217,7 @@ Examples:
|
|
|
194
217
|
exodus workflow run "Product Shots" --rig-overrides '{"rig_1":{"lines":{"line_1":{"count":3}}}}'
|
|
195
218
|
exodus workflow run "Product Shots" --rig-overrides @rig.json --wait
|
|
196
219
|
exodus workflow run "Podcast Ad" --input script=@script.txt --voices @voices.json --wait
|
|
220
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
197
221
|
exodus workflow run "Launch Flow" --wait --out ./deliverables
|
|
198
222
|
exodus workflow status # recent runs + their ids
|
|
199
223
|
exodus workflow status --id wr_123
|
|
@@ -268,6 +292,20 @@ Notes:
|
|
|
268
292
|
no speaker names to key on at the start, so it is refused with a pointer at
|
|
269
293
|
the review-time command. Keep voice ids in the brand's own folder, never in a
|
|
270
294
|
shared workflow or template. (#1846)
|
|
295
|
+
"workflow run --voice-treatment" says HOW a run makes its voices. Some ways of
|
|
296
|
+
working play a voice you picked (that is today's behaviour, and it still takes
|
|
297
|
+
--voices); others have the video model speak the CAST's lines itself, in voices
|
|
298
|
+
you describe in words. A described voice needs one description per speaker,
|
|
299
|
+
because a speaker nobody described gets a voice the model invents afresh on
|
|
300
|
+
every clip, and the drift only shows up once the clips are paid for. Written
|
|
301
|
+
voices and --voices are refused together, since the ElevenLabs voices would
|
|
302
|
+
never be heard. Everything the run could not do — a way of working this stack
|
|
303
|
+
has not switched on, a Video step whose model cannot do it, a missing key, a
|
|
304
|
+
rig that shows every line as on-screen text — stops the launch before the run
|
|
305
|
+
is created. The ways of working beyond the two shipped ones are switched on
|
|
306
|
+
only on the test stack while they are being tried out, so a live run refuses
|
|
307
|
+
them by name. Both the launch receipt and "workflow status" say which way a
|
|
308
|
+
run took, and "exodus video voices <run>" shows each written voice. (#1869)
|
|
271
309
|
workflow triggers are addressed by 1-based position — a trigger carries no id,
|
|
272
310
|
so the CLI reads the live list from the export contract and sends a fingerprint
|
|
273
311
|
of the trigger's fields as the guard; a concurrent edit fails loud rather than
|
|
@@ -310,6 +348,24 @@ Notes:
|
|
|
310
348
|
ids it wants). The "gate" verbs retired in 2.0 along with the Gate node — the
|
|
311
349
|
command now prints a pointer at "checkpoint" and exits 1.
|
|
312
350
|
`.trim();
|
|
351
|
+
export function formatRulesLine(provenance) {
|
|
352
|
+
const format = provenance?.format;
|
|
353
|
+
if (!format || typeof format !== "object")
|
|
354
|
+
return undefined;
|
|
355
|
+
const { rigName, specVersion, nodeId, rulesFrom } = format;
|
|
356
|
+
if (typeof rigName !== "string" || rigName === "")
|
|
357
|
+
return undefined;
|
|
358
|
+
const version = typeof specVersion === "string" && specVersion !== ""
|
|
359
|
+
? `, version ${specVersion}`
|
|
360
|
+
: "";
|
|
361
|
+
const where = typeof nodeId === "string" && nodeId !== ""
|
|
362
|
+
? ` (from the "${nodeId}" Rig box)`
|
|
363
|
+
: " (from the Show's rig)";
|
|
364
|
+
const borrowed = rulesFrom && typeof rulesFrom.rigName === "string"
|
|
365
|
+
? ` The rules come from "${rulesFrom.rigName}".`
|
|
366
|
+
: "";
|
|
367
|
+
return `Format rules: ${rigName}${version}${where}.${borrowed}`;
|
|
368
|
+
}
|
|
313
369
|
const LIST_PATH = "/api/v2/workflows";
|
|
314
370
|
const RUN_PATH = "/api/v2/workflows/run";
|
|
315
371
|
const STATUS_PATH = "/api/v2/workflow";
|
|
@@ -354,6 +410,7 @@ const VALUE_FLAGS = new Set([
|
|
|
354
410
|
"reject",
|
|
355
411
|
"rig-overrides",
|
|
356
412
|
"voices",
|
|
413
|
+
"voice-treatment",
|
|
357
414
|
"note",
|
|
358
415
|
]);
|
|
359
416
|
function defaultMkdirp(dir) {
|
|
@@ -976,6 +1033,57 @@ export function parseVoicesFlag(occurrences, readFile) {
|
|
|
976
1033
|
}
|
|
977
1034
|
return parsed;
|
|
978
1035
|
}
|
|
1036
|
+
export function parseVoiceTreatmentFlag(occurrences, readFile) {
|
|
1037
|
+
const usage = "--voice-treatment requires a treatment name, JSON, or @path/to/voice-treatment.json";
|
|
1038
|
+
let raw;
|
|
1039
|
+
for (const { flag, value } of occurrences) {
|
|
1040
|
+
if (flag !== "voice-treatment")
|
|
1041
|
+
continue;
|
|
1042
|
+
if (value === undefined || value.startsWith("--"))
|
|
1043
|
+
throw new Error(usage);
|
|
1044
|
+
raw = value;
|
|
1045
|
+
}
|
|
1046
|
+
if (raw === undefined)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
let text = raw.trim();
|
|
1049
|
+
if (!text)
|
|
1050
|
+
throw new Error(usage);
|
|
1051
|
+
let fromFile = false;
|
|
1052
|
+
if (text.startsWith("@@")) {
|
|
1053
|
+
text = text.slice(1);
|
|
1054
|
+
}
|
|
1055
|
+
else if (text.startsWith("@")) {
|
|
1056
|
+
const filePath = text.slice(1);
|
|
1057
|
+
if (!filePath) {
|
|
1058
|
+
throw new Error("--voice-treatment @<file> needs a file path after \"@\"");
|
|
1059
|
+
}
|
|
1060
|
+
if (!readFile) {
|
|
1061
|
+
throw new Error(`--voice-treatment: cannot load @${filePath} here (no file access)`);
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
text = readFile(filePath);
|
|
1065
|
+
}
|
|
1066
|
+
catch (e) {
|
|
1067
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1068
|
+
throw new Error(`--voice-treatment: could not read file "${filePath}": ${msg}`);
|
|
1069
|
+
}
|
|
1070
|
+
fromFile = true;
|
|
1071
|
+
}
|
|
1072
|
+
if (!fromFile && !text.startsWith("{"))
|
|
1073
|
+
return { path: text };
|
|
1074
|
+
let parsed;
|
|
1075
|
+
try {
|
|
1076
|
+
parsed = JSON.parse(text);
|
|
1077
|
+
}
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1080
|
+
throw new Error(`--voice-treatment is not valid JSON: ${msg}`);
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1083
|
+
throw new Error('--voice-treatment must be a treatment name, or a JSON object like {"path":"native-prompt","describe":{"HOST 1":"..."}}');
|
|
1084
|
+
}
|
|
1085
|
+
return parsed;
|
|
1086
|
+
}
|
|
979
1087
|
export function parseFillFlag(occurrences) {
|
|
980
1088
|
let name;
|
|
981
1089
|
for (const { flag, value } of occurrences) {
|
|
@@ -1095,6 +1203,12 @@ export function formatWorkflowRun(run) {
|
|
|
1095
1203
|
lines.push(`status: ${runVerdict(run)}${counts ? ` (${counts})` : ""}`);
|
|
1096
1204
|
if (run.isTerminal)
|
|
1097
1205
|
lines.push("terminal: yes");
|
|
1206
|
+
const rulesLine = formatRulesLine(run.provenance);
|
|
1207
|
+
if (rulesLine)
|
|
1208
|
+
lines.push(rulesLine);
|
|
1209
|
+
const treatmentLine = voiceTreatmentLine(run.provenance);
|
|
1210
|
+
if (treatmentLine)
|
|
1211
|
+
lines.push(treatmentLine);
|
|
1098
1212
|
if (run.error)
|
|
1099
1213
|
lines.push(`error: ${run.error}`);
|
|
1100
1214
|
if (Object.keys(run.inputs ?? {}).length > 0) {
|
|
@@ -1786,6 +1900,23 @@ function launchVoicesLine(attached) {
|
|
|
1786
1900
|
}
|
|
1787
1901
|
return said.length > 0 ? `Voices: ${said.join(", ")}` : undefined;
|
|
1788
1902
|
}
|
|
1903
|
+
export function voiceTreatmentLine(provenance) {
|
|
1904
|
+
const voice = provenance?.voice;
|
|
1905
|
+
if (!voice || typeof voice !== "object")
|
|
1906
|
+
return undefined;
|
|
1907
|
+
if (voice.source === "unreadable") {
|
|
1908
|
+
return ("Voice treatment: the setting saved when this run started cannot be read, so no " +
|
|
1909
|
+
"clip will be made. Start the run again.");
|
|
1910
|
+
}
|
|
1911
|
+
const path = voice.path;
|
|
1912
|
+
if (typeof path !== "string" || path === "")
|
|
1913
|
+
return undefined;
|
|
1914
|
+
const where = voice.source === "show" ? "from its Show" : "chosen at launch";
|
|
1915
|
+
const described = voice.source === "launch" && Array.isArray(voice.descriptions) && voice.descriptions.length > 0
|
|
1916
|
+
? `. Written voices: ${voice.descriptions.map((d) => d.speaker).join(", ")}`
|
|
1917
|
+
: "";
|
|
1918
|
+
return `Voice treatment: ${path} (${where})${described}`;
|
|
1919
|
+
}
|
|
1789
1920
|
export async function runFlow(workflowRef, opts, deps) {
|
|
1790
1921
|
let workflowId;
|
|
1791
1922
|
try {
|
|
@@ -1829,6 +1960,7 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1829
1960
|
...(opts.autoApprove === true ? { autoApprove: true } : {}),
|
|
1830
1961
|
...(opts.imageRigOverrides ? { imageRigOverrides: opts.imageRigOverrides } : {}),
|
|
1831
1962
|
...(opts.voices ? { voices: opts.voices } : {}),
|
|
1963
|
+
...(opts.voiceTreatment ? { voiceTreatment: opts.voiceTreatment } : {}),
|
|
1832
1964
|
launchedVia: "cli",
|
|
1833
1965
|
};
|
|
1834
1966
|
const start = await deps.post(RUN_PATH, body);
|
|
@@ -1843,6 +1975,12 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1843
1975
|
if (opts.json && !opts.wait)
|
|
1844
1976
|
return { code: 0, lines: [JSON.stringify(base)] };
|
|
1845
1977
|
const voicesLine = launchVoicesLine(data?.launchVoices);
|
|
1978
|
+
const provenance = { format: data?.format ?? null, voice: data?.voice ?? null };
|
|
1979
|
+
const rulesLine = formatRulesLine(provenance);
|
|
1980
|
+
const treatmentLine = voiceTreatmentLine(provenance);
|
|
1981
|
+
const advisories = Array.isArray(data?.advisories)
|
|
1982
|
+
? data.advisories.filter((line) => typeof line === "string")
|
|
1983
|
+
: [];
|
|
1846
1984
|
const lines = opts.json
|
|
1847
1985
|
? []
|
|
1848
1986
|
: [
|
|
@@ -1850,7 +1988,10 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1850
1988
|
"Workflow run started.",
|
|
1851
1989
|
`runId: ${data.runId}`,
|
|
1852
1990
|
`triggerRunId: ${data.triggerRunId}`,
|
|
1991
|
+
...(rulesLine ? [rulesLine] : []),
|
|
1992
|
+
...advisories,
|
|
1853
1993
|
...(voicesLine ? [voicesLine] : []),
|
|
1994
|
+
...(treatmentLine ? [treatmentLine] : []),
|
|
1854
1995
|
`Poll: exodus workflow status --id ${data.runId}`,
|
|
1855
1996
|
];
|
|
1856
1997
|
if (!opts.wait) {
|
|
@@ -2957,7 +3098,7 @@ export async function run(flags, occurrences) {
|
|
|
2957
3098
|
const workflowRef = rest[0];
|
|
2958
3099
|
if (!workflowRef) {
|
|
2959
3100
|
console.error("Error: workflow run requires <workflowId|name>.");
|
|
2960
|
-
console.log("Usage: exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=./path/to/file ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--auto-approve] [--wait] [--json]");
|
|
3101
|
+
console.log("Usage: exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=./path/to/file ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--voice-treatment <name|json|@file>] [--auto-approve] [--wait] [--json]");
|
|
2961
3102
|
process.exit(1);
|
|
2962
3103
|
}
|
|
2963
3104
|
let inputs;
|
|
@@ -2965,6 +3106,7 @@ export async function run(flags, occurrences) {
|
|
|
2965
3106
|
let autoApprove;
|
|
2966
3107
|
let imageRigOverrides;
|
|
2967
3108
|
let voices;
|
|
3109
|
+
let voiceTreatment;
|
|
2968
3110
|
try {
|
|
2969
3111
|
inputs = parseRawInputFlags(occurrences);
|
|
2970
3112
|
rejectTerminalFlag(occurrences);
|
|
@@ -2972,6 +3114,7 @@ export async function run(flags, occurrences) {
|
|
|
2972
3114
|
autoApprove = parseAutoApproveFlag(process.argv.slice(3));
|
|
2973
3115
|
imageRigOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
|
|
2974
3116
|
voices = parseVoicesFlag(occurrences, defaultDeps.readFile);
|
|
3117
|
+
voiceTreatment = parseVoiceTreatmentFlag(occurrences, defaultDeps.readFile);
|
|
2975
3118
|
}
|
|
2976
3119
|
catch (e) {
|
|
2977
3120
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -2983,6 +3126,7 @@ export async function run(flags, occurrences) {
|
|
|
2983
3126
|
autoApprove,
|
|
2984
3127
|
imageRigOverrides,
|
|
2985
3128
|
voices,
|
|
3129
|
+
voiceTreatment,
|
|
2986
3130
|
wait: flags["wait"] === true,
|
|
2987
3131
|
json,
|
|
2988
3132
|
out: flagString(flags, "out"),
|