@aicopycoders/exodus 2026.9.1900-beta.1 → 2026.9.2000-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/CLAUDE.md +1 -1
- package/assets/docs/PIPELINES.md +11 -0
- package/assets/skills/exodus-video/SKILL.md +129 -13
- package/assets/skills/exodus-workflow/SKILL.md +34 -2
- package/dist/commands/video.d.ts +11 -0
- package/dist/commands/video.js +55 -13
- package/dist/commands/workflow.d.ts +51 -0
- package/dist/commands/workflow.js +164 -4
- package/dist/lib/runNames.d.ts +10 -0
- package/dist/lib/runNames.js +10 -0
- 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
|
|
|
@@ -145,9 +147,15 @@ minutes with "Still running after 60 minutes" and exit 0; that is not done, so
|
|
|
145
147
|
check `status` before treating it as finished. Do not poll `status` in a tight
|
|
146
148
|
loop; a full run is several minutes.
|
|
147
149
|
|
|
148
|
-
**What costs money.** Starting the run
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
**What costs money.** Starting the run writes the storyboard and then draws the
|
|
151
|
+
reference, cast and scene pictures, and only then pauses. Each picture is a few
|
|
152
|
+
cents and each scene draws a few to choose from: about 25 cents on a small test,
|
|
153
|
+
and it can approach a dollar on a longer ad. Approving releases the expensive
|
|
154
|
+
part: the voices and the clips. So a run that has not paused yet is already
|
|
155
|
+
spending a little, and a run still drawing pictures is healthy. `exodus workflow
|
|
156
|
+
status` and `exodus video status` both print "This run will pause for your
|
|
157
|
+
approval once the frames are ready" on a run that is headed for the gate. Do not
|
|
158
|
+
cancel it because the pause has not arrived. `shows`, `workflow list`, `workflow describe`, `video status`,
|
|
151
159
|
`storyboard` and `pull` are free reads; run them as often as you like. Never
|
|
152
160
|
start a second run to "retry" without the user asking; flag the storyboard (Show
|
|
153
161
|
runs) or tell them what failed instead.
|
|
@@ -155,7 +163,7 @@ runs) or tell them what failed instead.
|
|
|
155
163
|
**`--auto-approve` does not cover either video gate.** It releases Checkpoint
|
|
156
164
|
boxes only (`convex/workflows.ts:7266` keys on `pauseReason === "checkpoint"`),
|
|
157
165
|
and the storyboard gate and the final watch carry no pause reason. An unattended
|
|
158
|
-
`workflow run --auto-approve` still stops dead at the
|
|
166
|
+
`workflow run --auto-approve` still stops dead at the storyboard gate — which is the
|
|
159
167
|
safe behaviour, since nothing should approve paid rendering or ship an uncut ad
|
|
160
168
|
on its own.
|
|
161
169
|
|
|
@@ -256,6 +264,13 @@ CTA:
|
|
|
256
264
|
THE SOFTGEL: Tap below and give your eyes ClearBlink.
|
|
257
265
|
```
|
|
258
266
|
|
|
267
|
+
**How many speakers you may have depends on the rig.** Three by default. A rig
|
|
268
|
+
that carries saved format rules sets its own number: an ensemble rig seats more,
|
|
269
|
+
a talking-head rig seats one. Over the number, the launch refuses before
|
|
270
|
+
anything is spent and says both figures ("4 speakers — this format supports 3").
|
|
271
|
+
The rulebook the run followed is on the start receipt, on `workflow status`, and
|
|
272
|
+
in `manifest.json` under `provenance`.
|
|
273
|
+
|
|
259
274
|
## Reading the storyboard gate
|
|
260
275
|
|
|
261
276
|
`storyboard <runId>` prints one card per scene on both routes: who speaks, the
|
|
@@ -267,8 +282,9 @@ for the user, then ask one question.
|
|
|
267
282
|
On a **workflow run** the question is approve or don't — there is no flag. If
|
|
268
283
|
the storyboard is wrong, say so plainly and let the user decide between
|
|
269
284
|
approving anyway, cancelling (`exodus workflow cancel <runId>`) and re-running
|
|
270
|
-
with a better script, or changing the workflow. Approving is
|
|
271
|
-
|
|
285
|
+
with a better script, or changing the workflow. Approving is what releases the
|
|
286
|
+
voices and the clips — the expensive part — so do not approve a storyboard you
|
|
287
|
+
have just told the user is wrong.
|
|
272
288
|
|
|
273
289
|
On a **Show run** you can also send it back: `video flag <runId> --note "…"`.
|
|
274
290
|
The note is read by the model that rewrites the storyboard, so make it concrete
|
|
@@ -357,6 +373,88 @@ written by a bot is refused with a pointer at the review-time command, because
|
|
|
357
373
|
nobody knows the speaker names yet. When the storyboard arrives, the voices are
|
|
358
374
|
already set and `exodus video voices <runId>` shows them.
|
|
359
375
|
|
|
376
|
+
**Choosing how the voices are made.** `--voice-treatment` on `workflow run` says
|
|
377
|
+
HOW a run makes its voices, and it is the only door on the workflow route. Two
|
|
378
|
+
kinds of answer.
|
|
379
|
+
|
|
380
|
+
A name on its own picks a way of working that plays a voice you already have,
|
|
381
|
+
and still pairs with `--voices`:
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt \
|
|
385
|
+
--voice-treatment lipsync-retarget --voices @voices.json
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The other kind has the video model speak the CAST's lines itself, in voices you
|
|
389
|
+
WRITE. Those take a `voice-treatment.json`, which is the whole request body:
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
{
|
|
393
|
+
"path": "native-prompt",
|
|
394
|
+
"describe": {
|
|
395
|
+
"BUDGET": "Dry, tired middle-aged baritone. Slight rasp. Deadpan, unhurried.",
|
|
396
|
+
"PIGGY BANK": "Bright quick alto, a little smug, crisp consonants."
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt \
|
|
403
|
+
--voice-treatment @voice-treatment.json
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Every speaker in the script needs a description. One left out gets a voice the
|
|
407
|
+
model invents afresh on every clip, and that drift only shows up once the clips
|
|
408
|
+
are paid for, so the launch refuses instead.
|
|
409
|
+
|
|
410
|
+
**The narration is not part of this.** A describing treatment changes how the
|
|
411
|
+
CAST's lines are voiced. A scene with narration (`voText`) is still recorded by
|
|
412
|
+
ElevenLabs on the member's own key by the Voiceover step, exactly as it always
|
|
413
|
+
was — so a run with `NARRATOR:` turns still needs an ElevenLabs key, and still
|
|
414
|
+
costs ElevenLabs money. `exodus video voices <runId>` says which of the two
|
|
415
|
+
shapes a run is, and only claims nothing is sent to ElevenLabs when nothing is.
|
|
416
|
+
|
|
417
|
+
**What refuses, all before the run is created, so nothing is spent.**
|
|
418
|
+
|
|
419
|
+
- A treatment name that does not exist. The message lists the ones that do.
|
|
420
|
+
- A describing treatment with a speaker undescribed, a speaker the script does
|
|
421
|
+
not have, the same speaker described twice, or an empty description.
|
|
422
|
+
- Written voices plus `--voices`. The video model owns the voice on those runs,
|
|
423
|
+
so the ElevenLabs voices would be paid for and never heard.
|
|
424
|
+
- Descriptions on a treatment that plays voices you picked instead.
|
|
425
|
+
- A workflow whose script is written by a bot. Nobody knows the speaker names
|
|
426
|
+
yet, so there is nothing to describe.
|
|
427
|
+
- A workflow with no Video step, a Video step whose model cannot do what was
|
|
428
|
+
asked, or a Video step with nothing wired into its voiceover port when the
|
|
429
|
+
treatment needs one.
|
|
430
|
+
- No Google AI key on the account, for the treatment that renders through
|
|
431
|
+
Google directly.
|
|
432
|
+
- A rig that shows every line on screen as text. No clip speaks, so there is no
|
|
433
|
+
voice to treat.
|
|
434
|
+
|
|
435
|
+
**Still being tested.** Everything beyond the two shipped ways of working is
|
|
436
|
+
switched on only on the test stack while it is being tried out. On the live
|
|
437
|
+
stack those names refuse by name, and the message says which two to use
|
|
438
|
+
instead. That is a property of the stack, not of the workflow, so the same
|
|
439
|
+
saved workflow behaves differently on each — and a retry asks again rather than
|
|
440
|
+
copying, so a run retried after a treatment was switched off refuses.
|
|
441
|
+
|
|
442
|
+
**A sub-workflow chooses none of this.** When a Call box runs another workflow
|
|
443
|
+
inside this one, the child run carries no voice treatment: it renders the
|
|
444
|
+
shipped default, whatever the parent was started with. A treatment is a choice a
|
|
445
|
+
person made at a keyboard for one run, and nobody made it for the sub-workflow.
|
|
446
|
+
|
|
447
|
+
**Written voices cannot be changed at the review.** They are chosen when the run
|
|
448
|
+
starts. `exodus video voices <runId>` on a described run names the treatment and
|
|
449
|
+
prints each written voice, and says plainly that `--voice-treatment` at launch is
|
|
450
|
+
what changes them. Do not tell anyone to create a Show for this, and do not set a
|
|
451
|
+
voice path in node config — neither is a door.
|
|
452
|
+
|
|
453
|
+
**Where it shows up afterwards.** The launch receipt and `exodus workflow status`
|
|
454
|
+
both print one line naming the treatment and the speakers who got a written
|
|
455
|
+
voice. `manifest.json` from `exodus video pull` carries the same under
|
|
456
|
+
`provenance.voice`, and each cast entry carries its `voiceDescription`.
|
|
457
|
+
|
|
360
458
|
**The JSON receipt** (`--json`), the same shape for reading and for setting:
|
|
361
459
|
|
|
362
460
|
```
|
|
@@ -365,12 +463,18 @@ runId
|
|
|
365
463
|
changed[] the characters whose voice actually moved (set only)
|
|
366
464
|
canChange false once the run has left the storyboard review
|
|
367
465
|
whyNot why, in one sentence, when canChange is false
|
|
466
|
+
treatment.path which way of making voices this run froze, or null
|
|
467
|
+
treatment.kind "performance-then-conversion", "voiceover-then-lipsync" or
|
|
468
|
+
"render-owns-voice"
|
|
469
|
+
treatment.pinsHeard true when the chosen ElevenLabs voices are heard on this
|
|
470
|
+
run; false only for "render-owns-voice"
|
|
368
471
|
cast[] one per character
|
|
369
472
|
characterId, name
|
|
370
473
|
voice { voiceId, label }: the voice this character's clips are
|
|
371
474
|
converted to. null only when they have no voice of their
|
|
372
475
|
own AND the run has no default voice
|
|
373
476
|
voiceFrom "own-pin" or "run-default"; absent when voice is null
|
|
477
|
+
description the WRITTEN voice, when the run was given one
|
|
374
478
|
availability { state: "available" | "missing" | "not-checked", … }
|
|
375
479
|
spokenScenes how many scenes this character speaks in
|
|
376
480
|
spokenSeconds those scenes' planned seconds — what ElevenLabs bills on
|
|
@@ -456,6 +560,15 @@ scenes[] one per scene, in order
|
|
|
456
560
|
on record when the voice pass replaced the
|
|
457
561
|
clip with a new file; null when the clip IS
|
|
458
562
|
that file, or there is no clip
|
|
563
|
+
provenance which saved rulebook this run followed:
|
|
564
|
+
{format: {via, nodeId, rigId, rigName,
|
|
565
|
+
rulesFrom, specVersion}}. Absent when the
|
|
566
|
+
run followed none. `nodeId` is the Rig box
|
|
567
|
+
that chose it (absent on a Show ad, which
|
|
568
|
+
has no box); `rulesFrom` names the rig the
|
|
569
|
+
rules were borrowed from when the picked
|
|
570
|
+
rig is a copy. Names, ids and a version
|
|
571
|
+
only — never the rules themselves
|
|
459
572
|
failed[] files that did not download: {file, url, error}
|
|
460
573
|
```
|
|
461
574
|
|
|
@@ -504,13 +617,16 @@ own ElevenLabs render.
|
|
|
504
617
|
**Which scenes carry their voice where.** A scene whose storyboard entry has a
|
|
505
618
|
non-empty `voText` is a narrator scene: its clip is rendered silent and the
|
|
506
619
|
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
|
-
|
|
620
|
+
speech inside the clip — under the default voice treatment the video model's
|
|
621
|
+
invented voice is then replaced, in place, with the cast member's pinned
|
|
622
|
+
ElevenLabs voice (that is `revoiced: true`). Other treatments leave the render
|
|
623
|
+
owning its own audio, e.g. lip-sync retargeting a VO track onto the video. The
|
|
624
|
+
treatment is frozen on the run when it starts, and there are exactly two doors
|
|
625
|
+
that set one: `--voice-path` on `video start` for a Show run, and
|
|
626
|
+
`--voice-treatment` on `workflow run` for a workflow run (see **Choosing how the
|
|
627
|
+
voices are made**). Node config is never a door — nothing on the canvas sets a
|
|
628
|
+
voice treatment, and a run that was given none renders the default. Read
|
|
629
|
+
`wordsFrom` and `revoiced` per scene rather than assuming.
|
|
514
630
|
|
|
515
631
|
**Word timings** (`scene-NN.words.json`). Seconds from the start of the file
|
|
516
632
|
`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
|
|
@@ -205,7 +237,7 @@ Two limits worth knowing before you treat an empty inbox as proof of anything:
|
|
|
205
237
|
|
|
206
238
|
**What `inbox` does not list is work that isn't waiting on anybody.** A queued run, a running run, and any automation that ran start-to-finish without an approval step never appear there — not because they're hidden, but because there is nothing for you to decide. Plenty of workflows are built that way on purpose; the shipped **Winner Flywheel** starter is one (brief → splitter → output, no Checkpoint box anywhere), so its run *can never* show up in `inbox`. An empty inbox after a promote means "nothing needs you", not "nothing ran".
|
|
207
239
|
|
|
208
|
-
**To see runs that aren't waiting on you, run `workflow status` with no `--id`.** That prints the brand's recent runs — newest first, one row each with the workflow name, its status, the **date** it started, and its **run id**. That bare list is how you discover a background run at all, including one that started and finished while you weren't looking; take the id from it and `status --id <runId>` for the full detail.
|
|
240
|
+
**To see runs that aren't waiting on you, run `workflow status` with no `--id`.** That prints the brand's recent runs — newest first, one row each with the run's own name (the name you gave it, else the first line of its brief, else the workflow's name), its type ("Copy run" or "Workflow"), its status, the **date** it started, and its **run id**. That bare list is how you discover a background run at all, including one that started and finished while you weren't looking; take the id from it and `status --id <runId>` for the full detail.
|
|
209
241
|
|
|
210
242
|
Two things about that list you have to know before you answer a question with it:
|
|
211
243
|
|
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, type WorkflowRun } 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 {
|
|
@@ -99,7 +100,9 @@ export interface VideoRun {
|
|
|
99
100
|
pauseReason?: BuilderPauseReason;
|
|
100
101
|
pausedNodeId?: string;
|
|
101
102
|
nodes: VideoRunNode[];
|
|
103
|
+
pauseAhead?: WorkflowRun["pauseAhead"];
|
|
102
104
|
castLock?: PullCastLock | null;
|
|
105
|
+
provenance?: RunProvenance;
|
|
103
106
|
workflowId?: string;
|
|
104
107
|
moduleOwned?: boolean;
|
|
105
108
|
}
|
|
@@ -113,6 +116,7 @@ export interface NodeItem {
|
|
|
113
116
|
attempt?: number;
|
|
114
117
|
flagged?: boolean;
|
|
115
118
|
findings?: ClipFinding[];
|
|
119
|
+
staleClaim?: boolean;
|
|
116
120
|
artifact?: ArtifactSubset;
|
|
117
121
|
}
|
|
118
122
|
export interface ShowRow {
|
|
@@ -225,6 +229,7 @@ export interface ManifestCastRef {
|
|
|
225
229
|
error: string | null;
|
|
226
230
|
voiceId: string | null;
|
|
227
231
|
voiceLabel: string | null;
|
|
232
|
+
voiceDescription: string | null;
|
|
228
233
|
}
|
|
229
234
|
export interface VideoManifest {
|
|
230
235
|
runId: string;
|
|
@@ -240,6 +245,7 @@ export interface VideoManifest {
|
|
|
240
245
|
} | null;
|
|
241
246
|
scenes: ManifestScene[];
|
|
242
247
|
failed: PullFailure[];
|
|
248
|
+
provenance?: RunProvenance;
|
|
243
249
|
}
|
|
244
250
|
export declare const CAST_LEDGER_BASE = 910000;
|
|
245
251
|
export interface PullPlan {
|
|
@@ -292,6 +298,7 @@ export interface ClipRedoTarget {
|
|
|
292
298
|
}
|
|
293
299
|
export declare function planClipRedo(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
294
300
|
export declare function retryClipFlow(runId: string, target: ClipRedoTarget, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
301
|
+
export declare const VOICE_PATHS_NEVER_HEARD: Set<string>;
|
|
295
302
|
export declare function planClipRevoice(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
296
303
|
export type RevoiceTarget = ClipRedoTarget | {
|
|
297
304
|
all: true;
|
|
@@ -314,6 +321,7 @@ export interface CastVoiceRow {
|
|
|
314
321
|
state: "not-checked";
|
|
315
322
|
why: string;
|
|
316
323
|
};
|
|
324
|
+
description?: string;
|
|
317
325
|
spokenScenes: number;
|
|
318
326
|
spokenSeconds: number;
|
|
319
327
|
}
|
|
@@ -327,6 +335,9 @@ export interface CastVoiceSheet {
|
|
|
327
335
|
label?: string;
|
|
328
336
|
} | null;
|
|
329
337
|
treatment: {
|
|
338
|
+
kind?: string;
|
|
339
|
+
path?: string | null;
|
|
340
|
+
usesElevenLabs?: boolean;
|
|
330
341
|
summary: string;
|
|
331
342
|
provider: string;
|
|
332
343
|
model: string;
|
package/dist/commands/video.js
CHANGED
|
@@ -5,7 +5,7 @@ import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
|
5
5
|
import { displayRunStatus, formatApiError } from "../lib/format.js";
|
|
6
6
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
7
7
|
import { hasBinary } from "../lib/preflight.js";
|
|
8
|
-
import { ASSET_UPLOAD_POLICY } from "./workflow.js";
|
|
8
|
+
import { ASSET_UPLOAD_POLICY, pauseAheadLine, } from "./workflow.js";
|
|
9
9
|
export const helpText = `
|
|
10
10
|
exodus video — make an ad from a Show, pull every piece, upload your cut
|
|
11
11
|
|
|
@@ -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
|
}
|
|
@@ -912,6 +921,8 @@ const ITEM_STATUS_WORD = {
|
|
|
912
921
|
function itemWord(item) {
|
|
913
922
|
if (!item)
|
|
914
923
|
return "—";
|
|
924
|
+
if (item.staleClaim === true)
|
|
925
|
+
return "stopped";
|
|
915
926
|
if (item.flagged === true)
|
|
916
927
|
return "flagged";
|
|
917
928
|
return ITEM_STATUS_WORD[item.status] ?? item.status;
|
|
@@ -965,6 +976,7 @@ export async function statusFlow(runId, json, deps) {
|
|
|
965
976
|
const lines = [
|
|
966
977
|
headline,
|
|
967
978
|
...stopLines(stop, runId, reviewUrl(deps.dashboardUrl, run)),
|
|
979
|
+
...(stop.at === "running" && run.pauseAhead ? [pauseAheadLine(run.pauseAhead)] : []),
|
|
968
980
|
...warnings.map((w) => `Heads-up (${w.step}): ${w.warning}`),
|
|
969
981
|
];
|
|
970
982
|
if (byScene.size === 0) {
|
|
@@ -1167,6 +1179,7 @@ export function planClipRedo(run, items, target) {
|
|
|
1167
1179
|
return { ok: false, reason: found.reason };
|
|
1168
1180
|
const { row, uploadedCut } = found;
|
|
1169
1181
|
const redoable = row.status === "failed" ||
|
|
1182
|
+
row.staleClaim === true ||
|
|
1170
1183
|
(row.status === "done" && (row.flagged === true || stop.at === "final-watch"));
|
|
1171
1184
|
if (!redoable) {
|
|
1172
1185
|
if (row.status === "done") {
|
|
@@ -1219,7 +1232,7 @@ function findClipRow(run, items, target) {
|
|
|
1219
1232
|
"and one clip can only be redone on a step that finished.",
|
|
1220
1233
|
};
|
|
1221
1234
|
}
|
|
1222
|
-
if (row.status === "running") {
|
|
1235
|
+
if (row.status === "running" && row.staleClaim !== true) {
|
|
1223
1236
|
return { reason: `Scene ${target.sceneIndex} is already being redone.` };
|
|
1224
1237
|
}
|
|
1225
1238
|
return { row, uploadedCut };
|
|
@@ -1285,20 +1298,34 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1285
1298
|
}
|
|
1286
1299
|
function clipIsStillRaw(item) {
|
|
1287
1300
|
const a = item.artifact;
|
|
1288
|
-
return (item.status === "done" &&
|
|
1301
|
+
return ((item.status === "done" || item.staleClaim === true) &&
|
|
1289
1302
|
a?.type === "video" &&
|
|
1290
1303
|
a.revoiced !== true &&
|
|
1291
1304
|
a.speechTrimmed !== true &&
|
|
1292
1305
|
a.tailTrimmed !== true);
|
|
1293
1306
|
}
|
|
1294
1307
|
const VOICE_NOT_CHANGED_CODES = new Set(["voice-unpinned", "voice-not-applied"]);
|
|
1308
|
+
export const VOICE_PATHS_NEVER_HEARD = new Set(["native-prompt", "omni-audio-ids", "gemini-direct"]);
|
|
1309
|
+
async function neverHeardRunLine(runId, deps) {
|
|
1310
|
+
const res = await deps
|
|
1311
|
+
.get(`${VOICES_PATH}?runId=${encodeURIComponent(runId)}`)
|
|
1312
|
+
.catch(() => null);
|
|
1313
|
+
if (!res?.ok)
|
|
1314
|
+
return null;
|
|
1315
|
+
const path = res.data?.treatment?.path;
|
|
1316
|
+
if (typeof path !== "string" || !VOICE_PATHS_NEVER_HEARD.has(path))
|
|
1317
|
+
return null;
|
|
1318
|
+
return (`This run's clips are not voiced by ElevenLabs — the "${path}" way settles each voice as ` +
|
|
1319
|
+
"the clip is rendered — so no clip on this run has a voice pass to redo. Nothing was queued " +
|
|
1320
|
+
"and nothing was spent. To make a clip again from scratch, redo the clip instead.");
|
|
1321
|
+
}
|
|
1295
1322
|
export function planClipRevoice(run, items, target) {
|
|
1296
1323
|
const found = findClipRow(run, items, target);
|
|
1297
1324
|
if ("reason" in found)
|
|
1298
1325
|
return { ok: false, reason: found.reason };
|
|
1299
1326
|
const { row, uploadedCut } = found;
|
|
1300
1327
|
const remake = `To make the clip again from scratch: exodus video retry-clip ${run._id} --scene ${target.sceneIndex}`;
|
|
1301
|
-
if (row.status !== "done" || row.artifact?.type !== "video") {
|
|
1328
|
+
if ((row.status !== "done" && row.staleClaim !== true) || row.artifact?.type !== "video") {
|
|
1302
1329
|
return {
|
|
1303
1330
|
ok: false,
|
|
1304
1331
|
reason: `Scene ${target.sceneIndex} has no finished clip, so there is no voice to redo. ${remake}`,
|
|
@@ -1342,10 +1369,11 @@ export async function revoiceFlow(runId, target, json, deps) {
|
|
|
1342
1369
|
.map((i) => ({ sceneIndex: i.sceneIndex, nodeId: i.nodeId }))
|
|
1343
1370
|
: [target];
|
|
1344
1371
|
if (targets.length === 0) {
|
|
1345
|
-
const
|
|
1372
|
+
const note = (await neverHeardRunLine(runId, deps)) ??
|
|
1373
|
+
"No clip on this run is waiting for a voice pass, so nothing was started.";
|
|
1346
1374
|
return {
|
|
1347
1375
|
code: 0,
|
|
1348
|
-
lines: json ? [JSON.stringify({ ok: true, runId, scenes: [], note
|
|
1376
|
+
lines: json ? [JSON.stringify({ ok: true, runId, scenes: [], note })] : [note],
|
|
1349
1377
|
};
|
|
1350
1378
|
}
|
|
1351
1379
|
const scenes = [];
|
|
@@ -1500,7 +1528,7 @@ export async function voicesFlow(runId, voices, json, deps) {
|
|
|
1500
1528
|
}
|
|
1501
1529
|
function availabilityWords(row) {
|
|
1502
1530
|
if (!row.voice)
|
|
1503
|
-
return "no voice yet";
|
|
1531
|
+
return row.description ? "voice written below" : "no voice yet";
|
|
1504
1532
|
switch (row.availability?.state) {
|
|
1505
1533
|
case "available":
|
|
1506
1534
|
return "ElevenLabs has it";
|
|
@@ -1534,12 +1562,18 @@ function voiceSummaryLines(cast, notices) {
|
|
|
1534
1562
|
}
|
|
1535
1563
|
export function voiceSheetLines(sheet) {
|
|
1536
1564
|
const lines = [`Voices for run ${sheet.runId}`];
|
|
1565
|
+
if (typeof sheet.treatment.path === "string" && sheet.treatment.path !== "") {
|
|
1566
|
+
lines.push(` Treatment: ${sheet.treatment.path}`);
|
|
1567
|
+
}
|
|
1537
1568
|
if (sheet.cast.length === 0)
|
|
1538
1569
|
lines.push(" Nobody is in this ad yet.");
|
|
1539
1570
|
for (const row of sheet.cast) {
|
|
1540
1571
|
const voice = voiceWords(row);
|
|
1541
1572
|
lines.push(` ${row.characterId} ${row.name} ${voice} ${availabilityWords(row)} ` +
|
|
1542
|
-
`speaks in ${row.spokenScenes}
|
|
1573
|
+
`speaks in ${row.spokenScenes} scene${row.spokenScenes === 1 ? "" : "s"}, ` +
|
|
1574
|
+
`about ${row.spokenSeconds}s`);
|
|
1575
|
+
if (row.description)
|
|
1576
|
+
lines.push(` written voice: ${row.description}`);
|
|
1543
1577
|
}
|
|
1544
1578
|
if (sheet.narrator) {
|
|
1545
1579
|
const label = sheet.narrator.label ? `${sheet.narrator.label} ` : "";
|
|
@@ -1551,9 +1585,17 @@ export function voiceSheetLines(sheet) {
|
|
|
1551
1585
|
: "Nothing changed — those voices were already set. Pictures and script were not touched.");
|
|
1552
1586
|
}
|
|
1553
1587
|
lines.push(`How voices are applied: ${sheet.treatment.summary}${sheet.treatment.speedChange ? "" : " No speed change."}`);
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1588
|
+
const playsPinnedVoices = sheet.treatment.kind !== "render-owns-voice";
|
|
1589
|
+
const convertsClips = playsPinnedVoices && sheet.treatment.usage.clips > 0;
|
|
1590
|
+
if (convertsClips || sheet.treatment.usesElevenLabs === undefined) {
|
|
1591
|
+
lines.push(`Cost: billed to your own ElevenLabs key. ${sheet.treatment.costNote} ` +
|
|
1592
|
+
`About ${sheet.treatment.usage.seconds} seconds of speech across ` +
|
|
1593
|
+
`${sheet.treatment.usage.clips} clips will be converted.`);
|
|
1594
|
+
}
|
|
1595
|
+
else if (sheet.treatment.usesElevenLabs) {
|
|
1596
|
+
lines.push("Cost: the narration on this run is voiced by ElevenLabs and billed to your own key. " +
|
|
1597
|
+
`${sheet.treatment.costNote} The clips themselves are not converted.`);
|
|
1598
|
+
}
|
|
1557
1599
|
for (const notice of sheet.notices)
|
|
1558
1600
|
lines.push(notice);
|
|
1559
1601
|
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,10 +352,43 @@ 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;
|
|
353
388
|
workflowName: string;
|
|
389
|
+
runTitle?: string;
|
|
390
|
+
title?: string;
|
|
391
|
+
moduleKey?: string;
|
|
354
392
|
status: WorkflowRunStatus;
|
|
355
393
|
error?: string;
|
|
356
394
|
counts?: WorkflowCounts;
|
|
@@ -372,7 +410,13 @@ export interface WorkflowRun {
|
|
|
372
410
|
nodeId: string;
|
|
373
411
|
approvedAt: number;
|
|
374
412
|
}[];
|
|
413
|
+
provenance?: RunProvenance;
|
|
414
|
+
pauseAhead?: {
|
|
415
|
+
nodeId: string;
|
|
416
|
+
after: "frames" | "storyboard";
|
|
417
|
+
};
|
|
375
418
|
}
|
|
419
|
+
export declare function pauseAheadLine(ahead: NonNullable<WorkflowRun["pauseAhead"]>): string;
|
|
376
420
|
export type WorkflowRunProjection = Omit<WorkflowRun, "nodes"> & {
|
|
377
421
|
nodes?: never;
|
|
378
422
|
};
|
|
@@ -410,6 +454,7 @@ interface RunFlowOptions {
|
|
|
410
454
|
autoApprove?: boolean;
|
|
411
455
|
imageRigOverrides?: Record<string, unknown>;
|
|
412
456
|
voices?: VoiceMap;
|
|
457
|
+
voiceTreatment?: VoiceTreatmentBody;
|
|
413
458
|
wait: boolean;
|
|
414
459
|
json: boolean;
|
|
415
460
|
out?: string;
|
|
@@ -431,6 +476,11 @@ export declare function rejectTerminalFlag(occurrences: FlagOccurrence[]): void;
|
|
|
431
476
|
export declare function parseAutoApproveFlag(args: string[]): boolean;
|
|
432
477
|
export declare function parseRigOverridesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
|
|
433
478
|
export declare function parseVoicesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceMap | undefined;
|
|
479
|
+
export interface VoiceTreatmentBody {
|
|
480
|
+
path: string;
|
|
481
|
+
describe?: Record<string, string>;
|
|
482
|
+
}
|
|
483
|
+
export declare function parseVoiceTreatmentFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceTreatmentBody | undefined;
|
|
434
484
|
export declare function parseFillFlag(occurrences: FlagOccurrence[]): string | undefined;
|
|
435
485
|
export declare function formatWorkflowList(workflows: WorkflowListItem[]): string;
|
|
436
486
|
export declare function formatRecentRuns(runs: WorkflowRunProjection[]): string;
|
|
@@ -464,6 +514,7 @@ export declare function botsFlow(opts: {
|
|
|
464
514
|
slug?: string;
|
|
465
515
|
json: boolean;
|
|
466
516
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
517
|
+
export declare function voiceTreatmentLine(provenance: unknown): string | undefined;
|
|
467
518
|
export declare function runFlow(workflowRef: string, opts: RunFlowOptions, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
468
519
|
export declare function statusFlow(opts: {
|
|
469
520
|
id?: string;
|
|
@@ -6,6 +6,7 @@ import { formatApiError } from "../lib/format.js";
|
|
|
6
6
|
import { pollUntilDone } from "../lib/poll.js";
|
|
7
7
|
import { normalizeRunStatus, isTerminalRunStatus, storedWorkflowStatusForms, TERMINAL_RUN_STATUSES, } from "../lib/runStatus.js";
|
|
8
8
|
import { runVerdict } from "../lib/runVerdict.js";
|
|
9
|
+
import { runDisplayName, runHeadingWord, runTypeWord } from "../lib/runNames.js";
|
|
9
10
|
import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
|
|
10
11
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
11
12
|
import { getChannel } from "../lib/channel.js";
|
|
@@ -20,7 +21,7 @@ Usage:
|
|
|
20
21
|
exodus workflow templates [list] [--json]
|
|
21
22
|
exodus workflow templates export <key> [--out <file>] [--json]
|
|
22
23
|
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]
|
|
24
|
+
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
25
|
exodus workflow status [--json]
|
|
25
26
|
exodus workflow status --id <runId> [--out <dir>] [--json]
|
|
26
27
|
exodus workflow cancel <runId> [--reason "..."] [--json]
|
|
@@ -102,6 +103,29 @@ Flags:
|
|
|
102
103
|
<run>" at the storyboard review instead. Voice ids
|
|
103
104
|
belong to the brand's own folder — never put them in a
|
|
104
105
|
shared workflow or template.
|
|
106
|
+
--voice-treatment <v> (run) Say HOW this run makes its voices. Two kinds of
|
|
107
|
+
answer. A name on its own picks a way of working that
|
|
108
|
+
uses voices you already have:
|
|
109
|
+
--voice-treatment sts-revoice
|
|
110
|
+
--voice-treatment lipsync-retarget --voices @voices.json
|
|
111
|
+
The other kind has the video model speak the lines
|
|
112
|
+
itself, in a voice you WRITE instead of one you pick.
|
|
113
|
+
Those need a description for every speaker in your
|
|
114
|
+
script, so they take a JSON object, or @path to a .json
|
|
115
|
+
file holding one:
|
|
116
|
+
--voice-treatment @voice-treatment.json
|
|
117
|
+
--voice-treatment '{"path":"native-prompt","describe":{"HOST 1":"Dry, tired baritone."}}'
|
|
118
|
+
Written voices and --voices are never used together:
|
|
119
|
+
the video model owns the voice on those runs, so
|
|
120
|
+
ElevenLabs voices would be paid for and never heard,
|
|
121
|
+
and asking for both stops the launch. Anything the run
|
|
122
|
+
could not actually do — a treatment this stack has not
|
|
123
|
+
switched on, a Video step on a model that cannot do it,
|
|
124
|
+
a missing key, a rig that shows every line as on-screen
|
|
125
|
+
text — stops the launch before the run is created, and
|
|
126
|
+
the message says which. Descriptions are chosen HERE,
|
|
127
|
+
at the start: "exodus video voices <run>" shows them at
|
|
128
|
+
the review but cannot change them.
|
|
105
129
|
--auto-approve (run) Deliberately unattended launch: every Checkpoint
|
|
106
130
|
box this run stops at is approved automatically, with
|
|
107
131
|
whatever it was holding left exactly as it is, and the
|
|
@@ -194,6 +218,7 @@ Examples:
|
|
|
194
218
|
exodus workflow run "Product Shots" --rig-overrides '{"rig_1":{"lines":{"line_1":{"count":3}}}}'
|
|
195
219
|
exodus workflow run "Product Shots" --rig-overrides @rig.json --wait
|
|
196
220
|
exodus workflow run "Podcast Ad" --input script=@script.txt --voices @voices.json --wait
|
|
221
|
+
exodus workflow run "Podcast Ad" --input script=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
197
222
|
exodus workflow run "Launch Flow" --wait --out ./deliverables
|
|
198
223
|
exodus workflow status # recent runs + their ids
|
|
199
224
|
exodus workflow status --id wr_123
|
|
@@ -268,6 +293,20 @@ Notes:
|
|
|
268
293
|
no speaker names to key on at the start, so it is refused with a pointer at
|
|
269
294
|
the review-time command. Keep voice ids in the brand's own folder, never in a
|
|
270
295
|
shared workflow or template. (#1846)
|
|
296
|
+
"workflow run --voice-treatment" says HOW a run makes its voices. Some ways of
|
|
297
|
+
working play a voice you picked (that is today's behaviour, and it still takes
|
|
298
|
+
--voices); others have the video model speak the CAST's lines itself, in voices
|
|
299
|
+
you describe in words. A described voice needs one description per speaker,
|
|
300
|
+
because a speaker nobody described gets a voice the model invents afresh on
|
|
301
|
+
every clip, and the drift only shows up once the clips are paid for. Written
|
|
302
|
+
voices and --voices are refused together, since the ElevenLabs voices would
|
|
303
|
+
never be heard. Everything the run could not do — a way of working this stack
|
|
304
|
+
has not switched on, a Video step whose model cannot do it, a missing key, a
|
|
305
|
+
rig that shows every line as on-screen text — stops the launch before the run
|
|
306
|
+
is created. The ways of working beyond the two shipped ones are switched on
|
|
307
|
+
only on the test stack while they are being tried out, so a live run refuses
|
|
308
|
+
them by name. Both the launch receipt and "workflow status" say which way a
|
|
309
|
+
run took, and "exodus video voices <run>" shows each written voice. (#1869)
|
|
271
310
|
workflow triggers are addressed by 1-based position — a trigger carries no id,
|
|
272
311
|
so the CLI reads the live list from the export contract and sends a fingerprint
|
|
273
312
|
of the trigger's fields as the guard; a concurrent edit fails loud rather than
|
|
@@ -310,6 +349,29 @@ Notes:
|
|
|
310
349
|
ids it wants). The "gate" verbs retired in 2.0 along with the Gate node — the
|
|
311
350
|
command now prints a pointer at "checkpoint" and exits 1.
|
|
312
351
|
`.trim();
|
|
352
|
+
export function formatRulesLine(provenance) {
|
|
353
|
+
const format = provenance?.format;
|
|
354
|
+
if (!format || typeof format !== "object")
|
|
355
|
+
return undefined;
|
|
356
|
+
const { rigName, specVersion, nodeId, rulesFrom } = format;
|
|
357
|
+
if (typeof rigName !== "string" || rigName === "")
|
|
358
|
+
return undefined;
|
|
359
|
+
const version = typeof specVersion === "string" && specVersion !== ""
|
|
360
|
+
? `, version ${specVersion}`
|
|
361
|
+
: "";
|
|
362
|
+
const where = typeof nodeId === "string" && nodeId !== ""
|
|
363
|
+
? ` (from the "${nodeId}" Rig box)`
|
|
364
|
+
: " (from the Show's rig)";
|
|
365
|
+
const borrowed = rulesFrom && typeof rulesFrom.rigName === "string"
|
|
366
|
+
? ` The rules come from "${rulesFrom.rigName}".`
|
|
367
|
+
: "";
|
|
368
|
+
return `Format rules: ${rigName}${version}${where}.${borrowed}`;
|
|
369
|
+
}
|
|
370
|
+
export function pauseAheadLine(ahead) {
|
|
371
|
+
return ahead.after === "frames"
|
|
372
|
+
? "This run will pause for your approval once the frames are ready. The reference, cast and scene pictures are made first (a few cents each). Nothing bigger is spent until you approve."
|
|
373
|
+
: "This run will pause for your approval once the storyboard is written. Nothing is spent on pictures, voices or clips until you approve.";
|
|
374
|
+
}
|
|
313
375
|
const LIST_PATH = "/api/v2/workflows";
|
|
314
376
|
const RUN_PATH = "/api/v2/workflows/run";
|
|
315
377
|
const STATUS_PATH = "/api/v2/workflow";
|
|
@@ -354,6 +416,7 @@ const VALUE_FLAGS = new Set([
|
|
|
354
416
|
"reject",
|
|
355
417
|
"rig-overrides",
|
|
356
418
|
"voices",
|
|
419
|
+
"voice-treatment",
|
|
357
420
|
"note",
|
|
358
421
|
]);
|
|
359
422
|
function defaultMkdirp(dir) {
|
|
@@ -976,6 +1039,57 @@ export function parseVoicesFlag(occurrences, readFile) {
|
|
|
976
1039
|
}
|
|
977
1040
|
return parsed;
|
|
978
1041
|
}
|
|
1042
|
+
export function parseVoiceTreatmentFlag(occurrences, readFile) {
|
|
1043
|
+
const usage = "--voice-treatment requires a treatment name, JSON, or @path/to/voice-treatment.json";
|
|
1044
|
+
let raw;
|
|
1045
|
+
for (const { flag, value } of occurrences) {
|
|
1046
|
+
if (flag !== "voice-treatment")
|
|
1047
|
+
continue;
|
|
1048
|
+
if (value === undefined || value.startsWith("--"))
|
|
1049
|
+
throw new Error(usage);
|
|
1050
|
+
raw = value;
|
|
1051
|
+
}
|
|
1052
|
+
if (raw === undefined)
|
|
1053
|
+
return undefined;
|
|
1054
|
+
let text = raw.trim();
|
|
1055
|
+
if (!text)
|
|
1056
|
+
throw new Error(usage);
|
|
1057
|
+
let fromFile = false;
|
|
1058
|
+
if (text.startsWith("@@")) {
|
|
1059
|
+
text = text.slice(1);
|
|
1060
|
+
}
|
|
1061
|
+
else if (text.startsWith("@")) {
|
|
1062
|
+
const filePath = text.slice(1);
|
|
1063
|
+
if (!filePath) {
|
|
1064
|
+
throw new Error("--voice-treatment @<file> needs a file path after \"@\"");
|
|
1065
|
+
}
|
|
1066
|
+
if (!readFile) {
|
|
1067
|
+
throw new Error(`--voice-treatment: cannot load @${filePath} here (no file access)`);
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
text = readFile(filePath);
|
|
1071
|
+
}
|
|
1072
|
+
catch (e) {
|
|
1073
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1074
|
+
throw new Error(`--voice-treatment: could not read file "${filePath}": ${msg}`);
|
|
1075
|
+
}
|
|
1076
|
+
fromFile = true;
|
|
1077
|
+
}
|
|
1078
|
+
if (!fromFile && !text.startsWith("{"))
|
|
1079
|
+
return { path: text };
|
|
1080
|
+
let parsed;
|
|
1081
|
+
try {
|
|
1082
|
+
parsed = JSON.parse(text);
|
|
1083
|
+
}
|
|
1084
|
+
catch (e) {
|
|
1085
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1086
|
+
throw new Error(`--voice-treatment is not valid JSON: ${msg}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1089
|
+
throw new Error('--voice-treatment must be a treatment name, or a JSON object like {"path":"native-prompt","describe":{"HOST 1":"..."}}');
|
|
1090
|
+
}
|
|
1091
|
+
return parsed;
|
|
1092
|
+
}
|
|
979
1093
|
export function parseFillFlag(occurrences) {
|
|
980
1094
|
let name;
|
|
981
1095
|
for (const { flag, value } of occurrences) {
|
|
@@ -1006,7 +1120,13 @@ const RECENT_RUNS_PAGE = 25;
|
|
|
1006
1120
|
export function formatRecentRuns(runs) {
|
|
1007
1121
|
if (runs.length === 0)
|
|
1008
1122
|
return "No workflow runs found for the active brand.";
|
|
1009
|
-
const rows = table(["
|
|
1123
|
+
const rows = table(["run", "type", "status", "created", "id"], runs.map((r) => [
|
|
1124
|
+
runDisplayName(r),
|
|
1125
|
+
runTypeWord(r),
|
|
1126
|
+
runVerdict(r),
|
|
1127
|
+
dateOnly(r.createdAt),
|
|
1128
|
+
r._id,
|
|
1129
|
+
]));
|
|
1010
1130
|
const notes = [];
|
|
1011
1131
|
if (runs.length >= RECENT_RUNS_PAGE) {
|
|
1012
1132
|
notes.push(`Showing the ${RECENT_RUNS_PAGE} newest runs — there may be older ones this list doesn't reach.`);
|
|
@@ -1087,7 +1207,7 @@ export function formatImportSummary(result, mode = {}) {
|
|
|
1087
1207
|
export function formatWorkflowRun(run) {
|
|
1088
1208
|
const lines = [];
|
|
1089
1209
|
const counts = formatCounts(run.counts);
|
|
1090
|
-
lines.push(
|
|
1210
|
+
lines.push(`${runHeadingWord(run)} — ${runDisplayName(run)}`);
|
|
1091
1211
|
lines.push(`runId: ${run._id}`);
|
|
1092
1212
|
lines.push(`workflowId: ${run.workflowId}`);
|
|
1093
1213
|
if (run.triggerRunId)
|
|
@@ -1095,6 +1215,12 @@ export function formatWorkflowRun(run) {
|
|
|
1095
1215
|
lines.push(`status: ${runVerdict(run)}${counts ? ` (${counts})` : ""}`);
|
|
1096
1216
|
if (run.isTerminal)
|
|
1097
1217
|
lines.push("terminal: yes");
|
|
1218
|
+
const rulesLine = formatRulesLine(run.provenance);
|
|
1219
|
+
if (rulesLine)
|
|
1220
|
+
lines.push(rulesLine);
|
|
1221
|
+
const treatmentLine = voiceTreatmentLine(run.provenance);
|
|
1222
|
+
if (treatmentLine)
|
|
1223
|
+
lines.push(treatmentLine);
|
|
1098
1224
|
if (run.error)
|
|
1099
1225
|
lines.push(`error: ${run.error}`);
|
|
1100
1226
|
if (Object.keys(run.inputs ?? {}).length > 0) {
|
|
@@ -1104,6 +1230,10 @@ export function formatWorkflowRun(run) {
|
|
|
1104
1230
|
if (run.autoApprovals && run.autoApprovals.length > 0) {
|
|
1105
1231
|
lines.push(`auto-approved: ${run.autoApprovals.length} checkpoint stop${run.autoApprovals.length === 1 ? "" : "s"} (${run.autoApprovals.map((a) => a.nodeId).join(", ")}) — launched with --auto-approve, nobody reviewed these`);
|
|
1106
1232
|
}
|
|
1233
|
+
if (run.pauseAhead) {
|
|
1234
|
+
lines.push("");
|
|
1235
|
+
lines.push(pauseAheadLine(run.pauseAhead));
|
|
1236
|
+
}
|
|
1107
1237
|
const parkedNodeId = gateParkedNodeId(run.status, run.pauseReason, run.pausedNodeId);
|
|
1108
1238
|
if (run.nodes.length > 0) {
|
|
1109
1239
|
lines.push("");
|
|
@@ -1786,6 +1916,23 @@ function launchVoicesLine(attached) {
|
|
|
1786
1916
|
}
|
|
1787
1917
|
return said.length > 0 ? `Voices: ${said.join(", ")}` : undefined;
|
|
1788
1918
|
}
|
|
1919
|
+
export function voiceTreatmentLine(provenance) {
|
|
1920
|
+
const voice = provenance?.voice;
|
|
1921
|
+
if (!voice || typeof voice !== "object")
|
|
1922
|
+
return undefined;
|
|
1923
|
+
if (voice.source === "unreadable") {
|
|
1924
|
+
return ("Voice treatment: the setting saved when this run started cannot be read, so no " +
|
|
1925
|
+
"clip will be made. Start the run again.");
|
|
1926
|
+
}
|
|
1927
|
+
const path = voice.path;
|
|
1928
|
+
if (typeof path !== "string" || path === "")
|
|
1929
|
+
return undefined;
|
|
1930
|
+
const where = voice.source === "show" ? "from its Show" : "chosen at launch";
|
|
1931
|
+
const described = voice.source === "launch" && Array.isArray(voice.descriptions) && voice.descriptions.length > 0
|
|
1932
|
+
? `. Written voices: ${voice.descriptions.map((d) => d.speaker).join(", ")}`
|
|
1933
|
+
: "";
|
|
1934
|
+
return `Voice treatment: ${path} (${where})${described}`;
|
|
1935
|
+
}
|
|
1789
1936
|
export async function runFlow(workflowRef, opts, deps) {
|
|
1790
1937
|
let workflowId;
|
|
1791
1938
|
try {
|
|
@@ -1829,6 +1976,7 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1829
1976
|
...(opts.autoApprove === true ? { autoApprove: true } : {}),
|
|
1830
1977
|
...(opts.imageRigOverrides ? { imageRigOverrides: opts.imageRigOverrides } : {}),
|
|
1831
1978
|
...(opts.voices ? { voices: opts.voices } : {}),
|
|
1979
|
+
...(opts.voiceTreatment ? { voiceTreatment: opts.voiceTreatment } : {}),
|
|
1832
1980
|
launchedVia: "cli",
|
|
1833
1981
|
};
|
|
1834
1982
|
const start = await deps.post(RUN_PATH, body);
|
|
@@ -1843,6 +1991,12 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1843
1991
|
if (opts.json && !opts.wait)
|
|
1844
1992
|
return { code: 0, lines: [JSON.stringify(base)] };
|
|
1845
1993
|
const voicesLine = launchVoicesLine(data?.launchVoices);
|
|
1994
|
+
const provenance = { format: data?.format ?? null, voice: data?.voice ?? null };
|
|
1995
|
+
const rulesLine = formatRulesLine(provenance);
|
|
1996
|
+
const treatmentLine = voiceTreatmentLine(provenance);
|
|
1997
|
+
const advisories = Array.isArray(data?.advisories)
|
|
1998
|
+
? data.advisories.filter((line) => typeof line === "string")
|
|
1999
|
+
: [];
|
|
1846
2000
|
const lines = opts.json
|
|
1847
2001
|
? []
|
|
1848
2002
|
: [
|
|
@@ -1850,7 +2004,10 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1850
2004
|
"Workflow run started.",
|
|
1851
2005
|
`runId: ${data.runId}`,
|
|
1852
2006
|
`triggerRunId: ${data.triggerRunId}`,
|
|
2007
|
+
...(rulesLine ? [rulesLine] : []),
|
|
2008
|
+
...advisories,
|
|
1853
2009
|
...(voicesLine ? [voicesLine] : []),
|
|
2010
|
+
...(treatmentLine ? [treatmentLine] : []),
|
|
1854
2011
|
`Poll: exodus workflow status --id ${data.runId}`,
|
|
1855
2012
|
];
|
|
1856
2013
|
if (!opts.wait) {
|
|
@@ -2957,7 +3114,7 @@ export async function run(flags, occurrences) {
|
|
|
2957
3114
|
const workflowRef = rest[0];
|
|
2958
3115
|
if (!workflowRef) {
|
|
2959
3116
|
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]");
|
|
3117
|
+
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
3118
|
process.exit(1);
|
|
2962
3119
|
}
|
|
2963
3120
|
let inputs;
|
|
@@ -2965,6 +3122,7 @@ export async function run(flags, occurrences) {
|
|
|
2965
3122
|
let autoApprove;
|
|
2966
3123
|
let imageRigOverrides;
|
|
2967
3124
|
let voices;
|
|
3125
|
+
let voiceTreatment;
|
|
2968
3126
|
try {
|
|
2969
3127
|
inputs = parseRawInputFlags(occurrences);
|
|
2970
3128
|
rejectTerminalFlag(occurrences);
|
|
@@ -2972,6 +3130,7 @@ export async function run(flags, occurrences) {
|
|
|
2972
3130
|
autoApprove = parseAutoApproveFlag(process.argv.slice(3));
|
|
2973
3131
|
imageRigOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
|
|
2974
3132
|
voices = parseVoicesFlag(occurrences, defaultDeps.readFile);
|
|
3133
|
+
voiceTreatment = parseVoiceTreatmentFlag(occurrences, defaultDeps.readFile);
|
|
2975
3134
|
}
|
|
2976
3135
|
catch (e) {
|
|
2977
3136
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -2983,6 +3142,7 @@ export async function run(flags, occurrences) {
|
|
|
2983
3142
|
autoApprove,
|
|
2984
3143
|
imageRigOverrides,
|
|
2985
3144
|
voices,
|
|
3145
|
+
voiceTreatment,
|
|
2986
3146
|
wait: flags["wait"] === true,
|
|
2987
3147
|
json,
|
|
2988
3148
|
out: flagString(flags, "out"),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const COPY_FACE_MODULE_KEY = "copy-face";
|
|
2
|
+
export interface RunNameFields {
|
|
3
|
+
workflowName?: string;
|
|
4
|
+
runTitle?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
moduleKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runDisplayName(run: RunNameFields): string;
|
|
9
|
+
export declare function runTypeWord(run: RunNameFields): string;
|
|
10
|
+
export declare function runHeadingWord(run: RunNameFields): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const COPY_FACE_MODULE_KEY = "copy-face";
|
|
2
|
+
export function runDisplayName(run) {
|
|
3
|
+
return run.title || run.runTitle || run.workflowName || "";
|
|
4
|
+
}
|
|
5
|
+
export function runTypeWord(run) {
|
|
6
|
+
return run.moduleKey === COPY_FACE_MODULE_KEY ? "Copy run" : "Workflow";
|
|
7
|
+
}
|
|
8
|
+
export function runHeadingWord(run) {
|
|
9
|
+
return run.moduleKey === COPY_FACE_MODULE_KEY ? "Copy run" : "Workflow run";
|
|
10
|
+
}
|