feat(tts): migrate RP-surface TTS chatterbox-fast → dots-tts
Swap the voice synthesis backend from chatterbox-fast (:8197 bespoke /tts)
to dots-tts (rednote-hilab dots.tts-soar, :8198 OpenAI-shaped
/v1/audio/speech), operator-directed after an A/B win. tts.py stays the
single swap seam.
- gateway body OpenAI-shaped: {input, voice, response_format, stream}
(was chatterbox {text, voice, format, stream})
- sample rate 24000 -> 48000 Hz (browser Web Audio SR)
- default voice glados_25s -> glados; donut voice carries over
- serialized single-consumer (satisfied by the existing DEC-5 lock)
- affect stays dropped (dots has no emotion knob, same as chatterbox)
DOTS_TTS_URL replaces CHATTERBOX_TTS_URL; RATATOSKR_TTS_URL override
unchanged. chatterbox-fast :8197 kept up as rollback. Contract amended
(donut_voiced_interview.contract.md). Live-verified end-to-end on :8765
(RIFF/WAVE 48kHz mono s16le through /api/tts). 520 tests green.
This commit is contained in:
+23
-25
@@ -1,10 +1,10 @@
|
||||
"""Tests for ratatoskr.tts — the STREAMING chatterbox-fast gateway client.
|
||||
"""Tests for ratatoskr.tts — the STREAMING dots-tts gateway client.
|
||||
|
||||
tts_stream proxies the gateway's chunked response verbatim (no buffering, no header
|
||||
rewrite — the placeholder-size streaming WAV is meant to be played progressively). It
|
||||
is the sole synthesis primitive: chatterbox chunks arbitrary-length text internally, so
|
||||
there is no client-side chunk-and-concatenate (retired with the Zonos migration), and no
|
||||
affect dials (Turbo has no emotion knob). The mid-stream degrade policy is folded in.
|
||||
is the sole synthesis primitive: dots streams a whole turn from one call, so there is
|
||||
no client-side chunk-and-concatenate, and no affect dials (dots has no emotion knob).
|
||||
The mid-stream degrade policy is folded in.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
@@ -12,13 +12,13 @@ import pytest
|
||||
import respx
|
||||
|
||||
from ratatoskr.tts import (
|
||||
CHATTERBOX_TTS_URL,
|
||||
DOTS_TTS_URL,
|
||||
TtsUnavailable,
|
||||
gateway_body,
|
||||
tts_stream,
|
||||
)
|
||||
|
||||
_URL = "http://tts.example/tts"
|
||||
_URL = "http://tts.example/v1/audio/speech"
|
||||
# The gateway's streaming WAV bytes (placeholder 0xFFFFFFFF sizes). We pass them through
|
||||
# untouched, so the content only has to round-trip.
|
||||
_WAV = (
|
||||
@@ -50,34 +50,32 @@ class _RaisingByteStream(httpx.AsyncByteStream):
|
||||
|
||||
|
||||
class TestGatewayBody:
|
||||
def test_bespoke_chatterbox_schema(self) -> None:
|
||||
def test_openai_dots_schema(self) -> None:
|
||||
b = gateway_body("hi", "donut")
|
||||
assert b["text"] == "hi" # "text", not "input"
|
||||
assert b["input"] == "hi" # OpenAI "input", not chatterbox "text"
|
||||
assert b["voice"] == "donut"
|
||||
assert b["format"] == "wav" # DEC-3 — "format", not "response_format"
|
||||
assert b["stream"] is True # DEC-2 — play-as-it-arrives
|
||||
assert b["response_format"] == "wav" # DEC-3 — "response_format", not "format"
|
||||
assert b["stream"] is True # DEC-2 — play-as-it-arrives
|
||||
|
||||
def test_default_sampling_no_client_side_curbs(self) -> None:
|
||||
# DEC-9 (real cause): the long-turn garble was Turbo over-running its GENERATION TAIL,
|
||||
# fixed SERVER-SIDE (:v2 max_chunk_chars=250). A client sampling curb was
|
||||
# counterproductive (tight sampling pulls the garble onset earlier), so gateway_body
|
||||
# sends NO temperature/top_p/top_k — the gateway's defaults govern.
|
||||
# No client sampling curbs — the gateway's defaults govern (a client-side curb
|
||||
# was counterproductive on the prior backend and dots exposes no such need).
|
||||
b = gateway_body("a long turn", "donut")
|
||||
for knob in ("temperature", "top_p", "top_k"):
|
||||
assert knob not in b
|
||||
|
||||
def test_no_zonos_era_fields(self) -> None:
|
||||
# The Zonos body fields are gone: no OpenAI `input`/`response_format`, no
|
||||
# `language` pin (DEC-9 retired), no affect dials (DEC-7 retired).
|
||||
b = gateway_body("hi", "Cora")
|
||||
for dead in ("input", "response_format", "language", "emotion_valence",
|
||||
def test_no_chatterbox_or_zonos_era_fields(self) -> None:
|
||||
# The chatterbox bespoke names + Zonos-era fields are gone: no `text`/`format`
|
||||
# (chatterbox), no `language` pin, no affect dials (DEC-7 retired).
|
||||
b = gateway_body("hi", "glados")
|
||||
for dead in ("text", "format", "language", "emotion_valence",
|
||||
"emotion_arousal", "emotion_enabled", "emotion_strength"):
|
||||
assert dead not in b
|
||||
|
||||
|
||||
class TestTtsStream:
|
||||
@respx.mock
|
||||
async def test_streams_chunks_and_posts_bespoke_body(self) -> None:
|
||||
async def test_streams_chunks_and_posts_openai_body(self) -> None:
|
||||
route = respx.post(_URL).mock(return_value=httpx.Response(200, content=_WAV))
|
||||
async with httpx.AsyncClient() as client:
|
||||
out = await _drain(tts_stream("hello there", voice="donut", client=client, url=_URL))
|
||||
@@ -85,19 +83,19 @@ class TestTtsStream:
|
||||
import json as _json
|
||||
|
||||
body = _json.loads(route.calls.last.request.content)
|
||||
assert body["text"] == "hello there"
|
||||
assert body["input"] == "hello there"
|
||||
assert body["voice"] == "donut"
|
||||
assert body["format"] == "wav"
|
||||
assert body["response_format"] == "wav"
|
||||
assert body["stream"] is True
|
||||
|
||||
@respx.mock
|
||||
async def test_default_url_is_chatterbox(self) -> None:
|
||||
route = respx.post(CHATTERBOX_TTS_URL).mock(
|
||||
async def test_default_url_is_dots(self) -> None:
|
||||
route = respx.post(DOTS_TTS_URL).mock(
|
||||
return_value=httpx.Response(200, content=_WAV)
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
await _drain(tts_stream("hi", voice="donut", client=client))
|
||||
assert route.called # the module default points at the chatterbox gateway
|
||||
assert route.called # the module default points at the dots-tts gateway
|
||||
|
||||
@respx.mock
|
||||
async def test_non_200_open_raises_before_any_chunk(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user