diff --git a/clients/aws-sdk-polly/tests/integration/__init__.py b/clients/aws-sdk-polly/tests/integration/__init__.py new file mode 100644 index 0000000..57dc918 --- /dev/null +++ b/clients/aws-sdk-polly/tests/integration/__init__.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from smithy_aws_core.identity import EnvironmentCredentialsResolver + +from aws_sdk_polly.client import PollyClient +from aws_sdk_polly.config import Config + +REGION = "us-east-1" +VOICE_ID = "Matthew" +ENGINE = "generative" +OUTPUT_FORMAT = "mp3" +SAMPLE_RATE = "24000" +TEST_TEXT = "Hello from the AWS SDK for Python Polly integration tests." + + +def create_polly_client(region: str) -> PollyClient: + """Helper to create a PollyClient for a given region.""" + return PollyClient( + config=Config( + endpoint_uri=f"https://polly.{region}.amazonaws.com", + region=region, + aws_credentials_identity_resolver=EnvironmentCredentialsResolver(), + ) + ) diff --git a/clients/aws-sdk-polly/tests/integration/test_bidirectional_streaming.py b/clients/aws-sdk-polly/tests/integration/test_bidirectional_streaming.py new file mode 100644 index 0000000..ef41d47 --- /dev/null +++ b/clients/aws-sdk-polly/tests/integration/test_bidirectional_streaming.py @@ -0,0 +1,114 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test bidirectional streaming event stream handling.""" + +import asyncio + +from smithy_core.aio.eventstream import DuplexEventStream + +from aws_sdk_polly.models import ( + CloseStreamEvent, + StartSpeechSynthesisStreamActionStream, + StartSpeechSynthesisStreamActionStreamCloseStreamEvent, + StartSpeechSynthesisStreamActionStreamTextEvent, + StartSpeechSynthesisStreamEventStream, + StartSpeechSynthesisStreamEventStreamAudioEvent, + StartSpeechSynthesisStreamEventStreamServiceFailureException, + StartSpeechSynthesisStreamEventStreamServiceQuotaExceededException, + StartSpeechSynthesisStreamEventStreamStreamClosedEvent, + StartSpeechSynthesisStreamEventStreamThrottlingException, + StartSpeechSynthesisStreamEventStreamUnknown, + StartSpeechSynthesisStreamEventStreamValidationException, + StartSpeechSynthesisStreamInput, + StartSpeechSynthesisStreamOutput, + TextEvent, +) + +from . import ( + ENGINE, + OUTPUT_FORMAT, + REGION, + SAMPLE_RATE, + TEST_TEXT, + VOICE_ID, + create_polly_client, +) + +ERROR_EVENT_TYPES = ( + StartSpeechSynthesisStreamEventStreamValidationException, + StartSpeechSynthesisStreamEventStreamServiceQuotaExceededException, + StartSpeechSynthesisStreamEventStreamServiceFailureException, + StartSpeechSynthesisStreamEventStreamThrottlingException, +) + + +async def _send_text( + stream: DuplexEventStream[ + StartSpeechSynthesisStreamActionStream, + StartSpeechSynthesisStreamEventStream, + StartSpeechSynthesisStreamOutput, + ], +) -> None: + """Send text input and close the input stream.""" + await stream.input_stream.send( + StartSpeechSynthesisStreamActionStreamTextEvent(value=TextEvent(text=TEST_TEXT)) + ) + await stream.input_stream.send( + StartSpeechSynthesisStreamActionStreamCloseStreamEvent(CloseStreamEvent()) + ) + await stream.input_stream.close() + + +async def _receive_audio( + stream: DuplexEventStream[ + StartSpeechSynthesisStreamActionStream, + StartSpeechSynthesisStreamEventStream, + StartSpeechSynthesisStreamOutput, + ], +) -> tuple[int, int | None]: + """Receive synthesized audio and the final stream summary.""" + audio_bytes = 0 + request_characters: int | None = None + + _, output_stream = await stream.await_output() + if output_stream is None: + return audio_bytes, request_characters + + async for event in output_stream: + if isinstance(event, StartSpeechSynthesisStreamEventStreamAudioEvent): + if event.value.audio_chunk: + audio_bytes += len(event.value.audio_chunk) + elif isinstance(event, StartSpeechSynthesisStreamEventStreamStreamClosedEvent): + request_characters = event.value.request_characters + break + elif isinstance(event, ERROR_EVENT_TYPES): + raise event.value + elif isinstance(event, StartSpeechSynthesisStreamEventStreamUnknown): + raise RuntimeError(f"Received unknown event in stream: {event.tag}") + else: + raise RuntimeError( + f"Received unexpected event type in stream: {type(event).__name__}" + ) + + return audio_bytes, request_characters + + +async def test_start_speech_synthesis_stream() -> None: + """Test bidirectional streaming with text input and audio output.""" + client = create_polly_client(REGION) + + stream = await client.start_speech_synthesis_stream( + input=StartSpeechSynthesisStreamInput( + engine=ENGINE, + output_format=OUTPUT_FORMAT, + sample_rate=SAMPLE_RATE, + voice_id=VOICE_ID, + ) + ) + + results = await asyncio.gather(_send_text(stream), _receive_audio(stream)) + audio_bytes, request_characters = results[1] + + assert audio_bytes > 0, "Expected to receive synthesized audio" + assert request_characters == len(TEST_TEXT) diff --git a/clients/aws-sdk-polly/tests/integration/test_non_streaming.py b/clients/aws-sdk-polly/tests/integration/test_non_streaming.py new file mode 100644 index 0000000..8143f72 --- /dev/null +++ b/clients/aws-sdk-polly/tests/integration/test_non_streaming.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test non-streaming output type handling.""" + +from aws_sdk_polly.models import DescribeVoicesInput, DescribeVoicesOutput + +from . import ENGINE, REGION, VOICE_ID, create_polly_client + + +async def test_describe_voices() -> None: + """Test non-streaming DescribeVoices operation.""" + client = create_polly_client(REGION) + + response = await client.describe_voices(input=DescribeVoicesInput(engine=ENGINE)) + + assert isinstance(response, DescribeVoicesOutput) + assert response.voices is not None + assert len(response.voices) > 0 + + voices_by_id = {voice.id: voice for voice in response.voices if voice.id} + assert VOICE_ID in voices_by_id + + voice = voices_by_id[VOICE_ID] + assert voice.language_code == "en-US" + assert voice.supported_engines is not None + assert ENGINE in voice.supported_engines diff --git a/clients/aws-sdk-polly/tests/integration/test_output_streaming.py b/clients/aws-sdk-polly/tests/integration/test_output_streaming.py new file mode 100644 index 0000000..d844602 --- /dev/null +++ b/clients/aws-sdk-polly/tests/integration/test_output_streaming.py @@ -0,0 +1,40 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test output streaming blob handling.""" + +from smithy_core.aio.utils import read_streaming_blob_async + +from aws_sdk_polly.models import SynthesizeSpeechInput, SynthesizeSpeechOutput + +from . import ( + ENGINE, + OUTPUT_FORMAT, + REGION, + SAMPLE_RATE, + TEST_TEXT, + VOICE_ID, + create_polly_client, +) + + +async def test_synthesize_speech() -> None: + """Test output-streaming SynthesizeSpeech operation.""" + client = create_polly_client(REGION) + + response = await client.synthesize_speech( + input=SynthesizeSpeechInput( + engine=ENGINE, + output_format=OUTPUT_FORMAT, + sample_rate=SAMPLE_RATE, + text=TEST_TEXT, + voice_id=VOICE_ID, + ) + ) + + assert isinstance(response, SynthesizeSpeechOutput) + assert response.content_type == "audio/mpeg" + assert response.request_characters == len(TEST_TEXT) + + audio = await read_streaming_blob_async(response.audio_stream) + assert len(audio) > 0