Found by actually looking at the deployed site instead of trusting the test suite. Rendered /seance in a browser with no GPU and got a completely blank page — `#root` with ZERO children, console reading "THREE.WebGLRenderer: Error creating WebGL context". Not a missing apparition: no modes, no ask field, no transcript, nothing. Desktop and mobile alike. Cause: `new GhostScene()` constructs a THREE.WebGLRenderer, which throws when a context cannot be created. The exception escaped its effect, and with no error boundary above it React unmounted the whole root. The apparition is atmosphere and must never be able to do that. It is now constructed inside a try/catch that degrades to a quiet violet glow in the same palette, so the stage reads as occupied rather than broken, and the séance carries on completely. This is not exotic. Any device with a blocklisted GPU, hardware acceleration switched off, or a driver too old for the browser hits exactly this path — and the failure mode was the worst available: a blank page with no explanation. Proven end to end, not just in tests: same browser, same URL, before the fix `rootChildren: 0` and an uncaught error; after it `rootChildren: 4`, zero uncaught errors, and the body reading WIRE GHOST / EVP / SPIRIT RADIO / OUIJA / FIELD / LENS. The screenshots confirm the phone shell and the untouched desktop layout both render. 432 tests (3 new, which fail without the guard). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 stormas 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 samplingseedderives 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.
/shopremains 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/guestmints a realwanderer-xxxxaccount 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.webmanifestmakes 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/sessionséance and/ws/device-feedsockets, 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_crowdederror 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_testis dropped and recreated by every test run. Never point the app's ownDATABASE_URLat 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 offirmware/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_ORDERinLandingPage.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.mdis 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/