music-video-gen/flow-state/src/checks/variety/print.js
Dejvino 656e062069 A seed variety test, and a metric that had to be gated before it was believed
Every other phase asks whether one video is correct. This asks whether two are
different — the failure the suite could not see, since a generator that ignores
its seed passes determinism, flash safety and liveness perfectly.

Frames reduce to a structural descriptor built to be blind to the cheap axes and
sensitive to the expensive ones: standardized luma kills exposure and palette, a
Laplacian pyramid gives the radial spectrum, and the gradient-angle histogram is
carried through a DFT magnitude so a rotation shifts it without moving it.
Colour is measured and never counted; its only job is to expose the case where
two seeds differ by a palette swap and nothing else.

The score means nothing on its own, so it sits between two references the same
instrument produced: a floor of how far one video travels from itself across its
own sections, and a ceiling of the same pipeline with every layer recast at
random. Two checks gate the instrument before any number from it is trusted —
recolour must move structure ~0 while moving colour a lot, and a quarter turn
must not move it at all.

Three things this got wrong first and now does not. Averaging each video's
probes into one descriptor washed out the structure being measured and put the
floor above the ceiling; probes are matched instead, which is fair because the
track is held fixed. Cosine distance on all-positive histograms scored unrelated
scenes at 0.05, too compressed to be read; chi-square replaces it. Single-link
clustering in the library sweep chained overlapping pairs into a fourteen-scene
group that did not exist; complete-link means every pair inside a group is
really a twin.

The sweep runs against all 61 visualizations, and it finds what the per-scene
distinct gate cannot, because that one compares raw pixels and structural twins
are merely differently coloured.

Both rendered gates fail as committed. That is the point of them: separation is
0.07 against a 0.35 target, two 4-cliques of scenes are one look each, and a
third of the library is never cast. The instrument passes; the generator does
not.

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

125 lines
6.3 KiB
JavaScript

// The variety report as text.
//
// Kept apart from the measurement so the gate and the diagnostic read the same
// numbers. The layout is chosen so the first three lines answer "is this bad",
// and everything below answers "where did the variety go" — which is the only
// question worth printing a table for.
import { FeatureTrack } from '../../audio/FeatureTrack.js';
import { synthesizeSectioned } from '../../audio/synth.js';
import { measureVariety, measureSpecDiversity, librarySweep } from './report.js';
import { generateLook, describeLook } from '../../look/LookGenerator.js';
const bar = (v, width = 24) => {
const n = Math.max(0, Math.min(width, Math.round(v * width)));
return '█'.repeat(n) + '·'.repeat(width - n);
};
const pct = (v) => `${(v * 100).toFixed(0)}%`.padStart(4);
export async function varietyReportLines({ seeds = 8, probes = 5, library = true } = {}) {
const track = FeatureTrack.fromAudioBuffer(
synthesizeSectioned({ bpm: 124, duration: 90, changeAt: 30 }), { fps: 60 });
const lines = [];
const spec = measureSpecDiversity(track, { seeds: Math.max(seeds, 24) });
// Yield so the "measuring…" line paints before the GPU work blocks.
await new Promise((r) => setTimeout(r, 0));
const r = measureVariety(track, { seeds, refScenes: 5, probes });
const ok = r.separation >= 0.35;
const headline = `seed separation ${r.separation.toFixed(2)} ` +
`(${ok ? 'acceptable' : 'TOO LOW'}) · ${pct(r.identity)} of what the library can express`;
lines.push('SEED VARIETY — one track, one analysis, only the seed changes');
lines.push('');
lines.push(` floor ${r.floor.toFixed(4)} one video against itself, across its own sections`);
lines.push(` observed ${r.observed.toFixed(4)} two seeds against each other`);
lines.push(` ceiling ${r.ceiling.toFixed(4)} same pipeline, every layer recast at random`);
lines.push('');
lines.push(` separation ${bar(r.separation)} ${r.separation.toFixed(2)}`);
lines.push(' 0 = the seed changes nothing a viewer could name');
lines.push(' 1 = two seeds as unalike as two randomly assembled videos');
lines.push('');
lines.push('WHERE THE VARIETY IS — per structural block, as a fraction of achievable');
lines.push('');
for (const [name, b] of Object.entries(r.byBlock)) {
const note = name === 'colour' ? ' (not counted — this is the axis that lies)' : '';
lines.push(` ${name.padEnd(8)} ${bar(b.ratio)} ${pct(b.ratio)}` +
` ${b.between.toFixed(3)} of ${b.ceiling.toFixed(3)}${note}`);
}
lines.push('');
lines.push(' scale feature size — fine texture vs large soft forms');
lines.push(' orient grid vs radial vs stripes, measured rotation-blind');
lines.push(' layout where in the frame the structure sits');
lines.push(' texture element count, sparsity, mirror and radial symmetry');
lines.push(' motion what moves and where, not how much');
lines.push('');
lines.push('CLOSEST SIBLING PER SEED — a seed below the floor is a duplicate video');
lines.push('');
r.nearest.forEach((d, i) => {
const flag = d < r.floor * 0.75 ? ' ← shadowed' : d < r.floor ? ' ← thin' : '';
lines.push(` seed ${r.seeds[i].toString(16).padStart(8, '0')} ${bar(d / Math.max(r.ceiling, 1e-6))} ${d.toFixed(3)}${flag}`);
});
if (r.worstPair) {
const w = r.worstPair;
const blocks = Object.entries(w.byBlock)
.map(([n, v]) => `${n} ${v.toFixed(3)}`).join(' · ');
lines.push('');
lines.push(` closest pair: seeds ${w.a} and ${w.b} at ${w.distance.toFixed(3)}${blocks}`);
}
lines.push('');
lines.push('THE DECISIONS BEHIND IT — spec-level, no rendering');
lines.push(' (high here + low above = the generator decides freely and the render flattens it;');
lines.push(' low here = casting is the bottleneck, and shader work will not fix it)');
lines.push('');
lines.push(` scene-set distance ${bar(spec.sceneSetDistance)} ${spec.sceneSetDistance.toFixed(2)} how differently ${spec.seeds} seeds cast`);
lines.push(` library coverage ${bar(spec.libraryCoverage)} ${pct(spec.libraryCoverage)} of non-accent scenes ever chosen`);
lines.push(` identical casts ${spec.identicalCasts} seed pairs`);
for (const key of ['director', 'paletteScheme', 'signature', 'grain', 'framing', 'paletteArc', 'anchorScenes']) {
const e = spec[key];
lines.push(` ${key.padEnd(20)} ${bar(e.normalized)} ${String(e.unique).padStart(3)} distinct (entropy ${e.entropy.toFixed(2)} bits)`);
}
lines.push('');
if (library) {
// The map the whole test is drawn on: a seed cannot reach more variety
// than the library holds, so a low separation is only the generator's
// fault once the library is known to hold distinct looks.
const sweep = librarySweep(track, { probes: 3 });
lines.push('THE LIBRARY — all ' + sweep.scenes.length + ' visualizations, every pair, colour not counted');
lines.push('');
lines.push(` median pair distance ${sweep.median.toFixed(3)} ` +
`(seed test ceiling for reference: ${r.ceiling.toFixed(3)})`);
lines.push(` closest 5% under ${sweep.p05.toFixed(3)}`);
lines.push('');
lines.push(` structural twins — every pair inside a group is under ${sweep.twinAt.toFixed(3)}:`);
if (sweep.twins.length === 0) {
lines.push(' no group of three or more; see the closest pairs below');
} else {
for (const group of sweep.twins) lines.push(` ${group.join(' ≈ ')}`);
}
lines.push('');
lines.push(' closest pairs in the library:');
for (const p of sweep.closestPairs) {
lines.push(` ${p.distance.toFixed(3)} ${p.name.padEnd(22)}${p.nearest} (${p.family})`);
}
lines.push('');
if (spec.uncast.length) {
lines.push(` never cast in ${spec.seeds} seeds: ${spec.uncast.join(', ')}`);
lines.push('');
}
}
lines.push('SAMPLE LOOKS');
lines.push('');
for (const seed of r.seeds.slice(0, 6)) {
lines.push(` ${describeLook(generateLook(track, { seed }))}`);
}
return { lines, ok, headline };
}