@aicopycoders/exodus 2026.9.2000-beta.9 → 2026.10.1200-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/docs/CLAUDE.md +1 -0
- package/assets/skills/exodus-primer/SKILL.md +10 -5
- package/assets/skills/exodus-video/SKILL.md +203 -47
- package/assets/skills/exodus-video/scripts/first-cut.d.mts +44 -0
- package/assets/skills/exodus-video/scripts/first-cut.mjs +1140 -92
- package/assets/skills/exodus-winners/SKILL.md +13 -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 +101 -0
- package/dist/commands/video-customer.js +158 -0
- package/dist/commands/video.d.ts +169 -9
- package/dist/commands/video.js +877 -87
- 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 +3 -3
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
|
|
@@ -32,7 +32,8 @@ workflow run`. From there the same nodes render, `video status` reads the run,
|
|
|
32
32
|
`video pull` writes the folder, and `video upload` and `approve` finish it.
|
|
33
33
|
|
|
34
34
|
A Show is no longer required. `exodus video start` starts from a script, a
|
|
35
|
-
conceit, and a style
|
|
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
|
|
36
37
|
"Starting a run". An optional `--direction` note is whole-ad rules, and that
|
|
37
38
|
note cannot contain a colon. "The script file" shows the right form and the
|
|
38
39
|
wrong one.
|
|
@@ -45,17 +46,57 @@ family (`familyTree.ts`). Those family names stay the code's names.
|
|
|
45
46
|
## Before anything
|
|
46
47
|
|
|
47
48
|
- `npx @aicopycoders/exodus video --help` and `workflow --help` are the
|
|
48
|
-
authoritative flag lists. The `video` verb is
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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".
|
|
54
59
|
- "Video workflows aren't available on your account. Ask an admin if you need
|
|
55
|
-
video.", from `workflow run`, is unambiguous
|
|
56
|
-
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.
|
|
57
63
|
|
|
58
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`, `--frame`, `--pacing`, `--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`.
|
|
59
100
|
- Other failures: `npx @aicopycoders/exodus doctor` first, then follow what it prints.
|
|
60
101
|
- `ffprobe` on the PATH lets `upload` read the cut's length by itself. Without
|
|
61
102
|
it, `upload` falls back to parsing an MP4's own header, and for anything else
|
|
@@ -84,8 +125,23 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
84
125
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
85
126
|
same, with the video model speaking voices you WROTE
|
|
86
127
|
exodus video status <runId> where it is, per scene
|
|
87
|
-
exodus video start --script <file> --conceit <
|
|
88
|
-
start from a script
|
|
128
|
+
exodus video start --script <file> --conceit <kind> --style <look> [--direction "…"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--review-storyboard] [--director] [--wait]
|
|
129
|
+
start from a script and the kinds the server offers.
|
|
130
|
+
No music bed unless you pass --music.
|
|
131
|
+
--frame pov (conversation conceits) and --pacing
|
|
132
|
+
(presenter conceits) change the shots; a conceit
|
|
133
|
+
that can't follow one refuses it.
|
|
134
|
+
The app approves its own storyboard when its
|
|
135
|
+
checks pass and goes straight on to the clips,
|
|
136
|
+
which is the part that costs. It stops at the
|
|
137
|
+
gate only when a check finds a problem, and says
|
|
138
|
+
why. --review-storyboard always stops there.
|
|
139
|
+
Each conceit has its own video model and voice;
|
|
140
|
+
--video-model / --voice override them for one run.
|
|
141
|
+
--director turns the trial director on (admins only)
|
|
142
|
+
exodus video director <runId> on|off|follow the trial director for one run (admins only);
|
|
143
|
+
follow = the brand's own setting. status shows
|
|
144
|
+
what it is doing and its last three notes
|
|
89
145
|
```
|
|
90
146
|
|
|
91
147
|
**From the storyboard on.**
|
|
@@ -96,9 +152,10 @@ exodus video voices <runId> who speaks and with wh
|
|
|
96
152
|
exodus video voices <runId> --set <character>=<voiceId> give a character a voice
|
|
97
153
|
exodus video approve <runId> keep going
|
|
98
154
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
155
|
+
(a beta tester leaves --node off)
|
|
99
156
|
exodus video status <runId> where it is, per scene
|
|
100
157
|
exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
|
|
101
|
-
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
|
|
158
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"] [--voice-first]
|
|
102
159
|
redo one clip at the final watch
|
|
103
160
|
exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
|
|
104
161
|
… make the cut (your choice of tool)
|
|
@@ -135,7 +192,11 @@ under it:
|
|
|
135
192
|
lines under `guidance`). It is also
|
|
136
193
|
the one park where a finished clip can be redone, with `retry-clip`. A run
|
|
137
194
|
only reaches this park if its video node sets `finalWatch: true` — otherwise
|
|
138
|
-
it skips straight to finished.
|
|
195
|
+
it skips straight to finished. If a scene has no clip, the stop block names
|
|
196
|
+
it instead (`stop.missingScenes` in `--json`) and approval is refused until
|
|
197
|
+
it has one. A scene whose picture the video service refused needs
|
|
198
|
+
`retry-frame --note` before `retry-clip`; the clip redo alone resends the
|
|
199
|
+
refused picture.
|
|
139
200
|
- paused: a builder checkpoint neither video verb resolves —
|
|
140
201
|
`exodus workflow checkpoint <runId>` is the verb for that one.
|
|
141
202
|
- failed: `status` shows how far it got, and a `--wait` that was running exits
|
|
@@ -162,18 +223,31 @@ minutes with "Still running after 60 minutes" and exit 0; that is not done, so
|
|
|
162
223
|
check `status` before treating it as finished. Do not poll `status` in a tight
|
|
163
224
|
loop; a full run is several minutes.
|
|
164
225
|
|
|
165
|
-
**What costs money.**
|
|
166
|
-
reference, cast and scene pictures
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
**What costs money.** Every run writes the storyboard first and then draws the
|
|
227
|
+
reference, cast and scene pictures. Each picture is a few cents and each scene
|
|
228
|
+
draws a few to choose from: about 25 cents on a small test, and it can approach
|
|
229
|
+
a dollar on a longer ad. The expensive part comes after the storyboard: the
|
|
230
|
+
voices and the clips. What happens at that point depends on how the run
|
|
231
|
+
started.
|
|
232
|
+
|
|
233
|
+
- A paste-a-script run (`exodus video start --conceit …`) checks its own
|
|
234
|
+
storyboard once the pictures are drawn. When every check passes, the app
|
|
235
|
+
approves the storyboard itself and goes straight on to the voices and the
|
|
236
|
+
clips, with no stop. It stops for the user only when a check finds a problem,
|
|
237
|
+
and the stop block says why in plain words. Started with
|
|
238
|
+
`--review-storyboard`, it always stops at the storyboard instead. `status`
|
|
239
|
+
prints "the app checks the storyboard and approves it itself if the checks
|
|
240
|
+
pass" on a run headed that way.
|
|
241
|
+
- A Show run (`video start --show`) and a saved workflow run (`workflow run`)
|
|
242
|
+
still pause at the storyboard for approval. Approving is what releases the
|
|
243
|
+
voices and the clips. `status` prints "This run will pause for your approval
|
|
244
|
+
once the frames are ready" on a run headed for that pause.
|
|
245
|
+
|
|
246
|
+
Either way, a run still drawing pictures is healthy and already spending a
|
|
247
|
+
little. Do not cancel it because nothing has happened yet. `workflow list`,
|
|
248
|
+
`workflow describe`, `video status`, `storyboard` and `pull` are free reads; run
|
|
249
|
+
them as often as you like. Never start a second run to "retry" without the user
|
|
250
|
+
asking; tell them what failed instead.
|
|
177
251
|
|
|
178
252
|
**`--auto-approve` does not cover either video gate.** It releases Checkpoint
|
|
179
253
|
boxes only (`convex/workflows.ts:7266` keys on `pauseReason === "checkpoint"`),
|
|
@@ -185,8 +259,8 @@ on its own.
|
|
|
185
259
|
## What the CLI can and cannot do
|
|
186
260
|
|
|
187
261
|
Verified against the CLI and the server, not assumed. Everything here is
|
|
188
|
-
admin
|
|
189
|
-
anything:
|
|
262
|
+
about the admin tools; a beta tester has only what "Beta testers" lists. Five
|
|
263
|
+
things worth knowing before you promise a user anything:
|
|
190
264
|
|
|
191
265
|
- **A workflow only stops for your cut if its video node asks to.** `finalWatch`
|
|
192
266
|
defaults to `false` (`convex/lib/workflow/catalog.ts:780`), and the run only
|
|
@@ -558,6 +632,20 @@ manifest.json the index below
|
|
|
558
632
|
runId, pulledAt, dashboardUrl the run and the page that opens it:
|
|
559
633
|
/workflows/<workflowId>/runs/<runId>
|
|
560
634
|
storyboard, reference, music filenames, or null when not delivered
|
|
635
|
+
musicBed on | off | unknown — whether the RUN has a
|
|
636
|
+
music bed, which is not the same question
|
|
637
|
+
as the `music` filename above. "unknown" is
|
|
638
|
+
a run made before the run carried the
|
|
639
|
+
answer; it is not "off"
|
|
640
|
+
musicHeardScenes[] scene numbers whose clip came back with
|
|
641
|
+
music the video model baked in. A run with
|
|
642
|
+
the bed off can still list scenes here, and
|
|
643
|
+
those are the ones to listen to
|
|
644
|
+
pendingRedo[] scene numbers the app is redoing right
|
|
645
|
+
now. Their clip and words are left out,
|
|
646
|
+
because the file on the run is the take
|
|
647
|
+
being replaced: run `exodus video wait`,
|
|
648
|
+
then pull again
|
|
561
649
|
narration { file, timing }, or null on a per-scene run
|
|
562
650
|
cast[] one per identity still
|
|
563
651
|
characterId, name from the run's cast lock; null for an
|
|
@@ -590,6 +678,9 @@ scenes[] one per scene, in order
|
|
|
590
678
|
on record when the voice pass replaced the
|
|
591
679
|
clip with a new file; null when the clip IS
|
|
592
680
|
that file, or there is no clip
|
|
681
|
+
voiceMode "voice-first" when the clip was redone voice
|
|
682
|
+
first (its line recorded, then the clip made
|
|
683
|
+
to match it); null otherwise
|
|
593
684
|
provenance which saved rulebook this run followed:
|
|
594
685
|
{format: {via, nodeId, rigId, rigName,
|
|
595
686
|
rulesFrom, specVersion}}. Absent when the
|
|
@@ -612,9 +703,12 @@ depends on the scene; see "Which scenes carry their voice where" below.
|
|
|
612
703
|
terms. `script[]` is the ad's spoken words in order — `{lineId, speakerId, text}`,
|
|
613
704
|
with `speakerId: null` meaning narrator VO. Each scene carries `sceneIndex`,
|
|
614
705
|
`durationSec`, `dialogue[]`, `voText`, `notes`, and `lineIds` (the script lines
|
|
615
|
-
that scene renders). A scene with `kind: "cutaway"` carries `cueLineId`
|
|
616
|
-
|
|
617
|
-
keeps running, so
|
|
706
|
+
that scene renders). A scene with `kind: "cutaway"` carries `cueLineId` and a
|
|
707
|
+
`cutawayType` instead. Most cutaways are a picture-only takeover of the frame
|
|
708
|
+
while the spoken track underneath keeps running, so they add no audio and take
|
|
709
|
+
no place in the running order. A `cutawayType: "reaction"` cutaway is the room
|
|
710
|
+
reacting out loud to its cue line: it is its own short beat after that line,
|
|
711
|
+
heard with its own sound while the speaker is silent. The
|
|
618
712
|
envelope also carries `overlays[]` (each with a `cueLineId` and a `placement`)
|
|
619
713
|
and `audio` (`roomTone`, `music`, `sfxCues[]`) — the planner's notes on what
|
|
620
714
|
should sit on top. The image and motion prompts (`framePrompt`, `videoPrompt`,
|
|
@@ -734,6 +828,7 @@ is no second run, no new storyboard and no re-render of anything else.
|
|
|
734
828
|
exodus video retry-clip <runId> --scene 3
|
|
735
829
|
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
736
830
|
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
831
|
+
exodus video retry-clip <runId> --scene 3 --voice-first
|
|
737
832
|
```
|
|
738
833
|
|
|
739
834
|
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
@@ -756,7 +851,17 @@ revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
|
756
851
|
scene's motion prompt only (`withRedoDirection`,
|
|
757
852
|
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
758
853
|
the approved storyboard. Asking for different words in a note will not change
|
|
759
|
-
them. If the words are wrong, that is a new run.
|
|
854
|
+
them. If the script's words are wrong, that is a new run.
|
|
855
|
+
|
|
856
|
+
**`--voice-first` fixes a clip that said its line wrong** (#2299). It is for a
|
|
857
|
+
clip whose findings include `speech-mismatch`, `speech-repeat`,
|
|
858
|
+
`speech-after-line`, `speech-extra-words` or `speech-long-pause`. The scene's line is recorded in each
|
|
859
|
+
speaker's pinned ElevenLabs voice, and MiniMax (`minimax-h3/reference-to-video`)
|
|
860
|
+
makes the clip to match the recording. Only that clip changes; the word check
|
|
861
|
+
and the other checks still run on it. It needs an ElevenLabs key and a voice for
|
|
862
|
+
every speaker in the scene, and it is refused for a scene with no spoken line.
|
|
863
|
+
The clip then reads `clip: redone voice first` in `status`, carries
|
|
864
|
+
`voiceMode: "voice-first"` in the manifest, and is listed in the `pull` summary.
|
|
760
865
|
|
|
761
866
|
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
762
867
|
clip the row stays `done` with the ORIGINAL take and only its findings are
|
|
@@ -868,8 +973,19 @@ Read these out of `storyboard.json` before you build anything, in any tool:
|
|
|
868
973
|
running order. To find the moment, locate the A-roll scene whose `lineIds`
|
|
869
974
|
contain the cue line, then read the start time of the first word of that line
|
|
870
975
|
out of that scene's `words.json`.
|
|
976
|
+
- **Reactions.** A cutaway with `cutawayType: "reaction"` is the exception. Do
|
|
977
|
+
not lay it over the speech. Put it into the running order as its own beat
|
|
978
|
+
right after its cue line ends, play its own sound (the audience laughing),
|
|
979
|
+
keep it to about 1.5 s, then carry on with the speaker. Nobody talks during
|
|
980
|
+
it. When more lines follow the cue line in the same scene, cut that scene
|
|
981
|
+
where the cue line's last word ends in its `words.json` and put the reaction
|
|
982
|
+
between the two halves.
|
|
871
983
|
- **Overlays and SFX.** `overlays[]` and `audio.sfxCues[]` are cued the same
|
|
872
984
|
way, by `cueLineId`. Nothing renders them for you.
|
|
985
|
+
- **Jump cuts.** When `manifest.json` has `shots.pacing: "jump-cuts"`, every
|
|
986
|
+
scene was filmed from the same framing on purpose. Punch in about 15% on
|
|
987
|
+
every other cut between one speaker's scenes, so each cut visibly moves in
|
|
988
|
+
or out. The bundled script does this for you.
|
|
873
989
|
- **Captions.** Nothing is burned into any clip. Captions belong in post; the
|
|
874
990
|
word timings are what an editor imports to build them.
|
|
875
991
|
|
|
@@ -879,36 +995,76 @@ State this to the user before you run it, so they know what the cut does with
|
|
|
879
995
|
the pieces:
|
|
880
996
|
|
|
881
997
|
- **It cuts the picture to the voice, and never the other way round.** A
|
|
882
|
-
narrated scene's segment runs
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
998
|
+
narrated scene's segment runs to the narrator's last word plus a beat of air:
|
|
999
|
+
about 0.25 s before the next scene, 0.5 s when it ends the ad. The last word
|
|
1000
|
+
comes from the voice track's own word timings; when the scene has none, the
|
|
1001
|
+
end of the voice file stands in for it. A clip that runs longer is trimmed; a
|
|
1002
|
+
clip that runs shorter holds its last frame for the difference. The narration
|
|
1003
|
+
is never sped up or slowed down, so every scene plays at the speed it was
|
|
1004
|
+
recorded, and the word times are handed on as they were measured. The voice
|
|
1005
|
+
file is cut only in the silence after its last word, never inside a word.
|
|
1006
|
+
- **It closes the dead air at the joins.** A clip that plays its own dialogue
|
|
1007
|
+
keeps about 0.15 s after its last word and about 0.1 s before its first (0.25 s
|
|
1008
|
+
when a different speaker answers), read from its `words.json`, so one clip
|
|
1009
|
+
runs into the next like one conversation. It only ever shortens a clip's
|
|
1010
|
+
silence and never cuts into a word. The ad's first clip also opens about 0.1 s
|
|
1011
|
+
before its first word, so a hum a clip makes before anyone speaks is not the
|
|
1012
|
+
first thing heard. The clip that ends the ad keeps about 0.5 s after its last
|
|
1013
|
+
word, the same beat a narrator line gets there, or longer when the storyboard
|
|
1014
|
+
marks an action after the line. A narrated scene, a still and a reaction beat
|
|
1015
|
+
keep their length.
|
|
1016
|
+
- **It shortens long pauses between sentences inside one clip.** A silence over
|
|
1017
|
+
0.6 s between two sentences is cut down to about 0.5 s, picture and sound
|
|
1018
|
+
together, unless the script writes the pause in (an ellipsis or a dash), a
|
|
1019
|
+
direction asks for one, or a reaction's laugh follows the line.
|
|
1020
|
+
- **It fades the audience reaction in and out.** A reaction beat fades in
|
|
1021
|
+
across any quiet start before its laugh (a generated laugh clip can hum
|
|
1022
|
+
there). When a line follows, the picture moves on early and the laugh's own
|
|
1023
|
+
remaining sound plays on under that line, quieter and fading out: up to
|
|
1024
|
+
0.75 s, and the beat keeps at least 1 s of picture (a 1.5 s clip shows 1 s
|
|
1025
|
+
and carries its last 0.5 s). A beat that ends the ad, or has under 0.4 s to
|
|
1026
|
+
carry, fades out over its last 0.4 s instead.
|
|
887
1027
|
- **It ignores the continuous master.** It reads only `scene-NN.voice` files;
|
|
888
1028
|
it never opens `narration.mp3` or `manifest.narration`. A tool that lays the
|
|
889
1029
|
master down whole and cuts the pictures to it keeps the read intact.
|
|
890
|
-
- Otherwise: A-roll in manifest order as the spine, each
|
|
891
|
-
|
|
892
|
-
|
|
1030
|
+
- Otherwise: A-roll in manifest order as the spine, each reaction put in as its
|
|
1031
|
+
own beat of at most 1.5 s right after its cue line with its own sound, each
|
|
1032
|
+
other cutaway laid over the spine at its cued word (falling back to a
|
|
1033
|
+
proportional estimate when the scene has no word times, and saying which
|
|
1034
|
+
method it used), a narrated scene's voice
|
|
893
1035
|
replacing whatever the clip recorded, a dialogue scene keeping its performed
|
|
894
|
-
line,
|
|
1036
|
+
line, everything normalized to 1080×1920
|
|
895
1037
|
at 24 fps and −16 LUFS per segment, a generated room-tone bed under the whole
|
|
896
1038
|
ad, and the music bed underneath at low level. Output is H.264 + AAC in an MP4
|
|
897
1039
|
with `-movflags +faststart`.
|
|
898
|
-
-
|
|
899
|
-
|
|
1040
|
+
- **It stops when a scene has no clip.** A scene the storyboard plans that is
|
|
1041
|
+
still being made, never started, or failed stops the cut, which names each
|
|
1042
|
+
one. Wait for the clip and pull again, leave the scene out with `--skip`, or
|
|
1043
|
+
pass `--still-fill` to hold that scene's keyframe on screen instead: under its
|
|
1044
|
+
voice track if it has one, else silent for its planned length. The still is
|
|
1045
|
+
only ever made when asked for, because a run of them reads as a broken ad.
|
|
1046
|
+
- Flags: `--out <file>`, `--skip 2,5`, `--no-music`, `--still-fill`. It prints what went in,
|
|
1047
|
+
what was left out and why, and the upload command. When a clip hums or
|
|
1048
|
+
clicks where nobody speaks at an edge the cut plays, it lists each one under
|
|
1049
|
+
"Sound at the edges" with its time in the cut. That is a warning to listen
|
|
1050
|
+
there; the cut is made anyway.
|
|
900
1051
|
- Beside the MP4 it writes `cut.words.json` (every spoken word of the finished
|
|
901
1052
|
ad with its start and end) and `cut.srt` (the same words grouped into subtitle
|
|
902
1053
|
lines), named from the `--out` basename. Those times are on the finished ad's
|
|
903
1054
|
timeline, each scene's own word times offset by where that scene starts in the
|
|
904
|
-
cut and otherwise unaltered. Scenes with no word times leave
|
|
1055
|
+
cut, less any lead-in trimmed at the join, and otherwise unaltered. Scenes with no word times leave
|
|
905
1056
|
gaps and are named in the report; when no scene delivered any, neither file is
|
|
906
|
-
written.
|
|
907
|
-
|
|
1057
|
+
written. It always writes `cut.scenes.json` too: where each scene plays in the
|
|
1058
|
+
cut, one `{scene, startSec, seconds}` entry per placement. `exodus video
|
|
1059
|
+
upload` sends `cut.scenes.json` and `cut.words.json` with the cut when they
|
|
1060
|
+
sit beside it, so the customer's edit list shows each scene's time on screen
|
|
1061
|
+
and their editor folder gets the captions. These sidecars are the script's own
|
|
1062
|
+
output — another tool will not produce them.
|
|
908
1063
|
|
|
909
1064
|
Before uploading anything, from any tool, check what you made:
|
|
910
1065
|
`ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 cut.mp4`
|
|
911
|
-
should be close to the sum of the scene durations you kept
|
|
1066
|
+
should be close to the sum of the scene durations you kept (the bundled
|
|
1067
|
+
script's cut runs somewhat shorter, by the silence it trimmed at the joins). A cut that is a few
|
|
912
1068
|
seconds long when five clips went in means a filter dropped inputs; do not
|
|
913
1069
|
upload it.
|
|
914
1070
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// #2988: the types of the exports 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
|
+
/** #3365: hold a scene's keyframe where it has no clip, instead of refusing. Defaults to off. */
|
|
18
|
+
stillFill?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FirstCut {
|
|
22
|
+
out: string;
|
|
23
|
+
durationSec: number;
|
|
24
|
+
/** The music file the cut used, or null. */
|
|
25
|
+
music: string | null;
|
|
26
|
+
timeline: { spine: Array<{ sceneIndex: number }>; left: string[] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function firstCut(options: FirstCutOptions): FirstCut;
|
|
30
|
+
|
|
31
|
+
/** #3365: the scenes the cut cannot be made without, by number. */
|
|
32
|
+
export function scenesWithNoClip(options: {
|
|
33
|
+
manifest: { scenes?: Array<{ sceneIndex: number; clip: string | null }> } | null;
|
|
34
|
+
storyboard: unknown;
|
|
35
|
+
skip: Set<number>;
|
|
36
|
+
exists?: (file: string | null) => boolean;
|
|
37
|
+
}): number[];
|
|
38
|
+
|
|
39
|
+
/** #3424: the sentence-gap cut, which scout's scribe.ts sentenceGapCuts mirrors. */
|
|
40
|
+
export function sentenceGapCuts(
|
|
41
|
+
words: Array<{ w: string; s: number; e: number }>,
|
|
42
|
+
lines: Array<{ text: string; asksPause: boolean; cuesBeat: boolean }>,
|
|
43
|
+
levels?: { windowSec: number; db: number[] } | null,
|
|
44
|
+
): Array<{ s: number; e: number; after: string; gap: number; kept: number; sound: boolean }>;
|