Skip to content

Commit bc78957

Browse files
authored
Add Polly client integration tests (#63)
1 parent 48b4da5 commit bc78957

4 files changed

Lines changed: 206 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from smithy_aws_core.identity import EnvironmentCredentialsResolver
5+
6+
from aws_sdk_polly.client import PollyClient
7+
from aws_sdk_polly.config import Config
8+
9+
REGION = "us-east-1"
10+
VOICE_ID = "Matthew"
11+
ENGINE = "generative"
12+
OUTPUT_FORMAT = "mp3"
13+
SAMPLE_RATE = "24000"
14+
TEST_TEXT = "Hello from the AWS SDK for Python Polly integration tests."
15+
16+
17+
def create_polly_client(region: str) -> PollyClient:
18+
"""Helper to create a PollyClient for a given region."""
19+
return PollyClient(
20+
config=Config(
21+
endpoint_uri=f"https://polly.{region}.amazonaws.com",
22+
region=region,
23+
aws_credentials_identity_resolver=EnvironmentCredentialsResolver(),
24+
)
25+
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test bidirectional streaming event stream handling."""
5+
6+
import asyncio
7+
8+
from smithy_core.aio.eventstream import DuplexEventStream
9+
10+
from aws_sdk_polly.models import (
11+
CloseStreamEvent,
12+
StartSpeechSynthesisStreamActionStream,
13+
StartSpeechSynthesisStreamActionStreamCloseStreamEvent,
14+
StartSpeechSynthesisStreamActionStreamTextEvent,
15+
StartSpeechSynthesisStreamEventStream,
16+
StartSpeechSynthesisStreamEventStreamAudioEvent,
17+
StartSpeechSynthesisStreamEventStreamServiceFailureException,
18+
StartSpeechSynthesisStreamEventStreamServiceQuotaExceededException,
19+
StartSpeechSynthesisStreamEventStreamStreamClosedEvent,
20+
StartSpeechSynthesisStreamEventStreamThrottlingException,
21+
StartSpeechSynthesisStreamEventStreamUnknown,
22+
StartSpeechSynthesisStreamEventStreamValidationException,
23+
StartSpeechSynthesisStreamInput,
24+
StartSpeechSynthesisStreamOutput,
25+
TextEvent,
26+
)
27+
28+
from . import (
29+
ENGINE,
30+
OUTPUT_FORMAT,
31+
REGION,
32+
SAMPLE_RATE,
33+
TEST_TEXT,
34+
VOICE_ID,
35+
create_polly_client,
36+
)
37+
38+
ERROR_EVENT_TYPES = (
39+
StartSpeechSynthesisStreamEventStreamValidationException,
40+
StartSpeechSynthesisStreamEventStreamServiceQuotaExceededException,
41+
StartSpeechSynthesisStreamEventStreamServiceFailureException,
42+
StartSpeechSynthesisStreamEventStreamThrottlingException,
43+
)
44+
45+
46+
async def _send_text(
47+
stream: DuplexEventStream[
48+
StartSpeechSynthesisStreamActionStream,
49+
StartSpeechSynthesisStreamEventStream,
50+
StartSpeechSynthesisStreamOutput,
51+
],
52+
) -> None:
53+
"""Send text input and close the input stream."""
54+
await stream.input_stream.send(
55+
StartSpeechSynthesisStreamActionStreamTextEvent(value=TextEvent(text=TEST_TEXT))
56+
)
57+
await stream.input_stream.send(
58+
StartSpeechSynthesisStreamActionStreamCloseStreamEvent(CloseStreamEvent())
59+
)
60+
await stream.input_stream.close()
61+
62+
63+
async def _receive_audio(
64+
stream: DuplexEventStream[
65+
StartSpeechSynthesisStreamActionStream,
66+
StartSpeechSynthesisStreamEventStream,
67+
StartSpeechSynthesisStreamOutput,
68+
],
69+
) -> tuple[int, int | None]:
70+
"""Receive synthesized audio and the final stream summary."""
71+
audio_bytes = 0
72+
request_characters: int | None = None
73+
74+
_, output_stream = await stream.await_output()
75+
if output_stream is None:
76+
return audio_bytes, request_characters
77+
78+
async for event in output_stream:
79+
if isinstance(event, StartSpeechSynthesisStreamEventStreamAudioEvent):
80+
if event.value.audio_chunk:
81+
audio_bytes += len(event.value.audio_chunk)
82+
elif isinstance(event, StartSpeechSynthesisStreamEventStreamStreamClosedEvent):
83+
request_characters = event.value.request_characters
84+
break
85+
elif isinstance(event, ERROR_EVENT_TYPES):
86+
raise event.value
87+
elif isinstance(event, StartSpeechSynthesisStreamEventStreamUnknown):
88+
raise RuntimeError(f"Received unknown event in stream: {event.tag}")
89+
else:
90+
raise RuntimeError(
91+
f"Received unexpected event type in stream: {type(event).__name__}"
92+
)
93+
94+
return audio_bytes, request_characters
95+
96+
97+
async def test_start_speech_synthesis_stream() -> None:
98+
"""Test bidirectional streaming with text input and audio output."""
99+
client = create_polly_client(REGION)
100+
101+
stream = await client.start_speech_synthesis_stream(
102+
input=StartSpeechSynthesisStreamInput(
103+
engine=ENGINE,
104+
output_format=OUTPUT_FORMAT,
105+
sample_rate=SAMPLE_RATE,
106+
voice_id=VOICE_ID,
107+
)
108+
)
109+
110+
results = await asyncio.gather(_send_text(stream), _receive_audio(stream))
111+
audio_bytes, request_characters = results[1]
112+
113+
assert audio_bytes > 0, "Expected to receive synthesized audio"
114+
assert request_characters == len(TEST_TEXT)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test non-streaming output type handling."""
5+
6+
from aws_sdk_polly.models import DescribeVoicesInput, DescribeVoicesOutput
7+
8+
from . import ENGINE, REGION, VOICE_ID, create_polly_client
9+
10+
11+
async def test_describe_voices() -> None:
12+
"""Test non-streaming DescribeVoices operation."""
13+
client = create_polly_client(REGION)
14+
15+
response = await client.describe_voices(input=DescribeVoicesInput(engine=ENGINE))
16+
17+
assert isinstance(response, DescribeVoicesOutput)
18+
assert response.voices is not None
19+
assert len(response.voices) > 0
20+
21+
voices_by_id = {voice.id: voice for voice in response.voices if voice.id}
22+
assert VOICE_ID in voices_by_id
23+
24+
voice = voices_by_id[VOICE_ID]
25+
assert voice.language_code == "en-US"
26+
assert voice.supported_engines is not None
27+
assert ENGINE in voice.supported_engines
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test output streaming blob handling."""
5+
6+
from smithy_core.aio.utils import read_streaming_blob_async
7+
8+
from aws_sdk_polly.models import SynthesizeSpeechInput, SynthesizeSpeechOutput
9+
10+
from . import (
11+
ENGINE,
12+
OUTPUT_FORMAT,
13+
REGION,
14+
SAMPLE_RATE,
15+
TEST_TEXT,
16+
VOICE_ID,
17+
create_polly_client,
18+
)
19+
20+
21+
async def test_synthesize_speech() -> None:
22+
"""Test output-streaming SynthesizeSpeech operation."""
23+
client = create_polly_client(REGION)
24+
25+
response = await client.synthesize_speech(
26+
input=SynthesizeSpeechInput(
27+
engine=ENGINE,
28+
output_format=OUTPUT_FORMAT,
29+
sample_rate=SAMPLE_RATE,
30+
text=TEST_TEXT,
31+
voice_id=VOICE_ID,
32+
)
33+
)
34+
35+
assert isinstance(response, SynthesizeSpeechOutput)
36+
assert response.content_type == "audio/mpeg"
37+
assert response.request_characters == len(TEST_TEXT)
38+
39+
audio = await read_streaming_blob_async(response.audio_stream)
40+
assert len(audio) > 0

0 commit comments

Comments
 (0)