Two devices that were built, wired up, and doing nothing. OVERLAYS. buildStack gated layering on `surfaceOf(m) === 'composable'`, and no scene in the library declared `surface` — so the only thing that could ever sit on top was the one scene declaring `role: 'accent'`, and the overlay roster excluded accents by construction. Empty intersection, every time: 0/144 stacks carried an overlay. Every layered frame in every song was the same particle field. Labelled the library from the phase 12 coverage gate rather than by eye: the 37 scenes painting under 30% of the frame are composable. Particle Field loses its privileged slot and becomes one of them, keeping only `background: false`, which is the honest part — points in empty space cannot carry a section alone. The reserved accent slot is gone; one roster, two passes at it. 1 distinct overlay scene becomes 27, and the rate lands at 34% of stacks after trimming a base chance that had been tuned while the branch was dead. THE CAMERA. framing.shift moved the frame by a median of 0.029 of a half-frame at a fresh random angle every shot, so successive offsets cancelled and the median jump at a cut was 0.014. Present in every frame, visible in none. look/Camera.js is the director's camera department: the story says tension, order and which act a section is in, and this turns that into where the frame looks and how it travels there. Each director names a camera. Jump distance follows tension and act, speed follows energy, and the curve is one of four. Cuts choose between reframing and matching, so two scenes can still read as one place. Median offset 0.190, median reframe 0.135, and 94% of shots now move during the shot rather than only at the cut. Four new gates, each with a FLOOR — the device this replaces passed every existing check while doing nothing, because the only bound was a ceiling. Also lands the in-progress Epic 4 story layer it builds on: Story.js, phase 13, and the direction statistic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
291 lines
14 KiB
Markdown
291 lines
14 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.
|