Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bebacfc3a5 | ||
|
|
4692350802 | ||
|
|
25ba285c5f | ||
|
|
1239fceb85 | ||
|
|
71da0c9a81 | ||
|
|
7520b8e037 |
No files matched your search
@@ -0,0 +1,142 @@
|
|||||||
|
# Notes for V2 from the POC thread (21–22 Oct 2026)
|
||||||
|
|
||||||
|
What came out of a long working session on the POC that the V2 spec doesn't already say, or where the POC
|
||||||
|
experience suggests a different call. Read alongside the V2 handover spec and `HANDOFF.md` (its **Player rules**
|
||||||
|
section is the iOS lessons; they apply to any client). Nothing here is decided unless it says so.
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
- **Leaning native-native** (Swift/SwiftUI first) over Expo. The reasons that matter for gandora specifically:
|
||||||
|
- **The DJ can run on the phone.** iOS 26 Foundation Models does guided generation into a Swift struct; the
|
||||||
|
struct is the sheet (`title, tags, lyrics, bpm, key, energy, seconds, meter, line`). Free, offline, ~1 s.
|
||||||
|
Rendering still goes to Runware (ACE-Step doesn't fit on a phone).
|
||||||
|
- **Testability.** Today's regressions all came from "the only iOS test rig is a phone". XCTest + simulator
|
||||||
|
exercise real AVFoundation: background audio, interruptions, lock screen, route changes.
|
||||||
|
- `AVQueuePlayer` pre-enqueues the next track, so most of the POC's hold/crackle/`ended` machinery goes away.
|
||||||
|
- Covers (`hash32`, `prng`, rings/chord/attractor) port line-for-line; keep integer ops identical for parity.
|
||||||
|
- The web POC stays as the lab, the Android/desktop fallback and the share-link landing. Features are invented
|
||||||
|
there and promoted once they've earned it (the spec's parity tracker).
|
||||||
|
- Alternatives considered: Capacitor (fastest path, wraps the current page), Tauri (early on mobile; good for a
|
||||||
|
desktop app with local ACE-Step), Flutter, Expo (right if the UI is being rebuilt anyway).
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
- **Auth: Supabase Web3** (`signInWithWeb3`, Solana/Ethereum, EIP-4361). chai's call.
|
||||||
|
- Put the ToS acceptance in `statement` (Venice ToS §7.3 needs end users bound by terms).
|
||||||
|
- Wallets are free to mint, so **free tier is per device, not per account**: no login needed to listen;
|
||||||
|
login is for sync and paying. Enable per-IP rate limits + captcha.
|
||||||
|
- No recovery for wallet-only accounts: nudge `linkIdentity()` once sync is on.
|
||||||
|
- iOS PWA + wallets is clunky (deep links / in-wallet browser). Native solves it.
|
||||||
|
- No nostr provider; a custom-JWT edge function could add NIP-07 later.
|
||||||
|
- Minimal first slice: one Supabase project, tables `stations`, `tracks`, `events` with RLS on the wallet
|
||||||
|
identity, the existing Cloudflare worker verifying the Supabase JWT, blobs in R2.
|
||||||
|
|
||||||
|
## Rendering: ACE-Step and Runware
|
||||||
|
|
||||||
|
- ACE-Step 1.5 is **open weights, MIT** (code and weights). Model card: commercial use of outputs allowed;
|
||||||
|
training data claimed as licensed + royalty-free/public-domain + synthetic MIDI audio.
|
||||||
|
- **Runware already exposes repaint and extend** (schema: `runware.ai/docs/models/ace-step-v1-5-xl-turbo/schema.json`):
|
||||||
|
- `inputs.audio` (URL/UUID/data URI) + `settings.repaintingStart` / `repaintingEnd` (s) = repaint a section;
|
||||||
|
`repaintingEnd` past the end appends (extend), negative `repaintingStart` prepends (intro).
|
||||||
|
- `strength` 0–1 with `inputs.audio` = restyle; `numberResults` 1–4 = variations.
|
||||||
|
- No stems/extract/lego/cover task types on Runware; those need self-hosting.
|
||||||
|
- `duration` must be omitted when `inputs.audio` is set; `timeSignature` is beats (2/3/4/6), not "3/4".
|
||||||
|
- Product ideas from this: repaint around a reaction pill ("redo this bit"), ⟶ extend, restyle remixes.
|
||||||
|
- **Self-hosting is for customization, not cost** (Princess's priority). Runware turbo is $0.0045 per 2-min
|
||||||
|
track, about what a fully busy GPU costs. Self-hosting buys: LoRAs (per station, per DJ persona, from old
|
||||||
|
tracks), the extra task modes, the planner LM, XL without paying double.
|
||||||
|
- VRAM tiers (repo): 8–16 GB = 2B turbo + small LM; 20–24 GB = XL turbo without offload.
|
||||||
|
- LoRA training: ~1 h for 8 songs on a 12 GB GPU (repo figure).
|
||||||
|
- **Measured 22 Oct on hb-5 (M1, 8 GB):** fails to load. Even the smallest config (2B turbo, no LM) ran out of
|
||||||
|
GPU memory loading the VAE after the DiT (9.05 of 9.07 GiB allowed, 3 GB into swap). Realistic floor: a 16 GB
|
||||||
|
Mac for 2B turbo, 24 GB+ for comfort and near-XL, 36 GB+ for a Studio machine that also trains LoRAs; on GPUs
|
||||||
|
8–12 GB for 2B turbo, ~20–24 GB for XL. For LoRA/stems experiments, rent a 24 GB GPU for an hour first.
|
||||||
|
|
||||||
|
## Taste: embeddings direction
|
||||||
|
|
||||||
|
- One multimodal space (Gemini Embedding 2, 768-d): seed, sheet, uploaded audio, rendered track all as points.
|
||||||
|
Station = region (centroid of kept, shadow of avoided); taste = cosine to that direction; far more
|
||||||
|
data-efficient than the 54→12→1 net on a few dozen reactions.
|
||||||
|
- DJ gets **nearest-neighbour exemplars** (RAG for the DJ) instead of `tasteText()`.
|
||||||
|
- Costs: text embeddings ~free; audio ~$0.01/min (≈4× a turbo render), so audio-embed uploads, kept tracks
|
||||||
|
and ~30 s around reaction ticks, not every track. Stamp vectors with model/dims/chunking.
|
||||||
|
- Later, owning the model: **gisting** (compress persona/rules into a few tokens) and **xRAG** (feed retriever
|
||||||
|
embeddings straight in as tokens) let a local DJ be conditioned on the taste vector directly. Needs an
|
||||||
|
open-weights DJ (Gemma-class) + a trained projector; every kept track is a training pair.
|
||||||
|
|
||||||
|
## Product
|
||||||
|
|
||||||
|
- **Market**: no shipping product combines per-listener *generated* songs + a talking DJ + learned taste.
|
||||||
|
Spotify DJ / Yoodio / Trending Music have the voice on licensed catalog; Endel / Noema generate but have no
|
||||||
|
voice; Suno Live Radio etc. are shared broadcasts. The window is cost-driven and won't stay open.
|
||||||
|
- **Pricing sketch** (not decided): Free (≈20 new tracks/day per device, then kept/catalog; noise/tones/ASMR
|
||||||
|
unlimited; DJ-read sponsor breaks) · Listener ~$7/mo (unlimited new music, soft cap ~3 h/day generation,
|
||||||
|
no sponsor breaks, all voices, sync) · Studio ~$18/mo (XL, remix/repaint/extend, downloads, own-voice DJ).
|
||||||
|
Price in time and tracks, never credits. Track **cost per kept track**.
|
||||||
|
- **Uploads / analyzer**: upload old tracks → local DSP (bpm, key, energy, sections) + Whisper lyrics + DJ
|
||||||
|
tags → an imported library row → station seeded from it with the file as reference audio.
|
||||||
|
- **Feedback button**: 🐛 files a Gitea issue with build rev, player state, current sheet, last ledger lines and
|
||||||
|
console errors attached; dedupe by fingerprint; the factory triages `feedback` issues.
|
||||||
|
|
||||||
|
## Rights and moderation
|
||||||
|
|
||||||
|
- Runware: outputs owned by the user; model licences are the user's responsibility. Venice: user keeps rights
|
||||||
|
in outputs; API products need end-user terms at least as protective; no third-party voice/likeness without
|
||||||
|
consent. Kokoro: Apache-2.0.
|
||||||
|
- US Copyright Office: purely AI-generated music isn't copyrightable. Don't promise exclusivity.
|
||||||
|
- **EU AI Act Art. 50** (in force 2 Aug 2026): mark synthetic audio machine-readably (at least ID3 /
|
||||||
|
C2PA-style tag from the worker) and disclose. Applies to EU listeners regardless of where we are.
|
||||||
|
- To do before anyone outside the team listens: ToS + AI disclosure; output tagging; written confirmation from
|
||||||
|
Runware / ACE Studio on commercial streaming; voice-clone policy (own sample + consent only).
|
||||||
|
- **Moderation**: check the sheet (tags, lyrics, DJ line) in the worker before render/TTS; strip real artist and
|
||||||
|
people names from tags; check persona text on save; report button on shared content; per-station explicit
|
||||||
|
setting (hard rules stay on). Venice is permissive, so the guard has to be ours.
|
||||||
|
|
||||||
|
## DJ prompt optimisation: DSPy / GEPA (proposed, not started)
|
||||||
|
|
||||||
|
- Treat the DJ as a DSPy program: station context + taste + recent reactions -> the sheet (strict schema). The
|
||||||
|
instructions to optimise are the next-track prompt, the break prompt, `MODE_BRIEF` and persona framing.
|
||||||
|
- **GEPA** fits because it learns from *textual* feedback via reflection on traces, and gandora has lots of it
|
||||||
|
(typed reactions, timed emojis, keeps/skips). It keeps a Pareto frontier of instructions, which matches
|
||||||
|
per-station taste (an ASMR-best prompt and a freestyle-best prompt both survive).
|
||||||
|
- Dataset: exported station histories (sheets + the reactions that followed; `stats.html` EXPORT JSON has most of it).
|
||||||
|
- Metric, two layers: (1) cheap checks: schema valid, avoid-list respected, no real artist/person names, variety
|
||||||
|
vs recent tracks; (2) an LLM judge for "does this sheet follow from these reactions", whose reasoning is the
|
||||||
|
feedback text GEPA reflects on. Human keep rate stays the final arbiter (judges can be gamed).
|
||||||
|
- Run offline in Python against the same Venice models via the worker; promote winners as pinned Langfuse prompt
|
||||||
|
versions (V2) or port back into the DJ prompt code (POC). Hold out stations; A/B on keep rate before switching.
|
||||||
|
|
||||||
|
## Renderer economics: Runware vs self-hosted (RunPod), 22 Oct
|
||||||
|
|
||||||
|
- Per 2-min turbo track: Runware $0.0045; RunPod serverless roughly the same when warm, worse with cold starts
|
||||||
|
(~30–60 s loading ~9 GB of weights); an always-on 4090 pod (~$0.34/hr community, ~$245/mo) only beats Runware
|
||||||
|
above ~75 tracks/hr sustained. XL roughly doubles both sides.
|
||||||
|
- Functionality is the real difference: Runware has text2music, reference audio, repaint/extend, strength,
|
||||||
|
variations; self-hosting adds LoRAs, stems/extract/lego/complete, full planner-LM and sampler control.
|
||||||
|
- Plan: Runware stays default. RunPod pod by the hour for LoRA training; a RunPod/Modal serverless endpoint only for
|
||||||
|
renders that need a LoRA or stems, selected by the worker from the sheet (e.g. `lora: "..."`).
|
||||||
|
|
||||||
|
## Taste dynamics: measurement ideas (research, not product yet)
|
||||||
|
|
||||||
|
- **Forward vs time-reversed training:** retrain the brain on a station's reactions in reverse order and compare
|
||||||
|
predictions; the gap = path dependence (how much is recency vs a stable summary).
|
||||||
|
- **Signed area of the taste path** (Lévy area of c(t), the station centroid over time): antisymmetric under time
|
||||||
|
reversal, so it separates a consistent drift/turn (non-zero) from wandering (cancels) and collapse to a default
|
||||||
|
(little area). Cheap enough to show as a curl indicator on the TASTE view.
|
||||||
|
- **Telephone loop:** sheet -> render -> (future) audio embedding -> DJ reads it back -> next sheet. Station drift
|
||||||
|
splits into noise (diffusive), attractor (collapse to generic, a bug) and holonomy (direction-dependent turn,
|
||||||
|
maybe a feature). Reversing loop order flips holonomy but not the other two.
|
||||||
|
- Context: a math thread on holonomy / Berry phase across embedding-space charts (xRAG projector as a transition
|
||||||
|
map; loop products of Procrustes maps between embedding models as discrete holonomy). Explainer video:
|
||||||
|
`teahubxyz/media` -> `manim/transition-map-narrated.mp4`.
|
||||||
|
|
||||||
|
## POC housekeeping that affects V2
|
||||||
|
|
||||||
|
- **The module split is done on the `split` branch** (ES modules, no build step: `app.js core.js covers.js dj.js
|
||||||
|
render.js store.js player.js mashup.js ui/*.js`; `index.html` is markup + CSS). It is current with `main` apart
|
||||||
|
from these docs, every module parses, and the Safari harness gives the same results as `main`. Not yet merged:
|
||||||
|
it needs the dev → phone → main pass. It produces the pure pieces a Swift port reads from (covers math, sheet
|
||||||
|
contract, brain, DJ prompts). `docs/v2/REFACTOR-PROMPT.md` was written before we saw the branch and is
|
||||||
|
superseded by it.
|
||||||
|
- `tools/harness.sh safari` runs the loop in WebKit via safaridriver on hb-5 (needs `safaridriver --enable`).
|
||||||
|
- Player changes go `dev` → phone → `main` (HANDOFF.md).
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Split gandora's index.html into ES modules — brief for a fresh agent
|
||||||
|
|
||||||
|
> **Superseded.** The split already exists on the `split` branch (see `docs/v2/NOTES-FROM-POC.md`). Kept for its
|
||||||
|
> rules and checklist, which still describe how to review that branch before it merges.
|
||||||
|
|
||||||
|
You are refactoring **gandora/site** on teahub.xyz (Gitea; use the Teahub MCP tools: `tea_file_get`, `tea_file_edit`, `tea_file_change`, `tea_commit`, `tea_branch_create`, `tea_pull_create`; there is no git access from the container). The Mac **hb-5** is reachable through `boring_mac_exec` (120 s per call; long jobs with `nohup … &` then poll); a clone lives at `~/scratch/gsite` (`git pull -q` to refresh). Node is at `/opt/homebrew/bin/node`.
|
||||||
|
|
||||||
|
Work on the **`dev` branch only.** Do not push to `main`. Open one PR at the end.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
`index.html` is a ~2,100-line single file holding the player, the DJ, rendering, covers, storage, the UI sheets and the usage ledger in one scope. Split it into ES modules **with no build step**: plain `<script type="module">`, static files served as-is by Pages, view-source still works, the service worker still caches a fixed list. Behaviour must be unchanged. The point is blast radius: after this, the player is the only file that needs the dev-branch-and-iPhone treatment.
|
||||||
|
|
||||||
|
## Read first, in this order
|
||||||
|
|
||||||
|
1. `HANDOFF.md` — especially **"Player rules"** (iOS invariants learned the hard way) and "What a refactor should keep".
|
||||||
|
2. `ARCHITECTURE.md` — the state machine and playback invariants.
|
||||||
|
3. `STORAGE.md` — the `db` adapter contract (chai's adapter targets it; keep the shape).
|
||||||
|
4. `tools/harness.sh`, `tools/harness.html`, `tools/harness-resume.html`, `tools/harness-cdp.py`, `tools/harness-safari.py` — these are your tests.
|
||||||
|
5. `index.html`, `brain.js`, `sw.js`, `stats.html`, `webamp.html` (the last two read IndexedDB and localStorage directly; don't change their contracts).
|
||||||
|
|
||||||
|
## Target layout
|
||||||
|
|
||||||
|
```
|
||||||
|
index.html markup + CSS only, plus <script type="module" src="app.js"> and <script src="brain.js">
|
||||||
|
app.js boot: wires modules, sets window.g, hooks for brain.js, URL/share handling, SW + update pill, theme
|
||||||
|
player.js the state machine and the ONE <audio>: state, setSource, toCooking, toHolding, queueBehindHold,
|
||||||
|
handOver, startNow, playNow, toPlaying, toEnded, toIdle, playJob, advance, prime, bed/crackle/bedStart,
|
||||||
|
makeCrackle, SILENCE, Media Session, vizBeat/analyser
|
||||||
|
dj.js djPrompt, djText, askDJ, askBreak, askMash, DJ_SCHEMA, BREAKS/BREAK_BRIEF, MODES/MODE_BRIEF,
|
||||||
|
PERSONAS, VOICES/voiceParts/voiceLabel, speak, remixText, stationFrom helpers that only build specs
|
||||||
|
render.js venice() (as an injectable fetcher), renderTrack, hedge/queue/poll path, makeNoise, noiseSpec,
|
||||||
|
makeTone, toneSpec, wavBlob, wavBlob2, cost bookkeeping
|
||||||
|
covers.js pure: hash32, prng, keyHue/palettes, drawCover, drawRings, drawChord, attractor, drawMark,
|
||||||
|
drawOnAir, tape covers. No DOM lookups, no state — take a canvas and a spec.
|
||||||
|
store.js db (open/get/put/all/del), stations (saveStation, listStations data), tapes incl. the KEPT virtual
|
||||||
|
tape, tapeSpec, logEvent (the ledger), LS keys
|
||||||
|
ui/sheet.js stationSheet (name, look, palette, mode, dj details, sliders, actions)
|
||||||
|
ui/library.js library view, in-place play, now-playing bar, 🦙 link
|
||||||
|
ui/player.js cover/title/tags/lyrics rendering, keep/share buttons, scrubbing, drawTicks, markReaction, emoji
|
||||||
|
tray + picker, react(), toasts, tap-to-play veil
|
||||||
|
mashup.js mashup() (OfflineAudioContext stitch) and askMash wiring
|
||||||
|
brain.js unchanged (it is a classic script; keep its globals and the hooks contract)
|
||||||
|
```
|
||||||
|
|
||||||
|
Names are suggestions; the boundaries are not. Hard boundaries:
|
||||||
|
|
||||||
|
- **Only `player.js` touches `state.audio`** (or any `Audio` element that carries the station). Everything else calls its exports or listens to its events.
|
||||||
|
- `covers.js` and the generators in `render.js` are **pure** (no `document`, no `state`), so they can be checked in node.
|
||||||
|
- `brain.js` keeps working as-is: it reads `hooks`, `state`, `cssv()`, `db`; expose exactly what it needs on `window` from `app.js` (see below).
|
||||||
|
- The `venice` fetcher must be **injectable** (e.g. `render.setFetcher(fn)`), because the harness fragments replace it. Reassigning a module binding from outside won't work.
|
||||||
|
|
||||||
|
## Globals for the harness and brain.js
|
||||||
|
|
||||||
|
The harness fragments and `brain.js` reach into `state`, `crackle`, `openStation`, `cookNext`, `venice`, `db`, `hooks`, `wavBlob`, `$`. In `app.js`, after wiring, set
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.g = { state, player, dj, render, store, covers, openStation, cookNext, crackle, hooks, db, wavBlob, $ };
|
||||||
|
```
|
||||||
|
|
||||||
|
and additionally put on `window` the exact names `brain.js` uses today (grep it). Then update the three harness fragments to read through `window.g` (minimal edits; same assertions). `tools/harness-cdp.py` and `tools/harness-safari.py` already serve over http; modules need http, so the `file://` loop mode of `harness.sh` is retired: make `sh tools/harness.sh` run the loop fragment through the CDP path (it already does for `resume`).
|
||||||
|
|
||||||
|
## Service worker
|
||||||
|
|
||||||
|
`sw.js` precaches a fixed `FILES` list and stamps `REV` from the commit. Add every new module to `FILES`. Make sure a half-updated cache can't mix versions: the update pill / RELOAD path must refresh all of them together (one cache name keyed by REV is enough; check it already is).
|
||||||
|
|
||||||
|
## CSP
|
||||||
|
|
||||||
|
`index.html` carries a strict CSP (`script-src 'self' 'unsafe-inline'`). Module scripts from `'self'` are fine; don't add inline module code and don't add any CDN.
|
||||||
|
|
||||||
|
## Method — in order of distance from the audio element
|
||||||
|
|
||||||
|
Do it as a sequence of small commits on `dev`, each leaving the page working and the harnesses green:
|
||||||
|
|
||||||
|
1. `covers.js` + the generators (pure) → node check: `node -e` import them and draw/generate once.
|
||||||
|
2. `store.js` (db, tapes, ledger). `stats.html` and `webamp.html` must still read the same IndexedDB/localStorage shapes.
|
||||||
|
3. `dj.js` and `render.js` with the injectable fetcher.
|
||||||
|
4. `ui/*`.
|
||||||
|
5. `player.js` **last and alone**, in its own commit, touching nothing else. Re-read HANDOFF "Player rules" before this step. Do not "improve" the state machine while moving it.
|
||||||
|
6. `app.js` wiring, `window.g`, harness fragment edits, `sw.js` list, `HANDOFF.md`/`ARCHITECTURE.md` updated to the new layout (short; keep the invariants section intact and add "file map").
|
||||||
|
|
||||||
|
After each step run, on hb-5 in `~/scratch/gsite` (after `git pull -q`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/opt/homebrew/bin/node --check app.js player.js dj.js render.js covers.js store.js ui/*.js # syntax
|
||||||
|
sh tools/harness.sh # loop, Chrome over CDP
|
||||||
|
sh tools/harness.sh resume # resume with autoplay blocked, expects TAP TO PLAY
|
||||||
|
sh tools/harness.sh safari # WebKit (iPhone's engine) with a real activation
|
||||||
|
```
|
||||||
|
|
||||||
|
All three must print the same expectations as before the split (compare against a run on `main` first and save that output). A diff in any `expect …` line is a regression; fix or revert the step.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- No behaviour changes. No TypeScript, no bundler, no framework, no new dependencies, no reformatting of lines you aren't moving (keep diffs reviewable).
|
||||||
|
- Preserve every comment that explains an invariant; move it with the code.
|
||||||
|
- Keep `index.html` the entry URL; keep the share link formats (`/#seed`, `/?t=`), the IndexedDB name/stores/record shapes, and all `localStorage` keys exactly.
|
||||||
|
- Don't touch `brain.js`, `webamp.html`, `stats.html`, `tools/icons.py`, `.gitea/workflows/shots.yml`, `manifest.json`, icons.
|
||||||
|
- If something can't be split without a behaviour change, leave it in place and say so in the PR rather than changing behaviour.
|
||||||
|
- Commit messages: one line saying what moved, with the attribution lines in use on this repo.
|
||||||
|
|
||||||
|
## Deliver
|
||||||
|
|
||||||
|
A PR `dev → main` with: the file map; the harness outputs before and after (all three modes); a list of every `window` global exposed and who uses it; anything you left unsplit and why; and the exact `sw.js` FILES list. Stop there — a human taps through dev.gandora.lol on an iPhone before merge.
|
||||||
Reference in new issue
Block a user