// Which families answer which section kind — as a per-track CHOICE rather than // as a constant. // // This was a module-level table in LookGenerator, identical for every track ever // generated: an intro was always minimal/flow/organic, a drop always // geometric/glitch/structural, and intro and outro were literally the same list. // Every video therefore made the same genre decisions before a single seeded // draw happened, which is a large source of cross-track sameness hiding inside // something that looks like configuration. // // Measured, the cost was concrete: across twelve tracks, twenty-nine of // forty-two scenes were cast in none of them. The library was not too small — // most of it was unreachable. // // A director is one coherent point of view about what a song looks like. Not a // shuffle of families: each mapping is internally consistent, and the ordering // within a kind matters because the first family is weighted three times the // third. Two tracks with different directors disagree about what a drop IS, // which is the level at which videos should differ. /** * Families a QUIET section is allowed to draw from, whatever the director * thinks. Phase 7 has enforced this since minimal existed, and the first draft * of this file broke it — `corrupt` opened on glitch and `geometer` opened on * geometric, on the theory that a point of view should apply everywhere. * * It should not. An intro that opens on a strobing scene and a breakdown that * answers a lull with a dense pattern are not bold, they are the two specific * mistakes the family coupling was introduced to prevent, and a viewer meets * them within fifteen seconds of pressing play. So intro, breakdown and outro * are off limits to the loud families for every director. * * The identity lives in build, drop and sustain — half the kinds, and the half * anyone remembers — plus which of the restful families a director leads with * when it is being quiet. */ export const RESTFUL_FAMILIES = ['minimal', 'flow', 'organic']; const QUIET_KINDS = ['intro', 'breakdown', 'outro']; /** * Each director maps every section kind to three families, most-preferred * first. Every kind must be present — a missing one silently falls back to the * whole library and the point of view is lost exactly where it matters most. */ export const DIRECTORS = [ { name: 'ambient', // Patient camera to match: long moves, mostly along one line. See // look/Camera.js — a director's point of view now includes how it // shoots, not only what it points at. camera: 'contemplative', // The original table. A drop resolves into geometry; everything quiet is // minimal. Still the most broadly applicable, so it keeps the most weight. weight: 3, families: { intro: ['minimal', 'flow', 'organic'], build: ['structural', 'geometric', 'flow'], drop: ['geometric', 'glitch', 'structural'], sustain: ['organic', 'flow', 'geometric'], breakdown: ['minimal', 'organic', 'flow'], outro: ['minimal', 'flow', 'organic'], }, }, { name: 'brutalist', // Holds, then commits to one large move. Architecture is looked AT. camera: 'deliberate', // Everything is architecture. Quiet means empty rather than soft, so it // leads on minimal and reaches for organic last. weight: 2, families: { intro: ['minimal', 'organic', 'flow'], build: ['structural', 'geometric', 'glitch'], drop: ['structural', 'glitch', 'geometric'], sustain: ['structural', 'geometric', 'flow'], breakdown: ['minimal', 'flow', 'organic'], outro: ['minimal', 'organic', 'flow'], }, }, { name: 'organicist', // Never settles, because nothing here is ever finished settling. camera: 'roaming', // Nothing is ever built; things grow and dissolve. Deliberately never // reaches for glitch — a point of view is defined by what it refuses. weight: 2, families: { intro: ['organic', 'flow', 'minimal'], build: ['organic', 'flow', 'structural'], drop: ['organic', 'geometric', 'flow'], sustain: ['organic', 'flow', 'minimal'], breakdown: ['organic', 'minimal', 'flow'], outro: ['flow', 'organic', 'minimal'], }, }, { name: 'corrupt', // Cuts with the camera already moving. camera: 'kinetic', // The signal is damaged and the damage is the subject — everywhere the // damage is allowed to be. Its quiet sections lead on flow, so the calm // reads as signal drifting rather than as rest. weight: 2, families: { intro: ['flow', 'minimal', 'organic'], build: ['glitch', 'structural', 'geometric'], drop: ['glitch', 'geometric', 'structural'], sustain: ['glitch', 'organic', 'flow'], breakdown: ['minimal', 'flow', 'organic'], outro: ['flow', 'minimal', 'organic'], }, }, { name: 'geometer', // Small, exact, always arrives — the pattern is the subject and the // camera does not editorialise about it. camera: 'precise', // Pattern first, everywhere, at every energy. The drop is not an // explosion, it is the pattern at its densest. weight: 2, families: { intro: ['minimal', 'flow', 'organic'], build: ['geometric', 'structural', 'flow'], drop: ['geometric', 'glitch', 'structural'], sustain: ['geometric', 'organic', 'flow'], breakdown: ['minimal', 'organic', 'flow'], outro: ['minimal', 'flow', 'organic'], }, }, ]; export const DIRECTOR_NAMES = DIRECTORS.map((d) => d.name); /** * Cast the director for a track. * * The audio tilts the odds and never decides: a noisy, loud track is more * likely to be filmed as corrupt or brutalist and a tonal one as organicist, * but every director stays reachable for every track. Predictable mapping from * measured features to look is the failure mode this whole layer exists to * avoid — it is how a library ends up with one house style per genre. */ export function pickDirector(summary, rng) { const noisy = Math.min(1, (summary.meanFlatness ?? 0.2) * 3); const bright = summary.meanCentroid ?? 0.5; const weights = DIRECTORS.map((d) => { let w = d.weight; if (d.name === 'corrupt') w *= 0.5 + noisy * 2.0; if (d.name === 'brutalist') w *= 0.6 + noisy * 1.2; if (d.name === 'organicist') w *= 0.6 + (1 - noisy) * 1.4; if (d.name === 'geometer') w *= 0.7 + bright * 1.0; return w; }); return rng.pickWeighted(DIRECTORS, weights); } export function directorByName(name) { return DIRECTORS.find((d) => d.name === name) || DIRECTORS[0]; }