diff --git a/CLAUDE.md b/CLAUDE.md index 0b79d5e..b3bf848 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Godstrument — Claude context -Music-instrument web app; hosted at godstrument.pro on the **botchat** VPS (`humanjing@100.71.119.27`, exposed via Cloudflare Tunnel — NOT dealgod, despite earlier notes). App dir `/home/humanjing/godstrument` (not a git checkout). Deploy = rsync git-tracked files only, **minus dev docs/scripts** (`--files-from=<(git ls-files | grep -vE '^(CLAUDE\.md|README\.md|ZERO_OMNI_BRIEF\.md|GODSTRUMENT_MANUAL_SOURCE\.md|godstrument\.txt|build_manual\.py|test_.*\.py)$')`, never `--delete`, never push `godstrument_users.db`/`auth_secret`/`patches/`) then `sudo -n systemctl restart godstrument`. The **user manual** ships as `viz/manual.html` (served at godstrument.pro/manual.html), generated from the in-app grimoire by `python3 build_manual.py` — re-run it after editing the grimoire. Working notes/planning docs stay OUT of the deploy. Origin is Gitea `ssh://git@100.71.119.27:222/monster/Godstrument.git`. See [[godstrument-pro-deploy]]. +Music-instrument web app; hosted at godstrument.pro on the **botchat** VPS (`humanjing@100.71.119.27`, exposed via Cloudflare Tunnel — NOT dealgod, despite earlier notes). App dir `/home/humanjing/godstrument` (not a git checkout). Deploy = rsync git-tracked files only, **minus dev docs/scripts** (`--files-from=<(git ls-files | grep -vE '^(CLAUDE\.md|README\.md|ZERO_OMNI_BRIEF\.md|LIFESTRUMENT_BRIEF\.md|GODSTRUMENT_MANUAL_SOURCE\.md|godstrument\.txt|build_manual\.py|test_.*\.py)$')`, never `--delete`, never push `godstrument_users.db`/`auth_secret`/`patches/`) then `sudo -n systemctl restart godstrument`. The **user manual** ships as `viz/manual.html` (served at godstrument.pro/manual.html), generated from the in-app grimoire by `python3 build_manual.py` — re-run it after editing the grimoire. Working notes/planning docs stay OUT of the deploy. Origin is Gitea `ssh://git@100.71.119.27:222/monster/Godstrument.git`. See [[godstrument-pro-deploy]]. - **Canon gotcha:** the real spec prose lives in `viz/index.html` (grimoire text around line ~950); `GODSTRUMENT_MANUAL_SOURCE.md` claims to be exhaustive but has drifted before (Earth Echo / Schumann layer was missing). When they disagree, **viz/index.html is truth** — update the manual to match, not the other way. - Secrets (Cloudflare token etc.): read from `.env`/local files, never from chat. A CF token was pasted in chat once and rolled — don't repeat that. diff --git a/LIFESTRUMENT_BRIEF.md b/LIFESTRUMENT_BRIEF.md new file mode 100644 index 0000000..ee38dbf --- /dev/null +++ b/LIFESTRUMENT_BRIEF.md @@ -0,0 +1,248 @@ +# LIFESTRUMENT — build brief + +> Coding instructions for a fresh Claude (Opus 4.8) session working in this repo. +> Read this whole file before writing code. Where this brief and the live code +> disagree, the code wins — verify against `viz/index.html`, `hub.py`, +> `config.json` before assuming. + +## 1. What you are building + +**LIFESTRUMENT** is a second, self-contained client for the existing godstrument +hub: one page, `viz/life.html`, served by the same server at +`godstrument.pro/life.html`. Where godstrument is the expert console — 80+ +sources, a patchable modulation matrix, fourteen belief-system skins, MIDI/OSC/CV +out — Lifestrument is the **fun one**: only the sources about *living* (bodies, +births, weather, the sun and moon, play), a handful of one-tap **Vibes** instead +of a patch matrix, two big knobs, immediate sound, and little dopamine **Moments** +(a chime when somewhere a baby is born; a golden flash at sunset; a whistle at +kickoff). A visitor should hear something beautiful within **five seconds of +tapping one button**, and never see a cable. + +Philosophy in one line: godstrument (and its ZERO mode) is *build anything*; +Lifestrument is *feel something, immediately*. + +**What it is NOT**: no money (no crypto/fx/market/econ/debt — banned outright), +no transport machinery (planes, ISS), no quakes/deaths in the sound (life, not +mortality), no matrix editing, no skins, no arranger/grooves, no commune, no +MIDI/OSC/CV out, no grimoire. It links back to the full instrument for people +who outgrow it. + +## 2. Non-negotiables + +- **One file**: `viz/life.html`, self-contained (inline CSS/JS, no frameworks, + no CDN, no build step) — same house style as `viz/index.html`. Target ≤ 3,000 + lines. +- **Zero changes to hub.py / run.py / config.json / workers.** Everything is + client-side. If you believe you need a server change, stop and leave a note + instead. +- **Do not modify `viz/index.html`** except (optionally) one additive line: a + "🌱 lifestrument" item in the right-click sky menu that opens `/life.html`. + Nothing else. +- **Mobile-first.** Design for a phone held in one hand (portrait, ~390px); + desktop is the adaptation, not the base. Godstrument is desktop-first; + Lifestrument is the opposite. +- Audio starts only after a user gesture (browser rule): one big button. +- This brief and all planning docs stay **out of the deploy** (see §12). +- Never `rsync --delete` to the box; never push `godstrument_users.db`, + `auth_secret`, or `patches/`. + +## 3. Architecture (all decided — don't relitigate) + +**Data in.** Connect to the same hub websocket the main viz uses. Copy the URL +derivation from `viz/index.html` `connect()` (~line 2240): localhost pages use +`ws://localhost:8765`, hosted pages use `wss:///ws`, and a `?ws=` +query-param override wins. Consume only `msg.sources` frames: each key carries +`{raw, norm, label, event}`. Ignore `dests`, `routes`, `macros` — Lifestrument +has no use for the shared matrix. Keep a small per-key history ring (~120 +values) like index.html does for its sparklines; the orb and meters draw from it. + +**Routing.** A local mini-matrix, entirely client-side: each Vibe is a static +list of `{source, param, amount, curve}` wirings applied at audio-frame rate +(`requestAnimationFrame` is fine; the synth reads a params object). The hub's +shared routes are never touched — this also means Lifestrument works against the +**read-only public hub** exactly as well as a local one. + +**Sound.** Its own compact Web Audio synth (§6). Do not try to reuse the +index.html synth — it is closure-scoped and expert-shaped. ~300 lines is enough. + +**Auth.** Same invite-only accounts. Reuse the exact `/api/*` endpoints +index.html calls (read its auth IIFE, ~line 6400: me/login/signup/logout with a +session cookie) — do not invent new endpoints. Landing = one screen with the +LIFESTRUMENT wordmark, a one-line poem, login/signup (reuse the flow, restyle to +§9), and after auth one giant **begin** button that starts audio and drops you +into the default Vibe already playing. + +**Local sources** (client-generated, never sent to the hub; they exist only in +the local matrix, prefixed `me.*`): +- `me.heart` — Web Bluetooth heart rate. Lift the pattern from index.html's + ♥ implementation (`0x180D` HR service). Chrome/Edge/Android only — feature- + detect and show "not on this browser" honestly on iOS Safari. +- `me.tilt.x` / `me.tilt.y` — DeviceOrientation beta/gamma normalized 0..1. + On iOS call `DeviceOrientationEvent.requestPermission()` from the begin + button's gesture. This is the universal free sensor: every phone becomes a + controller with zero hardware. +- `me.shake` — DeviceMotion impulse (accel magnitude over threshold, 400ms + cooldown), an event source. +- `me.voice` — mic RMS via getUserMedia + AnalyserNode, opt-in per Vibe. +- `me.tap` — taps on the orb: sets tempo (rolling average of last 4 intervals) + and fires a pluck. Fallback "heartbeat" for iOS. + +**Hub sources whitelist** (the ONLY hub keys Lifestrument may use — exact keys, +all verified present in `config.json` signals): +`clock.births`, `clock.pop`, `clock.popvel`, `world.lifeexp`, `world.fertility`, +`weather.temp`, `weather.wind`, `weather.precip`, `weather.humidity`, +`sky.day`, `sky.elev`, `season.north`, `season.south`, +`astro.moon`, `astro.harmony`, `almanac.fertile`, `almanac.waxing`, +`light.lux`, `light.flash.event`, `audio.rms`, `audio.centroid`, +`tof.near`, `tof.cx`, `tof.cy`, `hand.open`, +`sport.events_today`, `sport.inplay`, `sport.kickoff`, `wiki.rate`. +(`tof.*`/`light.*` flow when the sensor rig — or `workers/sim_esp32.py` — runs; +`sport.*` when the hub runs `--sports`. Absent keys must degrade gracefully: +a card shows a gentle "asleep" state, never an error.) + +## 4. The Vibes (presets) — build exactly these eight + +Each Vibe is: an emoji + name + one-line promise on a card; a wiring list; a +palette tint for the orb; and one or two "moment" hooks (§7). Switching Vibes +crossfades over ~2s (ramp old wirings' amounts to 0, new ones in). Store the +last Vibe + knob positions in `localStorage` (`ls_vibe`, `ls_energy`, +`ls_dream`). + +| # | Vibe | Promise | Core wiring (source → param · amount · curve) | +|---|------|---------|-----------------------------------------------| +| 1 | 💓 **Heartbeat** (default) | *your pulse is the tempo* | `me.heart` raw BPM → tempo (clamped 50–140); `me.heart` norm → brightness ·0.6·lin; no heart? `me.tap` sets tempo; kick ticks every beat, hat every half | +| 2 | 🌅 **Daylight** | *the sun plays the room* | `sky.elev` → brightness ·1.0·scurve; `sky.day` → pad warmth ·0.8; inverse `sky.day` → space ·0.6 (night opens reverb); `weather.temp` → root note (see §5 key map) | +| 3 | 🌧 **Rain Room** | *the weather writes the melody* | `weather.precip` → pluck density ·1.0·exp (rain literally plays notes); `weather.wind` → pan-wobble rate ·0.7; `weather.humidity` → wash level ·0.5; `weather.temp` → root note | +| 4 | 👶 **The Living** | *somewhere, a baby* | `clock.births` → chime rate (§7 Poisson, throttled); `world.lifeexp` norm → pad brightness ·0.7; `world.fertility` → warmth ·0.5; `clock.popvel` → drone level ·0.4 | +| 5 | 🌕 **Moon Bath** | *slow silver* | `astro.moon` → shimmer level ·0.9 (full = maximal); `almanac.fertile` → pad bloom ·0.5; `astro.harmony` → consonance (major-ness) ·0.6; tempo fixed 60, everything legato | +| 6 | 🏟 **Game Day** | *the world at play* | `sport.inplay` → percussion density ·0.9; `sport.events_today` → base energy ·0.4; `sport.kickoff` → riser+crash moment; hub without `--sports` → card asleep: "the stadium sleeps — ask the keeper" | +| 7 | 🎤 **The Room** | *your space sings back* | `me.voice` → swell ·1.0·scurve; `audio.centroid` (or local mic centroid) → filter ·0.5; `light.lux` → brightness ·0.5; `light.flash.event` → sparkle hit; `tof.near` → pluck when hand approaches | +| 8 | 🤲 **In Hand** | *tilt, and it bends* | `me.tilt.x` → filter sweep ·1.0; `me.tilt.y` → space ·0.8; `me.shake` → big hit + orb burst; `hand.open`/`tof.cx/cy` join identically when the rig is present | + +Curves are godstrument's: `lin`, `scurve` (ease in/out), `exp` (hugs the floor). + +## 5. The two knobs (the only "mixer") + +- **ENERGY** — one macro scaling: tempo ±20%, percussion density, pluck + probability, brightness ceiling. Low = ambient wash, high = it dances. +- **DREAM** — space macro: reverb size/mix, delay feedback, pad release, detune. + Low = dry and close, high = cathedral. + +Both are big thumb-friendly dials (or vertical sliders on desktop), default 0.5, +persisted. Nothing else is user-adjustable in v1. + +**Key map** (for `weather.temp` → root): map norm 0..1 across +`[D minor pent, F major pent, G mixolydian(pent subset), A major pent]` roots +38/41/43/45 — cold is minor and low, warm is major and higher. All melodic +content in Lifestrument is pentatonic — the no-wrong-notes rule is what makes it +un-mess-up-able. + +## 6. The synth (compact, ~300 lines) + +Voices (all Web Audio primitives, no worklets): +- **kick** — sine drop 120→45Hz, 90ms, on the beat when ENERGY > 0.2 +- **hat** — filtered noise burst, offbeats, level ~ percussion density +- **pluck** — 4-voice poly: triangle osc → lowpass → exp-decay gain; pentatonic + notes chosen by the active Vibe's melody driver +- **pad** — 2 detuned saws → lowpass (brightness) → slow attack/release; chord = + root + 5th + 9th; "warmth" morphs triangle↔saw mix +- **drone** — one sine + one sine at the 5th, sub register, level per Vibe +- **shimmer** — high sine cluster with slow random pan, for Moon Bath +- **chime** — the Moment sound: 2-osc bell (sine + 3.01× partial), 2.5s decay +- master: delay (0.375s, feedback = DREAM·0.5) + reverb (generated impulse: + 2s noise burst with exp decay into a ConvolverNode) + DynamicsCompressor + as a safety limiter. + +The params object (what wirings write into, smoothed with setTargetAtTime): +`tempo, brightness, warmth, space, wash, density, pluckDensity, droneLevel, +shimmerLevel, consonance, panWobble, swell, root`. + +## 7. Moments — the ding-ding engine + +A Moment = toast (emoji + one line, ~4s), a chime (pitch varies), and an orb +burst (particles in the Vibe's tint). **Cooldowns are what keep them precious** +— enforce per-moment minimum gaps: + +| Moment | Trigger | Cooldown | +|--------|---------|----------| +| 👶 *somewhere, a baby* | Poisson draw against `clock.births` raw (≈4/s worldwide — draw every frame, `p = rate·dt·THROTTLE` with THROTTLE tuned so it fires ~every 15–30s in The Living, ~2min elsewhere) | 12s | +| 🌅 / 🌇 *the sun rises/sets where you are* | `sky.elev` raw sign crossing (needs two frames straddling 0) | 6h | +| 🌕 *the moon is full tonight* | `astro.moon` norm > 0.97, once per session | session | +| ⚽ *kickoff — N matches begin* | `sport.kickoff` event arrives | 30s | +| 🌧 *rain has started* | `weather.precip` raw 0 → >0 | 1h | +| 💓 *new calm / new fire* | `me.heart` sets a session min/max (after 3min warmup) | 5min | +| 🎂 *your day* (v2, with natal) | — | — | + +First-visit onboarding is also a Moment sequence: three toasts introducing the +orb, the Vibes, the knobs. Never a modal tutorial. + +## 8. The orb (the whole visual) + +One canvas, one big central orb (~55% viewport width on phone): +- radius breathes with the master output RMS; a ring pulses on each kick +- hue = active Vibe's tint; inner gradient brightness = `brightness` param +- slow particle drift around it; Moment bursts emit 20–40 particles +- background: a vertical gradient that tracks `sky.elev` when available (night + → deep navy, day → pale warm) — the page itself knows what time it is +- tapping the orb = `me.tap` (pluck + tempo in Heartbeat) +- under the orb: current Vibe name + the *live fact* it runs on, in plain words, + updated ~10s: "144 hearts born this minute" / "the sun is 12° up" / + "3 matches in play". Real numbers, human phrasing — this is Lifestrument's + version of godstrument's data-honesty. + +## 9. Look & feel + +Dawn, not deep space: near-white warm background in day, dusky in night mode +(driven by `sky.elev`, not a toggle), one accent hue per Vibe, big rounded type +(system stack is fine; 17px minimum body), generous spacing, cards with soft +shadows, everything reachable by thumb. No tables, no panels, no right-click +anything. The wordmark is lowercase: **lifestrument** · "played by being alive". +Keep godstrument's honesty of voice ("the ECB fixes once a day" energy) but +warmer and shorter. + +## 10. Degradation matrix (build these states, they are most of the UX) + +- No Bluetooth API (iOS): Heartbeat card shows "🫱 tap your pulse" mode. +- Hub unreachable: local-only mode — me.* sources still play (tilt/tap/voice); + banner "the world is quiet — playing your half". +- Not signed in: landing; signup needs an invite code exactly like godstrument. +- Sensor rig absent: In Hand uses tilt only; The Room uses mic only. +- `--sports` off: Game Day asleep state. +- Reduced motion (`prefers-reduced-motion`): particles off, orb breathes gently. + +## 11. Acceptance checks (run all before calling it done) + +1. `python3 -m http.server 8899 --directory viz` + open + `http://localhost:8899/life.html?ws=ws://localhost:8790` against a running + hub (`godstrument-test` launch config) → sources flow, Daylight plays, zero + console errors. +2. Kill the hub → local-only banner, tilt/tap still make sound, no errors. +3. Chrome DevTools device mode iPhone 13: layout holds at 390×844; begin button + ≥ 56px tall; knobs usable with a thumb. +4. Each of the 8 Vibes: switch to it, confirm audibly distinct within 3s, no + errors, crossfade smooth. +5. Moments: fake `sky.elev` crossing and a `sport.kickoff` frame in the console + (expose a tiny `window.__lifeDebug.inject(key, val)` hook, dev-only) → toast + + chime + burst, cooldowns respected. +6. Reload → same Vibe and knob positions return. +7. Lighthouse mobile performance ≥ 90 (it's one file with no deps — this is + easy; if it isn't, the canvas loop is misbehaving). +8. Leave one runnable self-check in-repo: `test_lifestrument.py` (stdlib only) + asserting `life.html` parses as balanced HTML, whitelist keys ⊂ + `config.json` signals ∪ `me.*`, and banned prefixes (`crypto.`, `fx.`, + `market.`, `econ.`, `debt.`, `quake.`, `planes.`, `iss.`) appear nowhere. + +## 12. Deploy + +`viz/life.html` deploys with the normal flow (it must ship — it's product). +This brief does NOT: it is in the deploy exclusion list in `CLAUDE.md`. Deploy = +rsync git-tracked files minus that exclusion list (never `--delete`), then +`sudo -n systemctl restart godstrument` on the box, then verify +`https://godstrument.pro/life.html` returns 200 and plays. + +## 13. Explicitly deferred to v2 (do not build now) + +Natal/birthday integration (needs the chart caster), a shareable "postcard" +image of your session, preset remixing/saving, Lifestrument-specific accounts or +theming per user, Apple Watch workarounds, camera-based pulse detection, any +server-side anything.