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:
40
backend/app/tts/voices.py
Normal file
40
backend/app/tts/voices.py
Normal 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"]
|
||||
Reference in New Issue
Block a user