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:
103
backend/app/routes/codex.py
Normal file
103
backend/app/routes/codex.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user