Files
qtalker---/README.md
Indiana 761d6171b5 fix: actually strip stale SESSION_SECRET mentions from README
The gap-g-readme merge commit (57a8914) staged this fix but never
re-staged it after editing, so the merge landed with the pre-fix content —
the working tree had the correction but git didn't. No functional change,
just closing the gap between what was intended and what was committed.
2026-07-23 03:32:21 +00:00

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