@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.
@@ -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 to replace it (they've got better winning ads, the product changed). A rebuild *replaces* the existing primer.
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 1: …
72
- --- Ad 2: …
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` rebuilds in place (replaces it) rather than prompting — which is what you want when the user has explicitly asked to rebuild.
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. Video is admin-only: "video isn't enabled for this key" means either the dashboard user lacks the admin role or the run id is wrong, 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`.
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, and the same parks follow. The full line is under
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 admin-only and hidden from the
49
- top-level `--help`; that is expected, not a broken install.
50
- - **Video is admin-only**, and says so two different ways.
51
- - "video isn't enabled for this key", from a `video` verb, has two causes the
52
- CLI cannot tell apart: the user is not an admin on this brand, or the run id
53
- does not exist. Check the id first; if it is right, they need the admin role.
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 — `startRun` refuses any graph
56
- containing a video node for a non-admin (`convex/workflows.ts:6471`).
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 <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--no-music] [--wait]
88
- start from a script, a conceit and a look
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.** Starting the run writes the storyboard and then draws the
166
- reference, cast and scene pictures, and only then pauses. Each picture is a few
167
- cents and each scene draws a few to choose from: about 25 cents on a small test,
168
- and it can approach a dollar on a longer ad. Approving releases the expensive
169
- part: the voices and the clips. So a run that has not paused yet is already
170
- spending a little, and a run still drawing pictures is healthy. `exodus workflow
171
- status` and `exodus video status` both print "This run will pause for your
172
- approval once the frames are ready" on a run that is headed for the gate. Do not
173
- cancel it because the pause has not arrived. `workflow list`, `workflow describe`, `video status`,
174
- `storyboard` and `pull` are free reads; run them as often as you like. Never
175
- start a second run to "retry" without the user asking; tell them what failed
176
- instead.
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-gated on top. Five things worth knowing before you promise a user
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` instead:
616
- it is a picture-only takeover of the frame while the spoken track underneath
617
- keeps running, so it adds no audio and takes no place in the running order. The
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 exactly as long as that scene's voice track plus
883
- a beat of air. A clip that runs longer is trimmed; a clip that runs shorter
884
- holds its last frame for the difference. The narration is never sped up,
885
- slowed down or cut short, so every scene plays at the speed it was recorded,
886
- and the word times are handed on as they were measured.
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 cutaway laid over the
891
- spine at its cued word (falling back to a proportional estimate when the scene
892
- has no word times, and saying which method it used), a narrated scene's voice
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, a clip-less scene becoming a still, everything normalized to 1080×1920
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
- - Flags: `--out <file>`, `--skip 2,5`, `--no-music`. It prints what went in,
899
- what was left out and why, and the upload command.
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. These sidecars are the script's own output — another tool will not
907
- produce them.
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. A cut that is a few
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 }>;