feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite): - Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits - Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive), Ouija planchette board spelling utterances, transcript with TTS replay, entity dossier, direct contact streaming, passive/active listening - Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact - Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete - State: VeilSocket (reconnect/backoff), seance reducer, auth context - 72 vitest tests green; served by FastAPI at :7777 Docs: README + as-built plans 3-7
This commit is contained in:
312
README.md
Normal file
312
README.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# 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;"
|
||||
sudo -u postgres psql -c "CREATE DATABASE quantumancy_test OWNER quantumancy;"
|
||||
```
|
||||
|
||||
> `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: set a real SESSION_SECRET, 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`, `SESSION_SECRET`.
|
||||
|
||||
| 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` |
|
||||
| `SESSION_SECRET` | — | random 64-char string (session cookie signing) |
|
||||
| `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 \
|
||||
SESSION_SECRET=test-secret \
|
||||
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/`
|
||||
Reference in New Issue
Block a user