Files
qtalker---/docs/superpowers/plans/2026-07-20-quantumancy-05-spirit-radio.md
Indiana 6edbbbbc2a 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
2026-07-20 21:11:49 +00:00

11 KiB
Raw Blame History

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.

  • 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.

  • 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.

  • 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)}.

  • 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).

  • 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.

  • 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).

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.