Indiana bbcbaa0a36 feat: shared spectrum scope with audible anomalies, from real sources only
Replaces two ad-hoc canvases (a 256x110 fixed-size waterfall in the radio
panel, a bar-graph in the EVP panel) with one source-agnostic SpectrumScope
that renders any binned dB spectrum, plus sonification so anomalies can be
heard rather than watched.

Sources are real measured data only. The RTL-SDR path is genuine RF; the
EVP path is a genuine AnalyserNode FFT of the device microphone. The ESP32
deliberately does NOT feed this: its firmware reports four scalars
(temperature, pressure, evp level, presence) and has no spectrum at all,
and device_anomaly.frequency_for_sensor_type() invents a per-sensor-type
frequency for the fiction — neither is a real spectrum, so neither is
plotted as one.

SpectrumScope draws three layers because each answers a different question:
the live trace (what is happening now), a decaying peak-hold (what was
strongest recently, so a transient survives a glance away), and a waterfall
(what the last minute looked like, where a steady carrier separates from a
one-off burst). Anomaly markers flare at their frequency and fade over
~2.6s, so a spike already gone from the trace still says where to look.

Frames reach the scope through a ref, not a prop. Routing 60Hz frames
through React state re-renders the panel and the scope on every frame on
top of the rAF loop that actually draws — measurably the wrong call on a
phone. The EVP producer double-buffers into two fixed Float64Arrays so a
frame costs zero allocation.

spectrumSonify maps band position to pitch exponentially, so equal
fractions of the band are equal musical intervals (a linear Hz map crams
the bottom half into one indistinguishable octave), and magnitude to
gain via sqrt so faint hits stay audible. Pings are throttled to 90ms
because a busy band otherwise smears into a buzz that conveys nothing.
Every entry point no-ops rather than throwing when audio is unavailable —
a dead speaker must never take down the scope drawing the data.

Audio requires an explicit gesture (ListenToggle), because iOS keeps any
context created outside a touch handler permanently suspended.

Also exposes EvpListener.sampleRate/nyquistHz and labels the EVP axis from
the real hardware rate. The old code assumed 48kHz; Bluetooth headsets and
some Android inputs hand back 44.1k or 16k, which mislabelled the spectrum
by nearly an octave.

Mobile: DPR-aware canvases, ResizeObserver, 44px touch targets,
touch-action so dragging the scope pans the page, reduced-motion honoured,
crowded axis ticks dropped under 560px.

329 frontend tests pass (18 new); i18n en/es parity gate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 00:26:54 +00:00

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 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.

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 Reliquary 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):

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:

cp .env.example .env
# edit .env: confirm DATABASE_URL and OLLAMA_BASE_URL

3. Backend:

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 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:

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/):

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:

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):

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 utterances (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):

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):

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 Reliquary / 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/
Description
talking to spirits
Readme 2.7 MiB
Languages
TypeScript 46.3%
Python 32.6%
CSS 13%
C 7.3%
JavaScript 0.3%
Other 0.4%