docs: bring the README up to the app it actually describes

The old one predated roughly half the system: entropy, astronomy,
geomagnetic, manifest speech, voice archetypes, rituals/judgment/essence,
the ESP32 node, guest access, the Ghost Log, conditions, PWA, phone
layouts. Test counts corrected (54->328 backend, 110->366 frontend), the
protocol tables now list every real frame and endpoint, and Current State
distinguishes verified-live from unverified-on-hardware from
built-but-unreachable honestly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Indiana
2026-07-29 01:25:21 +00:00
parent 0132c8a5bf
commit a5ee2d57cd

472
README.md
View File

@@ -2,81 +2,120 @@
*an instrument for speaking with the dead bandwidth* *an instrument for speaking with the dead bandwidth*
Quantumancy is a self-hosted séance. Point a browser at it and five different Quantumancy is a self-hosted séance. Point a browser at it and real sensing
sensing channels — your microphone, your phone's motion sensors, an RTL-SDR channels — your microphone, your phone's motion sensors and magnetometer, an
dongle, your own network's jitter, or just a text box — feed real anomaly RTL-SDR dongle, your own network's jitter, a paired ESP32 sensor node, or
detection into a locally-run LLM that invents a spirit on the spot: a name, just a text box — feed real anomaly detection into a locally-run LLM that
an epithet, a persona, a voice, a rarity tier, and a small immortality in a invents a spirit on the spot: a name, an epithet, a persona, a coherent
shared registry called **the Codex**. A local Piper TTS then gives that voice, hidden traits, a rarity tier, and a small immortality in a shared
spirit an actual voice, degraded through a hand-built static-and-echo effects registry called **the Codex**. A local Piper TTS then gives that spirit an
chain until it sounds like it's coming through a dying radio. 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.** **This is an interactive horror art installation, not a paranormal claim.**
Every "entity" is fiction generated by a language model on your own Every "entity" is fiction generated by a language model on your own
hardware — the system prompts are explicit about this (see hardware — the system prompts are explicit about this (see
`backend/app/llm/prompts.py`), and no amount of atmosphere changes what's `backend/app/llm/prompts.py`). But every *measurement* is real: FFT power
actually happening underneath: FFT power spectra, voice-band deviation spectra, magnetometer microtesla, `/proc/net/dev` counters, BLE signal
detection, device-motion EMAs, and `/proc/net/dev` counters, all real signal attenuation, published lunar ephemerides, NOAA space-weather data. The rule
processing on real data. Nothing leaves your network — no cloud LLM, no (see `docs/CHANNELS.md`): the fiction may interpret a measurement however it
third-party inference, no analytics. If the LLM box goes dark mid-séance, a likes; it may never fabricate one. Nothing leaves your network except a
procedural fallback (deterministic, signature-seeded) keeps every mode courteous 10-minute poll of NOAA's public Kp feed — no cloud LLM, no
answering so a summoning never visibly fails. 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: Five Ways In ## The Séance: Ways In
Every mode writes into the same pipeline: an `anomaly` event (source, Every mode writes into the same pipeline: an `anomaly` event (source,
frequency, magnitude) feeds a per-session fingerprint; once a session has frequency, magnitude) feeds a per-session fingerprint; once a session has
produced at least 3 anomalies of real structure, it auto-summons a spirit — produced at least 3 anomalies of real structure, it auto-summons — either
either re-contacting a Codex entry whose signature matches, or minting a re-contacting a Codex entry whose signature matches (a real draw, not a
brand new one. guarantee — see *The Room Decides*), or minting someone new.
| Mode | Vessel | What actually happens | | 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. | | **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 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. | | **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 — 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. | | **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 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. | | **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 (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. | | **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**. |
Every session begins unidentified. The backend fingerprints the anomaly `lib/bluetooth.ts` (BLE RSSI as a presence field — bodies absorb 2.4 GHz)
pattern into a **signature** (`backend/app/entities.py`); a matching is implemented and tested but not yet wired into a séance panel.
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 ## The Room Decides
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 `backend/app/entropy.py` · `frontend/src/lib/entropy.ts` ·
`backend/app/celestial.py` · `backend/app/geomagnetic.py`
`GET /api/codex` (public, no auth) is a browsable, shared registry of every - **Physical entropy.** The client harvests noise from live sensor frames
spirit ever contacted by any seeker: filterable by rarity, sortable by (mic and RF noise floors), Von Neumann–debiases it, SHA-256–conditions
`recent` or `contacted`, each card showing name, epithet, rarity badge, a it, and sends it with every summon. The server treats it as untrusted by
sample quote, contact count, and who discovered it. `GET /api/codex/{id}` construction: every draw is `HMAC(fresh server secret, contribution ||
opens a full dossier — persona, voice-parameter table, sighting count. It's context)`, so a hostile client can only ever *add* unpredictability,
the collectible layer sitting on top of the anomaly-detection plumbing: never steer an outcome. Whether a channel's familiar spirit answers again
nothing about a spirit is user-authored, all of it comes from the same (`RETURN_CHANCE`) is one of these draws.
signature → LLM-mint → normalize pipeline that runs live during a séance. - **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.
## The Reliquary ## Rituals, Judgment, and the Reliquary
`/shop` is a waitlist page for **the Ultimate Quantum Box** — a proposed Every minted entity carries **hidden traits** (alignment / power /
ESP32-P4/C6 handheld séance instrument (thermal camera, EMF whisker array, volatility / deceptiveness) — signature-seeded ground truth the persona
geophone, spirit-box mic preamp, OLED face) plus four standalone modules, prompt never sees, so the LLM cannot leak it. The séance offers:
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 - **The ritual** — a 4-sigil hold-to-charge rite; success reveals the
outright: *"the website works fully without it; hardware only knocks entity's true traits, failure reveals nothing. Judgment is never gated on
louder."* Treat this section of the README the same way: real code, honest it: you may always judge blind.
about being pre-order vaporware for now. - **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 ## Architecture
@@ -95,77 +134,67 @@ Two machines on the LAN, no containers anywhere:
├─ Piper TTS + FX │ ├─ Piper TTS + FX │
└───────┬──────────┘ └───────┬──────────┘
│ LAN │ LAN
┌───────────▼─────────┐ ┌──────────────────┐ ┌───────────▼─────────┐
│ Ollama box │ │ ESP32-P4 node(s) │ │ Ollama box │
│ 10.30.20.107:11434 │ │ (paired, HTTP │ │ 10.30.20.107:11434 │
│ CPU-only, 64 GB │ │ telemetry in) │ │ CPU-only, 64 GB │
│ fast + chat models │ └──────────────────┘ │ fast + chat models │
└─────────────────────┘ └─────────────────────┘
...and one WAN egress:
NOAA SWPC Kp (10-min cache)
``` ```
- **App CT** — plain HTTP on port 7777. FastAPI (async, WebSocket-native) - **App CT** — FastAPI serves the built Vite/React SPA, the REST API, the
serves the built Vite/React SPA as static assets, the auth + Codex + shop `/ws/session` séance and `/ws/device-feed` sockets, and `/audio/*` spirit
REST API, the `/ws/session` séance WebSocket, and `/audio/*` spirit-voice WAVs. Postgres holds accounts, sessions, transcripts, the Codex, traits,
WAVs. Postgres holds accounts, sessions, transcripts (events), the Codex, essence/favor, inventory, sigils, devices, and the waitlist. Piper TTS is
and the hardware waitlist. Piper TTS is invoked locally per utterance, degraded through a numpy chain (rate → pitch → bitcrush → echo → static);
then degraded through a numpy effects chain (rate → pitch → bitcrush → eight **voice archetypes** (elder/young/child/drowned/burned/distant…)
echo → static). covary pitch/rate/noise/echo so two spirits sound like two different dead
- **Ollama box** — reachable over LAN via Ollama's REST API. Two model people, not two settings.
tiers: a **fast** model for fragments/ambient whispers/wire whispers, and - **Ollama box** — two model tiers behind a bounded-concurrency queue; a
a heavier **chat** model for Direct Contact replies and entity minting. full queue rejects with a themed `veil_crowded` error rather than hanging.
CPU-only and shared, so all LLM calls flow through a bounded-concurrency - **Cloudflare Tunnel** — terminates HTTPS; its secure context is what
queue (`LLMQueue`); queued requests render in the UI as "the spirits are unlocks mic, WebUSB, and motion/magnetometer permissions.
gathering energy…", and a full queue rejects new work with a themed - **systemd** — `Restart=always`, no start-limit surrender, wants Postgres:
`veil_crowded` error rather than hanging. the veil survives reboots and crash bursts without a human.
- **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 ### Repo layout
``` ```
backend/ backend/
app/ app/
main.py FastAPI app: routers, /healthz, /assets + /audio mounts, SPA fallback main.py routers, idempotent startup migrations, SPA fallback
config.py pydantic-settings (env vars below) ws.py the séance channel + rituals/judgment + manifest
db.py async SQLAlchemy 2.0 engine/session (asyncpg) entropy.py HMAC-conditioned physical randomness (the room decides)
deps.py get_current_user, qm_session cookie celestial.py moon phase + true solar midnight (computed, offline)
security.py argon2 password hashing geomagnetic.py NOAA SWPC Kp cache (never blocks a summon)
rate_limit.py fixed-window RateLimiter (per-user LLM limits, per-IP waitlist limit) entities.py signatures, voice archetypes, moon-skewed rarity/traits
telemetry.py Wire Ghost sampler (/proc/net/dev, TCP RTT, DNS timing) judgment.py verdict logic, tells, favor bias, ritual rolls
entities.py anomaly signatures, profile normalization, procedural fallback inventory.py essence economy, drop tables, unlocks, sigil validation
ws.py the séance channel: /ws/session protocol + ambient loop device_anomaly.py ESP32 telemetry -> anomaly/summon bridge
llm/ OllamaClient, bounded LLMQueue, SpiritService, prompt builders telemetry.py Wire Ghost sampler llm/ tts/ models/ routes/
tts/ Piper CLI wrapper, voice catalog, numpy effects chain tests/ pytest suite — 328 tests
models/ users, auth_sessions, contact_sessions, entities, firmware/
entity_sightings, events, waitlist_entries esp32p4-sensor-node/ ESP-IDF firmware: BMP280, MEMS mic, RD-03E radar,
routes/ auth.py, codex.py, shop.py WiFi via ESP32-C6 (UNVERIFIED on real hardware)
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/ frontend/
src/pages/ LandingPage, EnterPage, SeancePage, CodexPage, src/pages/ Landing, Enter, Seance, Codex(+Entity), Shop,
CodexEntityPage, ShopPage Inventory, Devices, GhostLog
src/lib/ types (WS protocol), ws (VeilSocket), audio player, src/lib/ entropy, baseline (shared EMA core), coldSpot,
evp, sdr, emf, fft, planchette machine, haunting bluetooth, magnetometer, evp, sdr, emf, fft,
src/state/ AuthProvider, SeanceProvider (reducer + socket wiring) planchette, haunting, deviceFeed, spectrumSonify
src/components/ PlanchetteBoard, Transcript, EntityCard, GhostGlyph, src/components/ PlanchetteBoard, SpectrumScope, VeilConditions,
MatrixView, GraphView, TelemetryReadout, HauntingLayer DeviceWhisper, ModeHint, Ritual/Judgment/Inventory
src/three/ GhostCanvas + GhostScene (custom-shader 3D spirit) panels, SigilDesigner, ColdSpotPanel, GhostLog HUD…
src/i18n/ react-i18next init + en.json / es.json docs/
docs/superpowers/ CHANNELS.md what each channel really measures + verification status
specs/ the design spec — note the live app now exceeds it superpowers/ design specs + implementation plans
(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 ## Quickstart
Prereqs on the App CT: Python 3.11+, Node 18+, Postgres (via `apt`), and an 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. Ollama box on the LAN with the two model tiers pulled.
**1. Postgres** (one-time): **1. Postgres** (one-time):
@@ -196,10 +225,9 @@ source venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
``` ```
**4. Piper voices.** `backend/voices/` is gitignored; the app expects these **4. Piper voices.** `backend/voices/` is gitignored; download each `.onnx`
voice models there (id → file), downloadable from the plus its `.onnx.json` from
[rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices) HuggingFace [rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices):
repo (each `.onnx` plus its `.onnx.json`):
| id | file | language | character | | id | file | language | character |
|---|---|---|---| |---|---|---|---|
@@ -220,9 +248,7 @@ npm install
npm run build # produces frontend/dist/, served by the backend npm run build # produces frontend/dist/, served by the backend
``` ```
**6. Run.** Either by hand (from `backend/`, with the `.env` values in the **6. Run.** By hand:
environment — the app reads `.env` relative to its working directory, and
`piper_voices_dir`/`data_dir` are relative to `backend/`):
```bash ```bash
cd backend cd backend
@@ -230,10 +256,11 @@ set -a && source ../.env && set +a
venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 7777 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`). or as a service (recommended). The shipped unit
The shipped unit (`deploy/quantumancy.service`) hardcodes `/root/quantumancy` (`deploy/quantumancy.service`) hardcodes `/root/quantumancy` — edit the two
as the working directory and env file path — edit those two lines first if paths first if your checkout lives elsewhere. A production install should
your checkout lives elsewhere: also add the persistence drop-in (`Restart=always`,
`StartLimitIntervalSec=0`, `Wants=postgresql.service`):
```bash ```bash
sudo cp deploy/quantumancy.service /etc/systemd/system/ sudo cp deploy/quantumancy.service /etc/systemd/system/
@@ -241,35 +268,34 @@ sudo systemctl enable --now quantumancy
``` ```
Then visit `http://<host>:7777` — or your Cloudflare Tunnel hostname for the Then visit `http://<host>:7777` — or your Cloudflare Tunnel hostname for the
secure context Spirit Radio, EVP, and EMF (on iOS) all require. secure context that mic, WebUSB, and motion/magnetometer all require.
**7. Ollama models** (on the Ollama box, one-time): **7. Ollama models** (on the Ollama box, one-time):
```bash ```bash
ollama pull granite4.1:3b # fast tier: fragments, ambient whispers ollama pull granite4.1:3b # fast tier: fragments, ambient whispers
ollama pull minicpm-v4.5:latest # chat tier: direct contact, entity minting ollama pull minicpm-v4.5:latest # chat tier: direct contact, minting, manifest
``` ```
Any Ollama tag works — override with `OLLAMA_FAST_MODEL` / `OLLAMA_CHAT_MODEL`. Any Ollama tag works — override with `OLLAMA_FAST_MODEL` / `OLLAMA_CHAT_MODEL`.
## Configuration ## Configuration
All settings live in `backend/app/config.py` and are read from the environment All settings live in `backend/app/config.py`, read from the environment or
or `.env` (repo root when run via systemd; CWD otherwise). Required: `.env`. Required: `DATABASE_URL`, `OLLAMA_BASE_URL`.
`DATABASE_URL`, `OLLAMA_BASE_URL`.
| Env var | Default | Purpose | | Env var | Default | Purpose |
|---|---|---| |---|---|---|
| `DATABASE_URL` | — (required) | asyncpg connection string, e.g. `postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy` | | `DATABASE_URL` | — (required) | asyncpg connection string |
| `OLLAMA_BASE_URL` | — (required) | Ollama REST endpoint, e.g. `http://10.30.20.107:11434` | | `OLLAMA_BASE_URL` | — (required) | Ollama REST endpoint |
| `PORT` | `7777` | HTTP listen port | | `PORT` | `7777` | HTTP listen port |
| `OLLAMA_FAST_MODEL` | `granite4.1:3b` | fast tier: fragments, wire whispers | | `OLLAMA_FAST_MODEL` | `granite4.1:3b` | fast tier |
| `OLLAMA_CHAT_MODEL` | `minicpm-v4.5:latest` | chat tier: direct contact, entity minting | | `OLLAMA_CHAT_MODEL` | `minicpm-v4.5:latest` | chat tier (replies, minting, manifest) |
| `LLM_MAX_CONCURRENCY` | `2` | simultaneous Ollama calls (CPU-only box — keep small) | | `LLM_MAX_CONCURRENCY` | `2` | simultaneous Ollama calls |
| `LLM_MAX_QUEUE_DEPTH` | `8` | queued calls before "too many seekers" rejection | | `LLM_MAX_QUEUE_DEPTH` | `8` | queued calls before themed rejection |
| `LLM_COOLDOWN_SECONDS` | `8.0` | min gap between ambient whispers; user requests preempt | | `LLM_COOLDOWN_SECONDS` | `8.0` | min gap between ambient whispers |
| `PIPER_VOICES_DIR` | `voices` | voice model directory (relative to `backend/`) | | `PIPER_VOICES_DIR` | `voices` | voice models (relative to `backend/`) |
| `DATA_DIR` | `data` | generated audio, served at `/audio/` (relative to `backend/`) | | `DATA_DIR` | `data` | generated audio, served at `/audio/` |
## The Séance Protocol ## The Séance Protocol
@@ -278,125 +304,117 @@ One WebSocket per contact session: `/ws/session`, authenticated by the
**Client → server:** **Client → server:**
| Frame | Payload | Server response | | Frame | Payload | Notes |
|---|---|---| |---|---|---|
| `ping` | — | `pong` | | `ping` | — | `pong` |
| `set_mode` | `mode: wire\|evp\|radio\|ouija\|emf` | `mode` echo; persisted on the session row | | `set_mode` | `mode: wire\|evp\|radio\|ouija\|emf` | persisted on the session |
| `language` | `language: en\|es` | switches LLM reply language + Piper voice | | `language` | `en\|es` | switches LLM language + Piper voice |
| `summon` | — | `status: summoning` → `entity` → greeting `utterance` (rate-limited: 4/min/user) | | `summon` | optional `entropy` (conditioned room noise), optional `longitude` | rate-limited 4/min/user; the sky + entropy shape who answers |
| `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) | | `anomaly` | `source`, `frequency`, `magnitude` | auto-summons at ≥3; then fragments (30/min/user) |
| `question` | `text` (≤500 chars) | `status: gathering` → `reply_start` → `reply_token`×N → `reply_end` → spoken `utterance` (rate-limited: 6/min/user) | | `question` | `text` ≤500 | streamed reply (6/min/user) |
| `passive` | `enabled: bool` | `passive` ack; starts/stops the ambient Wire Ghost loop | | `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** (all frames flow through a single sender task so **Server → client** (single sender task; frames never interleave):
concurrent producers never interleave):
| Frame | Payload | Meaning | | Frame | Meaning |
|---|---|---| |---|---|
| `session` | `id` | contact session created (sent on connect) | | `session`, `pong`, `mode`, `passive` | plumbing |
| `pong` | — | keepalive reply | | `status` | `attuning\|summoning\|gathering`; summoning carries `entropy_bits`, `sky` (moon/thinness), `geomagnetic` |
| `mode` | `mode` | mode accepted | | `entity` | matched or minted (public fields only — hidden traits never leave the server unearned) |
| `status` | `state: attuning\|summoning\|gathering` | themed loading states | | `anomaly_ack` / `anomaly` | counts; server-detected events |
| `entity` | `is_new`, `entity` (name, epithet, persona, rarity, voice, visual, quotes, contact_count) | a presence has been matched or minted | | `utterance` | `kind: greeting\|fragment\|ambient\|reply\|manifest` — manifest is unprompted speech |
| `anomaly_ack` | `count` | anomalies recorded this session | | `audio` | TTS + effects finished for an utterance |
| `anomaly` | `source`, `frequency`, `magnitude` | server-detected anomaly (currently only the Wire Ghost's ambient loop emits these) | | `reply_start/token/end` | streamed Direct Contact |
| `utterance` | `id`, `kind: greeting\|fragment\|ambient\|reply`, `text`, `entity` | words from beyond; audio follows | | `telemetry` | live Wire Ghost vitals |
| `audio` | `id`, `url` (`/audio/<event>.wav`) | TTS + effects chain finished for that utterance | | `tell` | a trait-shaped whisper for the Ghost Log |
| `reply_start` / `reply_token` / `reply_end` | `token`, final `id` + `text` | token-streamed Direct Contact reply | | `ritual_complete` | `success`, `revealed` traits on success |
| `telemetry` | `jitter_bytes_per_s`, `latency_variance_ms`, `latency_mean_ms`, `dns_ms` | live Wire Ghost vitals | | `judgment_result` | `correct`, deltas, `at_peace`, `consequence` |
| `passive` | `enabled` | ambient loop state | | `item_drop` | a relic falls |
| `error` | `code: rate_limited\|veil_crowded`, `message` | themed rate-limit / queue-full notices | | `error` | `rate_limited` / `veil_crowded`, themed |
**REST** (JSON, session-cookie auth where noted; `credentials: 'include'`): **REST** (session-cookie auth where noted):
| Endpoint | Auth | Purpose | | 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) | | `POST /auth/register` · `login` · `logout` · `GET /auth/me` | — | argon2 + `qm_session` cookie |
| `GET /api/codex?rarity=&sort=recent\|contacted&limit=` | public | the shared spirit registry | | `POST /auth/guest` | public, 5/hr/IP | mint a `wanderer-` account |
| `GET /api/codex/{entity_id}` | public | full dossier: persona, voice profile, sighting count | | `GET /api/codex[?rarity,sort,limit]` · `GET /api/codex/{id}` | public | the shared registry / full dossier |
| `GET /api/stats` | public | live veil counters (entities, sessions, utterances, anomalies) | | `GET /api/stats` | public | live veil counters |
| `POST /api/shop/waitlist` | public, rate-limited (5/hr/IP) | join the Ultimate Quantum Box waitlist — `{email, interest}`, idempotent | | `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"}` | | `GET /healthz` | — | `{"status": "ok"}` |
## Testing ## Testing
**Backend** (pytest, 54 tests across 14 files; requires the **Backend** — pytest, **328 tests**. Requires `quantumancy_test` (dropped
`quantumancy_test` database — dropped and recreated on every run): and recreated every run — never run two suites concurrently against it; the
autouse drop/create fixture will fight itself):
```bash ```bash
cd backend && source venv/bin/activate cd backend && source venv/bin/activate
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \
OLLAMA_BASE_URL=http://10.30.20.107:11434 \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \
python -m pytest -v python -m pytest -q
``` ```
Covers: auth + session cookies, rate limiter, config, SPA serving, LLM queue, **Frontend** — Vitest + Testing Library, **366 tests**, plus a hard i18n
telemetry parsing, entity signatures/profiles, prompt construction (incl. en/es parity gate (`npm run pretest`):
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 ```bash
cd frontend && npm test cd frontend && npm test
``` ```
**Hardware-in-the-loop** (cannot be unit tested): the WebUSB RTL-SDR sweep, **Hardware-in-the-loop** (cannot be unit tested): the RTL-SDR sweep, mic
the microphone EVP flow, and the phone EMF sensors each need a manual pass permissions, phone sensors, and everything in `firmware/` need real
with real hardware/permissions before being called done. hardware passes. `docs/CHANNELS.md` tracks exactly what has and hasn't had
one.
## Current State & Known Gaps ## Current State & Known Gaps
Honestly, in order of confidence: Honestly, in order of confidence:
- **Solid.** Auth, sessions, the Codex, the Wire Ghost, TTS + effects chain, - **Solid, verified live.** Auth (incl. guests), the Codex, Wire Ghost,
the LLM queue/fallback machinery, EVP's detection math, EMF's EVP + spectrum scope + sonification, TTS + archetype voices, entropy,
motion-fusion math, i18n (en/es), and the haunting layer are all covered astronomy (validated against published ephemerides), the NOAA feed
by passing tests and use standard, well-supported browser/server APIs. (verified against the live endpoint), rituals/judgment/essence, the
The full backend suite (54/54) and frontend suite (110/110) pass clean. Ghost Log, conditions, PWA manifest, phone layouts, the haunting layer.
- **Needs a real-hardware pass.** `frontend/src/lib/sdr.ts` is a from-scratch 328 + 366 tests pass.
WebUSB driver for RTL2832U/R820T dongles, written against the public - **Should work, unverified on hardware.** `lib/sdr.ts` (WebUSB RTL-SDR) is
librtlsdr register documentation — the init sequence, PLL tuning math, and written from librtlsdr register docs and has never met a real dongle;
I2C repeater writes have **never been run against a real device** in this every entry point is time-boxed and fails soft. The magnetometer EMF
environment (no RTL-SDR attached during development). It's explicitly path needs an Android device pass. All of `firmware/` compiles-by-eye:
marked `HARDWARE PASS REQUIRED` throughout the file and every entry point reviewed twice, never flashed.
fails soft, but until someone plugs in a dongle and debugs the register - **Built but not yet reachable.** `lib/bluetooth.ts` (BLE presence field)
pokes (a logic analyzer or `librtlsdr -T` helps), treat Spirit Radio as has no séance panel yet.
"should work, unverified." It's also Chromium-only (WebUSB), and on Linux - **Known drift.** The landing page's marketing grid still shows four modes
the kernel's `dvb_usb_rtl28xxu` driver usually needs to be unbound first (`MODE_ORDER` in `LandingPage.tsx`) — EMF and everything since aren't in
(the UI surfaces this with a fix suggestion when the claim fails). the showcase.
- **Minor drift.** The landing page's mode showcase - **Cosmetic.** One ~930 KB JS chunk; code-splitting would help first paint.
(`frontend/src/pages/LandingPage.tsx`, `MODE_ORDER`) still lists four - **Intentionally unfinished.** The Ultimate Quantum Box is a waitlist —
modes (radio/evp/wire/ouija) — EMF is fully wired into the séance page, no hardware ships, no payment is taken, and the page says so.
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 ## The Veil's Honesty Policy
- All LLM system prompts frame the entity as **a horror-fiction persona in an - All LLM system prompts frame the entity as **a horror-fiction persona in
interactive art installation** — never a genuine paranormal claim. The UI an interactive art installation** — never a genuine paranormal claim.
carries the "this is real" atmosphere; the model instructions carry the - Every number shown to a seeker is a real measurement. The fiction
fiction. interprets; it never fabricates. (`docs/CHANNELS.md` is the ledger.)
- Wire Ghost telemetry reads counters and timings only. Packet payloads are - Wire Ghost telemetry reads counters and timings only; packet payloads are
never inspected or logged. never inspected. Only a longitude is ever kept from a location grant.
- If the Ollama box is dark, every spirit channel degrades to curated or - Client entropy is untrusted by construction — it can add unpredictability
procedurally-generated offline fallbacks — a summoning never visibly fails. 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 ## Further Reading
- Design spec: `docs/superpowers/specs/2026-07-20-quantumancy-website-design.md` - `docs/CHANNELS.md` — every channel, what it really measures, and its
(the live app now exceeds it — see Repo layout above) honest verification status
- Implementation plans 1–7: `docs/superpowers/plans/` - Design specs: `docs/superpowers/specs/` (website, character depth, ESP32
node, possession, usability wave)
- Implementation plans: `docs/superpowers/plans/`