feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite): - Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits - Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive), Ouija planchette board spelling utterances, transcript with TTS replay, entity dossier, direct contact streaming, passive/active listening - Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact - Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete - State: VeilSocket (reconnect/backoff), seance reducer, auth context - 72 vitest tests green; served by FastAPI at :7777 Docs: README + as-built plans 3-7
This commit is contained in:
131
docs/superpowers/plans/2026-07-20-quantumancy-05-spirit-radio.md
Normal file
131
docs/superpowers/plans/2026-07-20-quantumancy-05-spirit-radio.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Quantumancy Plan 5/7: Spirit Radio — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** A spirit box built from a real radio (spec §3.1): the browser claims a user-supplied RTL-SDR dongle over WebUSB, sweeps the FM broadcast band, computes FFT power per bin in JavaScript, and emits anomaly events when a spike jumps the rolling noise floor — each one triggering an Ovilus-style single-word fragment from the fast LLM tier.
|
||||
|
||||
**Architecture:** Everything RF happens client-side, in three pure-ish layers: `fft.ts` (radix-2 Cooley–Tukey FFT + dB power spectrum from interleaved I/Q), `RtlSdr` (WebUSB control-transfer driver for RTL2832U + R820T: init sequence, PLL tuning, sample-rate programming, bulk I/Q reads, and a continuous `sweep()` loop), and `SpectrumAnomalyDetector` (per-bin EMA noise floor + throttled spike detection). Anomalies flow to the backend as `{type: "anomaly", source: "radio", frequency /* MHz */, magnitude /* dB */}` over Plan 3's séance channel. The backend needs nothing new — this mode reuses Plan 3's anomaly→fragment pipeline exactly as EVP does (Plan 4).
|
||||
|
||||
**Tech Stack:** TypeScript, WebUSB (`navigator.usb`), `Float64Array` DSP; backend unchanged from Plans 3-4.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Chromium-only.** WebUSB exists only in Chrome/Edge/Brave/Opera; Firefox and Safari lack it entirely. `isSupported()` must gate the mode, which self-disables with an in-UI explanation (spec §7) — never a silent failure, never a broken page on other browsers.
|
||||
- **Kernel driver contention is expected, not exceptional.** Linux's `dvb_usb_rtl28xxu` claims RTL-SDR dongles before WebUSB can; claim failure surfaces the mode's troubleshooting guide link (spec §3.1/§7). Windows+Zadig/WinUSB typically works out of the box.
|
||||
- **Fail soft everywhere.** Every entry point of the driver treats a thrown error as "this vessel cannot hear the radio dead" — the UI degrades; the séance continues on other modes.
|
||||
- **No new backend surface.** Do not add WS frames or REST endpoints; `source: "radio"` anomalies already have a home.
|
||||
- **Hardware honesty.** The driver was structured from public librtlsdr register documentation in an environment with **no RTL-SDR attached**. It is marked `HARDWARE PASS REQUIRED` in the source and must not be called done until a real dongle validates it (see Task 4).
|
||||
|
||||
## Plan Series
|
||||
|
||||
This is 5 of 7 plans implementing the Quantumancy website spec (`docs/superpowers/specs/2026-07-20-quantumancy-website-design.md`). Plans 3-4 provide the anomaly pipeline this mode feeds.
|
||||
1. Foundation & Auth (complete)
|
||||
2. Frontend, LLM & Realtime Pipeline (complete)
|
||||
3. Wire Ghost mode + Ouija/Planchette UI (complete)
|
||||
4. EVP Listening mode (complete)
|
||||
5. **Spirit Radio mode** (this plan)
|
||||
6. Codex & entity persistence
|
||||
7. Internationalization (EN/ES)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: FFT & Power Spectrum
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/lib/fft.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `nextPow2(n)`; `fftInPlace(re: Float64Array, im: Float64Array)` (iterative radix-2, bit-reversal ordered, length must be a power of two); `magnitudeSpectrum(re, im) -> Float64Array` (first n/2 bins); `powerSpectrumDb(iq: Float64Array) -> Float64Array` — interleaved `[i0, q0, i1, q1, …]` in, n/2 dB values out, power normalized by n², floored at 1e-12 before log.
|
||||
|
||||
- [x] **Step 1: Implement the transform**
|
||||
|
||||
Plain dependency-free Cooley–Tukey: bit-reversal permutation, then butterfly passes with a twiddle recurrence (no per-butterfly trig). Throws on re/im length mismatch and non-power-of-two input — programming errors should be loud here, not silent spectrum garbage.
|
||||
|
||||
- [x] **Step 2: I/Q → dB**
|
||||
|
||||
`powerSpectrumDb` zero-pads/truncates to `nextPow2(iq.length / 2)`, splits interleaved I/Q into re/im planes, transforms, and returns `10 * log10(power)`. This is what the sweep loop hands the anomaly detector per tuning step.
|
||||
|
||||
- [x] **Step 3: Verify** — `npx tsc --noEmit` clean; `src/lib/fft.test.ts` (11 tests) validates the transform against known inputs — impulse spectra, cosine peaks landing on the expected bin for two different frequencies, power-of-two enforcement.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: RtlSdr — WebUSB Driver for RTL2832U + R820T
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/lib/sdr.ts` (driver half)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `RTL2832U_VENDOR = 0x0bda`, `RTL2832U_PRODUCTS = [0x2832, 0x2834, 0x2838, 0x2837]`; `isSupported(): boolean`; `class RtlSdr` with `requestDevice()`, `open(sampleRateHz = 2_048_000)`, `close()`, `setFrequency(hz)`, `setSampleRate(hz)`, `readSamples(bytes) -> Uint8Array`, `sweep(startHz, endHz, stepHz, cb, fftSize = 512, settleMs = 25)`, `stopSweep()`, `isOpen`. Callback types `RtlSampleBlock` and `SweepCallbacks {onSpectrum?(centerHz, db), onError?(err)}`.
|
||||
|
||||
- [x] **Step 1: Device selection & claim**
|
||||
|
||||
`requestDevice()` filters on the four known RTL2832U product IDs. `open()` locates the first bulk-IN endpoint, claims the interface (kernel-driver detach is best-effort; failure throws a themed "could not claim the radio dead (interface busy?)" error for the UI's troubleshooting link).
|
||||
|
||||
- [x] **Step 2: Init sequence** *(HARDWARE PASS REQUIRED)*
|
||||
|
||||
Follows librtlsdr's known-good order via `demodWrite` (paged demod registers) and `i2cWrite` (tuner registers tunneled through the demod's I2C repeater at 0x1a): soft reset → `demod_ctl` → suspend/standby off → AGC mode → R820T LNA/mixer/IF power-on → sample rate → initial 98 MHz tune → endpoint reset. Register pokes are commented as unverified against a real device.
|
||||
|
||||
- [x] **Step 3: Tuning & sample rate** *(HARDWARE PASS REQUIRED)*
|
||||
|
||||
`setFrequency` programs the R820T fractional-N PLL with the 3.57 MHz IF offset against the 28.8 MHz crystal reference (integer part + 16-bit SDM fraction; documented simplification of librtlsdr's exact sdm/vco math). `setSampleRate` programs the demod resampling ratio (`crystal·2²²/hz`, 4-aligned).
|
||||
|
||||
- [x] **Step 4: The sweep loop**
|
||||
|
||||
`sweep()` tunes in `stepHz` steps across `[startHz, endHz]`, waits `settleMs`, discards one 16 KiB block (PLL settle), reads `fftSize·4` bytes of unsigned I/Q, zero-centers to `[-1, 1)`, runs `powerSpectrumDb`, and emits `onSpectrum(centerHz, db)`. Read errors go to `onError` and the sweep continues; wrapping past `endHz` restarts at `startHz`; `stopSweep()` exits cleanly from the `finally`. Exported band constants: `SWEEP_START_MHZ = 88`, `SWEEP_END_MHZ = 108`.
|
||||
|
||||
- [x] **Step 5: Verify** — `npx tsc --noEmit` clean. Runtime verification deferred to Task 4.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: SpectrumAnomalyDetector — Rolling Floor Over the Airwaves
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/lib/sdr.ts` (detector half)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `class SpectrumAnomalyDetector(thresholdDb = 10, throttleMs = 2000, alpha = 0.1)` with `process(centerHz, sampleRateHz, db, nowMs) -> {frequency /* MHz */, magnitude /* dB */} | null` and `reset()`. Pure and testable: no USB types, injected spectrum + clock.
|
||||
|
||||
- [x] **Step 1: Floor + spike logic**
|
||||
|
||||
First spectrum seeds the per-bin EMA floor. Each subsequent spectrum updates the floor (`alpha` 0.1) and finds the peak deviation; a spike fires when `peak ≥ thresholdDb` (10 dB over the rolling floor — the radio dead must shout) and ≥ `throttleMs` (2 s) since the last emission. Bin→frequency maps across the baseband: `binHz = sampleRate/2 / bins`, offset from center, reported in MHz to match the `anomaly` frame contract.
|
||||
|
||||
- [x] **Step 2: Into the séance**
|
||||
|
||||
The mode page forwards detections as `sendAnomaly('radio', frequencyMHz, magnitudeDb)`. Backend flow is Plan 3 verbatim: `anomaly_ack`, auto-summon once ≥3 anomalies fingerprint, then `spirit_service.fragment('radio', …)` — the radio variant of the prompt ("A burst of static at {frequency} MHz, magnitude {magnitude} dB above the noise floor…") with the `"a spirit box"` system framing.
|
||||
|
||||
- [x] **Step 3: Verify** — `npx tsc --noEmit` clean; backend anomaly path covered by `tests/test_ws_session.py` (`test_anomalies_attune_then_produce_fragments` uses `source: "radio"`, 6/6 PASS).
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Hardware-in-the-Loop Validation
|
||||
|
||||
**Files:**
|
||||
- Modify (expected): `frontend/src/lib/sdr.ts` register sequences after testing
|
||||
|
||||
- [ ] **Step 1: Real-dongle smoke test — PENDING**
|
||||
|
||||
No RTL-SDR is attached to the build environment, so the driver has never touched silicon. Before this mode is called done: plug an RTL2832U+R820T dongle into a Chromium machine, claim it through the mode UI, and confirm the init sequence completes and bulk I/Q flows. Expect to debug register pokes with `librtlsdr -T` / a logic analyzer — the source is pre-marked `HARDWARE PASS REQUIRED` at the file header, the init sequence, and `setFrequency`.
|
||||
|
||||
- [ ] **Step 2: Sweep & detector tuning — PENDING**
|
||||
|
||||
With live RF: verify the 88–108 MHz sweep shows real broadcast peaks, calibrate `thresholdDb`/`settleMs` against a known station, and confirm anomalies reach the séance (fragment utterances arrive, transcript shows `radio` entries).
|
||||
|
||||
- [ ] **Step 3: Kernel-claim runbook — PENDING**
|
||||
|
||||
Validate the failure paths on Linux (`dvb_usb_rtl28xxu` bound → themed claim error + guide link) and an unsupported browser (mode self-disables with the spec §7 explanation, other modes unaffected).
|
||||
|
||||
---
|
||||
|
||||
## Testing Status & Self-Review
|
||||
|
||||
**Spec coverage:** WebUSB sweep + browser FFT + spike anomalies → Ovilus fragments (§3.1) → Tasks 1-3. Chromium-gating and self-disable (§3.1, §7) → `isSupported()` + Task 4 step 3. Driver-claim troubleshooting (§3.1, §7) → `open()`'s themed claim error. Manual hardware pass before done (§8) → Task 4, **pending**.
|
||||
|
||||
**Automated tests:** `fft.ts` is covered by `src/lib/fft.test.ts` (11 tests, PASS in the 71-test vitest run); the backend side is covered by `tests/test_ws_session.py` radio-anomaly flow (6/6 in the 45-test suite). **Honest gaps:** no vitest yet for `SpectrumAnomalyDetector` (pure and test-ready — the detector logic mirrors the covered `EvpDetectorCore`), and no automated coverage is possible for the USB path itself.
|
||||
|
||||
**Placeholder scan:** the driver is real code, not a stub — but its register sequences are deliberately labeled unverified, and this plan does not claim otherwise. Task 4's pending checkboxes are the whole truth.
|
||||
|
||||
**Type consistency:** detector output `{frequency: MHz, magnitude: dB}` matches `SeanceApi.sendAnomaly` and the `anomaly` ClientFrame; MHz (radio) vs Hz (EVP) unit mixing is handled server-side by `signature_from_anomalies`' digit-count bucketing (documented in `app/entities.py`). `powerSpectrumDb` output length (n/2) matches `SpectrumAnomalyDetector.process`'s bin math.
|
||||
|
||||
---
|
||||
|
||||
**Status: IMPLEMENTATION COMPLETE — HARDWARE VERIFICATION PENDING.** The mode must not be shipped as "done" until Task 4 passes against a physical RTL-SDR on Chromium.
|
||||
Reference in New Issue
Block a user