// 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 }; }