From a5ee2d57cd586cd6d310e197598eb3055f588fc5 Mon Sep 17 00:00:00 2001 From: Indiana Date: Wed, 29 Jul 2026 01:25:21 +0000 Subject: [PATCH] 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 --- README.md | 472 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 245 insertions(+), 227 deletions(-) diff --git a/README.md b/README.md index c6ba126..eeaaf9e 100644 --- a/README.md +++ b/README.md @@ -2,81 +2,120 @@ *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. +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`), 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. +`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: Five Ways In +## 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 a spirit — -either re-contacting a Codex entry whose signature matches, or minting a -brand new one. +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 | 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. | +| **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**. | -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. +`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 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 Room Decides -## 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 -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. +- **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. -## The Reliquary +## Rituals, Judgment, and 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. +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 @@ -95,77 +134,67 @@ Two machines on the LAN, no containers anywhere: ├─ Piper TTS + FX │ └───────┬──────────┘ │ LAN - ┌───────────▼─────────┐ - │ Ollama box │ - │ 10.30.20.107:11434 │ - │ CPU-only, 64 GB │ - │ fast + chat models │ + ┌──────────────────┐ ┌───────────▼─────────┐ + │ 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** — 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). +- **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 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 + 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/ 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 + 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 (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. **1. Postgres** (one-time): @@ -196,10 +225,9 @@ 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`): +**4. Piper voices.** `backend/voices/` is gitignored; download each `.onnx` +plus its `.onnx.json` from +[rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices): | id | file | language | character | |---|---|---|---| @@ -220,9 +248,7 @@ 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/`): +**6. Run.** By hand: ```bash 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 ``` -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: +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`): ```bash sudo cp deploy/quantumancy.service /etc/systemd/system/ @@ -241,35 +268,34 @@ 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. +secure context that mic, WebUSB, and motion/magnetometer 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 +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` and are read from the environment -or `.env` (repo root when run via systemd; CWD otherwise). Required: -`DATABASE_URL`, `OLLAMA_BASE_URL`. +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, e.g. `postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy` | -| `OLLAMA_BASE_URL` | — (required) | Ollama REST endpoint, e.g. `http://10.30.20.107:11434` | +| `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: 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/`) | +| `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 @@ -278,125 +304,117 @@ One WebSocket per contact session: `/ws/session`, authenticated by the **Client → server:** -| Frame | Payload | Server response | +| Frame | Payload | Notes | |---|---|---| | `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 | +| `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** (all frames flow through a single sender task so -concurrent producers never interleave): +**Server → client** (single sender task; frames 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 | +| 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** (JSON, session-cookie auth where noted; `credentials: 'include'`): +**REST** (session-cookie auth where noted): | 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 | +| `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, 54 tests across 14 files; requires the -`quantumancy_test` database — dropped and recreated on every run): +**Backend** — pytest, **328 tests**. Requires `quantumancy_test` (dropped +and recreated every run — never run two suites concurrently against it; the +autouse drop/create fixture will fight itself): ```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 \ -python -m pytest -v +python -m pytest -q ``` -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): +**Frontend** — Vitest + Testing Library, **366 tests**, plus a hard i18n +en/es parity gate (`npm run pretest`): ```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. +**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.** 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 Reliquary / Ultimate Quantum Box is a - waitlist only — no hardware exists, no payment is taken, and the page - says so. +- **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. 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. +- 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 -- 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/` +- `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/`