feat: spirit engine — seance WS, entity minting/Codex, Piper TTS voices, wire telemetry

- WS /ws/session: modes, summon, anomaly fragments, streaming direct contact,
  passive wire-ghost ambient loop, per-user rate limits, event transcript
- entities: anomaly-signature fingerprinting, LLM minting + procedural
  fallback, Codex matching with contact counts and sightings
- tts: 8 local Piper voices (EN/ES), per-entity voice profiles, numpy
  effects chain (pitch/rate/bitcrush/echo/static)
- llm: streaming client, submit_stream in bounded queue, SpiritService
  with offline fallbacks for every channel
- routes: public /api/codex, /api/codex/{id}, /api/stats; /audio static mount
- models: Entity, EntitySighting, Event, ContactSession(entity_id, language)
This commit is contained in:
Indiana
2026-07-20 20:13:28 +00:00
parent 52afad1ad5
commit b9110f45de
28 changed files with 1997 additions and 4 deletions

103
backend/app/routes/codex.py Normal file
View File

@@ -0,0 +1,103 @@
"""Public Codex endpoints — the browsable registry of every spirit ever
contacted, shared across all users (spec §4)."""
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_db
from app.models.contact_session import ContactSession
from app.models.entity import Entity
from app.models.entity_sighting import EntitySighting
from app.models.event import Event
from app.models.user import User
router = APIRouter(prefix="/api", tags=["codex"])
def _entity_card(entity: Entity, discoverer: str | None) -> dict:
return {
"id": str(entity.id),
"name": entity.name,
"epithet": entity.epithet,
"rarity": entity.rarity_tier,
"visual": entity.visual_profile,
"quotes": entity.sample_quotes,
"contact_count": entity.contact_count,
"discovered_at": entity.discovered_at.isoformat(),
"discovered_by": discoverer,
}
@router.get("/codex")
async def list_codex(
rarity: str | None = None,
sort: str = "recent",
limit: int = 60,
db: AsyncSession = Depends(get_db),
):
query = select(Entity)
if rarity:
query = query.where(Entity.rarity_tier == rarity)
if sort == "contacted":
query = query.order_by(desc(Entity.contact_count))
else:
query = query.order_by(desc(Entity.discovered_at))
query = query.limit(min(limit, 200))
entities = (await db.execute(query)).scalars().all()
discoverers = {
user.id: user.username
for user in (
await db.execute(
select(User).where(
User.id.in_({e.discovered_by for e in entities if e.discovered_by})
)
)
).scalars()
}
return {
"entities": [
_entity_card(e, discoverers.get(e.discovered_by)) for e in entities
]
}
@router.get("/codex/{entity_id}")
async def get_entity(entity_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
entity = await db.get(Entity, entity_id)
if entity is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "no such spirit in the codex")
discoverer = None
if entity.discovered_by:
user = await db.get(User, entity.discovered_by)
discoverer = user.username if user else None
sightings = await db.scalar(
select(func.count(EntitySighting.id)).where(EntitySighting.entity_id == entity.id)
)
card = _entity_card(entity, discoverer)
card["persona"] = entity.persona
card["voice"] = entity.voice_profile
card["sightings"] = sightings or 0
return card
@router.get("/stats")
async def veil_stats(db: AsyncSession = Depends(get_db)):
"""Live counters for the landing page's 'veil activity' ticker."""
return {
"entities": await db.scalar(select(func.count(Entity.id))) or 0,
"sessions": await db.scalar(select(func.count(ContactSession.id))) or 0,
"utterances": await db.scalar(
select(func.count(Event.id)).where(Event.kind == "utterance")
)
or 0,
"anomalies": await db.scalar(
select(func.count(Event.id)).where(Event.kind == "anomaly")
)
or 0,
}