// How much does the IDENTITY itself vary between songs? // // The measured state after Epic 3's first two slices: stages have the lowest // floor of any arm, so sharing content does make a video look like itself — but // they do not beat ordinary scenes on the distance BETWEEN songs. The diagnosis // offered for that was "the identity's expressive range is the bottleneck", // which is a claim, and claims of that shape have been wrong twice already in // this epic. // // It is also cheap to check, because it needs no GPU. If two songs' identities // are already far apart and their videos are not, the stages are failing to // express what they are given and more registers will not help. If the // identities themselves are clustered, the registers are the problem and the // diagnosis stands. // // node tools/identity-census.js // node tools/identity-census.js 17 use the whole bank import { songBank } from '../src/audio/songbank.js'; import { generateIdentity, FILLS, LATTICES } from '../src/look/Identity.js'; import { Rng, hashString } from '../src/engine/rng.js'; const COUNT = Number(process.argv[2]) || 12; const bank = songBank({ count: COUNT }); const identities = bank.map((e) => ({ name: e.name, id: generateIdentity(e.track.summary, new Rng(hashString(e.name)), e.track.sections.length), })); /** * Identity distance, 0..1, split by register. * * Categorical decisions (sides, fill, lattice) count as a flat 1 when they * differ, because a hexagon is not "nearer" a pentagon than a circle in any way * a viewer experiences. Continuous ones are scaled by the range they can take. */ function registerDistances(a, b) { const cat = (x, y) => (x === y ? 0 : 1); const num = (x, y, span) => Math.min(1, Math.abs(x - y) / span); const cast = [ cat(a.cast.protagonist.sides, b.cast.protagonist.sides), cat(a.cast.chorus.sides, b.cast.chorus.sides), cat(a.cast.protagonist.notchCount > 0, b.cast.protagonist.notchCount > 0), cat(a.cast.protagonist.hollow > 0, b.cast.protagonist.hollow > 0), num(a.cast.protagonist.round, b.cast.protagonist.round, 0.5), num(a.cast.protagonist.elong, b.cast.protagonist.elong, 0.7), ]; const ink = [ cat(a.ink.fill, b.ink.fill), cat(a.ink.outline > 0, b.ink.outline > 0), cat(a.ink.posterize > 0, b.ink.posterize > 0), num(a.ink.weight, b.ink.weight, 1), num(a.ink.edge, b.ink.edge, 1), ]; const staging = [ cat(a.lattice.kind, b.lattice.kind), num(a.lattice.jitter, b.lattice.jitter, 0.8), num(a.lattice.spread, b.lattice.spread, 0.7), num(a.lattice.scaleSpread, b.lattice.scaleSpread, 1), num(Math.log2(a.lattice.elementScale), Math.log2(b.lattice.elementScale), 2.8), ]; const mean = (v) => v.reduce((x, y) => x + y, 0) / v.length; return { cast: mean(cast), ink: mean(ink), staging: mean(staging) }; } const pairs = []; for (let i = 0; i < identities.length; i++) { for (let j = i + 1; j < identities.length; j++) { pairs.push({ a: identities[i].name, b: identities[j].name, ...registerDistances(identities[i].id, identities[j].id), }); } } const mean = (v) => v.reduce((x, y) => x + y, 0) / v.length; const bar = (v, w = 24) => '█'.repeat(Math.round(v * w)) + '·'.repeat(w - Math.round(v * w)); console.log(`\n${identities.length} songs · ${pairs.length} pairs\n`); console.log('IDENTITY DISTANCE BETWEEN SONGS — per register, 0 = the same decisions\n'); for (const reg of ['cast', 'ink', 'staging']) { const v = pairs.map((p) => p[reg]); const identical = v.filter((x) => x < 0.05).length; console.log(` ${reg.padEnd(8)} ${bar(mean(v))} ${mean(v).toFixed(2)}` + ` min ${Math.min(...v).toFixed(2)} max ${Math.max(...v).toFixed(2)}` + ` ${identical} pairs near-identical`); } console.log(` ${'overall'.padEnd(8)} ${bar(mean(pairs.map((p) => (p.cast + p.ink + p.staging) / 3)))}` + ` ${mean(pairs.map((p) => (p.cast + p.ink + p.staging) / 3)).toFixed(2)}`); // --- which decisions are actually being used ------------------------------ // A register can look varied on average while one option takes nine songs in // ten. Coverage of the option space is the other half of the question. console.log('\nOPTION COVERAGE — how much of each decision space the bank reaches\n'); const tally = (label, values, space) => { const counts = new Map(); for (const v of values) counts.set(v, (counts.get(v) || 0) + 1); const used = [...counts.entries()].sort((x, y) => y[1] - x[1]); console.log(` ${label.padEnd(16)} ${counts.size}/${space.length} used ` + used.map(([k, n]) => `${k}×${n}`).join(' ')); }; tally('ink fill', identities.map((e) => e.id.ink.fill), FILLS); tally('lattice', identities.map((e) => e.id.lattice.kind), LATTICES); tally('protagonist', identities.map((e) => String(e.id.cast.protagonist.sides)), ['0', '3', '4', '5', '6', '8']); tally('element scale', identities.map((e) => (e.id.lattice.elementScale < 0.2 ? 'tiny' : e.id.lattice.elementScale > 0.6 ? 'huge' : 'mid')), ['tiny', 'mid', 'huge']); console.log('\nCLOSEST IDENTITY PAIRS\n'); for (const p of pairs.slice().sort((x, y) => (x.cast + x.ink + x.staging) - (y.cast + y.ink + y.staging)).slice(0, 5)) { console.log(` ${((p.cast + p.ink + p.staging) / 3).toFixed(2)} ${p.a.padEnd(9)} ≈ ${p.b.padEnd(9)}` + ` cast ${p.cast.toFixed(2)} ink ${p.ink.toFixed(2)} staging ${p.staging.toFixed(2)}`); } console.log('\nREAD THIS AGAINST THE VISUAL NUMBERS\n'); console.log(' The stages arm separates songs by about 0.10 in visual distance against a'); console.log(' reference of roughly 0.19. If identity distance here is already high, the'); console.log(' stages are not expressing what they are handed and more registers will not'); console.log(' help. If it is low, the registers are the bottleneck and widening them is'); console.log(' the right next move.\n');