@aicopycoders/exodus 2026.10.2300-beta.1 → 2026.10.2400-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.
|
@@ -1237,9 +1237,12 @@ Read these out of `storyboard.json` before you build anything, in any tool:
|
|
|
1237
1237
|
running order. To find the moment, locate the A-roll scene whose `lineIds`
|
|
1238
1238
|
contain the cue line, then read the start time of the first word of that line
|
|
1239
1239
|
out of that scene's `words.json`. A B-roll cutaway (one with `broll`) starts
|
|
1240
|
-
|
|
1240
|
+
0.5 s before the first word of `broll.covers` instead (even if that reaches
|
|
1241
|
+
back into the scene before, but never into a reaction or before 0 s), and
|
|
1242
|
+
holds through its last word or for `broll.plannedSec`, whichever is longer,
|
|
1241
1243
|
0.8 s to 4 s; a shot planned longer than 4 s (a talking character) holds at
|
|
1242
|
-
least `broll.plannedSec`.
|
|
1244
|
+
least `broll.plannedSec`. The next cutaway, a reaction or the ad's end cuts
|
|
1245
|
+
it short.
|
|
1243
1246
|
- **Reactions.** A cutaway with `cutawayType: "reaction"` is the exception. Do
|
|
1244
1247
|
not lay it over the speech. Put it into the running order as its own beat
|
|
1245
1248
|
right after its cue line ends, play its own sound (the audience laughing),
|
|
@@ -112,9 +112,15 @@ export const JOIN_LEAD_NEW_SPEAKER_SEC = 0.25;
|
|
|
112
112
|
/** The gap a cut keeps from an extra heard sound it leaves out (#2541). */
|
|
113
113
|
const EXTRA_CLEARANCE_SEC = 0.05;
|
|
114
114
|
/**
|
|
115
|
-
* A B-roll shot
|
|
116
|
-
*
|
|
117
|
-
|
|
115
|
+
* A B-roll shot arrives this long before the first word it covers, the lead the
|
|
116
|
+
* top ads use and the planner teaches (#3936).
|
|
117
|
+
*/
|
|
118
|
+
export const BROLL_LEAD_SEC = 0.5;
|
|
119
|
+
/**
|
|
120
|
+
* A B-roll shot holds over the words it covers (#3912) or for its planned
|
|
121
|
+
* length, whichever runs longer (#3936), but never shorter than the eye needs
|
|
122
|
+
* to read it nor so long it drags; a shot planned past the longest hold (a
|
|
123
|
+
* talking character) keeps its planned length.
|
|
118
124
|
*/
|
|
119
125
|
export const BROLL_MIN_HOLD_SEC = 0.8;
|
|
120
126
|
export const BROLL_MAX_HOLD_SEC = 4;
|
|
@@ -821,12 +827,14 @@ function heardCovers(seg, texts, cueLineId, covers) {
|
|
|
821
827
|
}
|
|
822
828
|
|
|
823
829
|
/**
|
|
824
|
-
* #3912: a B-roll shot's place in its segment. It starts
|
|
825
|
-
* covers
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
* its planned length
|
|
829
|
-
*
|
|
830
|
+
* #3912: a B-roll shot's place in its segment. It starts BROLL_LEAD_SEC before
|
|
831
|
+
* the first word it covers, which can fall before the segment's own start
|
|
832
|
+
* (#3936), and holds through the last word or for its planned length,
|
|
833
|
+
* whichever is longer, between BROLL_MIN_HOLD_SEC and BROLL_MAX_HOLD_SEC, or
|
|
834
|
+
* for its planned length when that runs longer than the cap. Covered words the
|
|
835
|
+
* line does not say leave it at the line's start for its planned length.
|
|
836
|
+
* `texts` maps each line id to its script text. Null when the segment does not
|
|
837
|
+
* speak the cue line.
|
|
830
838
|
*/
|
|
831
839
|
export function brollWindow(seg, counts, cueLineId, texts, broll) {
|
|
832
840
|
const line = lineInSegment(seg, counts, cueLineId);
|
|
@@ -840,12 +848,13 @@ export function brollWindow(seg, counts, cueLineId, texts, broll) {
|
|
|
840
848
|
};
|
|
841
849
|
}
|
|
842
850
|
const heard = heardCovers(seg, texts, cueLineId, broll.covers);
|
|
843
|
-
const
|
|
851
|
+
const wordSec = heard?.startSec ?? wordTimeInSegment(seg, line.before + run.first, line.total, "s");
|
|
844
852
|
const endSec = heard?.endSec ?? wordTimeInSegment(seg, line.before + run.last, line.total, "e");
|
|
853
|
+
const startSec = wordSec - BROLL_LEAD_SEC;
|
|
845
854
|
const span = Math.max(0, endSec - startSec);
|
|
846
855
|
const holdSec = broll.plannedSec > BROLL_MAX_HOLD_SEC
|
|
847
856
|
? Math.max(span, broll.plannedSec)
|
|
848
|
-
: Math.min(Math.max(span, BROLL_MIN_HOLD_SEC), BROLL_MAX_HOLD_SEC);
|
|
857
|
+
: Math.min(Math.max(span, broll.plannedSec, BROLL_MIN_HOLD_SEC), BROLL_MAX_HOLD_SEC);
|
|
849
858
|
return { startSec, holdSec, method, found: true };
|
|
850
859
|
}
|
|
851
860
|
|
|
@@ -1221,6 +1230,7 @@ export function buildTimeline({
|
|
|
1221
1230
|
});
|
|
1222
1231
|
|
|
1223
1232
|
const lineTexts = new Map([...scriptLines].map(([id, line]) => [id, line?.text]));
|
|
1233
|
+
const beats = spine.filter((seg) => seg.reaction);
|
|
1224
1234
|
for (const c of waiting.filter((w) => !w.reaction)) {
|
|
1225
1235
|
const cue = c.cueLineId ? cueOffsetOnSpine(spine, counts, c.cueLineId) : null;
|
|
1226
1236
|
if (!cue) {
|
|
@@ -1233,7 +1243,9 @@ export function buildTimeline({
|
|
|
1233
1243
|
? brollWindow(seg, counts, c.cueLineId, lineTexts, c.broll)
|
|
1234
1244
|
: null;
|
|
1235
1245
|
if (win) {
|
|
1236
|
-
|
|
1246
|
+
// A shot's lead may reach back into the segment before, never into a reaction beat or before the ad.
|
|
1247
|
+
const floor = Math.max(0, ...beats.filter((b) => b.startSec < seg.startSec).map((b) => b.startSec + b.seconds));
|
|
1248
|
+
placed.startSec = Math.max(floor, seg.startSec + win.startSec);
|
|
1237
1249
|
placed.method = win.method;
|
|
1238
1250
|
placed.seconds = c.source === "clip" ? Math.min(win.holdSec, c.seconds) : win.holdSec;
|
|
1239
1251
|
placed.coversFound = win.found;
|
|
@@ -1241,7 +1253,6 @@ export function buildTimeline({
|
|
|
1241
1253
|
cutaways.push(placed);
|
|
1242
1254
|
}
|
|
1243
1255
|
cutaways.sort((a, b) => a.startSec - b.startSec);
|
|
1244
|
-
const beats = spine.filter((seg) => seg.reaction);
|
|
1245
1256
|
for (let i = 0; i < cutaways.length; i++) {
|
|
1246
1257
|
const c = cutaways[i];
|
|
1247
1258
|
const stops = [];
|