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
16 changes: 16 additions & 0 deletions livekit-plugins/livekit-plugins-aws/livekit/plugins/aws/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from livekit.agents import (
APIConnectionError,
APIConnectOptions,
APIStatusError,
APITimeoutError,
LanguageCode,
tts,
Expand Down Expand Up @@ -187,5 +188,20 @@ async def _run(self, output_emitter: tts.AudioEmitter) -> None:
output_emitter.push(data)
except botocore.exceptions.ConnectTimeoutError:
raise APITimeoutError() from None
except botocore.exceptions.ClientError as e:
meta = e.response.get("ResponseMetadata", {})
status_error = APIStatusError(
f"aws polly tts: error synthesizing speech: {e}",
status_code=meta.get("HTTPStatusCode", -1),
request_id=meta.get("RequestId"),
)
# Polly throttles with a 400, and request signing can raise STS's ClientErrors here
if (
status_error.retryable
or e.operation_name != "SynthesizeSpeech"
or e.response.get("Error", {}).get("Code") == "ThrottlingException"
):
raise APIConnectionError() from e
raise status_error from e
except Exception as e:
raise APIConnectionError() from e
50 changes: 50 additions & 0 deletions tests/test_plugin_aws_tts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations

import pytest
from botocore.exceptions import ClientError

from livekit.agents import APIConnectOptions, APIError, APIStatusError
from livekit.plugins.aws import TTS as PollyTTS

pytestmark = pytest.mark.unit


@pytest.mark.parametrize(
("operation", "code", "attempts"),
[
("SynthesizeSpeech", "InvalidSsmlException", 1),
("SynthesizeSpeech", "ThrottlingException", 4),
("AssumeRole", "Throttling", 4),
],
)
async def test_polly_400_fails_fast_throttling_retried(
monkeypatch: pytest.MonkeyPatch, operation: str, code: str, attempts: int
) -> None:
# Polly throttles with a 400, and request signing can raise STS's ClientErrors
calls = 0

class _Client:
async def __aenter__(self) -> _Client:
return self

async def __aexit__(self, *exc: object) -> None:
return None

async def synthesize_speech(self, **kwargs: object) -> dict:
nonlocal calls
calls += 1
raise ClientError(
{"Error": {"Code": code}, "ResponseMetadata": {"HTTPStatusCode": 400}}, operation
)

instance = PollyTTS()
monkeypatch.setattr(instance._session, "create_client", lambda *a, **kw: _Client())
stream = instance.synthesize("hi", conn_options=APIConnectOptions(retry_interval=0))

with pytest.raises(APIError) as exc_info:
await stream.collect()

assert calls == attempts
if attempts == 1:
assert isinstance(exc_info.value, APIStatusError)
assert exc_info.value.status_code == 400
Loading