The first measurement offered three readings for a direction of -0.14. The third one — the arc is not reaching the image — was correct, and it is now +0.24 against an arcless reference of -0.02, with the floor rising exactly as §8 predicted. Records what the two dead channels were, since neither was visible from the source and both passed every gate that existed: the overlay path had an empty candidate set (0/144 stacks), and the camera moved the frame by a median of 0.029 of a half-frame at a random angle so successive shots cancelled. Both cleared their checks because every bound on them was a ceiling. A gate on a device needs a floor. Also ranks what remains, with the broken seed-variety instrument first: three of the four open items are measurements and they all report through it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
397 lines
20 KiB
Markdown
397 lines
20 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.
|