- 25 Jun, 2026 7 commits
-
-
Identical audio is analyzed ONCE and served forever. This is the economic core that makes cheap-analysis-at-scale viable, and it makes the live emotion endpoint feel instant on repeats. WHY: per the design, repeat calls must cost ~nothing — that's the margin story vs cloud egress. A re-analyzed track shouldn't pay the CLAP compute twice. WHAT: - cache.py — content_id = sha256 of DECODED PCM (+ samplerate) so re-encodes / re-uploads of the same sound dedupe; raw-bytes fallback for formats we can't decode here. params_hash folds engine settings (model, bars…) so different params cache separately. get/put (JSON inline or a result_ref path for big artifacts), yt-id/url → content_id aliases (#29 will use them), and an LRU/size-cap gc() that unlinks evicted artifact files (cache is transient on the freebox — rebuildable). - db.py — cache + aliases tables (WAL already on), LRU index on (kind, last_access). - app.py — /v1/analyze/emotion now content-addresses the upload, serves cached reads with X-Douanier-Cache: hit, computes+stores on miss. Engine call still behind engines.ears (unchanged contract). - douanier.py — cache stats / gc admin commands. VALIDATION: - 22/22 tests green (+7 cache: stable/decode-invariant content id, raw fallback, miss→put→hit, params separate entries, hit counter, alias resolve, LRU gc spares the recently-touched entry). - Real-CLAP end-to-end: same clip twice → call 1 miss 10.6s, call 2 hit 0.002s = 5663x speedup, identical V/A. The headline product benefit, measured.
PLN (Algolia) authored -
Replace the walking skeleton's hardcoded dev-token stub with a real, self-hosted token store — the customs gate's papers check — keeping the dev token as a documented local-only bootstrap. WHY: hexa needs a proper 31-day scoped token (#33), and every future engine endpoint needs per-engine authorization. Self-hosted SQLite (no vendor, same DB that'll hold jobs/cache/usage) matches the 'self-host all' decision. WHAT: - db.py — stdlib sqlite3, WAL mode so the API + worker daemon read/write concurrently. Tables: accounts, tokens (token_hash, prefix, scopes, quota_json, expires_at, revoked). The DB is the one thing worth backing up; the artifact cache is rebuildable (noted in SRE.md). - auth.py — mint() returns the plaintext ONCE and stores only sha256(token), so a DB leak isn't replayable. verify() is an indexed hash lookup → a Principal (account, scopes, expiry); checks revoked + expiry. has_scope() honors the '*' wildcard. Scope-gated dependency require_scope(scope) in app.py: 401 unknown/expired/revoked, 403 scoped-out; scope=None = any valid token. - douanier.py — admin CLI (token mint/list/revoke, db init); the only way tokens are created (no public signup). list masks to a 6-char prefix. - /v1/me echoes the caller's identity (onboarding smoke test); /v1/analyze/emotion now requires the 'emotion' scope. VALIDATION: - 15/15 tests green (8 auth: mint→verify, wildcard, unknown→None, revoke, expiry, never-expires, only-hash-stored, dev-bootstrap; 7 smoke incl. /me guarded). No CLAP/torch needed; DB points at a temp file via conftest. - CLI smoke end-to-end: mint prints secret once + masks in list, expiry lands exactly 31 days out (2026-07-26), revoke flips state to REVOKED.
PLN (Algolia) authored -
Stand up Douanier — the ParVagues Audio-Intelligence API — as a new self-hosted service in armada/api/, and prove the lean MVP vertical end-to-end before investing in the auth/quota/jobs/cache machinery it'll later sit on. WHY: hexa (Shipow's hydra-live-hexa Studio) wants to call our engines (stems, loops, emotion, samples, features) live from his Vercel world. Rather than build six foundational tasks before the first useful call, we ship a walking skeleton: scaffold + ONE real synchronous endpoint + a stub token, to de-risk the three seams that actually matter — engines import cleanly, the service runs, a client can auth & call. WHAT: - FastAPI app (app.py) versioned at /v1: /v1/healthz (status + engine availability) and POST /v1/analyze/emotion (upload clip -> valence/arousal + top emotions), guarded by a hardcoded dev bearer that FAILS CLOSED (503 if no token configured, never accidentally unauthenticated). - engines/ adapter layer with lazy heavy imports — ears.emotion_of() wraps the real tide-table seam: sample_semantics.embed_audios -> emotion_ontology.score. The route never touches the engine directly, so #28 can wrap it with cache+async later without changing the public shape. - config.py (env-overridable paths/token/limits); requirements.txt documents the --system-site-packages venv trick (reuse the box's multi-GB torch/CLAP/demucs stack, add only FastAPI on top — Arch PEP-668 blocks system pip). - SRE.md: a context letter to whoever maps the public api.nech.pl path — the 10 hosting decisions that are theirs (path vs host routing, systemd vs Docker, ports, TLS, GPU dispatch, freebox mount, secrets, observability, WAF, CORS). - README.md + smoke tests. VALIDATION: - 5/5 smoke tests green (healthz; auth fails-closed/opens; happy-path mocked; 413 oversize) with no CLAP/torch needed. - Real engine seam confirmed importable, and the FULL pipeline run end-to-end on a synthetic clip: real CLAP read, valence 0.154 / arousal 0.141, V/A in range, 34.4s cold (model load — which is precisely why #28 warms CLAP once in the worker rather than per request).
PLN (Algolia) authored -
Structured archive of the night's 11 completed tasks (#5-#13,#16-#18 + audio fix) — grader/tierlist/correlation/finder/naming/merge, with the load-bearing learnings and measured numbers (ρ=0.34, recall 0.27→0.34, the 56min→coarse-to-fine + silent-window ∞ + PLP-octave + HTTP/1.0-audio gotchas). Blog/video source material; the board now shows only live work (#19 auto-tune, #20 batch-explore).
PLN (Algolia) authored -
Two PLN UX fixes from auditioning the loops panel: 1. Chops were way too short (0.06 s slices = clicks, not cuts). analyze_chops now floors at min_len_s=0.25 and extends to onset→skip-two for phrase-length options; vocal chops now land 0.29–1.07 s (verified) — actual word/phrase chops. 2. A 10+ vertical list was impractical to navigate. Replaced it with a TIMELINE overview (the research §6 design): every candidate is placed on the track by its start/length, coloured by lead stem, height + opacity by score — so you SEE where the candidates are at a glance and click one to focus. Below sits a single detail panel (stems, waveforms, audition, keep-toggles, Forge) for the selected candidate, plus prev/next stepping and a "candidate N / M" counter. Scannable map + one focused editor instead of a scroll-wall. Verified in Chromium: 8 segments render, click-to-focus (candidate 1→4), prev/next, no console errors; stem durations now display (the HTTP/1.1 + preload audio fix shows 2:43). Works for loops and chops modes (header reads "N-bar" or "chop · Ns").
PLN (Algolia) authored -
PLN's "merge of stems": the best vocal isolation cascaded into the best 4-way split. There is no 4-stem Roformer, so MultiModelBackend orchestrates a cascade — 1. Roformer (audio-separator, py3.12 venv) isolates vocals + instrumental; 2. demucs runs on the INSTRUMENTAL (vocals already gone → cleaner) for drums/bass/other; 3. merge = Roformer vocals ⊕ demucs drums/bass/other in the standard 4-stem contract. Refactored separate.py for composite backends: a backend declares what it `produces` and (for composites) a `run_composite` hook; the subprocess runner is factored into _run_cmd so the cascade can drive several sub-separations. RoformerBackend is now wired (was a stub): audio-separator CLI, 2-stem (vocals/instrumental), normalize() globs the "(Vocals)"/"(Instrumental)" output names. The default normalizer maps by produces[]. Verified end to end on the Loituma source: merge → vocals (Roformer) + drums/bass/other (demucs-on-instrumental), 4 clean PCM_16/44.1k stems. All three backends report available on this box. GUI renders whatever stems a backend emits (demucs/merge 4, roformer 2 incl. instrumental), canonical roles first. 53 tests (added: registry has the three engines, roformer produces 2 + builds its cmd, merge is composite/4-stem). Keeps the 2-stem Roformer door open as its own backend (vocal isolation), and the merge as the flagship. Foundry's engine registry now showcases the pluggable design PLN wants to grow into a product/API/service.
PLN (Algolia) authored -
PLN couldn't hear the Loituma stems and wanted the original too. The bytes/format were fine (PCM_16/44.1k, audio/x-wav, 206 Range all verified) — the problems were presentation + protocol: - Both audio servers defaulted to HTTP/1.0, which closes the connection after each response. <audio> seeking fires many small Range requests, so HTTP/1.0 makes playback/seek flaky in real browsers. Set protocol_version="HTTP/1.1" on the Foundry Handler and armada RangeHandler (every response sets an exact Content-Length, so keep-alive is safe). Reliable streaming + seeking now. - The stem players used preload="none", so they showed a misleading "0:00 / 0:00" until play — easy to read as "broken". Now preload="metadata": durations load and display (verified 164s on all five). - The ORIGINAL is now offered on separated cards too (an "original" row), not just on raw catches — so PLN can A/B the source against each stem. Verified end to end in Chromium: original + all 4 stems load 164s duration and currentTime advances on play, no errors.
PLN (Algolia) authored
-
- 23 Jun, 2026 20 commits
-
-
Follows 720e14d0 (canonical roots + catalog regen). Sweeps the rename through the authored layer that the catalog generators don't own, and corrects the release artifacts: Authored text (hand-fixed): backlog.md, release_priority.md, manifeste/calendar.md (setlist mentions); master_edl_take89.json (boundary name + bleed reason — Take89 is the Montreuil set); boundaries_take89_validated.json (ear-validated cut-6 label). Regenerated from those: boundary_bleed_take89.json. Found+fixed a hardcoded SETLIST in punkachien/build_stemmap_html.py (the name was baked into the generator, not read from data) and regenerated stemmap.html. Release masters (#29 + #22): retagged all 15 FLACs in Prod/Montreuil26_master/tracks_bandcamp/ — album → "Montreuil Algorave V3 — Mai Floral" (the CANONICAL tracks.json title; the old tag said "…V3 — Live@Les Nouveaux Sauvages", a venue string not the set title). Track 08 title → "Mafia Sans Serif" and file renamed 08-Aria_Sans_Serif.flac → 08-Mafia_Sans_Serif.flac. (Prod is plain files, not git — FLACs are the artifact.) Deliberately NOT touched: sources/soundcloud.jsonl is an EXTERNAL MIRROR of the published SC upload (which itself still reads "Aria Sans Serif" + "Montreuil Algorave III") — editing it would desync it from reality; the real fix is publish-side (tracked as a follow-up). eda_report.json carries the name as a cosmetic label and is freebox-EDA-gated — it refreshes on the next EDA pass, not worth an expensive regen now. The hexa public-audio copy of the FLACs is a downstream sync (Shipow's repo) — flagged, not edited here.
PLN (Algolia) authored -
Two findings while fixing the #22 track-name typo, both surfaced by tracing where the name actually lives rather than sed-ing the blast radius: PARSER BUG — the catalog generators were reading a dead path. Commit 6e51fff hoisted the site app out of next/ to the repo root, but five tide-table modules still pointed LIVES at /Work/Web/www/next/content/lives — which is now EMPTY. The committed catalog/map JSONs were stale pre-hoist artifacts; any regen would have silently produced an empty catalog. Fixed the path in build_track_recording_map, build_catalog_view, pattern_ngrams, tide_eda, boundary_bleed (+ 4 docs/README/conftest that documented the dead path). This is exactly the "a parser miss must never masquerade as data" footgun — the path was wrong at the source while the output looked plausible. RENAME — the track was renamed Aria→Mafia Sans Serif long ago in the score, but the canonical site tracks.json still carried name "Aria Sans Serif" AND a dangling file ref (aria_sans_serif.tidal, which no longer exists; the real score is mafia_sans_serif.tidal). Fixed at the canonical root (tracks.json, committed separately in the www repo) and REGENERATED the catalog via `tide.py build` — not hand-edited — so catalog.generated / catalog_view / pattern_registry / track_recording_map all pick up the rename from source. Validation: regen diff is clean — only the Mafia rename plus one legitimate side-effect (the catalog re-read the *current* mafia_sans_serif.tidal source, which had resetCycles commented out since the last build; the old cached source was from the pre-rename file). No other track's data moved (verified by grepping the diff for non-Mafia id/title/track changes — none). Neighbor-list reordering in pattern_registry is benign. Test suite: 80 pass, 1 fail — the failure (38c3-toilet recovers 0 backlog tracks) is PRE-EXISTING (reproduced with these changes stashed), a real data gap belonging to #27/#66, not this change. Interim n=172 emotion calibration JSON left uncommitted (regenerates at 534).
PLN (Algolia) authored -
Adds a sub-bar "chops" mode alongside bar-aligned "loops": analyze_chops segments a stem by onset (librosa.onset_detect + backtrack) and offers onset→onset / onset→skip-one slices (0.05–2.0 s), scored on the seam/zc/level rules a short chop still must honour (bars=0 marks a chop). find_takes(mode=) selects it; /api/loops takes mode; the GUI gains a loops/chops dropdown per catch (header + waveform render chop durations). 9 finder tests. HONEST measurement (not a recall win): chop-mode recall on the GT is 0.13 vs the bar-loop 0.34 — WORSE. Two real reasons, both worth recording: - PLN selects a few chops by TASTE from many onset candidates, so "did top-N contain his exact picks" is a harsh yardstick for a palette-browsing task; precision (0.15) confirms most onset slices aren't ones he'd keep. recall is arguably the wrong metric for chops. - naive onset windows over-segment (clean seam on any short slice ≠ a musically useful chop). A better chop ranker would need transient saliency + pitch/word-boundary cues. So LOOPS stays the default; CHOPS is an opt-in capability for when PLN is word-chopping a vocal — it produces clean, auditioning-ready sub-bar slices (verified end to end in the GUI: 0.06–0.51 s slices, waveforms, Forge), it just doesn't predict his selections. The recall gap on chop-heavy sources is a genuine research problem, parked on #18 with this finding rather than papered over. finder_eval_chops.json carries the numbers.
PLN (Algolia) authored -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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): grade
↔ usage ρ=+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 -
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 -
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 -
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: grade
↔ usage 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 -
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
-
- 22 Jun, 2026 13 commits
-
-
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 -
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 -
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 -
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 -
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 -
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 -
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 -
#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 -
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 -
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 -
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 -
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 -
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
-