diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index 2d986fc..320141d 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "1.8.1" +__version__ = "1.8.2" diff --git a/assemblyai/tts/v1/models.py b/assemblyai/tts/v1/models.py index d56afa5..4d00239 100644 --- a/assemblyai/tts/v1/models.py +++ b/assemblyai/tts/v1/models.py @@ -254,7 +254,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 the + API's 600-second limit (`session_expired`) bills only the text whose + audio had been generated, and one cut by a client reading too slowly + (`audio_backlog`) bills every character.""" audio_duration_ms: Optional[int] = None """The duration of the audio in milliseconds, on a complete response: diff --git a/tests/unit/test_tts.py b/tests/unit/test_tts.py index 0c985cd..57bd943 100644 --- a/tests/unit/test_tts.py +++ b/tests/unit/test_tts.py @@ -320,6 +320,31 @@ 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 cut body is released only by + # the stream. Holding the error keeps `cm` alive, so GC cannot do it. + 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 diff --git a/tests/unit/test_tts_async.py b/tests/unit/test_tts_async.py index 23b3f1e..b8c3cda 100644 --- a/tests/unit/test_tts_async.py +++ b/tests/unit/test_tts_async.py @@ -187,6 +187,31 @@ 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 cut body is released only by + # the stream. Holding the error keeps `cm` alive, so GC cannot do it. + 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