@aicopycoders/exodus 2026.9.2000 → 2026.10.1200-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/exodus-video/SKILL.md +143 -14
- package/assets/skills/exodus-video/scripts/first-cut.d.mts +18 -1
- package/assets/skills/exodus-video/scripts/first-cut.mjs +434 -93
- package/dist/commands/video-customer.d.ts +3 -0
- package/dist/commands/video-customer.js +12 -1
- package/dist/commands/video.d.ts +35 -2
- package/dist/commands/video.js +126 -25
- package/package.json +3 -3
|
@@ -38,6 +38,37 @@ storyboard itself when its checks pass (see "What costs money"). The full line i
|
|
|
38
38
|
note cannot contain a colon. "The script file" shows the right form and the
|
|
39
39
|
wrong one.
|
|
40
40
|
|
|
41
|
+
**The kinds of ad.** `--conceit` takes one of the kinds the server offers. The
|
|
42
|
+
server's list is the truth, and nothing prints it on request. Pass a key it
|
|
43
|
+
doesn't offer and the refusal names every key it does: `video start --conceit
|
|
44
|
+
must be one of …`. Nothing is spent on a refused start. Today the server offers
|
|
45
|
+
15 formats and then Custom:
|
|
46
|
+
|
|
47
|
+
| Key | Kind |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `podcast` | Podcast |
|
|
50
|
+
| `ugc` | UGC talking head |
|
|
51
|
+
| `stage` | Speaking on stage |
|
|
52
|
+
| `street` | Street interview |
|
|
53
|
+
| `personification` | Personification |
|
|
54
|
+
| `news-broadcast` | News Broadcast |
|
|
55
|
+
| `court-tv` | Court TV |
|
|
56
|
+
| `soap-opera` | Soap Opera |
|
|
57
|
+
| `storytime` | Storytime While Doing a Task |
|
|
58
|
+
| `customer-service-call` | Customer Service Call |
|
|
59
|
+
| `debate-circle` | Debate Circle |
|
|
60
|
+
| `talk-show-panel` | Talk Show Panel |
|
|
61
|
+
| `infomercial` | Infomercial |
|
|
62
|
+
| `dating-show` | Dating Show |
|
|
63
|
+
| `game-show` | Game Show |
|
|
64
|
+
| `custom` | Custom |
|
|
65
|
+
|
|
66
|
+
Custom builds the ad from the script itself. A script with no `CAST:` runs as a
|
|
67
|
+
voice-over, and a script with people runs as a scene. Voice-over and Scene are
|
|
68
|
+
not on the menu by name; Custom is the way to them. A narrator-only script fits
|
|
69
|
+
Custom alone. If the refusal lists different keys from this table, the server
|
|
70
|
+
is right and this table is stale.
|
|
71
|
+
|
|
41
72
|
The Rig box on that workflow is where the run's Conceit and Style come from.
|
|
42
73
|
Conceit is the family plus Wrapper, Narrator Person, and Delivery. Style is the
|
|
43
74
|
look. Narrated Story is the voice-over family and Acted Story is the dialogue
|
|
@@ -70,7 +101,7 @@ never how it is made. Every answer to them says `"view": "customer"`, and the
|
|
|
70
101
|
CLI words it the way the run page does. On their own brands they can:
|
|
71
102
|
|
|
72
103
|
- `video start --script <file> --conceit <key> --style <slug>` with
|
|
73
|
-
`--direction`, `--music`, `--review-storyboard` and `--wait`. Any other start
|
|
104
|
+
`--direction`, `--frame`, `--pacing`, `--music`, `--review-storyboard` and `--wait`. Any other start
|
|
74
105
|
flag is refused with one sentence naming it. The answer is the run id and the
|
|
75
106
|
link to the video's page.
|
|
76
107
|
- `video status <runId>`: where the video is, in plain words (waiting in line,
|
|
@@ -125,9 +156,12 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
125
156
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
126
157
|
same, with the video model speaking voices you WROTE
|
|
127
158
|
exodus video status <runId> where it is, per scene
|
|
128
|
-
exodus video start --script <file> --conceit <
|
|
129
|
-
start from a script
|
|
159
|
+
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]
|
|
160
|
+
start from a script and the kinds the server offers.
|
|
130
161
|
No music bed unless you pass --music.
|
|
162
|
+
--frame pov (conversation conceits) and --pacing
|
|
163
|
+
(presenter conceits) change the shots; a conceit
|
|
164
|
+
that can't follow one refuses it.
|
|
131
165
|
The app approves its own storyboard when its
|
|
132
166
|
checks pass and goes straight on to the clips,
|
|
133
167
|
which is the part that costs. It stops at the
|
|
@@ -365,6 +399,78 @@ Do not use a long dash in words a character says, or in words shown on screen.
|
|
|
365
399
|
Synthesised speech reads a long dash oddly, and so does burned-in text. A spoken
|
|
366
400
|
line stays `NAME: what they say`.
|
|
367
401
|
|
|
402
|
+
### Directing a line
|
|
403
|
+
|
|
404
|
+
A spoken line can carry two kinds of note. Neither is required.
|
|
405
|
+
|
|
406
|
+
- **How the line is said goes in parentheses.** The voice acts it out.
|
|
407
|
+
- **What happens on camera goes in square brackets.** The storyboard plans the
|
|
408
|
+
shot around it.
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
MIA: (whispering, excited) I can't believe this actually worked.
|
|
412
|
+
MIA: [holds the jar up to the camera] This is the one.
|
|
413
|
+
MIA: [leans in] (quietly) Don't tell my sister.
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
A note may sit anywhere on the line. It comes out of the spoken words, so the
|
|
417
|
+
voice never says it. Several notes of one kind on a line are joined. Notes work
|
|
418
|
+
on `CTA:` lines and on narrator lines too. They do not work on `CAST:` lines,
|
|
419
|
+
where anything in brackets is just part of the description.
|
|
420
|
+
|
|
421
|
+
How the voice uses a delivery note. By default a run records each line first,
|
|
422
|
+
on the Eleven v4 voices, which take the note as a short cue in front of the
|
|
423
|
+
line. The cue is the note's first clause, at most six words. A full stop,
|
|
424
|
+
colon, semicolon, question mark, exclamation mark, or a dash with a space on
|
|
425
|
+
each side ends the clause. A comma does not, so `whispering, excited` reaches
|
|
426
|
+
the voice whole. Keep the note short and put the part that matters first. Put
|
|
427
|
+
one delivery note on a line: two are joined with a semicolon, so the voice
|
|
428
|
+
hears only the first.
|
|
429
|
+
|
|
430
|
+
Right.
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
MIA: (whispering, excited) I can't believe this actually worked.
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Wrong.
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
MIA: (She is whispering. She can barely contain how excited she is) I can't believe this actually worked.
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The wrong note reaches the voice as `[she is whispering]`. The excitement is
|
|
443
|
+
lost.
|
|
444
|
+
|
|
445
|
+
In a narrated scene the whole narration is read as one part, so it takes one
|
|
446
|
+
cue: the note on the first narrator line in that scene.
|
|
447
|
+
|
|
448
|
+
What the reader refuses, with the code `malformed-direction`:
|
|
449
|
+
|
|
450
|
+
- an empty note, `()` or `[]`
|
|
451
|
+
- a square bracket that is never closed, or a stray `]`
|
|
452
|
+
- a line with no spoken words left once the notes come out
|
|
453
|
+
|
|
454
|
+
An unclosed `(` is not refused. It is read as ordinary words, and the voice
|
|
455
|
+
says it. Check every `(` has its `)`.
|
|
456
|
+
|
|
457
|
+
Both ways in read the file the same way: `exodus video start --script` and
|
|
458
|
+
`exodus workflow run --input <field>=@script.txt`. The second holds only when
|
|
459
|
+
the script goes straight from a Brief into the Storyboard. In a workflow where a
|
|
460
|
+
bot writes or rewrites the script first, the notes are not read this way, and
|
|
461
|
+
the bot may drop them or leave them in the spoken words.
|
|
462
|
+
|
|
463
|
+
With `--voice native` (admins only) the video model speaks the lines itself and
|
|
464
|
+
nothing is recorded first, so the six-word cue above does not apply.
|
|
465
|
+
|
|
466
|
+
**A line with no note still gets a natural read.** A note is the surest way to
|
|
467
|
+
get a particular delivery. The storyboard planner may add a note of its own to
|
|
468
|
+
a line you left plain. A note you wrote always wins over the planner's.
|
|
469
|
+
|
|
470
|
+
When you help the user write or tidy a script, suggest a delivery note where
|
|
471
|
+
the tone carries the ad: the hook, the turn, the CTA. Do not put a note on every
|
|
472
|
+
line. A note on every line reads as a script nobody trusted.
|
|
473
|
+
|
|
368
474
|
**How many speakers you may have depends on the rig.** Three by default. A rig
|
|
369
475
|
that carries saved format rules sets its own number: an ensemble rig seats more,
|
|
370
476
|
a talking-head rig seats one. Over the number, the launch refuses before
|
|
@@ -979,6 +1085,10 @@ Read these out of `storyboard.json` before you build anything, in any tool:
|
|
|
979
1085
|
between the two halves.
|
|
980
1086
|
- **Overlays and SFX.** `overlays[]` and `audio.sfxCues[]` are cued the same
|
|
981
1087
|
way, by `cueLineId`. Nothing renders them for you.
|
|
1088
|
+
- **Jump cuts.** When `manifest.json` has `shots.pacing: "jump-cuts"`, every
|
|
1089
|
+
scene was filmed from the same framing on purpose. Punch in about 15% on
|
|
1090
|
+
every other cut between one speaker's scenes, so each cut visibly moves in
|
|
1091
|
+
or out. The bundled script does this for you.
|
|
982
1092
|
- **Captions.** Nothing is burned into any clip. Captions belong in post; the
|
|
983
1093
|
word timings are what an editor imports to build them.
|
|
984
1094
|
|
|
@@ -988,19 +1098,28 @@ State this to the user before you run it, so they know what the cut does with
|
|
|
988
1098
|
the pieces:
|
|
989
1099
|
|
|
990
1100
|
- **It cuts the picture to the voice, and never the other way round.** A
|
|
991
|
-
narrated scene's segment runs
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1101
|
+
narrated scene's segment runs to the narrator's last word plus a beat of air:
|
|
1102
|
+
about 0.25 s before the next scene, 0.5 s when it ends the ad. The last word
|
|
1103
|
+
comes from the voice track's own word timings; when the scene has none, the
|
|
1104
|
+
end of the voice file stands in for it. A clip that runs longer is trimmed; a
|
|
1105
|
+
clip that runs shorter holds its last frame for the difference. The narration
|
|
1106
|
+
is never sped up or slowed down, so every scene plays at the speed it was
|
|
1107
|
+
recorded, and the word times are handed on as they were measured. The voice
|
|
1108
|
+
file is cut only in the silence after its last word, never inside a word.
|
|
996
1109
|
- **It closes the dead air at the joins.** A clip that plays its own dialogue
|
|
997
1110
|
keeps about 0.15 s after its last word and about 0.1 s before its first (0.25 s
|
|
998
1111
|
when a different speaker answers), read from its `words.json`, so one clip
|
|
999
1112
|
runs into the next like one conversation. It only ever shortens a clip's
|
|
1000
1113
|
silence and never cuts into a word. The ad's first clip also opens about 0.1 s
|
|
1001
1114
|
before its first word, so a hum a clip makes before anyone speaks is not the
|
|
1002
|
-
first thing heard. The
|
|
1003
|
-
|
|
1115
|
+
first thing heard. The clip that ends the ad keeps about 0.5 s after its last
|
|
1116
|
+
word, the same beat a narrator line gets there, or longer when the storyboard
|
|
1117
|
+
marks an action after the line. A narrated scene, a still and a reaction beat
|
|
1118
|
+
keep their length.
|
|
1119
|
+
- **It shortens long pauses between sentences inside one clip.** A silence over
|
|
1120
|
+
0.6 s between two sentences is cut down to about 0.5 s, picture and sound
|
|
1121
|
+
together, unless the script writes the pause in (an ellipsis or a dash), a
|
|
1122
|
+
direction asks for one, or a reaction's laugh follows the line.
|
|
1004
1123
|
- **It fades the audience reaction in and out.** A reaction beat fades in
|
|
1005
1124
|
across any quiet start before its laugh (a generated laugh clip can hum
|
|
1006
1125
|
there). When a line follows, the picture moves on early and the laugh's own
|
|
@@ -1017,11 +1136,17 @@ the pieces:
|
|
|
1017
1136
|
proportional estimate when the scene has no word times, and saying which
|
|
1018
1137
|
method it used), a narrated scene's voice
|
|
1019
1138
|
replacing whatever the clip recorded, a dialogue scene keeping its performed
|
|
1020
|
-
line,
|
|
1139
|
+
line, everything normalized to 1080×1920
|
|
1021
1140
|
at 24 fps and −16 LUFS per segment, a generated room-tone bed under the whole
|
|
1022
1141
|
ad, and the music bed underneath at low level. Output is H.264 + AAC in an MP4
|
|
1023
1142
|
with `-movflags +faststart`.
|
|
1024
|
-
-
|
|
1143
|
+
- **It stops when a scene has no clip.** A scene the storyboard plans that is
|
|
1144
|
+
still being made, never started, or failed stops the cut, which names each
|
|
1145
|
+
one. Wait for the clip and pull again, leave the scene out with `--skip`, or
|
|
1146
|
+
pass `--still-fill` to hold that scene's keyframe on screen instead: under its
|
|
1147
|
+
voice track if it has one, else silent for its planned length. The still is
|
|
1148
|
+
only ever made when asked for, because a run of them reads as a broken ad.
|
|
1149
|
+
- Flags: `--out <file>`, `--skip 2,5`, `--no-music`, `--still-fill`. It prints what went in,
|
|
1025
1150
|
what was left out and why, and the upload command. When a clip hums or
|
|
1026
1151
|
clicks where nobody speaks at an edge the cut plays, it lists each one under
|
|
1027
1152
|
"Sound at the edges" with its time in the cut. That is a warning to listen
|
|
@@ -1032,8 +1157,12 @@ the pieces:
|
|
|
1032
1157
|
timeline, each scene's own word times offset by where that scene starts in the
|
|
1033
1158
|
cut, less any lead-in trimmed at the join, and otherwise unaltered. Scenes with no word times leave
|
|
1034
1159
|
gaps and are named in the report; when no scene delivered any, neither file is
|
|
1035
|
-
written.
|
|
1036
|
-
|
|
1160
|
+
written. It always writes `cut.scenes.json` too: where each scene plays in the
|
|
1161
|
+
cut, one `{scene, startSec, seconds}` entry per placement. `exodus video
|
|
1162
|
+
upload` sends `cut.scenes.json` and `cut.words.json` with the cut when they
|
|
1163
|
+
sit beside it, so the customer's edit list shows each scene's time on screen
|
|
1164
|
+
and their editor folder gets the captions. These sidecars are the script's own
|
|
1165
|
+
output — another tool will not produce them.
|
|
1037
1166
|
|
|
1038
1167
|
Before uploading anything, from any tool, check what you made:
|
|
1039
1168
|
`ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 cut.mp4`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// #2988: the types of the
|
|
1
|
+
// #2988: the types of the exports the Trigger worker calls. The script is
|
|
2
2
|
// plain JS so the CLI can run it with `node` and no build.
|
|
3
3
|
|
|
4
4
|
export class FirstCutError extends Error {
|
|
@@ -14,6 +14,8 @@ export interface FirstCutOptions {
|
|
|
14
14
|
skip?: Set<number>;
|
|
15
15
|
/** The music bed, when the pull has one. Defaults to on, as the CLI does. */
|
|
16
16
|
music?: boolean;
|
|
17
|
+
/** #3365: hold a scene's keyframe where it has no clip, instead of refusing. Defaults to off. */
|
|
18
|
+
stillFill?: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface FirstCut {
|
|
@@ -25,3 +27,18 @@ export interface FirstCut {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
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 }>;
|