From 89277705c4d0d84c889f9d729e23d506478867a7 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Wed, 19 Aug 2026 17:16:02 +0200 Subject: [PATCH] Nothing is held for two minutes, not even one visual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- flow-state/src/look/shots.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/flow-state/src/look/shots.js b/flow-state/src/look/shots.js index 53b3df2..6ca2304 100644 --- a/flow-state/src/look/shots.js +++ b/flow-state/src/look/shots.js @@ -137,7 +137,18 @@ export function planShots(section, track, bias, variantCount, rng, story = null) const lengths = fitPattern(rhythmFor(bias.energy, rng, story), barSeconds); 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; // 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 cuts = []; - if (!single) { + { let at = section.start; for (let k = 0; k < 512; k++) { 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 // gated on real energy rather than sprinkled everywhere: nothing below // 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)), }); }