music-video-gen/flow-state/MIGRATION.md
Dejvino c3b67f45bd Record the migration's end-to-end numbers
Song separation went from not computable — the reference sat below the floor —
to 0.38, with the floor falling from 0.1449 to 0.1185 and the gap widening.
Every structural block moved: scale 40 to 59%, orient 65 to 89%, layout 60 to
111%, texture 89 to 81%, motion 53 to 65% of the reference.

Container variety rose throughout, 0.0557 to 0.0815 to 0.0901, which is the stop
condition holding: the library did not homogenise as it converged on shared
content.

Coupling did not move and is recorded as such. Whether a song looks different in
proportion to how it sounds different is still unsolved, and the migration was
never aimed at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 06:45:59 +02:00

249 lines
8.9 KiB
Markdown

# Migrating a scene onto the identity artifacts
The recipe `tools/migration-status.js` reports progress against. Sixty-one
scenes is too many to convert from taste, so this is written to be followed
mechanically and to produce the same result twice.
Background is EPIC-3.md. The one-line version: a scene draws its own content, a
migrated scene draws the *song's* content — its cast, in its ink, on its lattice
— and the measured payoff is additive with everything the scene already did.
```bash
node tools/migration-status.js # the queue, and what each scene needs
node tools/migration-status.js --all # every scene and its tier
```
---
## The rule that decides everything
> **An artifact is content a scene could not have invented for itself.**
If the scene still looks right with the identity switched off, it has used the
artifact as a modifier and the migration has not happened. That is not a style
preference — it is the exact failure mode of `u_sigSides`, which thirty scenes
declare and most quietly ignore, and it is why the gate below exists.
---
## The four tiers
The classifier assigns these from the shader source. Check its answer, don't
trust it: it reads patterns, and a scene that places elements in an unusual way
will be misfiled.
| tier | what it looks like | consumes |
|---|---|---|
| **drawn** | loops over discrete elements at computed positions | `cast, ink, staging` |
| **figure** | one or a few forms, its own composition | `cast, ink` |
| **field** | a continuous surface — noise, flow, terrain | `ink` |
| **treatment** | an effect over an image rather than an image | `ink` |
Accents are excluded. They are mostly-empty depth passes, not subjects.
---
## Recipe: `drawn`
The highest payoff and the least invention. Four edits.
**1. Declare it.** Add to the module, above `params`:
```js
consumes: ['cast', 'ink', 'staging'],
```
**2. Replace the primitive with the cast.** Whatever the loop was drawing —
a circle, a box, `sigShape`, a bespoke SDF — becomes one of:
```glsl
float d = castMain(q) * size; // the protagonist: large, few
float d = castChorus(q) * size; // the chorus: small, many
```
`q` is the element-local coordinate, `(p - pos) / size`. Multiplying the result
back by `size` restores it to scene units, which is what the ink expects.
Use `castMain` when the scene draws a handful of things and `castChorus` when it
draws a crowd. A scene that draws both should use both — that is what the two
members are for.
**3. Replace the placement with the lattice.**
```glsl
vec3 node = stageNode(fi, float(u_count)); // xy position, z scale multiplier
vec2 pos = node.xy;
float size = u_size * node.z;
```
Keep whatever the scene did that was *motion* — a drift, an orbit, a wander, a
march. Give up what was *composition*. The split is the point: identity owns
where things are, the scene owns what they do.
A scene whose composition IS its identity — a spiral that must be a spiral — can
keep it and take `stageScale()` alone, which is the song's element size. Declare
`staging` either way.
**4. Replace the edge with the ink.**
```glsl
col = mix(col, pal(i + 1), inkMask(d, uv));
```
`inkMask` does fill, fill treatment (hatch, stipple, halftone), outline and edge
hardness in one call. Delete the scene's own `smoothstep(soft, -soft, d)` and
its `sigEdge` — the ink supersedes both.
Then wrap the return:
```glsl
return vec4(inkValue(col), 1.0);
```
**Do not** delete `sigCamera`, `sigGrain`, `sigHorizonY` or `sigAir`. Traits and
artifacts are different layers and both still apply.
---
## Recipe: `figure`
Steps 1, 2 and 4, skipping the lattice. Declare `consumes: ['cast', 'ink']`.
Take `stageScale()` if the figure has a size worth scaling and add `staging` if
you do.
---
## Recipe: `field` and `treatment`
There are no elements to replace, so this is one edit plus a judgement.
```js
consumes: ['ink'],
```
```glsl
return vec4(inkValue(col), 1.0);
```
If the field already dithers, hatches or posterises internally, replace that
with `inkPattern(uv)` so the treatment is the song's rather than the scene's.
If it does not, `inkValue` alone is the whole migration — a value structure
shared across every scene in a video is worth having and costs one line.
Be honest about `treatment` scenes. Most of them are effects wearing a scene's
clothes, and EPIC-3 §8 argues they should move into the identity's EFFECTS
register rather than compete for screen time as subjects. Migrating one is a
holding action, not the answer.
---
## Verifying — the part that makes this reproducible
A migration is not done when the code looks right. It is done when the gate
passes:
```
checks.html?scene=<Scene%20Name>
```
Every artifact in `consumes` must show a passing line:
```
PASS consumes: cast delta 255/255 (floor 24)
PASS consumes: ink delta 255/255 (floor 24)
PASS consumes: staging delta 255/255 (floor 24)
```
That check renders the scene twice under two deliberately distant identities and
requires the picture to change. A scene that declares `cast` and ignores it
fails here, which is the only reason `consumes` can be trusted at library scale.
`npm run lint:scenes` enforces the other half in both directions: declaring an
artifact without calling it, and calling one without declaring it. The second
matters more than it looks — an undeclared artifact hides the scene from this
report and from anything that later selects scenes on capability.
Then the usual battery still applies. `renders something`, `animates`,
`deterministic`, `distinct`, `param sweep`, `flash rate` and every declared
trait must all still pass. A migration that breaks `distinct` has made the scene
into one of its neighbours, which is a real risk here: the more scenes share a
cast, the more two weakly-composed ones converge.
---
## Measuring the payoff
Per scene, the gate. Across the library, two numbers:
```
checks.html?decompose=1 identity against container, measured apart
checks.html?experiment=1 stages against legacy, with error bars
```
`decompose` is the one to watch. Before the migration, over the four purpose-
built stages:
```
identity only 0.0299 container only 0.0557 identity = 54% of container
```
After the first eighteen scenes, over the eight that consumed the cast:
```
identity only 0.1287 container only 0.0815 identity = 158% of container
```
Complete, over all thirty-two:
```
identity only 0.1054 container only 0.0901 identity = 117% of container
```
The number that matters in that row is `container only`, which went 0.0557 →
0.0815 → 0.0901 as the migration progressed. That is the stop condition below
holding: the library did not homogenise. `identity only` is noisier than it
looks because the scene it probes is picked from the migrated set and changes
between runs — Constellation, then Metaballs, then Floating Geometry — so read
it as "comparable to the container" rather than as a precise ratio.
End to end, song variety across twelve songs before and after the epic:
```
floor observed separation
before 0.1449 0.1508 not computable (the reference sat below the floor)
after 0.1185 0.1376 0.38
```
The floor fell and the gap widened, which is both halves of what the inversion
was supposed to buy. Every structural block moved with it — scale 40→59%,
orient 65→89%, layout 60→111%, texture 89→81%, motion 53→65% of reference.
Coupling did not move: -0.09, still indistinguishable from zero. Whether a song
LOOKS different in proportion to how it SOUNDS different remains unsolved, and
nothing in the migration addressed it.
Read that with its caveat: the stage set changed underneath the measurement, so
part of the jump is that Metaballs expresses a cast more strongly than
Constellation did rather than that the migration itself moved anything. What it
does establish is the thing worth knowing — a migrated LIBRARY scene carries the
identity better than a stage written from scratch to carry it. The bespoke
stages were the wrong shape of effort. As scenes migrate, the
`identity only` number should climb while `container only` holds — because the
whole point of the correction in EPIC-3 §9b is that these add rather than trade.
If `container only` falls as scenes migrate, the migration is homogenising the
library and should stop.
---
## A caution learned the expensive way
This harness's noise floor is large enough to invent findings. Three claims in
this epic were made from single runs and withdrawn after repeats: that roster
size was the dominant lever, that stages beat legacy by 14%, and that the
identity's range was the bottleneck. Anything under about 0.01 of spread needs
`&repeats=3` before it is believed, and a difference that changes sign with the
sample size is not a difference.
Migrate in batches, measure after each batch, and expect the per-batch effect to
be inside the noise. The trend across batches is the signal.