Two cheap measurements settled a question three expensive ones had not, and both
of them contradicted the diagnosis offered for it.
The stated diagnosis was that the identity's expressive range had become the
bottleneck. It has not. A census of the identities themselves — no GPU, seconds
to run — puts mean distance between twelve songs at 0.43 with no near-identical
pairs and full coverage of every decision space: six of six fills, five of five
lattices, six of six protagonist forms, three of three element scales. The songs
are handed genuinely different designs.
The decomposition then asked whether those designs reach the picture, by holding
the container fixed and varying only the identity, then the reverse:
identity only 0.0299
container only 0.0557
both 0.1101
neither 0.0000
Identity is worth 54% of what the container is worth, against an instrument
noise floor of exactly zero, and the two compose to more than their sum. The
inversion works at the frame level. What it does not do is replace the container.
That corrects EPIC-3 §5, which proposed a song picking two to five stages on the
theory that shared content would substitute for container variety. Container
variety is the larger of the two effects and identity adds to it. Four stages
with a rich identity throws away the 0.056 the library was already providing —
which is exactly the shape of every measurement in this epic: stages have the
lowest floor of any arm and no advantage in spread.
The direction is therefore not a small set of stages. It is the whole library
consuming the cast: keep the sixty-one containers and make them draw the song's
content rather than their own. The migration was filed as a cost; it is the
payoff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
5.9 KiB
JavaScript
121 lines
5.9 KiB
JavaScript
// 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');
|