Both measure genuine physics rather than dressing up a random number. Bluetooth (lib/bluetooth.ts): BLE advertises in the 2.4GHz ISM band, and the human body is mostly water, which absorbs 2.4GHz strongly — the same physics that makes a microwave work and that degrades your wifi when someone stands between you and the router. So RSSI genuinely drops when a body crosses the path. That makes signal-strength variance a real, physically-grounded movement signal. The module reports exactly that and nothing more: it never claims a drop *is* a presence, only that the field changed. Threshold is 6dB — above the 2-4dB of idle multipath wander, and inside the 3-10dB a real body actually causes. Magnetometer (lib/magnetometer.ts): the existing EMF mode infers field disturbance from DeviceMotion/DeviceOrientation, which is a real measurement but measures *movement*, not magnetism — a phone sitting still beside a running motor reads nothing. This reads the actual magnetometer, so the EMF meter measures what an EMF meter is supposed to. Real ghost-hunting EMF meters are just magnetometers, and the spikes they pick up come from mains wiring, motors and moving ferrous mass — all of which this picks up, for the same real reasons. 3uT threshold clears the ~0.5-1uT sensor noise while still catching household sources. Earth's constant 25-65uT background is explicitly what the rolling baseline exists to subtract. Both are additive: neither replaces the existing motion-based EMF, which stays the fallback because it works on iOS where neither of these do (no Web Bluetooth, no Generic Sensor API in any iOS browser). Both reuse the time-aware EMA baseline shape from coldSpot.ts, since advertisement and sensor intervals are irregular and a fixed per-sample alpha would weight a burst and a long gap identically. Web Bluetooth types are declared locally rather than pulling in @types/web-bluetooth for three shapes — same approach lib/emf.ts already takes with its non-standard sensor types. Also renders unprompted 'manifest' utterances as an intrusion: violet edge, full opacity (unlike the faded ambient/fragment murmurs), and a brief blur-in. The unsettling part is that it is perfectly clear and completely unbidden. 355 frontend tests pass (26 new); i18n parity gate passes. 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 five different sensing channels — your microphone, your phone's motion sensors, an RTL-SDR dongle, your own network's jitter, 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 voice, 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.
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), and no amount of atmosphere changes what's
actually happening underneath: FFT power spectra, voice-band deviation
detection, device-motion EMAs, and /proc/net/dev counters, all real signal
processing on real data. Nothing leaves your network — no cloud LLM, no
third-party inference, no analytics. If the LLM box goes dark mid-séance, a
procedural fallback (deterministic, signature-seeded) keeps every mode
answering so a summoning never visibly fails.
The Séance: Five 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 a spirit —
either re-contacting a Codex entry whose signature matches, or minting a
brand new one.
| Mode | Vessel | What actually happens |
|---|---|---|
| Wire Ghost | Nothing — works for everyone | The backend samples real, non-content network telemetry from the host: interface byte-counter jitter (/proc/net/dev), TCP connect-latency variance against reference hosts, DNS resolution timing. A rolling baseline (needs 6+ samples, an absolute 20 KB/s floor, a 3σ + 2.5× threshold) flags genuine surges as anomalies, throttled to one every 20s. Packet payloads are never inspected — a hard privacy boundary. "The wire knocks loudest when nobody is touching it" — flip on passive listening and let the line murmur on its own. |
| EVP Listening | Microphone (getUserMedia) |
A Web Audio AnalyserNode watches the voice band (~300 Hz–3.4 kHz) for a brief deviation ≥ 8 dB above the room's rolling noise floor, throttled to one per 2s — the floor only adapts during genuinely quiet stretches so sustained speech can't mask a real spike. The classic "record silence, review for voices" technique, done live. |
| Spirit Radio | RTL-SDR dongle (WebUSB, Chromium only) | The browser drives the dongle directly — no native driver, no server round-trip — sweeping the FM band (88–108 MHz) in 1.8 MHz steps, computing a power spectrum per tune and comparing it to a rolling noise floor. Spikes ≥ 10 dB above floor become anomaly events, throttled to one per 2s: an FM sweep pushed through a lantern dragged across a dark field. |
| Ouija / Direct Contact | The shared front door | A canvas planchette drifts on its own, then spells the spirit's words letter by letter. Ask a free-text question (≤ 500 chars) and the chat-tier model streams a full reply token by token while the planchette works through it. |
| EMF / Field Meter | A phone (DeviceMotion + DeviceOrientation) | The seeker's own phone becomes the meter: acceleration-magnitude deviation from a slow gravity EMA, fused with orientation angular velocity, tracked against an adapting baseline. A reading past baseline × 2.5 (and an absolute floor, so a resting phone stays silent) fires an anomaly, throttled to one per 2.5s. iOS 13+ gates this behind an explicit gesture-triggered permission grant, handled in-app. |
Every session begins unidentified. The backend fingerprints the anomaly
pattern into a signature (backend/app/entities.py); a matching
signature re-contacts an existing spirit and bumps its contact count, a new
one gets minted by the chat-tier LLM and dropped into the Codex with a name,
epithet, 2–3 sentences of lore, a rarity tier (common/uncommon/rare/mythic —
weighted 55/30/12/3 in the offline fallback), a voice profile (pitch, rate,
noise, echo), a visual hue/form, and two sample quotes.
The haunting doesn't stop at the séance panel. A framework-free
possession layer (frontend/src/lib/haunting.ts,
frontend/src/components/HauntingLayer.tsx) runs across the whole app: an
idle-aware escalator that gets bolder the longer you sit still, a gesture-armed
ambient dread-bed (brown noise through a breathing lowpass, a 55/55.7 Hz
detuned drone beat, occasional reversed-noise swells), fleeting whisper-words
drifting across the glass, and document.title glitches when you tab away.
Nothing stirs for the first ten seconds after load — it creeps in, it never
jumpscares on arrival — and it's fully inert under prefers-reduced-motion.
The Codex
GET /api/codex (public, no auth) is a browsable, shared registry of every
spirit ever contacted by any seeker: filterable by rarity, sortable by
recent or contacted, each card showing name, epithet, rarity badge, a
sample quote, contact count, and who discovered it. GET /api/codex/{id}
opens a full dossier — persona, voice-parameter table, sighting count. It's
the collectible layer sitting on top of the anomaly-detection plumbing:
nothing about a spirit is user-authored, all of it comes from the same
signature → LLM-mint → normalize pipeline that runs live during a séance.
The Reliquary
/shop is a waitlist page for the Ultimate Quantum Box — a proposed
ESP32-P4/C6 handheld séance instrument (thermal camera, EMF whisker array,
geophone, spirit-box mic preamp, OLED face) plus four standalone modules,
none of which exist yet. POST /api/shop/waitlist (public, rate-limited to
5/hour/IP) is pure email capture — no payment is taken, and the page says so
outright: "the website works fully without it; hardware only knocks
louder." Treat this section of the README the same way: real code, honest
about being pre-order vaporware for now.
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
┌───────────▼─────────┐
│ Ollama box │
│ 10.30.20.107:11434 │
│ CPU-only, 64 GB │
│ fast + chat models │
└─────────────────────┘
- App CT — plain HTTP on port 7777. FastAPI (async, WebSocket-native)
serves the built Vite/React SPA as static assets, the auth + Codex + shop
REST API, the
/ws/sessionséance WebSocket, and/audio/*spirit-voice WAVs. Postgres holds accounts, sessions, transcripts (events), the Codex, and the hardware waitlist. Piper TTS is invoked locally per utterance, then degraded through a numpy effects chain (rate → pitch → bitcrush → echo → static). - Ollama box — reachable over LAN via Ollama's REST API. Two model
tiers: a fast model for fragments/ambient whispers/wire whispers, and
a heavier chat model for Direct Contact replies and entity minting.
CPU-only and shared, so all LLM calls flow through a bounded-concurrency
queue (
LLMQueue); queued requests render in the UI as "the spirits are gathering energy…", and a full queue rejects new work with a themedveil_crowdederror rather than hanging. - Cloudflare Tunnel — managed outside this repo; terminates HTTPS and
points at
http://<app-ct-ip>:7777. The app never handles TLS. The tunnel's HTTPS origin satisfies browser secure-context requirements for mic (EVP), WebUSB (Spirit Radio), and motion sensors (EMF on iOS).
Repo layout
backend/
app/
main.py FastAPI app: routers, /healthz, /assets + /audio mounts, SPA fallback
config.py pydantic-settings (env vars below)
db.py async SQLAlchemy 2.0 engine/session (asyncpg)
deps.py get_current_user, qm_session cookie
security.py argon2 password hashing
rate_limit.py fixed-window RateLimiter (per-user LLM limits, per-IP waitlist limit)
telemetry.py Wire Ghost sampler (/proc/net/dev, TCP RTT, DNS timing)
entities.py anomaly signatures, profile normalization, procedural fallback
ws.py the séance channel: /ws/session protocol + ambient loop
llm/ OllamaClient, bounded LLMQueue, SpiritService, prompt builders
tts/ Piper CLI wrapper, voice catalog, numpy effects chain
models/ users, auth_sessions, contact_sessions, entities,
entity_sightings, events, waitlist_entries
routes/ auth.py, codex.py, shop.py
tests/ pytest suite (54 tests, 14 files)
voices/ Piper .onnx voice models (gitignored — see setup)
data/ generated utterance audio, served at /audio/ (gitignored)
deploy/
quantumancy.service systemd unit template
frontend/
src/pages/ LandingPage, EnterPage, SeancePage, CodexPage,
CodexEntityPage, ShopPage
src/lib/ types (WS protocol), ws (VeilSocket), audio player,
evp, sdr, emf, fft, planchette machine, haunting
src/state/ AuthProvider, SeanceProvider (reducer + socket wiring)
src/components/ PlanchetteBoard, Transcript, EntityCard, GhostGlyph,
MatrixView, GraphView, TelemetryReadout, HauntingLayer
src/three/ GhostCanvas + GhostScene (custom-shader 3D spirit)
src/i18n/ react-i18next init + en.json / es.json
docs/superpowers/
specs/ the design spec — note the live app now exceeds it
(5 modes, not 4; the Codex, the Reliquary and the
haunting layer are all further along than documented)
plans/ the 7 implementation plans this repo was built from
Quickstart
Prereqs on the App CT: Python 3.11+, Node 18+, Postgres (via 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; the app expects these
voice models there (id → file), downloadable from the
rhasspy/piper-voices HuggingFace
repo (each .onnx plus its .onnx.json):
| 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. Either by hand (from backend/, with the .env values in the
environment — the app reads .env relative to its working directory, and
piper_voices_dir/data_dir are relative to backend/):
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 — auto-restart, boot-start, journalctl -u quantumancy).
The shipped unit (deploy/quantumancy.service) hardcodes /root/quantumancy
as the working directory and env file path — edit those two lines first if
your checkout lives elsewhere:
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 Spirit Radio, EVP, and EMF (on iOS) 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, entity minting
Any Ollama tag works — override with OLLAMA_FAST_MODEL / OLLAMA_CHAT_MODEL.
Configuration
All settings live in backend/app/config.py and are read from the environment
or .env (repo root when run via systemd; CWD otherwise). Required:
DATABASE_URL, OLLAMA_BASE_URL.
| Env var | Default | Purpose |
|---|---|---|
DATABASE_URL |
— (required) | asyncpg connection string, e.g. postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy |
OLLAMA_BASE_URL |
— (required) | Ollama REST endpoint, e.g. http://10.30.20.107:11434 |
PORT |
7777 |
HTTP listen port |
OLLAMA_FAST_MODEL |
granite4.1:3b |
fast tier: fragments, wire whispers |
OLLAMA_CHAT_MODEL |
minicpm-v4.5:latest |
chat tier: direct contact, entity minting |
LLM_MAX_CONCURRENCY |
2 |
simultaneous Ollama calls (CPU-only box — keep small) |
LLM_MAX_QUEUE_DEPTH |
8 |
queued calls before "too many seekers" rejection |
LLM_COOLDOWN_SECONDS |
8.0 |
min gap between ambient whispers; user requests preempt |
PIPER_VOICES_DIR |
voices |
voice model directory (relative to backend/) |
DATA_DIR |
data |
generated audio, served at /audio/ (relative to backend/) |
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 | Server response |
|---|---|---|
ping |
— | pong |
set_mode |
mode: wire|evp|radio|ouija|emf |
mode echo; persisted on the session row |
language |
language: en|es |
switches LLM reply language + Piper voice |
summon |
— | status: summoning → entity → greeting utterance (rate-limited: 4/min/user) |
anomaly |
source: wire|evp|radio|emf, frequency, magnitude |
anomaly_ack; auto-summons once the stream can fingerprint (≥3 anomalies), then fragment utterances (rate-limited: 30/min/user) |
question |
text (≤500 chars) |
status: gathering → reply_start → reply_token×N → reply_end → spoken utterance (rate-limited: 6/min/user) |
passive |
enabled: bool |
passive ack; starts/stops the ambient Wire Ghost loop |
Server → client (all frames flow through a single sender task so concurrent producers never interleave):
| Frame | Payload | Meaning |
|---|---|---|
session |
id |
contact session created (sent on connect) |
pong |
— | keepalive reply |
mode |
mode |
mode accepted |
status |
state: attuning|summoning|gathering |
themed loading states |
entity |
is_new, entity (name, epithet, persona, rarity, voice, visual, quotes, contact_count) |
a presence has been matched or minted |
anomaly_ack |
count |
anomalies recorded this session |
anomaly |
source, frequency, magnitude |
server-detected anomaly (currently only the Wire Ghost's ambient loop emits these) |
utterance |
id, kind: greeting|fragment|ambient|reply, text, entity |
words from beyond; audio follows |
audio |
id, url (/audio/<event>.wav) |
TTS + effects chain finished for that utterance |
reply_start / reply_token / reply_end |
token, final id + text |
token-streamed Direct Contact reply |
telemetry |
jitter_bytes_per_s, latency_variance_ms, latency_mean_ms, dns_ms |
live Wire Ghost vitals |
passive |
enabled |
ambient loop state |
error |
code: rate_limited|veil_crowded, message |
themed rate-limit / queue-full notices |
REST (JSON, session-cookie auth where noted; credentials: 'include'):
| Endpoint | Auth | Purpose |
|---|---|---|
POST /auth/register · POST /auth/login · POST /auth/logout · GET /auth/me |
— | argon2 username/password, qm_session cookie (14-day server-side sessions) |
GET /api/codex?rarity=&sort=recent|contacted&limit= |
public | the shared spirit registry |
GET /api/codex/{entity_id} |
public | full dossier: persona, voice profile, sighting count |
GET /api/stats |
public | live veil counters (entities, sessions, utterances, anomalies) |
POST /api/shop/waitlist |
public, rate-limited (5/hr/IP) | join the Ultimate Quantum Box waitlist — {email, interest}, idempotent |
GET /healthz |
— | {"status": "ok"} |
Testing
Backend (pytest, 54 tests across 14 files; requires the
quantumancy_test database — dropped and recreated on every run):
cd backend && source venv/bin/activate
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \
OLLAMA_BASE_URL=http://10.30.20.107:11434 \
python -m pytest -v
Covers: auth + session cookies, rate limiter, config, SPA serving, LLM queue,
telemetry parsing, entity signatures/profiles, prompt construction (incl.
Spanish language clauses), TTS effects chain, Codex REST, the shop waitlist,
and the full WS séance flow (auth, ping/pong, summon/mint/greet, streamed
replies, anomaly attunement, re-contact by signature). One test
(test_unknown_path_serves_spa_index) asserts the SPA fallback and needs
frontend/dist/ to exist first — run npm run build in frontend/ before
the full suite, or it 503s (correctly — that's the "frontend not built" error
path, also tested separately).
Frontend (Vitest + Testing Library — 110 tests across 9 files: the reconnecting VeilSocket, the possession/haunting primitives, the séance reducer, the planchette state machine, the EVP detector core, the Matrix/Graph board views, the FFT, the EMF field-fusion core, and the App shell):
cd frontend && npm test
Hardware-in-the-loop (cannot be unit tested): the WebUSB RTL-SDR sweep, the microphone EVP flow, and the phone EMF sensors each need a manual pass with real hardware/permissions before being called done.
Current State & Known Gaps
Honestly, in order of confidence:
- Solid. Auth, sessions, the Codex, the Wire Ghost, TTS + effects chain, the LLM queue/fallback machinery, EVP's detection math, EMF's motion-fusion math, i18n (en/es), and the haunting layer are all covered by passing tests and use standard, well-supported browser/server APIs. The full backend suite (54/54) and frontend suite (110/110) pass clean.
- Needs a real-hardware pass.
frontend/src/lib/sdr.tsis a from-scratch WebUSB driver for RTL2832U/R820T dongles, written against the public librtlsdr register documentation — the init sequence, PLL tuning math, and I2C repeater writes have never been run against a real device in this environment (no RTL-SDR attached during development). It's explicitly markedHARDWARE PASS REQUIREDthroughout the file and every entry point fails soft, but until someone plugs in a dongle and debugs the register pokes (a logic analyzer orlibrtlsdr -Thelps), treat Spirit Radio as "should work, unverified." It's also Chromium-only (WebUSB), and on Linux the kernel'sdvb_usb_rtl28xxudriver usually needs to be unbound first (the UI surfaces this with a fix suggestion when the claim fails). - Minor drift. The landing page's mode showcase
(
frontend/src/pages/LandingPage.tsx,MODE_ORDER) still lists four modes (radio/evp/wire/ouija) — EMF is fully wired into the séance page, the WebSocket protocol, and the i18n catalogs, but hasn't been added to the landing page's marketing grid yet. - Cosmetic. The frontend build ships one 871 KB JS chunk (Vite warns about it); code-splitting would help first paint but hasn't been done.
- Intentionally unfinished. The Reliquary / Ultimate Quantum Box is a waitlist only — no hardware exists, 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. The UI carries the "this is real" atmosphere; the model instructions carry the fiction.
- Wire Ghost telemetry reads counters and timings only. Packet payloads are never inspected or logged.
- If the Ollama box is dark, every spirit channel degrades to curated or procedurally-generated offline fallbacks — a summoning never visibly fails.
Further Reading
- Design spec:
docs/superpowers/specs/2026-07-20-quantumancy-website-design.md(the live app now exceeds it — see Repo layout above) - Implementation plans 1–7:
docs/superpowers/plans/