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

View File

@@ -1,4 +1,6 @@
import pytest
import pytest_asyncio
from fastapi.testclient import TestClient
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
@@ -37,3 +39,25 @@ async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="https://test") as ac:
yield ac
@pytest.fixture
def sync_client(monkeypatch):
"""Sync TestClient (supports WebSocket tests). The WS handler's session
maker is swapped to the NullPool test engine so its writes land in the
same test database the async fixtures see. The lifespan's engine is also
swapped: TestClient runs the lifespan on its own portal loop, and the
pooled production engine would carry connections across loops."""
import app.main as main_module
import app.ws as ws_module
monkeypatch.setattr(ws_module, "session_maker", TestSessionLocal)
monkeypatch.setattr(main_module, "engine", test_engine)
with TestClient(app, base_url="https://testserver") as tc:
yield tc
@pytest_asyncio.fixture
async def db_session():
async with TestSessionLocal() as session:
yield session