Files
qtalker---/README.md
Indiana e02aecee65 test: check the app in a real browser, because the suites cannot
Every serious defect this session survived a green suite. The sharpest:
THREE.WebGLRenderer throws on a device with no WebGL, the exception escaped
its effect, React unmounted the whole root, and every page rendered blank.
432 frontend tests passed throughout, because jsdom never builds a WebGL
context and nothing exercised a real page in a real browser.

scripts/smoke.py closes that blind spot. It drives Chrome over the DevTools
Protocol — no framework, no npm dependency — and checks each of the 13
routes for the four failures that have actually happened here: a page that
renders nothing, horizontal scroll on a phone, an input under 16px (iOS
zooms on focus and never zooms back), and any uncaught error during load.
Non-zero exit, so it can gate a deploy.

All four checks are PROVEN to fire, by pointing the script at a page built
with each defect on purpose — which is how I found that my own
horizontal-scroll check was broken. It compared scrollWidth against
window.innerWidth, but under Chrome's mobile emulation innerWidth expands to
the CONTENT width when a page overflows: 1200 on a 390px viewport, so the
difference was always 0 and the check could never fire. It reported "ok" on
a deliberately 1200px-wide page. Now compares against
documentElement.clientWidth, which stays at the true 390, and reports
"H-SCROLL +810px" on that same page.

Worth stating plainly: the earlier clean sweep I reported was measured with
that broken check, so its horizontal-scroll result was meaningless. Re-run
with the fix, all 13 routes still pass at 390x844 and 1440x900 — but that is
the run that counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:57:37 +00:00

456 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QUANTUMANCY
*an instrument for speaking with the dead bandwidth*
Quantumancy is a self-hosted séance. Point a browser at it and real sensing
channels — your microphone, your phone's motion sensors and magnetometer, an
RTL-SDR dongle, your own network's jitter, a paired ESP32 sensor node, or
just a text box — feed real anomaly detection into a locally-run LLM that
invents a spirit on the spot: a name, an epithet, a persona, a coherent
voice, hidden traits, a rarity tier, and a small immortality in a shared
registry called **the Codex**. A local Piper TTS then gives that spirit an
actual voice, degraded through a hand-built static-and-echo effects chain
until it sounds like it's coming through a dying radio.
And the physical world genuinely participates: every summon carries entropy
harvested from your room's own noise floors, tonight's **real moon phase**
skews who answers, NOAA's **measured geomagnetic K-index** thins the veil,
and unprompted speech is token-sampled with a seed derived from the room —
the room literally selects the words.
**This is an interactive horror art installation, not a paranormal claim.**
Every "entity" is fiction generated by a language model on your own
hardware — the system prompts are explicit about this (see
`backend/app/llm/prompts.py`). But every *measurement* is real: FFT power
spectra, magnetometer microtesla, `/proc/net/dev` counters, BLE signal
attenuation, published lunar ephemerides, NOAA space-weather data. The rule
(see `docs/CHANNELS.md`): the fiction may interpret a measurement however it
likes; it may never fabricate one. Nothing leaves your network except a
courteous 10-minute poll of NOAA's public Kp feed — no cloud LLM, no
analytics. If the LLM box goes dark mid-séance, a procedural fallback keeps
every mode answering so a summoning never visibly fails.
## The Séance: Ways In
Every mode writes into the same pipeline: an `anomaly` event (source,
frequency, magnitude) feeds a per-session fingerprint; once a session has
produced at least 3 anomalies of real structure, it auto-summons — either
re-contacting a Codex entry whose signature matches (a real draw, not a
guarantee — see *The Room Decides*), or minting someone new.
| Mode | Vessel | What actually happens |
|---|---|---|
| **Wire Ghost** | Nothing — works for everyone | Real, non-content network telemetry from the host: byte-counter jitter (`/proc/net/dev`), TCP connect-latency variance, DNS timing, against a rolling baseline. Payloads are never inspected — a hard privacy boundary. Passive listening lets the line murmur on its own. |
| **EVP Listening** | Microphone (`getUserMedia`) | A Web Audio `AnalyserNode` watches the voice band (~300 Hz–3.4 kHz) for deviations ≥ 8 dB over the room's rolling noise floor — live spectrum scope, audible anomalies, and every frame feeds the entropy pool. Works everywhere, including iPhone. |
| **Spirit Radio** | RTL-SDR dongle (WebUSB, Chromium only) | The browser drives the dongle directly, sweeping FM 88–108 MHz, computing power spectra against a rolling floor. Every USB call is time-boxed (`withTimeout`) so a sulking dongle errors instead of hanging silently. |
| **Ouija / Direct Contact** | The shared front door | A canvas planchette wanders the full board, brushes letters, visits YES/NO, and rests on GOODBYE every few minutes; questions stream back token by token while it spells. |
| **EMF / Field Meter** | A phone | Where the Generic Sensor API exists (Chromium/Android), the **actual magnetometer** in microtesla — a true EMF meter, baseline-subtracting Earth's constant field. Everywhere else (including iOS), the motion-fusion fallback: acceleration deviation + orientation angular velocity. Both are real physics; `lib/baseline.ts` gives them one shared rolling-EMA core. |
| **The Vessel** | ESP32-P4 sensor node (LAN) | Paired hardware POSTs temperature/pressure/mic/radar readings; the Cold Spot Detector and the séance's DeviceWhisper panel render them live, and hardware anomalies can trigger a summon in an open séance. Firmware in `firmware/` — written and reviewed, **never yet flashed to a real board**. |
`lib/bluetooth.ts` (BLE RSSI as a presence field — bodies absorb 2.4 GHz)
is implemented and tested but not yet wired into a séance panel.
## The Room Decides
`backend/app/entropy.py` · `frontend/src/lib/entropy.ts` ·
`backend/app/celestial.py` · `backend/app/geomagnetic.py`
- **Physical entropy.** The client harvests noise from live sensor frames
(mic and RF noise floors), Von Neumann–debiases it, SHA-256–conditions
it, and sends it with every summon. The server treats it as untrusted by
construction: every draw is `HMAC(fresh server secret, contribution ||
context)`, so a hostile client can only ever *add* unpredictability,
never steer an outcome. Whether a channel's familiar spirit answers again
(`RETURN_CHANCE`) is one of these draws.
- **Real astronomy.** Moon phase from orbital mechanics (validated against
published ephemeris dates, not against itself) and true solar midnight
from the seeker's longitude (only the longitude is kept, never a full
coordinate). A full moon roughly **triples** rare/mythic mint odds — a
skew, never a gate — nudges hidden traits stronger and stranger, and
erodes a familiar spirit's claim on its channel so strangers push
through on thin nights.
- **Real space weather.** NOAA SWPC's planetary K-index, cached 10 minutes,
never blocking: a geomagnetic storm measurably thins the veil, and the
entity is told `Kp 6.33 — moderate geomagnetic storm` as a fact of its
room.
- **Generation from nothing.** `SpiritService.manifest()` produces
unprompted speech: the prompt contains no seeker input at all — only
measured room state — and Ollama's sampling `seed` derives from the
room's entropy. Change the noise, get different words. Rendered in the
transcript as an intrusion (violet edge), not a reply.
## Rituals, Judgment, and the Reliquary
Every minted entity carries **hidden traits** (alignment / power /
volatility / deceptiveness) — signature-seeded ground truth the persona
prompt never sees, so the LLM cannot leak it. The séance offers:
- **The ritual** — a 4-sigil hold-to-charge rite; success reveals the
entity's true traits, failure reveals nothing. Judgment is never gated on
it: you may always judge blind.
- **Judgment** — trust / banish / test / cross over. Correct calls pay
essence and favor; wrongly trusting a demon sharpens the haunting,
wrongly banishing a benevolent spirit wounds it, and a correctly-judged
stuck spirit **crosses over**: at peace, permanently retired from its
channel, its signature freed for someone new.
- **The Reliquary** — essence (earned, never bought), item drops on
milestone moments, purchasable unlocks, and hand-drawn sigils.
`/shop` remains a waitlist for the **Ultimate Quantum Box** hardware —
email capture only, no payment taken, and the page says so.
A **Ghost Log** HUD whispers entity tells app-wide, and `/log` is the
recallable record: your last séances with entity, counts, and echo lines.
## Coming In From the Cold
- **Guests:** `POST /auth/guest` mints a real `wanderer-xxxx` account and a
normal session — the front door (`/enter`) offers it as *"slip through as
a wanderer"*, and the séance nudges wanderers to claim a name so their
codex outlives the mist.
- **Conditions:** `GET /api/conditions` (and the séance's side-column
strip) surfaces tonight's moon, veil thinness, and geomagnetic state.
- **First contact:** each mode shows one in-fiction hint if its sensor sits
unused, dismissible forever; error copy redirects instead of dead-ending
(no mic? "the board needs no ear").
- **PWA:** a `manifest.webmanifest` makes it installable to a home screen
(standalone, no service worker — an offline séance is meaningless).
- **Phones:** dedicated ≤560px and ≤380px layouts, 44px touch targets,
16px inputs (so iOS Safari doesn't zoom-and-stick).
## Architecture
Two machines on the LAN, no containers anywhere:
```
┌────────────────────────────────────────────┐
seekers ──HTTPS──▶ Cloudflare Tunnel ──HTTP──▶ App CT :7777 │
(external machine, FastAPI (uvicorn, │
terminates TLS) Python venv, │
systemd service) │
│ │
├─ serves built │
│ React SPA │
├─ Postgres (apt) │
├─ Piper TTS + FX │
└───────┬──────────┘
│ LAN
┌──────────────────┐ ┌───────────▼─────────┐
│ ESP32-P4 node(s) │ │ Ollama box │
│ (paired, HTTP │ │ 10.30.20.107:11434 │
│ telemetry in) │ │ CPU-only, 64 GB │
└──────────────────┘ │ fast + chat models │
└─────────────────────┘
...and one WAN egress:
NOAA SWPC Kp (10-min cache)
```
- **App CT** — FastAPI serves the built Vite/React SPA, the REST API, the
`/ws/session` séance and `/ws/device-feed` sockets, and `/audio/*` spirit
WAVs. Postgres holds accounts, sessions, transcripts, the Codex, traits,
essence/favor, inventory, sigils, devices, and the waitlist. Piper TTS is
degraded through a numpy chain (rate → pitch → bitcrush → echo → static);
eight **voice archetypes** (elder/young/child/drowned/burned/distant…)
covary pitch/rate/noise/echo so two spirits sound like two different dead
people, not two settings.
- **Ollama box** — two model tiers behind a bounded-concurrency queue; a
full queue rejects with a themed `veil_crowded` error rather than hanging.
- **Cloudflare Tunnel** — terminates HTTPS; its secure context is what
unlocks mic, WebUSB, and motion/magnetometer permissions.
- **systemd** — `Restart=always`, no start-limit surrender, wants Postgres:
the veil survives reboots and crash bursts without a human.
### Repo layout
```
backend/
app/
main.py routers, idempotent startup migrations, SPA fallback
ws.py the séance channel + rituals/judgment + manifest
entropy.py HMAC-conditioned physical randomness (the room decides)
celestial.py moon phase + true solar midnight (computed, offline)
geomagnetic.py NOAA SWPC Kp cache (never blocks a summon)
entities.py signatures, voice archetypes, moon-skewed rarity/traits
judgment.py verdict logic, tells, favor bias, ritual rolls
inventory.py essence economy, drop tables, unlocks, sigil validation
device_anomaly.py ESP32 telemetry -> anomaly/summon bridge
telemetry.py Wire Ghost sampler llm/ tts/ models/ routes/
tests/ pytest suite — 328 tests
firmware/
esp32p4-sensor-node/ ESP-IDF firmware: BMP280, MEMS mic, RD-03E radar,
WiFi via ESP32-C6 (UNVERIFIED on real hardware)
frontend/
src/pages/ Landing, Enter, Seance, Codex(+Entity), Shop,
Inventory, Devices, GhostLog
src/lib/ entropy, baseline (shared EMA core), coldSpot,
bluetooth, magnetometer, evp, sdr, emf, fft,
planchette, haunting, deviceFeed, spectrumSonify
src/components/ PlanchetteBoard, SpectrumScope, VeilConditions,
DeviceWhisper, ModeHint, Ritual/Judgment/Inventory
panels, SigilDesigner, ColdSpotPanel, GhostLog HUD…
docs/
CHANNELS.md what each channel really measures + verification status
superpowers/ design specs + implementation plans
```
## Quickstart
Prereqs on the App CT: Python 3.11+, Node 18+, Postgres (`apt`), and an
Ollama box on the LAN with the two model tiers pulled.
**1. Postgres** (one-time):
```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: 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; download each `.onnx`
plus its `.onnx.json` from
[rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices):
| id | file | language | character |
|---|---|---|---|
| `lessac` | `en_US-lessac-low.onnx` | en | a measured American woman (default) |
| `amy` | `en_US-amy-low.onnx` | en | a soft American woman |
| `ryan` | `en_US-ryan-low.onnx` | en | a deep American man |
| `alan` | `en_GB-alan-low.onnx` | en | a low British man |
| `hfc_male` | `en_US-hfc_male-medium.onnx` | en | a worn male voice |
| `hfc_female` | `en_US-hfc_female-medium.onnx` | en | a worn female voice |
| `davefx` | `es_ES-davefx-medium.onnx` | es | una voz masculina grave (ES default) |
| `ald` | `es_MX-ald-medium.onnx` | es | una voz masculina seca |
**5. Frontend:**
```bash
cd frontend
npm install
npm run build # produces frontend/dist/, served by the backend
```
**6. Run.** By hand:
```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). 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/
sudo systemctl enable --now quantumancy
```
Then visit `http://<host>:7777` — or your Cloudflare Tunnel hostname for the
secure context that mic, WebUSB, and motion/magnetometer all require.
**7. Ollama models** (on the Ollama box, one-time):
```bash
ollama pull granite4.1:3b # fast tier: fragments, ambient whispers
ollama pull minicpm-v4.5:latest # chat tier: direct contact, minting, manifest
```
Any Ollama tag works — override with `OLLAMA_FAST_MODEL` / `OLLAMA_CHAT_MODEL`.
## Configuration
All settings live in `backend/app/config.py`, read from the environment or
`.env`. Required: `DATABASE_URL`, `OLLAMA_BASE_URL`.
| Env var | Default | Purpose |
|---|---|---|
| `DATABASE_URL` | — (required) | asyncpg connection string |
| `OLLAMA_BASE_URL` | — (required) | Ollama REST endpoint |
| `PORT` | `7777` | HTTP listen port |
| `OLLAMA_FAST_MODEL` | `granite4.1:3b` | fast tier |
| `OLLAMA_CHAT_MODEL` | `minicpm-v4.5:latest` | chat tier (replies, minting, manifest) |
| `LLM_MAX_CONCURRENCY` | `2` | simultaneous Ollama calls |
| `LLM_MAX_QUEUE_DEPTH` | `8` | queued calls before themed rejection |
| `LLM_COOLDOWN_SECONDS` | `8.0` | min gap between ambient whispers |
| `PIPER_VOICES_DIR` | `voices` | voice models (relative to `backend/`) |
| `DATA_DIR` | `data` | generated audio, served at `/audio/` |
## The Séance Protocol
One WebSocket per contact session: `/ws/session`, authenticated by the
`qm_session` cookie (close code `4401` otherwise). JSON frames both ways.
**Client → server:**
| Frame | Payload | Notes |
|---|---|---|
| `ping` | — | `pong` |
| `set_mode` | `mode: wire\|evp\|radio\|ouija\|emf` | persisted on the session |
| `language` | `en\|es` | switches LLM language + Piper voice |
| `summon` | optional `entropy` (conditioned room noise), optional `longitude` | rate-limited 4/min/user; the sky + entropy shape who answers |
| `anomaly` | `source`, `frequency`, `magnitude` | auto-summons at ≥3; then fragments (30/min/user) |
| `question` | `text` ≤500 | streamed reply (6/min/user) |
| `passive` | `enabled` | ambient Wire Ghost loop |
| `ritual_start` / `ritual_step` | `step` | 4 steps complete an attempt (6/min/user) |
| `judgment` | `verdict: trust\|banish\|test\|cross_over` | 10/min/user |
**Server → client** (single sender task; frames never interleave):
| Frame | Meaning |
|---|---|
| `session`, `pong`, `mode`, `passive` | plumbing |
| `status` | `attuning\|summoning\|gathering`; summoning carries `entropy_bits`, `sky` (moon/thinness), `geomagnetic` |
| `entity` | matched or minted (public fields only — hidden traits never leave the server unearned) |
| `anomaly_ack` / `anomaly` | counts; server-detected events |
| `utterance` | `kind: greeting\|fragment\|ambient\|reply\|manifest` — manifest is unprompted speech |
| `audio` | TTS + effects finished for an utterance |
| `reply_start/token/end` | streamed Direct Contact |
| `telemetry` | live Wire Ghost vitals |
| `tell` | a trait-shaped whisper for the Ghost Log |
| `ritual_complete` | `success`, `revealed` traits on success |
| `judgment_result` | `correct`, deltas, `at_peace`, `consequence` |
| `item_drop` | a relic falls |
| `error` | `rate_limited` / `veil_crowded`, themed |
**REST** (session-cookie auth where noted):
| Endpoint | Auth | Purpose |
|---|---|---|
| `POST /auth/register` · `login` · `logout` · `GET /auth/me` | — | argon2 + `qm_session` cookie |
| `POST /auth/guest` | public, 5/hr/IP | mint a `wanderer-` account |
| `GET /api/codex[?rarity,sort,limit]` · `GET /api/codex/{id}` | public | the shared registry / full dossier |
| `GET /api/stats` | public | live veil counters |
| `GET /api/conditions[?lon]` | public | moon, veil thinness, geomagnetic |
| `GET /api/seances/recent` | auth | the Ghost Log: last 12 sessions + echoes |
| `GET/POST /api/device` · `POST /api/device/telemetry` · WS `/ws/device-feed` | auth / device token | ESP32 pairing + live readings |
| `GET /api/inventory/{unlocks,items,sigils}` · `POST /api/inventory/unlocks/{key}` · `POST /api/inventory/sigils` | auth | the Reliquary |
| `POST /api/shop/waitlist` | public, 5/hr/IP | hardware waitlist |
| `GET /healthz` | — | `{"status": "ok"}` |
## Testing
**Backend** — pytest, **400 tests**. Pass your NORMAL `DATABASE_URL`;
`tests/conftest.py` derives the test database itself by appending `_test` to
the name, so the command below creates and uses `quantumancy_test`. That
database is dropped and recreated every run — never run two suites
concurrently against it; the autouse drop/create fixture will fight itself.
```bash
cd backend && source venv/bin/activate
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy \
OLLAMA_BASE_URL=http://10.30.20.107:11434 \
python -m pytest -q
```
Do **not** point `DATABASE_URL` at `quantumancy_test` here — the suffix is
appended, not checked for, so you would get `quantumancy_test_test`, which
does not exist, and all 400 tests error at setup. To use a database that
isn't `<your db>_test`, set `TEST_DATABASE_URL` explicitly instead; its name
must still end in `_test`.
Two guards in `conftest.py` make it safe to name the live database here: the
derived URL must differ from `DATABASE_URL`, **and** the derived database
name must end in `_test`. The second exists because the first alone is
defeated by `localhost` vs `127.0.0.1` addressing the same database. This
matters — an early version of the suite ran `drop_all` against production
and destroyed live séance data.
**Frontend** — Vitest + Testing Library, **385 tests**, plus a hard i18n
en/es parity gate (`npm run pretest`):
```bash
cd frontend && npm test
```
**Browser smoke** (`scripts/smoke.py`) — renders every route in a real
Chrome and checks four things jsdom structurally cannot see:
```bash
chrome --headless --remote-debugging-port=9222 &
backend/venv/bin/python scripts/smoke.py # 390x844, live site
backend/venv/bin/python scripts/smoke.py --width 1440 # desktop
```
Run it after any UI change and before any deploy; it exits non-zero on
failure. **This is not optional belt-and-braces.** Both suites were fully
green while `new THREE.WebGLRenderer()` threw on any device without WebGL,
the exception escaped its effect, and React unmounted the entire root —
every page a blank screen, no modes, no ask field, nothing. 432 frontend
tests could not see it because jsdom never builds a WebGL context. This
script catches that, plus horizontal scroll on a phone, inputs under 16px
(which make iOS zoom in and never back out), and any uncaught error during
load. All four are proven to fire, by pointing it at a page that has each
defect on purpose.
**Hardware-in-the-loop** (cannot be unit tested): the RTL-SDR sweep, mic
permissions, phone sensors, and everything in `firmware/` need real
hardware passes. `docs/CHANNELS.md` tracks exactly what has and hasn't had
one.
## Current State & Known Gaps
Honestly, in order of confidence:
- **Solid, verified live.** Auth (incl. guests), the Codex, Wire Ghost,
EVP + spectrum scope + sonification, TTS + archetype voices, entropy,
astronomy (validated against published ephemerides), the NOAA feed
(verified against the live endpoint), rituals/judgment/essence, the
Ghost Log, conditions, PWA manifest, phone layouts, the haunting layer.
328 + 366 tests pass.
- **Should work, unverified on hardware.** `lib/sdr.ts` (WebUSB RTL-SDR) is
written from librtlsdr register docs and has never met a real dongle;
every entry point is time-boxed and fails soft. The magnetometer EMF
path needs an Android device pass. All of `firmware/` compiles-by-eye:
reviewed twice, never flashed.
- **Built but not yet reachable.** `lib/bluetooth.ts` (BLE presence field)
has no séance panel yet.
- **Known drift.** The landing page's marketing grid still shows four modes
(`MODE_ORDER` in `LandingPage.tsx`) — EMF and everything since aren't in
the showcase.
- **Cosmetic.** One ~930 KB JS chunk; code-splitting would help first paint.
- **Intentionally unfinished.** The Ultimate Quantum Box is a waitlist —
no hardware ships, no payment is taken, and the page says so.
## The Veil's Honesty Policy
- All LLM system prompts frame the entity as **a horror-fiction persona in
an interactive art installation** — never a genuine paranormal claim.
- Every number shown to a seeker is a real measurement. The fiction
interprets; it never fabricates. (`docs/CHANNELS.md` is the ledger.)
- Wire Ghost telemetry reads counters and timings only; packet payloads are
never inspected. Only a longitude is ever kept from a location grant.
- Client entropy is untrusted by construction — it can add unpredictability
to a draw, never steer one.
- If the Ollama box is dark, every channel degrades to procedural fallbacks
— a summoning never visibly fails.
## Further Reading
- `docs/CHANNELS.md` — every channel, what it really measures, and its
honest verification status
- Design specs: `docs/superpowers/specs/` (website, character depth, ESP32
node, possession, usability wave)
- Implementation plans: `docs/superpowers/plans/`