Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@
# Rotate to a fresh stream at a sentence boundary
# well before Soniox's fixed 2-minute per-stream cap.
MAX_STREAM_AGE = 90.0 # seconds
# Models that report supports_silence_reduction: false in the List models API
# (https://soniox.com/docs/api-reference/tts/get_tts_models). Newer models are
# assumed to support reduce_silence; the server still rejects unknown models.
MODELS_WITHOUT_SILENCE_REDUCTION = frozenset({"tts-rt-v1", "tts-rt-v1-preview"})


def _validate_reduce_silence(model: str, reduce_silence: bool) -> None:
if reduce_silence and model in MODELS_WITHOUT_SILENCE_REDUCTION:
raise ValueError(
f"reduce_silence is not supported on model {model!r}; use a model with "
"supports_silence_reduction (e.g. tts-rt-v2). "
"See https://soniox.com/docs/tts/concepts/reduce-silence"
)


def _audio_format_to_mime_type(audio_format: str) -> str:
Expand Down Expand Up @@ -88,6 +101,7 @@ def __init__(
sample_rate: int = DEFAULT_SAMPLE_RATE,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only supported on models with supports_silence_reduction: true - see List models. Setting it to true on any other model returns an invalid_request error. See Reduce silence.

we should validate the model option here when it is True.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in da9b084 - added a _validate_reduce_silence() check that raises ValueError from both the constructor and update_options (validated against the effective model/flag combination there, before any state is mutated). Implemented as a blocklist of the models that report supports_silence_reduction: false in the List models API (tts-rt-v1, tts-rt-v1-preview), so future models keep working without a plugin update and the server stays the final validator for unknown models. Covered by a new test, and updated the existing one which was enabling reduce_silence on the default v1-preview model.

bitrate: int | None = None,
speed: float = DEFAULT_SPEED,
reduce_silence: bool = False,
api_key: str | None = None,
websocket_url: str = WEBSOCKET_URL,
http_session: aiohttp.ClientSession | None = None,
Expand All @@ -105,6 +119,9 @@ def __init__(
bitrate (int): Codec bitrate in bps for compressed formats. Optional.
speed (float): Speaking rate. 1.0 is the normal rate; values below 1.0 slow speech
down and values above 1.0 speed it up. Range is [0.7, 1.3]. Defaults to 1.0.
reduce_silence (bool): Shorten the pauses between words. Only supported on
models that report supports_silence_reduction (e.g. tts-rt-v2); the server
rejects it on other models. Defaults to False.
api_key (str): Soniox API key. If not provided, will look for SONIOX_API_KEY env variable.
websocket_url (str): Base WebSocket URL for Soniox TTS API.
http_session (aiohttp.ClientSession): Optional aiohttp.ClientSession to use for requests.
Expand Down Expand Up @@ -132,10 +149,13 @@ def __init__(
if stream_idle_timeout <= 0:
raise ValueError(f"stream_idle_timeout must be > 0, but got {stream_idle_timeout}")

_validate_reduce_silence(model, reduce_silence)

self._opts = _TTSOptions(
model=model,
language=language,
voice=voice,
reduce_silence=reduce_silence,
audio_format=audio_format,
sample_rate=sample_rate,
bitrate=bitrate,
Expand Down Expand Up @@ -198,6 +218,7 @@ def update_options(
language: NotGivenOr[str] = NOT_GIVEN,
voice: NotGivenOr[str] = NOT_GIVEN,
speed: NotGivenOr[float] = NOT_GIVEN,
reduce_silence: NotGivenOr[bool] = NOT_GIVEN,
stream_idle_timeout: NotGivenOr[float] = NOT_GIVEN,
) -> None:
"""
Expand All @@ -206,8 +227,14 @@ def update_options(
language: Language code to use.
voice: Voice to use.
speed: Speaking rate in the range [0.7, 1.3]; 1.0 is the normal rate.
reduce_silence: Shorten the pauses between words (models with
supports_silence_reduction only).
stream_idle_timeout: Idle seconds before the current stream is finalized.
"""
_validate_reduce_silence(
model if is_given(model) else self._opts.model,
reduce_silence if is_given(reduce_silence) else self._opts.reduce_silence,
)
if is_given(model):
self._opts.model = model
if is_given(language):
Expand All @@ -220,6 +247,8 @@ def update_options(
f"speed must be between {MIN_SPEED} and {MAX_SPEED}, but got {speed}"
)
self._opts.speed = speed
if is_given(reduce_silence):
self._opts.reduce_silence = reduce_silence
if is_given(stream_idle_timeout):
if stream_idle_timeout <= 0:
raise ValueError(f"stream_idle_timeout must be > 0, but got {stream_idle_timeout}")
Expand Down Expand Up @@ -520,6 +549,7 @@ class _TTSOptions:
model: str
language: str
voice: str
reduce_silence: bool
audio_format: str
sample_rate: int
bitrate: int | None
Expand Down Expand Up @@ -704,6 +734,8 @@ async def _send_loop(self) -> None:
}
if msg.opts.bitrate is not None:
config["bitrate"] = msg.opts.bitrate
if msg.opts.reduce_silence:
config["reduce_silence"] = True
await self._ws.send_str(json.dumps(config))
elif isinstance(msg, _SendText):
payload: dict[str, Any] = {"stream_id": msg.stream_id}
Expand Down
29 changes: 29 additions & 0 deletions tests/test_plugin_soniox_tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,32 @@ async def test_invalid_stream_idle_timeout_rejected() -> None:
soniox.TTS(api_key="fake-key", stream_idle_timeout=0)
with pytest.raises(ValueError):
soniox.TTS(api_key="fake-key", stream_idle_timeout=-1.0)


async def test_reduce_silence_propagates_to_options() -> None:
tts = soniox.TTS(api_key="fake-key", model="tts-rt-v2")
assert tts._opts.reduce_silence is False

tts.update_options(reduce_silence=True)
assert tts._opts.reduce_silence is True

tts = soniox.TTS(api_key="fake-key", model="tts-rt-v2", reduce_silence=True)
assert tts._opts.reduce_silence is True


async def test_reduce_silence_rejected_on_unsupported_model() -> None:
# default model is tts-rt-v1-preview, which lacks supports_silence_reduction
with pytest.raises(ValueError, match="reduce_silence"):
soniox.TTS(api_key="fake-key", reduce_silence=True)
with pytest.raises(ValueError, match="reduce_silence"):
soniox.TTS(api_key="fake-key", model="tts-rt-v1", reduce_silence=True)

tts = soniox.TTS(api_key="fake-key")
with pytest.raises(ValueError, match="reduce_silence"):
tts.update_options(reduce_silence=True)
assert tts._opts.reduce_silence is False

tts = soniox.TTS(api_key="fake-key", model="tts-rt-v2", reduce_silence=True)
with pytest.raises(ValueError, match="reduce_silence"):
tts.update_options(model="tts-rt-v1")
assert tts._opts.model == "tts-rt-v2"