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
12 KiB
Quantumancy Plan 6/7: Codex & Entity Persistence — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Give spirits memory (spec §4): fingerprint each session's anomaly stream into a signature, match it against the Codex of known entities, mint new ones with a full persona when no match exists, and expose the whole registry through public REST endpoints — the primary multi-user/community hook.
Architecture: Hybrid persistence per spec §4. app.entities computes signatures (pure, deterministic) and coerces any profile — LLM-minted or procedurally generated — into the exact shape the DB and frontend expect. SpiritService.mint_profile asks the chat-tier model for a JSON persona and falls back to a signature-deterministic procedural profile when the box is dark. The summon flow in app.ws (Plan 3) performs match-or-mint, links sessions via entity_sightings, and bumps contact_count. app.routes.codex serves the registry publicly: list (filterable by rarity, sortable), detail (with sighting count), and live veil stats for the landing page.
Tech Stack: FastAPI, SQLAlchemy 2.0 (JSONB profiles), hashlib (signatures), the Plan 2 LLM queue, and Plan 3's séance channel.
Global Constraints
- A summoning never visibly fails. Every failure mode of the LLM path (queue full, HTTP error, unparseable JSON, missing keys) degrades to
fallback_profile/ curated defaults. No 500s from minting. - Determinism where it matters: the same anomaly pattern must fingerprint to the same signature (re-contact works), and
fallback_profile(signature)must be reproducible for a given signature. - Normalization is mandatory. Nothing reaches
Entityrows or the frontend without passingnormalize_profile: rarity clamped tocommon|uncommon|rare|mythic, form towisp|banshee|fairy|shade, voice ids to installed Piper voices, pitch/rate/noise/echo/hue clamped to the effects chain's ranges. - The Codex is public (spec §4): no auth on
GET /api/codex*orGET /api/stats. It still writes nothing — reads only. - Prompt framing per spec §4:
MINT_SYSTEMis fiction framing ("interactive horror art installation"), never a paranormal claim.
Plan Series
This is 6 of 7 plans implementing the Quantumancy website spec (docs/superpowers/specs/2026-07-20-quantumancy-website-design.md). Plan 3 wires the summon flow that consumes this plan's machinery.
- Foundation & Auth (complete)
- Frontend, LLM & Realtime Pipeline (complete)
- Wire Ghost mode + Ouija/Planchette UI (complete)
- EVP Listening mode (complete)
- Spirit Radio mode (implementation complete; hardware pass pending)
- Codex & entity persistence (this plan)
- Internationalization (EN/ES)
Task 1: Entity & Sighting Models
Files:
- Create:
backend/app/models/entity.py - Create:
backend/app/models/entity_sighting.py - Modify:
backend/app/models/__init__.py - Modify:
backend/app/models/contact_session.py(entity_idFK — landed with Plan 3 Task 2)
Interfaces:
-
Produces:
RARITY_TIERS = ("common", "uncommon", "rare", "mythic");Entity—id,name(unique, indexed),epithet,persona(Text),rarity_tier(indexed),signature(unique, indexed),voice_profile/visual_profile(JSONB),sample_quotes(JSONB list),contact_count,discovered_by(FK users, nullable),discovered_at;EntitySighting— join row (entity_id,session_id,user_id,seen_at, both FKs indexed). -
Step 1: Write the models — as above;
signatureunique+indexed because it is the match key,nameunique because the Codex addresses spirits by name (collisions suffixed by the summon flow, Plan 3). -
Step 2: Verify — metadata creates cleanly; covered by every later test that inserts entities (
tests/test_codex.py).
Task 2: Signatures & Profile Normalization
Files:
- Create:
backend/app/entities.py - Test:
backend/tests/test_entities.py
Interfaces:
-
Produces:
MIN_ANOMALIES_FOR_SIGNATURE = 3;signature_from_anomalies(anomalies: list[dict]) -> str | None;fallback_signature(seed: str) -> str;parse_mint_response(text: str) -> dict | None;normalize_profile(profile: dict, signature: str) -> dict;fallback_profile(signature: str) -> dict. -
Step 1: Write the failing tests (6 tests)
Signature needs ≥3 anomalies (else None); signature deterministic for the same pattern; parse_mint_response extracts a JSON object from chatty LLM output ("Sure! Here you go: {...}") and rejects garbage/nameless objects; normalize_profile fills and clamps (bogus rarity → common, bogus form → a real form, pitch 99 → within ±6, non-string quotes dropped); fallback_profile deterministic and valid.
- Step 2: Implement signatures
signature_from_anomalies buckets frequencies by decimal digit count (MHz radio and Hz audio land in the same log-scale band space — magnitude ordering is what matters, not the unit), sorts the last 16 magnitudes to 1-decimal, and SHA1s the pattern to 16 hex chars. fallback_signature(seed) is the same digest over "ambient:<seed>" for anomaly-thin sessions (pure chat still gets a stable identity).
- Step 3: Implement normalization & the procedural fallback
normalize_profile clamps every field into range (pitch ±6 semitones, rate 0.8–1.15, noise 0.01–0.08, echo 0–0.5, hue 0–360), validates voice_id against installed voices (EN ids plus the two Spanish ids), trims name/epithet/persona/quotes to their column budgets, and fills gaps from a Random("norm:<signature>") so defaults are per-spirit stable. fallback_profile composes gothic names from part lists ("Ash" + "moor"), an epithet ("the Static Widow", …), a persona template, weighted rarity (55/30/12/3), and two quotes from the curated bank — all seeded by "fallback:<signature>".
-
Step 4: Verify —
tests/test_entities.pyPASS (6/6). -
Step 5: Commit — in
b9110f4(spirit engine).
Task 3: LLM Minting
Files:
- Modify:
backend/app/llm/prompts.py(MINT_SYSTEM,MINT_PROMPT,mint_prompt) - Modify:
backend/app/llm/service.py(SpiritService.mint_profile)
Interfaces:
-
Consumes: chat-tier model via the bounded queue (Plan 2), Task 2's parsers/normalizers.
-
Produces:
mint_profile(signature, channel, anomalies, language = "en") -> dict— always a normalized profile. -
Step 1: The mint prompt
MINT_PROMPT requests exactly one JSON object with keys name, epithet, persona, rarity, voice {voice_id, pitch, rate, noise}, visual {hue, form}, quotes — with the installed voice ids for the session language interpolated in, and the last ≤10 anomalies (≤600 chars JSON) as evidence. MINT_SYSTEM pins the fiction framing and "output only valid JSON."
- Step 2: The service path
Chat-tier, num_predict: 400, temperature 0.9, through LLMQueue.submit. Response → parse_mint_response → normalize_profile; parse failure or any queue/HTTP error → fallback_profile(signature). Verified behavior: minting never raises to the WS layer.
- Step 3: Verify — prompt-shape covered by
tests/test_prompts.py::test_mint_prompt_requests_exact_json_keys(4/4 PASS); end-to-end mint covered bytests/test_ws_session.py::test_summon_mints_entity_and_greetsagainst the fake service (6/6 PASS).
Task 4: Codex REST & Veil Stats
Files:
- Create:
backend/app/routes/codex.py - Modify:
backend/app/main.py(include router) - Test:
backend/tests/test_codex.py
Interfaces:
-
Produces:
GET /api/codex?rarity=&sort=recent|contacted&limit=(limit capped at 200) →{entities: [card…]}where a card is{id, name, epithet, rarity, visual, quotes, contact_count, discovered_at, discovered_by}(username resolved in one batched query);GET /api/codex/{entity_id}→ card +persona,voice,sightings(count ofentity_sightingsrows), 404"no such spirit in the codex";GET /api/stats→{entities, sessions, utterances, anomalies}live counts for the landing page ticker. -
Step 1: Write the failing tests (5 tests)
List returns all entities by name; ?rarity=rare filters; detail returns persona and sightings: 0 and 404s on a random UUID; stats counts veil activity; both codex and stats answer 200 without any auth cookie — the registry is public by design.
-
Step 2: Implement the routes — as above; discoverer usernames resolved with one
INquery (no N+1), sort bydiscovered_atdesc orcontact_countdesc. -
Step 3: Verify —
tests/test_codex.pyPASS (5/5) as part of the 45-test suite. -
Step 4: Commit — in
b9110f4.
Task 5: Voice & Visual Profiles — Every Spirit Its Own Throat
Files:
- Create:
backend/app/tts/voices.py - Modify:
backend/app/tts/piper.py(synthesize_spirit_voice),backend/app/tts/effects.py(full chain)
Interfaces:
-
Produces:
Voicedataclass +VOICEScatalog (6 EN: lessac, amy, ryan, alan, hfc_male, hfc_female; 2 ES: davefx, ald),EN_VOICE_IDS/ES_VOICE_IDS,pick_voice(voice_id, language) -> Voice(language-mismatched ids fall back tolessac/davefx);synthesize_spirit_voice(text, voice, voice_profile) -> bytes— Piper synth, thenapply_effectswith the entity'snoise/pitch/rate/bitcrush/echo. -
Step 1: The catalog — each entity's
voice_profile.voice_idpins one installed Piper model;pitch(−6…+6 semitones),rate(0.8–1.15),noise(0.01–0.08),echo(0–0.5) shape it. Profiles survive in JSONB and return to the client in theentityframe (voicekey), so the Codex page can display them. -
Step 2: The effects chain —
apply_effects(wav, *, noise_level, pitch_semitones, rate, bitcrush_bits, echo): tempo resample → pitch shift re-fitted to duration → optional bitcrush → 180 ms slap echo with renormalization → static with fade in/out edges so it breathes like a real spirit-box sweep. Pure numpy on mono 16-bit WAV. -
Step 3: Verify —
tests/test_tts_effects.pyPASS (5/5): silent/tone WAVs through the chain keep valid headers, noise actually lands, pitch/rate change the signal as expected.
Testing Status & Self-Review
Spec coverage: signature from anomaly fingerprint + LLM name (§4) → Task 2. Rare-trigger matching against existing Codex entities, persona/memory loaded into session context (§4) → Plan 3's _summon + chat_system(entity). Minting of sufficiently strong new identities (§4) → Tasks 2-3. Public browsable Codex with name/first-contact/rarity/quotes/contact count (§4) → Task 4. entities and entity_sightings tables (§5) → Task 1. Voice per entity (§6 effects chain) → Task 5.
Automated tests: tests/test_entities.py (6) + tests/test_codex.py (5) + tests/test_prompts.py mint test + WS summon/re-contact tests — all PASS in the 45-test suite.
Honest notes: contact_count increments on every summon including re-contacts (matches spec "contact count"); discovered_by is nullable and rendered as discoveredAnon in the UI when absent; rarity weighting applies only to the procedural fallback — LLM-minted rarities are clamped, not re-weighted.
Placeholder scan: none.
Type consistency: serialize_entity (ws.py) and _entity_card (codex.py) match the frontend's SpiritEntity / CodexEntity(Detail) in types.ts key-for-key, including rarity naming and ISO discovered_at. normalize_profile's output keys match the Entity columns one-to-one.
Status: COMPLETE (commit b9110f4; 45-test backend suite green).