A cast census, and the arrangement it needed to be true

"Twenty-three of sixty-one scenes are never cast" was an artefact of the song it
was measured on. synthesizeSectioned has one change point, so it segments into
exactly two sections and both are quiet kinds — and intro, breakdown and outro
are restricted to the restful families for every director. Half the library was
unreachable before a seed was drawn, and the measurement reported that as a
casting failure.

synthesizeArrangement builds a real one: intro, build, drop, breakdown, drop,
outro, shaped to hit the segmenter's own classifier rather than to sound like
anything. Measured against a bank of those, two scenes out of sixty-one are
never a background, not twenty-three.

The census exists because "never cast" without a reason is unactionable. A scene
can die at the signature gate, at the director's family table, or in the roster
draw, and those are three different repairs — a trait declaration, a table, and
arithmetic that neither fixes. It reports which one, and it separates being cast
as a background from appearing as a translucent overlay, because a library can
look fully used while nine scenes carry every frame.

What it finds is that eligibility is almost entirely a function of how many
traits a scene declares. Four-trait scenes are eligible for every track and open
half of all videos; two-trait scenes are eligible for one track in fourteen; the
single scene declaring one trait is eligible for none, ever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-17 06:45:42 +02:00
co-authored by Claude Opus 5
parent 656e062069
commit 91e74ff167
2 changed files with 263 additions and 0 deletions
+65
View File
@@ -119,6 +119,71 @@ export function synthesizeSectioned({
return buffer;
}
/**
* A track with a full ARRANGEMENT — intro, build, drop, breakdown, drop, outro.
*
* `synthesizeSectioned` has one change point, so it segments into exactly two
* sections and both of them are quiet kinds. That is fine for testing that the
* segmenter finds a boundary, and it was quietly useless for anything that
* measures what the generator DOES with a song: intro, breakdown and outro are
* restricted to the restful families for every director, so a two-section track
* cannot reach geometric, glitch or structural scenes at all. Half the library
* is unreachable before the seed is even drawn, and a test built on it will
* report that as a casting failure.
*
* The stages here are shaped to hit the segmenter's own classifier: a build
* needs a rising energy slope, a drop needs energy and flux together, and a
* breakdown needs to fall well below the median.
*/
export function synthesizeArrangement({
bpm = 128,
duration = 120,
sampleRate = 44100,
brightness = 1,
density = 1,
} = {}) {
const length = Math.round(duration * sampleRate);
const buffer = new MockAudioBuffer(2, length, sampleRate);
const left = buffer.getChannelData(0);
const right = buffer.getChannelData(1);
// Proportions of the track, in order. Kick gain, hat gain, pad root, pad gain.
const stages = [
{ span: 0.12, kick: 0.00, hat: 0.00, root: 110, pad: 0.09 }, // intro
{ span: 0.16, kick: 0.55, hat: 0.20, root: 165, pad: 0.14, ramp: true }, // build
{ span: 0.22, kick: 1.00, hat: 0.50, root: 440, pad: 0.24 }, // drop
{ span: 0.14, kick: 0.05, hat: 0.02, root: 110, pad: 0.07 }, // breakdown
{ span: 0.24, kick: 1.00, hat: 0.55, root: 440, pad: 0.26 }, // drop
{ span: 0.12, kick: 0.25, hat: 0.08, root: 110, pad: 0.08 }, // outro
];
const beat = 60 / bpm;
let at = 0;
for (const stage of stages) {
const from = at;
const to = Math.min(duration, at + stage.span * duration);
at = to;
let index = Math.round(from / beat);
for (let t = from; t < to; t += beat, index++) {
// A build ramps across its own span so the energy slope is positive
// enough for the classifier to call it one.
const ramp = stage.ramp ? (t - from) / Math.max(1e-6, to - from) : 1;
const gain = stage.kick * density * (0.25 + ramp * 0.75);
if (gain > 0.02) addKick(left, sampleRate, t, gain * (index % 4 === 0 ? 1 : 0.8));
if (stage.hat > 0.02) {
const h = stage.hat * density * ramp;
addHat(left, sampleRate, t + beat / 2, h, index + 1);
if (h > 0.3) addHat(left, sampleRate, t + beat / 4, h * 0.6, index + 7);
}
}
addPad(left, sampleRate, from, to, stage.pad, stage.root * brightness);
}
for (let i = 0; i < length; i++) right[i] = left[i] * 0.98;
return buffer;
}
/** Silence, for degenerate-input checks. */
export function synthesizeSilence({ duration = 10, sampleRate = 44100 } = {}) {
return new MockAudioBuffer(2, Math.round(duration * sampleRate), sampleRate);