Epic 5 Phase 2.3 — instrument Assembly for measurement
Gallery, metadata, scene-gate and scene-sweep now handle kind:'model'
with a deterministic ActorSpec. Drawing uses a forked ActorGenerator
so the actor stream never shifts the identity/palette streams:
gallery.renderScene → generateActor per context for model stages
metadata.measureLibrary now includes model stages, SCHEMA 5,
fingerprint covers actors/
scene-gate.runSceneGate → alive/animates/deterministic/distinct/
param-sweep/flash-rate all drive Engine.setLayerSpecs with
actorSpec, and consumes: searches re-derive the actor for the
swapped identity (so the consumes: gate is still testable).
scene-sweep behaves the same per-cell.
Engine.setLayerSpecs now forwards actorSpec/framing into createLayer
so the checks don't need ArcDriver.
SCHEMA bump 4→5 forces metadata refresh on next gallery build,
which is intentional: model stages now participate. Gates remain
all green (69 scenes) and vite builds clean — no runtime change to
the render path for fragment stages.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
16bb67e703
commit
93a5dc8437
@ -27,6 +27,7 @@ import { describeIdentity } from '../look/Identity.js';
|
|||||||
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
|
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
|
||||||
import { frameLuminance, frameVariance } from '../engine/hash.js';
|
import { frameLuminance, frameVariance } from '../engine/hash.js';
|
||||||
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';
|
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';
|
||||||
|
import { generateActor } from '../actors/ActorGenerator.js';
|
||||||
|
|
||||||
const THUMB = { width: 256, height: 144 };
|
const THUMB = { width: 256, height: 144 };
|
||||||
|
|
||||||
@ -89,6 +90,11 @@ export function renderScene(engine, module, contexts) {
|
|||||||
|
|
||||||
const rng = new Rng(hashString(`${module.name}:${ctx.name}`));
|
const rng = new Rng(hashString(`${module.name}:${ctx.name}`));
|
||||||
const params = sampleValues(module, rng, ctx.bias, ctx.personality.temperament);
|
const params = sampleValues(module, rng, ctx.bias, ctx.personality.temperament);
|
||||||
|
const actorRng = new Rng(rng.fork(`actor:${module.actor || 'model'}`).seed);
|
||||||
|
const summary = ctx.track.summary;
|
||||||
|
const actorSpec = module.kind === 'model' && module.actor
|
||||||
|
? generateActor({ summary, rng: actorRng, archetype: module.actor, personality: ctx.personality, identity: ctx.personality.identity })
|
||||||
|
: null;
|
||||||
|
|
||||||
engine.setLayerSpecs([{
|
engine.setLayerSpecs([{
|
||||||
module,
|
module,
|
||||||
@ -98,6 +104,7 @@ export function renderScene(engine, module, contexts) {
|
|||||||
blend: 'normal',
|
blend: 'normal',
|
||||||
palette: ctx.palette,
|
palette: ctx.palette,
|
||||||
personality: ctx.personality,
|
personality: ctx.personality,
|
||||||
|
actorSpec,
|
||||||
}]);
|
}]);
|
||||||
engine.compositor.reset();
|
engine.compositor.reset();
|
||||||
// A few frames of warm-up so anything with state is past its first frame.
|
// A few frames of warm-up so anything with state is past its first frame.
|
||||||
@ -181,7 +188,7 @@ export function renderScene(engine, module, contexts) {
|
|||||||
export async function buildGallery({ contexts, onScene, only = null } = {}) {
|
export async function buildGallery({ contexts, onScene, only = null } = {}) {
|
||||||
const list = only
|
const list = only
|
||||||
? scenes.filter((m) => only.includes(m.name))
|
? scenes.filter((m) => only.includes(m.name))
|
||||||
: scenes.filter((m) => m.kind === 'fragment');
|
: scenes.filter((m) => m.kind === 'fragment' || m.kind === 'model');
|
||||||
|
|
||||||
const engine = new Engine({ ...THUMB });
|
const engine = new Engine({ ...THUMB });
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
|||||||
@ -50,11 +50,12 @@ const SOURCES = {
|
|||||||
...import.meta.glob('/src/look/Identity.js', { query: '?raw', import: 'default', eager: true }),
|
...import.meta.glob('/src/look/Identity.js', { query: '?raw', import: 'default', eager: true }),
|
||||||
...import.meta.glob('/src/look/Personality.js', { query: '?raw', import: 'default', eager: true }),
|
...import.meta.glob('/src/look/Personality.js', { query: '?raw', import: 'default', eager: true }),
|
||||||
...import.meta.glob('/src/look/palette.js', { query: '?raw', import: 'default', eager: true }),
|
...import.meta.glob('/src/look/palette.js', { query: '?raw', import: 'default', eager: true }),
|
||||||
|
...import.meta.glob('/src/actors/**/*.js', { query: '?raw', import: 'default', eager: true }),
|
||||||
...import.meta.glob('/src/audio/songbank.js', { query: '?raw', import: 'default', eager: true }),
|
...import.meta.glob('/src/audio/songbank.js', { query: '?raw', import: 'default', eager: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The version of the measurement itself. Bump to force a refresh of everything. */
|
/** The version of the measurement itself. Bump to force a refresh of everything. */
|
||||||
export const SCHEMA = 4;
|
export const SCHEMA = 5;
|
||||||
|
|
||||||
export function metricsFingerprint() {
|
export function metricsFingerprint() {
|
||||||
let h = 0x811c9dc5 >>> 0;
|
let h = 0x811c9dc5 >>> 0;
|
||||||
@ -130,7 +131,7 @@ export function measureLibrary({ onScene = null, contexts = null } = {}) {
|
|||||||
const engine = new Engine({ ...THUMB });
|
const engine = new Engine({ ...THUMB });
|
||||||
const out = {};
|
const out = {};
|
||||||
try {
|
try {
|
||||||
const list = scenes.filter((m) => m.kind === 'fragment');
|
const list = scenes.filter((m) => m.kind === 'fragment' || m.kind === 'model');
|
||||||
for (const module of list) {
|
for (const module of list) {
|
||||||
const { thumbs, variety, byBlock, descriptors, error } = renderScene(engine, module, ctx);
|
const { thumbs, variety, byBlock, descriptors, error } = renderScene(engine, module, ctx);
|
||||||
const bed = renderScene(engine, module, bedCtx);
|
const bed = renderScene(engine, module, bedCtx);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { frameLuminance, frameVariance, frameMaxDelta } from '../engine/hash.js'
|
|||||||
import { peakFlashRate } from '../engine/flash.js';
|
import { peakFlashRate } from '../engine/flash.js';
|
||||||
import { generatePersonality } from '../look/Personality.js';
|
import { generatePersonality } from '../look/Personality.js';
|
||||||
import { generateIdentity } from '../look/Identity.js';
|
import { generateIdentity } from '../look/Identity.js';
|
||||||
|
import { generateActor } from '../actors/ActorGenerator.js';
|
||||||
|
|
||||||
const PALETTE = [
|
const PALETTE = [
|
||||||
[0.06, 0.03, 0.16], [0.85, 0.15, 0.55], [0.15, 0.75, 0.95],
|
[0.06, 0.03, 0.16], [0.85, 0.15, 0.55], [0.15, 0.75, 0.95],
|
||||||
@ -63,13 +64,26 @@ export function runSceneGate(name) {
|
|||||||
engine.setFeatureProvider(featureProviderFor(track));
|
engine.setFeatureProvider(featureProviderFor(track));
|
||||||
|
|
||||||
const personality = generatePersonality(SUMMARY, new Rng(9001));
|
const personality = generatePersonality(SUMMARY, new Rng(9001));
|
||||||
|
// Model stages need a deterministic ActorSpec — derived the same way
|
||||||
|
// gallery/metadata derive it: fork from the scene seed + song context.
|
||||||
|
const actorSpec = (module.kind === 'model' && module.actor)
|
||||||
|
? generateActor({ summary: SUMMARY, rng: new Rng(4242).fork(`actor:${module.actor}`), archetype: module.actor, personality, identity: personality.identity })
|
||||||
|
: null;
|
||||||
const draw = (params, frame, seed = 4242) => {
|
const draw = (params, frame, seed = 4242) => {
|
||||||
|
// For model stages the per-draw ActorSpec stays consistent within a
|
||||||
|
// gate run (same personality), so geometry parity is testable.
|
||||||
|
const actor = actorSpec && module.kind === 'model' ? actorSpec : null;
|
||||||
engine.setLayerSpecs([{
|
engine.setLayerSpecs([{
|
||||||
module, params, seed, opacity: 1, blend: 'normal', palette: PALETTE, personality,
|
module, params, seed, opacity: 1, blend: 'normal', palette: PALETTE, personality, actorSpec: actor,
|
||||||
}]);
|
}]);
|
||||||
engine.compositor.reset();
|
engine.compositor.reset();
|
||||||
return Uint8Array.from(engine.readPixels(engine.renderFrame(frame)));
|
return Uint8Array.from(engine.readPixels(engine.renderFrame(frame)));
|
||||||
};
|
};
|
||||||
|
// Re-derive actor for consumptive identity probes — the identity is swapped.
|
||||||
|
const actorFor = (personalityOverride) => {
|
||||||
|
if (!(module.kind === 'model' && module.actor)) return null;
|
||||||
|
return generateActor({ summary: SUMMARY, rng: new Rng(31337).fork(`actor:${module.actor}`), archetype: module.actor, personality: personalityOverride, identity: personalityOverride.identity });
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- alive -------------------------------------------------------
|
// --- alive -------------------------------------------------------
|
||||||
@ -93,10 +107,13 @@ export function runSceneGate(name) {
|
|||||||
let closest = 255;
|
let closest = 255;
|
||||||
let closestName = '';
|
let closestName = '';
|
||||||
for (const other of scenes) {
|
for (const other of scenes) {
|
||||||
if (other === module || other.kind !== 'fragment') continue;
|
if (other === module) continue;
|
||||||
|
const otherActor = (other.kind === 'model' && other.actor)
|
||||||
|
? generateActor({ summary: SUMMARY, rng: new Rng(4242).fork(`actor:${other.actor}`), archetype: other.actor, personality, identity: personality.identity })
|
||||||
|
: null;
|
||||||
engine.setLayerSpecs([{
|
engine.setLayerSpecs([{
|
||||||
module: other, params: defaultValues(other), seed: 4242,
|
module: other, params: defaultValues(other), seed: 4242,
|
||||||
opacity: 1, blend: 'normal', palette: PALETTE, personality,
|
opacity: 1, blend: 'normal', palette: PALETTE, personality, actorSpec: otherActor,
|
||||||
}]);
|
}]);
|
||||||
engine.compositor.reset();
|
engine.compositor.reset();
|
||||||
const d = frameMaxDelta(base, Uint8Array.from(engine.readPixels(engine.renderFrame(600))));
|
const d = frameMaxDelta(base, Uint8Array.from(engine.readPixels(engine.renderFrame(600))));
|
||||||
@ -123,8 +140,11 @@ export function runSceneGate(name) {
|
|||||||
|
|
||||||
// --- flash rate ------------------------------------------------------
|
// --- flash rate ------------------------------------------------------
|
||||||
const hot = sampleValues(module, new Rng(77), { energy: 0.95, density: 0.9, motion: 0.9 });
|
const hot = sampleValues(module, new Rng(77), { energy: 0.95, density: 0.9, motion: 0.9 });
|
||||||
|
const hotActor = actorSpec && module.kind === 'model'
|
||||||
|
? generateActor({ summary: SUMMARY, rng: new Rng(99).fork(`actor:${module.actor}`), archetype: module.actor, personality, identity: personality.identity })
|
||||||
|
: null;
|
||||||
engine.setLayerSpecs([{
|
engine.setLayerSpecs([{
|
||||||
module, params: hot, seed: 99, opacity: 1, blend: 'normal', palette: PALETTE, personality,
|
module, params: hot, seed: 99, opacity: 1, blend: 'normal', palette: PALETTE, personality, actorSpec: hotActor,
|
||||||
}]);
|
}]);
|
||||||
engine.compositor.reset();
|
engine.compositor.reset();
|
||||||
const luminance = [];
|
const luminance = [];
|
||||||
@ -173,9 +193,10 @@ export function runSceneGate(name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const otherActor = actorFor(other);
|
||||||
engine.setLayerSpecs([{
|
engine.setLayerSpecs([{
|
||||||
module, params: defaultValues(module), seed: 4242,
|
module, params: defaultValues(module), seed: 4242,
|
||||||
opacity: 1, blend: 'normal', palette: PALETTE, personality: other,
|
opacity: 1, blend: 'normal', palette: PALETTE, personality: other, actorSpec: otherActor,
|
||||||
}]);
|
}]);
|
||||||
engine.compositor.reset();
|
engine.compositor.reset();
|
||||||
const changed = frameMaxDelta(base,
|
const changed = frameMaxDelta(base,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { sampleValues } from '../params/schema.js';
|
|||||||
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
|
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
|
||||||
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';
|
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';
|
||||||
import { THUMB } from './gallery.js';
|
import { THUMB } from './gallery.js';
|
||||||
|
import { generateActor } from '../actors/ActorGenerator.js';
|
||||||
|
|
||||||
/** How many songs the bank can offer. The grid cannot ask for more. */
|
/** How many songs the bank can offer. The grid cannot ask for more. */
|
||||||
export const MAX_SONGS = SONGS.length;
|
export const MAX_SONGS = SONGS.length;
|
||||||
@ -167,10 +168,13 @@ export async function sweepScene({ engine, module, cells, onCell }) {
|
|||||||
const rng = new Rng(ctx.seed ^ hashString(module.name));
|
const rng = new Rng(ctx.seed ^ hashString(module.name));
|
||||||
const params = sampleValues(module, rng, ctx.bias, ctx.personality.temperament);
|
const params = sampleValues(module, rng, ctx.bias, ctx.personality.temperament);
|
||||||
|
|
||||||
|
const actorSpec = module.kind === 'model' && module.actor
|
||||||
|
? generateActor({ summary: ctx.track.summary, rng: rng.fork(`actor:${module.actor}`), archetype: module.actor, personality: ctx.personality, identity: ctx.personality.identity })
|
||||||
|
: null;
|
||||||
engine.setLayerSpecs([{
|
engine.setLayerSpecs([{
|
||||||
module, params, seed: rng.int(0, 0x7fffffff),
|
module, params, seed: rng.int(0, 0x7fffffff),
|
||||||
opacity: 1, blend: 'normal',
|
opacity: 1, blend: 'normal',
|
||||||
palette: ctx.palette, personality: ctx.personality,
|
palette: ctx.palette, personality: ctx.personality, actorSpec,
|
||||||
}]);
|
}]);
|
||||||
engine.compositor.reset();
|
engine.compositor.reset();
|
||||||
for (let f = ctx.frame - 6; f < ctx.frame; f++) engine.renderFrame(f);
|
for (let f = ctx.frame - 6; f < ctx.frame; f++) engine.renderFrame(f);
|
||||||
|
|||||||
@ -55,9 +55,15 @@ export class Engine {
|
|||||||
setLayerSpecs(specs) {
|
setLayerSpecs(specs) {
|
||||||
this.ownedLayers.forEach((l) => l.dispose());
|
this.ownedLayers.forEach((l) => l.dispose());
|
||||||
const layers = specs.map((s) => {
|
const layers = specs.map((s) => {
|
||||||
|
// actorSpec is part of the spec for kind:'model' — passed through to
|
||||||
|
// ModelLayer at construction so build() sees it. For checks/gallery the
|
||||||
|
// caller generates it deterministically via ActorGenerator; Show's
|
||||||
|
// ArcDriver already did the same via look.actors.
|
||||||
const layer = createLayer(s.module, s);
|
const layer = createLayer(s.module, s);
|
||||||
if (s.palette) layer.setPalette(s.palette);
|
if (s.palette) layer.setPalette(s.palette);
|
||||||
if (s.personality) layer.setPersonality(s.personality);
|
if (s.personality) layer.setPersonality(s.personality);
|
||||||
|
if (s.actorSpec && layer.setActor) layer.setActor(s.actorSpec);
|
||||||
|
if (s.framing) layer.setFraming(s.framing);
|
||||||
return layer;
|
return layer;
|
||||||
});
|
});
|
||||||
this.ownedLayers = layers;
|
this.ownedLayers = layers;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user