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 { frameLuminance, frameVariance } from '../engine/hash.js';
|
||||
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';
|
||||
import { generateActor } from '../actors/ActorGenerator.js';
|
||||
|
||||
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 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([{
|
||||
module,
|
||||
@ -98,6 +104,7 @@ export function renderScene(engine, module, contexts) {
|
||||
blend: 'normal',
|
||||
palette: ctx.palette,
|
||||
personality: ctx.personality,
|
||||
actorSpec,
|
||||
}]);
|
||||
engine.compositor.reset();
|
||||
// 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 } = {}) {
|
||||
const list = only
|
||||
? 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 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/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/actors/**/*.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. */
|
||||
export const SCHEMA = 4;
|
||||
export const SCHEMA = 5;
|
||||
|
||||
export function metricsFingerprint() {
|
||||
let h = 0x811c9dc5 >>> 0;
|
||||
@ -130,7 +131,7 @@ export function measureLibrary({ onScene = null, contexts = null } = {}) {
|
||||
const engine = new Engine({ ...THUMB });
|
||||
const out = {};
|
||||
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) {
|
||||
const { thumbs, variety, byBlock, descriptors, error } = renderScene(engine, module, ctx);
|
||||
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 { generatePersonality } from '../look/Personality.js';
|
||||
import { generateIdentity } from '../look/Identity.js';
|
||||
import { generateActor } from '../actors/ActorGenerator.js';
|
||||
|
||||
const PALETTE = [
|
||||
[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));
|
||||
|
||||
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) => {
|
||||
// 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([{
|
||||
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();
|
||||
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 {
|
||||
// --- alive -------------------------------------------------------
|
||||
@ -93,10 +107,13 @@ export function runSceneGate(name) {
|
||||
let closest = 255;
|
||||
let closestName = '';
|
||||
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([{
|
||||
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();
|
||||
const d = frameMaxDelta(base, Uint8Array.from(engine.readPixels(engine.renderFrame(600))));
|
||||
@ -123,8 +140,11 @@ export function runSceneGate(name) {
|
||||
|
||||
// --- flash rate ------------------------------------------------------
|
||||
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([{
|
||||
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();
|
||||
const luminance = [];
|
||||
@ -173,9 +193,10 @@ export function runSceneGate(name) {
|
||||
};
|
||||
}
|
||||
|
||||
const otherActor = actorFor(other);
|
||||
engine.setLayerSpecs([{
|
||||
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();
|
||||
const changed = frameMaxDelta(base,
|
||||
|
||||
@ -33,6 +33,7 @@ import { sampleValues } from '../params/schema.js';
|
||||
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
|
||||
import { descriptorDistance, STRUCTURAL } from './variety/signature.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. */
|
||||
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 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([{
|
||||
module, params, seed: rng.int(0, 0x7fffffff),
|
||||
opacity: 1, blend: 'normal',
|
||||
palette: ctx.palette, personality: ctx.personality,
|
||||
palette: ctx.palette, personality: ctx.personality, actorSpec,
|
||||
}]);
|
||||
engine.compositor.reset();
|
||||
for (let f = ctx.frame - 6; f < ctx.frame; f++) engine.renderFrame(f);
|
||||
|
||||
@ -55,9 +55,15 @@ export class Engine {
|
||||
setLayerSpecs(specs) {
|
||||
this.ownedLayers.forEach((l) => l.dispose());
|
||||
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);
|
||||
if (s.palette) layer.setPalette(s.palette);
|
||||
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;
|
||||
});
|
||||
this.ownedLayers = layers;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user