89e05459c0
73 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
89e05459c0 |
Every shot stands on something, and the frame has two ends
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>
|
||
|
|
7d151be6e1 |
Pylon Grid stands on the chorus instead of on a strut
The legs were the last thing in this scene it had invented for itself: a fixed-width vertical capsule, the same object in every video, with only the palette separating one track's structure from another's. Against migrated crowns they read as scaffolding left up around a finished building — and structure is half this frame, so half of it was still saying what the SCENE is rather than what the song is. A leg is now a stack of the song's chorus. How many members make up a column is the song's element size deciding it — a track of few huge forms stands on three, a track of many small ones on a ladder of nine — and how wide the column is comes from the chorus's own proportions. Each member turns at its own angle, so a column is one form seen many ways rather than one form printed nine times. That makes `staging` a third declared artifact, which the lint caught before the gate had to. Measured on the crop that contains the change, since the frame-level score cannot see it: over six identities the bottom third of the frame moves 13.4/255 with the old strut and 22.2/255 with the chorus stacked. The whole-frame variety score goes 0.1322 to 0.1348, and the header now explains why the two disagree and why both are right — the crowns dominate a descriptor averaged over the whole image. Cost is unchanged in practice: 7.7ms/frame at 4K, 12.4ms with every param at the top of its range, against a 60ms ceiling. The column's bounding box is two comparisons for a pixel outside it, which with a hundred and sixty pylons is most of the budget. The crown is resolved before the leg now. Resolved the other way round, the first-wins rule let the column paint over the object standing on it and every subject was cut off at the waist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
61e0c66f61 |
Pylon Grid stands the song's object on its pylons
The scene already had the depth axis the solid wants — near rows large, far rows small, converging on the track's horizon — which made the flat crown the one element in it drawn without perspective: a sticker facing the viewer on top of a receding structure. Each crown is now marched, and each pylon turns its own at its own angle, so a single field shows the form from a dozen sides at once. 12/12 on the gate, 7ms/frame at 4K against a 60ms ceiling. A hundred and sixty instances stay affordable on the two guards MIGRATION.md now documents, and here first-wins is the correct occlusion as well as the cheap one, because the rows are already walked near to far. IT MEASURES SLIGHTLY WORSE and the scene's header says so: 0.141 to 0.134 over 8 songs x 8 seeds, nearly all of it layout (0.156 to 0.126). Two candidate causes were tested and both came back flat — keeping the far rows stamped scored 0.132, and running the lit surface through inkPattern scored 0.132 here and moved Effigy by 0.0009. It is the solid itself: a body always fills its own silhouette, where a stamped cast form paints anywhere between an outline and a disc depending on whether the identity is hollow, and that swing was worth three hundredths of layout variety. Kept because the score does not measure what was wrong. A crown that ignores the perspective of the field it stands in is a defect no coverage variance reports, and the header records the number and the two-line revert for whoever disagrees. The inkPattern experiment is reverted rather than left in as a plausible-looking no-op, with its measurements written where the next person will look for them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a89676cc8 |
One scene, wide: songs against seeds
Six thumbnails is enough to rank sixty-eight scenes and far too few to study one. Worse, six songs at one seed each confounds the two inputs: a scene whose frames are interchangeable might be ignoring the identity or ignoring the music, and the library gallery cannot say which. So a scene's name in the gallery now opens it on a grid — songs down, seeds across, up to every song in the bank by ten draws. Buttons pick the size and walk onto a fresh block of seeds, which is a much better answer to "is it flat or was that ten unlucky rolls" than staring at the same ten. The state lives in the URL, so every button is also a back button. Three scores rather than one, and the two new ones are the diagnosis: ACROSS SEEDS is the same song with a different draw, so low means the scene ignores the identity; ACROSS SONGS is the same draw against different music, so low means it ignores the song. They point at different fixes. The closest pair is outlined, because at 170 cells no eye is finding it. The first version hashed the song name into the seed so a column would not be one roll repeated down the grid. It reads better and measures nothing: with the seed varying on both axes the two scores are the same comparison, and they came back within 0.001 of each other for every scene tried. A column holds its seed fixed now, which is what makes it attributable to the music — and the scores separate, with the seed moving every scene tried more than the song does. 170 cells in 9s. The songs are the slow part and are analysed one at a time so the count moves, then cached for the session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
89277705c4 |
Nothing is held for two minutes, not even one visual
planShots skipped cut planning entirely when a section had a single visual to show — "nothing to cut to" — and the exemption swallowed the ceiling with it. Adding a scene shifted the casting enough to land a 150 BPM track in that case, and it held one image for 151.98 seconds against a 22-second limit. Any new scene could have exposed it; the shot-length gate had simply never met a single-visual section before. The premise was wrong rather than the arithmetic. A shot boundary changes the FRAMING as well as the image, so the same visual filmed again at another size is a shot, and the ceiling is about how long one image is held rather than about how many images a section has. Single-visual sections now plan shots like any other. What the single case still suppresses is the hard cut: cutting straight between two framings of one image is a jump cut, so those boundaries always dissolve. Two intermediate versions were wrong in ways worth not repeating: splitting the section into equal spans put a 23.5s shot past the ceiling once the cuts snapped to downbeats, and read as a metronome (cv 0.048) to the rhythm check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
144c8c918e |
The song brings a body, not only an outline
A silhouette is the same picture from every angle, so a scene that turns one is
showing you the same shape rotated. That is the ceiling the cast has been under:
it can be notched and hollowed and it still cannot be walked around.
So the identity generates an ASSEMBLY — two to six parts, each a primitive with
an offset, a scale, a rotation and a boolean op, all under a symmetry. The
symmetry is the load-bearing half: parts unioned at random positions read as
debris, the same parts folded read as designed, and only a designed object is
worth calling a protagonist. Prism parts are the existing 2D profile extruded,
so the solid and the silhouette stay one character rather than two generators
running side by side.
It travels as data like every other artifact: three vec4 rows per part in
u_formPart, plus the scalars. Scenes declare `consumes: ['form']` and get
castSDF3, castMarch, castSolid, castChorusSolid and castLit; with no identity
they fall back to the flat profile extruded, so the helpers are safe to call
unconditionally. The chorus is the same rows with fewer parts and its own
proportions — a relative, not a second generator, and no extra uniforms.
Three scenes carry it. Effigy is new and holds the object still while it turns.
Floating Geometry and Swarm were already loops of stamps and are now loops of
bodies; Swarm is what the chorus solid exists for. That is 34.8% of videos
containing a 3D cast, against 11.9% when only Effigy had it.
Measured, the outline does change rather than merely spin: over one turn the lit
area of Effigy's subject varies 14-113% against Soloist's 3-51% for the same
rotation. Whether that reaches the variety blocks is not yet measured, and
HOWTO-variety says so rather than claiming the win.
Four costs, each found by measuring rather than by reading:
* every pixel evaluated every instance's field — Swarm at 59ms/frame against a
60ms ceiling. Bounding-sphere reject first, now 12.3ms.
* instances overlap several deep at the top of the size range, and marching
all of them made Floating Geometry's own gate run for minutes. First-wins
instead of last-wins, which was arbitrary either way.
* a normal inside the march loop multiplies four copies of the SDF by the step
count, because GLSL unrolls a fixed bound. Hoisted out.
* the helpers in the shared preamble made all 68 scenes compile what 3 of them
call. FORM_PREAMBLE is appended per scene instead.
The lint's backtick check was green through two of my own breakages: quoting a
name in a doc comment adds backticks in PAIRS, so parity survives and the
pair-scanner just re-partitions the file. It now finds where a shader literal
opens and requires the next backtick to be a real terminator — which
immediately found a second stray pair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
1ca9fa2f1f |
Let scenes layer on each other, and give the camera somewhere to look
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> |
||
|
|
eda8ac3810 |
Composable vs canvas: say which scenes are meant to sit on top of others
The top of the gallery is full of black frames with a bright element on them. That is genuinely varied and thin to watch, and the reason it ranks is that the gallery previews every scene ALONE — which is not how anyone watches one. Measured, the split is not marginal: 37 of 66 scenes paint under 30% of the frame, and the sparsest are exactly the ones topping the ranking. Storm Rift and Pendulum Trace paint 0%, Droste Feedback and Spectrum Sculpture 2%. At the other end Block Mosh covers 98% and Scan Tear 96%. So a scene now declares a `surface`. A canvas fills the frame and belongs underneath; two of them stacked is two pictures fighting rather than one picture with depth. A composable scene is mostly empty by design and reads as elements ON something. The distinction was implicit in `role: 'accent'`, which marked the depth passes and said nothing about the sixty scenes treated as interchangeable backgrounds while half of them were sparse. Verified rather than trusted, in the pattern the rest of the project uses: a gate renders every scene and measures what it actually paints, failing a composable that covers more than 55% — it would hide whatever it sits on — and a canvas that covers less than 8%, which is a canvas in name only. The gate also prints the whole coverage table, since that list is how the library gets labelled in the first place. Casting uses it: only composable scenes are drawn as overlays, and because what goes on top is now guaranteed to leave the shot underneath visible, layering happens far more often — the overlay chance went from 0.12 to 0.3 at its base. The gallery shows coverage and surface beside each score, sorts by sparsest, and says in the header why the two numbers explain each other. Nothing is labelled `composable` yet. Every scene keeps the behaviour it had through `surfaceOf`, which defaults to canvas unless the scene is an accent, so this commit changes the mechanism and not the output. The coverage table is what the labelling should be read off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c099501ead |
Raise the variety bar to 0.1
Deliberately a target rather than a description of where the library sits. Of the scenes measured since the region block went in, only Droste Feedback at 0.324 is comfortably over; Plasma Bloom is 0.082, Voronoi Shatter and Apollonian Gasket 0.062, Moiré Grid 0.047. Most of the library fails this, which is the point — a bar set where the work already is measures nothing. Third value in two days, and both previous moves were instrument corrections rather than changes of mind: the motion block was reading zero for the entire library, and adding the region block made the total a mean over six blocks instead of five. Both raised every score, so 0.04 had quietly drifted down relative to the thing it was judging. Documented as something to re-read off a fresh gallery whenever the descriptor changes, rather than a number to carry forward. The marker now sits at the middle of each meter and the grades land where they should: 0.047, 0.062 and 0.082 all red, 0.140 amber, 0.324 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b879abe230 |
A region block: how differently the parts of a frame behave from each other
The descriptor could say where a frame's energy sat and not whether one part of
the picture was doing something different from another — and that second thing
is what "nothing to watch" means. A pattern spread evenly over the screen has no
subject precisely because every region is the same region, and `layout`
normalises that away: a uniform field and a field with a form standing in it can
come out at nearly the same distribution.
So each ninth of the frame is now characterised in its own right — how much
detail, which way it runs, how many elements — and expressed as its deviation
from the frame's average. Heterogeneity rather than level, so a brighter or
busier frame does not register as a more varied one.
It is immediately the strongest block in the descriptor, and it credits scenes
the old one was underrating:
Apollonian Gasket 0.039 -> 0.062 region 0.180
Plasma Bloom 0.053 -> 0.082 region 0.182
Moiré Grid 0.037 -> 0.047 region 0.097
Droste Feedback 0.219 -> 0.324 region 0.446
Apollonian Gasket is the interesting one. It was called out as looking good and
scoring badly, and the answer turns out to be partly that the instrument was
missing the axis it is good on rather than that beauty and variety are simply
different things. Both were true; only one of them was the instrument's fault.
The metric's own gates still hold, which is the condition for believing any of
this: recolour moves structure 0.019 while moving colour 0.723, a quarter turn
moves it 0.0000, and the same scene against itself is 0.0000 against 0.0992 for
two different scenes.
EVERY SCORE HAS MOVED. The total is a mean over six blocks now rather than five,
and the 0.04 bar was derived under the old one. It needs re-deriving from a fresh
gallery before it is used to judge anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8defef3ed0 |
Tell a dead shader apart from a boring scene in the gallery
A failed shader compile renders black, and black frames are identical to each other, so every block scores 0.000 and the row reads as the least varied scene in the library rather than as a broken one. That happened yesterday — the subject helpers were declared above the ink they call, GLSL has no forward declarations, the whole preamble failed to compile, and the gallery reported a variety of exactly 0.000 with no indication anything was wrong. It is a particularly bad failure mode for a tool whose entire job is ranking scenes by that number. A scene whose first frame has no luminance and no variance is now reported as broken, with both figures and a pointer to the console, and the row is coloured apart from the merely flat ones so it cannot be mistaken for a bad score. The summary counts them separately. Verified against both answers rather than only the happy one: a healthy Moiré Grid comes back 0.0365 with no error, and a deliberately broken copy of the same scene comes back with the diagnosis instead of a zero. Recorded in HOWTO-variety.md alongside the other instrument traps, since the general lesson outlives this instance — a broken render produces the most boring possible numbers rather than an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c950f8f3b1 |
Give a full-frame field a subject the eye can track, and let the song choose
what it does "One pattern across the screen" scenes have nothing to watch. Warping the field around a focal point was the previous attempt and it did not fix that — measured on Voronoi it raised orientation variety while layout stayed at 0.004, which is another way of saying the cells changed and the frame still had no subject. So a subject is now a form that does something TO the field, and which thing is the song's decision rather than the scene's: shift, warp, punch, morph or overlay. The same form punching a hole, bending the pattern or running it at another rate are three different videos, and across six songs the bank picks overlay, morph, shift, morph, warp and punch — so it is a real axis rather than a constant with five names. subjectSDF, subjectMask, subjectEdge and subjectWarp are in the contract, so any field scene can take a subject without knowing where the focal points are. Moiré Grid is the pilot and implements all five impacts. 0.031 to 0.037 — up a fifth, and the layout block moved from essentially nothing to 0.018, which is the first time anything has moved layout on a full-frame field. That is the number that speaks to the complaint: the frame's energy is no longer spread evenly, so there is somewhere to look. It is still under the 0.04 bar, and I have not seen it. The score says there is now a subject; whether it is worth watching is the question the gallery cannot answer. One bug worth recording: the subject helpers were placed above the ink in the preamble, and subjectEdge draws with inkStroke. GLSL has no forward declarations, so the whole preamble failed to compile and every scene using it rendered pure black — which the gallery reported as a variety of exactly 0.000 across all six blocks rather than as an error. An all-zero row means a dead shader, not a boring scene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5a9dfa6b2b |
A scatter point in Isometric Blocks — which measures worse, twice
Asked for, built, and honest about the result: the focal point costs this scene
a quarter of its variety and I could not make it pay for itself.
uniform scatter, no focus 0.0490
focus, trading scatter for it 0.0374
focus, added on top of it 0.0365
The first version spent the scatter budget near the focus and calmed the rest,
which put most of the field back on the rigid lattice and took the orientation
variety with it, 0.164 down to 0.105. That diagnosis looked right, so the second
version made the focus additive — every cell keeps its full scatter and the ones
near the point get more. It changed nothing: 0.0365, orientation still 0.104.
So the diagnosis was wrong and I do not have the real one. The remaining suspect
is the height modulation, which lifts blocks toward the focus and may be
flattening the skyline's variety between songs by dominating it — but that is a
guess, and two guesses have already been wrong here.
The feature is committed rather than reverted because the metric does not
measure what was asked for. A scatter point is a compositional idea, and the
gallery scores how much a scene changes BETWEEN songs, which is a different
question — Apollonian Gasket makes the same point from the other direction. This
may well look better and score worse. It needs eyes on it before the number
decides.
`gather` is a parameter and seed-driven like any other, so the amount varies per
song as asked; the focal points themselves come from the identity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b1e58a6603 |
A focus: somewhere for a full-frame field to be about, tried on Voronoi
Edge-to-edge textures have no composition to vary. Two songs of Voronoi Shatter measured 0.004 apart in the layout block, and that was honest rather than a metric failure — the cells changed and the arrangement did not, because there was no arrangement. So the identity now decides a FOCUS: one to three points, a reach, and a pull that either draws the field in and densifies it or opens a void. The points come off the same lattice everything else is placed on, so a scene using them is composing in the song's terms rather than inventing a centre of its own. focusField and focusWarp are in the contract, available to any field scene that wants somewhere to be about. Voronoi Shatter is the pilot. It tiles in the warped coordinate, so cells crowd toward the focus or pull away from it, and its seams tighten where the field gathers so the effect reads as a change in the shatter rather than a brightness blob laid over one. 0.0463 to 0.0588, comfortably clear of the bar. It did NOT work the way the hypothesis said. The gain is in orientation, 0.140 to 0.194, and in texture; the layout block moved from 0.004 to 0.007, which is still nothing. Warping where the cells sit changes what they look like without changing where the energy is, because the field still covers the frame corner to corner. Layout stays blind to this family until a field is allowed to actually fall away from its focus and stop being full-frame — which is a bigger change to what these scenes are, and worth deciding rather than sliding into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8d9b8a593a |
Fix a permanently-zero motion block, and let the song arrange Isometric Blocks
Investigating three pieces of feedback turned up a measurement bug underneath
all of them. The gallery built four of the descriptor's five structural blocks
and never rendered a second frame, so `motion` came back 0.000 for every scene
in the library and dragged every score down by a fifth. It read as a property of
the scenes; it was a missing render. Every number in the gallery so far,
including the ones the 0.04 bar was calibrated against, was depressed by it.
Plasma Bloom 0.0532 -> 0.0624
Voronoi Shatter 0.0431 -> 0.0463
Apollonian Gasket 0.0340 -> 0.0388
The Plasma Bloom versus Voronoi Shatter oddity is real and is the metric's
limit rather than a mistake. Plasma Bloom is a centred subject whose position
moves between songs, so its layout distance is 0.096; Voronoi Shatter is
edge-to-edge cells, and a full-frame texture has the same layout however its
cells fall, so its layout distance is 0.004. The descriptor cannot see "the
cells are different" as composition, and for full-frame work the layout block
contributes almost nothing. Worth knowing before the bar is used to judge that
family.
Apollonian Gasket scoring low with good-looking frames is the gallery working:
it measures how much a scene changes between songs, not how good it looks. A
scene can be beautiful six times and identical six times.
Isometric Blocks gets the displacement it was asked for. Every block sat exactly
on its lattice slot, so the plan of the field was the same plan in every song
and only the heights moved — a layout distance of 0.005. `scatter` steps each
cell off its slot by a fixed amount of its own, bounded under half a cell so
blocks do not cross and break the isometric read, and it drives the block's
height and side wall as well as its ground position. 0.034 to 0.049, clearing
the bar, with orientation variety nearly doubling as the rigid lattice softens.
Its layout distance stayed at 0.004, for the same reason Voronoi's did: the
field fills the frame either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
71ddd37711 |
A minimum variety bar in the gallery, drawn as a red line
A visualizer that looks the same wherever it appears does not just fail to vary — it leaks between songs, because it is cast into many of them and the viewer who watches two videos ends up recognising the shot rather than the song. That makes the floor a property of the scene, so the gallery now states it and the scene has to clear it. 0.04 to start with, and honestly a starting bar rather than a derived one: set where the flat cluster measurably sits, to be raised as scenes are lifted to it. Drawn twice, because the two readings are different questions. Every meter carries a red marker at the bar, so a single row can be judged on its own. And in the default sort the list is cut by one red line where the scores cross it, which answers the browsing question — everything above that line repeats itself between songs. Also restored progressive drawing, which the caching rewrite had quietly lost: every draw had moved to the end of the build, leaving three minutes of spinner and nothing to look at. The first rows are the interesting ones, since the sort puts the repetitive scenes on top. Verified by seeding the cache with rows spanning the bar and reloading: four below, four above, the line between them, the marker at 20% of each meter, and the grade colours switching at the right place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e2e5104646 |
Take the effects off the visualizers and leave them to the grade
Forty-nine scenes carried their own `glow`, every one of the sixty-five added its own grain, and a handful had bloom, chroma, haze, smear or trail besides — all of which the post chain already does, with an envelope and a threshold no scene can see. Two uncoordinated effect layers is the smaller half of the problem. The larger half is that an effect knob sampled per song makes a scene look varied across parameter draws while its structure never moves, so the gallery and the variety harness were both being told a scene was original when only its halo had changed. Removed in three shapes. Scene grain goes entirely — the post chain owns it. Statements that ADD a falloff term scaled by the effect are bloom by another name and are deleted. What remains is an intensity on something already drawn, so the uniform is pinned to its default and the knob deleted: the picture survives, the fake variety does not. Reactive entries driving those params went too, since an effect modulated by the audio was the most convincing fake of the lot. That broke the style trait, which is the interesting part. Surface treatment had been leaving the scenes for two commits — sigGrain to post, sigEdge to inkStroke — and removing the last of it left `style` with almost nothing to express: a style+shape signature had two eligible scenes in the whole library. The trait is not obsolete, it has moved, so inkStroke and inkMask now read u_sigLine and u_sigSoft alongside the ink's own weight and edge. A scene drawing in the song's hand honours the track's line weight by construction, and the runtime probe confirms it rather than taking it on trust. This reverses a call made two commits ago for a reason that only became true now. One check needed re-aiming rather than fixing. Two independently built shows are two WebGL contexts, and engine/hash.js says at the top that bit-exactness is a same-context guarantee; the check had been demanding it anyway and getting away with it because the scenes it happened to cast were bit-stable. A casting change put smooth-gradient scenes in frame and the last bit moved. Measured before touching the check: three levels out of 255 across 2.6% of pixels, invisible. The cross-context comparison is now a distance with a ceiling of four, and bit-exactness within one show is still demanded by the check below it. 89/89 checks, 11/11 tests, all static gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e90951c277 |
Cache the gallery against a fingerprint of the source that built it
Three minutes of GPU work is fine once and intolerable on every page load, so the gallery now builds once and comes back from IndexedDB after that. The key is the part worth getting right. A cache you clear by hand is worse than no cache: it will eventually show last week's pixels while you are judging this morning's change, and it will look like a render, so you will believe it. Here the key is a fingerprint of every file under src/, collected through import.meta.glob so it covers the shaders, the identity, the look generator, the engine and the descriptors without naming any of them — the file that invalidates a render is exactly the one nobody remembers to list. Edit anything and the entry stops matching and the page rebuilds without being asked. Thumbnails are WebP blobs rather than raw pixels: sixty-five scenes at six frames of 256x144 is 57MB raw, and measured, the compressed form is 4.1MB for the whole library. Saving a build evicts every other one, so the store cannot grow without bound. The spinner is a CSS animation on purpose. Building blocks the main thread in bursts and a JS-driven spinner would freeze mid-turn, which reads as a hang exactly when the page most needs to look alive. Verified rather than assumed, six behaviours: the fingerprint is stable across calls; a build round-trips through storage and decodes to a real image (51% non-black); a different fingerprint misses; saving a new key evicts the old; and appending one comment to one scene file moved the fingerprint from 125-f842b05e to 125-c056d7b9, which is the invalidation working end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c6d96c9d40 |
A gallery, and a debug index to reach it from
Sixty-five visualizers, six renders each, one per song — each carrying that song's whole identity: its cast, ink, lattice, palette and a fresh parameter draw. Side by side, a scene that cannot be changed by the song is obvious in a way no aggregate could show, which is the complaint this answers. It scores as well as shows. Each row carries the mean structural distance between its own six frames, on the same descriptor the variety harness uses and with colour excluded, so six palettes cannot disguise one image. Sorted least-varied first, because browsing sixty-five scenes hunting for the repetitive ones is precisely what a sort order should do for you. The result names names. Thirteen scenes barely change across six songs, and they fall into two groups that were already known separately. The ink-only migrations — Analog Wow, Halftone Misprint, Pitch Shatter, Block Mosh, Scan Tear — are the shallow tier flagged in MIGRATION.md, where the whole migration was one posterisation. And Moiré Grid, Isometric Blocks, Quasicrystal, Truchet Fold, Voronoi Shatter and Apollonian Gasket are the structural twin cliques the library sweep found weeks of measurement ago, arriving here by a completely different route: the sweep compared scenes to each other, the gallery compares a scene to itself, and they agree on the same offenders. debug.html collects the tools, since there are now enough of them that knowing which to open is its own problem. It also carries the two things a newcomer would otherwise learn the hard way: which measurements are safe to steer by (direct render comparisons) and which are not (aggregate ratios), and what to do when a page renders perfectly and does nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4217cca3d3 |
Rename clicktrack.js — a content blocker was eating it and killing the app
A file called `clicktrack.js` matches the click-tracking telemetry patterns that EasyPrivacy and similar lists block by substring. In a browser with a content blocker the request never completes, and because it is a module import that takes the entire graph with it: main.js never runs, no handler is ever bound, and every control in the UI sits there looking correct and doing nothing. The failure is unusually expensive to diagnose because everything else looks healthy. The dev server returns 200 with the right MIME type, curl fetches it fine, node imports it fine, and the app loads perfectly in any browser without a blocker — which is how it passed every check here. Only the console names it, and only as one line about a module that failed to load. Renamed to metronome.js, which is also the better name for what it does. The button id went with it, since cosmetic filter rules can hit ids too. The general rule, recorded at the top of the file: anything shipped to a browser and named like tracking will be treated as tracking. Avoid click, track, analytics, pixel, beacon and ad in filenames and URL paths, however honest the code behind them is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4267c588aa |
Fix two checks the migration broke, and re-aim the signature gate
Phase 2 kept its own list of contract uniforms and never learned about the identity artifacts, so Prism Bloom reading u_castSides looked like an undeclared uniform. It now takes the list from the contract itself. Phase 9 demanded that every cast scene honour the track's signature, which was true when the signature was a hard filter and stopped being true when it became a weight. The check was asserting the old contract against the new generator. Rather than delete it, it now checks the claim that actually matters. A track may reach outside its signature — that change was made deliberately, because the filter was disqualifying a third of the library and funnelling eleven scenes into half of all videos — but it must still LEAN on it. Honouring scenes have to carry the majority of the cast against a chance baseline near 25%, and section anchors, which open a section and return most often, have to honour it almost always. 89/89 checks pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
572b090b83 |
Fix two scenes that were failing their gate before the migration touched them
The library-wide gate run left two failures after the migration was complete, both of the shape "renders nothing at the bottom of a param range". Running the same gate against the pre-migration source shows them failing identically, so the migration did not cause either — it exposed them. The per-scene battery is opt-in and nobody had ever run it across all sixty-five at once. The defect is the RANGE, not the shader. Spectrum Sculpture at radius 0.15 is too small to register and Circuit Bloom at grown 0.25 has no pads yet, and both values are ones the generator is free to sample. Floors raised to where the scene actually draws something. All sixty-five now pass the full battery: renders, animates, deterministic, distinct, param sweep, flash rate, every declared trait and every declared artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d3ed8eb02 |
Finish the migration: all 65 scenes on the identity artifacts
Two helpers made the bulk of it mechanical. `inkStroke` is a drop-in for
sigEdge — the same line at the identity's weight rather than the track's — and
`castForm` is a drop-in for sigForm, same signature so call sites do not change
shape. With those in place the substitution table is one-to-one:
sigForm( -> castForm( sigShape( -> castMain( sigEdge( -> inkStroke(
Forty-seven scenes went through that pass in one run: twenty-six take the cast
and the ink, twenty-one take the ink alone. Then the gate ran over all sixty-five
and found three the pass had broken, which is the entire reason it exists.
Spectrum Sculpture rendered pure black. It had been using sigShape as a RADIAL
METRIC rather than drawing it, and the cast carries notches and a hollow — an
annulus used as a radius turns a sculpture inside out. Reverted to sigShape and
dropped to ink only. The lesson generalises: a scene that reads a form as
geometry is not a scene that draws it, and the classifier cannot tell those
apart from the source.
Eclipse Field stopped honouring its `style` trait. It opts out of surface grain,
so sigEdge was its only style evidence, and the ink replaced it. The trait claim
is now dropped — and so is the lint change that had let inkMask count as style
evidence, which was wrong and was hiding exactly this. A trait is a property of
the track a scene may honour; an artifact is content it draws. Taking the ink
says nothing about whether a scene responds to u_sigLine.
Circuit Bloom went empty at the bottom of its `grown` range, where the pads were
carried by a hairline and the ink's stroke is thinner than the edge it replaced.
Now filled as well as stroked.
Also: the backtick check now covers every shader literal rather than only the
preamble, because a mechanical pass over sixty files reintroduced one
immediately. Three rounds lost to that typo is enough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e5bb7d77e0 |
A reproducible migration, and the first eighteen scenes through it
The library is sixty-one scenes, which is too many to convert from memory or from taste, so the migration is a queue with a gate rather than a judgement call per file. Three pieces. A classifier reads each shader and assigns a tier from evidence in the source — drawn, figure, field, treatment — so two passes over the library reach the same answer and the work has an order. MIGRATION.md is the recipe per tier, written to be followed mechanically. And a gate makes the result verifiable: `consumes` is now a schema field, the lint enforces it in both directions, and the per-scene battery renders each scene under two deliberately distant identities and requires the picture to change. That gate is the part that matters. Without it `consumes` is a comment, and the whole inversion becomes unverifiable at exactly the point where it stops being checkable by eye. With it, a scene that declares the cast and ignores it fails. Eighteen scenes migrated. Four by hand at the drawn tier — Firefly Drift, Metaballs, Floating Geometry, Prism Bloom — and ten at the field tier by script, which is one declaration and one wrapped return. All eighteen pass. The field tier is honestly marginal and the gate says so: every one of the ten moves by 37 to 39 of 255, against 173 to 255 for the drawn tier, and the uniformity across ten unrelated scenes is the tell. That is one global posterisation applying, not ten scenes expressing anything. Cheap, real, shallow. The decomposition moved from identity being worth 54% of the container to 158%, but the stage set changed underneath the measurement and part of that is Metaballs expressing a cast better than Constellation did. What survives the caveat is the useful finding: a migrated library scene carries the identity better than a stage written from scratch to carry it. The four bespoke stages were the wrong shape of effort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
00ad1d8c2b |
Measure where visual difference comes from, and correct the epic's framing
Two cheap measurements settled a question three expensive ones had not, and both
of them contradicted the diagnosis offered for it.
The stated diagnosis was that the identity's expressive range had become the
bottleneck. It has not. A census of the identities themselves — no GPU, seconds
to run — puts mean distance between twelve songs at 0.43 with no near-identical
pairs and full coverage of every decision space: six of six fills, five of five
lattices, six of six protagonist forms, three of three element scales. The songs
are handed genuinely different designs.
The decomposition then asked whether those designs reach the picture, by holding
the container fixed and varying only the identity, then the reverse:
identity only 0.0299
container only 0.0557
both 0.1101
neither 0.0000
Identity is worth 54% of what the container is worth, against an instrument
noise floor of exactly zero, and the two compose to more than their sum. The
inversion works at the frame level. What it does not do is replace the container.
That corrects EPIC-3 §5, which proposed a song picking two to five stages on the
theory that shared content would substitute for container variety. Container
variety is the larger of the two effects and identity adds to it. Four stages
with a rich identity throws away the 0.056 the library was already providing —
which is exactly the shape of every measurement in this epic: stages have the
lowest floor of any arm and no advantage in spread.
The direction is therefore not a small set of stages. It is the whole library
consuming the cast: keep the sixty-one containers and make them draw the song's
content rather than their own. The migration was filed as a cost; it is the
payoff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6e4106048b |
Staging: a shared lattice, and the element size the songs were all sharing
The cast and ink slice lowered the floor as predicted but left `scale` — feature
size — consistently WORSE than the legacy arm, 0.033 against 0.053 and well
outside the noise. Every stage still chose its own element size from its own
param range, so every song landed in the same place: the videos shared a cast
and also, accidentally, shared how big everything was.
Staging is the third artifact. A lattice — grid, radial, spiral, scatter,
strata — with jitter, spread, a size hierarchy and a bias toward the middle or
the edges, transported the same way the cast is: uniforms plus a `stageNode`
function in the preamble, no new engine plumbing. Constellation, Swarm and
Procession place on it; the stage keeps its motion and gives up its composition.
Soloist takes only the scale, since a close-up has no composition to share.
`elementScale` is the part that mattered. It is the song's answer to "how big is
this made of", spanning about a factor of six, and it is the decision that was
missing rather than mis-set. Measured, over twelve songs and three runs:
stages spread +0.0111 ±0.0043 → +0.0143 ±0.0008
scale block 0.033 ±0.004 → 0.041 ±0.007
Against the legacy arm at +0.0108 ±0.0071 the harness still says
indistinguishable, and it is right to: the gap is +0.0035 and the legacy arm's
own run-to-run range is twice that. What can be said is narrower and holds up.
Stages have the lowest floor of the three arms by a clear margin — 0.082 against
0.095 and 0.104 — so sharing content does make a video look like itself, which
was the central prediction. And the targeted fix moved the block it was aimed at
in the direction it was aimed.
Also added: a lint check that the shader preamble contains no backticks. Twice
now one has closed the template literal and produced a check page that hangs on
"starting…" with an empty console, which is an expensive way to find a typo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
577ec107f6 |
Sweep pool size directly, and withdraw the claim that it mattered
The Epic 3 arms appeared to show that a small casting roster was the largest available win. Swept directly across 4, 8, 16 and 32 over twelve songs with three pool draws each, the differences are 0.003 to 0.007 against a run-to-run noise of +/-0.003 to +/-0.005. Pool size does nothing measurable. The arms varied two things at once — smaller pool AND the same pool for every song — and only the stages-versus-legacy comparison inside them held everything else constant. That one still stands at +14%. POOL_SIZE stays at 8, now on grounds the metric cannot see: about nine distinct scenes in a video rather than seventeen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ec38def1a9 |
Epic 3, first slice: the song brings its own cast
Four stages, an identity layer, and an A/B that says the idea is right and the
reason it works is not the reason I expected.
A stage has no image of its own. It owns arrangement — a procession, a
constellation, a soloist, a swarm — and what it arranges comes from the track:
`castMain` and `castChorus` for the forms, `inkMask` and `inkValue` for the hand
they are drawn in. The identity generates a protagonist and a chorus with sides,
notches and hollows, plus an ink treatment of weight, edge, fill, outline and
posterisation. All of it travels as uniforms, so it is data rather than code and
a stage consumes it without knowing any other stage exists.
The protagonist IS the signature form rather than a second opinion about it.
They were separate draws in the first version, which let a track built on
hexagons put a round protagonist on screen — the signature said one thing and
the picture said another, and the shape trait stopped meaning anything for
stages. The cast now reads its geometry from the personality live and adds the
notches and hollows that turn a shape into a character.
Measured across seven songs, three arms, same instrument:
arm floor observed spread
stages, four of them 0.0801 0.1133 +0.0332
legacy scenes, four of them 0.0830 0.1122 +0.0292
the unrestricted generator 0.1058 0.1146 +0.0088
The prediction in EPIC-3 §7 was that sharing a cast would drop the floor
sharply. The floor did drop sharply — by a quarter — but nearly all of that came
from casting FEWER scenes per video, not from sharing content: the legacy arm,
four ordinary scenes with no cast at all, gets most of the same benefit. Stages
add about 14% on top of that.
That is worth knowing before building the other four registers. The single
largest available win was hiding in the roster size all along, and it costs one
number to take.
One result runs the wrong way and is recorded rather than explained: coupling is
+0.29 on the legacy arm and -0.18 on the stages. At twenty-one pairs neither is
distinguishable from zero, so it is a flag for a larger sample rather than a
finding.
Also fixed: a backtick inside the shader preamble's template literal, which
closed it and made every check page hang on "starting…" with no error in the
console.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
75e0f0ef01 |
Make the whole library reachable, and measure what that did and did not buy
Three changes, from the list the measurements pointed at. The personality's values now centre on the audio and the seed picks within that centre, rather than every value being drawn from the seed alone. A bright track sits high in its frame, a dynamic one has depth, noise earns corners where tonal material stays round. The signature's own choice is tilted by the music too — it decides which scenes a track can cast, so leaving it to the seed meant the most consequential decision in the generator had no relationship to the song. The signature stopped being a hard filter. As a filter it was the single largest cause of sameness: a scene declaring all four traits was eligible for every track and opened half of all videos, one declaring two was eligible for one track in fourteen, and eleven scenes out of sixty-one carried nearly everything. It is a weight now, worth six times at full honour. That fix alone made things worse, which is worth recording. The filter was doing two jobs — collapsing the library, and giving each track a DIFFERENT pool to cast from — and removing it kept the second loss. Every track drew from the same weighted library and measured song separation fell. So each track now draws its own pool of about a third of the library, weighted by the signature but sampled without replacement, keeping the differentiation and dropping the bias. Motion became a character rather than a rate. Tempo was the only lever and tempo compresses; stillness is a separate question from speed, and it can now halve the animation rate or raise it by a third. What it bought, measured against the same instrument: every scene in the library is now cast, where sixteen were never reached; identical casts across seeds went from four pairs to none; and the raw structural distances all rose — motion by 57%, layout by 22%, scale by 23%. What it did not buy: videos also became more varied INTERNALLY, by more than they became different from each other. Two songs are no more distinguishable relative to how much one video already changes over its own length than before, and the coupling between musical distance and visual distance is still indistinguishable from noise at this sample size. The ceiling reference now sits below real outputs, so the separation ratio is reported as not computable rather than as a large number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a946f0e105 | wip: instrument guards | ||
|
|
2bbf5bcf42 |
Run both variety tests against the song bank, and fix the ceiling twice more
The tests now run on real bank entries. The seed gates used to run on a two-section synthetic whose only kinds were intro and outro, so half the scene library was unreachable and the number was measuring that rather than the seed. Probes are labelled by section kind and occurrence rather than by index, which is what makes two different songs comparable at all — they have different section counts, so probe 3 of one is not probe 3 of the other, and matching by position would compare a drop against an outro and score the mismatch as variety. For two seeds of one song the labels are identical and this changes nothing, which is the point. The song test measures one thing the seed test does not: coupling, the rank correlation between how different two tracks sound and how different their videos look. Separation alone can be had by a generator that ignores the audio and hashes the file, and that would be a perfect score for a completely wrong video. Separation without coupling is not variety, it is a different seed per file. The ceiling took two more attempts. Recasting every layer at random averages a dozen scenes together and a dozen random scenes converge on the same generic busy image, so two references came out closer to each other than two real videos and blocks scored over 100% of achievable. Forcing one scene per reference collapsed the other way: a video that never changes scene has almost no internal variation, so the ceiling landed BELOW the floor, which is a within-video quantity. A reference has to match the structure of what it bounds. They now keep the real pipeline — rosters, shots, per-section sampling, so a reference rotates between three or four scenes exactly as a real video does — while drawing from disjoint slices of the library. Same complexity, nothing in common. The fingerprint measurement had the same shape of error: pooling every probe mixed in how much each video varies over its own length, which is large for everything, and washed the answer to a flat 100% while the separation score said almost everything was collapsed. One vector per video now. Both tests fail as committed. Seed separation 0.04, song separation 0.03, coupling -0.03 — two different songs differ from each other by about as much as one video differs from itself, and that difference has no relationship to the music. Colour scores 112%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e0c0dd664a |
Give the sections shapes, and the songs an opinion about their own edges
Two complaints, one cause: stages were flat blocks and the noise bed was glued to the pad level, so the bed was a constant hiss under a track that never went anywhere. Sections have contours now — rise, fall, swell, dip, surge — sampled per note rather than per stage, so a build builds through its chords instead of stepping between two flat halves. The noise gets its own per-stage level instead of following the pad, which makes it an arrangement element: a riser through a build with its filter sweeping up, a wash under a drop, nearly absent in a breakdown. That is also the better test signal, since the segmenter classifies a section partly on its energy slope and a build that does not build is one it has to guess at. Transition hardness is a per-song axis. Genres differ on this more than they differ on tempo — an ambient record dissolves between its sections and a club record cuts — and until now every song in the bank cut. Soft songs crossfade across a couple of bars; the hardest get the pre-drop trick, where everything stops for most of a beat before the loud stage lands. Five soft, three mid, nine hard, and the builder fails if the bank ever loses either end. The contours immediately broke the axis they were layered onto: section-to- section contrast is dynamic range, so adding it put a floor of 0.50 under a statistic that had reached 0.18. That is what compression IS, so contour depth now scales with the track's dynamics — a limitered master has shallow section contrast as well as a shallow crest. Back to 0.29, which is as low as enveloped notes and hard cuts will go. One regression accepted rather than fixed: the soft fades cost the beatless entries their tempo detection, since the attack that gave a drone a pulse is exactly what a crossfade removes. `drone` reads 170bpm for a 62bpm source. A beatless track has no tempo and the detector is guessing either way; `ember` exists to anchor the low end with a beat that is actually there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d59dd9d5dd |
Give the song bank notes instead of a test tone
The tonal content was a sustained sine stack at a fixed root, and the stage multiplier scaled that root directly — so a drop arrived at roughly 1.2kHz with ten harmonics stacked on top of it and held there for thirty seconds. Every statistic it was built to control came out correct. It was also unlistenable, and a bank nobody can stand to play is a bank nobody audits. There is a scale, a chord progression, a bass on the chord roots and a seeded motif over the loud stages. Key, mode and progression come off the song's seed so two entries are not the same four chords at different tempos. This is the plainest thing that qualifies as music — not trying to be good, trying to be playable enough that a person will listen to the bank and notice what is wrong with it. Register is the real fix. `stage.root` used to multiply the fundamental and now adds voices upward instead: bass and chord stay where they belong, a lead octave arrives when the arrangement opens up. Brightness still moves the harmonic content and the key's register, but across an octave and a half rather than a piercing sweep. Harmonics above Nyquist are dropped rather than left to alias, since folded energy would corrupt the centroid and flatness the bank exists to control. Two measured consequences. Tempo detection on the beatless entries got better, not worse — the bass gives a pulse where the drone gave nothing, and `drone` now reads 62bpm instead of 88. And enveloped notes put a floor of 0.27 under the measured dynamic range, because the gaps between notes are gaps a limitered master does not have; note decay flattens toward a hold as dynamics falls, which takes the compressed end back to 0.18. Coverage still passes on all six axes and all six section kinds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cfe77fd524 |
Finish the song bank: axis ranges that are real, and a collinearity check
Completes the bank whose first half went in with the scene commits. The remaining work was all in what "covered" means. Two axis ranges were wrong, and being wrong made the coverage report lie in both directions. Loudness is a raw mean spectral magnitude, not a normalised 0..1, so every song read as 0.00 and the axis looked dead. Centroid is mapped to a log frequency axis, so a track of nothing but sub-bass and a 55Hz pad still measures 0.28 and pure hiss measures 0.93 — against a nominal [0,1] the bank would have reported a permanent 50% gap that no synth change could close. Both now carry the range the statistic can really take on, with the reason. Spread on every axis is not coverage of the space, so the tool also measures correlation between axes. It immediately found brightness and noisiness moving together at r=0.95: noise colour had been tied to brightness, one axis wearing two names, leaving the dark-and-noisy quadrant unreachable. Noise colour is now its own parameter — hiss over a sub-bass pad is an ordinary record. That got r to 0.94, and no further. Flatness is the geometric mean of the spectrum over its arithmetic mean across the whole band, so a signal only measures flat if it has energy everywhere, which is the same thing as measuring bright; band-limiting the noise to hold the centroid down empties the top and drops the flatness with it. It is a property of the analyser and real material does the same, so it is allowlisted with its reason. Anything not on that list still fails — the check is there to catch a bank that has collapsed, not to relitigate physics every run. The output is gitignored. It is 172MB of deterministic audio derived from a table that will keep changing: rebuild it, do not carry it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0c5b0a8d4d |
Close out the twenty: what the gates taught
Three lessons worth more than the scenes: a slow axis has to move a large low-frequency area or the camera's own drift beats it, whole-frame luminance on the kick is the strobe the flash gate exists for and it arrives by accident, and a style trait expressed only as grain measures as no style at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
905c217193 |
Three glitch scenes: a press, a loop and a tape
Halftone Misprint is a printing fault rather than an electronic one — the image is never damaged, only separated and reassembled out of register, and nothing else in the library is made of dots. Droste Feedback scales the previous frame where Time Smear translates it, so the image never clears. Analog Wow is the continuous, wet counterpart to Scan Tear and Block Mosh: the error varies smoothly down the frame because every line was written at a different moment. Three things that had to be got right. The halftone ruling is a count of dots across the frame, not a pixel pitch, or a 4K export is the same dot on a bigger sheet. The Droste loop has to CONTRACT — expanding pushes every copy off the edge and leaves a plume instead of a corridor. And all three expressed style only through grain at first, which is the Side Quest 1 complaint: they now put the track's line weight and edge softness into the dot, the ring and the band boundary, taking the measured style response from 24/11/18 to 86/255/88. Neither feedback scene declares a slow axis, with the numbers recorded in the files: their own history moves the ten-second average further than any parameter does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91e74ff167 |
A cast census, and the arrangement it needed to be true
"Twenty-three of sixty-one scenes are never cast" was an artefact of the song it was measured on. synthesizeSectioned has one change point, so it segments into exactly two sections and both are quiet kinds — and intro, breakdown and outro are restricted to the restful families for every director. Half the library was unreachable before a seed was drawn, and the measurement reported that as a casting failure. synthesizeArrangement builds a real one: intro, build, drop, breakdown, drop, outro, shaped to hit the segmenter's own classifier rather than to sound like anything. Measured against a bank of those, two scenes out of sixty-one are never a background, not twenty-three. The census exists because "never cast" without a reason is unactionable. A scene can die at the signature gate, at the director's family table, or in the roster draw, and those are three different repairs — a trait declaration, a table, and arithmetic that neither fixes. It reports which one, and it separates being cast as a background from appearing as a translucent overlay, because a library can look fully used while nine scenes carry every frame. What it finds is that eligibility is almost entirely a function of how many traits a scene declares. Four-trait scenes are eligible for every track and open half of all videos; two-trait scenes are eligible for one track in fourteen; the single scene declaring one trait is eligible for none, ever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
656e062069 |
A seed variety test, and a metric that had to be gated before it was believed
Every other phase asks whether one video is correct. This asks whether two are different — the failure the suite could not see, since a generator that ignores its seed passes determinism, flash safety and liveness perfectly. Frames reduce to a structural descriptor built to be blind to the cheap axes and sensitive to the expensive ones: standardized luma kills exposure and palette, a Laplacian pyramid gives the radial spectrum, and the gradient-angle histogram is carried through a DFT magnitude so a rotation shifts it without moving it. Colour is measured and never counted; its only job is to expose the case where two seeds differ by a palette swap and nothing else. The score means nothing on its own, so it sits between two references the same instrument produced: a floor of how far one video travels from itself across its own sections, and a ceiling of the same pipeline with every layer recast at random. Two checks gate the instrument before any number from it is trusted — recolour must move structure ~0 while moving colour a lot, and a quarter turn must not move it at all. Three things this got wrong first and now does not. Averaging each video's probes into one descriptor washed out the structure being measured and put the floor above the ceiling; probes are matched instead, which is fair because the track is held fixed. Cosine distance on all-positive histograms scored unrelated scenes at 0.05, too compressed to be read; chi-square replaces it. Single-link clustering in the library sweep chained overlapping pairs into a fourteen-scene group that did not exist; complete-link means every pair inside a group is really a twin. The sweep runs against all 61 visualizations, and it finds what the per-scene distinct gate cannot, because that one compares raw pixels and structural twins are merely differently coloured. Both rendered gates fail as committed. That is the point of them: separation is 0.07 against a 0.35 target, two 4-cliques of scenes are one look each, and a third of the library is never cast. The instrument passes; the generator does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
875c3868dd |
Three geometric scenes, and two ways to strobe a whole frame
Voronoi Shatter (cells with no fixed shape, re-cut on the phrase), Apollonian Gasket (a packing built by inversion, so zooming finds more circles rather than finer noise) and Isometric Blocks (a lit, gridded, solid surface where Floating Geometry is bodies adrift). Two flash failures, both the same mistake in different clothes. Apollonian's hard depth cutoff popped a whole generation of discs in and out as the fold crossed it — now faded rather than cut. Isometric Blocks drove block height off the low end, so every tower in the frame grew and shrank together: 4/s. Moving the reaction to the edges and to a hash-chosen quarter of the tops keeps the pulse and drops the frame-wide swing. Voronoi Shatter declares no slow axis: its re-cut puts the ten-second average 0.127 from itself between any two windows, the highest noise floor in the library, and no parameter competes with that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
50ceb7afa1 |
Four structural scenes: a span, a well, an arcade and an aisle
Suspension Span is two curves and a rhythm of hangers where Girder Lattice is a texture of members. Stairwell Descent puts the vanishing point dead centre and turns each flight, so the spiral is in the structure rather than in the motion. Aqueduct March is masonry — the light comes through holes cut in a solid wall — and Data Aisle is the interior, close counterpart to Neon City's exterior. Two bugs worth naming: the arch openings compared a cell-local centre against a global coordinate, which put every arch out in the wings and left a blank wall; and the stairwell's depth fade was keyed on floor index, so it only darkened the few pixels at the centre and measured as doing nothing. Aqueduct March declares no slow axis and records the three candidates measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c8658a6d15 |
Four minimal scenes, and a transition control that was reading its denominator
Pendulum Trace (a harmonograph over its own fading ghost), Contour Map (a survey of a landscape with a tide in it), Shoji Grid (backlit panels that slide) and Balance Stack (a leaning pile under a crossing sun). Two of the four leave the slow axis undeclared and say why in the file: in a frame this empty the camera's own drift moves the ten-second average more than anything in the scene can, so a declaration would be a claim Phase 11 contradicts. Phase 4's transition control changed from max(before, after) to their sum. These scenes are calm, so casting them into the mid-shot control windows halved the denominator while the boundary peak stayed put — 0.124 with them, 0.130 without. For most of the window both stacks are live, four layers with accents in overlay and screen, and a nonlinear blend of two moving stacks moves more than either alone; "no worse than the busier half" was never a property a dissolve had. A real pop still clears the threshold by an order of magnitude. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b01e3aff6f |
Three organic scenes: a reaction, a mat and a surface
Turing Bloom runs activator-inhibitor in the feedback buffer, so the pattern is formed rather than drawn — the one organic here that will not go uniform. It declares no slow axis on purpose, and says why: its own convergence path moves the ten-second average by 0.10, and every candidate axis measured under that. Mycelium Web puts an organic on the ground instead of standing in front of the camera; the colony front is legible as area, which is also what makes its axis measurable. Scale Mosaic is a log-polar lattice with a shear, so the rows are spirals and every scale is the track's signature form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
10a4cff0f6 |
Three flow scenes with a still point in them
Rain Column, Magnet Lines and Kármán Street. The flow family was seven ways of advecting a noise field; none of them had an upstream, an obstacle or a rule. These do: rain falls in three parallax layers under one shear, the field lines are exact contours of the poles' stream function so they close on themselves, and the vortex street is shed by a body that never moves. The slow axes took four rounds to land. A fine-grained axis — line count, drop density — loses to the camera's own drift in the time-averaged comparison Phase 11 makes, so all three ended up on a param that moves a large low-frequency area: streak length, pole spread, and a wake haze that dissolves the streamlines it widens over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e6f5a2f0d5 |
Epic 2.5.2: make the framing actually reach the image
Revisiting the framing layer turned up that it was not reaching four of the forty-two scenes, and that none of its gates could have told us. Those gates check the PLAN — cue sizes, the distribution of shot sizes across a population, headroom, seek determinism — and a plan that never reaches the image passes every one of them. Rendered at wide, normal and close, Scan Tear, Pylon Grid, Pitch Shatter and the 3D Particle Field came back byte-identical at every size. The cause was a category error in the first implementation. Framing was applied inside sigCamera, which is gated on the `camera` personality trait — so a scene that declined the track's drift and sway silently declined the shot size as well. That gating is right for a TRAIT and wrong for framing, which is not one: framing is where the camera is standing for this shot, and no scene should be exempt from it because of an unrelated art-direction decision. - Framing now lives in the shader epilogue, applied to the coordinate every fragment scene is handed, so honouring it is not optional. uv is left unframed on purpose: it is screen space, and prev() and sigGrain belong to the output image rather than to the scene being filmed. - Scan Tear and Pitch Shatter build their image from uv deliberately — a signal artefact happens to the signal, not to the world behind it. They now slice on raw uv and build the field they displace from a new framedUv(p), so the tear stays locked to the frame while the imagery behind it is filmed wide or close. - Particle Field receives framing in update() and honours it as a camera dolly, which is what framing literally is when a layer has a real camera. Distance divided by scale, matching the fragment path where the coordinate is divided by it. New gate renders instead of inspecting: 42 of 42 scenes now respond to framing, weakest Ridge Terrain at 0.22 of its own brightness, against a 0.05 floor. Also drops a stale comment on Layer.setFraming that still claimed sigCamera applied it. 105/105 checks pass including the slow set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8f5ff3a239 |
Side quest 2: give three churning scenes something to develop
Curl Flow, Signal Decay and Circuit Bloom had no parameter that changed their structure. Each was one process at one scale, statistically identical everywhere and at every moment — which is the technical statement of "it looks the same for five minutes". Every pixel moving, the image never changing. Each got the large-scale structure it was missing, and the slow axis now has something real to walk: - Curl Flow gains a CURRENT: one broad band carries the filaments and the rest runs bare. The axis is the band's strength rather than its position, and that choice was measured — moving the band preserves the frame's total energy, so a ten-second average of it is nearly the same image wherever it sits (0.0028 across a full sweep). Opening and closing it changes how much of the frame is lit at all. The current gates the final image rather than only the vein term, because the feedback trail and the flow tint each fill the frame on their own. - Signal Decay gains a DAMAGE FRONT: lock falls away on one side of a moving boundary, so the stack has a shape instead of every lane being an independent coin flip. Hiss had to be gated by it too — hiss rises as lock falls, so without that a damaged lane simply traded signal for noise and carried the same energy, and the front cancelled itself out. - Circuit Bloom now actually BLOOMS. `reach` was a fixed vignette, so the packets ran, the pads blinked and the board never changed. Its range stops at 1.6 because the frame's far corner is ~1.4 units out and travel spent past that changes nothing — the first attempt wasted most of the axis up there. The gate itself was wrong, and this is the more important half of the commit. It compared the scene at thirty seconds against the scene at two and a half minutes and divided by what it did between those points with frozen parameters. Those are two different places in the song, so the denominator was full of audio reactivity: it was really asking "does the axis move this scene MORE than the music does", which no scene should have to pass. Circuit Bloom failed at 0.92x while its mean luminance moved 5.6x across the axis — a large structural change scored as nothing. The experiment now holds time constant and varies only the axis, against a control that is the same parameters over the next window — the residual churn the ten-second average failed to cancel, which is the noise floor it has to beat. With the axis disabled the two parameter sets are identical and the ratio is 0.00 by construction, so the companion check is a real discriminator rather than a formality. It reads 0.00x on all six scenes. Moiré Grid 28.7x · Gate Corridor 14.3x · Truchet Fold 3.3x · Signal Decay 1.9x · Curl Flow 1.7x · Circuit Bloom 1.4x, against a 1.25x bar. 104/104 checks pass including the slow set. Closes SIDE-QUESTS.md §2 and the open half of EPIC-2.md §3.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c44b527fe8 |
Side quest 3: fix the Horizon Lines determinism flake at the cause
Phase 7's determinism check reported Horizon Lines at 2/255 against a 1/255 tolerance, but only when phase 7 ran in isolation — a full run warmed the GPU first and it passed. A gate whose verdict depends on preceding load will eventually stay green through a real regression, and determinism is the one property this whole project is built on. The cause turned out to be geometry, not the driver. The lines were drawn with `smoothstep(u_thickness * (0.5 + u_sigLine), 0.0, d)` where the width is 0.008 scene units against a 0.028-unit pixel at 720p — a line under a THIRD of a pixel wide, ramping from full brightness to nothing across that third. The parameter range goes down to 0.002, which is a fourteenth of a pixel. That is a near-vertical cliff, and a cliff turns a float wobble of 1e-7 in a cancelling subtraction into a whole byte of colour. Nothing else in the library sums 40 such terms. Replaced with analytic coverage: `sat((w - d) / px + 0.5)`, where px is the exact height of a pixel in scene units. The transition now always spans one pixel, so no pixel sits on a discontinuity — and a line thinner than a pixel comes out DIM instead of being drawn at full brightness wherever a pixel centre happens to land on it. That second part is a real image fix as well as a determinism one: it is the end of the shimmer this scene has always had. Width stays in scene units, so resolution independence is untouched — the 320x180 vs 1280x720 diff still reads 0.00183 against a 0.06 limit. Measured: worst delta over two identical 20-frame captures drops from 2 to at most 1, usually 0. Phase 7 now passes in isolation, repeatedly, which is the condition that was failing. Horizon Lines is no longer even the worst scene on that metric. Also removed the dead `total` accumulator while in there. 104/104 checks pass including the slow set. See SIDE-QUESTS.md §3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
17a583a87f |
Side quest 1: give four scenes art direction other than grain
Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declared the `style` trait and honoured it with `col += sigGrain(uv)` and nothing else. A declared trait is a contract — the disqualification rule in Personality.js is the only thing keeping off-design scenes out of a track — so honouring it with dirt meant these four could not take the `texture: 0` opt-out the grain work introduced. Phase 9 measured their style response at exactly 0. They sat at texture: 0.35 as a stopgap, which kept speckle on the library's cleanest scenes purely to keep a gate green. Each already had the knob; it just was not wired to the track: - Classic Wave contrasts its wave through a bare smoothstep(0.2, 0.8). The transition width now comes from u_sigSoft and u_sigLine, centred on 0.5 so changing the hand does not change the exposure. Crest concentration is driven separately by u_sigLine, because a track is free to sample the scene's own u_softness at zero and the art direction must still show. - Silk Ribbon's strand width is u_sigLine and its falloff exponent u_sigSoft: a sharp track gets a filament with a defined edge, a soft one a haze. - Kaleido Tunnel drew its grid against a bare 0.42 — a line weight with no name. It is now a weight and a feather, both from the track. - Slow Orb's body edge multiplies the scene's softness by the video's, and gains a sigEdge rim so a sharp-handed track gets a defined limb. All four are now texture: 0. Style response measured against a maximally soft-handed versus maximally sharp-handed personality: Classic Wave 111, Silk Ribbon 216, Kaleido Tunnel 206, Slow Orb 102, out of 255 — previously 0. 104/104 checks pass including the slow set. See SIDE-QUESTS.md §1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b7c6fd1c5a |
Fix export frame loss: reject codec profiles that reorder frames
The exporter was losing roughly three of every four frames. A 20s render
muxed 319 samples instead of 1200 and played at 15.9 fps, with no error
reported and a file that looked superficially fine.
The cause was B-frames. WebCodecs delivers chunks in decode order, but
EncodedVideoChunk carries only a presentation timestamp — there is no decode
timestamp to recover the real order from. Handed presentation timestamps as
if they were decode timestamps, mp4-muxer saw DTS run backwards and rejected
every reordered chunk. That throw happened inside the encoder's output
callback, where it could not reach the export loop, so it surfaced as an
uncaught error and the render carried on. Only the I/P frames survived, one
per 4-frame GOP, which is exactly the stts pattern the files showed.
Writing the correct timeline instead is not available to us: it needs
negative composition offsets, and mp4-muxer emits ctts as a version-0 box,
which is unsigned. isConfigSupported says nothing about reordering, and
measurement showed latencyMode: 'realtime' does not prevent it either.
So pickVideoConfig now test-encodes 12 frames per candidate and checks the
order they come back in, taking the first profile that does not reorder.
Candidates stay in quality order, high profile down to baseline, so browsers
that never reorder keep the better profiles; baseline forbids B-slices by
spec and is the guaranteed floor. If every supported profile reorders the
export fails up front rather than after minutes of rendering.
Two guards so this class of loss cannot be silent again:
- The output callback catches, routing muxer rejections to the error list
the loop actually checks.
- Frames in and chunks accepted are counted and compared after flush, with
the reordering count and a gap histogram alongside. The count deliberately
tracks chunks the muxer took, not chunks that arrived — counting arrivals
reports success for frames rejected a line later.
Failures now raise a toast over the stage that stays until dismissed. An
export that dies after a long render should not sit unread in a panel.
Also corrects the record from
|
||
|
|
d31d0fc3a1 |
Fix export stutter: backpressure encoder queue and align opus rate
VideoEncoder.encode() silently drops frames once its internal queue saturates; the previous loop only polled encodeQueueSize every 10 frames, so a lagging hardware encoder lost frames in bursts (the "few frames every beat" bug). Gate every encode on the dequeue event instead, keeping the queue small but non-empty. Also resample PCM to 48 kHz when encoding opus so the muxer timescale, chunk timestamps and bitstream all agree (opus is natively 48k; a 44.1k source previously produced a file claiming 44100 while the stream was 48k). |
||
|
|
19bfff8651 | Epic 2.5.1: basis for framing layer |