feat(tts): map ratatoskr:sindra -> miranda voice

Sindra now voices with the dots "miranda" voice (operator-directed);
donut stays on "donut", other agents fall to the "glados" default.
One entry in _TTS_VOICE_MAP + a test; contract DEC-8 updated.
Live-verified on :8765 (/api/tts with agent_id=ratatoskr:sindra -> 200
audio/wav @ 48kHz).
This commit is contained in:
2026-08-10 21:55:11 -07:00
parent 38b78d8a4a
commit 3e69bc9c01
3 changed files with 18 additions and 2 deletions
@@ -174,7 +174,8 @@ each independently shippable. Slice order is chosen for fastest visible result.
dots clones a voice server-side from a reference clip + transcript; the client just
passes a voice NAME (GET /v1/voices lists them: donut/glados/emmie/miranda). The
`donut` voice carries over from chatterbox, so `_TTS_VOICE_MAP` maps
`ratatoskr:donut → "donut"` unchanged. NOTE the case: lowercase `"donut"` (Zonos used
`ratatoskr:donut → "donut"`; `ratatoskr:sindra → "miranda"` (operator-directed
2026-08-10). NOTE the case: lowercase `"donut"` (Zonos used
`"Donut"`). Non-interview agents fall to the dots default `"glados"` (was chatterbox
`"glados_25s"` / Zonos `"Cora"`, neither of which exists on dots). New voices are a
one-line request to infra-ops (derived from the canonical voice corpus).
+1 -1
View File
@@ -545,7 +545,7 @@ async def _memory_chunks_endpoint(request: Request) -> JSONResponse:
# Per-character voice map (DEC-8): interview characters resolve to their registered
# dots voice name (GET /v1/voices lists them); everything else falls to the gateway default.
# NOTE the case: dots wants lowercase "donut" (Zonos used "Donut").
_TTS_VOICE_MAP = {"ratatoskr:donut": "donut"}
_TTS_VOICE_MAP = {"ratatoskr:donut": "donut", "ratatoskr:sindra": "miranda"}
_TTS_DEFAULT_VOICE = "glados" # a dots voice (chatterbox "glados_25s" / Zonos "Cora" do not exist here)
# The text rides the POST body (DEC-10a), so URL length is not the bound — this is a safety
# ceiling on the shared-GPU hold. dots streams a whole turn from one call (no client concat),
+15
View File
@@ -1505,6 +1505,21 @@ class TestTtsEndpoint:
body = json.loads(route.calls.last.request.content)
assert body["voice"] == "glados" # dots default voice (DEC-8)
@respx.mock
def test_sindra_resolves_miranda_voice(self) -> None:
from ratatoskr.web.server import create_app
route = respx.post(self._TTS).mock(
return_value=httpx.Response(200, content=self._WAV)
)
app = create_app(_mock_client_factory(), tts_url=self._TTS)
resp = TestClient(app).post(
"/api/tts", json={"text": "Hello there.", "agent_id": "ratatoskr:sindra"}
)
assert resp.status_code == 200
body = json.loads(route.calls.last.request.content)
assert body["voice"] == "miranda" # per-character map (DEC-8)
def test_missing_text_returns_400(self) -> None:
from ratatoskr.web.server import create_app