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
5 changes: 4 additions & 1 deletion assemblyai/tts/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ class TTSResponseInfo(BaseModel):

billed_characters: Optional[int] = None
"""The characters the request bills. On a stream, the value as the stream
started: a body cut by a server failure bills nothing."""
started: a body cut by a server failure bills nothing, one cut by
`session_expired` (the API's 600-second limit) bills the text before the
sentence being spoken, and one cut by `audio_backlog` (a client reading
too slowly) bills every character."""

audio_duration_ms: Optional[int] = None
"""The duration of the audio in milliseconds, on a complete response:
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,33 @@ def test_a_refused_stream_releases_its_connection():
assert body.closed


class _CutJsonBody(httpx.SyncByteStream):
closed = False

def __iter__(self) -> Iterator[bytes]:
yield b'{"code": '
raise httpx.ReadError("connection reset")

def close(self) -> None:
self.closed = True


def test_a_refused_stream_releases_its_connection_if_its_body_fails():
# httpx closes a fully read body itself; a body cut while it is read is
# released only by the stream. The error stays referenced until the end,
# as a caller that logs it would, so its traceback keeps the stream's
# frames alive and garbage collection cannot release the body instead.
body = _CutJsonBody()
with httpx.Client(transport=_transport(body, status=503)) as http:
speech = api.stream_speech(
http, base_url="https://tts.test", body={"text": "Hi."}, timeout=5.0
)
with pytest.raises(httpx.ReadError) as exc_info:
speech.read()
assert body.closed
assert "connection reset" in str(exc_info.value)


def test_a_client_built_from_api_key_is_closed_with_the_instance():
with aai.TextToSpeech(api_key="k") as tts:
http = tts.client.http_client
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_tts_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,33 @@ async def test_a_refused_stream_releases_its_connection():
assert body.closed


class _CutJsonBody(httpx.AsyncByteStream):
closed = False

async def __aiter__(self) -> AsyncIterator[bytes]:
yield b'{"code": '
raise httpx.ReadError("connection reset")

async def aclose(self) -> None:
self.closed = True


async def test_a_refused_stream_releases_its_connection_if_its_body_fails():
# httpx closes a fully read body itself; a body cut while it is read is
# released only by the stream. The error stays referenced until the end,
# as a caller that logs it would, so its traceback keeps the stream's
# frames alive and garbage collection cannot release the body instead.
body = _CutJsonBody()
async with httpx.AsyncClient(transport=_transport(body, status=503)) as http:
speech = async_api.stream_speech(
http, base_url="https://tts.test", body={"text": "Hi."}, timeout=5.0
)
with pytest.raises(httpx.ReadError) as exc_info:
await speech.aread()
assert body.closed
assert "connection reset" in str(exc_info.value)


async def test_an_owned_pool_is_closed_on_aclose():
tts = aai.AsyncTextToSpeech(api_key="k")
http = tts.client.http_client
Expand Down
Loading