Nothing is held for two minutes, not even one visual

planShots skipped cut planning entirely when a section had a single visual to
show — "nothing to cut to" — and the exemption swallowed the ceiling with it.
Adding a scene shifted the casting enough to land a 150 BPM track in that case,
and it held one image for 151.98 seconds against a 22-second limit. Any new
scene could have exposed it; the shot-length gate had simply never met a
single-visual section before.

The premise was wrong rather than the arithmetic. A shot boundary changes the
FRAMING as well as the image, so the same visual filmed again at another size is
a shot, and the ceiling is about how long one image is held rather than about how
many images a section has. Single-visual sections now plan shots like any other.

What the single case still suppresses is the hard cut: cutting straight between
two framings of one image is a jump cut, so those boundaries always dissolve.

Two intermediate versions were wrong in ways worth not repeating: splitting the
section into equal spans put a 23.5s shot past the ceiling once the cuts snapped
to downbeats, and read as a metronome (cv 0.048) to the rhythm check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino 2026-08-19 17:16:02 +02:00
parent 144c8c918e
commit 89277705c4

View File

@ -137,7 +137,18 @@ export function planShots(section, track, bias, variantCount, rng, story = null)
const lengths = fitPattern(rhythmFor(bias.energy, rng, story), barSeconds); const lengths = fitPattern(rhythmFor(bias.energy, rng, story), barSeconds);
const shortest = Math.min(...lengths); const shortest = Math.min(...lengths);
// A section with only one visual to show has nothing to cut to. // A section with only one visual to show has nothing to cut TO. It still
// gets shot boundaries: a boundary changes the FRAMING as well as the
// image, so the same visual filmed again at another size is a shot, and the
// ceiling is about how long one image is held rather than about how many
// images a section has.
//
// This used to skip cut planning entirely, and the exemption swallowed the
// ceiling with it — measured, a 150 BPM track held one visual for 151.98
// seconds, seven times the limit, which is exactly the complaint this whole
// level of hierarchy exists to answer. What it does still suppress is the
// hard cut: cutting straight between two framings of one image is a jump
// cut, so those boundaries always dissolve.
const single = variantCount < 2; const single = variantCount < 2;
// Walk the pattern, laying shots end to end from the section start. Each cut // Walk the pattern, laying shots end to end from the section start. Each cut
@ -149,7 +160,7 @@ export function planShots(section, track, bias, variantCount, rng, story = null)
const downbeats = track.tempo.downbeats || []; const downbeats = track.tempo.downbeats || [];
const cuts = []; const cuts = [];
if (!single) { {
let at = section.start; let at = section.start;
for (let k = 0; k < 512; k++) { for (let k = 0; k < 512; k++) {
const raw = at + lengths[k % lengths.length]; const raw = at + lengths[k % lengths.length];
@ -193,7 +204,10 @@ export function planShots(section, track, bias, variantCount, rng, story = null)
// edited, but on anything calmer it reads as a glitch, so cuts are // edited, but on anything calmer it reads as a glitch, so cuts are
// gated on real energy rather than sprinkled everywhere: nothing below // gated on real energy rather than sprinkled everywhere: nothing below
// the threshold ever cuts, and only the loudest material cuts often. // the threshold ever cuts, and only the loudest material cuts often.
hardCut: bias.energy > HARD_CUT_ENERGY // A single-visual section changes framing, not image. Cutting hard
// between two framings of the same thing is a jump cut, so those
// boundaries always dissolve.
hardCut: !single && bias.energy > HARD_CUT_ENERGY
&& rng.bool(Math.min(0.85, (bias.energy - HARD_CUT_ENERGY) * 2.5)), && rng.bool(Math.min(0.85, (bias.energy - HARD_CUT_ENERGY) * 2.5)),
}); });
} }