Skip to content

Commit e0714e3

Browse files
committed
Add live integration tests for the Polly and Transcribe speech clients
Integration tests belong with the code they cover, so the speech-clients PR now carries them. AWS.Speech.MEAI.IntegrationTests is a net8.0 project that runs against live AWS with the ambient credential and region chain. The tests always run; they are not gated on credentials, so a missing or unauthorized environment fails them rather than skipping. - Polly GetAudioAsync returns non-empty audio/lpcm. - Polly GetStreamingAudioAsync emits SessionOpen, audio chunks, SessionClose. - Transcribe transcribes Polly-synthesized speech: the phrase is synthesized to 16 kHz PCM, a second of trailing silence is appended so Transcribe endpoints and stabilizes a final result, and the recognized text is asserted to contain a distinctive word. This exercises both clients end to end with no checked-in audio fixture. Registered in AWS.DotNetAI.slnx. Also suppresses the xUnit1051 analyzer advisory in the unit-test project (those tests drive the loop with their own tokens). Verified: all three integration tests pass against live AWS in us-west-2.
1 parent 9dfd09e commit e0714e3

4 files changed

Lines changed: 139 additions & 2 deletions

File tree

AWS.DotNetAI.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@
2626
<Project Path="test/AWS.AgentCore.Hosting.IntegrationTests/AWS.AgentCore.Hosting.IntegrationTests.csproj" />
2727
<Project Path="test/AWS.Bedrock.MEAI.UnitTests/AWS.Bedrock.MEAI.UnitTests.csproj" />
2828
<Project Path="test/AWS.Speech.MEAI.UnitTests/AWS.Speech.MEAI.UnitTests.csproj" />
29+
<Project Path="test/AWS.Speech.MEAI.IntegrationTests/AWS.Speech.MEAI.IntegrationTests.csproj" />
2930
</Folder>
3031
</Solution>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<!-- Amazon Transcribe streaming and the VoiceAgent loop are net8.0-only, so the integration
6+
tests target net8.0 and run against live AWS. -->
7+
<TargetFramework>net8.0</TargetFramework>
8+
<Nullable>enable</Nullable>
9+
<LangVersion>Latest</LangVersion>
10+
<IsPackable>false</IsPackable>
11+
<!-- MEAI001: uses evaluation-stage Microsoft.Extensions.AI APIs. -->
12+
<NoWarn>$(NoWarn);MEAI001</NoWarn>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.8.3" />
17+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
18+
<PackageReference Include="xunit.v3" Version="3.2.2" />
19+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\..\src\AWS.Speech.MEAI\AWS.Speech.MEAI.csproj" />
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Amazon.Polly;
5+
using Amazon.TranscribeStreaming;
6+
using Microsoft.Extensions.AI;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Xunit;
14+
15+
namespace AWS.Speech.MEAI.IntegrationTests;
16+
17+
/// <summary>
18+
/// Live integration tests for the standalone speech clients. These call Amazon Polly and Amazon
19+
/// Transcribe with the ambient AWS credential and region chain and always run (they are not gated on
20+
/// credentials): a missing or unauthorized environment fails the test rather than skipping it.
21+
/// </summary>
22+
[Trait("Category", "Integration")]
23+
public class SpeechClientsIntegrationTests
24+
{
25+
private const string SpokenPhrase = "the quick brown fox jumps over the lazy dog";
26+
private static readonly string[] DistinctiveWords = { "quick", "brown", "fox", "lazy", "dog" };
27+
28+
[Fact]
29+
[Trait("IntegrationTest", "Speech")]
30+
public async Task Polly_GetAudioAsync_ReturnsPcmAudio()
31+
{
32+
using var polly = new AmazonPollyClient();
33+
var tts = polly.AsITextToSpeechClient();
34+
35+
var response = await tts.GetAudioAsync("Hello from Amazon Polly.", cancellationToken: TestContext.Current.CancellationToken);
36+
37+
var audio = Assert.IsType<DataContent>(Assert.Single(response.Contents));
38+
Assert.Equal("audio/lpcm", audio.MediaType);
39+
Assert.False(audio.Data.IsEmpty, "Amazon Polly returned no audio.");
40+
}
41+
42+
[Fact]
43+
[Trait("IntegrationTest", "Speech")]
44+
public async Task Polly_GetStreamingAudioAsync_EmitsSessionFramedChunks()
45+
{
46+
using var polly = new AmazonPollyClient();
47+
var tts = polly.AsITextToSpeechClient();
48+
49+
var kinds = new List<TextToSpeechResponseUpdateKind>();
50+
long audioBytes = 0;
51+
await foreach (var update in tts.GetStreamingAudioAsync("Streaming from Amazon Polly.")
52+
.WithCancellation(TestContext.Current.CancellationToken))
53+
{
54+
kinds.Add(update.Kind);
55+
foreach (var content in update.Contents.OfType<DataContent>())
56+
{
57+
audioBytes += content.Data.Length;
58+
}
59+
}
60+
61+
Assert.Equal(TextToSpeechResponseUpdateKind.SessionOpen, kinds.First());
62+
Assert.Equal(TextToSpeechResponseUpdateKind.SessionClose, kinds.Last());
63+
Assert.Contains(TextToSpeechResponseUpdateKind.AudioUpdating, kinds);
64+
Assert.True(audioBytes > 0, "Amazon Polly streamed no audio.");
65+
}
66+
67+
[Fact]
68+
[Trait("IntegrationTest", "Speech")]
69+
public async Task Transcribe_TranscribesPollyGeneratedSpeech()
70+
{
71+
// Synthesize the phrase with Amazon Polly (16 kHz mono PCM), then transcribe it with Amazon
72+
// Transcribe. This exercises both clients end to end without a checked-in audio fixture.
73+
using var polly = new AmazonPollyClient();
74+
var tts = polly.AsITextToSpeechClient(defaultSampleRateHertz: 16000);
75+
var synthesized = await tts.GetAudioAsync(SpokenPhrase, cancellationToken: TestContext.Current.CancellationToken);
76+
var speech = synthesized.Contents.OfType<DataContent>().Single().Data.ToArray();
77+
Assert.True(speech.Length > 0, "Amazon Polly returned no audio to transcribe.");
78+
79+
// Append ~1s of trailing silence (16 kHz, 16-bit mono => 32000 bytes) so Amazon Transcribe
80+
// detects end-of-utterance and stabilizes a final result rather than only partials.
81+
var pcm = new byte[speech.Length + 32000];
82+
Array.Copy(speech, pcm, speech.Length);
83+
84+
using var transcribe = new AmazonTranscribeStreamingClient();
85+
var stt = transcribe.AsISpeechToTextClient("en-US", 16000);
86+
87+
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
88+
timeout.CancelAfter(TimeSpan.FromSeconds(60));
89+
90+
string? latestRecognized = null;
91+
bool sawFinal = false;
92+
using var audioStream = new MemoryStream(pcm);
93+
await foreach (var update in stt.GetStreamingTextAsync(audioStream, cancellationToken: timeout.Token))
94+
{
95+
if ((update.Kind == SpeechToTextResponseUpdateKind.TextUpdating ||
96+
update.Kind == SpeechToTextResponseUpdateKind.TextUpdated) && !string.IsNullOrEmpty(update.Text))
97+
{
98+
latestRecognized = update.Text; // successive results refine the same utterance
99+
sawFinal |= update.Kind == SpeechToTextResponseUpdateKind.TextUpdated;
100+
}
101+
}
102+
103+
Assert.False(string.IsNullOrWhiteSpace(latestRecognized), "Amazon Transcribe recognized no speech.");
104+
Assert.True(sawFinal, "Amazon Transcribe produced no final (TextUpdated) result.");
105+
var transcript = latestRecognized!.ToLowerInvariant();
106+
Assert.Contains(DistinctiveWords, word => transcript.Contains(word, StringComparison.Ordinal));
107+
}
108+
}

test/AWS.Speech.MEAI.UnitTests/AWS.Speech.MEAI.UnitTests.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
<Nullable>enable</Nullable>
1313
<LangVersion>Latest</LangVersion>
1414
<IsPackable>false</IsPackable>
15-
<!-- MEAI001: uses evaluation-stage Microsoft.Extensions.AI APIs. -->
16-
<NoWarn>$(NoWarn);MEAI001</NoWarn>
15+
<!-- MEAI001: uses evaluation-stage Microsoft.Extensions.AI APIs.
16+
xUnit1051: these unit tests deliberately drive the loop with their own tokens (or none);
17+
they do not need TestContext.Current.CancellationToken threaded through every call. -->
18+
<NoWarn>$(NoWarn);MEAI001;xUnit1051</NoWarn>
1719
<SignAssembly>true</SignAssembly>
1820
<AssemblyOriginatorKeyFile>..\..\public.snk</AssemblyOriginatorKeyFile>
1921
</PropertyGroup>

0 commit comments

Comments
 (0)