A section used to be one scene, and two thirds of the library is composable —
sparse by design, elements ON something. Cast as backgrounds anyway, they left
9 of 40 sampled frames under 20% painted, the darkest at 0.3%: a minute and a
half of a few bright things on black, invisible to every gate because every
gate on the stack was a limit rather than a floor.
Every section now stands on a GROUND: a canvas that fills the frame, cast per
section kind so a shot cut changes the shot and not the world. When the shot
fills the frame itself it IS the ground — two canvases stacked is two pictures
fighting. Above that, a coverage BUDGET: director appetite times the section's
energy times where the story is, capped at two frames' worth of material.
The measured facts move into the repo. scenes/metadata.json is generated from
the gallery — coverage as a shot, coverage as a bed, variety, the structural
profile — tracked in git, stamped with a fingerprint of the scenes and the
metric definitions, and refreshed from gallery.html. `surface` is derived from
it rather than declared; nine scenes claimed `canvas` while painting under a
third of the frame, and declaring it is now a lint error. The generator weights
every layering choice by measured structural distance, because family labels
and the render disagree: two `geometric` scenes can be 0.31 apart and a `flow`
and an `organic` scene 0.04.
The gallery's 0.1 red line is gone. It was right when a section was one scene
and wrong now — nineteen scenes were failing a bar for being consistent, which
is a virtue in an ingredient.
Chasing the numbers turned up four real faults:
* A scene that reads prev() cannot be a ground. It returns the whole
composited frame including the layers above it, so a datamosh under a shot
is eating it: the render stopped reproducing from a seek and two WebGL
contexts diverged by 91/255 against a tolerance of 4.
* Screen was the wrong operator for a shot over a bed. It lightens, so a
median quarter of every frame clipped to paper and whole sections rendered
100% white. Replaced by a lumakey — the shot's brightness is its alpha.
* Feedback was an accumulator: a still image settled at 2.3x its own
brightness. Fine over black, fatal over a filled ground. Normalised at 0.6,
plus a highlight shoulder so the top rolls off instead of clipping.
* useTrack never prewarmed, so a fresh Show's first frame differed from every
later render of it — the export-breaking hazard Compositor.prime documents.
Blazing is a decision now, not a side effect: directors declare an appetite for
it, a section must be loud and late in the story to earn one, and quiet kinds
never do. The ceiling gate matches that — a hard cap per section, and no more
than a fifth of them hot at all.
Rendered across twelve videos, middle of every section:
painted 51% mean, darkest 0.3% -> 87% mean, darkest 43%
clipped white 24% median, worst 100% -> 1% mean, worst 30%
separation 0.10 -> 0.44
Seven scenes can ground a section — five geometric, two organic — so every
quiet section of every video stands on one of two beds. That is the library's
largest hole and it is scene work: there is no minimal or flow canvas that
fills half the frame without reading prev().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
585 lines
31 KiB
Markdown
585 lines
31 KiB
Markdown
# Epic 4 — the song tells a story
|
||
|
||
Epic 3 asked what is on screen. This one asks a question none of the existing layers can
|
||
answer:
|
||
|
||
> Why is this the *last* drop rather than the first one?
|
||
|
||
Nothing in the generator knows. And a viewer who cannot tell the difference is watching a
|
||
loop with good production design.
|
||
|
||
---
|
||
|
||
## 1. What is actually missing
|
||
|
||
The generator has four timescales, and `ArcDriver`'s header lists them honestly: per frame,
|
||
per shot, per section, whole song. Three of the four are cyclic or local. The fourth — the
|
||
"whole song" one — turns out to be much thinner than it reads:
|
||
|
||
* **`_slowAxisFor`** is the only thing in the system that travels one way across the track.
|
||
It moves one or two params per scene, `journey = smoothstep(t/duration)`, in a direction
|
||
picked by `rng.bool()`. It is real progression, and it is blind: it does not know where the
|
||
drop is, it does not know a section from a boundary, and half the time it travels the wrong
|
||
way for what the song is doing.
|
||
* **`paletteArc.underDrift`** — a bounded hue crawl, plus per-*kind* offsets.
|
||
* **`buildSlope`** — one bar of lookahead. Local by design.
|
||
|
||
Everything else that decides what a video looks like is keyed on section **kind**, and kinds
|
||
recur:
|
||
|
||
```
|
||
assignRostersByKind() roster per KIND — every drop cuts between the same visuals
|
||
KIND_ENERGY / biasFor() same energy, density, motion for every drop
|
||
rhythmFor(energy) same cutting pattern for every section of that energy
|
||
derivePaletteArc() kindHue.drop — the same hue offset at every drop
|
||
frameShot(style, ...) framing from previous shot + energy; no notion of when
|
||
```
|
||
|
||
So the fourth drop is cast from the same roster as the first, biased to the same energy,
|
||
cut at the same rate, tinted the same hue and framed by the same rule. The *content* of the
|
||
video is a function of `kind`, and `kind` has no arrow of time in it. That is precisely a
|
||
song structure without a story: recurrence without consequence.
|
||
|
||
This is not a bug in any of those modules. Kind-keying is what gave the video its identity
|
||
(§`assignRostersByKind`), and it should stay. What is missing is the second coordinate.
|
||
|
||
> Today a section is identified by **what kind of thing it is**.
|
||
> It should be identified by **what kind of thing it is, and where in the story it sits**.
|
||
|
||
---
|
||
|
||
## 2. The proposal, in one paragraph
|
||
|
||
A new pure module, `look/Story.js`, runs once per track between segmentation and the look
|
||
generator. It reads the section list and the summary, picks a **plot** the way
|
||
`directors.js` picks a director, locates the song's **key moments**, and emits a small
|
||
**narrative state** per section and per frame. Everything that currently keys on `kind`
|
||
keys on `(kind, story)` instead. No scene changes, and — in the first slice — no new
|
||
uniforms: the story acts by rewriting inputs the whole library already consumes, so it
|
||
applies to all 61 scenes on day one.
|
||
|
||
---
|
||
|
||
## 3. What a story is, here
|
||
|
||
Three layers, smallest to largest.
|
||
|
||
### Position — where a section sits
|
||
|
||
Derived, not invented. From the sections `segment.js` already produces:
|
||
|
||
```js
|
||
{
|
||
index, kind,
|
||
ordinal, ordinalOf, // "the 3rd of 4 drops" — the single most useful missing fact
|
||
act, // setup | development | turn | climax | resolution
|
||
isMoment, // one of the key moments below
|
||
}
|
||
```
|
||
|
||
`ordinal` alone unlocks most of the devices in §5, and costs a `Map` and a loop.
|
||
|
||
### Moments — the frames the story turns on
|
||
|
||
Measured from the track, never imposed. A short list, and each one has a definition that
|
||
falls out of data already on the FeatureTrack:
|
||
|
||
| moment | definition |
|
||
|---|---|
|
||
| **arrival** | first section whose energy clears 0.6 × max — the video's first "here it is" |
|
||
| **turn** | largest energy *fall* between adjacent sections after the arrival |
|
||
| **climax** | max-energy section; ties broken toward the later one |
|
||
| **resolution** | first section after the climax whose energy stays below it |
|
||
|
||
A two-section track collapses these onto each other, and that is fine — a track with no
|
||
structure gets almost no story, which is the correct behaviour and not a degenerate case
|
||
to defend against.
|
||
|
||
### Plot — what the track does with them
|
||
|
||
One coherent narrative shape per track, chosen weighted-random with the audio tilting the
|
||
odds, exactly the way `pickDirector` works, and for the same reason: a fixed mapping from
|
||
measured features to narrative is how a library ends up with one story per genre.
|
||
|
||
| plot | shape |
|
||
|---|---|
|
||
| **emergence** | almost nothing, then something. Reveal rises monotonically and stays. |
|
||
| **escalation** | each recurrence of a kind is further than the last. A ratchet, not a curve. |
|
||
| **collapse** | order → entropy. The climax is a breakdown of the thing, not a peak of it. |
|
||
| **return** | ABA. The outro rhymes with the intro, transformed by what happened between. |
|
||
| **unveiling** | the protagonist is withheld until the climax and then it is all there is. |
|
||
|
||
Each plot is a set of curves over five **story variables**, evaluated per frame:
|
||
|
||
```
|
||
tension 0..1 how hard everything is pushed
|
||
reveal 0..1 how much of the song's identity has been shown
|
||
closeness 0..1 wide and distant → close and involved
|
||
population 0..1 sparse → crowded → alone
|
||
order 0..1 regular → broken (or the reverse; the plot decides the sign)
|
||
```
|
||
|
||
These are *staged* curves, not ramps: they hold flat inside a section and move at
|
||
boundaries, with a step at the climax. That is what makes them read as a story rather than
|
||
as a slow zoom — a story advances in scenes.
|
||
|
||
---
|
||
|
||
## 4. Where it plugs in
|
||
|
||
Every one of these is an existing call site gaining an argument.
|
||
|
||
| site | change |
|
||
|---|---|
|
||
| `biasFor()` | tension modulates energy/density **within the kind's envelope**, bounded to ±0.2, so a breakdown at high tension is still a breakdown |
|
||
| `sampleValues(…, temperament)` | `extremity` scaled by tension — the ratchet for **escalation** |
|
||
| `assignRostersByKind()` | roster stays per kind; *which member plays* becomes a story decision (§5.1) |
|
||
| `buildStack()` | overlay chance follows `population`, not just energy |
|
||
| `rhythmFor()` | later acts pick from faster patterns; the resolution gets a held shot |
|
||
| `frameShot()` | `closeness` biases the size draw |
|
||
| `derivePaletteArc()` | new `'narrative'` mode keyed on act rather than on kind |
|
||
| `ArcDriver._paramsAt()` | `journey` comes from `story.journeyAt(frame)`; the slow axis gets its **sign from the plot**, not from `rng.bool()` |
|
||
| `ArcDriver.update()` | passes a story-shifted personality clone, memoised on rounded `reveal` exactly as `_paletteAt` memoises on rounded shift |
|
||
|
||
That last one is how the story reaches Epic 3's content registers without a new uniform:
|
||
`setPersonality` is already called every frame, and the identity uniforms are derived from
|
||
the personality object. Scaling `notchDepth`, `hollow`, `inkOutline`, `posterize` and
|
||
`latScaleSpread` toward their full values as `reveal` rises makes the song's cast literally
|
||
arrive over the course of the video.
|
||
|
||
---
|
||
|
||
## 5. The devices, ranked by legibility per unit of work
|
||
|
||
**5.1 The anchor is earned.** Today `roster[0]` opens every section of its kind. Instead,
|
||
reserve it: earlier drops play companions, and the anchor arrives at the climax. Same
|
||
roster, same identity, and now the biggest visual in the video lands on the biggest moment.
|
||
Roughly twenty lines.
|
||
|
||
**5.2 Recapitulation.** Under the **return** plot, the outro re-casts the intro's scene,
|
||
with the story's parameters rather than the intro's. The oldest device in music video and
|
||
the cheapest one here — the scene is already built and cached.
|
||
|
||
**5.3 The ratchet.** Under **escalation**, `ordinal/ordinalOf` scales temperament extremity
|
||
and slow-axis travel per recurrence. The fourth drop is measurably further out than the
|
||
first, on every param the scenes declare.
|
||
|
||
**5.4 Reveal schedule.** §4's identity scaling. Under **unveiling** the protagonist's
|
||
`u_cast*` form is a near-circle until the climax, then snaps to full at a downbeat.
|
||
|
||
**5.5 Punctuation.** A budget of **two or three single-use events** for the entire video,
|
||
spent at the moments in §3 — the only cut to black, the only symmetry-fold flip, the only
|
||
feedback reset. Single-use is the whole point: a device used twice is a style, used once it
|
||
is a moment. These are the only additions that need a flash-gate review.
|
||
|
||
---
|
||
|
||
## 6. How to know if it worked
|
||
|
||
The existing instrument can measure this almost unmodified. Build the section×section
|
||
descriptor distance matrix that `checks/variety` already knows how to produce, and ask
|
||
three questions of it:
|
||
|
||
* **Direction.** Does distance correlate with |i−j| beyond what kind explains? Today this is
|
||
~0 by construction: the matrix is kind-blocked, all drops mutually near, and time is
|
||
invisible. A story makes it a gradient. This is the headline number.
|
||
* **Recurrence.** For a repeated kind, is `d(first, last) > d(first, middle)`? That is the
|
||
ratchet, and it is the one a viewer names as "it kept going somewhere".
|
||
* **Coherence bound.** Adjacent-section distance must stay under the existing ceiling. A
|
||
story that maximises Direction by shuffling scenes is the failure mode, and this is the
|
||
gate that catches it.
|
||
|
||
Plus: each punctuation fires exactly once, on a downbeat, under the flash limit; and every
|
||
curve is a pure function of frame, so seek-exactness and the determinism grep hold.
|
||
|
||
**A prediction, stated in advance:** the seed-variety *floor* — how far a video travels from
|
||
itself — will **rise**, because that is what progression is. Epic 3 spent its effort pushing
|
||
that number down. Both are correct, and the instrument is what is wrong: it measures
|
||
distance and calls it drift, with no way to tell wandering from travelling. The fix is one
|
||
extra statistic, not a retreat from the feature — split the self-distance into an *ordered*
|
||
component (monotone with time; a story) and an *unordered* residual (a shuffle). Ship that
|
||
statistic **before** the feature, or the first honest measurement of Epic 4 will read as a
|
||
regression and be argued about instead of read.
|
||
|
||
---
|
||
|
||
## 7. Risks
|
||
|
||
**The story overrides the song.** A plot that declares a climax where the track is quiet is
|
||
worse than no plot. Mitigation is structural: moments are *found* in the audio (§3), never
|
||
placed by the seed, and tension is bounded inside the kind envelope so the quiet-kind
|
||
coupling in `directors.js` — the one that keeps an intro off a strobing scene — still holds
|
||
absolutely.
|
||
|
||
**Every video tells the same story.** The exact failure `directors.js` was written to fix.
|
||
Same mitigation: five plots, weighted, with seeded curve parameters inside each.
|
||
|
||
**It becomes a slow zoom.** If the curves are smooth ramps, this is an effect, not a
|
||
narrative. Staged curves with plateaus and a step at the climax are load-bearing, not a
|
||
refinement.
|
||
|
||
**Short tracks.** Under three sections, most of this has nothing to work with. Degrade to
|
||
the current behaviour explicitly rather than letting the curves do something arbitrary.
|
||
|
||
---
|
||
|
||
## 8. The smallest experiment worth running first
|
||
|
||
Do not build five plots on a prediction. Three changes, no new uniforms, no scene edits:
|
||
|
||
1. `Story.js` with position and moments only — no plot templates, one hardcoded
|
||
**escalation** curve.
|
||
2. Wire it to exactly two sites: the slow-axis sign/magnitude in `_paramsAt`, and the anchor
|
||
reservation in §5.1.
|
||
3. Add the **Direction** statistic to the variety report and run the song bank.
|
||
|
||
The prediction is specific: **Direction moves off zero and Coherence holds**, while the
|
||
between-song distance is unchanged — the story should differentiate a video *from itself in
|
||
time*, and have no opinion about other songs. If Direction does not move, the story is not
|
||
reaching the image and the rest of the epic is worth nothing until it does.
|
||
|
||
---
|
||
|
||
## 11. What was built, and what it measured
|
||
|
||
Built, in `look/Story.js` plus one argument at each call site listed in §4:
|
||
|
||
* position (`ordinal`/`ordinalOf`/act), the four moments, five plots with seeded curves;
|
||
* the anchor is earned (§5.1), the ratchet on temperament (§5.3), recapitulation (§5.2),
|
||
the reveal schedule on the identity uniforms (§5.4), story-driven cut rate, framing
|
||
closeness, and a `narrative` palette-arc mode;
|
||
* the slow axis now takes its **direction from the track** rather than a per-scene coin
|
||
flip, and its journey from the staged story curve;
|
||
* Phase 13 (`checks/phase13.js`, 8 checks, no GPU) and the **direction** statistic in the
|
||
variety report (`checks/variety/signature.js`).
|
||
|
||
Punctuation (§5.5) was **not** built — it is the only part that needs a flash-gate review,
|
||
and it is worth doing after the numbers below are understood rather than before.
|
||
|
||
### The first direction measurement
|
||
|
||
`checks.html?variety=1&library=0&seeds=6`, one song, against the arcless single-scene
|
||
reference:
|
||
|
||
```
|
||
floor 0.1697 direction -0.14
|
||
observed 0.1594 arcless ref 0.01
|
||
ceiling 0.1996
|
||
```
|
||
|
||
The prediction in §8 was that direction moves off zero **upward**. It did not. Three
|
||
readings, in the order they should be checked:
|
||
|
||
1. **The recapitulation is fighting the statistic, by construction.** Roughly two videos in
|
||
five recap, and `return` — the plot most likely to — is an arch that comes back. Its
|
||
first and last probes are *deliberately* similar, which is exactly what a negative rank
|
||
correlation between time separation and distance means. The statistic as written cannot
|
||
tell ABA from no story at all; it may need to be measured against the journey curve
|
||
rather than against clock time.
|
||
2. **Six seeds of one song is a small sample**, and the probe count (5) makes each video's
|
||
correlation rest on ten pairs.
|
||
3. **The arc may not be reaching the image**, which is the reading that matters and the one
|
||
§8 was written to expose. If 1 and 2 are controlled for and direction stays at zero, the
|
||
story is moving parameters that do not change the picture — the same failure the slow
|
||
axis had before scenes declared `slowAxis`, and the fix would be the same: name the
|
||
levers rather than guessing at them.
|
||
|
||
The no-story control arm (the same report with the story layer bypassed) **did not
|
||
complete** — the run hung in the browser after the first arm, so the floor and separation
|
||
figures above are not yet attributable to this work either way. That comparison is the next
|
||
thing to run, and it should be run before any conclusion is drawn from the numbers.
|
||
|
||
---
|
||
|
||
## 12. The second direction measurement — reading 3 was right
|
||
|
||
Same invocation, `checks.html?variety=1&library=0&seeds=6`, after the two fixes in §12.1:
|
||
|
||
```
|
||
§11 now
|
||
direction -0.14 +0.24 arcless reference -0.02
|
||
floor 0.1697 0.2130
|
||
```
|
||
|
||
§8 predicted direction moves off zero **upward** and that the floor **rises**, because that
|
||
is what progression is. Both happened, and the epic's headline hypothesis is confirmed.
|
||
|
||
Of the three readings offered in §11, **reading 3 was the correct one**: the arc was not
|
||
reaching the image. The story layer was never the problem — it had nothing to speak
|
||
through. Two of the channels a narrative would have to travel down were inert, and neither
|
||
failure was visible from the source:
|
||
|
||
**The overlay path was structurally dead.** `buildStack` gated layering on
|
||
`surfaceOf(m) === 'composable'`, and no scene in the library declared `surface` — so the
|
||
only scene that could ever sit on top was the one declaring `role: 'accent'`, which the
|
||
overlay roster excluded by construction. Empty intersection, every time: **0/144 stacks
|
||
carried an overlay**. `population` had been wired to the overlay chance per §4 and moved a
|
||
number nothing read. Fixed by labelling the library from the phase 12 coverage gate — the
|
||
37 scenes painting under 30% of the frame are composable — and collapsing the reserved
|
||
accent slot into one path. 1 distinct overlay scene became 27, at 34% of stacks.
|
||
|
||
**The camera was inert.** `framing.shift` moved the frame by a median of **0.029** of a
|
||
half-frame at a fresh uniform angle every shot, so successive offsets cancelled and the
|
||
median jump at a cut was **0.014**. `closeness` reached shot SIZE per §4; the recentre got a
|
||
per-track constant and a die roll. Fixed by `look/Camera.js` — see §12.1. Median offset is
|
||
now 0.190 and the median reframe 0.135.
|
||
|
||
This is the same failure mode the slow axis had before scenes declared `slowAxis`, exactly
|
||
as reading 3 predicted, and it had the same shape: a lever that existed, was wired, was
|
||
covered by gates, and moved nothing. **Both passed every check that existed**, because every
|
||
bound on them was a ceiling. A device doing nothing clears a ceiling comfortably.
|
||
|
||
> The lesson worth carrying out of this epic: a gate on a *device* needs a floor, not only a
|
||
> limit. Three of the four new camera checks are floors for this reason.
|
||
|
||
Readings 1 and 2 are still uncontrolled — the recap arch and the small sample both still
|
||
apply, and the honest statement is that direction was measured under them both times, so the
|
||
*change* is attributable even though neither absolute figure is clean.
|
||
|
||
### 12.1 Built since §11
|
||
|
||
* `look/Camera.js` — the director's camera department, and the answer to "who translates
|
||
story into imagery". Story says tension, order and act; this turns that into where the
|
||
frame looks and how it travels there. Each director in `directors.js` names a camera.
|
||
Jump distance follows tension and act, speed follows energy, curve is one of four, and a
|
||
cut chooses between reframing and matching so two scenes can still read as one place.
|
||
94% of shots now move *during* the shot — the recentre is no longer per-shot constant,
|
||
which is a deliberate departure from the rule in `framing.js` (that rule was right about
|
||
size and wrong about where the camera is looking).
|
||
* `surface` declared across the library, `role: 'accent'` retired in favour of
|
||
`surface: 'composable'` plus `background: false`, and one layering path instead of two.
|
||
* Four camera gates in phase 11, each with a floor.
|
||
|
||
---
|
||
|
||
## 13. What is left
|
||
|
||
Ranked. Three of the four are measurements, and they all report through an instrument that
|
||
is currently broken — hence the ordering.
|
||
|
||
**13.1 The seed-variety instrument does not report.** Not in the original plan, and now the
|
||
first thing to fix:
|
||
|
||
```
|
||
separation — not computable: the reference landed below the floor
|
||
floor 0.2130 · ceiling 0.2179 · all six seeds read "thin"
|
||
```
|
||
|
||
The ceiling — videos built from casts sharing *no scenes* — is 0.2179 against a floor of
|
||
0.2130. Real seeds already differ by about as much as maximally-unrelated ones, so
|
||
separation divides by a near-zero interval and comes back `NaN`. This is the `separation
|
||
NaN` in the standing phase 12 failure, and it has been there across every run including the
|
||
pre-work baseline.
|
||
|
||
§11 anticipated this precisely — *"the instrument is what is wrong"* — and proposed the
|
||
direction split as the fix. But direction was added **alongside** the broken separation
|
||
rather than replacing it, so the gate now leads with an unreadable number while a working
|
||
one sits underneath. The decision to make: repair the ceiling, or retire separation and read
|
||
**(floor, direction)** as the pair.
|
||
|
||
**13.2 The coherence bound (§6).** Never built. Optional while direction sat at zero;
|
||
load-bearing now that it is +0.24, because nothing currently stops future work from buying
|
||
direction by making adjacent sections incoherent — the named failure mode in §6.
|
||
|
||
**13.3 Recurrence (§6).** Never built. `phase13` has a spec-level cousin — was the kind
|
||
recast, did tension move more than 0.08 — but not the descriptor-distance statistic
|
||
`d(first, last) > d(first, middle)`. That is the ratchet measured in the image rather than
|
||
in the spec, and §5.3 calls it the one a viewer names as "it kept going somewhere".
|
||
|
||
**13.4 Punctuation (§5.5).** Still the only part of the plan wholly unbuilt. Nothing in
|
||
`src/` mentions it. Needs the flash-gate review, which is why it was deferred; §11 said to
|
||
do it once the numbers were understood, and they now are.
|
||
|
||
**13.5 The no-story control arm.** Still never completed. Less urgent than when §11 was
|
||
written — direction clears the arcless reference by 0.26, which is hard to explain without
|
||
a story — but it remains the only thing that would attribute the floor rise (0.1697 →
|
||
0.2130) to this work rather than to everything else that changed alongside it.
|
||
|
||
---
|
||
|
||
## 14. Composition — the frame has a floor and a ceiling
|
||
|
||
Not part of the original plan. It comes out of the same place §12 did: the layer stack was
|
||
being asked to carry the video, and half of what it was stacking was nothing.
|
||
|
||
**The floor.** Two thirds of the library is `composable` — sparse by design, elements ON
|
||
something. `canBackground` let almost all of it anchor a section anyway, so a section's
|
||
bottom layer was regularly a scene painting 2% of the frame with black behind it. Measured
|
||
across six videos, sampling the middle of every section: **9 of 40 frames were under 20%
|
||
painted, the darkest at 0.3%**. That is a minute and a half of a few bright things on black,
|
||
and no gate could see it, because every gate on the stack was a limit rather than a floor.
|
||
|
||
Every section now stands on a GROUND: a canvas painting at least half the frame, cast per
|
||
section kind so a section's shot cuts change the shot and not the world. When the shot is
|
||
itself a full canvas it *is* the ground — two canvases stacked is two pictures fighting. The
|
||
same measurement now reads **mean 92% painted, darkest 63%, nothing under 30%**, and phase 12
|
||
gates it in the image, not in the spec.
|
||
|
||
**The ceiling.** Layering had a rate but no budget, so how full a frame got was a die roll
|
||
that knew nothing about the song. A stack now has a coverage budget — director appetite
|
||
(`crowd`, 1.05 brutalist to 1.7 corrupt), times the section's energy and density, times where
|
||
the story is — capped at **200%**, two frames' worth of material. The ground and the shot are
|
||
paid for first; the budget governs what may be stacked on top, and the odds of an overlay
|
||
fall off as the headroom does rather than only at the wall.
|
||
|
||
**What it cost, and what it bought.** Same instrument, same six seeds:
|
||
|
||
```
|
||
baseline with grounds
|
||
separation 0.10 0.44
|
||
direction 0.06 0.21 (arcless reference 0.02 → 0.08)
|
||
library 80% 97% of what the library can express
|
||
scale 42% 56%
|
||
motion 68% 96%
|
||
texture 74% 90%
|
||
```
|
||
|
||
The ceiling *fell* (0.2334 → 0.2031) while observed rose, so part of that separation gain is
|
||
the reference arm coming down: videos that share a ground vocabulary are less unalike even
|
||
when they share no shots. The honest reading is that the floor rose 0.009, direction tripled,
|
||
and the ceiling moved toward the floor — the numbers are up, and not all of the rise is
|
||
signal.
|
||
|
||
**A scene that reads the previous frame cannot be a ground.** `prev()` returns the whole
|
||
composited frame, *including whatever is layered on top of this scene*, so a datamosh under a
|
||
shot is not grounding it, it is eating it. Measured the moment one became a bed: the render
|
||
stopped reproducing from a seek, and two WebGL contexts diverged by 91/255 against a ceiling
|
||
of 4 — phase 4 caught both. `readsHistory` is derived from the shader source rather than
|
||
declared, and costs the ground pool six of twenty scenes, five of them glitch.
|
||
|
||
`scenes/coverage.js` is the measured coverage of every scene, pasted back out of the phase 12
|
||
gate, which re-measures and fails on drift over a tenth of a frame. A new scene has no entry
|
||
and therefore cannot ground anything until someone renders it.
|
||
|
||
### 14.1 The measurements moved into the repo
|
||
|
||
Everything in §14 depends on a number — how much of the frame a scene paints — and the
|
||
first version of it was a table pasted into a source file by hand. That is the same mistake
|
||
`surface` already was: a measured fact written down by a person, correct on the day and
|
||
wrong after the next shader edit.
|
||
|
||
**`src/scenes/metadata.json`** is now generated, tracked in git, and holds every measured
|
||
fact about every visualizer: coverage as a shot, coverage as a bed, the variety score, the
|
||
per-block scores, and the mean structural profile. Nothing in it is typed by a human.
|
||
`surface` is derived from it (canvas at 50%, composable below) and declaring `surface:` in a
|
||
scene file is now a lint error.
|
||
|
||
It is refreshed from **gallery.html → refresh metadata**, which re-renders the library and
|
||
writes the file through a dev-only endpoint in `vite.config.js`. The file carries a
|
||
fingerprint of everything that can move a number in it — the scenes, the shader contract,
|
||
the identities and palettes they are handed, the descriptor definitions — and phase 12 fails
|
||
when that stops matching. A stale metadata file does not produce a stale report, it produces
|
||
wrong videos, so it has to be detectable.
|
||
|
||
**Measuring at the wrong bias measures nothing.** Coverage is mostly a function of a scene's
|
||
parameters, and the first measurement took one number per scene at the busiest section of
|
||
six songs. Metaballs measures 69% there and painted **6%** as an intro's ground. Three
|
||
things came out of chasing that, in order of how much each was worth:
|
||
|
||
* A bed is drawn with a **moderate hand**. `extremity` pushes parameters toward the ends of
|
||
their ranges, and half of what decides coverage is a parameter with no `bias` key at all —
|
||
Metaballs' `threshold` — so extremity is the only thing moving it and one end is an empty
|
||
frame. Grounds sample at `extremity * 0.2`. The extremes belong to the shot.
|
||
* Energy and density are **floors** for a ground, not reductions. Calming the bed by
|
||
lowering energy is how to empty it: scenes bias their fill against `energy` as often as
|
||
against `density`.
|
||
* Coverage is measured **twice**, once as a shot and once as a bed, at the same bias and
|
||
temperament the generator will use. `GROUND_BIAS` and `groundTemperamentFrom` live in
|
||
`scenes/surface.js` and are imported by both the generator and the measuring pass, so the
|
||
two cannot drift apart.
|
||
|
||
Rendered result across six videos, sampling the middle of every section: **mean 87% painted,
|
||
darkest 30%**, against 51%/0.3% before any of this.
|
||
|
||
**Eleven scenes can ground a section** — five organic, five geometric, one structural. There
|
||
is no `minimal`, `flow` or `glitch` canvas in the library that paints half the frame without
|
||
reading `prev()`, so every quiet section in every video stands on one of five organic beds.
|
||
That is the largest remaining hole and it is library work, not generator work: the fix is
|
||
writing dense, self-contained canvases in the three thin families.
|
||
|
||
### 14.2 The red line is gone
|
||
|
||
`MIN_VARIETY` — 0.1, drawn across the gallery, "below this a scene is the same picture
|
||
wherever it appears" — was right when a section was one scene and is wrong now. A section is
|
||
a ground, a shot on it and sometimes a pass over that, so what a viewer sees is a
|
||
combination, and a scene that is reliably itself is a good ingredient in one. Nineteen
|
||
scenes were failing a bar for being consistent.
|
||
|
||
What replaced it is a level up, and it is the reason the profiles are in the metadata: the
|
||
generator weights every layering choice by **structural distance** — how unalike two scenes
|
||
measured, on the same descriptor the variety harness compares videos with. Family labels and
|
||
the render disagree often enough to matter; two `geometric` scenes can be 0.31 apart and a
|
||
`flow` and an `organic` scene 0.04, and stacking the second pair is one picture at double
|
||
density. Measured over 436 stacked pairs: **mean distance 0.268, zero near-twins**.
|
||
|
||
### 14.3 The other end of the frame
|
||
|
||
The floor had a ceiling missing. Measured across twelve videos, sampling the middle of every
|
||
section: **a median 24% of every frame was clipped to pure white, and whole sections
|
||
rendered at 100%.** The videos were washed out, and no gate could see it because every guard
|
||
on the frame asked whether there was enough in it.
|
||
|
||
Three causes, all introduced or exposed by §14, in the order they were found:
|
||
|
||
**The shot was screened over its ground.** Screen is a lightening operator — correct for a
|
||
few bright elements over a bed, and over a filled canvas it drives everything toward white.
|
||
Replaced with a **lumakey** blend (`passes.js`): the shot's own brightness is its alpha, so
|
||
it replaces the ground where it paints and leaves it where it does not, keeping its own
|
||
colour instead of adding it to the bed's. Median clipping 24% → 11%.
|
||
|
||
**The feedback loop was an accumulator.** `cur + hist * decay * amount` settles a still
|
||
image at `1/(1 - decay*amount)` times its drawn brightness — **2.3x** at the settings the
|
||
generator hands out. Survivable when a frame was a few bright things on black; fatal the
|
||
moment every section stood on a filled ground. Turning feedback off took a blown frame from
|
||
100% to 34% mean luminance, which is the whole diagnosis in one number. It now divides by
|
||
the same gain, at 0.6 rather than 1.0: full normalisation took the lift out along with the
|
||
blowout — the median frame fell from 95% painted to 71% and fourteen sections dropped
|
||
through the black-frame floor.
|
||
|
||
**Nothing rolled off.** The grade clipped. A **highlight shoulder** now compresses everything
|
||
above 0.75 toward but never to 1. Clipped white is not brightness, it is missing information:
|
||
every difference inside it has been deleted.
|
||
|
||
Rendered, after all three — 74 sections, twelve videos:
|
||
|
||
```
|
||
before after
|
||
painted 51% mean 88% mean, darkest 43% (was darkest 0.3%)
|
||
clipped white 24% median 0% median, worst 21% (was worst 100%)
|
||
luminance 77% median 42% median, worst 80%
|
||
```
|
||
|
||
The gate is now two-ended: `composition · a rendered section is neither black nor blown out`.
|
||
|
||
**A ground has to survive the song's identity.** Chasing the dark end turned up the same
|
||
class of error as §14.1: a ground measured at 61% painted **2%** in one particular video,
|
||
because that song's ink treatment is `hollow` — outlines, no fill. Two changes came out of
|
||
it. The world is now drawn solid and the song's hand is kept for the subject
|
||
(`groundPersonalityFrom`), and eligibility takes the **worst** identity into account as well
|
||
as the mean: a ground fills the frame on average and never vanishes. Holding the worst case
|
||
to the full 50% would leave *four* castable beds in the library, which is a worse video than
|
||
an occasionally dim intro.
|
||
|
||
**Seven scenes can ground a section** — five geometric, two organic. Every quiet section of
|
||
every video stands on one of two beds. Restated from §14.1 because it got worse, not better:
|
||
this is the library's largest hole.
|
||
|
||
### 14.4 Blazing on purpose
|
||
|
||
Screening the shot over its ground and letting the two brightnesses sum is a real effect — a
|
||
drop that goes to paper for eight bars reads as the song peaking. The mistake in §14.3 was
|
||
not the effect, it was that the effect was the **default**: every section did it, so a median
|
||
quarter of every frame in every video was clipped, and nothing about that said "peak".
|
||
|
||
It is a decision now. Each director declares a `blaze` appetite — brutalist 0.05 (mass does
|
||
not glow), corrupt 0.5 (overload is the subject) — and a section still has to earn one: loud,
|
||
and late in the story. Quiet kinds never blaze, because a breakdown that goes white is not a
|
||
choice, it is a bug with a rationale. The default composite is the lumakey, which keeps the
|
||
shot's own colour.
|
||
|
||
The ceiling gate matches the intent rather than banning brightness. Per section, only a hard
|
||
cap — past about half the frame at full white there is no picture left to read. Across the
|
||
population, **no more than a fifth of sections may be hot at all**, which is the number that
|
||
actually distinguishes a director choosing to peak from a pipeline with no headroom.
|
||
Measured now: **1 of 74 sections blazing**, worst 30% clipped.
|