1. 23 Jun, 2026 17 commits
    • data(tide-table): finder v2 pt.1 measured — recall 0.27→0.34 from grid-from-drums (#18) · cdf2f132
      Re-ran build_finder_eval with the shared drums-derived beat grid. Recall 0.27→0.34
      (+26% rel), precision 0.24→0.31 (+29% rel) over the 8 most-cut stems. The lift is
      concentrated exactly where predicted — vocal stems: Xxplosive/vocals 0.27→0.50,
      Doors/other 0.27→0.37. Confirms the §7 diagnosis (weak per-stem PLP grids on
      sparse-onset stems). Still below the 0.70 target; the remaining gap is sub-bar
      chops the bar-aligned finder can't form — #18 pt.2 (onset-driven chop mode).
      PLN (Algolia) authored
    • feat(foundry): finder v2 pt.1 — one beat grid from the drums, shared across stems (#18) · de0cb113
      Directly addresses the #7 finding: per-stem PLP grids are weak on sparse-onset stems
      (Doors/vocals recall 0.06 vs Gil-Scott-Heron 0.56), because PLP needs onsets to lock a
      pulse and a vocal stem barely has them. Fix: find_takes now computes ONE beat grid from
      the most rhythmic stem (drums → bass → first available) and reuses it for every stem, so
      vocals/other are cut on the same musical bar lines as the drums instead of each guessing
      its own. analyze_stem gained an optional `grid=(peaks,times,bpm)` param (None ⇒ per-stem,
      the CLI case); find_takes and build_finder_eval both pass the shared grid.
      
      Verified on Loituma: "beat grid from drums" → all four stems analysed on it, including a
      cross-stem bass+vocals take at 137s. 8 finder tests (added: analyze_stem honours a shared
      grid and adopts its tempo). build_finder_eval re-run with per-track drums grid is in
      flight to quantify the recall lift over the 0.27 baseline; the correctness win (shared
      grid ≫ weak per-stem grid on sparse stems) holds regardless. Pt.2 (sub-bar chop mode)
      remains on #18.
      PLN (Algolia) authored
    • feat(tide-table): finder recall harness + honest result (#7) — 0.27, below target · 09bace6a
      build_finder_eval.py measures whether the loop finder surfaces the windows PLN
      actually cut, using the correlation engine's provenance map (#12) as ground truth
      at scale. Metric v2: dedup GT into distinct sampled windows (PLN cuts many
      overlapping loops per stem), give the finder N≥#distinct candidates so recall isn't
      candidate-capped, and also report precision.
      
      Result on the 8 most-cut stems: recall 0.27 (45/169 distinct windows), precision
      0.24 — BELOW the 0.70 v1 target. Honest and diagnostic, not a pass:
      - The eval picked the hardest stems by design (ranked by #cuts) — CHOP-HEAVY sources
        (14–42 distinct cuts each: Doors, Xxplosive, MC Fioti, Brubeck). PLN chops them into
        short, often sub-bar pieces; the finder targets 2/4/8-bar LOOPS, so it can't
        reproduce sub-bar chops.
      - Vocal stems worst (Doors/vocals 0.06) vs rhythmic best (Gil Scott-Heron 0.56): the
        per-stem PLP beat grid is weak on sparse-onset vocals → poor candidate windows.
      - Loop choice is taste-driven anyway (house rule: finder suggests, human picks).
      
      Next steps surfaced (new task): (1) borrow the beat grid from drums/full-mix for all
      stems; (2) add a sub-bar onset-driven "chop mode". The harness is the katana for both.
      PLN (Algolia) authored
    • feat(foundry): expose demucs model variants — instant 4-stem A/B (#9, easy win) · 853eeaf7
      Groundwork toward PLN's "merge-of-stems multimodel": the finding that reshaped #9
      is that there is NO 4-stem Roformer — all Roformer models are 2-stem; the only
      4-stem (drums/bass/other/vocals) separators are the Demucs family. So the immediate,
      zero-risk stem-quality A/B is between demucs variants, which the DemucsBackend already
      supports via its `model` arg.
      
      Backends now declare a `models` list (demucs: htdemucs / htdemucs_ft / hdemucs_mmi /
      htdemucs_6s). /api/backends exposes it; the Foundry GUI gains a model dropdown beside
      the engine picker (hidden when a backend has ≤1 model), passed through to /api/separate.
      So PLN can now separate with htdemucs_ft (fine-tuned: vocals SDR 10.8 vs 9.9, bass 12.0
      vs 11.6) or htdemucs_6s (adds guitar+piano stems) and compare — no new backend.
      
      Verified: build_cmd respects the chosen model (-n htdemucs_ft), /api/backends returns
      the list, the selector renders all four. The full multimodel engine2 (Roformer-vocals
      + demucs-rest merged, the door PLN asked to keep open) stays on #9 with the design.
      PLN (Algolia) authored
    • feat(emotion): paired whole-mix-vs-stem comparator — the katana for #92 · e0635928
      The whole-mix-vs-stem decision (#92) was about to be called on a rigged
      comparison. `fit` grades whole-mix on every mix-analyzed track (163 so far,
      heading to 534) and `fit --stem` on every stem-analyzed track (24) — different
      SETS and different SIZES. The unpaired numbers (calibrated LOO 0.418 whole-mix
      vs 0.618 stem) screamed "whole-mix decisively wins," but most of that gap was
      sample size: 163 points fit a stable affine (in-sample→LOO gap 0.005), 24 points
      overfit it (gap 0.063). That's a sample-size artifact masquerading as a
      mode-quality verdict — exactly the kind of premature conclusion the build-the-
      katana-first discipline exists to catch.
      
      Approach: extract the raw/in-sample/LOO scoring into a shared `_evaluate()` so
      every mode is graded by identical code, then add `paired` — whole-mix vs stem
      head-to-head restricted to the slugs analyzed BOTH ways (the only honest
      comparison). Reports both modes' raw + calibrated-LOO V/A-err, quad-hit,
      emo-hit, plus an in-sample→LOO conditioning gap with an overfit flag.
      
      The surprise (n=26 paired, validated this run): on equal footing the verdict
      nearly inverts. whole-mix cal·LOO 0.569 / quad 54% / emo 23% vs stem 0.605 /
      58% / 27% — whole-mix wins distance by Δ0.037, but stem wins BOTH quadrant and
      emotion-hit, and both overfit (n=26 too small). The honest #92 answer is now
      "inconclusive until more stems exist," not "whole-mix decisively wins." Caveat:
      the paired subset is the hard cases (first-fetched = aggressive techno/dnb/
      dubstep, the high-arousal region drift + the ontology critique both flagged).
      
      Regression: `fit` output unchanged after the refactor. Interim calibration JSON
      (n=163 fit) left uncommitted — it regenerates at the full 534.
      PLN (Algolia) authored
    • feat(foundry): GUI loop panel — the Audacity-replacement picker (#6) · a94bf65a
      The Foundry's payoff surface: per separated catch, a "Find loops" button runs the
      finder (#5) and lays out its ranked Takes for the human to audition, prune, name,
      and forge into a kit — closing the url→stems→loops loop the whole tool was for.
      
      server.py: /api/loops (async job → find_takes → Takes JSON) and /api/export (runs
      export_take with the kept stems → Samples/<kit>/, then publish.link_kit). Stems
      already serve Range-capable for <audio>.
      
      ui/index.html: a Loops panel. Each Take shows score, bar-count, time window, bpm,
      tempo-unstable flag, and a row per stem with: a keep checkbox, role tag, a
      dep-free CANVAS waveform of that loop slice (decoded client-side via Web Audio,
      cached per stem, drawn in the role colour), a region-audition play button (loops
      [start,end] on the full stem via timeupdate), and the candidate score. A kit-name
      field (defaults to the slug) + Forge button writes the kept stems and auto-links.
      
      Verified end to end in Chromium (Playwright): Find loops → 8 Takes render, all 11
      stem-slice canvases paint (role-coloured waveforms — vocals pink, other cyan, drums
      faint where quiet), no console errors. Caveat: first waveform per stem waits on the
      full-stem decode (~1s each, 4 stems ~5s); subsequent takes from a cached stem draw
      instantly. A future win is byte-range slice fetch instead of full-stem decode.
      PLN (Algolia) authored
    • feat(foundry): correlation engine — recover loop provenance at scale (#12) · 7a79dc83
      engine/correlate.py recovers where each hand-cut loop was cut from: a loop is a
      literal slice of a separated stem, so normalized cross-correlation of the waveform
      has a razor peak at the true offset (true slice ~1.0, foreign ~0.03). build_provenance.py
      runs it over the whole corpus → provenance.json.
      
      Result (honest): 482/1690 loops matched (29%) across 28 kits — only the recent
      demucs-derived kits have a source in separated/; the rest predate the workflow, and
      we quantify that rather than pretend. Recoveries are exact and name-confirming:
      bumbum←MC Fioti "Bum Bum Tam Tam", diams_dj←Diam's "DJ", like_sugar←Chaka Khan
      "Like Sugar", praise←Fatboy Slim. Cross-stem alignment 0.34 — the measured answer to
      §0's open question (loops share a window across stems about a third of the time).
      
      Two bugs caught and fixed during the build, both load-bearing:
      - NAIVE all-pairs full-res NCC took 56 MIN on 1690×120. Fix: coarse-to-fine —
        rank every stem by NCC on a frame-RMS *energy envelope* (phase-robust, ~40× cheaper),
        then run the precise sample-domain NCC only on the top-3 candidates. Minutes, not an hour.
      - SILENT-WINDOW BLOWUP: catastrophic cancellation in the sliding-variance cumsum
        drove the normaliser to ~0 in quiet stem regions, so silence scored ∞ and
        masqueraded as a perfect match (an early run reported a bogus 55%/0.69 — ~440 false
        positives). Fix: floor the window norm at 5% of the stem's global energy and clamp
        NCC to [-1,1]. Scores are now all ≤1.0 (max 1.000, median 0.969); the honest 29%
        is what survived. Regression test added.
      
      forkserver gotcha: py3.14's default mp start method re-imports the module per worker
      (empty globals), so the preloaded-stems COW trick silently fails — pinned mp fork
      context. 8 correlate tests (NCC offset/foreign/gain-invariance/silent-window, envelope
      prefilter, locate top-k). Feeds the tierlist cut-critique and the finder recall eval (#7).
      PLN (Algolia) authored
    • feat(foundry): loop naming convention — engine/naming.py, derived from the corpus (#13) · 146070b5
      Recovered the ParVagues loop naming scheme empirically from 1690 existing loops,
      rather than inventing one: NN_<role>[_<character>]. The corpus EDA that grounds it
      (reproducible via `naming.analyze_corpus`, parsers-over-copy): 46% carry a 2-digit
      NN_ index (the hand-cut-kit signature, vs keyed/tempo packs like 120g/80c), 74%
      underscore-joined, only 16% repeat the kit name (the folder already carries it), and
      the role tokens cluster on voice/vocals, keys, guitar, bass, brass, synth.
      
      naming.py provides:
      - suggest_name(index, role, character=, section=) → NN_role[_section][_character],
        slugged + zero-padded + de-duped against names already taken in the kit.
      - stem_of(role) → maps a role/character word back to its stem family, so a name
        stays honest to what was actually separated (never infer role FROM the name —
        the finder knows the stem; the name reflects it).
      - lint(name) → flags drift (no index, spaces, uppercase, odd chars).
      - analyze_corpus() → re-derives the stats above from the live Samples tree.
      
      Wired as export_take's default namer (replaces the inline f"{i:02d}_{name}"), so the
      GUI Forge button and CLI export both land convention-compliant names; the GUI's
      per-stem name field still overrides. CLI: python3 -m engine.naming --suggest 3 synth dark
      | --lint <name> | --analyze. 6 tests; full foundry suite green.
      PLN (Algolia) authored
    • feat(bridge): live MIDI dashboard panel — SSE wired end to end (#16) · c0fd5d42
      Completes the parked MIDI feature (midimon.py + midistream.py were done/tested in
      270a01d6; this wires them into the always-on Bridge). The "do better than a raw
      aseqdump in a terminal" monitor now lives in the web dashboard.
      
      server.py: a module-global MidiStream fan-out (lazy — opens aseqdump only while a
      tab is watching, closes on the last unsubscribe). Two GET routes:
        /api/midi/ports  → list ALSA seq ports
        /api/midi/stream → Server-Sent Events of parsed events, ?port=addr to pick one.
      ThreadingHTTPServer gives each SSE connection its own thread, so blocking on the
      subscriber queue is fine; a ': ping' every 15 s holds the pipe open, and
      BrokenPipe/ConnectionReset on tab-close is swallowed and triggers unsubscribe.
      
      ui/index.html: a MIDI panel — port picker + connect/disconnect (EventSource) +
      rescan, and a live log. Rows are colour-coded by event (note-on green, note-off
      faint, control-change amber, pitch-bend magenta), show source/event/channel/
      note-name and a velocity bar (val/127), newest on top, capped at 200. Never hue
      alone: the event word carries the meaning, colour reinforces (DESIGN principle 4).
      
      Verified end to end: systemd --user restart, /api/midi/ports lists the seq ports,
      /api/midi/stream emits ': connected' then live events, and the UI connects (green
      "live") and renders rows — no console errors. With no hardware controller attached
      the feed shows system/subscription events; note/CC rows appear once a controller
      port is selected and played.
      PLN (Algolia) authored
    • feat(tide-table): self-updating corpus essay — template Story 02 from EDA + freshness badge (#17) · 24606d49
      PLN flagged the "2024, everything at once" stats as stale. Root cause: the chart
      BARS were data-driven but the PROSE around them was frozen string literals
      ("334 sample folders", "tripled to 14", "2 → 4 → 14 → 14 → 3-so-far", the section
      title, the chart tooltips, the magenta-highlight year). They'd drift the moment
      the corpus moved. Now every one of them derives from EDA at render:
      
      - breakoutFacts() reads vocabulary_growth + cadence → peak import month/count, the
        breakout year (argmax gigs), the year-over-year multiple, and the gig sequence.
      - Story 02's title, dek, legend, note, the magenta band, and both chart tooltips
        are templated from it. parsers-over-copy: no number is typed by hand.
      - Honesty fix surfaced by the templating: 4→14 gigs is 3.5×, not "tripled" (the old
        hand-copy undersold) nor "quadrupled" (a naive round oversells). The multiple-word
        only fires when the ratio is within 0.15 of an integer; otherwise it states the
        exact "grew 3.5×". Trust the instrument (DESIGN principle 1).
      - New freshness badge in the hero reads EDA.as_of and shows days-since (green ≤14d,
        amber ≤30d) plus the one-line refresh command (python3 tide.py build). Staleness
        is now visible and curable instead of silent — the data is 17 days old today.
      
      Verified in Chromium via Playwright: title/dek/badge all render from data, no
      console errors. Follow-up (needs PLN's call): a one-click rebuild from the Bridge
      (/api/rebuild → tide.py build) vs the current command hint.
      PLN (Algolia) authored
    • docs(tasks): archive #85 (pack-level grouping) to completed-archive · 0ca81596
      Pre-compact Pass 4: structured long-term-learning entry. The keeper insight —
      each grouping grain answers a different question (folder=author's label,
      pack=origin, sample=timbre); the origin-vs-sound MISMATCH (15/22 packs span the
      family wheel by design) is the story, not an error.
      PLN (Algolia) authored
    • feat(foundry): the loop finder (engine/loops.py) — §1 pipeline, per-stem (#5) · c17dd0d5
      Generates ranked loop candidates from a separated stem, the phase-2 design made
      real (PHASE2_FINDINGS §1/§3/§4): drift-tolerant PLP beat grid → beat-sync SSM
      (MFCC⊕chroma → recurrence affinity) → Foote checkerboard novelty → candidate
      windows at {1,2,4,8} bars → §3 composite score → NMS dedup. Works PER STEM (§0),
      then loosely groups candidates across stems by shared time window into Takes the
      human auditions together. export_take honours the B-rulebook (B1 ZC-snap, B6 bass
      mono-sum, B8 24-bit/44.1k, B10 no-normalize) and calls publish.link_kit.
      
      DRY with the katana: the loopability score reuses engine.grade's seam_score /
      zc_score on each candidate slice, and adds the two terms only the finder has the
      SSM context for — structural (does the window recur off its own diagonal?) and
      boundary novelty (clean segment edges). Weights w_struct .4 / w_novel .2 / w_seam
      .3 / w_zc .1, minus a tempo-instability penalty (§3).
      
      Caught while validating on the real Loituma stems: PLP latched onto the eighth-
      note pulse and reported ~250 bpm (Loituma is ~125), so 2-bar windows came out
      half-length — the §7 octave hazard, live. Fixed by pinning PLP's tempo range to
      one musical octave (70–160) and enforcing a min inter-beat spacing in peak-pick.
      Now reads ~120 bpm and 2-bar windows land at ~3.9s, squarely in the A-profile
      band (3.7–8.0s). find_takes on the 4-stem catch correctly clusters time-aligned
      candidates (e.g. vocals+drums sharing a 2-bar window).
      
      CLI: python3 -m engine.loops <stem.wav> [--bars 2 4 8] [--top N] [--json].
      7 new tests (checkerboard signs, Foote peak at a block seam, structural recurrence,
      NMS dedup, ZC-snap, and the tempo-octave guard). Full foundry suite: 41 pass.
      PLN (Algolia) authored
    • feat(tide-table): Hall of Fame showcase viz — tierlist.html (#11) · 4c9a8312
      The Foundry Hall of Fame, built through /impeccable in the Ship's Bridge language
      (armada/DESIGN.md): dark instrument surface, Geist + Geist-Mono, brand magenta
      reserved for the one earned accent (here the S-tier champions, the brand eyebrow,
      and the now-playing pulse). Register = product: quiet, instrument-grade, the ear
      leads and the screen serves.
      
      What it shows:
      - A classic S→D tier board. Tier is encoded redundantly (letter badge + lane
        position + colour), never hue alone — colour-blind safe by construction (DESIGN
        principle 4). Each kit is a chip: median grade, usage (tracks that play it),
        loop count, and a row of flag dots (seam/clip/off-grid/dc).
      - Click a kit → a drawer of its loops, each with a grade bar, tier dot, and a
        PLAY button. The ear leads: every loop auditions in place (loop-on-repeat via a
        shared <audio>, _corpus/<kit>/<file>.wav over serve.py), with a now-playing
        footer. (DESIGN principle 2.)
      - The rubric-validation callout, shown HONESTLY (principle 1, trust the
        instrument): gradeusage ρ=+0.34, with a log-scale scatter that *labels its own
        confound* — risers and weird_dialogs sit low-right (used most, tier low) because
        they're FX, not loops. The footnote states the coverage caveat (34/97 kits carry
        a usage token) rather than implying full coverage.
      - Search, filter (all/played/flagged), sort (grade/usage/name); skeleton load,
        empty states, focus-visible, keyboard audition, and prefers-reduced-motion
        fallbacks for every animation.
      
      Verified in Chromium at 1280/390 widths via Playwright: no console errors, drawer +
      audition + scatter all render, copy carries no em-dashes (impeccable ban). Audio
      served through the local _corpus symlink (gitignored). Runs via
      `python3 serve.py --dir tide-table --port 8731` → /tierlist.html.
      PLN (Algolia) authored
    • docs(blog): 02 — scaling the feeling + cross-model drift (#93) · 91334e26
      The overnight story: 60→534-track corpus via a 24-subagent workflow (schema-
      enforced GT, 0 invalid); cross-model drift (Sonnet relabels all 534) as a
      conscience — 72%% quadrant agreement, 174 uncertain-GT tracks concentrated on
      circumplex-straddling genres (hardstyle, uplifting trance, neoclassical); and
      the unplanned corroboration — a separate ontology-critique panel converged on
      the SAME high-arousal gap the drift flagged (groovy/propulsive/transcendent/
      menacing). Plus the infra lesson: only a systemd --user unit survives sandbox
      teardown. Calibration verdict (whole-mix vs stem at scale) left as the
      cliffhanger pending the grind.
      PLN (Algolia) authored
    • data(emotion): Sonnet ontology-critique panel — 40 proposals, the convergence (#93) · 75dba15e
      A 5-lens Sonnet panel (electronic gaps / canon gaps / synonym enrichment /
      overlap-anchors skeptic / cross-cultural) critiqued the 12-emotion wheel:
      18 new-emotion, 10 anchor-fix, 10 synonym-add, 2 redundancy proposals.
      
      The signal is the CONVERGENCE:
      - 'groovy' proposed independently by 3+ lenses (electronic, canon, cross-cultural)
        — the in-the-pocket body-lock register the wheel has no home for (hypnotic too
        cerebral, playful too cartoonish, warm has no swagger). Clearest accept.
      - the high-arousal boundary-straddlers — propulsive (peak-time diesel momentum),
        transcendent (blissed-out rave plateau), menacing (rolling predatory dread) —
        fill EXACTLY the region the cross-model drift report independently flagged as
        uncertain GT (hardstyle's euphoric-aggression, uplifting trance). Two separate
        analyses, same hole: the (+/- valence, high arousal) corner is under-vocabularied.
      - bittersweet (melodic-techno's beautiful ache), psychedelic (hallucinatory
        disorientation), plus cross-cultural saudade/duende/ennui (mostly fold into
        existing). dark anchor-fix A+0.20→+0.35 to separate from cold.
      
      NOT folded in tonight: changing the wheel mid-grind would split the corpus across
      two ontologies and invalidate the #92 calibration. Expansion + re-analysis is a
      deliberate v2 pass (new task). Tonight's corpus stays on the consistent 12.
      PLN (Algolia) authored
    • feat(tide-table): Hall of Fame tierlist data — grade 1690 loops, validate the rubric (#11) · f88273b8
      build_tierlist.py grades EVERY hand-cut loop in the Samples corpus with the Foundry
      katana (engine/grade.py — DRY, one rubric) and joins each kit against how often PLN
      actually plays it (`s "kit"` track-counts from catalog_view.json). 1690 wavs across
      97 loop-kits graded in parallel (~98s, 6 workers). Tiers: 4 S, 3 A, 24 B, 51 C, 15 D.
      
      The point of #11 was never the ranking — it's VALIDATING the rubric against reality
      (PHASE2_FINDINGS §5b): do the kits PLN reaches for grade higher? Result:
      gradeusage Spearman ρ = +0.34 — positive, the katana points the right way.
      
      The confound is the interesting part. The single most-used kit, `risers` (16 tracks),
      tiers only C — and that's CORRECT: risers are FX sweeps, they go quiet→loud and never
      wrap cleanly, so they fail seam + bar-consistency by design. They're heavily used as
      one-shot transitions, not loops. Same for weird_dialogs. So loop-mechanical-grade and
      usage diverge exactly where they should — for non-loop material. Filtering FX out would
      lift ρ; the divergence is a feature (it tells loops from FX), not a rubric failure.
      Meanwhile PLN's custom `f*` palette validates the rubric from the other side: fsynth,
      fguitar, fmono all land A-tier; fpiano/forgan/fbreak* B. S-tier exemplars: simmons,
      electrn, samples-cello-plucked, ouais.
      
      Caveat logged for follow-up: only 34/97 kits matched a usage token (folder name vs
      `s`-token mismatch for the rest, and catalog_view covers 73 tracks) — the correlation
      is over those 34. The grader's v1 thresholds (noisy dc-offset flag at 1e-4, seam dB
      cutoffs) are the next calibration lever now that we can measure against the corpus.
      
      7 pure-logic tests (spearman incl. ties/inverse, usage = tracks-not-occurrences,
      tier thresholds mirror the grader). Next: tierlist.html showcase (Ship's Bridge + impeccable).
      PLN (Algolia) authored
    • data(emotion): cross-model GT drift report — opus vs sonnet on 534 (#93) · 1081c74b
      A second model (Sonnet) independently labeled all 534 tracks; emotion_drift.py
      measures where it disagrees with the opus GT. Result:
        mean V/A drift     0.322
        quadrant agreement 72%
        emotion Jaccard    0.53
        174 uncertain-GT tracks (top-quartile drift or quadrant mismatch)
      
      The disagreements aren't noise — they CONCENTRATE on genres that straddle the
      circumplex, which is a real finding:
        - hardstyle (The Prophet, Headhunterz): triumphant/euphoric (+,+) vs
          aggressive/tense (-,+) — the genre's 'euphoric aggression' lives on the
          quadrant line.
        - uplifting trance (Cosmic Gate, Ferry Corsten, Markus Schulz): euphoric vs
          driving/tense — same boundary.
        - neoclassical (Nils Frahm, Ólafur Arnalds, Max Richter): calm-positive vs
          melancholic-calm.
      
      So ~33% of the corpus carries genuinely uncertain emotional GT, and it's
      predictable WHICH third. Those tracks should be down-weighted (not trusted) in
      the upcoming scale calibration — single-model GT would have hidden this.
      PLN (Algolia) authored
  2. 22 Jun, 2026 23 commits
    • data(emotion): GT corpus 60 → 534 tracks (electronic-leaning, +474, #93) · cb52ad1d
      Overnight scale-up. A workflow fanned 24 opus subagents over genre cells —
      electronic-heavy (Detroit/Chicago, modern techno, house, trance, dnb/jungle,
      dubstep, garage, ambient, IDM, trip-hop, dub, synthwave, EDM, minimal, French
      touch, hardcore, deconstructed, lofi) anchored by canon + diversity (hip-hop,
      rock/metal, soul/funk, jazz, world, modern classical). 480 authored → 475 unique
      → 474 merged (1 dup, 0 invalid: schema enforcement on the 12-emotion vocab +
      V/A range held perfectly).
      
      Coverage: all 12 emotions, 289 genre tags, every V/A quadrant >> floor (thinnest
      --=47). Labels ring true — Cybotron 'Clear' cold/hypnotic, 'Strings of Life'
      euphoric, Jeff Mills 'The Bells' hypnotic/tense, de Witte dark/aggressive.
      
      PLN's 'dataset size is the lever' — 9x the corpus for the scale calibration the
      n=5/n=60 reads couldn't settle. Audio fetch/analyze runs overnight (disk-safe).
      PLN (Algolia) authored
    • feat(emotion): overnight autonomous scale-up infra (#93) · 82ca8bb0
      Three tools for the unattended ~6h night run that scales the emotion GT corpus
      and validates it across models:
      
      - emotion_corpus_merge.py — fold model-authored GT batches into the corpus
        SAFELY: validate vs the fixed 12-emotion vocab + V/A range, dedup by slug AND
        by normalized artist|title (no track twice under two slugs), default the
        yt-dlp query. Idempotent.
      - emotion_overnight.py — disk-safe (volume at 97%%), deadline-bounded grind:
        phase 1 fetch→whole-mix→delete mp3 over the whole corpus (bank the cheap
        signal first); phase 2 re-fetch→demucs stems→delete for as many as the
        deadline allows (grows the #92 stem evidence base). Per-track try/except,
        yt-dlp throttle + consecutive-failure backoff, audio purged after every track.
      - emotion_drift.py — cross-model agreement on the GT itself: a second model
        (sonnet) labels the same tracks; high opus-vs-sonnet V/A drift or quadrant
        mismatch flags UNCERTAIN ground truth to down-weight, not trust. Single-model
        GT is a blind spot — a second pair of ears finds it.
      
      Spirit of PLN's overnight brief: scale the dataset (the lever), and check
      cross-model drift instead of trusting one model's priors.
      PLN (Algolia) authored
    • feat(foundry): the loop grader (engine/grade.py) — the katana (#10) · edb62258
      A pure grade(wav) -> LoopGrade that scores ANY sample against the empirical
      A-profile + the B mechanical rulebook (PHASE2_FINDINGS §2-§3, §5a): seam click,
      zero-crossing cleanliness, DC offset, bar-length self-consistency, level sanity,
      bass mono-compatibility, and the one-shot/loop class. Composite 0-1 → S/A/B/C/D
      tier + per-rule sub-scores + human-readable flags. This is step 1 of the phase-2
      build order — the instrument the finder (#5) and the Hall of Fame tierlist (#11)
      both reuse (DRY, feedback_build_katana_first): the finder grades hypothetical
      windows, the tierlist grades existing files, same sub-scorers.
      
      Two findings while validating against real ParVagues loops (crimewave/diams_dj/humpty):
      
      1. bar-consistency was DEAD — `librosa.feature.rhythm.tempo` doesn't exist in
         librosa 0.11 (it's `librosa.feature.tempo`), so a swallowed AttributeError sent
         every file down the no-tempo fallback → a flat 0.50 for all. Fixed; now crimewave
         drums correctly reads as 4 bars @ 121.9 bpm (matches the GT note ~121.8).
      
      2. seam was measuring the single-sample wrap step |x0-x[-1]| vs the signal's MEAN
         step — which unfairly punishes a loop for wrapping at a zero crossing (the
         steepest point: a *perfect* integer-period sine scored only 0.40). §3 wants
         waveform CONTINUITY, so seam now measures the second difference (jerk) at the
         wrap, |x0 - 2·x[-1] + x[-2]|, normalized by typical curvature — captures both a
         value jump and a slope break, and a clean wrap now grades ~1.0.
      
      Sanity: crimewave (a core-palette kit) lands mostly B-tier or above, with clipping
      /off-grid cuts correctly sunk to D — consistent with §5b's "core kits must tier high"
      validation target. The composite WEIGHTS + THRESH are tagged v1/provisional: the
      dc-offset flag is still noisy (1e-4 is spec-strict) and the seam dB cutoffs want
      calibrating — that's #11's job (corpus-wide grade.tidal-usage correlation), not
      eyeball-tuning on 3 kits. 28 tests pass (synthetic signals, relative assertions).
      
      Runs under system python3 (numpy/scipy/soundfile/librosa already present); no GPU,
      no network. CLI: python3 -m engine.grade <wav>… [--json].
      PLN (Algolia) authored
    • feat(foundry): engine2 Roformer VRAM spike — fits the 6GB card (py3.12, 3GB peak) · 1a201dfa
      Task #8. Question: is a SOTA Mel-/BS-Band Roformer reachable on the 6GB RTX 2060,
      or are we stuck on demucs? Answer: it fits comfortably — but the wall was never VRAM.
      
      Run 1 (system python 3.14) died at model instantiation: audio-separator's Roformer
      loader carries `Callable | None` type hints that beartype rejects under 3.14
      (BeartypeDecorHintNonpepException on `stft_window_fn`). Peak VRAM at crash: 921 MiB —
      nowhere near the limit. So engine2 is gated on the interpreter, not the card.
      
      Run 2 (python 3.12 venv) succeeded: bs_roformer_ep_317 separated the 164s Loituma
      source in 90s (~0.55× realtime) at a peak of 3051/6144 MiB — half the card, ~3GB
      headroom. torch 2.12.1+cu130, onnxruntime-gpu 1.27, CUDA detected fine.
      
      Caveat for the wiring task (#9): ep_317 is a 2-stem model (Instrumental/Vocals); the
      Foundry contract is 4 stems (drums/bass/other/vocals). #9 must pick a 4-stem Roformer
      checkpoint and re-confirm VRAM — but with 3GB to spare on a 2-stem pass, a 4-stem run
      is plausible. The spike is self-contained + idempotent (research/engine2_spike/spike.sh,
      SPIKE_PY pins the interpreter); result.json carries the numbers.
      PLN (Algolia) authored
    • docs(tasks): captain's log 016 — packs, the grouping above the folder (#85) · e4216f84
      The origin-vs-sound mismatch as a feature: a pack defined by where a sound came
      from (a song, a machine, a console) is expected to span the family wheel, and
      that scatter is the story. Layered grouping discipline = same as the folder: a
      hint to compare against, never a law.
      PLN (Algolia) authored
    • feat(tide-table): pack-level sample grouping above the folder (#85) · 96bc7bcd
      PLN's insight: the FOLDER is a loose convention, not the true grouping. rample*
      is ONE bank sliced A0..S57; 808* is one drum machine split per voice; *_commodore
      is one chip sliced by role. So adds the PACK granularity above the folder —
      per-pack / per-folder / per-sample, weakest-to-strongest binding (per-sample stays
      the only ground truth).
      
      sample_packs.py: a curated REGISTRY (drum_machine / bank / chip / source / genre)
      + an auto-grouper (prefix-before-digit & underscore head/tail tokens, ≥2 folders)
      that PROPOSES packs the registry missed — it caught trance_* and voices_*, now
      folded in. A pack needs ≥2 grounded folders (a 1-folder pack is just a folder; it
      may gain siblings when #84 grounds all 718 — provenance notes this).
      
      Rolls the per-SAMPLE family counts up to the pack and asks the real question:
      does a pack live in one audio family, or span the wheel? 22 packs over 83/150
      folders; 15 of them deliberately span multiple families:
      
        rample  → 8 families (snare/kick/hat/fx/break/pad…) — a bank IS a grab-bag
        jungle  → 11 families — a whole genre's kit
        808     → kick/hat/perc — one machine, three voices
        commodore → bass/fx/pad/lead/synth — the chip, every role
      
      That origin-vs-sound mismatch is the point, not noise: a pack defined by ORIGIN
      (a song, a machine, a console) is EXPECTED to scatter across the audio clusters.
      Feeds the Unwrapped viz (group-by-pack vs audio-cluster as a story lens) and
      vibe-search scoping. Never lets a pack boundary bias the audio analysis — it's a
      hint to compare against, never a law.
      PLN (Algolia) authored
    • docs(blog): start long-form devlog series — 01 emotion calibration · 4e5d6a23
      CLAUDE.md blog-archival rule: commit messages + task logs are blog source
      material. This adds the *narrative* layer above the terse armada/tasks/ log —
      the documentary 'what happened / what surprised us / what we learned.'
      
      01 tells the emotion-engine story: building CLAP synonym-cluster emotion reads,
      the surprise that a 22%% score hid a *systematic* warm-positive center-pull
      ('playful' fired for 18/60 tracks), and the affine-calibration fix that named
      the bias (valence +0.25 too warm, arousal compressed 1.7x) and corrected it
      under leave-one-out CV. Includes the n=5 stem-vs-mix call PLN refused to
      conclude on — honesty over a clean win.
      PLN (Algolia) authored
    • feat(emotion): fit whole-mix V/A calibration on full 60-track corpus · f67edc67
      #91 batch finished — all 60 famous tracks fetched + whole-mix analyzed.
      First honest baseline on the full corpus (was n=5 before):
      
        top-1 emotion hit : 22%   (47% w/ overlap)
        V/A quadrant hit  : 42%
        mean V/A error    : 0.573
      
      The center-compression hypothesis is confirmed at scale: 'playful' is the
      predicted top emotion for ~18 of 60 tracks (Fleetwood, Marvin, Coltrane,
      Avicii, Journey, Eminem...) because the distribution-weighted mean of the
      circumplex anchors collapses toward the warm-positive centre.
      
      Affine de-compression (gt ≈ a·pred + b, leave-one-out CV) learns exactly
      that bias and corrects it:
        valence:  ×1.03  −0.25   (reads +0.25 too WARM — the playful pull)
        arousal:  ×1.70  −0.39   (compressed ~1.7x — needs the most stretch)
        LOO V/A err 0.573 → 0.519,  quad 42% → 50%.  Modest but real & cross-validated.
      
      calibrate.py now writes per-mode files (calibration.whole-mix.json /
      calibration.stem.json) so the #92 whole-mix-vs-stem decision can compare both
      without one clobbering the other; calibration.json mirrors the freshest fit.
      
      Stem-aware batch (demucs, all 60) launched in background to settle #92 on the
      full corpus, not the n=5 that PLN rightly refused to conclude on.
      PLN (Algolia) authored
    • docs(tasks): structured completed-task archive (#65, #64) for long-term learning · 73063f4a
      New armada/tasks/completed-archive.md — per-task entries with description/done/learnings/
      deps + commit hashes, richer than board-archive.md's terse snapshot, written as blog/video
      source material. Seeded with this session's completed tasks: #65 (serve.py broken-pipe fix,
      d70c3ee7 — keystone audition QOL) and #64 (Information-is-Beautiful dataviz epic + its
      craft learnings). Appended by the /pre-compact ritual before clearing the board.
      PLN (Algolia) authored
    • feat(bridge): midistream groundwork for the live MIDI dashboard panel · 270a01d6
      MidiStream fans one aseqdump subprocess out to N web subscribers (SSE), reusing
      midimon.parse_line + enrich (one parsing source of truth); reader runs only
      while watched (lazy start / stop on last unsubscribe, so no MIDI port held idle).
      parse_ports is pure + tested. NOT yet wired into server/UI — see resume task.
      
      Parked mid-feature: server SSE endpoint + dashboard panel still to do.
      PLN (Algolia) authored
    • fix(bridge): Armada launches into the catalog viz, not a dir listing · 55660a2c
      Clicking L'Armada served armada/ root → a file listing (felt broken). Point it
      at armada/tide-table (where the viz lives) and open /triangle.html — same as
      `tide.py serve`. Verified: /triangle.html → 200.
      
      Also lands midimon.enrich() (note/velocity/controller→structured) + tests, the
      parsing groundwork for the live MIDI dashboard panel.
      PLN (Algolia) authored
    • feat(bridge): fleet launchers + MIDI monitor + unified tray · 1eba89fc
      The hub was dead links (clicking Foundry 404'd — its server wasn't running).
      Now it's a real control center, with one shared registry driving both faces.
      
      - launchers.py: allow-listed registry. Web tools (Foundry/Armada) = start-or-open
        via port-check (spawn the server if down, then open); native apps
        (Pulsar/Ardour/qjackctl) launch detached with running + availability state.
        Wayland has no portable focus → running apps report 'already-running'.
      - midimon.py: 'aseqdump | tr' done right — parses each event, renders aligned &
        colourised with note names (C4) and velocity bars. parse_line is pure/tested.
      - Bridge UI: hub shows live running dots; start-or-open opens a blank tab within
        the click gesture then navigates once the server binds (no popup block).
      - perf-tray: 'Open Bridge' + 'Launch ▸' submenu off the SAME launchers.py — the
        tray is now the unified ParVagues tray (DRY with the web hub).
      - 17 tests (launcher registry/state + MIDI parser). Verified: Foundry card boots
        the server → :8765 200.
      PLN (Algolia) authored
    • feat(bridge): desktop launcher entry + ParVagues wave icon · ba1cb149
      So 'parvagues' is findable in the KDE app menu (the Bridge runs headless as a
      systemd service → had no .desktop, nothing to index). Entry opens the dashboard;
      ParVagues two-tone wave SVG as the icon. Installed via symlink into
      ~/.local/share/applications + kbuildsycoca.
      PLN (Algolia) authored
    • feat(tide-table): GT corpus wave 2 (→60, quadrant-balanced) + V/A calibrator · 0dcd2f10
      Corpus 22→60 tracks, all 4 V/A quadrants now ≥13 (was +-:4, --:2 thin), 50 genres,
      all 12 emotions — chosen for confident, balanced priors (quality of pseudo-GT > raw
      count for calibration). Added the sad/dark-calm + calm/pleasant fillers (Portishead,
      Mazzy Star, Satie, Debussy, Coltrane, Bon Iver…) the gaps report flagged.
      
      emotion_calibrate.py: fits per-axis affine gt≈a·pred+b over analyzed∩GT to DE-COMPRESS
      the center-biased CLAP read (learns the scale-up from data, not a guessed tau).
      Reports leave-one-out CV error (honest generalization, not in-sample) + re-derives the
      label from calibrated V/A (nearest anchor) so we see if labels improve too. Pragmatic
      call: scale corpus = whole-mix analysis (demucs doesn't scale to 100s/1k; stems didn't
      beat mix on accuracy) — stems reserved for the hexa lane.
      PLN (Algolia) authored
    • refactor(perf-tray): fold onto shared tools/bridge/perf.py (DRY) + wave icon · ce3da569
      - perf-tray.py drops its duplicated Thermals/detect_mode/_read*/MODES/SCRIPT
        and imports them from tools/bridge/perf.py — the tray is now just the Qt face
        of the same logic the web Bridge serves (one source of truth)
      - tray icon restyled with the ParVagues signature wave (drawn in Qt, no asset)
        under the heat-coloured temp badge
      - parvagues-bridge.service → WantedBy=default.target so it boots headless with
        linger (always-on dashboard, no login needed); tray stays graphical-session
      - deployed: bridge enabled+active on :8773, tray restarted, linger enabled
      PLN (Algolia) authored
    • feat(tide-table): emotion GT corpus + grader — training-knowledge priors vs CLAP · 3afc4309
      PLN's validation idea: famous tracks whose feeling/structure/genre the model already
      knows = pseudo-ground-truth to grade the emotion engine, disagreements driving tuning +
      coverage (self-correcting 30→100→300). emotion_corpus_gt.json: wave-1 22 tracks spanning
      all 12 emotions + V/A quadrants + genres + structural archetypes (steady / quiet-loud /
      long-build); GT = emotion labels + V/A centre + approx fractional section structure +
      genre. emotion_corpus.py: fetch (yt-dlp android+bestaudio 403 workaround) → analyze
      (demucs+stem-aware or --mix) → grade (emotion-hit, V/A error, quadrant, vocal-presence) →
      gaps (coverage histogram → next wave).
      
      BASELINE (whole-mix, 5 tracks): 40% emotion-hit, 40% quadrant-hit, mean V/A err 0.75 —
      honest verdict: NOT yet trustworthy. Three systematic biases surfaced: (1) center/positive
      compression (extremes pulled to mild +V/+A), (2) 'playful' over-fires (3/5), (3) whole-mix
      misses aggression entirely (Prodigy Firestarter→playful +V) — the exact case stem-aware
      (drums/bass arousal) should fix. Corpus now drives calibration before the #90 hexa handoff.
      gaps: +- and -- quadrants thin, fill at wave 30.
      PLN (Algolia) authored
    • feat(tide-table): stem-aware emotion_timeline — fuse per-stem CLAP reads · 59fb2a11
      Adds analyze_stems(): separates via foundry_stems (vocals/bass/drums/other), scores
      each stem on a shared window grid, fuses V/A by stem role (valence rides vocals+other,
      arousal rides drums+bass+mix), and flags vocal presence per window as a structure cue.
      Refactor: shared _finalize (smooth→relabel→section→summary) now serves both whole-mix
      analyze() and analyze_stems(); embed_at/window_grid/windows helpers extracted.
      
      FINDING (Bohemian Rhapsody, mix vs 4-stem): stem-aware gives a cleaner valence track +
      vocal-presence sections (88% vocal; outro correctly reads calm, not 'cold'), BUT the
      current arousal fusion UNDER-weights the mix and softened the hard-rock peak that the
      whole-mix caught (tense A+0.77 → hypnotic A+0.55) — that section's intensity is a
      full-mix gestalt isolated stems lose. So fusion weights need empirical tuning; the
      ground-truth corpus (next) drives it. Both modes run end-to-end (#89).
      PLN (Algolia) authored
    • feat(bridge): The Bridge — always-on ParVagues web dashboard + perf toolbar · 63ce1034
      Answers 'is there a ParVagues web service running a dashboard?' — now yes.
      Folds perf-tray's function into a web toolbar + extensible fleet hub.
      
      - perf.py: rootless Thermals (hwmon) + detect_mode + sudo-backed set_mode,
        ported from perf-tray.py with Qt dropped (DRY-able with the tray later)
      - server.py: stdlib http.server (Foundry lineage); GET/POST /api/perf, /api/hub.
        Binds 127.0.0.1 by default — mode-switch flips the CPU governor via sudo, so
        not LAN-triggerable unless --host 0.0.0.0
      - ui/index.html: Ship's Bridge toolbar (live temp badge 55/70/85, mode switch,
        freq/fan/throttle) + hub cards (Foundry :8765, Armada :8731)
      - bridge.py CLI (serve/status/perf); parvagues-bridge.service (systemd --user)
      - 6 tests on pure perf logic; README incl. deploy + security notes
      
      Smoke-tested: tests green, API live, mode detect + thermals reading correctly.
      PLN (Algolia) authored
    • feat(tide-table): foundry_stems — bridge to the Foundry demucs engine · 607c408d
      Stem-aware emotion needs the voice isolated: the full mix is muddy for CLAP, but
      the VOCALS carry valence, DRUMS+BASS carry arousal, and vocal presence is itself a
      section/structure cue. ensure_stems(file, slug) consumes the Foundry's in-process
      Python API (tools/foundry, the shared separation engine) → {mix,vocals,bass,drums,
      other} absolute paths, idempotent via catch.json (re-runs free). Validated: 4-stem
      demucs separation of a 6-min track (Bohemian Rhapsody) lands all four stems clean.
      Feeds emotion_timeline's stem-aware analysis (#89) and the hexa lane (#90).
      PLN (Algolia) authored
    • docs(foundry): API integration guide + phase-2 synthesis & research · ecfa108c
      - README: 'Use as an API' — Python import / CLI subprocess / HTTP API, the
        path+catch.json contract, env overrides (for sibling tools e.g. the #89
        emotional-arc tool that consumes the demucs engine for stems)
      - README: add 'link' to CLI reference
      - research/{A,B,C}_*.md: the three strand reports (empirical profile,
        fundamentals rulebook, prior-art + recommended zero-dep stack)
      - research/PHASE2_FINDINGS.md: strand-D synthesis — corrected per-stem model
        (loops cut per stem, often-but-not-always time-aligned), loopability rubric,
        Take/StemSlice model, correlation engine (§5·0 GT-at-scale), Hall of Fame
        tierlist, section-label grounding ( #89), 80/20 build order
      - PHASE2_LOOPFINDER.md charter: grounded in the real flow + ground-truth pairs
      PLN (Algolia) authored
    • feat(tide-table): emotion_timeline — slide CLAP emotion kernel over a track → arc · 20bc875e
      Windows a master (~12s/50% hop), CLAP-embeds each window (new sample_semantics.
      embed_audio_arrays seam), runs the emotion_ontology kernel per window → (valence,
      arousal, top-emotion) series; median-smooths, relabels from the smoothed circumplex
      point, fuses short runs into emotion SECTIONS. Emits frames+sections+summary JSON —
      the continuous-V/A + discrete-scene lane hexa wants. Validated on Bohemian Rhapsody:
      caught the warm-intro → energetic-middle → tense rock-peak (A+0.77) → calm-outro arc.
      PLN (Algolia) authored
    • feat(tide-table): CLAP emotion ontology — synonym clusters on the V/A circumplex · cc0dc274
      Each emotion is a CLUSTER of angle prompts (adjective pairs + scene metaphors +
      bodily framings, EN+FR) marginalized to one robust per-emotion score (mean cosine,
      size-invariant) — the synonym→emotion fold mirrors the resolver's descriptor→family.
      12 emotions × 6 angles, each placed on Russell's valence/arousal circumplex, so the
      distribution yields a continuous V/A point + a discrete top-emotion label.
      
      Per-window kernel: score(ae_row) takes one CLAP audio embed → {scores, dist, top,
      valence, arousal, confidence}. Validated: probe archetypes land correctly (melancholic
      0.76, aggressive 0.94, hypnotic 0.94, cold 0.72); real-audio path confirmed (pad→
      dreamy/melancholic calm). Feeds the planned per-track emotion timeline (hexa lane).
      PLN (Algolia) authored
    • feat(foundry): close-the-loop publish + phase-2 loop-finder charter · 56c81e57
      - engine/publish.py: link_kit() symlinks Samples/<kit> → Dirt-Samples/<kit>
        so a freshly-cut kit loads as s "<kit>" (325/508 Dirt-Samples entries are
        this pattern). Idempotent; repoints stale links; refuses to clobber real dirs.
      - foundry.py link <kit> CLI; 5 new tests (15 total, all green)
      - PHASE2_LOOPFINDER.md: research charter for the loop-finder (Sonnet-agent
        deep-research pass) — grounded in the REAL flow (yt → separated/htdemucs →
        hand-cut → Work/Sound/Samples → ln → Dirt-Samples → .tidal), with the
        crimewave/diams_dj/humpty stem→loop pairs as free ground truth
      - README: export target corrected to Work/Sound/Samples + link step
      PLN (Algolia) authored