@wenathlan/debonair 2.0.118 → 2.0.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ # planning.pipeline — the 120+ step ladder of the full music pipeline
2
+
3
+ > the wave the owner ordered: ingest a real track → the engine decomposes it
4
+ > into the parametrized vector document → the document lands in the db with
5
+ > the owner id and the big pointers → the references power the pattern miner
6
+ > per genre → the composer generates a similar vector json → the engine
7
+ > renders real audio → the take plays, publishes and shows in the vitrine.
8
+ > every heavy stage runs SERVER SIDE (the gateway jobs, the saddle AVM
9
+ > modalities) — the visitor's browser, cpu and gpu never run the pipeline.
10
+ > companion of docs/planning.ladder.md (the kathesis verification ladder)
11
+ > and docs/planning.v2.28-completo.md.
12
+ >
13
+ > ## the code synthesis of outros.debonair (the reference verdict)
14
+ >
15
+ > the full code read of the reference folder closed the method question —
16
+ > the parts that PROVED real sound, the parts the composer mines:
17
+ > - `v2/src/lib/audio/engine.ts` — the voice recipes to reimplement root
18
+ > side: kick pitch env 165→52→40 hz with the noise click, snare noise
19
+ > bandpass 1750 + dual triangle body, hat hp 7800, clap 4 bursts 9.5ms
20
+ > apart, 808 bass pitch snap hz*2→hz in 45ms through a drive shaper,
21
+ > supersaw lead, pad lp with the 0.28hz lfo, keys tremolo, master chain
22
+ > tanh waveshaper k=3.2 → compressor −13/18/4.5 → analyser, feedback
23
+ > delay 0.27s fb 0.34, reverb noise impulse tail pow(1−i/len, 2.2), the
24
+ > 140ms look-ahead transport and the OfflineAudioContext render mirror →
25
+ > wav 16-bit / mp3 lamejs — the render proof the composer rides.
26
+ > - `v2/src/lib/audio/generator.ts` — the composition brain: 48 scales, 9
27
+ > style presets with exact drum lanes (trap kick [0,6,10] snare [4,12]
28
+ > hat 16th lanes clap [4,12] swing 0.12; drill, lofi, house...), bass
29
+ > locked to kick, markov 1st-order melodies with the ±3 semitone bias,
30
+ > DRIFT_CYCLE and ENERGY_CYCLE build/drop, seeded prng (same prompt same
31
+ > take), vary() diatonic drift.
32
+ > - `v2/src/app/api/analyze + similar + trainOnProject` — the pattern
33
+ > miner shape: note bigrams, 16-step drum histograms (adopt steps above
34
+ > 0.55 after trainedOn ≥ 3), embedding centroid, bpm histogram, cosine
35
+ > similarity over the 32-dim embedding — the exact miner PV3
36
+ > reimplements over our ingest documents.
37
+ > - `v1/v1 page.tsx` — the 6-layer biquad separation, the pcm store by
38
+ > session:second:track and the priority playback ladder (stored pcm →
39
+ > hex inline → seed synth → pattern synth) with the AudioBufferSource
40
+ > scheduling and the 1.0s per-second grid.
41
+ > - `mjs/analyser.mjs + ffmpeg.mjs` — the node decode lane (ffmpeg f32le
42
+ > → stft → per_second vectors) with the known bpm bugs (no octave fold
43
+ > guard) our analyze already handles.
44
+ > - the FAKE parts the new code never repeats: the ~200 decorative
45
+ > *Id/*Hash fields per second (constants and xor cosmetics), the
46
+ > expandFromSeed checksum claim (the checksum never validates — synthesis
47
+ > cannot reproduce pcm), the hex semantic signatures (0xKICK_SUB_HEAVY),
48
+ > the empty prisma/db/mini-services scaffolds, the yamnet removal.
49
+ > - the honest separation limit the reference states: elements sharing the
50
+ > spectral range AND the pan position are mathematically inseparable
51
+ > without a trained model — the hpss pair (harmonic/percussive) is the
52
+ > proven floor, the vocal/bass/drum lanes ride the band filters on top.
53
+
54
+ ## wave PV0 — what already stands (verified, committed)
55
+
56
+ - [x] PV0.001 hpss real separation with centered framing, ifft conj law and
57
+ f64 ola — conservation 2.3e-7 over the whole signal (hpss.ts)
58
+ - [x] PV0.002 the ingest document debonair.ingest/1: mix + harmonic/percussive
59
+ stems, one vector frame per second (centroid, flatness, chroma, bands,
60
+ zcr, rms, beat, onsets) and the fnv-1a pcm hash (ingest.ts)
61
+ - [x] PV0.003 the ingest store: gateway https with the house host law and the
62
+ byte-capped offline mirror (ingeststore.ts); sqlite rows through
63
+ parameter binding only (ingestdb.ts)
64
+ - [x] PV0.004 the /ingest bench walking decode → separate → analyze →
65
+ segment → store with the phase meter (Sol/ingest/ingest.tsx)
66
+ - [x] PV0.005 the typed-URL law: devthink barra <app> lands inside the os on
67
+ all ten family apps (?arrive handoff, ?independent escape)
68
+ - [x] PV0.006 the 339-test battery green, the 402kb bundle building clean
69
+
70
+ ## wave PV1 — the reference system (the ids, the pointers, the server law)
71
+
72
+ - [ ] PV1.001 schema upgrade of ingest_documents: owner_id (BIGINT pointer),
73
+ kind ("reference" | "render" | "upload"), visibility ("private" |
74
+ "vitrine"), blob pointers for mix and stems, size, sha256 — parameter
75
+ binding only, mirroring prisma/schema.prisma
76
+ - [ ] PV1.002 the id mint: bigintids.ts — time-ordered 64-bit ids (the big
77
+ pointers the house law names), zero storage, deterministic tests
78
+ - [ ] PV1.003 the gateway routes of ingest: POST /api/v1/ingest/upload
79
+ (multipart, streams to the job store, returns the job id), GET
80
+ /api/v1/ingest/jobs/:id (progress: decode, separate, analyze, segment,
81
+ store), GET /api/v1/ingest (the shelf rows), GET /api/v1/ingest/:id,
82
+ DELETE /api/v1/ingest/:id
83
+ - [ ] PV1.004 the server worker ingestworker.ts (node, the gateway lane):
84
+ decode (audio-decode/wav) → hpss separate → analyze → segmentsof →
85
+ ingestPcm → wavwriter per stem + mix → blob store — the browser never
86
+ runs any of it
87
+ - [ ] PV1.005 the blob store contract blobstore.ts: put(stream) → pointer
88
+ {id, bytes, sha256, kind}, get(id) → stream — the vault law (the
89
+ storage app owns the engine; the gateway serves it), git-lfs friendly
90
+ - [ ] PV1.006 the job ledger table ingest_jobs (id BIGINT, owner_id, state,
91
+ progress, error, document_id, created_at, finished_at)
92
+ - [ ] PV1.007 the bench rewired: file → POST upload → poll the job → the
93
+ phase meter reads the server progress → the document renders — zero
94
+ dsp on the visitor's machine; the local opt-in path stays behind an
95
+ explicit "run on this device" key
96
+ - [ ] PV1.008 every document row carries the owner id (the user ingest or
97
+ the system ia ingest — both stamp the same pointer chain)
98
+ - [ ] PV1.009 the system ingest lane: the ia (or an mcp automation, opt-in)
99
+ posts the same upload route with the service identity — the reference
100
+ library grows without a human
101
+ - [ ] PV1.010 tests: bigintids monotonicity, blobstore round trip, worker
102
+ end-to-end on a synthetic wav, the routes contract with a fake fetch
103
+
104
+ ## wave PV2 — the playback proof (the ingested track plays)
105
+
106
+ - [ ] PV2.001 the mix and stem wavs ride the blob store; GET
107
+ /api/v1/blob/:id streams them with the right mime (audio/wav)
108
+ - [ ] PV2.002 the playerbus learns the reference take: the ingested mix
109
+ loads as a take the bar plays (the same bus the generated takes ride)
110
+ - [ ] PV2.003 the bench document view gains the play key: the stored mix
111
+ streams through the shared bar (the proof the owner demanded: ingest →
112
+ play, no re-render)
113
+ - [ ] PV2.004 stem audition: harmonic and percussive playable solo/muted
114
+ (the stems the worker stored), the conservation audible
115
+ - [ ] PV2.005 the library shelf rows carry the play key too (the ingested
116
+ references sit beside the generated takes, one grammar)
117
+ - [ ] PV2.006 tests: the wav round trip (store → stream → decode equals the
118
+ source samples), the bus contract for reference takes
119
+
120
+ ## wave PV3 — the pattern miner (the genre tool the references feed)
121
+
122
+ - [ ] PV3.001 patternminer.ts: reads the ingested documents of a genre and
123
+ answers the genre profile — bpm distribution, key pool, chord ladder
124
+ frequency, onset density curves, band balance, loudness targets
125
+ - [ ] PV3.002 the genre profile document debonair.profile/1 (deterministic,
126
+ parametrized, versioned) — stored in genre_profiles with the reference
127
+ ids that fed it (the pointer chain audit-ready)
128
+ - [ ] PV3.003 similarity: the vector distance over frames (cosine on the
129
+ normalized segment vectors) — nearest references answer a prompt
130
+ - [ ] PV3.004 the miner runs server side (a gateway job like the ingest
131
+ worker); the browser asks, never computes
132
+ - [ ] PV3.005 the vitrine surfaces the profiles (the genre cards the home
133
+ shows come from the db, never hardcoded)
134
+ - [ ] PV3.006 tests: the miner on synthetic documents of two genres
135
+ separates, the profile determinism, the similarity ranking
136
+
137
+ ## wave PV4 — the composer (prompt → similar json → real audio → play)
138
+
139
+ - [ ] PV4.001 the composer prompt → genre match (genrematrix) → the profile
140
+ of that genre → a seeded vector plan similar to the references
141
+ - [ ] PV4.002 the render: audioengine renders the plan offline (the existing
142
+ renderTakeOffline lane) — kick/snare/hats sample-accurate, bass, keys,
143
+ pads, the arrangement the plan carries
144
+ - [ ] PV4.003 the render job table render_jobs (exists) wired to the worker:
145
+ queued → rendering → done, the wav lands in the blob store, the track
146
+ row lands in tracks with the pointers
147
+ - [ ] PV4.004 the generated take plays in the same bar, publishes to the
148
+ vitrine (the spotify/netflix shelf the owner described) and downloads
149
+ - [ ] PV4.005 the vocal lane grows after the instrumental proof (the
150
+ SINTESE-VOCAL-AI line), opt-in, never blocking the wave
151
+ - [ ] PV4.006 tests: the same prompt+seed renders the same bytes, the render
152
+ job lifecycle, the vitrine rows
153
+
154
+ ## wave PV5 — the design reform (debonair wears the devthink index face)
155
+
156
+ - [ ] PV5.001 screenshot the live os home and every debonair page (the
157
+ browser session) — the gap list against the index face
158
+ - [ ] PV5.002 the shell chrome: the window, the pin rail and the player bar
159
+ re-tuned to the os grammar (the trylle surfaces, the fluent shadows,
160
+ the campaign motion) — no drift tokens
161
+ - [ ] PV5.003 the home: the prompt-first hero over the noir stage retuned,
162
+ the song grid rows in the streaming grammar (cover, title, genre line,
163
+ duration, play on hover), the shimmer and the stagger
164
+ - [ ] PV5.004 the ingest bench in the same face (the dropzone, the meter,
165
+ the cards, the shelf)
166
+ - [ ] PV5.005 the generate/library/studio/settings pages one pass each —
167
+ short copy, no dots, the same nav law, every key answering
168
+ - [ ] PV5.006 the 404 and the intro re-faced
169
+ - [ ] PV5.007 the live verification: every route of the app inside the power
170
+ clip and standalone (?independent), screenshots compared
171
+ - [ ] PV5.008 the same face check on light theme
172
+
173
+ ## wave PV6 — the family integration (the engines talk)
174
+
175
+ - [ ] PV6.001 the gateway app serves the debonair api lane (the routes of
176
+ PV1/PV3/PV4 ride the gateway the family shares)
177
+ - [ ] PV6.002 the vault stores the blobs (the blobstore pointer resolves
178
+ through the vault engine when configured, git-lfs backed)
179
+ - [ ] PV6.003 the saddle AVM modalities run the heavy jobs (the worker lane
180
+ declares the sandbox modality, opt-in per deploy) — the visitor's
181
+ machine stays clean even at scale
182
+ - [ ] PV6.004 the mcp automation lane (opt-in): an external agent ingests
183
+ and generates through the same gateway with its own identity
184
+ - [ ] PV6.005 the debonair package.json exports the engine library (the npm
185
+ face: analyze, hpss, ingest, patternminer, audioengine, wavwriter —
186
+ zero hardcode, everything configured through options/db)
187
+
188
+ ## wave PV7 — the verification ladder (the proof the owner reads)
189
+
190
+ - [ ] PV7.001 the full battery green (vitest) on every wave's modules
191
+ - [ ] PV7.002 the typecheck + biome + build clean on every push
192
+ - [ ] PV7.003 the live proof: ingest a real track through the ui, watch the
193
+ server do the work, press play, hear the mix, hear the stems
194
+ - [ ] PV7.004 the live proof: generate from a genre with references, hear
195
+ the take, publish to the vitrine
196
+ - [ ] PV7.005 the live proof: devthink barra debonair → the window inside
197
+ the os, the bench inside it, no flash, one boot
198
+ - [ ] PV7.006 the worklog and the ladder ticked, the wave committed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/debonair",
3
- "version": "2.0.118",
3
+ "version": "2.0.127",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/wenathlan/devthink"
@@ -75,6 +75,10 @@
75
75
  "wouter": "^3.13.0",
76
76
  "z-ai-web-dev-sdk": "^0.0.18"
77
77
  },
78
+ "overrides": {
79
+ "esbuild": "^0.28.2",
80
+ "uuid": "^11.1.1"
81
+ },
78
82
  "devDependencies": {
79
83
  "@biomejs/biome": "^2.5.15",
80
84
  "@capacitor/android": "^8.5.2",
@@ -0,0 +1,32 @@
1
+ // # bigintids.test — the pointer mint the storage law names: ids sort by
2
+ // creation (the high bits carry the clock), never collide within a tick
3
+ // (the tail increments), self-describe their mint time and ride the strong
4
+ // platform randomness. runnable with vitest: pnpm vitest run tests/bigintids.test.ts
5
+ import assert from "node:assert/strict";
6
+ import { describe, it } from "vitest";
7
+ import { idtick, mintid } from "../bigintids.ts";
8
+
9
+ describe("bigintids", () => {
10
+ it("mints ascending ids across ticks", () => {
11
+ const base = 1_700_000_000_000;
12
+ let previous = 0n;
13
+ for (let i = 0; i < 200; i++) {
14
+ const id = BigInt(mintid(base + i));
15
+ assert.ok(id > previous, `id ${id} must exceed ${previous}`);
16
+ previous = id;
17
+ }
18
+ });
19
+
20
+ it("never repeats inside one millisecond (the tail walks)", () => {
21
+ const base = 1_700_000_000_000;
22
+ const seen = new Set<string>();
23
+ for (let i = 0; i < 5000; i++) seen.add(mintid(base));
24
+ assert.equal(seen.size, 5000);
25
+ });
26
+
27
+ it("self-describes the mint tick", () => {
28
+ const at = 1_700_000_012_345;
29
+ const id = mintid(at);
30
+ assert.equal(idtick(id), at);
31
+ });
32
+ });
@@ -0,0 +1,127 @@
1
+ // # ingest.test — the decomposition pipeline contract: the hpss separation
2
+ // conserves the mix (harmonic + percussive == the original), the ingest
3
+ // document is deterministic for a given pcm and carries the vector
4
+ // segments the pattern miner reads (one frame per second, chroma, bands,
5
+ // onsets), the pcm hash is stable and the store guard refuses foreign
6
+ // schemas. runnable with vitest: pnpm vitest run tests/ingest.test.ts
7
+ import assert from "node:assert/strict";
8
+ import { describe, it } from "vitest";
9
+ import { separate } from "../hpss.ts";
10
+ import type { IngestDocument } from "../ingest.ts";
11
+ import { INGEST_SCHEMA_VERSION, ingestPcm, onsetSecondsOf, pcmhash, segmentsof } from "../ingest.ts";
12
+
13
+ /** A deterministic synthetic take: a 110 hz sawtooth bass (harmonic) over
14
+ * an 8 hz amplitude-gated click train (percussive), 3 seconds at 8 khz —
15
+ * small enough to analyze fast, structured enough to exercise both stems. */
16
+ function synthetictake(): { mono: Float32Array; sampleRate: number } {
17
+ const sampleRate = 8000;
18
+ const length = sampleRate * 3;
19
+ const mono = new Float32Array(length);
20
+ for (let i = 0; i < length; i++) {
21
+ const t = i / sampleRate;
22
+ const bass = 0.4 * Math.sign(Math.sin(2 * Math.PI * 110 * t));
23
+ const gate = t % 0.5 < 0.02 ? 0.8 : 0;
24
+ mono[i] = Math.max(-1, Math.min(1, bass * 0.7 + gate));
25
+ }
26
+ return { mono, sampleRate };
27
+ }
28
+
29
+ describe("hpss separation", () => {
30
+ it("conserves the mix: harmonic + percussive equals the original", () => {
31
+ const { mono, sampleRate } = synthetictake();
32
+ const { harmonic, percussive } = separate(mono);
33
+ assert.equal(harmonic.length, mono.length);
34
+ let worst = 0;
35
+ // the centered framing keeps every input sample near a window peak, so
36
+ // the whole signal conserves to the f32 storage floor
37
+ for (let i = 0; i < mono.length; i++) {
38
+ worst = Math.max(worst, Math.abs(harmonic[i] + percussive[i] - mono[i]));
39
+ }
40
+ assert.ok(worst < 1e-3, `reconstruction error ${worst}`);
41
+ void sampleRate;
42
+ });
43
+
44
+ it("pushes the click train into the percussive stem and the sawtooth into the harmonic one", () => {
45
+ const { mono } = synthetictake();
46
+ const { harmonic, percussive } = separate(mono);
47
+ const energy = (pcm: Float32Array): number => {
48
+ let sum = 0;
49
+ for (let i = 0; i < pcm.length; i++) sum += pcm[i] * pcm[i];
50
+ return sum;
51
+ };
52
+ assert.ok(energy(harmonic) > energy(percussive), "the tonal sawtooth carries most of the energy");
53
+ });
54
+ });
55
+
56
+ describe("ingest document", () => {
57
+ const { mono, sampleRate } = synthetictake();
58
+ const doc = ingestPcm(mono, sampleRate, { name: "synthetic take", at: "2026-01-01T00:00:00.000Z" });
59
+
60
+ it("writes the one schema with the mix, two stems and per-second frames", () => {
61
+ assert.equal(doc.schema, INGEST_SCHEMA_VERSION);
62
+ assert.equal(doc.name, "synthetic take");
63
+ assert.equal(doc.stems.length, 2);
64
+ assert.equal(doc.stems[0].role, "harmonic");
65
+ assert.equal(doc.stems[1].role, "percussive");
66
+ assert.equal(doc.segments.length, 3);
67
+ for (const stem of doc.stems) assert.equal(stem.segments.length, 3);
68
+ });
69
+
70
+ it("is deterministic: the same pcm ingests to the same bytes", () => {
71
+ const again = ingestPcm(mono, sampleRate, { name: "synthetic take", at: "2026-01-01T00:00:00.000Z" });
72
+ assert.equal(JSON.stringify(again), JSON.stringify(doc));
73
+ });
74
+
75
+ it("carries the vector fields every frame needs", () => {
76
+ for (const frame of doc.segments) {
77
+ assert.equal(frame.s >= 0, true);
78
+ assert.equal(frame.bands.length, 3);
79
+ assert.equal(frame.chroma.length, 12);
80
+ const total = frame.bands.reduce((sum, value) => sum + value, 0);
81
+ assert.ok(Math.abs(total - 1) < 0.01, `bands sum ${total}`);
82
+ assert.ok(frame.rms >= 0 && frame.peak >= 0 && frame.zcr >= 0);
83
+ assert.ok(["", ...["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"]].includes(frame.note));
84
+ }
85
+ });
86
+
87
+ it("hashes the pcm deterministically and differently per stem", () => {
88
+ assert.equal(pcmhash(mono), doc.pcmhash);
89
+ assert.notEqual(doc.stems[0].pcmhash, doc.stems[1].pcmhash);
90
+ const scratch = new Float32Array(mono);
91
+ scratch[10] += 0.5;
92
+ assert.notEqual(pcmhash(scratch), doc.pcmhash);
93
+ });
94
+
95
+ it("detects the gated clicks as onsets of the percussive stem", () => {
96
+ const onsets = onsetSecondsOf(mono, sampleRate);
97
+ assert.ok(onsets.length >= 4, `onsets ${onsets.length}`);
98
+ for (let i = 1; i < onsets.length; i++) assert.ok(onsets[i] > onsets[i - 1], "ascending");
99
+ });
100
+
101
+ it("segments count the onsets that fall inside each second", () => {
102
+ const onsets = onsetSecondsOf(mono, sampleRate);
103
+ const frames = segmentsof(mono, sampleRate, onsets);
104
+ let counted = 0;
105
+ for (const frame of frames) counted += frame.onsets;
106
+ assert.equal(counted, onsets.length);
107
+ });
108
+ });
109
+
110
+ describe("the store guard", () => {
111
+ it("accepts only schema-matching documents (the browser guard shape)", () => {
112
+ const { mono, sampleRate } = synthetictake();
113
+ const doc = ingestPcm(mono, sampleRate, { name: "guard take", at: "2026-01-01T00:00:00.000Z" });
114
+ const broken = { schema: "other/1" } as unknown as IngestDocument;
115
+ // the guard lives in the store module: the shape check mirrors it here
116
+ const guard = (candidate: IngestDocument): boolean =>
117
+ !!candidate &&
118
+ candidate.schema === INGEST_SCHEMA_VERSION &&
119
+ typeof candidate.name === "string" &&
120
+ candidate.name.length > 0 &&
121
+ Array.isArray(candidate.stems) &&
122
+ candidate.stems.length > 0 &&
123
+ Array.isArray(candidate.segments);
124
+ assert.equal(guard(broken), false);
125
+ assert.equal(guard(doc), true);
126
+ });
127
+ });
@@ -3,7 +3,7 @@
3
3
  // octave doubles. The recipes ride this one function; the tests pin it.
4
4
  import assert from "node:assert/strict";
5
5
  import { describe, it } from "vitest";
6
- import { A4_MIDI, CONCERT_PITCH, midiToHz } from "../Sol/studio/tuning.ts";
6
+ import { A4_MIDI, CONCERT_PITCH, midiToHz } from "../tuning.ts";
7
7
 
8
8
  describe("tuning — midiToHz", () => {
9
9
  it("anchors A4 at the concert pitch", () => {