@aicopycoders/exodus 2026.9.2000-beta.22 → 2026.9.2000-beta.24
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.
|
@@ -962,8 +962,11 @@ the pieces:
|
|
|
962
962
|
reaction beat keep their length.
|
|
963
963
|
- **It fades the audience reaction in and out.** A reaction beat fades in
|
|
964
964
|
across any quiet start before its laugh (a generated laugh clip can hum
|
|
965
|
-
there)
|
|
966
|
-
|
|
965
|
+
there). When a line follows, the picture moves on early and the laugh's own
|
|
966
|
+
remaining sound plays on under that line, quieter and fading out: up to
|
|
967
|
+
0.75 s, and the beat keeps at least 1 s of picture (a 1.5 s clip shows 1 s
|
|
968
|
+
and carries its last 0.5 s). A beat that ends the ad, or has under 0.4 s to
|
|
969
|
+
carry, fades out over its last 0.4 s instead.
|
|
967
970
|
- **It ignores the continuous master.** It reads only `scene-NN.voice` files;
|
|
968
971
|
it never opens `narration.mp3` or `manifest.narration`. A tool that lays the
|
|
969
972
|
master down whole and cuts the pictures to it keeps the read intact.
|
|
@@ -71,8 +71,10 @@ export const REACTION_MAX_SEC = 1.5;
|
|
|
71
71
|
*/
|
|
72
72
|
export const REACTION_TAIL_SEC = 0.75;
|
|
73
73
|
export const REACTION_TAIL_GAIN = 0.35;
|
|
74
|
-
/** A reaction clip
|
|
74
|
+
/** A reaction clip with too little sound for a tail fades out over this long instead (#2666). */
|
|
75
75
|
export const REACTION_FADE_OUT_SEC = 0.4;
|
|
76
|
+
/** The least picture a reaction beat keeps when it gives up its end to a tail (#2700). */
|
|
77
|
+
export const REACTION_MIN_BEAT_SEC = 1.0;
|
|
76
78
|
/**
|
|
77
79
|
* The air a join keeps around its words (#2388): after one clip's last word, and
|
|
78
80
|
* before the next clip's first, less when the same speaker carries on than when
|
|
@@ -539,17 +541,21 @@ function placeReaction(spine, counts, reaction) {
|
|
|
539
541
|
}
|
|
540
542
|
|
|
541
543
|
/**
|
|
542
|
-
* The sound a reaction beat carries under the next segment (#2406)
|
|
543
|
-
*
|
|
544
|
-
* the
|
|
545
|
-
*
|
|
544
|
+
* The sound a reaction beat carries under the next segment (#2406), as a J/L
|
|
545
|
+
* cut: the picture moves on early and the clip's sound from where it stops
|
|
546
|
+
* plays under the next line. A clip that ends with its beat gives up the
|
|
547
|
+
* beat's end, down to REACTION_MIN_BEAT_SEC, rather than replaying sound the
|
|
548
|
+
* beat already played, which was heard as a stutter (#2666, #2700). Shortens
|
|
549
|
+
* the beat, so it runs before the spine's start times are set. Null when
|
|
550
|
+
* under 0.4 s of sound would carry over; the beat fades itself out then.
|
|
546
551
|
*/
|
|
547
|
-
function
|
|
552
|
+
function carveReactionTail(seg, next) {
|
|
548
553
|
const clipLen = seg.clipSeconds ?? seg.seconds;
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
554
|
+
const overlap =
|
|
555
|
+
Math.round(Math.min(REACTION_TAIL_SEC, clipLen - seg.inSec - REACTION_MIN_BEAT_SEC, next.seconds) * 1000) / 1000;
|
|
556
|
+
if (overlap < REACTION_FADE_OUT_SEC) return null;
|
|
557
|
+
seg.seconds = Math.min(seg.seconds, Math.round((clipLen - seg.inSec - overlap) * 1000) / 1000);
|
|
558
|
+
return { fromSec: seg.inSec + seg.seconds, seconds: overlap, gain: REACTION_TAIL_GAIN };
|
|
553
559
|
}
|
|
554
560
|
|
|
555
561
|
/**
|
|
@@ -673,17 +679,20 @@ export function buildTimeline({ manifest, storyboard, skip, probe, exists, readW
|
|
|
673
679
|
dropped.push({ n: c.sceneIndex, reason: `cutaway ${c.sceneIndex}: ${noCue(c)}` });
|
|
674
680
|
}
|
|
675
681
|
}
|
|
682
|
+
spine.forEach((seg, k) => {
|
|
683
|
+
if (!seg.reaction || seg.audio !== "clip") return;
|
|
684
|
+
const tail = spine[k + 1] ? carveReactionTail(seg, spine[k + 1]) : null;
|
|
685
|
+
if (tail) seg.reaction.tail = tail;
|
|
686
|
+
else seg.fadeOutSec = Math.min(REACTION_FADE_OUT_SEC, seg.seconds);
|
|
687
|
+
seg.fadeInSec = quietStart(readLevels(seg.file), seg.seconds);
|
|
688
|
+
});
|
|
676
689
|
let total = 0;
|
|
677
690
|
for (const seg of spine) {
|
|
678
691
|
seg.startSec = total;
|
|
679
692
|
total += seg.seconds;
|
|
680
693
|
}
|
|
681
694
|
spine.forEach((seg, k) => {
|
|
682
|
-
if (
|
|
683
|
-
seg.fadeInSec = quietStart(readLevels(seg.file), seg.seconds);
|
|
684
|
-
const tail = spine[k + 1] ? reactionTail(seg, spine[k + 1]) : null;
|
|
685
|
-
if (tail) seg.reaction.tail = tail;
|
|
686
|
-
else seg.fadeOutSec = Math.min(REACTION_FADE_OUT_SEC, seg.seconds);
|
|
695
|
+
if (seg.reaction?.tail) seg.reaction.tail.atSec = spine[k + 1].startSec;
|
|
687
696
|
});
|
|
688
697
|
|
|
689
698
|
for (const c of waiting.filter((w) => !w.reaction)) {
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -63,7 +63,8 @@ export type ArtifactSubset = {
|
|
|
63
63
|
type: "image";
|
|
64
64
|
imageUrl?: string;
|
|
65
65
|
storageId?: string;
|
|
66
|
-
anchor?: "set-plate" | "product-sheet";
|
|
66
|
+
anchor?: "set-plate" | "product-sheet" | "filled-plate";
|
|
67
|
+
holds?: string[];
|
|
67
68
|
} | {
|
|
68
69
|
type: "video";
|
|
69
70
|
sceneIndex?: number;
|
|
@@ -342,6 +343,10 @@ export interface RunVideoChoice {
|
|
|
342
343
|
voiceModeLabel: string;
|
|
343
344
|
}
|
|
344
345
|
export declare const MUSIC_HEARD_CODE = "music-heard";
|
|
346
|
+
export interface ManifestFilledPlate {
|
|
347
|
+
file: string | null;
|
|
348
|
+
holds: string[];
|
|
349
|
+
}
|
|
345
350
|
export interface VideoManifest {
|
|
346
351
|
runId: string;
|
|
347
352
|
pulledAt: string;
|
|
@@ -350,6 +355,7 @@ export interface VideoManifest {
|
|
|
350
355
|
reference: string | null;
|
|
351
356
|
productSheet: string | null;
|
|
352
357
|
setPlate: string | null;
|
|
358
|
+
filledPlates: ManifestFilledPlate[];
|
|
353
359
|
music: string | null;
|
|
354
360
|
musicBed: MusicBedState;
|
|
355
361
|
musicHeardScenes: number[];
|
package/dist/commands/video.js
CHANGED
|
@@ -140,8 +140,9 @@ Options:
|
|
|
140
140
|
--voice-path <path> How the voices are made (start)
|
|
141
141
|
--video-model <id> Which video model films this ad, instead of the kind of
|
|
142
142
|
ad's own default (start, with --conceit)
|
|
143
|
-
--voice <mode>
|
|
144
|
-
|
|
143
|
+
--voice <mode> voice-first (the default): the lines are recorded
|
|
144
|
+
first. native: the video model speaks the lines
|
|
145
|
+
(start, with --conceit)
|
|
145
146
|
--music Put a music bed under the ad (start). An ad has no music
|
|
146
147
|
unless you ask for it here
|
|
147
148
|
--no-music Leave the music bed off (start), which it already is
|
|
@@ -790,9 +791,15 @@ export function planPull(run, items, opts) {
|
|
|
790
791
|
let reference = null;
|
|
791
792
|
let productSheet = null;
|
|
792
793
|
let setPlate = null;
|
|
794
|
+
const filledByStage = new Map();
|
|
793
795
|
for (const artifact of outputsOfNodeKind(run, "reference")) {
|
|
794
796
|
if (artifact.type !== "image" || !artifact.imageUrl)
|
|
795
797
|
continue;
|
|
798
|
+
if (artifact.anchor === "filled-plate") {
|
|
799
|
+
const holds = artifact.holds ?? [];
|
|
800
|
+
filledByStage.set(JSON.stringify(holds), { holds, url: artifact.imageUrl });
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
796
803
|
const stem = artifact.anchor ?? "reference";
|
|
797
804
|
const download = { file: `${stem}.${extFor(artifact.imageUrl, "image")}`, url: artifact.imageUrl };
|
|
798
805
|
if (artifact.anchor === "product-sheet")
|
|
@@ -802,6 +809,10 @@ export function planPull(run, items, opts) {
|
|
|
802
809
|
else if (!artifact.anchor)
|
|
803
810
|
reference = download;
|
|
804
811
|
}
|
|
812
|
+
const filledPlates = [...filledByStage.values()].map((plate, i) => ({
|
|
813
|
+
holds: plate.holds,
|
|
814
|
+
download: { file: `filled-plate-${i + 1}.${extFor(plate.url, "image")}`, url: plate.url },
|
|
815
|
+
}));
|
|
805
816
|
const keyframeByScene = new Map();
|
|
806
817
|
for (const artifact of run.nodes.flatMap((n) => n.outputs ?? [])) {
|
|
807
818
|
if (artifact.type !== "frames")
|
|
@@ -969,6 +980,7 @@ export function planPull(run, items, opts) {
|
|
|
969
980
|
...(reference ? [reference] : []),
|
|
970
981
|
...(productSheet ? [productSheet] : []),
|
|
971
982
|
...(setPlate ? [setPlate] : []),
|
|
983
|
+
...filledPlates.map((p) => p.download),
|
|
972
984
|
...keyframeByScene.values(),
|
|
973
985
|
...[...voiceByScene.values()].map((v) => v.download),
|
|
974
986
|
...(narrationDownload ? [narrationDownload] : []),
|
|
@@ -987,6 +999,7 @@ export function planPull(run, items, opts) {
|
|
|
987
999
|
reference: reference?.file ?? null,
|
|
988
1000
|
productSheet: productSheet?.file ?? null,
|
|
989
1001
|
setPlate: setPlate?.file ?? null,
|
|
1002
|
+
filledPlates: filledPlates.map((p) => ({ file: p.download.file, holds: p.holds })),
|
|
990
1003
|
music: music?.file ?? null,
|
|
991
1004
|
musicBed: musicBedState(run),
|
|
992
1005
|
musicHeardScenes: scenes
|
|
@@ -1023,6 +1036,10 @@ export function markPullFailure(manifest, failure) {
|
|
|
1023
1036
|
manifest.productSheet = null;
|
|
1024
1037
|
if (manifest.setPlate === failure.file)
|
|
1025
1038
|
manifest.setPlate = null;
|
|
1039
|
+
for (const plate of manifest.filledPlates) {
|
|
1040
|
+
if (plate.file === failure.file)
|
|
1041
|
+
plate.file = null;
|
|
1042
|
+
}
|
|
1026
1043
|
if (manifest.music === failure.file)
|
|
1027
1044
|
manifest.music = null;
|
|
1028
1045
|
for (const ref of manifest.cast) {
|