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

40
backend/app/tts/voices.py Normal file
View File

@@ -0,0 +1,40 @@
"""Catalog of locally installed Piper voices.
Each entity's voice_profile pins one of these voice ids plus pitch/rate/noise
shaping, giving every spirit its own recognizable throat."""
from dataclasses import dataclass
@dataclass(frozen=True)
class Voice:
id: str
model_file: str
language: str
description: str
VOICES: dict[str, Voice] = {
"lessac": Voice("lessac", "en_US-lessac-low.onnx", "en", "a measured American woman"),
"amy": Voice("amy", "en_US-amy-low.onnx", "en", "a soft American woman"),
"ryan": Voice("ryan", "en_US-ryan-low.onnx", "en", "a deep American man"),
"alan": Voice("alan", "en_GB-alan-low.onnx", "en", "a low British man"),
"hfc_male": Voice("hfc_male", "en_US-hfc_male-medium.onnx", "en", "a worn male voice"),
"hfc_female": Voice(
"hfc_female", "en_US-hfc_female-medium.onnx", "en", "a worn female voice"
),
"davefx": Voice("davefx", "es_ES-davefx-medium.onnx", "es", "una voz masculina grave"),
"ald": Voice("ald", "es_MX-ald-medium.onnx", "es", "una voz masculina seca"),
}
EN_VOICE_IDS = [v.id for v in VOICES.values() if v.language == "en"]
ES_VOICE_IDS = [v.id for v in VOICES.values() if v.language == "es"]
def pick_voice(voice_id: str | None, language: str) -> Voice:
"""Resolve an entity's voice id, falling back to a language-appropriate default."""
if voice_id and voice_id in VOICES:
voice = VOICES[voice_id]
if voice.language == language:
return voice
return VOICES["davefx" if language == "es" else "lessac"]