music-video-gen/flow-state/src/checks/phase12.js
Dejvino 2bbf5bcf42 Run both variety tests against the song bank, and fix the ceiling twice more
The tests now run on real bank entries. The seed gates used to run on a
two-section synthetic whose only kinds were intro and outro, so half the scene
library was unreachable and the number was measuring that rather than the seed.

Probes are labelled by section kind and occurrence rather than by index, which
is what makes two different songs comparable at all — they have different
section counts, so probe 3 of one is not probe 3 of the other, and matching by
position would compare a drop against an outro and score the mismatch as
variety. For two seeds of one song the labels are identical and this changes
nothing, which is the point.

The song test measures one thing the seed test does not: coupling, the rank
correlation between how different two tracks sound and how different their
videos look. Separation alone can be had by a generator that ignores the audio
and hashes the file, and that would be a perfect score for a completely wrong
video. Separation without coupling is not variety, it is a different seed per
file.

The ceiling took two more attempts. Recasting every layer at random averages a
dozen scenes together and a dozen random scenes converge on the same generic
busy image, so two references came out closer to each other than two real
videos and blocks scored over 100% of achievable. Forcing one scene per
reference collapsed the other way: a video that never changes scene has almost
no internal variation, so the ceiling landed BELOW the floor, which is a
within-video quantity. A reference has to match the structure of what it bounds.
They now keep the real pipeline — rosters, shots, per-section sampling, so a
reference rotates between three or four scenes exactly as a real video does —
while drawing from disjoint slices of the library. Same complexity, nothing in
common.

The fingerprint measurement had the same shape of error: pooling every probe
mixed in how much each video varies over its own length, which is large for
everything, and washed the answer to a flat 100% while the separation score said
almost everything was collapsed. One vector per video now.

Both tests fail as committed. Seed separation 0.04, song separation 0.03,
coupling -0.03 — two different songs differ from each other by about as much as
one video differs from itself, and that difference has no relationship to the
music. Colour scores 112%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:00:14 +02:00

181 lines
9.4 KiB
JavaScript

// Phase 12 — the SEED VARIETY TEST.
//
// Every other phase asks whether one video is correct. This one asks whether
// two videos are different, which is the failure the rest of the suite is
// structurally unable to see: a generator that ignores its seed passes
// determinism, flash safety, liveness and end-to-end rendering perfectly.
//
// The measurement lives in checks/variety/. The two checks that come first here
// are not about the generator at all — they are about the instrument. A
// structural variety score is worthless unless it can be shown to ignore the
// cheap axes (recolour, rotate) and to react to the expensive one (a different
// scene), so those are gated before any number derived from them is trusted.
//
// The library sweep in the middle is the map the rest is drawn on: the seed can
// only produce as much variety as the library holds, so "how many structurally
// distinct looks are there" is measured before "how many does a seed reach".
import { check, expect } from './framework.js';
import { song } from '../audio/songbank.js';
import { Show } from '../Show.js';
import { generateLook } from '../look/LookGenerator.js';
import { scenes } from '../scenes/registry.js';
import { frameDescriptor, rotate90, recolour } from './variety/descriptors.js';
import { descriptorDistance, signatureDistance } from './variety/signature.js';
import {
measureVariety, measureSpecDiversity, signatureForScene, librarySweep,
} from './variety/report.js';
// `centre` is the bank's null hypothesis: the middle of every axis, a full
// six-stage arrangement. The gates used to run on a two-section synthetic whose
// only kinds were intro and outro, which left half the scene library unreachable
// and made every number here a measurement of that rather than of the seed.
export const varietyTrack = () => song('centre').track;
// The library sweep is the most expensive thing in the suite — every scene
// rendered — and three checks read it. Computed once per page load.
let cachedSweep = null;
export function librarySweepCached() {
if (!cachedSweep) cachedSweep = librarySweep(varietyTrack(), { probes: 3 });
return cachedSweep;
}
/** One frame of one seed, rendered the normal way. */
function sampleFrame(seed = 12001, width = 160, height = 90) {
const track = varietyTrack();
const show = new Show({ width, height });
try {
show.useTrack(track, generateLook(track, { seed }));
const section = show.look.sections[Math.floor(show.look.sections.length / 2)];
const frame = section.startFrame + Math.floor((section.endFrame - section.startFrame) / 2);
show.engine.compositor.reset();
for (let f = Math.max(0, frame - 20); f < frame; f++) show.renderFrame(f);
return Uint8Array.from(show.readPixels(show.renderFrame(frame)));
} finally {
show.dispose();
}
}
check(12, 'seed variety · the metric ignores colour and rotation', () => {
const w = 160, h = 90;
const pixels = sampleFrame(12001, w, h);
const base = frameDescriptor(pixels, w, h);
// Hue-rotated and brightened: the same picture in a different palette, which
// is precisely the difference we refuse to count as variety.
const graded = descriptorDistance(base, frameDescriptor(recolour(pixels), w, h));
// Turned ninety degrees. Layout is EXCLUDED here by design — where structure
// sits in the frame is real information, and a metric blind to it could not
// see a library that centres everything.
const spun = rotate90(pixels, w, h);
const turned = descriptorDistance(base, frameDescriptor(spun.pixels, spun.width, spun.height));
const structural = ['scale', 'orient', 'texture'];
const worstGrade = Math.max(...structural.concat('layout').map((b) => graded[b]));
const worstTurn = Math.max(...structural.map((b) => turned[b]));
return expect(worstGrade < 0.05 && worstTurn < 0.02 && graded.colour > 0.2,
`recolour moves structure ${worstGrade.toFixed(4)} (colour block ${graded.colour.toFixed(3)}) · ` +
`rotate moves structure ${worstTurn.toFixed(4)}`);
});
check(12, 'seed variety · the metric separates different scenes from the same scene', () => {
// The instrument's other half: it must react to the thing that IS a
// difference. Two renders of one scene must land far below two renders of
// two scenes, or a low variety score would just be a blind metric.
const track = varietyTrack();
const pool = scenes.filter((m) => m.role !== 'accent' && m.kind === 'fragment');
const a = pool[0], b = pool[Math.floor(pool.length / 2)], c = pool[pool.length - 1];
const sigA = signatureForScene(track, a, 777, { probes: 2 });
const sigA2 = signatureForScene(track, a, 777, { probes: 2 });
const sigB = signatureForScene(track, b, 777, { probes: 2 });
const sigC = signatureForScene(track, c, 777, { probes: 2 });
const same = signatureDistance(sigA, sigA2).total;
const diff = Math.min(
signatureDistance(sigA, sigB).total,
signatureDistance(sigA, sigC).total,
signatureDistance(sigB, sigC).total,
);
return expect(same < 0.01 && diff > same * 8,
`same scene ${same.toFixed(4)} · different scenes ${diff.toFixed(4)} ` +
`(${a.name} / ${b.name} / ${c.name})`);
}, { slow: true });
check(12, 'seed variety · every visualization in the library is a distinct look', () => {
// Runs against ALL of them, not a sample. The existing per-scene gate does
// a version of this on raw pixels, where two scenes that are the same image
// in different colours pass comfortably; here colour is not counted, so a
// structural twin is visible as one.
//
// Twins are reported rather than failed on a fixed distance: what matters is
// that the library does not contain a CLUSTER of scenes that are one look
// wearing several names, because the casting code will happily "vary" a
// video by rotating between them.
const sweep = librarySweepCached();
const bigTwins = sweep.twins.filter((g) => g.length >= 3);
return expect(bigTwins.length === 0,
(bigTwins.length ? `structural twin groups: ${bigTwins.map((g) => g.join('≈')).join(' · ')}` : '') +
`${sweep.scenes.length} scenes · median distance ${sweep.median.toFixed(3)} · ` +
`closest ${sweep.closestPairs[0].name}${sweep.closestPairs[0].nearest} ` +
`at ${sweep.closestPairs[0].distance.toFixed(3)}`);
}, { slow: true });
check(12, 'seed variety · the generator casts a different show for a different seed', () => {
// Cheap, GPU-free, and the first thing to read when the rendered score
// drops: this says whether the generator ever DECIDED to make two different
// videos, before asking whether the pixels came out different.
const d = measureSpecDiversity(varietyTrack(), { seeds: 24 });
const problems = [];
if (d.sceneSetDistance < 0.6) problems.push(`scene sets only ${d.sceneSetDistance.toFixed(2)} apart`);
if (d.identicalCasts > 0) problems.push(`${d.identicalCasts} seed pairs cast identically`);
if (d.libraryCoverage < 0.6) {
problems.push(`only ${(d.libraryCoverage * 100).toFixed(0)}% of the library used ` +
`(never cast: ${d.uncast.slice(0, 6).join(', ')})`);
}
if (d.director.unique < 2) problems.push('one director for every seed');
return expect(problems.length === 0,
problems.length ? problems.join(' · ')
: `cast distance ${d.sceneSetDistance.toFixed(2)} · ` +
`library ${(d.libraryCoverage * 100).toFixed(0)}% · ` +
`${d.director.unique} directors · ${d.paletteScheme.unique} schemes · ` +
`${d.signature.unique} signatures · ${d.grain.unique} grain modes`);
});
check(12, 'seed variety · different seeds render structurally different videos', () => {
// THE gate. The threshold is a target the generator does not currently meet
// — a failure here is the known open defect, not a flaky check. The detail
// line carries the full breakdown so a run can be compared against the last
// one while the number is being moved.
const r = measureVariety(varietyTrack(), { seeds: 6, refScenes: 4, probes: 4 });
const problems = [];
// Against the floor: two seeds must differ by more than one seed differs
// from itself across its own sections. Below this the seed is decoration.
if (r.separation < 0.35) {
problems.push(`separation ${r.separation.toFixed(2)} (floor ${r.floor.toFixed(3)}, ` +
`observed ${r.observed.toFixed(3)}, ceiling ${r.ceiling.toFixed(3)})`);
}
// No seed may be shadowed by another. A healthy mean hides pairs that are
// the same video, and a viewer only ever sees the pair.
const shadowed = r.nearest.filter((d) => d < r.floor * 0.75).length;
if (shadowed) problems.push(`${shadowed} seeds shadowed by another seed`);
// Per block: it is not enough for the total to pass on colour-adjacent
// motion while every frame is composed the same way.
for (const [name, b] of Object.entries(r.byBlock)) {
if (name === 'colour') continue;
if (b.ratio < 0.25) problems.push(`${name} at ${(b.ratio * 100).toFixed(0)}% of achievable`);
}
const blocks = Object.entries(r.byBlock)
.map(([n, b]) => `${n} ${(b.ratio * 100).toFixed(0)}%`).join(' · ');
return expect(problems.length === 0,
(problems.length ? problems.join(' · ') + ' — ' : '') +
`separation ${r.separation.toFixed(2)} · ${blocks}`);
}, { slow: true });