Indiana b2b0dc319a fix: no WebGL took down the entire app, not just the ghost
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>
2026-08-02 03:11:02 +00:00
2026-08-01 21:14:59 +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%