@aicopycoders/exodus 2026.9.2000-beta.8 → 2026.9.2000
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 -0
- package/assets/skills/exodus-primer/SKILL.md +10 -5
- package/assets/skills/exodus-video/SKILL.md +265 -51
- package/assets/skills/exodus-video/scripts/first-cut.d.mts +27 -0
- package/assets/skills/exodus-video/scripts/first-cut.mjs +766 -59
- package/assets/skills/exodus-winners/SKILL.md +13 -1
- package/assets/skills/exodus-workflow/SKILL.md +1 -1
- package/assets/skills/swipe/SKILL.md +78 -0
- package/dist/commands/ads.js +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/primer.js +84 -12
- package/dist/commands/video-customer.d.ts +98 -0
- package/dist/commands/video-customer.js +147 -0
- package/dist/commands/video.d.ts +156 -5
- package/dist/commands/video.js +945 -63
- package/dist/commands/whoami.js +2 -1
- package/dist/commands/workflow.d.ts +2 -1
- package/dist/commands/workflow.js +16 -2
- package/dist/lib/meta-ads.d.ts +1 -0
- package/package.json +1 -1
package/assets/docs/CLAUDE.md
CHANGED
|
@@ -83,6 +83,7 @@ These are not pipelines. They are Luke's methods written as skills, so the think
|
|
|
83
83
|
| `find-competitors` | Who is actually strong in a niche, on Meta first and Amazon second. Ranked competitor sheet and a watch list. Optional scripts for Atria and the Meta Ad Library via Apify (each needs its own key; they bill per result and say the plan back before spending). |
|
|
84
84
|
| `patterns-and-gaps` | What repeats and matters across own ads, competitor ads, organic content, and research, read layer by layer, and the three kinds of gap. |
|
|
85
85
|
| `anatomy-of-ads` | The parts list of an ad (Messaging × Creative). Write a brief, dissect any ad into the same parts, tag a batch so patterns can be counted. Also the vocabulary the other skills use. |
|
|
86
|
+
| `swipe` | Swipe a winning ad for the active brand at one of three closeness levels (1 same scene, 2 new scene same beats, 3 same bones new drama). Gathers the ad, product, and level, then calls the `swipe-narrative-agent` Genesis bot (needs `GENESIS_API_KEY` plus a provider key). |
|
|
86
87
|
| `copy-instincts` | See, Attract, Make. Run on a draft that already exists (hook, ad, mechanism paragraph) to take it from good to great. |
|
|
87
88
|
| `bot-builder` | Build a custom bot the way Luke and Mario build the Genesis bots (SEAK), and put it where it runs: an Exodus workflow Prompt node, a Genesis bot, or a skill. |
|
|
88
89
|
| `remember-that` | Bank one reaction mid-session ("remember that", "never do X") into `STANDARDS.md`, and push approved winning lines to the matching Exodus bank. |
|
|
@@ -52,7 +52,7 @@ Cheap, one query. It tells you whether this brand already has a primer.
|
|
|
52
52
|
- **✗ Not ready** — no primer yet. Proceed to Step 1.
|
|
53
53
|
- **✓ Ready** — a primer is already saved. **Don't rebuild blind.** Surface it and offer the lighter moves first:
|
|
54
54
|
- `npx @aicopycoders/exodus primer show` — print the saved primer so the user can see what's there.
|
|
55
|
-
- Rebuild only if the user explicitly wants
|
|
55
|
+
- Rebuild only if the user explicitly wants a newer one (they've got better winning ads, the product changed). A rebuild adds a new primer and makes it your default; the old one stays in your list.
|
|
56
56
|
|
|
57
57
|
This follows the same "check first, don't re-ask" discipline as the `exodus-foundation` skill. Use the status result to decide what to do — don't open by asking the user where they want to start.
|
|
58
58
|
|
|
@@ -67,11 +67,16 @@ WHO IT'S FOR:
|
|
|
67
67
|
OFFER / PRICE (optional):
|
|
68
68
|
PROOF / NUMBERS (optional):
|
|
69
69
|
|
|
70
|
-
WINNING ADS (paste 10, full copy; fewer is OK — tell me if you have none)
|
|
71
|
-
--- Ad
|
|
72
|
-
--- Ad
|
|
70
|
+
WINNING ADS (paste 10, full copy; fewer is OK — tell me if you have none).
|
|
71
|
+
Paste each ad on the lines under its "--- Ad N:" line, and leave that line as it is:
|
|
72
|
+
--- Ad 1:
|
|
73
|
+
(ad 1, full copy)
|
|
74
|
+
--- Ad 2:
|
|
75
|
+
(ad 2, full copy)
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
Leave nothing after "Ad 1:" — no name or title. The builder names every ad itself, and text on that line would have to be guessed as either a name or the ad's first line.
|
|
79
|
+
|
|
75
80
|
State plainly that the product facts matter as much as the ads — the winning ads frequently don't name the product, and you will **not** invent it. If the user says they have *no* winning ads, stop here and jump to the **No-Ads Branch** below; don't push them to manufacture examples.
|
|
76
81
|
|
|
77
82
|
### Step 2 — Assemble the submission
|
|
@@ -86,7 +91,7 @@ npx @aicopycoders/exodus primer --file state/primer-submission.md --yes
|
|
|
86
91
|
|
|
87
92
|
The build reads every ad and writes the full module taxonomy. **The `--yes` is load-bearing in Claude Code:** without it, the final "accept & save" prompt silently no-ops in the non-TTY shell and the primer is never saved (the symptom that forced a `printf 'a\n' |` hack before this flag existed). With `--yes`, the command builds and saves in one shot and prints the same `✓ saved` / `✓ ready` output.
|
|
88
93
|
|
|
89
|
-
If a primer already exists, `--yes`
|
|
94
|
+
If a primer already exists, `--yes` builds and saves without prompting — which is what you want when the user has explicitly asked to rebuild. It adds a new primer and makes it your default; the old one stays in your list.
|
|
90
95
|
|
|
91
96
|
### Step 4 — Verify
|
|
92
97
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: exodus-video
|
|
3
|
-
description: Make a video ad with Exodus from the terminal. The main path is script-first: run a saved video workflow with `npx @aicopycoders/exodus workflow run`. You read and approve the storyboard, pull every finished piece (per-scene clips, the narration track and per-scene voice, keyframes, cast identity stills, word timings, the music bed) into a folder with `exodus video pull`, and upload a finished cut for approval. Exodus makes the pieces; the cut is made outside it, in whatever editor the user wants — this skill hands over the pieces and the facts about them, it does not prescribe an editor. Use it whenever the user has invoked Exodus and wants anything to do with a video ad, an ad run's clips or storyboard, "pull the pieces", "stitch it together", "make the cut", "upload my cut", or checking on a video run ("exodus, make a video ad from this script", "exodus, is my video run done", "exodus, pull the clips for run X and cut them", "exodus, what's wrong with scene 3", "exodus video status"). Also use it when the user ran an `npx @aicopycoders/exodus video` command or the `exodus` hub skill routed here.
|
|
3
|
+
description: Make a video ad with Exodus from the terminal. The main path is script-first: run a saved video workflow with `npx @aicopycoders/exodus workflow run`. You read and approve the storyboard, pull every finished piece (per-scene clips, the narration track and per-scene voice, keyframes, cast identity stills, word timings, the music bed) into a folder with `exodus video pull`, and upload a finished cut for approval. Exodus makes the pieces; the cut is made outside it, in whatever editor the user wants — this skill hands over the pieces and the facts about them, it does not prescribe an editor. Use it whenever the user has invoked Exodus and wants anything to do with a video ad, an ad run's clips or storyboard, "pull the pieces", "stitch it together", "make the cut", "upload my cut", or checking on a video run ("exodus, make a video ad from this script", "exodus, is my video run done", "exodus, pull the clips for run X and cut them", "exodus, what's wrong with scene 3", "exodus video status"). Also use it when the user ran an `npx @aicopycoders/exodus video` command or the `exodus` hub skill routed here. Admins get every video command; beta testers with video switched on get start, status, storyboard, approve, voices, retry-frame, pull and upload on their own brands (see "Beta testers"). "video isn't enabled for this key" means the run id is wrong, the command is admin-only, or video isn't switched on for that dashboard user, so check the id and then say so; do not retry. Never claim generic video-editing asks ("edit this mp4", "add captions to my reel") without Exodus context; in shared folders those belong to the user's other tools. Running or authoring workflows generally is `exodus-workflow`; static image ads are `exodus-image`; copy is `exodus-write`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Video: make the pieces, cut the ad, hand it back
|
|
@@ -21,26 +21,82 @@ Your job in this skill is to drive that loop, hand over the pieces with the
|
|
|
21
21
|
facts about them, and make whatever cut the user asked for. **How to edit is the
|
|
22
22
|
user's call, not this skill's** — see "Cutting the ad".
|
|
23
23
|
|
|
24
|
+
This file ships on the member's machine. It holds commands, the manifest's
|
|
25
|
+
shape, and recipes for the cut. It does not hold prompt text, house primers,
|
|
26
|
+
judge wording, or model instructions.
|
|
27
|
+
|
|
24
28
|
## How a run starts
|
|
25
29
|
|
|
26
30
|
The ad is a saved workflow in the brand, started script-first with `exodus
|
|
27
31
|
workflow run`. From there the same nodes render, `video status` reads the run,
|
|
28
32
|
`video pull` writes the folder, and `video upload` and `approve` finish it.
|
|
29
33
|
|
|
34
|
+
A Show is no longer required. `exodus video start` starts from a script, a
|
|
35
|
+
conceit, and a style. The same parks follow, except that the app approves the
|
|
36
|
+
storyboard itself when its checks pass (see "What costs money"). The full line is under
|
|
37
|
+
"Starting a run". An optional `--direction` note is whole-ad rules, and that
|
|
38
|
+
note cannot contain a colon. "The script file" shows the right form and the
|
|
39
|
+
wrong one.
|
|
40
|
+
|
|
41
|
+
The Rig box on that workflow is where the run's Conceit and Style come from.
|
|
42
|
+
Conceit is the family plus Wrapper, Narrator Person, and Delivery. Style is the
|
|
43
|
+
look. Narrated Story is the voice-over family and Acted Story is the dialogue
|
|
44
|
+
family (`familyTree.ts`). Those family names stay the code's names.
|
|
45
|
+
|
|
30
46
|
## Before anything
|
|
31
47
|
|
|
32
48
|
- `npx @aicopycoders/exodus video --help` and `workflow --help` are the
|
|
33
|
-
authoritative flag lists. The `video` verb is
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
authoritative flag lists. The `video` verb is hidden from the top-level
|
|
50
|
+
`--help` because most members can't use it; that is expected, not a broken
|
|
51
|
+
install.
|
|
52
|
+
- **Who can use video.** Admins get everything in this file. A beta tester
|
|
53
|
+
(video switched on for their dashboard user) gets a smaller set on their own
|
|
54
|
+
brands, listed under "Beta testers" below. Two refusals:
|
|
55
|
+
- "video isn't enabled for this key", from a `video` verb, has three causes
|
|
56
|
+
the CLI cannot tell apart: the run id does not exist on this brand, the
|
|
57
|
+
command is admin-only, or video isn't switched on for this user. Check the
|
|
58
|
+
id first, then check the command against "Beta testers".
|
|
39
59
|
- "Video workflows aren't available on your account. Ask an admin if you need
|
|
40
|
-
video.", from `workflow run`, is unambiguous
|
|
41
|
-
containing a video node for a non-admin
|
|
60
|
+
video.", from `workflow run`, is unambiguous. `startRun` refuses any graph
|
|
61
|
+
containing a video node for a non-admin, beta testers included. A beta
|
|
62
|
+
tester starts with `exodus video start` instead.
|
|
42
63
|
|
|
43
64
|
No flag or retry fixes either one.
|
|
65
|
+
|
|
66
|
+
## Beta testers
|
|
67
|
+
|
|
68
|
+
A beta tester's key gets the same video the dashboard's Video page makes, and
|
|
69
|
+
never how it is made. Every answer to them says `"view": "customer"`, and the
|
|
70
|
+
CLI words it the way the run page does. On their own brands they can:
|
|
71
|
+
|
|
72
|
+
- `video start --script <file> --conceit <key> --style <slug>` with
|
|
73
|
+
`--direction`, `--music`, `--review-storyboard` and `--wait`. Any other start
|
|
74
|
+
flag is refused with one sentence naming it. The answer is the run id and the
|
|
75
|
+
link to the video's page.
|
|
76
|
+
- `video status <runId>`: where the video is, in plain words (waiting in line,
|
|
77
|
+
planning, drawing, storyboard ready for review, filming, ready with its link,
|
|
78
|
+
or stopped and why). `--json` prints the server's answer as it came.
|
|
79
|
+
- `video storyboard <runId>`: each scene's line and picture, while the
|
|
80
|
+
storyboard waits for review.
|
|
81
|
+
- `video approve <runId>`: approves the storyboard. Nothing else.
|
|
82
|
+
- `video voices <runId>`: who speaks, their current voice, and the voices on
|
|
83
|
+
the tester's own ElevenLabs account. `--set <character>=<voiceId>` picks one,
|
|
84
|
+
until the storyboard is approved.
|
|
85
|
+
- `video retry-frame <runId> --scene <n>`: redraws one scene's picture while
|
|
86
|
+
the storyboard waits. No `--node`; the server finds the step.
|
|
87
|
+
- `video pull <runId> --out <dir>`: the finished video (`video.<ext>`), each
|
|
88
|
+
scene's clip (`scene-NN.<ext>`) and voice (`scene-NN.voice.<ext>`), and the
|
|
89
|
+
music (`music.<ext>`), with a `manifest.json` holding only `runId`,
|
|
90
|
+
`pulledAt`, `dashboardUrl`, `scenes` (`index`, `clip`, `voice`), `music` and
|
|
91
|
+
`video`. A file that did not download is `null` there. Nothing else in "The
|
|
92
|
+
handover" below applies to them.
|
|
93
|
+
- `video upload <runId> --file <cut>`: attaches their cut and prints the page
|
|
94
|
+
to watch it on.
|
|
95
|
+
|
|
96
|
+
Admin-only, answered "video isn't enabled for this key" (or, for retry-clip and
|
|
97
|
+
revoice, a sentence saying they are for admins): `director`,
|
|
98
|
+
`status --rejected-draft`, `retry-clip`, `revoice`, and the start flags
|
|
99
|
+
`--video-model`, `--voice`, `--voice-path` and `--director`.
|
|
44
100
|
- Other failures: `npx @aicopycoders/exodus doctor` first, then follow what it prints.
|
|
45
101
|
- `ffprobe` on the PATH lets `upload` read the cut's length by itself. Without
|
|
46
102
|
it, `upload` falls back to parsing an MP4's own header, and for anything else
|
|
@@ -69,6 +125,20 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
69
125
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
70
126
|
same, with the video model speaking voices you WROTE
|
|
71
127
|
exodus video status <runId> where it is, per scene
|
|
128
|
+
exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--review-storyboard] [--director] [--wait]
|
|
129
|
+
start from a script, a conceit and a look.
|
|
130
|
+
No music bed unless you pass --music.
|
|
131
|
+
The app approves its own storyboard when its
|
|
132
|
+
checks pass and goes straight on to the clips,
|
|
133
|
+
which is the part that costs. It stops at the
|
|
134
|
+
gate only when a check finds a problem, and says
|
|
135
|
+
why. --review-storyboard always stops there.
|
|
136
|
+
Each conceit has its own video model and voice;
|
|
137
|
+
--video-model / --voice override them for one run.
|
|
138
|
+
--director turns the trial director on (admins only)
|
|
139
|
+
exodus video director <runId> on|off|follow the trial director for one run (admins only);
|
|
140
|
+
follow = the brand's own setting. status shows
|
|
141
|
+
what it is doing and its last three notes
|
|
72
142
|
```
|
|
73
143
|
|
|
74
144
|
**From the storyboard on.**
|
|
@@ -79,9 +149,10 @@ exodus video voices <runId> who speaks and with wh
|
|
|
79
149
|
exodus video voices <runId> --set <character>=<voiceId> give a character a voice
|
|
80
150
|
exodus video approve <runId> keep going
|
|
81
151
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
152
|
+
(a beta tester leaves --node off)
|
|
82
153
|
exodus video status <runId> where it is, per scene
|
|
83
154
|
exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
|
|
84
|
-
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
|
|
155
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"] [--voice-first]
|
|
85
156
|
redo one clip at the final watch
|
|
86
157
|
exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
|
|
87
158
|
… make the cut (your choice of tool)
|
|
@@ -98,7 +169,15 @@ already printed the same stop block, and `workflow inbox` lists a parked video
|
|
|
98
169
|
run, but neither shows the per-scene detail, and the checkpoint verbs cannot
|
|
99
170
|
resolve a video park — see "What the CLI can and cannot do".
|
|
100
171
|
|
|
101
|
-
**Where a run can stop.**
|
|
172
|
+
**Where a run can stop.** The two video parks come in order when the run has
|
|
173
|
+
both. The storyboard gate is first. You read the cards there and you set the
|
|
174
|
+
voices there. Approving that gate releases the voices and the clips. That is the
|
|
175
|
+
expensive part. The final watch is next, after the pieces exist, and only when
|
|
176
|
+
the video node sets `finalWatch: true`. That park is for the cut. You pull, you
|
|
177
|
+
cut, and you upload. Approving the upload makes it the ad. A node with
|
|
178
|
+
`finalWatch` off skips the second park and the run finishes.
|
|
179
|
+
|
|
180
|
+
Five stops, each with the exact next command printed
|
|
102
181
|
under it:
|
|
103
182
|
|
|
104
183
|
- storyboard gate: needs `approve`. When the stop block prints a `Redo one
|
|
@@ -110,7 +189,11 @@ under it:
|
|
|
110
189
|
lines under `guidance`). It is also
|
|
111
190
|
the one park where a finished clip can be redone, with `retry-clip`. A run
|
|
112
191
|
only reaches this park if its video node sets `finalWatch: true` — otherwise
|
|
113
|
-
it skips straight to finished.
|
|
192
|
+
it skips straight to finished. If a scene has no clip, the stop block names
|
|
193
|
+
it instead (`stop.missingScenes` in `--json`) and approval is refused until
|
|
194
|
+
it has one. A scene whose picture the video service refused needs
|
|
195
|
+
`retry-frame --note` before `retry-clip`; the clip redo alone resends the
|
|
196
|
+
refused picture.
|
|
114
197
|
- paused: a builder checkpoint neither video verb resolves —
|
|
115
198
|
`exodus workflow checkpoint <runId>` is the verb for that one.
|
|
116
199
|
- failed: `status` shows how far it got, and a `--wait` that was running exits
|
|
@@ -137,18 +220,31 @@ minutes with "Still running after 60 minutes" and exit 0; that is not done, so
|
|
|
137
220
|
check `status` before treating it as finished. Do not poll `status` in a tight
|
|
138
221
|
loop; a full run is several minutes.
|
|
139
222
|
|
|
140
|
-
**What costs money.**
|
|
141
|
-
reference, cast and scene pictures
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
223
|
+
**What costs money.** Every run writes the storyboard first and then draws the
|
|
224
|
+
reference, cast and scene pictures. Each picture is a few cents and each scene
|
|
225
|
+
draws a few to choose from: about 25 cents on a small test, and it can approach
|
|
226
|
+
a dollar on a longer ad. The expensive part comes after the storyboard: the
|
|
227
|
+
voices and the clips. What happens at that point depends on how the run
|
|
228
|
+
started.
|
|
229
|
+
|
|
230
|
+
- A paste-a-script run (`exodus video start --conceit …`) checks its own
|
|
231
|
+
storyboard once the pictures are drawn. When every check passes, the app
|
|
232
|
+
approves the storyboard itself and goes straight on to the voices and the
|
|
233
|
+
clips, with no stop. It stops for the user only when a check finds a problem,
|
|
234
|
+
and the stop block says why in plain words. Started with
|
|
235
|
+
`--review-storyboard`, it always stops at the storyboard instead. `status`
|
|
236
|
+
prints "the app checks the storyboard and approves it itself if the checks
|
|
237
|
+
pass" on a run headed that way.
|
|
238
|
+
- A Show run (`video start --show`) and a saved workflow run (`workflow run`)
|
|
239
|
+
still pause at the storyboard for approval. Approving is what releases the
|
|
240
|
+
voices and the clips. `status` prints "This run will pause for your approval
|
|
241
|
+
once the frames are ready" on a run headed for that pause.
|
|
242
|
+
|
|
243
|
+
Either way, a run still drawing pictures is healthy and already spending a
|
|
244
|
+
little. Do not cancel it because nothing has happened yet. `workflow list`,
|
|
245
|
+
`workflow describe`, `video status`, `storyboard` and `pull` are free reads; run
|
|
246
|
+
them as often as you like. Never start a second run to "retry" without the user
|
|
247
|
+
asking; tell them what failed instead.
|
|
152
248
|
|
|
153
249
|
**`--auto-approve` does not cover either video gate.** It releases Checkpoint
|
|
154
250
|
boxes only (`convex/workflows.ts:7266` keys on `pauseReason === "checkpoint"`),
|
|
@@ -160,8 +256,8 @@ on its own.
|
|
|
160
256
|
## What the CLI can and cannot do
|
|
161
257
|
|
|
162
258
|
Verified against the CLI and the server, not assumed. Everything here is
|
|
163
|
-
admin
|
|
164
|
-
anything:
|
|
259
|
+
about the admin tools; a beta tester has only what "Beta testers" lists. Five
|
|
260
|
+
things worth knowing before you promise a user anything:
|
|
165
261
|
|
|
166
262
|
- **A workflow only stops for your cut if its video node asks to.** `finalWatch`
|
|
167
263
|
defaults to `false` (`convex/lib/workflow/catalog.ts:780`), and the run only
|
|
@@ -211,11 +307,42 @@ anything:
|
|
|
211
307
|
|
|
212
308
|
## The script file
|
|
213
309
|
|
|
214
|
-
Plain text.
|
|
215
|
-
`
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
310
|
+
Plain text. An optional `DIRECTION:` block may open the file, before `CAST:` or
|
|
311
|
+
the first spoken line. Then a `CAST:` block naming the speakers, one line per
|
|
312
|
+
turn, then a `CTA:` block.
|
|
313
|
+
|
|
314
|
+
`DIRECTION:` carries whole-ad rules and no spoken words. The header is its own
|
|
315
|
+
line. The lines under it are the rules. The block ends at a blank line, at
|
|
316
|
+
`CAST:`, at `CTA:`, or at the first line that names a speaker. A second
|
|
317
|
+
`DIRECTION:` block is refused. So is a `DIRECTION:` after the first spoken line.
|
|
318
|
+
An empty block is refused too. The error code is `malformed-direction`.
|
|
319
|
+
|
|
320
|
+
Do not put a colon inside direction prose. The reader splits a line at its first
|
|
321
|
+
colon to find a speaker, so a colon in that prose is read as a name.
|
|
322
|
+
|
|
323
|
+
Right.
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
DIRECTION:
|
|
327
|
+
Nobody looks at the camera. The product stays on the table until the last line.
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Wrong.
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
DIRECTION:
|
|
334
|
+
Rule: nobody looks at the camera.
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The wrong line is not direction. `Rule` is read as the speaker, and the
|
|
338
|
+
block above it is empty, so the file is refused.
|
|
339
|
+
|
|
340
|
+
The planner sizes every line at 2.5 words per second (`WORD_LADDER_WPS`), so a
|
|
341
|
+
turn near 14 words plans a clip near 6 seconds. If a line does get truncated in
|
|
342
|
+
its clip, the manifest says so (`speech-cutoff`). That finding means the clip
|
|
343
|
+
cut the words off. Redo the clip. Do not speed the speech or cut it short to
|
|
344
|
+
hide the finding. The product rule is under "What the product actually
|
|
345
|
+
requires".
|
|
219
346
|
|
|
220
347
|
```
|
|
221
348
|
CAST:
|
|
@@ -229,6 +356,15 @@ CTA:
|
|
|
229
356
|
THE SOFTGEL: Tap below and give your eyes ClearBlink.
|
|
230
357
|
```
|
|
231
358
|
|
|
359
|
+
The dash in a `CAST:` line is the parser's own separator. The example above
|
|
360
|
+
already shows that dash. A cast line may separate the name from the description
|
|
361
|
+
with an em dash, an en dash, or a hyphen with a space on each side. Keep that
|
|
362
|
+
dash on cast lines. A reader who strips it makes the line fail.
|
|
363
|
+
|
|
364
|
+
Do not use a long dash in words a character says, or in words shown on screen.
|
|
365
|
+
Synthesised speech reads a long dash oddly, and so does burned-in text. A spoken
|
|
366
|
+
line stays `NAME: what they say`.
|
|
367
|
+
|
|
232
368
|
**How many speakers you may have depends on the rig.** Three by default. A rig
|
|
233
369
|
that carries saved format rules sets its own number: an ensemble rig seats more,
|
|
234
370
|
a talking-head rig seats one. Over the number, the launch refuses before
|
|
@@ -261,9 +397,13 @@ unattended.
|
|
|
261
397
|
By default a workflow run's characters have no voice of their own. A character
|
|
262
398
|
without one gets the run's default voice (the workflow's narrator voice) when the
|
|
263
399
|
run has one; with no default either, the clip keeps the voice the video model
|
|
264
|
-
invents. `exodus video voices` shows which of the three applies and is where
|
|
265
|
-
|
|
266
|
-
|
|
400
|
+
invents. `exodus video voices` shows which of the three applies, and it is where you
|
|
401
|
+
fix the mapping. Fix it before you approve the storyboard gate. Approving that
|
|
402
|
+
gate locks the mapping, and `exodus video voices` refuses afterwards. On a
|
|
403
|
+
finished clip whose voice was never applied, the later command is `exodus video
|
|
404
|
+
revoice`, and that pass bills again. A clip that already went through the voice
|
|
405
|
+
pass is refused there. Use `retry-clip` for that one. See "Redoing only the
|
|
406
|
+
voice on finished clips".
|
|
267
407
|
|
|
268
408
|
```
|
|
269
409
|
exodus video voices <runId>
|
|
@@ -337,7 +477,7 @@ nobody knows the speaker names yet. When the storyboard arrives, the voices are
|
|
|
337
477
|
already set and `exodus video voices <runId>` shows them.
|
|
338
478
|
|
|
339
479
|
**Choosing how the voices are made.** `--voice-treatment` on `workflow run` says
|
|
340
|
-
HOW a run makes its voices, and it is the only
|
|
480
|
+
HOW a run makes its voices, and it is the only way. Two
|
|
341
481
|
kinds of answer.
|
|
342
482
|
|
|
343
483
|
A name on its own picks a way of working that plays a voice you already have,
|
|
@@ -410,7 +550,8 @@ person made at a keyboard for one run, and nobody made it for the sub-workflow.
|
|
|
410
550
|
**Written voices cannot be changed at the review.** They are chosen when the run
|
|
411
551
|
starts. `exodus video voices <runId>` on a described run names the treatment and
|
|
412
552
|
prints each written voice, and says plainly that `--voice-treatment` at launch is
|
|
413
|
-
what changes them. Do not set a voice path in node config
|
|
553
|
+
what changes them. Do not set a voice path in node config. Node config does
|
|
554
|
+
not set the treatment.
|
|
414
555
|
|
|
415
556
|
**Where it shows up afterwards.** The launch receipt and `exodus workflow status`
|
|
416
557
|
both print one line naming the treatment and the speakers who got a written
|
|
@@ -488,6 +629,20 @@ manifest.json the index below
|
|
|
488
629
|
runId, pulledAt, dashboardUrl the run and the page that opens it:
|
|
489
630
|
/workflows/<workflowId>/runs/<runId>
|
|
490
631
|
storyboard, reference, music filenames, or null when not delivered
|
|
632
|
+
musicBed on | off | unknown — whether the RUN has a
|
|
633
|
+
music bed, which is not the same question
|
|
634
|
+
as the `music` filename above. "unknown" is
|
|
635
|
+
a run made before the run carried the
|
|
636
|
+
answer; it is not "off"
|
|
637
|
+
musicHeardScenes[] scene numbers whose clip came back with
|
|
638
|
+
music the video model baked in. A run with
|
|
639
|
+
the bed off can still list scenes here, and
|
|
640
|
+
those are the ones to listen to
|
|
641
|
+
pendingRedo[] scene numbers the app is redoing right
|
|
642
|
+
now. Their clip and words are left out,
|
|
643
|
+
because the file on the run is the take
|
|
644
|
+
being replaced: run `exodus video wait`,
|
|
645
|
+
then pull again
|
|
491
646
|
narration { file, timing }, or null on a per-scene run
|
|
492
647
|
cast[] one per identity still
|
|
493
648
|
characterId, name from the run's cast lock; null for an
|
|
@@ -520,6 +675,9 @@ scenes[] one per scene, in order
|
|
|
520
675
|
on record when the voice pass replaced the
|
|
521
676
|
clip with a new file; null when the clip IS
|
|
522
677
|
that file, or there is no clip
|
|
678
|
+
voiceMode "voice-first" when the clip was redone voice
|
|
679
|
+
first (its line recorded, then the clip made
|
|
680
|
+
to match it); null otherwise
|
|
523
681
|
provenance which saved rulebook this run followed:
|
|
524
682
|
{format: {via, nodeId, rigId, rigName,
|
|
525
683
|
rulesFrom, specVersion}}. Absent when the
|
|
@@ -542,15 +700,19 @@ depends on the scene; see "Which scenes carry their voice where" below.
|
|
|
542
700
|
terms. `script[]` is the ad's spoken words in order — `{lineId, speakerId, text}`,
|
|
543
701
|
with `speakerId: null` meaning narrator VO. Each scene carries `sceneIndex`,
|
|
544
702
|
`durationSec`, `dialogue[]`, `voText`, `notes`, and `lineIds` (the script lines
|
|
545
|
-
that scene renders). A scene with `kind: "cutaway"` carries `cueLineId`
|
|
546
|
-
|
|
547
|
-
keeps running, so
|
|
703
|
+
that scene renders). A scene with `kind: "cutaway"` carries `cueLineId` and a
|
|
704
|
+
`cutawayType` instead. Most cutaways are a picture-only takeover of the frame
|
|
705
|
+
while the spoken track underneath keeps running, so they add no audio and take
|
|
706
|
+
no place in the running order. A `cutawayType: "reaction"` cutaway is the room
|
|
707
|
+
reacting out loud to its cue line: it is its own short beat after that line,
|
|
708
|
+
heard with its own sound while the speaker is silent. The
|
|
548
709
|
envelope also carries `overlays[]` (each with a `cueLineId` and a `placement`)
|
|
549
710
|
and `audio` (`roomTone`, `music`, `sfxCues[]`) — the planner's notes on what
|
|
550
711
|
should sit on top. The image and motion prompts (`framePrompt`, `videoPrompt`,
|
|
551
712
|
`referencePrompt`) reach an admin key in full. A caller who cannot use video on
|
|
552
|
-
this brand gets them stripped before the file arrives
|
|
553
|
-
(`redactVideoIp` in `convex/workflows.ts`).
|
|
713
|
+
this brand gets them stripped before the file arrives. They are private Style
|
|
714
|
+
fragment IP (`redactVideoIp` in `convex/workflows.ts`). Style pack is the
|
|
715
|
+
retired name.
|
|
554
716
|
|
|
555
717
|
**The narration track** (`narration.mp3` + `narration.json`). Present when the
|
|
556
718
|
run recorded continuously: every narrated scene's `voText` joined into one
|
|
@@ -577,9 +739,9 @@ speech inside the clip — under the default voice treatment the video model's
|
|
|
577
739
|
invented voice is then replaced, in place, with the cast member's pinned
|
|
578
740
|
ElevenLabs voice (that is `revoiced: true`). Other treatments leave the render
|
|
579
741
|
owning its own audio, e.g. lip-sync retargeting a VO track onto the video. The
|
|
580
|
-
treatment is frozen on the run when it starts, and there is exactly one
|
|
742
|
+
treatment is frozen on the run when it starts, and there is exactly one flag
|
|
581
743
|
that sets it: `--voice-treatment` on `workflow run` (see **Choosing how the
|
|
582
|
-
voices are made**). Node config
|
|
744
|
+
voices are made**). Node config never sets it. Nothing on the canvas sets a
|
|
583
745
|
voice treatment, and a run that was given none renders the default. Read
|
|
584
746
|
`wordsFrom` and `revoiced` per scene rather than assuming.
|
|
585
747
|
|
|
@@ -641,6 +803,11 @@ blocking the run.
|
|
|
641
803
|
A `speech-cutoff` on the CTA scene is the one to worry about; the ad's last
|
|
642
804
|
words are missing. `status` shows the checker's own wording behind each
|
|
643
805
|
finding; the manifest carries only the verdict.
|
|
806
|
+
- A `pass` verdict is not a review. The checks are often optimistic. Watch the
|
|
807
|
+
frames and listen to the audio before you call the run good. Two checks fail
|
|
808
|
+
the wrong way. A cast-count check can reject a silent
|
|
809
|
+
listener who belongs in the shot. A clip check can flag a clip that is fine.
|
|
810
|
+
Read the finding, then watch the scene.
|
|
644
811
|
- `clipStatus: "failed"` or `"missing"` with a `keyframe`: `durationSec` is null
|
|
645
812
|
(it comes from the clip), but the scene can still hold its place as a still —
|
|
646
813
|
for the length of its voice track, or the storyboard's planned duration when
|
|
@@ -658,6 +825,7 @@ is no second run, no new storyboard and no re-render of anything else.
|
|
|
658
825
|
exodus video retry-clip <runId> --scene 3
|
|
659
826
|
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
660
827
|
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
828
|
+
exodus video retry-clip <runId> --scene 3 --voice-first
|
|
661
829
|
```
|
|
662
830
|
|
|
663
831
|
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
@@ -680,7 +848,17 @@ revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
|
680
848
|
scene's motion prompt only (`withRedoDirection`,
|
|
681
849
|
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
682
850
|
the approved storyboard. Asking for different words in a note will not change
|
|
683
|
-
them. If the words are wrong, that is a new run.
|
|
851
|
+
them. If the script's words are wrong, that is a new run.
|
|
852
|
+
|
|
853
|
+
**`--voice-first` fixes a clip that said its line wrong** (#2299). It is for a
|
|
854
|
+
clip whose findings include `speech-mismatch`, `speech-repeat`,
|
|
855
|
+
`speech-after-line`, `speech-extra-words` or `speech-long-pause`. The scene's line is recorded in each
|
|
856
|
+
speaker's pinned ElevenLabs voice, and MiniMax (`minimax-h3/reference-to-video`)
|
|
857
|
+
makes the clip to match the recording. Only that clip changes; the word check
|
|
858
|
+
and the other checks still run on it. It needs an ElevenLabs key and a voice for
|
|
859
|
+
every speaker in the scene, and it is refused for a scene with no spoken line.
|
|
860
|
+
The clip then reads `clip: redone voice first` in `status`, carries
|
|
861
|
+
`voiceMode: "voice-first"` in the manifest, and is listed in the `pull` summary.
|
|
684
862
|
|
|
685
863
|
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
686
864
|
clip the row stays `done` with the ORIGINAL take and only its findings are
|
|
@@ -767,7 +945,10 @@ they want; if they have not said and are not around, say which you used and why.
|
|
|
767
945
|
- **HyperFrames** — HTML/CSS compositions rendered to video. Suits an ad that
|
|
768
946
|
needs captions, title cards, overlays or motion graphics laid over the clips.
|
|
769
947
|
It is a separate skill pack; check that a `hyperframes` skill is installed
|
|
770
|
-
before offering it.
|
|
948
|
+
before offering it. The cut is made outside Exodus. `exodus video pull` is the
|
|
949
|
+
handover. The folder it writes is what the next tool reads. When a HyperFrames
|
|
950
|
+
skill pack is installed, that pack is the route for an HTML and CSS
|
|
951
|
+
composition. Do not look for a stitch inside Exodus.
|
|
771
952
|
- **An NLE over MCP** — DaVinci Resolve publishes an MCP server, so a timeline
|
|
772
953
|
can be built in a real editor and the user can carry on by hand afterwards.
|
|
773
954
|
Suits a user who wants to keep editing after you stop. Check what MCP servers
|
|
@@ -789,6 +970,13 @@ Read these out of `storyboard.json` before you build anything, in any tool:
|
|
|
789
970
|
running order. To find the moment, locate the A-roll scene whose `lineIds`
|
|
790
971
|
contain the cue line, then read the start time of the first word of that line
|
|
791
972
|
out of that scene's `words.json`.
|
|
973
|
+
- **Reactions.** A cutaway with `cutawayType: "reaction"` is the exception. Do
|
|
974
|
+
not lay it over the speech. Put it into the running order as its own beat
|
|
975
|
+
right after its cue line ends, play its own sound (the audience laughing),
|
|
976
|
+
keep it to about 1.5 s, then carry on with the speaker. Nobody talks during
|
|
977
|
+
it. When more lines follow the cue line in the same scene, cut that scene
|
|
978
|
+
where the cue line's last word ends in its `words.json` and put the reaction
|
|
979
|
+
between the two halves.
|
|
792
980
|
- **Overlays and SFX.** `overlays[]` and `audio.sfxCues[]` are cued the same
|
|
793
981
|
way, by `cueLineId`. Nothing renders them for you.
|
|
794
982
|
- **Captions.** Nothing is burned into any clip. Captions belong in post; the
|
|
@@ -805,31 +993,52 @@ the pieces:
|
|
|
805
993
|
holds its last frame for the difference. The narration is never sped up,
|
|
806
994
|
slowed down or cut short, so every scene plays at the speed it was recorded,
|
|
807
995
|
and the word times are handed on as they were measured.
|
|
996
|
+
- **It closes the dead air at the joins.** A clip that plays its own dialogue
|
|
997
|
+
keeps about 0.15 s after its last word and about 0.1 s before its first (0.25 s
|
|
998
|
+
when a different speaker answers), read from its `words.json`, so one clip
|
|
999
|
+
runs into the next like one conversation. It only ever shortens a clip's
|
|
1000
|
+
silence and never cuts into a word. The ad's first clip also opens about 0.1 s
|
|
1001
|
+
before its first word, so a hum a clip makes before anyone speaks is not the
|
|
1002
|
+
first thing heard. The ad's last tail, a narrated scene, a still and a
|
|
1003
|
+
reaction beat keep their length.
|
|
1004
|
+
- **It fades the audience reaction in and out.** A reaction beat fades in
|
|
1005
|
+
across any quiet start before its laugh (a generated laugh clip can hum
|
|
1006
|
+
there). When a line follows, the picture moves on early and the laugh's own
|
|
1007
|
+
remaining sound plays on under that line, quieter and fading out: up to
|
|
1008
|
+
0.75 s, and the beat keeps at least 1 s of picture (a 1.5 s clip shows 1 s
|
|
1009
|
+
and carries its last 0.5 s). A beat that ends the ad, or has under 0.4 s to
|
|
1010
|
+
carry, fades out over its last 0.4 s instead.
|
|
808
1011
|
- **It ignores the continuous master.** It reads only `scene-NN.voice` files;
|
|
809
1012
|
it never opens `narration.mp3` or `manifest.narration`. A tool that lays the
|
|
810
1013
|
master down whole and cuts the pictures to it keeps the read intact.
|
|
811
|
-
- Otherwise: A-roll in manifest order as the spine, each
|
|
812
|
-
|
|
813
|
-
|
|
1014
|
+
- Otherwise: A-roll in manifest order as the spine, each reaction put in as its
|
|
1015
|
+
own beat of at most 1.5 s right after its cue line with its own sound, each
|
|
1016
|
+
other cutaway laid over the spine at its cued word (falling back to a
|
|
1017
|
+
proportional estimate when the scene has no word times, and saying which
|
|
1018
|
+
method it used), a narrated scene's voice
|
|
814
1019
|
replacing whatever the clip recorded, a dialogue scene keeping its performed
|
|
815
1020
|
line, a clip-less scene becoming a still, everything normalized to 1080×1920
|
|
816
1021
|
at 24 fps and −16 LUFS per segment, a generated room-tone bed under the whole
|
|
817
1022
|
ad, and the music bed underneath at low level. Output is H.264 + AAC in an MP4
|
|
818
1023
|
with `-movflags +faststart`.
|
|
819
1024
|
- Flags: `--out <file>`, `--skip 2,5`, `--no-music`. It prints what went in,
|
|
820
|
-
what was left out and why, and the upload command.
|
|
1025
|
+
what was left out and why, and the upload command. When a clip hums or
|
|
1026
|
+
clicks where nobody speaks at an edge the cut plays, it lists each one under
|
|
1027
|
+
"Sound at the edges" with its time in the cut. That is a warning to listen
|
|
1028
|
+
there; the cut is made anyway.
|
|
821
1029
|
- Beside the MP4 it writes `cut.words.json` (every spoken word of the finished
|
|
822
1030
|
ad with its start and end) and `cut.srt` (the same words grouped into subtitle
|
|
823
1031
|
lines), named from the `--out` basename. Those times are on the finished ad's
|
|
824
1032
|
timeline, each scene's own word times offset by where that scene starts in the
|
|
825
|
-
cut and otherwise unaltered. Scenes with no word times leave
|
|
1033
|
+
cut, less any lead-in trimmed at the join, and otherwise unaltered. Scenes with no word times leave
|
|
826
1034
|
gaps and are named in the report; when no scene delivered any, neither file is
|
|
827
1035
|
written. These sidecars are the script's own output — another tool will not
|
|
828
1036
|
produce them.
|
|
829
1037
|
|
|
830
1038
|
Before uploading anything, from any tool, check what you made:
|
|
831
1039
|
`ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 cut.mp4`
|
|
832
|
-
should be close to the sum of the scene durations you kept
|
|
1040
|
+
should be close to the sum of the scene durations you kept (the bundled
|
|
1041
|
+
script's cut runs somewhat shorter, by the silence it trimmed at the joins). A cut that is a few
|
|
833
1042
|
seconds long when five clips went in means a filter dropped inputs; do not
|
|
834
1043
|
upload it.
|
|
835
1044
|
|
|
@@ -864,6 +1073,11 @@ lines, re-recording narration, or cutting words out of the CTA will ship,
|
|
|
864
1073
|
silently. Cut the pictures to the words, not the words to the pictures. If the
|
|
865
1074
|
words are wrong, that is a new run, not an edit.
|
|
866
1075
|
|
|
1076
|
+
Never stretch speech, speed it up, slow it down, or cut it short so it fits a
|
|
1077
|
+
clip. That is a product rule, and it holds for every tool that makes the cut,
|
|
1078
|
+
including the bundled script. If the audio does not fit, redo the clip with
|
|
1079
|
+
`exodus video retry-clip`. Do not repair the fit in the editor.
|
|
1080
|
+
|
|
867
1081
|
## Reporting to the user
|
|
868
1082
|
|
|
869
1083
|
Lead with where the run is and what it needs from them. Then, when you pulled
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// #2988: the types of the one export the Trigger worker calls. The script is
|
|
2
|
+
// plain JS so the CLI can run it with `node` and no build.
|
|
3
|
+
|
|
4
|
+
export class FirstCutError extends Error {
|
|
5
|
+
constructor(code: 1 | 2, message: string);
|
|
6
|
+
readonly code: 1 | 2;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FirstCutOptions {
|
|
10
|
+
/** A folder written by `exodus video pull`. */
|
|
11
|
+
dir: string;
|
|
12
|
+
/** Defaults to <dir>/cut.mp4. */
|
|
13
|
+
out?: string;
|
|
14
|
+
skip?: Set<number>;
|
|
15
|
+
/** The music bed, when the pull has one. Defaults to on, as the CLI does. */
|
|
16
|
+
music?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FirstCut {
|
|
20
|
+
out: string;
|
|
21
|
+
durationSec: number;
|
|
22
|
+
/** The music file the cut used, or null. */
|
|
23
|
+
music: string | null;
|
|
24
|
+
timeline: { spine: Array<{ sceneIndex: number }>; left: string[] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function firstCut(options: FirstCutOptions): FirstCut;
|