// 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 { song } from '../../audio/songbank.js'; import { measureVariety, measureSpecDiversity, librarySweep, measureSongVariety, measureFingerprint, measureDecomposition, } 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 } = {}) { // A real bank entry, not the two-section synthetic this used to run on. // That one segmented into intro and outro only, and quiet kinds are // restricted to the restful families for every director — a third of the // library was unreachable and the score was measuring that, not the seed. const track = song('centre').track; 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.ceilingValid && r.separation >= 0.35; const headline = r.ceilingValid ? `seed separation ${r.separation.toFixed(2)} (${ok ? 'acceptable' : 'TOO LOW'}) · ` + `${pct(r.identity)} of what the library can express` : `observed ${r.observed.toFixed(3)} vs floor ${r.floor.toFixed(3)} · ` + 'reference below the floor, separation not computable'; 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, casts that share no scenes at all`); lines.push(''); lines.push(r.ceilingValid ? ` separation ${bar(r.separation)} ${r.separation.toFixed(2)}` : ' separation — not computable: the reference landed below the floor,\n' + ' which means real seeds already differ by more than videos\n' + ' built from casts that share no scenes.'); 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('DIRECTION — of the floor above, how much is the video GOING somewhere'); lines.push(''); lines.push(` direction ${bar((r.direction + 1) / 2)} ${r.direction.toFixed(2)}` + ` (arcless reference ${r.directionFloor.toFixed(2)})`); lines.push(' rank correlation between how far apart two probes are in'); lines.push(' time and how far apart they look. A video with an arc is'); lines.push(' most unlike itself at its two ends; one that rotates a'); lines.push(' roster is as unlike itself after ten seconds as after four'); lines.push(' minutes. Both score the same floor. Read them together:'); lines.push(' a floor that rises WITH direction is a story, and a floor'); lines.push(' that rises without one is a shuffle.'); 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 castable 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 }; } /** * The SONG variety report. * * Deliberately laid out in the order the questions have to be answered. Are two * songs different at all; is that difference DERIVED from the music or merely * random; and what does every output have in common regardless — which is the * one that speaks to "you can tell what made it". */ export async function songVarietyReportLines({ songs = 6, probes = 5 } = {}) { const lines = []; await new Promise((r) => setTimeout(r, 0)); const r = measureSongVariety({ songs, probes }); const f = measureFingerprint({ songs: Math.min(songs, 5), probes: 3 }); const ok = r.ceilingValid && r.separation >= 0.45 && r.coupling >= 0.3; const headline = (r.ceilingValid ? `song separation ${r.separation.toFixed(2)}` : `observed ${r.observed.toFixed(3)} vs floor ${r.floor.toFixed(3)} (no valid ceiling)`) + ` · coupling ${r.coupling.toFixed(2)} (${ok ? 'acceptable' : 'TOO LOW'})`; lines.push('SONG VARIETY — different songs, each with its own audio-derived seed'); lines.push(''); for (const b of r.bank) lines.push(` ${b.name.padEnd(9)} ${b.kinds.join(' ')}`); 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 songs against each other`); lines.push(` ceiling ${r.ceiling.toFixed(4)} same pipeline, casts that share no scenes at all`); lines.push(''); lines.push(r.ceilingValid ? ` separation ${bar(r.separation)} ${r.separation.toFixed(2)}` : ' separation — not computable: reference below the floor'); lines.push(` coupling ${bar(Math.max(0, r.coupling))} ${r.coupling.toFixed(2)}`); lines.push(' coupling is how strongly musical distance predicts visual distance.'); lines.push(' near 0 means the look is unrelated to the song — separation without'); lines.push(' it is not variety, it is noise with a different seed per file.'); lines.push(''); lines.push('WHERE THE VARIETY IS'); lines.push(''); for (const [name, b] of Object.entries(r.byBlock)) { const note = name === 'colour' ? ' (not counted)' : ''; 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('CLOSEST SONG PAIRS — two songs that came out as one video'); lines.push(''); for (const p of r.pairs.slice(0, 6)) { lines.push(` ${p.total.toFixed(3)} ${p.a.padEnd(9)} ≈ ${p.b.padEnd(9)}` + ` (they sound ${p.musical < 0.25 ? 'alike' : p.musical > 0.5 ? 'nothing alike' : 'somewhat alike'}` + `, musical distance ${p.musical.toFixed(2)})`); } lines.push(''); lines.push('THE HOUSE FINGERPRINT — what every output has in common'); lines.push(' (variance across our videos as a fraction of variance across random ones;'); lines.push(' a low number is a constant the generator imposes on everything it makes)'); lines.push(''); for (const t of f.tells) { lines.push(` ${t.block.padEnd(8)} ${bar(Math.min(1, t.ratio))} ${pct(Math.min(1, t.ratio))}` + ` ${t.frozen}/${t.dims} dimensions effectively frozen`); } lines.push(''); return { lines, ok, headline }; } /** * The Epic 3 experiment: does a video built from STAGES beat one built from * comparable legacy scenes? * * Same songs, same instrument, same number of scenes available — the only * difference is whether those scenes draw the song's cast in the song's ink, or * their own content. Three arms, because two would not distinguish "stages are * better" from "a small pool is better". */ export async function experimentReportLines({ songs = 6, probes = 4, repeats = 3 } = {}) { const { scenes } = await import('../../scenes/registry.js'); const byName = (n) => scenes.find((m) => m.name === n); const arms = [ { label: 'STAGES — draw the song\'s cast and ink', pool: ['Procession', 'Constellation', 'Soloist', 'Swarm'].map(byName), }, { label: 'LEGACY — four comparable element-placing scenes', pool: ['Floating Geometry', 'Firefly Drift', 'Scale Mosaic', 'Metaballs'].map(byName), }, { label: 'FULL — the unrestricted generator, for reference', pool: null, }, ]; const lines = []; lines.push('EPIC 3 EXPERIMENT — container vs content'); lines.push(''); lines.push(' Same songs, same instrument, same pool size. The stages own arrangement'); lines.push(' and nothing else: what is on screen comes from the song. The legacy arm'); lines.push(' is four scenes that each invent their own content.'); lines.push(''); lines.push(' Lower floor = a video that looks like itself over its own length.'); lines.push(' Higher observed = two songs that look like different work.'); lines.push(''); // Repeats, with error bars. The first run of this comparison used seven // songs and put stages 14% ahead; at twelve songs the ordering flipped. A // difference that changes sign with the sample is a difference that has to // be reported with its spread or not at all. const results = []; for (const arm of arms) { const runs = []; for (let k = 0; k < repeats; k++) { await new Promise((r) => setTimeout(r, 0)); runs.push(measureSongVariety({ songs, probes, pool: arm.pool, seedSalt: k * 7919, })); } results.push({ arm, runs, r: runs[0] }); } const mean = (a) => a.reduce((x, y) => x + y, 0) / a.length; const half = (a) => (Math.max(...a) - Math.min(...a)) / 2; lines.push(` arm floor observed spread (${repeats} runs)`); lines.push(' ' + '-'.repeat(80)); for (const { arm, runs } of results) { const spreads = runs.map((x) => x.observed - x.floor); lines.push(` ${arm.label.padEnd(44)}${mean(runs.map((x) => x.floor)).toFixed(4)}` + ` ${mean(runs.map((x) => x.observed)).toFixed(4)}` + ` ${mean(spreads) >= 0 ? '+' : ''}${mean(spreads).toFixed(4)} ±${half(spreads).toFixed(4)}`); } lines.push(''); const gap = mean(results[0].runs.map((x) => x.observed - x.floor)) - mean(results[1].runs.map((x) => x.observed - x.floor)); const noise = Math.max( half(results[0].runs.map((x) => x.observed - x.floor)), half(results[1].runs.map((x) => x.observed - x.floor))); lines.push(` stages minus legacy: ${gap >= 0 ? '+' : ''}${gap.toFixed(4)} against a noise band of ±${noise.toFixed(4)}`); lines.push(` → ${Math.abs(gap) > noise * 2 ? 'a real difference' : 'INDISTINGUISHABLE at this sample size'}`); lines.push(''); for (const { arm, runs, r } of results) { lines.push(` ${arm.label}`); for (const [name] of Object.entries(r.byBlock)) { const v = runs.map((x) => x.byBlock[name].between); lines.push(` ${name.padEnd(8)} between ${mean(v).toFixed(3)} ±${half(v).toFixed(3)}`); } const c = runs.map((x) => x.coupling); lines.push(` coupling ${mean(c).toFixed(2)} ±${half(c).toFixed(2)}`); lines.push(''); } const ok = gap > noise * 2; const headline = `stages ${gap >= 0 ? '+' : ''}${gap.toFixed(4)} vs legacy, noise ±${noise.toFixed(4)} — ` + (Math.abs(gap) > noise * 2 ? (gap > 0 ? 'the inversion helps' : 'the inversion hurts') : 'indistinguishable'); return { lines, ok, headline }; } /** * How big should a track's casting pool be? * * The Epic 3 experiment found the roster size, not the content sharing, was * carrying most of the improvement — so the number deserves to be measured * rather than picked. Reported as spread, because both ends of it matter: a * one-scene pool would score perfectly on the floor and be unwatchable. */ export async function poolSweepLines({ songs = 6, probes = 4, sizes = null, repeats = 1 } = {}) { const list = sizes || [3, 4, 6, 8, 12, 18, 24, 40]; const lines = []; lines.push('CASTING POOL SWEEP — how many scenes one track may draw on'); lines.push(''); lines.push(' floor one video against itself, across its own sections'); lines.push(' observed two songs against each other'); lines.push(' spread the gap between them — the thing worth maximising'); lines.push(''); lines.push(' pool floor observed spread ratio distinct scenes/video'); lines.push(' ' + '-'.repeat(66)); const { generateLook } = await import('../../look/LookGenerator.js'); const { song } = await import('../../audio/songbank.js'); const track = song('centre').track; // Repeats exist because the first run of this sweep was pure noise: spread // was non-monotonic in pool size and peaked at the LARGEST pool, which is // the opposite of what the Epic 3 arms suggested. Each size draws a // different random pool per song, so a single run measures which scenes // happened to come up as much as it measures the size. Repeating with a // different draw and reporting the range is how to tell those apart. let best = null; for (const size of list) { await new Promise((r) => setTimeout(r, 0)); const runs = []; for (let k = 0; k < repeats; k++) { const rk = measureSongVariety({ songs, probes, poolSize: size, seedSalt: k * 7919, }); runs.push(rk); } const r = runs[0]; const spreads = runs.map((x) => x.observed - x.floor); const spread = spreads.reduce((a, b) => a + b, 0) / spreads.length; const range = repeats > 1 ? ` ±${((Math.max(...spreads) - Math.min(...spreads)) / 2).toFixed(4)}` : ''; // How many distinct scenes a video actually ends up showing, which is // the number a viewer experiences rather than the pool it was drawn from. let distinct = 0; for (let s = 0; s < 4; s++) { const look = generateLook(track, { seed: 900 + s * 7919, poolSize: size }); distinct += new Set(look.sections.flatMap( (sec) => sec.variants.flatMap((v) => v.map((l) => l.module.name)))).size / 4; } lines.push(` ${String(size).padStart(4)} ${r.floor.toFixed(4)} ${r.observed.toFixed(4)}` + ` ${spread >= 0 ? '+' : ''}${spread.toFixed(4)} ${(r.observed / r.floor).toFixed(3)}` + ` ${distinct.toFixed(1)}${range}`); if (!best || spread > best.spread) best = { size, spread }; } lines.push(''); lines.push(` widest spread at pool ${best.size} (+${best.spread.toFixed(4)})`); lines.push(''); lines.push(' Read the whole column, not the winner. A very small pool wins this'); lines.push(' metric by making every video repetitive, which the metric cannot see'); lines.push(' and a viewer cannot miss — pick the knee, not the peak.'); return { lines, ok: true, headline: `widest spread at pool ${best.size}` }; } /** * Identity versus container, measured separately. * * The one experiment that says whether Epic 3 can work at all: hold the stage * fixed and vary only the song's identity, then hold the identity fixed and * vary only the stage. */ export async function decomposeReportLines({ songs = 6, probes = 3 } = {}) { await new Promise((r) => setTimeout(r, 0)); const d = measureDecomposition({ songs, probes }); const lines = []; lines.push('IDENTITY vs CONTAINER — where visual difference actually comes from'); lines.push(''); lines.push(` ${d.songs} songs · one fixed track · ${d.stageCount} cast-consuming scenes`); lines.push(''); lines.push(` identity only ${bar(Math.min(1, d.identityOnly * 5))} ${d.identityOnly.toFixed(4)}`); lines.push(` one stage (${d.stage}), each song's cast, ink and lattice`); lines.push(''); lines.push(` container only ${bar(Math.min(1, d.containerOnly * 5))} ${d.containerOnly.toFixed(4)}`); lines.push(' one identity, four different stages'); lines.push(''); lines.push(` both ${bar(Math.min(1, d.both * 5))} ${d.both.toFixed(4)}`); lines.push(' what the generator actually does'); lines.push(''); lines.push(` neither ${bar(Math.min(1, d.sameBoth * 5))} ${d.sameBoth.toFixed(4)}`); lines.push(' the same thing rendered twice — the instrument\'s own noise floor'); lines.push(''); const ratio = d.containerOnly > 1e-6 ? d.identityOnly / d.containerOnly : 0; lines.push(` identity is worth ${(ratio * 100).toFixed(0)}% of what the container is worth.`); lines.push(''); if (d.identityOnly < d.sameBoth * 3) { lines.push(' IDENTITY IS NOT REACHING THE FRAME. Swapping every design decision a song'); lines.push(' makes moves the picture barely more than rendering the same thing twice.'); lines.push(' More registers cannot help until this number moves.'); } else if (ratio < 0.5) { lines.push(' Identity reaches the frame but the container still dominates. The stages'); lines.push(' express what they are given only weakly — worth fixing the stages before'); lines.push(' adding registers for them to ignore.'); } else { lines.push(' Identity carries as much as the container does. The inversion is working'); lines.push(' at the frame level, and the place to look next is whether the harness'); lines.push(' aggregates it away.'); } lines.push(''); return { lines, ok: ratio >= 0.5, headline: `identity ${d.identityOnly.toFixed(4)} vs container ${d.containerOnly.toFixed(4)} (${(ratio * 100).toFixed(0)}%)` }; }