# 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 Armory `/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/session` sé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 themed `veil_crowded` error rather than hanging. - **Cloudflare Tunnel** — managed outside this repo; terminates HTTPS and points at `http://: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 Armory 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): ```bash 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:** ```bash cp .env.example .env # edit .env: set a real SESSION_SECRET, confirm DATABASE_URL and OLLAMA_BASE_URL ``` **3. Backend:** ```bash 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](https://huggingface.co/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:** ```bash 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/`): ```bash 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: ```bash sudo cp deploy/quantumancy.service /etc/systemd/system/ sudo systemctl enable --now quantumancy ``` Then visit `http://: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): ```bash 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`, `SESSION_SECRET`. | 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` | | `SESSION_SECRET` | — (required) | present in `Settings` but not currently read anywhere else in the app — session auth actually runs on random per-login tokens hashed into `auth_sessions` (`backend/app/models/auth_session.py`), not a signing secret. Still required to boot; set it to any random string. | | `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 `utterance`s (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/.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): ```bash 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 \ SESSION_SECRET=test-secret \ 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): ```bash 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.ts` is 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 marked `HARDWARE PASS REQUIRED` throughout the file and every entry point fails soft, but until someone plugs in a dongle and debugs the register pokes (a logic analyzer or `librtlsdr -T` helps), treat Spirit Radio as "should work, unverified." It's also Chromium-only (WebUSB), and on Linux the kernel's `dvb_usb_rtl28xxu` driver 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 Armory / 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/`