@aicopycoders/exodus 2026.9.2000 → 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/skills/exodus-video/SKILL.md +40 -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
|
@@ -70,7 +70,7 @@ never how it is made. Every answer to them says `"view": "customer"`, and the
|
|
|
70
70
|
CLI words it the way the run page does. On their own brands they can:
|
|
71
71
|
|
|
72
72
|
- `video start --script <file> --conceit <key> --style <slug>` with
|
|
73
|
-
`--direction`, `--music`, `--review-storyboard` and `--wait`. Any other start
|
|
73
|
+
`--direction`, `--frame`, `--pacing`, `--music`, `--review-storyboard` and `--wait`. Any other start
|
|
74
74
|
flag is refused with one sentence naming it. The answer is the run id and the
|
|
75
75
|
link to the video's page.
|
|
76
76
|
- `video status <runId>`: where the video is, in plain words (waiting in line,
|
|
@@ -125,9 +125,12 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
125
125
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
126
126
|
same, with the video model speaking voices you WROTE
|
|
127
127
|
exodus video status <runId> where it is, per scene
|
|
128
|
-
exodus video start --script <file> --conceit <
|
|
129
|
-
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
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.
|
|
131
134
|
The app approves its own storyboard when its
|
|
132
135
|
checks pass and goes straight on to the clips,
|
|
133
136
|
which is the part that costs. It stops at the
|
|
@@ -979,6 +982,10 @@ Read these out of `storyboard.json` before you build anything, in any tool:
|
|
|
979
982
|
between the two halves.
|
|
980
983
|
- **Overlays and SFX.** `overlays[]` and `audio.sfxCues[]` are cued the same
|
|
981
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.
|
|
982
989
|
- **Captions.** Nothing is burned into any clip. Captions belong in post; the
|
|
983
990
|
word timings are what an editor imports to build them.
|
|
984
991
|
|
|
@@ -988,19 +995,28 @@ State this to the user before you run it, so they know what the cut does with
|
|
|
988
995
|
the pieces:
|
|
989
996
|
|
|
990
997
|
- **It cuts the picture to the voice, and never the other way round.** A
|
|
991
|
-
narrated scene's segment runs
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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.
|
|
996
1006
|
- **It closes the dead air at the joins.** A clip that plays its own dialogue
|
|
997
1007
|
keeps about 0.15 s after its last word and about 0.1 s before its first (0.25 s
|
|
998
1008
|
when a different speaker answers), read from its `words.json`, so one clip
|
|
999
1009
|
runs into the next like one conversation. It only ever shortens a clip's
|
|
1000
1010
|
silence and never cuts into a word. The ad's first clip also opens about 0.1 s
|
|
1001
1011
|
before its first word, so a hum a clip makes before anyone speaks is not the
|
|
1002
|
-
first thing heard. The
|
|
1003
|
-
|
|
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.
|
|
1004
1020
|
- **It fades the audience reaction in and out.** A reaction beat fades in
|
|
1005
1021
|
across any quiet start before its laugh (a generated laugh clip can hum
|
|
1006
1022
|
there). When a line follows, the picture moves on early and the laugh's own
|
|
@@ -1017,11 +1033,17 @@ the pieces:
|
|
|
1017
1033
|
proportional estimate when the scene has no word times, and saying which
|
|
1018
1034
|
method it used), a narrated scene's voice
|
|
1019
1035
|
replacing whatever the clip recorded, a dialogue scene keeping its performed
|
|
1020
|
-
line,
|
|
1036
|
+
line, everything normalized to 1080×1920
|
|
1021
1037
|
at 24 fps and −16 LUFS per segment, a generated room-tone bed under the whole
|
|
1022
1038
|
ad, and the music bed underneath at low level. Output is H.264 + AAC in an MP4
|
|
1023
1039
|
with `-movflags +faststart`.
|
|
1024
|
-
-
|
|
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,
|
|
1025
1047
|
what was left out and why, and the upload command. When a clip hums or
|
|
1026
1048
|
clicks where nobody speaks at an edge the cut plays, it lists each one under
|
|
1027
1049
|
"Sound at the edges" with its time in the cut. That is a warning to listen
|
|
@@ -1032,8 +1054,12 @@ the pieces:
|
|
|
1032
1054
|
timeline, each scene's own word times offset by where that scene starts in the
|
|
1033
1055
|
cut, less any lead-in trimmed at the join, and otherwise unaltered. Scenes with no word times leave
|
|
1034
1056
|
gaps and are named in the report; when no scene delivered any, neither file is
|
|
1035
|
-
written.
|
|
1036
|
-
|
|
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.
|
|
1037
1063
|
|
|
1038
1064
|
Before uploading anything, from any tool, check what you made:
|
|
1039
1065
|
`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 }>;
|