- 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)
104 lines
3.2 KiB
Python
104 lines
3.2 KiB
Python
"""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,
|
|
}
|