Files
qtalker---/README.md
Indiana d2f4c0a993 fix: remove unused SESSION_SECRET config
Session security already comes from a cryptographically random
256-bit token (secrets.token_urlsafe) hashed before storage —
SESSION_SECRET was required config that nothing ever read.
2026-07-21 03:43:08 +00:00

311 lines
15 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, gothic-hacker web experience that lets visitors
**talk to spirits** through four channels, each grounded in a real paranormal-
investigation technique or a real data source. Real anomalies — RF power
spikes, voice-band blips, network jitter — are detected in genuine sensor and
telemetry streams, and a locally-run LLM gives the presence behind them a
voice. A local Piper TTS then renders that voice through a static-choked
spirit-box effects chain.
**This is an interactive horror art installation.** The spirits are fiction;
the static is real. Every "entity" is a persona generated by a language model
on your own hardware. Nothing leaves your network: no cloud APIs, no
third-party inference, no telemetry of ours — only yours.
## The Four Modes
| Mode | Vessel | What actually happens |
|---|---|---|
| **Spirit Radio** | RTL-SDR dongle (WebUSB, Chromium only) | The browser sweeps the FM band (88–108 MHz) computing FFT power per bin; spikes above the rolling noise floor become anomaly events that summon one-word fragments, Ovilus-style. |
| **EVP Listening** | Microphone (`getUserMedia`) | A Web Audio `AnalyserNode` watches the voice band (~300 Hz–3.4 kHz) for brief deviations ≥ 8 dB above the room's rolling silence — the classic "record quiet, review for voices" technique. |
| **The Wire Ghost** | Nothing — works for everyone | The backend samples real, non-content network telemetry from the host (interface throughput jitter, TCP connect latency variance, DNS hesitation) and a fragmented consciousness whispers about it every few seconds. Packet payloads are never inspected — a hard privacy boundary. |
| **Ouija / Direct Contact** | The shared front door | A canvas planchette drifts, then spells the spirit's words letter by letter. Ask a free-text question and the conversational model streams a full reply token by token while the planchette works. |
Every session begins unidentified. The backend fingerprints the session's
anomaly pattern into a **signature**; a matching signature re-contacts an
existing spirit, a new one gets **minted into the Codex** — a publicly
browsable registry of every spirit ever contacted, shared across all users,
with name, epithet, rarity tier, persona lore, sample quotes, voice/visual
profiles, and contact counts.
## 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 REST
API, the `/ws/session` séance WebSocket, and `/audio/*` spirit-voice WAVs.
Postgres holds accounts, sessions, transcripts (events), and the Codex.
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/… and a heavier **chat**
model for Direct Contact 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…".
- **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 and WebUSB.
### 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)
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
routes/ /auth/* and /api/codex, /api/stats
tests/ pytest suite (45 tests)
voices/ Piper .onnx voice models (gitignored — see setup)
data/ generated utterance audio, served at /audio/ (gitignored)
deploy/
quantumancy.service systemd unit template
frontend/
src/lib/ types (WS protocol), VeilSocket, audio player, evp, sdr,
fft, planchette machine
src/state/ AuthProvider, SeanceProvider (reducer + socket wiring)
src/components/ PlanchetteBoard, Transcript, EntityCard, GhostGlyph, …
src/three/ GhostCanvas + GhostScene (custom-shader 3D spirit)
src/i18n/ react-i18next init + en.json / es.json
docs/superpowers/
specs/ the design spec
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`):
```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 and EVP 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` | — | asyncpg connection string, e.g. `postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy` |
| `OLLAMA_BASE_URL` | — | 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` | `mode` echo; persisted on the session row |
| `language` | `language: en\|es` | switches LLM reply language + Piper voice |
| `summon` | — | `status: summoning` → `entity` → greeting `utterance` |
| `anomaly` | `source`, `frequency`, `magnitude` | `anomaly_ack`; auto-summons once the stream can fingerprint (≥3 anomalies), then fragment `utterance`s |
| `question` | `text` (≤500 chars) | `status: gathering` → `reply_start` → `reply_token`×N → `reply_end` → spoken `utterance` |
| `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 |
| `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) |
| `GET /healthz` | — | `{"status": "ok"}` |
## Testing
**Backend** (pytest, 45 tests; 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, and the full WS
séance flow (auth, ping/pong, summon/mint/greet, streamed replies, anomaly
attunement, re-contact by signature).
**Frontend** (Vitest + Testing Library — 71 tests across 6 files: the
planchette state machine, the EVP detector core, the FFT, the reconnecting
VeilSocket, the séance reducer, and the App shell):
```bash
cd frontend && npm test
```
**Hardware-in-the-loop** (cannot be unit tested, per the spec): the WebUSB
RTL-SDR sweep and the microphone EVP flow each need a manual pass with real
hardware/permissions before being called done. Spirit Radio is Chromium-only
and its driver is marked *HARDWARE PASS REQUIRED* in `frontend/src/lib/sdr.ts`.
RTL-SDR dongles are often claimed by the OS kernel driver
(`dvb_usb_rtl28xxu` on Linux) before WebUSB can reach them — Windows users
with Zadig/WinUSB usually work out of the box; Linux/Mac may need a driver
unbind.
## 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 offline
fallbacks — a summoning never visibly fails.
## Further Reading
- Design spec: `docs/superpowers/specs/2026-07-20-quantumancy-website-design.md`
- Implementation plans 1–7: `docs/superpowers/plans/`