// 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 { surfaceOf } from '../params/schema.js'; import { Engine } from '../engine/Engine.js'; import { defaultValues } from '../params/schema.js'; import { featureProviderFor } from '../audio/FeatureTrack.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 }); /** * How much of the frame a scene actually paints. * * A composable scene has to leave room for what it sits on. One that claims to * and covers the frame anyway will hide its background completely, which is the * failure the declaration exists to prevent — and it is not something you can * see from the source, only from the render. */ function coverageOf(engine, module) { engine.setLayerSpecs([{ module, params: defaultValues(module), seed: 4242, opacity: 1, blend: 'normal', palette: [[0.05, 0.05, 0.1], [0.9, 0.3, 0.5], [0.3, 0.8, 0.9], [0.95, 0.9, 0.4]], }]); engine.compositor.reset(); for (let f = 594; f < 600; f++) engine.renderFrame(f); const px = Uint8Array.from(engine.readPixels(engine.renderFrame(600))); let lit = 0; for (let i = 0; i < px.length; i += 4) { // Anything a viewer would read as painted rather than as backdrop. if (px[i] + px[i + 1] + px[i + 2] > 90) lit++; } return lit / (px.length / 4); } check(12, 'surface · a composable scene leaves room for what it sits on', () => { const engine = new Engine({ width: 128, height: 72 }); const track = varietyTrack(); engine.timeline.setDuration(track.duration); engine.setFeatureProvider(featureProviderFor(track)); const wrong = []; const measured = []; try { for (const module of scenes) { if (module.kind !== 'fragment') continue; const cover = coverageOf(engine, module); const surface = surfaceOf(module); measured.push({ name: module.name, surface, cover }); // A composable scene painting most of the frame hides its // background; a canvas leaving it nearly empty is a canvas in name // only and will read as a black frame when nothing is under it. if (surface === 'composable' && cover > 0.55) wrong.push(`${module.name} claims composable but covers ${(cover * 100).toFixed(0)}%`); if (surface === 'canvas' && cover < 0.08) wrong.push(`${module.name} claims canvas but covers only ${(cover * 100).toFixed(0)}%`); } } finally { engine.dispose(); } // Reported whatever the verdict, because the useful output of this check is // the list itself — it is how the library gets labelled in the first place. const sorted = measured.sort((a, b) => a.cover - b.cover); window.__COVERAGE__ = sorted; const sparse = sorted.filter((m) => m.cover < 0.3).length; return expect(wrong.length === 0, (wrong.length ? wrong.slice(0, 4).join(' · ') + ' — ' : '') + `${measured.length} scenes · ${sparse} paint under 30% of the frame ` + `· sparsest ${sorted[0].name} at ${(sorted[0].cover * 100).toFixed(0)}%`); }, { slow: true });