Indiana da675bd198 fix: port the real librtlsdr init — the RTL-SDR never had a chance
Reported symptom: the dongle connects through the browser fine and then
produces nothing. The driver was a sketch written from memory and marked
HARDWARE PASS REQUIRED, and that caveat never reached the UI.

The decisive bug was not the tuner at all: `demodWrite` had every field of
its control transfer wrong. librtlsdr uses value=(addr<<8)|0x20,
index=0x10|page, big-endian; this used the block in value, page and address
transposed in index, and little-endian. Every demod register write went to
the wrong place. The block constants were wrong too (DEMOD=0 USB=1 SYS=2,
not 0x03/0x02/0x09), as was the I2C repeater control (page 1 reg 0x01,
0x18/0x10 — not reg 0x02, 0x41/0x01).

Then the tuner, as originally suspected: the R820T's full 27-register init
(0x05-0x1f) replaces three pokes, with a shadow array since those registers
are write-only; IF filter calibration; a real r82xx_set_pll with VCO band
scan, nint+SDM into 0x14/0x16/0x17, and a lock poll that names the frequency
that failed instead of streaming silence. Gain defaults to tuner AGC. The
buffer reset is 0x1002 -> 0x0000, not 0xffff.

Two more that would each have been fatal alone: the demod was left in
zero-IF mode though the R820T delivers a 3.57MHz IF, so a locked PLL would
still have been off-centre; and setSampleRate masked with JS's 32-bit `&`
on a ~1.2e14 value, mangling the ratio below ~1.15Msps.

Tuner detected by chip id, throwing "unsupported tuner: <name>" rather than
running R82xx sequences against foreign silicon.

The arithmetic is pure and tested against hand-derived vectors, including
107.9MHz where mix_div drops 32->16 — a boundary the old fixed mixDiv=2
could never have reached. Independently re-derived 88.5MHz (0x26/0x66) and
98MHz (0x6d/0x82) and they match exactly.

HONEST LIMIT: none of this has touched hardware. The arithmetic is proven;
the register pokes and transfer encodings are reasoned, not observed.
Several constants are marked UNCONFIRMED in the file, chiefly the tracking
filter table and the SDM register pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:46:56 +00:00

QUANTUMANCY

an instrument for speaking with the dead bandwidth

Quantumancy is a self-hosted séance. Point a browser at it and real sensing channels — your microphone, your phone's motion sensors and magnetometer, an RTL-SDR dongle, your own network's jitter, a paired ESP32 sensor node, or just a text box — feed real anomaly detection into a locally-run LLM that invents a spirit on the spot: a name, an epithet, a persona, a coherent voice, hidden traits, a rarity tier, and a small immortality in a shared registry called the Codex. A local Piper TTS then gives that spirit an actual voice, degraded through a hand-built static-and-echo effects chain until it sounds like it's coming through a dying radio.

And the physical world genuinely participates: every summon carries entropy harvested from your room's own noise floors, tonight's real moon phase skews who answers, NOAA's measured geomagnetic K-index thins the veil, and unprompted speech is token-sampled with a seed derived from the room — the room literally selects the words.

This is an interactive horror art installation, not a paranormal claim. Every "entity" is fiction generated by a language model on your own hardware — the system prompts are explicit about this (see backend/app/llm/prompts.py). But every measurement is real: FFT power spectra, magnetometer microtesla, /proc/net/dev counters, BLE signal attenuation, published lunar ephemerides, NOAA space-weather data. The rule (see docs/CHANNELS.md): the fiction may interpret a measurement however it likes; it may never fabricate one. Nothing leaves your network except a courteous 10-minute poll of NOAA's public Kp feed — no cloud LLM, no analytics. If the LLM box goes dark mid-séance, a procedural fallback keeps every mode answering so a summoning never visibly fails.

The Séance: Ways In

Every mode writes into the same pipeline: an anomaly event (source, frequency, magnitude) feeds a per-session fingerprint; once a session has produced at least 3 anomalies of real structure, it auto-summons — either re-contacting a Codex entry whose signature matches (a real draw, not a guarantee — see The Room Decides), or minting someone new.

Mode Vessel What actually happens
Wire Ghost Nothing — works for everyone Real, non-content network telemetry from the host: byte-counter jitter (/proc/net/dev), TCP connect-latency variance, DNS timing, against a rolling baseline. Payloads are never inspected — a hard privacy boundary. Passive listening lets the line murmur on its own.
EVP Listening Microphone (getUserMedia) A Web Audio AnalyserNode watches the voice band (~300 Hz–3.4 kHz) for deviations ≥ 8 dB over the room's rolling noise floor — live spectrum scope, audible anomalies, and every frame feeds the entropy pool. Works everywhere, including iPhone.
Spirit Radio RTL-SDR dongle (WebUSB, Chromium only) The browser drives the dongle directly, sweeping FM 88–108 MHz, computing power spectra against a rolling floor. Every USB call is time-boxed (withTimeout) so a sulking dongle errors instead of hanging silently.
Ouija / Direct Contact The shared front door A canvas planchette wanders the full board, brushes letters, visits YES/NO, and rests on GOODBYE every few minutes; questions stream back token by token while it spells.
EMF / Field Meter A phone Where the Generic Sensor API exists (Chromium/Android), the actual magnetometer in microtesla — a true EMF meter, baseline-subtracting Earth's constant field. Everywhere else (including iOS), the motion-fusion fallback: acceleration deviation + orientation angular velocity. Both are real physics; lib/baseline.ts gives them one shared rolling-EMA core.
The Vessel ESP32-P4 sensor node (LAN) Paired hardware POSTs temperature/pressure/mic/radar readings; the Cold Spot Detector and the séance's DeviceWhisper panel render them live, and hardware anomalies can trigger a summon in an open séance. Firmware in firmware/ — written and reviewed, never yet flashed to a real board.

lib/bluetooth.ts (BLE RSSI as a presence field — bodies absorb 2.4 GHz) is implemented and tested but not yet wired into a séance panel.

The Room Decides

backend/app/entropy.py · frontend/src/lib/entropy.ts · backend/app/celestial.py · backend/app/geomagnetic.py

  • Physical entropy. The client harvests noise from live sensor frames (mic and RF noise floors), Von Neumann–debiases it, SHA-256–conditions it, and sends it with every summon. The server treats it as untrusted by construction: every draw is HMAC(fresh server secret, contribution || context), so a hostile client can only ever add unpredictability, never steer an outcome. Whether a channel's familiar spirit answers again (RETURN_CHANCE) is one of these draws.
  • Real astronomy. Moon phase from orbital mechanics (validated against published ephemeris dates, not against itself) and true solar midnight from the seeker's longitude (only the longitude is kept, never a full coordinate). A full moon roughly triples rare/mythic mint odds — a skew, never a gate — nudges hidden traits stronger and stranger, and erodes a familiar spirit's claim on its channel so strangers push through on thin nights.
  • Real space weather. NOAA SWPC's planetary K-index, cached 10 minutes, never blocking: a geomagnetic storm measurably thins the veil, and the entity is told Kp 6.33 — moderate geomagnetic storm as a fact of its room.
  • Generation from nothing. SpiritService.manifest() produces unprompted speech: the prompt contains no seeker input at all — only measured room state — and Ollama's sampling seed derives from the room's entropy. Change the noise, get different words. Rendered in the transcript as an intrusion (violet edge), not a reply.

Rituals, Judgment, and the Reliquary

Every minted entity carries hidden traits (alignment / power / volatility / deceptiveness) — signature-seeded ground truth the persona prompt never sees, so the LLM cannot leak it. The séance offers:

  • The ritual — a 4-sigil hold-to-charge rite; success reveals the entity's true traits, failure reveals nothing. Judgment is never gated on it: you may always judge blind.
  • Judgment — trust / banish / test / cross over. Correct calls pay essence and favor; wrongly trusting a demon sharpens the haunting, wrongly banishing a benevolent spirit wounds it, and a correctly-judged stuck spirit crosses over: at peace, permanently retired from its channel, its signature freed for someone new.
  • The Reliquary — essence (earned, never bought), item drops on milestone moments, purchasable unlocks, and hand-drawn sigils. /shop remains a waitlist for the Ultimate Quantum Box hardware — email capture only, no payment taken, and the page says so.

A Ghost Log HUD whispers entity tells app-wide, and /log is the recallable record: your last séances with entity, counts, and echo lines.

Coming In From the Cold

  • Guests: POST /auth/guest mints a real wanderer-xxxx account and a normal session — the front door (/enter) offers it as "slip through as a wanderer", and the séance nudges wanderers to claim a name so their codex outlives the mist.
  • Conditions: GET /api/conditions (and the séance's side-column strip) surfaces tonight's moon, veil thinness, and geomagnetic state.
  • First contact: each mode shows one in-fiction hint if its sensor sits unused, dismissible forever; error copy redirects instead of dead-ending (no mic? "the board needs no ear").
  • PWA: a manifest.webmanifest makes it installable to a home screen (standalone, no service worker — an offline séance is meaningless).
  • Phones: dedicated ≤560px and ≤380px layouts, 44px touch targets, 16px inputs (so iOS Safari doesn't zoom-and-stick).

Architecture

Two machines on the LAN, no containers anywhere:

                        ┌────────────────────────────────────────────┐
  seekers ──HTTPS──▶ Cloudflare Tunnel ──HTTP──▶  App CT  :7777      │
                        (external machine,       FastAPI (uvicorn,   │
                         terminates TLS)          Python venv,       │
                                                  systemd service)   │
                                                  │                  │
                                                  ├─ serves built    │
                                                  │   React SPA      │
                                                  ├─ Postgres (apt)  │
                                                  ├─ Piper TTS + FX  │
                                                  └───────┬──────────┘
                                                          │ LAN
                       ┌──────────────────┐   ┌───────────▼─────────┐
                       │ ESP32-P4 node(s) │   │ Ollama box          │
                       │ (paired, HTTP    │   │ 10.30.20.107:11434  │
                       │  telemetry in)   │   │ CPU-only, 64 GB     │
                       └──────────────────┘   │ fast + chat models  │
                                              └─────────────────────┘
                                                  ...and one WAN egress:
                                                  NOAA SWPC Kp (10-min cache)
  • App CT — FastAPI serves the built Vite/React SPA, the REST API, the /ws/session séance and /ws/device-feed sockets, and /audio/* spirit WAVs. Postgres holds accounts, sessions, transcripts, the Codex, traits, essence/favor, inventory, sigils, devices, and the waitlist. Piper TTS is degraded through a numpy chain (rate → pitch → bitcrush → echo → static); eight voice archetypes (elder/young/child/drowned/burned/distant…) covary pitch/rate/noise/echo so two spirits sound like two different dead people, not two settings.
  • Ollama box — two model tiers behind a bounded-concurrency queue; a full queue rejects with a themed veil_crowded error rather than hanging.
  • Cloudflare Tunnel — terminates HTTPS; its secure context is what unlocks mic, WebUSB, and motion/magnetometer permissions.
  • systemd — Restart=always, no start-limit surrender, wants Postgres: the veil survives reboots and crash bursts without a human.

Repo layout

backend/
  app/
    main.py          routers, idempotent startup migrations, SPA fallback
    ws.py            the séance channel + rituals/judgment + manifest
    entropy.py       HMAC-conditioned physical randomness (the room decides)
    celestial.py     moon phase + true solar midnight (computed, offline)
    geomagnetic.py   NOAA SWPC Kp cache (never blocks a summon)
    entities.py      signatures, voice archetypes, moon-skewed rarity/traits
    judgment.py      verdict logic, tells, favor bias, ritual rolls
    inventory.py     essence economy, drop tables, unlocks, sigil validation
    device_anomaly.py ESP32 telemetry -> anomaly/summon bridge
    telemetry.py     Wire Ghost sampler   llm/  tts/  models/  routes/
  tests/             pytest suite — 328 tests
firmware/
  esp32p4-sensor-node/   ESP-IDF firmware: BMP280, MEMS mic, RD-03E radar,
                         WiFi via ESP32-C6 (UNVERIFIED on real hardware)
frontend/
  src/pages/         Landing, Enter, Seance, Codex(+Entity), Shop,
                     Inventory, Devices, GhostLog
  src/lib/           entropy, baseline (shared EMA core), coldSpot,
                     bluetooth, magnetometer, evp, sdr, emf, fft,
                     planchette, haunting, deviceFeed, spectrumSonify
  src/components/    PlanchetteBoard, SpectrumScope, VeilConditions,
                     DeviceWhisper, ModeHint, Ritual/Judgment/Inventory
                     panels, SigilDesigner, ColdSpotPanel, GhostLog HUD…
docs/
  CHANNELS.md        what each channel really measures + verification status
  superpowers/       design specs + implementation plans

Quickstart

Prereqs on the App CT: Python 3.11+, Node 18+, Postgres (apt), and an Ollama box on the LAN with the two model tiers pulled.

1. Postgres (one-time):

sudo apt-get update && sudo apt-get install -y postgresql
sudo -u postgres psql -c "CREATE ROLE quantumancy WITH LOGIN PASSWORD 'quantumancy';"
sudo -u postgres psql -c "CREATE DATABASE quantumancy OWNER quantumancy ENCODING 'UTF8' TEMPLATE template0;"
sudo -u postgres psql -c "CREATE DATABASE quantumancy_test OWNER quantumancy ENCODING 'UTF8' TEMPLATE template0;"

quantumancy_test is dropped and recreated by every test run. Never point the app's own DATABASE_URL at it.

2. Configuration:

cp .env.example .env
# edit .env: confirm DATABASE_URL and OLLAMA_BASE_URL

3. Backend:

cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

4. Piper voices. backend/voices/ is gitignored; download each .onnx plus its .onnx.json from rhasspy/piper-voices:

id file language character
lessac en_US-lessac-low.onnx en a measured American woman (default)
amy en_US-amy-low.onnx en a soft American woman
ryan en_US-ryan-low.onnx en a deep American man
alan en_GB-alan-low.onnx en a low British man
hfc_male en_US-hfc_male-medium.onnx en a worn male voice
hfc_female en_US-hfc_female-medium.onnx en a worn female voice
davefx es_ES-davefx-medium.onnx es una voz masculina grave (ES default)
ald es_MX-ald-medium.onnx es una voz masculina seca

5. Frontend:

cd frontend
npm install
npm run build        # produces frontend/dist/, served by the backend

6. Run. By hand:

cd backend
set -a && source ../.env && set +a
venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 7777

or as a service (recommended). The shipped unit (deploy/quantumancy.service) hardcodes /root/quantumancy — edit the two paths first if your checkout lives elsewhere. A production install should also add the persistence drop-in (Restart=always, StartLimitIntervalSec=0, Wants=postgresql.service):

sudo cp deploy/quantumancy.service /etc/systemd/system/
sudo systemctl enable --now quantumancy

Then visit http://<host>:7777 — or your Cloudflare Tunnel hostname for the secure context that mic, WebUSB, and motion/magnetometer all require.

7. Ollama models (on the Ollama box, one-time):

ollama pull granite4.1:3b        # fast tier: fragments, ambient whispers
ollama pull minicpm-v4.5:latest  # chat tier: direct contact, minting, manifest

Any Ollama tag works — override with OLLAMA_FAST_MODEL / OLLAMA_CHAT_MODEL.

Configuration

All settings live in backend/app/config.py, read from the environment or .env. Required: DATABASE_URL, OLLAMA_BASE_URL.

Env var Default Purpose
DATABASE_URL — (required) asyncpg connection string
OLLAMA_BASE_URL — (required) Ollama REST endpoint
PORT 7777 HTTP listen port
OLLAMA_FAST_MODEL granite4.1:3b fast tier
OLLAMA_CHAT_MODEL minicpm-v4.5:latest chat tier (replies, minting, manifest)
LLM_MAX_CONCURRENCY 2 simultaneous Ollama calls
LLM_MAX_QUEUE_DEPTH 8 queued calls before themed rejection
LLM_COOLDOWN_SECONDS 8.0 min gap between ambient whispers
PIPER_VOICES_DIR voices voice models (relative to backend/)
DATA_DIR data generated audio, served at /audio/

The Séance Protocol

One WebSocket per contact session: /ws/session, authenticated by the qm_session cookie (close code 4401 otherwise). JSON frames both ways.

Client → server:

Frame Payload Notes
ping — pong
set_mode mode: wire|evp|radio|ouija|emf persisted on the session
language en|es switches LLM language + Piper voice
summon optional entropy (conditioned room noise), optional longitude rate-limited 4/min/user; the sky + entropy shape who answers
anomaly source, frequency, magnitude auto-summons at ≥3; then fragments (30/min/user)
question text ≤500 streamed reply (6/min/user)
passive enabled ambient Wire Ghost loop
ritual_start / ritual_step step 4 steps complete an attempt (6/min/user)
judgment verdict: trust|banish|test|cross_over 10/min/user

Server → client (single sender task; frames never interleave):

Frame Meaning
session, pong, mode, passive plumbing
status attuning|summoning|gathering; summoning carries entropy_bits, sky (moon/thinness), geomagnetic
entity matched or minted (public fields only — hidden traits never leave the server unearned)
anomaly_ack / anomaly counts; server-detected events
utterance kind: greeting|fragment|ambient|reply|manifest — manifest is unprompted speech
audio TTS + effects finished for an utterance
reply_start/token/end streamed Direct Contact
telemetry live Wire Ghost vitals
tell a trait-shaped whisper for the Ghost Log
ritual_complete success, revealed traits on success
judgment_result correct, deltas, at_peace, consequence
item_drop a relic falls
error rate_limited / veil_crowded, themed

REST (session-cookie auth where noted):

Endpoint Auth Purpose
POST /auth/register · login · logout · GET /auth/me — argon2 + qm_session cookie
POST /auth/guest public, 5/hr/IP mint a wanderer- account
GET /api/codex[?rarity,sort,limit] · GET /api/codex/{id} public the shared registry / full dossier
GET /api/stats public live veil counters
GET /api/conditions[?lon] public moon, veil thinness, geomagnetic
GET /api/seances/recent auth the Ghost Log: last 12 sessions + echoes
GET/POST /api/device · POST /api/device/telemetry · WS /ws/device-feed auth / device token ESP32 pairing + live readings
GET /api/inventory/{unlocks,items,sigils} · POST /api/inventory/unlocks/{key} · POST /api/inventory/sigils auth the Reliquary
POST /api/shop/waitlist public, 5/hr/IP hardware waitlist
GET /healthz — {"status": "ok"}

Testing

Backend — pytest, 400 tests. Pass your NORMAL DATABASE_URL; tests/conftest.py derives the test database itself by appending _test to the name, so the command below creates and uses quantumancy_test. That database is dropped and recreated every run — never run two suites concurrently against it; the autouse drop/create fixture will fight itself.

cd backend && source venv/bin/activate
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy \
OLLAMA_BASE_URL=http://10.30.20.107:11434 \
python -m pytest -q

Do not point DATABASE_URL at quantumancy_test here — the suffix is appended, not checked for, so you would get quantumancy_test_test, which does not exist, and all 400 tests error at setup. To use a database that isn't <your db>_test, set TEST_DATABASE_URL explicitly instead; its name must still end in _test.

Two guards in conftest.py make it safe to name the live database here: the derived URL must differ from DATABASE_URL, and the derived database name must end in _test. The second exists because the first alone is defeated by localhost vs 127.0.0.1 addressing the same database. This matters — an early version of the suite ran drop_all against production and destroyed live séance data.

Frontend — Vitest + Testing Library, 385 tests, plus a hard i18n en/es parity gate (npm run pretest):

cd frontend && npm test

Hardware-in-the-loop (cannot be unit tested): the RTL-SDR sweep, mic permissions, phone sensors, and everything in firmware/ need real hardware passes. docs/CHANNELS.md tracks exactly what has and hasn't had one.

Current State & Known Gaps

Honestly, in order of confidence:

  • Solid, verified live. Auth (incl. guests), the Codex, Wire Ghost, EVP + spectrum scope + sonification, TTS + archetype voices, entropy, astronomy (validated against published ephemerides), the NOAA feed (verified against the live endpoint), rituals/judgment/essence, the Ghost Log, conditions, PWA manifest, phone layouts, the haunting layer. 328 + 366 tests pass.
  • Should work, unverified on hardware. lib/sdr.ts (WebUSB RTL-SDR) is written from librtlsdr register docs and has never met a real dongle; every entry point is time-boxed and fails soft. The magnetometer EMF path needs an Android device pass. All of firmware/ compiles-by-eye: reviewed twice, never flashed.
  • Built but not yet reachable. lib/bluetooth.ts (BLE presence field) has no séance panel yet.
  • Known drift. The landing page's marketing grid still shows four modes (MODE_ORDER in LandingPage.tsx) — EMF and everything since aren't in the showcase.
  • Cosmetic. One ~930 KB JS chunk; code-splitting would help first paint.
  • Intentionally unfinished. The Ultimate Quantum Box is a waitlist — no hardware ships, no payment is taken, and the page says so.

The Veil's Honesty Policy

  • All LLM system prompts frame the entity as a horror-fiction persona in an interactive art installation — never a genuine paranormal claim.
  • Every number shown to a seeker is a real measurement. The fiction interprets; it never fabricates. (docs/CHANNELS.md is the ledger.)
  • Wire Ghost telemetry reads counters and timings only; packet payloads are never inspected. Only a longitude is ever kept from a location grant.
  • Client entropy is untrusted by construction — it can add unpredictability to a draw, never steer one.
  • If the Ollama box is dark, every channel degrades to procedural fallbacks — a summoning never visibly fails.

Further Reading

  • docs/CHANNELS.md — every channel, what it really measures, and its honest verification status
  • Design specs: docs/superpowers/specs/ (website, character depth, ESP32 node, possession, usability wave)
  • Implementation plans: docs/superpowers/plans/
Description
talking to spirits
Readme 2.7 MiB
Languages
TypeScript 46.3%
Python 32.6%
CSS 13%
C 7.3%
JavaScript 0.3%
Other 0.4%