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:
Dejvino 2026-08-20 18:38:21 +02:00
parent 16bb67e703
commit 93a5dc8437
5 changed files with 48 additions and 9 deletions

View File

@ -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 = [];

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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;