From db6b7e31c01a83bf92b11e3d10d3bc34cbf7f227 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:43:03 -0400 Subject: [PATCH 01/27] Initial speech to text abstractions --- eng/spellchecking_exclusions.dic | Bin 198 -> 216 bytes .../ChatCompletion/ChatResponseExtensions.cs | 98 ++--- .../Contents/AIContent.cs | 1 + .../Contents/ErrorContent.cs | 50 +++ .../DelegatingSpeechToTextClient.cs | 74 ++++ .../SpeechToText/ISpeechToTextClient.cs | 57 +++ .../SpeechToTextClientExtensions.cs | 116 ++++++ .../SpeechToTextClientMetadata.cs | 41 ++ .../SpeechToText/SpeechToTextMessage.cs | 96 +++++ .../SpeechToText/SpeechToTextOptions.cs | 57 +++ .../SpeechToText/SpeechToTextResponse.cs | 143 +++++++ .../SpeechToTextResponseUpdate.cs | 123 ++++++ .../SpeechToTextResponseUpdateExtensions.cs | 167 ++++++++ .../SpeechToTextResponseUpdateKind.cs | 103 +++++ .../Utilities/AIJsonUtilities.Defaults.cs | 5 + .../Utilities/StreamExtensions.cs | 47 +++ .../AsyncEnumerableExtensions.cs | 25 ++ .../DataContentAsyncEnumerableStream.cs | 205 +++++++++ .../OpenAIClientExtensions.cs | 14 + .../OpenAIModelMapper.AudioTranscription.cs | 127 ++++++ .../OpenAIModelMapper.AudioTranslation.cs | 103 +++++ .../OpenAIModelMapper.ChatCompletion.cs | 2 +- .../OpenAISpeechToTextClient.cs | 220 ++++++++++ .../AnonymousDelegatingSpeechToTextClient.cs | 217 ++++++++++ .../ConfigureOptionsSpeechToTextClient.cs | 58 +++ ...ionsSpeechToTextClientBuilderExtensions.cs | 36 ++ .../SpeechToText/LoggingSpeechToTextClient.cs | 196 +++++++++ .../LoggingSpeechToTextClientExtensions.cs | 46 +++ .../SpeechToText/SpeechToTextClientBuilder.cs | 140 +++++++ ...lientBuilderServiceCollectionExtensions.cs | 79 ++++ ...ientBuilderSpeechToTextClientExtensions.cs | 25 ++ .../Contents/ErrorContentTests.cs | 53 +++ .../DataContentAsyncEnumerableStreamTests.cs | 352 ++++++++++++++++ .../DelegatingSpeechToTextClientTests.cs | 165 ++++++++ .../SpeechToTextClientExtensionsTests.cs | 95 +++++ .../SpeechToTextClientMetadataTests.cs | 29 ++ .../SpeechToText/SpeechToTextClientTests.cs | 44 ++ .../SpeechToText/SpeechToTextMessageTests.cs | 353 ++++++++++++++++ .../SpeechToText/SpeechToTextOptionsTests.cs | 113 +++++ .../SpeechToText/SpeechToTextResponseTests.cs | 298 ++++++++++++++ .../SpeechToTextResponseUpdateKindTests.cs | 65 +++ .../SpeechToTextResponseUpdateTests.cs | 170 ++++++++ .../TestJsonSerializerContext.cs | 4 + .../TestSpeechToTextClient.cs | 60 +++ .../DelegatedHttpHandler.cs | 20 + ...oft.Extensions.AI.Integration.Tests.csproj | 10 +- .../Resources/audio001.wav | Bin 0 -> 138248 bytes .../Resources/audio002.wav | Bin 0 -> 149768 bytes .../{ => Resources}/dotnet.png | Bin .../SpeechToTextClientIntegrationTests.cs | 130 ++++++ .../VerbatimMultiPartHttpHandler.cs | 215 ++++++++++ ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 5 + ...penAISpeechToTextClientIntegrationTests.cs | 11 + .../OpenAISpeechToTextClientTests.cs | 388 ++++++++++++++++++ .../Resources/audio001.mp3 | Bin 0 -> 13400 bytes .../Resources/audio001.wav | Bin 0 -> 138248 bytes .../Microsoft.Extensions.AI.Tests.csproj | 1 + ...ConfigureOptionsSpeechToTextClientTests.cs | 98 +++++ .../LoggingSpeechToTextClientTests.cs | 155 +++++++ .../SingletonSpeechToTextClientExtensions.cs | 11 + ...ToTextClientDependencyInjectionPatterns.cs | 162 ++++++++ .../UseDelegateSpeechToTextClientTests.cs | 261 ++++++++++++ 62 files changed, 5888 insertions(+), 51 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ErrorContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateKindTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.wav create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio002.wav rename test/Libraries/Microsoft.Extensions.AI.Integration.Tests/{ => Resources}/dotnet.png (100%) create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimMultiPartHttpHandler.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.mp3 create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.wav create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SingletonSpeechToTextClientExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs diff --git a/eng/spellchecking_exclusions.dic b/eng/spellchecking_exclusions.dic index 7259681651670edef6d5aad2d32ac8843ddc50fe..2abdfbd64a2f3bcb843520b8144acfc2921e240f 100644 GIT binary patch delta 25 gcmX@cc!P1mF`@te@)-&kQW^3X5*acXcp11D0D?^inE(I) delta 6 Ncmcb?c#LtvF#rl20`~v_ diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 84eb7b99e8e..9cc27f14924 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -180,6 +180,55 @@ static async Task ToChatResponseAsync( } } + /// Coalesces sequential content elements. + internal static void CoalesceTextContent(List contents) + { + StringBuilder? coalescedText = null; + + // Iterate through all of the items in the list looking for contiguous items that can be coalesced. + int start = 0; + while (start < contents.Count - 1) + { + // We need at least two TextContents in a row to be able to coalesce. + if (contents[start] is not TextContent firstText) + { + start++; + continue; + } + + if (contents[start + 1] is not TextContent secondText) + { + start += 2; + continue; + } + + // Append the text from those nodes and continue appending subsequent TextContents until we run out. + // We null out nodes as their text is appended so that we can later remove them all in one O(N) operation. + coalescedText ??= new(); + _ = coalescedText.Clear().Append(firstText.Text).Append(secondText.Text); + contents[start + 1] = null!; + int i = start + 2; + for (; i < contents.Count && contents[i] is TextContent next; i++) + { + _ = coalescedText.Append(next.Text); + contents[i] = null!; + } + + // Store the replacement node. + contents[start] = new TextContent(coalescedText.ToString()) + { + // We inherit the properties of the first text node. We don't currently propagate additional + // properties from the subsequent nodes. If we ever need to, we can add that here. + AdditionalProperties = firstText.AdditionalProperties?.Clone(), + }; + + start = i; + } + + // Remove all of the null slots left over from the coalescing process. + _ = contents.RemoveAll(u => u is null); + } + /// Finalizes the object. private static void FinalizeResponse(ChatResponse response) { @@ -280,53 +329,4 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon } } } - - /// Coalesces sequential content elements. - private static void CoalesceTextContent(List contents) - { - StringBuilder? coalescedText = null; - - // Iterate through all of the items in the list looking for contiguous items that can be coalesced. - int start = 0; - while (start < contents.Count - 1) - { - // We need at least two TextContents in a row to be able to coalesce. - if (contents[start] is not TextContent firstText) - { - start++; - continue; - } - - if (contents[start + 1] is not TextContent secondText) - { - start += 2; - continue; - } - - // Append the text from those nodes and continue appending subsequent TextContents until we run out. - // We null out nodes as their text is appended so that we can later remove them all in one O(N) operation. - coalescedText ??= new(); - _ = coalescedText.Clear().Append(firstText.Text).Append(secondText.Text); - contents[start + 1] = null!; - int i = start + 2; - for (; i < contents.Count && contents[i] is TextContent next; i++) - { - _ = coalescedText.Append(next.Text); - contents[i] = null!; - } - - // Store the replacement node. - contents[start] = new TextContent(coalescedText.ToString()) - { - // We inherit the properties of the first text node. We don't currently propagate additional - // properties from the subsequent nodes. If we ever need to, we can add that here. - AdditionalProperties = firstText.AdditionalProperties?.Clone(), - }; - - start = i; - } - - // Remove all of the null slots left over from the coalescing process. - _ = contents.RemoveAll(u => u is null); - } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 6562b7bcc42..068bd1ce447 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -8,6 +8,7 @@ namespace Microsoft.Extensions.AI; /// Provides a base class for all content used with AI services. [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(DataContent), typeDiscriminator: "data")] +[JsonDerivedType(typeof(ErrorContent), typeDiscriminator: "error")] [JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: "functionCall")] [JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: "functionResult")] [JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs new file mode 100644 index 00000000000..276c85f0a52 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents an error content. +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public class ErrorContent : AIContent +{ + /// Initializes a new instance of the class with the specified message. + /// The message to store in this content. + [JsonConstructor] + public ErrorContent(string message) + { + Message = Throw.IfNull(message); + } + + /// Gets or sets the error message. + public string Message { get; set; } + + /// Gets or sets the error code. + public string? ErrorCode { get; set; } + + /// Gets or sets the error details. + public string? Details { get; set; } + + /// Gets a string representing this instance to display in the debugger. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + string display = $"Message = {Message} "; + + display += ErrorCode is not null ? + $", ErrorCode = {ErrorCode}" : string.Empty; + + display += Details is not null ? + $", Details = {Details}" : string.Empty; + + return display; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs new file mode 100644 index 00000000000..11caeff92e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an optional base class for an that passes through calls to another instance. +/// +/// +/// This is recommended as a base type when building clients that can be chained in any order around an underlying . +/// The default implementation simply passes each call to the inner client instance. +/// +public class DelegatingSpeechToTextClient : ISpeechToTextClient +{ + /// + /// Initializes a new instance of the class. + /// + /// The wrapped client instance. + protected DelegatingSpeechToTextClient(ISpeechToTextClient innerClient) + { + InnerClient = Throw.IfNull(innerClient); + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// Gets the inner . + protected ISpeechToTextClient InnerClient { get; } + + /// + public virtual Task GetResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerClient.GetResponseAsync(speechContents, options, cancellationToken); + } + + /// + public virtual IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerClient.GetStreamingResponseAsync(speechContents, options, cancellationToken); + } + + /// + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + // If the key is non-null, we don't know what it means so pass through to the inner service. + return + serviceKey is null && serviceType.IsInstanceOfType(this) ? this : + InnerClient.GetService(serviceType, serviceKey); + } + + /// Provides a mechanism for releasing unmanaged resources. + /// if being called from ; otherwise, . + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + InnerClient.Dispose(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs new file mode 100644 index 00000000000..b4370a22ddb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// Represents a speech to text client. +/// +/// +/// Unless otherwise specified, all members of are thread-safe for concurrent use. +/// It is expected that all implementations of support being used by multiple requests concurrently. +/// +/// +/// However, implementations of might mutate the arguments supplied to and +/// , such as by configuring the options instance. Thus, consumers of the interface either should avoid +/// using shared instances of these arguments for concurrent invocations or should otherwise ensure by construction that no +/// instances are used which might employ such mutation. For example, the ConfigureOptions method be +/// provided with a callback that could mutate the supplied options argument, and that should be avoided if using a singleton options instance. +/// +/// +public interface ISpeechToTextClient : IDisposable +{ + /// Sends speech speech audio contents to the model and returns the generated text. + /// The list of speech speech audio contents to send. + /// The speech to text options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The text generated by the client. + Task GetResponseAsync( + IList> speechContents, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default); + + /// Sends speech speech audio contents to the model and streams back the generated text. + /// The list of speech speech audio contents to send. + /// The speech to text options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The response messages generated by the client. + IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default); + + /// Asks the for an object of the specified type . + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that might be provided by the , + /// including itself or any services it might be wrapping. + /// + object? GetService(Type serviceType, object? serviceKey = null); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs new file mode 100644 index 00000000000..12ee9cb8085 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Extensions for . +public static class SpeechToTextClientExtensions +{ + /// Asks the for an object of type . + /// The type of the object to be retrieved. + /// The client. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , + /// including itself or any services it might be wrapping. + /// + public static TService? GetService(this ISpeechToTextClient client, object? serviceKey = null) + { + _ = Throw.IfNull(client); + + return (TService?)client.GetService(typeof(TService), serviceKey); + } + + /// Generates text from speech providing a single speech audio . + /// The client. + /// The single speech audio content. + /// The speech to text options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The text generated by the client. + public static Task GetResponseAsync( + this ISpeechToTextClient client, + DataContent speechContent, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default) + { + IEnumerable speechContents = [Throw.IfNull(speechContent)]; + return Throw.IfNull(client) + .GetResponseAsync( + [speechContents.ToAsyncEnumerable()], + options, + cancellationToken); + } + + /// Generates text from speech providing a single speech audio . + /// The client. + /// The single speech audio stream. + /// The speech to text options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The text generated by the client. + public static Task GetResponseAsync( + this ISpeechToTextClient client, + Stream speechStream, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default) + => Throw.IfNull(client) + .GetResponseAsync( + [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], + options, + cancellationToken); + + /// Generates text from speech providing a single speech audio . + /// The client. + /// The single speech audio stream. + /// The speech to text options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The text generated by the client. + public static IAsyncEnumerable GetStreamingResponseAsync( + this ISpeechToTextClient client, + Stream speechStream, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default) + => Throw.IfNull(client) + .GetStreamingResponseAsync( + [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], + options, + cancellationToken); + + /// Generates text from speech providing a single speech audio . + /// The client. + /// The single speech audio content. + /// The speech to text options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The text generated by the client. + public static IAsyncEnumerable GetStreamingResponseAsync( + this ISpeechToTextClient client, + DataContent speechContent, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default) + { + IEnumerable speechContents = [Throw.IfNull(speechContent)]; + return Throw.IfNull(client) + .GetStreamingResponseAsync( + [speechContents.ToAsyncEnumerable()], + options, + cancellationToken); + } + +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + private static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) + { + foreach (var item in source) + { + yield return item; + } + } +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods +#pragma warning restore CS1998 // Unused private types or members should be removed +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs new file mode 100644 index 00000000000..49e35824757 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.AI; + +/// Provides metadata about an . +public class SpeechToTextClientMetadata +{ + /// Initializes a new instance of the class. + /// + /// The name of the speech to text provider, if applicable. Where possible, this should map to the + /// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems. + /// + /// The URL for accessing the speech to text provider, if applicable. + /// The ID of the speech to text model used, if applicable. + public SpeechToTextClientMetadata(string? providerName = null, Uri? providerUri = null, string? modelId = null) + { + ModelId = modelId; + ProviderName = providerName; + ProviderUri = providerUri; + } + + /// Gets the name of the speech to text provider. + /// + /// Where possible, this maps to the appropriate name defined in the + /// OpenTelemetry Semantic Conventions for Generative AI systems. + /// + public string? ProviderName { get; } + + /// Gets the URL for accessing the speech to text provider. + public Uri? ProviderUri { get; } + + /// Gets the ID of the model used by this speech to text provider. + /// + /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance. + /// An individual request may override this value via . + /// + public string? ModelId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs new file mode 100644 index 00000000000..ccfdbb08622 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Represents a choice in an speech to text. +public class SpeechToTextMessage +{ + private IList? _contents; + + /// Initializes a new instance of the class. + [JsonConstructor] + public SpeechToTextMessage() + { + } + + /// Initializes a new instance of the class. + /// Content of the message. + public SpeechToTextMessage(string? content) + : this(content is null ? [] : [new TextContent(content)]) + { + } + + /// Initializes a new instance of the class. + /// The contents for this message. + public SpeechToTextMessage( + IList contents) + { + _contents = Throw.IfNull(contents); + } + + /// Gets or sets the start time of the speech to text choice. + /// This represents the start of the generated text in relation to the original speech audio source length. + public TimeSpan? StartTime { get; set; } + + /// Gets or sets the end time of the speech to text choice. + /// This represents the end of the generated text in relation to the original speech audio source length. + public TimeSpan? EndTime { get; set; } + + /// + /// Gets or sets the text of the first instance in . + /// + /// + /// If there is no instance in , then the getter returns , + /// and the setter adds a new instance with the provided value. + /// + [JsonIgnore] + public string? Text + { + get => Contents.OfType().FirstOrDefault()?.Text; + set + { + if (Contents.OfType().FirstOrDefault() is { } textContent) + { + textContent.Text = value; + } + else if (value is not null) + { + Contents.Add(new TextContent(value)); + } + } + } + + /// Gets or sets the generated content items. + [AllowNull] + public IList Contents + { + get => _contents ??= []; + set => _contents = value; + } + + /// Gets or sets the zero-based index of the input list with which this choice is associated. + public int InputIndex { get; set; } + + /// Gets or sets the raw representation of the speech to text choice from an underlying implementation. + /// + /// If a is created to represent some underlying object from another object + /// model, this property can be used to store that original object. This can be useful for debugging or + /// for enabling a consumer to access the underlying object model if needed. + /// + [JsonIgnore] + public object? RawRepresentation { get; set; } + + /// Gets or sets any additional properties associated with the message. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + + /// + public override string ToString() => Contents.ConcatText(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs new file mode 100644 index 00000000000..1c174eae051 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace Microsoft.Extensions.AI; + +/// Represents the options for an speech to text request. +public class SpeechToTextOptions +{ + private CultureInfo? _speechLanguage; + private CultureInfo? _textLanguage; + + /// Gets or sets the ID for the speech to text. + /// Long running jobs may use this ID for status pooling. + public string? ResponseId { get; set; } + + /// Gets or sets the model ID for the speech to text. + public string? ModelId { get; set; } + + /// Gets or sets the language of source speech. + public string? SpeechLanguage + { + get => _speechLanguage?.Name; + set => _speechLanguage = value is null ? null : CultureInfo.GetCultureInfo(value); + } + + /// Gets or sets the language for the target generated text. + public string? TextLanguage + { + get => _textLanguage?.Name; + set => _textLanguage = value is null ? null : CultureInfo.GetCultureInfo(value); + } + + /// Gets or sets the sample rate of the speech input audio. + public int? SpeechSampleRate { get; set; } + + /// Gets or sets any additional properties associated with the options. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + + /// Produces a clone of the current instance. + /// A clone of the current instance. + public virtual SpeechToTextOptions Clone() + { + SpeechToTextOptions options = new() + { + ResponseId = ResponseId, + ModelId = ModelId, + SpeechLanguage = SpeechLanguage, + TextLanguage = TextLanguage, + SpeechSampleRate = SpeechSampleRate, + AdditionalProperties = AdditionalProperties?.Clone(), + }; + + return options; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs new file mode 100644 index 00000000000..a4546633cd4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Represents the result of an speech to text request. +public class SpeechToTextResponse +{ + /// The list of choices in the generated text response. + private IList _choices; + + /// Initializes a new instance of the class. + /// the generated text representing the singular choice message in the response. + public SpeechToTextResponse(SpeechToTextMessage message) + : this([Throw.IfNull(message)]) + { + } + + /// Initializes a new instance of the class. + /// The list of choices in the response, one message per choice. + [JsonConstructor] + public SpeechToTextResponse(IList choices) + { + _choices = Throw.IfNull(choices); + } + + /// Gets the speech to text message details. + /// + /// If no speech to text was generated, this property will throw. + /// + [JsonIgnore] + public SpeechToTextMessage Message + { + get + { + var choices = Choices; + if (choices.Count == 0) + { + throw new InvalidOperationException($"The {nameof(SpeechToTextResponse)} instance does not contain any {nameof(SpeechToTextMessage)} choices."); + } + + return choices[0]; + } + } + + /// Gets or sets the ID of the speech to text response. + public string? ResponseId { get; set; } + + /// Gets or sets the model ID used in the creation of the speech to text completion. + public string? ModelId { get; set; } + + /// Gets or sets the raw representation of the speech to text completion from an underlying implementation. + /// + /// If a is created to represent some underlying object from another object + /// model, this property can be used to store that original object. This can be useful for debugging or + /// for enabling a consumer to access the underlying object model if needed. + /// + [JsonIgnore] + public object? RawRepresentation { get; set; } + + /// Gets or sets any additional properties associated with the speech to text completion. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + + /// + public override string ToString() + { + if (Choices.Count == 1) + { + return Choices[0].ToString(); + } + + StringBuilder sb = new(); + for (int i = 0; i < Choices.Count; i++) + { + if (i > 0) + { + _ = sb.AppendLine().AppendLine(); + } + + _ = sb.Append("Choice ").Append(i).AppendLine(":").Append(Choices[i]); + } + + return sb.ToString(); + } + + /// Creates an array of instances that represent this . + /// An array of instances that may be used to represent this . + public SpeechToTextResponseUpdate[] ToSpeechToTextResponseUpdates() + { + SpeechToTextResponseUpdate? extra = null; + if (AdditionalProperties is not null) + { + extra = new SpeechToTextResponseUpdate + { + Kind = SpeechToTextResponseUpdateKind.TextUpdated, + AdditionalProperties = AdditionalProperties, + }; + } + + int choicesCount = Choices.Count; + var updates = new SpeechToTextResponseUpdate[choicesCount + (extra is null ? 0 : 1)]; + + for (int choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) + { + SpeechToTextMessage choice = Choices[choiceIndex]; + updates[choiceIndex] = new SpeechToTextResponseUpdate + { + ChoiceIndex = choiceIndex, + InputIndex = choice.InputIndex, + + AdditionalProperties = choice.AdditionalProperties, + Contents = choice.Contents, + RawRepresentation = choice.RawRepresentation, + StartTime = choice.StartTime, + EndTime = choice.EndTime, + + Kind = SpeechToTextResponseUpdateKind.TextUpdated, + ResponseId = ResponseId, + ModelId = ModelId, + }; + } + + if (extra is not null) + { + updates[choicesCount] = extra; + } + + return updates; + } + + /// Gets or sets the list of speech to text choices. + public IList Choices + { + get => _choices; + set => _choices = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs new file mode 100644 index 00000000000..e0b1521d9c7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a single streaming response chunk from an . +/// +/// +/// is so named because it represents streaming updates +/// to an speech to text generation. As such, it is considered erroneous for multiple updates that are part +/// of the same speech audio to contain competing values. For example, some updates that are part of +/// the same speech audio may have a value, and others may have a non- value, +/// but all of those with a non- value must have the same value (e.g. ). +/// +/// +/// The relationship between and is +/// codified in the and +/// , which enable bidirectional conversions +/// between the two. Note, however, that the conversion may be slightly lossy, for example if multiple updates +/// all have different objects whereas there's +/// only one slot for such an object available in . +/// +/// +public class SpeechToTextResponseUpdate +{ + private IList? _contents; + + /// Initializes a new instance of the class. + [JsonConstructor] + public SpeechToTextResponseUpdate() + { + } + + /// Initializes a new instance of the class. + /// The contents for this message. + public SpeechToTextResponseUpdate(IList contents) + { + _contents = Throw.IfNull(contents); + } + + /// Initializes a new instance of the class. + /// Content of the message. + public SpeechToTextResponseUpdate(string? content) + : this(content is null ? [] : [new TextContent(content)]) + { + } + + /// Gets or sets the zero-based index of the input list with which this update is associated in the streaming sequence. + public int InputIndex { get; set; } + + /// Gets or sets the zero-based index of the resulting choice with which this update is associated in the streaming sequence. + public int ChoiceIndex { get; set; } + + /// Gets or sets the kind of the generated text update. + public SpeechToTextResponseUpdateKind Kind { get; set; } = SpeechToTextResponseUpdateKind.TextUpdating; + + /// Gets or sets the ID of the generated text response of which this update is a part. + public string? ResponseId { get; set; } + + /// Gets or sets the start time of the text segment associated with this update in relation to the full speech audio length. + public TimeSpan? StartTime { get; set; } + + /// Gets or sets the end time of the text segment associated with this update in relation to the full speech audio length. + public TimeSpan? EndTime { get; set; } + + /// Gets or sets the model ID using in the creation of the speech to text of which this update is a part. + public string? ModelId { get; set; } + + /// Gets or sets the raw representation of the generated text update from an underlying implementation. + /// + /// If a is created to represent some underlying object from another object + /// model, this property can be used to store that original object. This can be useful for debugging or + /// for enabling a consumer to access the underlying object model if needed. + /// + [JsonIgnore] + public object? RawRepresentation { get; set; } + + /// Gets or sets additional properties for the update. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + + /// + /// Gets or sets the text of the first instance in . + /// + /// + /// If there is no instance in , then the getter returns , + /// and the setter adds a new instance with the provided value. + /// + [JsonIgnore] + public string? Text + { + get => Contents.OfType().FirstOrDefault()?.Text; + set + { + if (Contents.OfType().FirstOrDefault() is { } textContent) + { + textContent.Text = value; + } + else if (value is not null) + { + Contents.Add(new TextContent(value)); + } + } + } + + /// Gets or sets the generated content items. + [AllowNull] + public IList Contents + { + get => _contents ??= []; + set => _contents = value; + } + + /// + public override string ToString() => Contents.ConcatText(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs new file mode 100644 index 00000000000..23f464ea265 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +#if NET +using System.Runtime.InteropServices; +#endif +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides extension methods for working with instances. +/// +public static class SpeechToTextResponseUpdateExtensions +{ + /// Combines instances into a single . + /// The updates to be combined. + /// + /// to attempt to coalesce contiguous items, where applicable, + /// into a single , in order to reduce the number of individual content items that are included in + /// the manufactured instances. When , the original content items are used. + /// The default is . + /// + /// The combined . + public static SpeechToTextResponse ToSpeechToTextResponse( + this IEnumerable updates, bool coalesceContent = true) + { + _ = Throw.IfNull(updates); + + SpeechToTextResponse response = new([]); + Dictionary choices = []; + + foreach (var update in updates) + { + ProcessUpdate(update, choices, response); + } + + AddChoicesToCompletion(choices, response, coalesceContent); + + return response; + } + + /// Combines instances into a single . + /// The updates to be combined. + /// + /// to attempt to coalesce contiguous items, where applicable, + /// into a single , in order to reduce the number of individual content items that are included in + /// the manufactured instances. When , the original content items are used. + /// The default is . + /// + /// The to monitor for cancellation requests. The default is . + /// The combined . + public static Task ToSpeechToTextResponseAsync( + this IAsyncEnumerable updates, bool coalesceContent = true, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(updates); + + return ToResponseAsync(updates, coalesceContent, cancellationToken); + + static async Task ToResponseAsync( + IAsyncEnumerable updates, bool coalesceContent, CancellationToken cancellationToken) + { + SpeechToTextResponse response = new([]); + Dictionary choices = []; + + await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + ProcessUpdate(update, choices, response); + } + + AddChoicesToCompletion(choices, response, coalesceContent); + + return response; + } + } + + /// Processes the , incorporating its contents into and . + /// The update to process. + /// The dictionary mapping to the being built for that choice. + /// The object whose properties should be updated based on . + private static void ProcessUpdate(SpeechToTextResponseUpdate update, Dictionary choices, SpeechToTextResponse response) + { + response.ResponseId ??= update.ResponseId; + response.ModelId ??= update.ModelId; + +#if NET + SpeechToTextMessage choice = CollectionsMarshal.GetValueRefOrAddDefault(choices, update.ChoiceIndex, out _) ??= + new(new List()); +#else + if (!choices.TryGetValue(update.ChoiceIndex, out SpeechToTextMessage? choice)) + { + choices[update.ChoiceIndex] = choice = new(new List()); + } +#endif + + ((List)choice.Contents).AddRange(update.Contents); + + if (update.AdditionalProperties is not null) + { + if (choice.AdditionalProperties is null) + { + choice.AdditionalProperties = new(update.AdditionalProperties); + } + else + { + foreach (var entry in update.AdditionalProperties) + { + // Use first-wins behavior to match the behavior of the other properties. + _ = choice.AdditionalProperties.TryAdd(entry.Key, entry.Value); + } + } + } + } + + /// Finalizes the object by transferring the into it. + /// The messages to process further and transfer into . + /// The result being built. + /// The corresponding option value provided to or . + private static void AddChoicesToCompletion(Dictionary choices, SpeechToTextResponse response, bool coalesceContent) + { + if (choices.Count <= 1) + { + // Add the single message if there is one. + foreach (var entry in choices) + { + AddChoice(response, coalesceContent, entry); + } + + // In the vast majority case where there's only one choice, promote any additional properties + // from the single choice to the speech to text response, making them more discoverable and more similar + // to how they're typically surfaced from non-streaming services. + if (response.Choices.Count == 1 && + response.Choices[0].AdditionalProperties is { } messageProps) + { + response.Choices[0].AdditionalProperties = null; + response.AdditionalProperties = messageProps; + } + } + else + { + // Add all of the messages, sorted by choice index. + foreach (var entry in choices.OrderBy(entry => entry.Key)) + { + AddChoice(response, coalesceContent, entry); + } + + // If there are multiple choices, we don't promote additional properties from the individual messages. + // At a minimum, we'd want to know which choice the additional properties applied to, and if there were + // conflicting values across the choices, it would be unclear which one should be used. + } + + static void AddChoice(SpeechToTextResponse response, bool coalesceContent, KeyValuePair entry) + { + if (coalesceContent) + { + // Consider moving to a utility method. + ChatResponseExtensions.CoalesceTextContent((List)entry.Value.Contents); + } + + response.Choices.Add(entry.Value); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs new file mode 100644 index 00000000000..cfae35f94dd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Describes the intended purpose of a specific update during streaming of speech to text updates. +/// +[JsonConverter(typeof(Converter))] +public readonly struct SpeechToTextResponseUpdateKind : IEquatable +{ + /// Gets when the generated text session is opened. + public static SpeechToTextResponseUpdateKind SessionOpen { get; } = new("sessionopen"); + + /// Gets when a non-blocking error occurs during speech to text updates. + public static SpeechToTextResponseUpdateKind Error { get; } = new("error"); + + /// Gets when the text update is in progress, without waiting for silence. + public static SpeechToTextResponseUpdateKind TextUpdating { get; } = new("textupdating"); + + /// Gets when the text was generated after small period of silence. + public static SpeechToTextResponseUpdateKind TextUpdated { get; } = new("textupdated"); + + /// Gets when the generated text session is closed. + public static SpeechToTextResponseUpdateKind SessionClose { get; } = new("sessionclose"); + + /// + /// Gets the value associated with this . + /// + /// + /// The value will be serialized into the "kind" message field of the speech to text update format. + /// + public string Value { get; } + + /// + /// Initializes a new instance of the struct with the provided value. + /// + /// The value to associate with this . + [JsonConstructor] + public SpeechToTextResponseUpdateKind(string value) + { + Value = Throw.IfNullOrWhitespace(value); + } + + /// + /// Returns a value indicating whether two instances are equivalent, as determined by a + /// case-insensitive comparison of their values. + /// + /// The first instance to compare. + /// The second instance to compare. + /// if left and right are both null or have equivalent values; otherwise, . + public static bool operator ==(SpeechToTextResponseUpdateKind left, SpeechToTextResponseUpdateKind right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances are not equivalent, as determined by a + /// case-insensitive comparison of their values. + /// + /// The first instance to compare. + /// The second instance to compare. + /// if left and right have different values; if they have equivalent values or are both null. + public static bool operator !=(SpeechToTextResponseUpdateKind left, SpeechToTextResponseUpdateKind right) + { + return !(left == right); + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is SpeechToTextResponseUpdateKind otherRole && Equals(otherRole); + + /// + public bool Equals(SpeechToTextResponseUpdateKind other) + => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() + => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SpeechToTextResponseUpdateKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + new(reader.GetString()!); + + /// + public override void Write(Utf8JsonWriter writer, SpeechToTextResponseUpdateKind value, JsonSerializerOptions options) + => Throw.IfNull(writer).WriteStringValue(value.Value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index f7d1c4bf036..2b3b4cf3082 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -79,12 +79,17 @@ private static JsonSerializerOptions CreateDefaultOptions() WriteIndented = true)] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(ChatOptions))] + [JsonSerializable(typeof(SpeechToTextOptions))] [JsonSerializable(typeof(EmbeddingGenerationOptions))] [JsonSerializable(typeof(ChatClientMetadata))] + [JsonSerializable(typeof(SpeechToTextClientMetadata))] [JsonSerializable(typeof(EmbeddingGeneratorMetadata))] [JsonSerializable(typeof(ChatResponse))] [JsonSerializable(typeof(ChatResponseUpdate))] [JsonSerializable(typeof(IReadOnlyList))] + [JsonSerializable(typeof(SpeechToTextResponse))] + [JsonSerializable(typeof(SpeechToTextResponseUpdate))] + [JsonSerializable(typeof(IReadOnlyList))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(JsonDocument))] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs new file mode 100644 index 00000000000..98f402a722f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods + +namespace Microsoft.Extensions.AI; + +/// Provides extension methods for . +public static class StreamExtensions +{ + /// Converts a to an . + /// The to convert. + /// The optional media type of the audio stream. + /// The optional buffer size to use when reading from the audio stream. The default is 4096. + /// The to monitor for cancellation requests. The default is . + /// An . + public static async IAsyncEnumerable ToAsyncEnumerable( + this Stream stream, + string mediaType = "audio/*", + int bufferSize = 4096, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + int bytesRead; +#if NET8_0_OR_GREATER + Memory buffer = new byte[bufferSize]; + while ((bytesRead = await Throw.IfNull(stream).ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) + { + yield return new DataContent(buffer.Slice(0, bytesRead), mediaType)!; + } +#else + var buffer = new byte[bufferSize]; + while ((bytesRead = await Throw.IfNull(stream).ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) + { + byte[] chunk = new byte[bytesRead]; + Array.Copy(buffer, 0, chunk, 0, bytesRead); + yield return new DataContent((ReadOnlyMemory)chunk, mediaType)!; + } +#endif + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs new file mode 100644 index 00000000000..ed8c2c0738a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extension methods for . +internal static class AsyncEnumerableExtensions +{ + /// Converts an to a . + /// The data content async enumerable to convert. + /// The first data content chunk to write back to the stream. + /// The to monitor for cancellation requests. The default is . + /// The stream containing the data content. + /// + /// needs to be considered back in the stream if was iterated before creating the stream. + /// This can happen to check if the first enumerable item contains data or is just a reference only content. + /// + internal static Stream ToStream(this IAsyncEnumerable dataAsyncEnumerable, DataContent? firstDataContent = null, CancellationToken cancellationToken = default) + => new DataContentAsyncEnumerableStream(Throw.IfNull(dataAsyncEnumerable), firstDataContent, cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs new file mode 100644 index 00000000000..ed9b0e031ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs @@ -0,0 +1,205 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Utility class to stream data content as a . +/// +#if !NET8_0_OR_GREATER +internal sealed class DataContentAsyncEnumerableStream : Stream, IAsyncDisposable +#else +internal sealed class DataContentAsyncEnumerableStream : Stream +#endif +{ + private readonly IAsyncEnumerator _enumerator; + private bool _isCompleted; + private ReadOnlyMemory? _remainingData; + private int _remainingDataOffset; + private long _position; + private DataContent? _firstDataContent; + + /// + /// Initializes a new instance of the class/>. + /// + /// The async enumerable to stream. + /// The first chunk of data to reconsider when reading. + /// The to monitor for cancellation requests. The default is . + /// + /// needs to be considered back in the stream if was iterated before creating the stream. + /// This can happen to check if the first enumerable item contains data or is just a reference only content. + /// + internal DataContentAsyncEnumerableStream(IAsyncEnumerable dataAsyncEnumerable, DataContent? firstDataContent = null, CancellationToken cancellationToken = default) + { + _enumerator = Throw.IfNull(dataAsyncEnumerable).GetAsyncEnumerator(cancellationToken); + _remainingData = Memory.Empty; + _remainingDataOffset = 0; + _position = 0; + _firstDataContent = firstDataContent; + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + _ = Throw.IfNull(destination); + + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + int bytesRead; + while ((bytesRead = await EnumeratorReadAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false)) != 0) + { +#if NET + await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); +#else + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); +#endif + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + /// + public override void Flush() => throw new NotSupportedException(); + + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + /// + public override void SetLength(long value) => + throw new NotSupportedException(); + + /// + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("Use ReadAsync instead for asynchronous reading."); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => EnumeratorReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + +#if NET8_0_OR_GREATER + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => EnumeratorReadAsync(buffer, cancellationToken); + + /// + public override async ValueTask DisposeAsync() + { + await _enumerator.DisposeAsync().ConfigureAwait(false); + + await base.DisposeAsync().ConfigureAwait(false); + } +#else + /// + public async ValueTask DisposeAsync() + { + await _enumerator.DisposeAsync().ConfigureAwait(false); + } + +#pragma warning disable SA1202 // "protected" methods should come before "private" members +#pragma warning disable VSTHRD002 // Synchrnously waiting on tasks or awaiters may cause deadlocks. + /// + protected override void Dispose(bool disposing) + { + _enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); + + base.Dispose(disposing); + } +#pragma warning restore SA1202 // "protected" methods should come before "private" members +#pragma warning restore VSTHRD002 + +#endif + + private async ValueTask EnumeratorReadAsync(Memory buffer, CancellationToken cancellationToken) + { + if (_isCompleted) + { + return 0; + } + + int bytesRead = 0; + int totalToRead = buffer.Length; + + while (bytesRead < totalToRead) + { + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Operation was canceled by the caller.", cancellationToken); + } + + // If there's still data in the current iteration + if (_remainingData is not null && _remainingDataOffset < _remainingData.Value.Length) + { + int bytesToCopy = Math.Min(totalToRead - bytesRead, _remainingData.Value.Length - _remainingDataOffset); + _remainingData.Value.Slice(_remainingDataOffset, bytesToCopy) + .CopyTo(buffer.Slice(bytesRead, bytesToCopy)); + + _remainingDataOffset += bytesToCopy; + bytesRead += bytesToCopy; + _position += bytesToCopy; + } + else + { + // If the first data content was never read, attempt to read it now + if (_position == 0 && _firstDataContent is not null) + { + _remainingData = _firstDataContent.Data; + _remainingDataOffset = 0; + continue; + } + + // Move to the next data content in the async enumerator + if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + _isCompleted = true; + break; + } + + _remainingData = _enumerator.Current.Data; + _remainingDataOffset = 0; + } + } + + return bytesRead; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 483786a3174..70d383b9bc6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,6 +3,7 @@ using OpenAI; using OpenAI.Assistants; +using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; using OpenAI.Responses; @@ -44,6 +45,19 @@ public static IChatClient AsChatClient(this AssistantClient assistantClient, str new OpenAIAssistantClient(assistantClient, assistantId, threadId); #pragma warning restore OPENAI001 + /// Gets an for use with this . + /// The client. + /// The model. + /// An that can be used to transcribe audio via the . + public static ISpeechToTextClient AsSpeechToTextClient(this OpenAIClient openAIClient, string modelId) => + new OpenAISpeechToTextClient(openAIClient, modelId); + + /// Gets an for use with this . + /// The client. + /// An that can be used to transcribe audio via the . + public static ISpeechToTextClient AsSpeechToTextClient(this AudioClient audioClient) => + new OpenAISpeechToTextClient(audioClient); + /// Gets an for use with this . /// The client. /// The model to use. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs new file mode 100644 index 00000000000..258ebe17df9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Shared.Diagnostics; +using OpenAI.Audio; + +#pragma warning disable S3440 // Variables should not be checked against the values they're about to be assigned + +namespace Microsoft.Extensions.AI; + +internal static partial class OpenAIModelMappers +{ + public static SpeechToTextMessage FromOpenAIAudioTranscription(OpenAI.Audio.AudioTranscription audioTranscription, int inputIndex) + { + _ = Throw.IfNull(audioTranscription); + + var segmentCount = audioTranscription.Segments.Count; + var wordCount = audioTranscription.Words.Count; + + TimeSpan? endTime = null; + TimeSpan? startTime = null; + if (segmentCount > 0) + { + endTime = audioTranscription.Segments[segmentCount - 1].EndTime; + startTime = audioTranscription.Segments[0].StartTime; + } + else if (wordCount > 0) + { + endTime = audioTranscription.Words[wordCount - 1].EndTime; + startTime = audioTranscription.Words[0].StartTime; + } + + // Create the return choice. + return new SpeechToTextMessage + { + RawRepresentation = audioTranscription, + InputIndex = inputIndex, + Text = audioTranscription.Text, + StartTime = startTime, + EndTime = endTime, + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(audioTranscription.Language)] = audioTranscription.Language, + [nameof(audioTranscription.Duration)] = audioTranscription.Duration + }, + }; + } + + public static SpeechToTextOptions FromOpenAITranscriptionOptions(OpenAI.Audio.AudioTranscriptionOptions options) + { + SpeechToTextOptions result = new(); + + if (options is not null) + { + result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString() switch + { + null or "" => null, + var modelId => modelId, + }; + + result.SpeechLanguage = options.Language; + + if (options.Temperature is float temperature) + { + (result.AdditionalProperties ??= [])[nameof(options.Temperature)] = temperature; + } + + if (options.TimestampGranularities is AudioTimestampGranularities timestampGranularities) + { + (result.AdditionalProperties ??= [])[nameof(options.TimestampGranularities)] = timestampGranularities; + } + + if (options.Prompt is string prompt) + { + (result.AdditionalProperties ??= [])[nameof(options.Prompt)] = prompt; + } + + if (options.ResponseFormat is AudioTranscriptionFormat jsonFormat) + { + (result.AdditionalProperties ??= [])[nameof(options.ResponseFormat)] = jsonFormat; + } + } + + return result; + } + + /// Converts an extensions options instance to an OpenAI options instance. + public static OpenAI.Audio.AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) + { + OpenAI.Audio.AudioTranscriptionOptions result = new(); + + if (options is not null) + { + if (options.SpeechLanguage is not null) + { + result.Language = options.SpeechLanguage; + } + + if (options.AdditionalProperties is { Count: > 0 } additionalProperties) + { + if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) + { + result.Temperature = temperature; + } + + if (additionalProperties.TryGetValue(nameof(result.TimestampGranularities), out object? timestampGranularities)) + { + result.TimestampGranularities = timestampGranularities is AudioTimestampGranularities granularities ? granularities : default; + } + + if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) + { + result.Prompt = prompt; + } + + if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranscriptionFormat? responseFormat)) + { + result.ResponseFormat = responseFormat; + } + } + } + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs new file mode 100644 index 00000000000..00ba0da6d56 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; +using OpenAI.Audio; + +#pragma warning disable S3440 // Variables should not be checked against the values they're about to be assigned + +namespace Microsoft.Extensions.AI; + +internal static partial class OpenAIModelMappers +{ + public static SpeechToTextMessage FromOpenAIAudioTranslation(OpenAI.Audio.AudioTranslation audioTranslation, int inputIndex) + { + _ = Throw.IfNull(audioTranslation); + + var segmentCount = audioTranslation.Segments.Count; + + TimeSpan? endTime = null; + TimeSpan? startTime = null; + if (segmentCount > 0) + { + endTime = audioTranslation.Segments[segmentCount - 1].EndTime; + startTime = audioTranslation.Segments[0].StartTime; + } + + // Create the return choice. + return new SpeechToTextMessage + { + RawRepresentation = audioTranslation, + InputIndex = inputIndex, + Text = audioTranslation.Text, + StartTime = startTime, + EndTime = endTime, + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(audioTranslation.Language)] = audioTranslation.Language, + [nameof(audioTranslation.Duration)] = audioTranslation.Duration + }, + }; + } + + public static SpeechToTextOptions FromOpenAITranslationOptions(OpenAI.Audio.AudioTranslationOptions options) + { + SpeechToTextOptions result = new(); + + if (options is not null) + { + result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString() switch + { + null or "" => null, + var modelId => modelId, + }; + + if (options.Temperature is float temperature) + { + (result.AdditionalProperties ??= [])[nameof(options.Temperature)] = temperature; + } + + if (options.Prompt is string prompt) + { + (result.AdditionalProperties ??= [])[nameof(options.Prompt)] = prompt; + } + + if (options.ResponseFormat is AudioTranslationFormat jsonFormat) + { + (result.AdditionalProperties ??= [])[nameof(options.ResponseFormat)] = jsonFormat; + } + } + + return result; + } + + /// Converts an extensions options instance to an OpenAI options instance. + public static OpenAI.Audio.AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) + { + OpenAI.Audio.AudioTranslationOptions result = new(); + + if (options is not null) + { + if (options.AdditionalProperties is { Count: > 0 } additionalProperties) + { + if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) + { + result.Temperature = temperature; + } + + if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) + { + result.Prompt = prompt; + } + + if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranslationFormat? responseFormat)) + { + result.ResponseFormat = responseFormat; + } + } + } + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs index 9edcac55c5e..9980f785325 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs @@ -621,7 +621,7 @@ private static FunctionCallContent ParseCallContentFromBinaryData(BinaryData ut8 private static T? GetValueOrDefault(this AdditionalPropertiesDictionary? dict, string key) => dict?.TryGetValue(key, out T? value) is true ? value : default; - private static string CreateCompletionId() => $"chatcmpl-{Guid.NewGuid():N}"; + private static string CreateCompletionId(string? prefix = "chatcmpl") => $"{prefix}-{Guid.NewGuid():N}"; /// Used to create the JSON payload for an OpenAI chat tool description. public sealed class OpenAIChatToolJson diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs new file mode 100644 index 00000000000..da39e95681b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using OpenAI; +using OpenAI.Audio; + +#pragma warning disable S1067 // Expressions should not be too complex +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + +namespace Microsoft.Extensions.AI; + +/// Represents an for an OpenAI or . +public sealed class OpenAISpeechToTextClient : ISpeechToTextClient +{ + /// Default OpenAI endpoint. + private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1"); + + /// Metadata about the client. + private readonly SpeechToTextClientMetadata _metadata; + + /// The underlying . + private readonly OpenAIClient? _openAIClient; + + /// The underlying . + private readonly AudioClient _audioClient; + + /// Initializes a new instance of the class for the specified . + /// The underlying client. + /// The model to use. + public OpenAISpeechToTextClient(OpenAIClient openAIClient, string modelId) + { + _ = Throw.IfNull(openAIClient); + _ = Throw.IfNullOrWhitespace(modelId); + + _openAIClient = openAIClient; + _audioClient = openAIClient.GetAudioClient(modelId); + + // https://github.com/openai/openai-dotnet/issues/215 + // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages + // implement the abstractions directly rather than providing adapters on top of the public APIs, + // the package can provide such implementations separate from what's exposed in the public API. + Uri providerUrl = typeof(OpenAIClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(openAIClient) as Uri ?? _defaultOpenAIEndpoint; + + _metadata = new("openai", providerUrl, modelId); + } + + /// Initializes a new instance of the class for the specified . + /// The underlying client. + public OpenAISpeechToTextClient(AudioClient audioClient) + { + _ = Throw.IfNull(audioClient); + + _audioClient = audioClient; + + // https://github.com/openai/openai-dotnet/issues/215 + // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages + // implement the abstractions directly rather than providing adapters on top of the public APIs, + // the package can provide such implementations separate from what's exposed in the public API. + Uri providerUrl = typeof(AudioClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(audioClient) as Uri ?? _defaultOpenAIEndpoint; + string? model = typeof(AudioClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(audioClient) as string; + + _metadata = new("openai", providerUrl, model); + } + + /// + public object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return + serviceKey is not null ? null : + serviceType == typeof(SpeechToTextClientMetadata) ? _metadata : + serviceType == typeof(OpenAIClient) ? _openAIClient : + serviceType == typeof(AudioClient) ? _audioClient : + serviceType.IsInstanceOfType(this) ? this : + null; + } + + /// + public async IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNullOrEmpty(speechContents); + + for (var inputIndex = 0; inputIndex < speechContents.Count; inputIndex++) + { + var speechContent = speechContents[inputIndex]; + _ = Throw.IfNull(speechContent); + + var speechResponse = await GetResponseAsync([speechContent], options, cancellationToken).ConfigureAwait(false); + + foreach (var choice in speechResponse.Choices) + { + yield return new SpeechToTextResponseUpdate(choice.Contents) + { + InputIndex = inputIndex, + Kind = SpeechToTextResponseUpdateKind.TextUpdated, + RawRepresentation = choice.RawRepresentation + }; + } + } + } + + /// + public async Task GetResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNullOrEmpty(speechContents); + + List choices = []; + + // A translation is triggered when the target text language is specified and the source language is not provided or different. + static bool IsTranslationRequest(SpeechToTextOptions? options) + => options is not null && options.TextLanguage is not null + && (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage); + + for (var inputIndex = 0; inputIndex < speechContents.Count; inputIndex++) + { + var speechContent = speechContents[inputIndex]; + _ = Throw.IfNull(speechContent); + + var enumerator = speechContent.GetAsyncEnumerator(cancellationToken); + if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + throw new InvalidOperationException($"The audio content provided in the index: {inputIndex} is empty."); + } + + var firstChunk = enumerator.Current; + + if (IsTranslationRequest(options)) + { + _ = Throw.IfNull(options); + + // Translation request will be triggered whenever the source language is not specified and a target text language is and different from the output text language + if (CultureInfo.GetCultureInfo(options.TextLanguage!).TwoLetterISOLanguageName != "en") + { + throw new NotSupportedException($"Only translation to english is supported."); + } + + AudioTranslation translationResult = await GetTranslationResultAsync(options, speechContent, firstChunk, cancellationToken).ConfigureAwait(false); + + var choice = OpenAIModelMappers.FromOpenAIAudioTranslation(translationResult, inputIndex); + choices.Add(choice); + } + else + { + var openAIOptions = OpenAIModelMappers.ToOpenAITranscriptionOptions(options); + + // Transcription request + AudioTranscription transcriptionResult = await GetTranscriptionResultAsync(speechContent, firstChunk, openAIOptions, cancellationToken).ConfigureAwait(false); + + var choice = OpenAIModelMappers.FromOpenAIAudioTranscription(transcriptionResult, inputIndex); + choices.Add(choice); + } + } + + return new SpeechToTextResponse(choices); + } + + /// + void IDisposable.Dispose() + { + // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. + } + + private async Task GetTranscriptionResultAsync( + IAsyncEnumerable speechContent, DataContent firstChunk, AudioTranscriptionOptions openAIOptions, CancellationToken cancellationToken) + { + OpenAI.Audio.AudioTranscription transcriptionResult; + + var audioFileStream = speechContent.ToStream(firstChunk, cancellationToken); +#if NET + await using (audioFileStream.ConfigureAwait(false)) +#else + using (audioFileStream) +#endif + { + transcriptionResult = (await _audioClient.TranscribeAudioAsync( + audioFileStream, + "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. + openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + } + + return transcriptionResult; + } + + private async Task GetTranslationResultAsync( + SpeechToTextOptions? options, IAsyncEnumerable speechContent, DataContent firstChunk, CancellationToken cancellationToken) + { + var openAIOptions = OpenAIModelMappers.ToOpenAITranslationOptions(options); + OpenAI.Audio.AudioTranslation translationResult; + + var audioFileStream = speechContent.ToStream(firstChunk, cancellationToken); +#if NET + await using (audioFileStream.ConfigureAwait(false)) +#else + using (audioFileStream) +#endif + { + translationResult = (await _audioClient.TranslateAudioAsync( + audioFileStream, + "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. + openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + } + + return translationResult; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs new file mode 100644 index 00000000000..f36fc1d69b0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + +namespace Microsoft.Extensions.AI; + +/// A delegating speech to text client that wraps an inner client with implementations provided by delegates. +public sealed class AnonymousDelegatingSpeechToTextClient : DelegatingSpeechToTextClient +{ + /// The delegate to use as the implementation of . + private readonly Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, Task>? _getResponseFunc; + + /// The delegate to use as the implementation of . + /// + /// When non-, this delegate is used as the implementation of and + /// will be invoked with the same arguments as the method itself, along with a reference to the inner client. + /// When , will delegate directly to the inner client. + /// + private readonly Func< + IList>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, IAsyncEnumerable>? _getStreamingResponseFunc; + + /// The delegate to use as the implementation of both and . + private readonly GetResponseSharedFunc? _sharedFunc; + + /// + /// Initializes a new instance of the class. + /// + /// The inner client. + /// + /// A delegate that provides the implementation for both and . + /// In addition to the arguments for the operation, it's provided with a delegate to the inner client that should be + /// used to perform the operation on the inner client. It will handle both the non-streaming and streaming cases. + /// + /// + /// This overload may be used when the anonymous implementation needs to provide pre- and/or post-processing, but doesn't + /// need to interact with the results of the operation, which will come from the inner client. + /// + /// is . + /// is . + public AnonymousDelegatingSpeechToTextClient(ISpeechToTextClient innerClient, GetResponseSharedFunc sharedFunc) + : base(innerClient) + { + _ = Throw.IfNull(sharedFunc); + + _sharedFunc = sharedFunc; + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner client. + /// + /// A delegate that provides the implementation for . When , + /// must be non-null, and the implementation of + /// will use for the implementation. + /// + /// + /// A delegate that provides the implementation for . When , + /// must be non-null, and the implementation of + /// will use for the implementation. + /// + /// is . + /// Both and are . + public AnonymousDelegatingSpeechToTextClient( + ISpeechToTextClient innerClient, + Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, Task>? getResponseFunc, + Func< + IList>, + SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, IAsyncEnumerable>? getStreamingResponseFunc) + : base(innerClient) + { + ThrowIfBothDelegatesNull(getResponseFunc, getStreamingResponseFunc); + + _getResponseFunc = getResponseFunc; + _getStreamingResponseFunc = getStreamingResponseFunc; + } + + /// + public override Task GetResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(speechContents); + + if (_sharedFunc is not null) + { + return RespondViaSharedAsync(speechContents, options, cancellationToken); + + async Task RespondViaSharedAsync( + IList> audioContents, SpeechToTextOptions? options, CancellationToken cancellationToken) + { + SpeechToTextResponse? completion = null; + await _sharedFunc(audioContents, options, async (audioContents, options, cancellationToken) => + { + completion = await InnerClient.GetResponseAsync(audioContents, options, cancellationToken).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); + + if (completion is null) + { + throw new InvalidOperationException("The wrapper completed successfully without producing a SpeechToTextResponse."); + } + + return completion; + } + } + else if (_getResponseFunc is not null) + { + return _getResponseFunc(speechContents, options, InnerClient, cancellationToken); + } + else + { + Debug.Assert(_getStreamingResponseFunc is not null, "Expected non-null streaming delegate."); + return _getStreamingResponseFunc!(speechContents, options, InnerClient, cancellationToken) + .ToSpeechToTextResponseAsync(coalesceContent: true, cancellationToken); + } + } + + /// + public override IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(speechContents); + + if (_sharedFunc is not null) + { + var updates = Channel.CreateBounded(1); + +#pragma warning disable CA2016 // explicitly not forwarding the cancellation token, as we need to ensure the channel is always completed + _ = Task.Run(async () => +#pragma warning restore CA2016 + { + Exception? error = null; + try + { + await _sharedFunc(speechContents, options, async (speechContents, options, cancellationToken) => + { + await foreach (var update in InnerClient.GetStreamingResponseAsync(speechContents, options, cancellationToken).ConfigureAwait(false)) + { + await updates.Writer.WriteAsync(update, cancellationToken).ConfigureAwait(false); + } + }, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + error = ex; + throw; + } + finally + { + _ = updates.Writer.TryComplete(error); + } + }); + + return updates.Reader.ReadAllAsync(cancellationToken); + } + else if (_getStreamingResponseFunc is not null) + { + return _getStreamingResponseFunc(speechContents, options, InnerClient, cancellationToken); + } + else + { + Debug.Assert(_getResponseFunc is not null, "Expected non-null non-streaming delegate."); + return GetStreamingResponseAsyncViaGetResponseAsync(_getResponseFunc!(speechContents, options, InnerClient, cancellationToken)); + + static async IAsyncEnumerable GetStreamingResponseAsyncViaGetResponseAsync(Task task) + { + SpeechToTextResponse completion = await task.ConfigureAwait(false); + foreach (var update in completion.ToSpeechToTextResponseUpdates()) + { + yield return update; + } + } + } + } + + /// Throws an exception if both of the specified delegates are null. + /// Both and are . + internal static void ThrowIfBothDelegatesNull(object? getResponseFunc, object? getStreamingResponseFunc) + { + if (getResponseFunc is null && getStreamingResponseFunc is null) + { + Throw.ArgumentNullException(nameof(getResponseFunc), $"At least one of the {nameof(getResponseFunc)} or {nameof(getStreamingResponseFunc)} delegates must be non-null."); + } + } + + // Design note: + // The following delegate could juse use Func<...>, but it's defined as a custom delegate type + // in order to provide better discoverability / documentation / usability around its complicated + // signature with the nextAsync delegate parameter. + + /// + /// Represents a method used to call or . + /// + /// The audio contents to send. + /// The speech to text options to configure the request. + /// + /// A delegate that provides the implementation for the inner client's or + /// . It should be invoked to continue the pipeline. It accepts + /// the audio contents, options, and cancellation token, which are typically the same instances as provided to this method + /// but need not be. + /// + /// The to monitor for cancellation requests. The default is . + /// A that represents the completion of the operation. + public delegate Task GetResponseSharedFunc( + IList> speechContents, + SpeechToTextOptions? options, + Func>, SpeechToTextOptions?, CancellationToken, Task> nextAsync, + CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs new file mode 100644 index 00000000000..a05ac872d1b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Represents a delegating chat client that configures a instance used by the remainder of the pipeline. +public sealed class ConfigureOptionsSpeechToTextClient : DelegatingSpeechToTextClient +{ + /// The callback delegate used to configure options. + private readonly Action _configureOptions; + + /// Initializes a new instance of the class with the specified callback. + /// The inner client. + /// + /// The delegate to invoke to configure the instance. It is passed a clone of the caller-supplied instance + /// (or a newly constructed instance if the caller-supplied instance is ). + /// + /// + /// The delegate is passed either a new instance of if + /// the caller didn't supply a instance, or a clone (via of the caller-supplied + /// instance if one was supplied. + /// + public ConfigureOptionsSpeechToTextClient(ISpeechToTextClient innerClient, Action configure) + : base(innerClient) + { + _configureOptions = Throw.IfNull(configure); + } + + /// + public override Task GetResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + return base.GetResponseAsync(speechContents, Configure(options), cancellationToken); + } + + /// + public override IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + return base.GetStreamingResponseAsync(speechContents, Configure(options), cancellationToken); + } + + /// Creates and configures the to pass along to the inner client. + private SpeechToTextOptions Configure(SpeechToTextOptions? options) + { + options = options?.Clone() ?? new(); + + _configureOptions(options); + + return options; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs new file mode 100644 index 00000000000..bbba647d782 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1629 // Documentation text should end with a period + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +public static class ConfigureOptionsSpeechToTextClientBuilderExtensions +{ + /// + /// Adds a callback that configures a to be passed to the next client in the pipeline. + /// + /// The . + /// + /// The delegate to invoke to configure the instance. + /// It is passed a clone of the caller-supplied instance (or a newly constructed instance if the caller-supplied instance is ). + /// + /// + /// This method can be used to set default options. The delegate is passed either a new instance of + /// if the caller didn't supply a instance, or a clone (via ) + /// of the caller-supplied instance if one was supplied. + /// + /// The . + public static SpeechToTextClientBuilder ConfigureOptions( + this SpeechToTextClientBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.Use(innerClient => new ConfigureOptionsSpeechToTextClient(innerClient, configure)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs new file mode 100644 index 00000000000..c9c08532fba --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// A delegating speech to text client that logs speech to text operations to an . +/// +/// The provided implementation of is thread-safe for concurrent use so long as the +/// employed is also thread-safe for concurrent use. +/// +public partial class LoggingSpeechToTextClient : DelegatingSpeechToTextClient +{ + /// An instance used for all logging. + private readonly ILogger _logger; + + /// The to use for serialization of state written to the logger. + private JsonSerializerOptions _jsonSerializerOptions; + + /// Initializes a new instance of the class. + /// The underlying . + /// An instance that will be used for all logging. + public LoggingSpeechToTextClient(ISpeechToTextClient innerClient, ILogger logger) + : base(innerClient) + { + _logger = Throw.IfNull(logger); + _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; + } + + /// Gets or sets JSON serialization options to use when serializing logging data. + public JsonSerializerOptions JsonSerializerOptions + { + get => _jsonSerializerOptions; + set => _jsonSerializerOptions = Throw.IfNull(value); + } + + /// + public override async Task GetResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogInvokedSensitive(nameof(GetResponseAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + } + else + { + LogInvoked(nameof(GetResponseAsync)); + } + } + + try + { + var completion = await base.GetResponseAsync(speechContents, options, cancellationToken).ConfigureAwait(false); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogCompletedSensitive(nameof(GetResponseAsync), AsJson(completion)); + } + else + { + LogCompleted(nameof(GetResponseAsync)); + } + } + + return completion; + } + catch (OperationCanceledException) + { + LogInvocationCanceled(nameof(GetResponseAsync)); + throw; + } + catch (Exception ex) + { + LogInvocationFailed(nameof(GetResponseAsync), ex); + throw; + } + } + + /// + public override async IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogInvokedSensitive(nameof(GetStreamingResponseAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + } + else + { + LogInvoked(nameof(GetStreamingResponseAsync)); + } + } + + IAsyncEnumerator e; + try + { + e = base.GetStreamingResponseAsync(speechContents, options, cancellationToken).GetAsyncEnumerator(cancellationToken); + } + catch (OperationCanceledException) + { + LogInvocationCanceled(nameof(GetStreamingResponseAsync)); + throw; + } + catch (Exception ex) + { + LogInvocationFailed(nameof(GetStreamingResponseAsync), ex); + throw; + } + + try + { + SpeechToTextResponseUpdate? update = null; + while (true) + { + try + { + if (!await e.MoveNextAsync().ConfigureAwait(false)) + { + break; + } + + update = e.Current; + } + catch (OperationCanceledException) + { + LogInvocationCanceled(nameof(GetStreamingResponseAsync)); + throw; + } + catch (Exception ex) + { + LogInvocationFailed(nameof(GetStreamingResponseAsync), ex); + throw; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogStreamingUpdateSensitive(AsJson(update)); + } + else + { + LogStreamingUpdate(); + } + } + + yield return update; + } + + LogCompleted(nameof(GetStreamingResponseAsync)); + } + finally + { + await e.DisposeAsync().ConfigureAwait(false); + } + } + + private string AsJson(T value) => LoggingHelpers.AsJson(value, _jsonSerializerOptions); + + [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] + private partial void LogInvoked(string methodName); + + [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: Audio contents: {AudioContents}. Options: {SpeechToTextOptions}. Metadata: {SpeechToTextClientMetadata}.")] + private partial void LogInvokedSensitive(string methodName, string audioContents, string SpeechToTextOptions, string SpeechToTextClientMetadata); + + [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] + private partial void LogCompleted(string methodName); + + [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {SpeechToTextResponse}.")] + private partial void LogCompletedSensitive(string methodName, string SpeechToTextResponse); + + [LoggerMessage(LogLevel.Debug, "GetStreamingResponseAsync received update.")] + private partial void LogStreamingUpdate(); + + [LoggerMessage(LogLevel.Trace, "GetStreamingResponseAsync received update: {SpeechToTextResponseUpdate}")] + private partial void LogStreamingUpdateSensitive(string speechToTextResponseUpdate); + + [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] + private partial void LogInvocationCanceled(string methodName); + + [LoggerMessage(LogLevel.Error, "{MethodName} failed.")] + private partial void LogInvocationFailed(string methodName, Exception error); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs new file mode 100644 index 00000000000..55e38382854 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +public static class LoggingSpeechToTextClientExtensions +{ + /// Adds logging to the audio transcription client pipeline. + /// The . + /// + /// An optional used to create a logger with which logging should be performed. + /// If not supplied, a required instance will be resolved from the service provider. + /// + /// An optional callback that can be used to configure the instance. + /// The . + public static SpeechToTextClientBuilder UseLogging( + this SpeechToTextClientBuilder builder, + ILoggerFactory? loggerFactory = null, + Action? configure = null) + { + _ = Throw.IfNull(builder); + + return builder.Use((innerClient, services) => + { + loggerFactory ??= services.GetRequiredService(); + + // If the factory we resolve is for the null logger, the LoggingAudioTranscriptionClient will end up + // being an expensive nop, so skip adding it and just return the inner client. + if (loggerFactory == NullLoggerFactory.Instance) + { + return innerClient; + } + + var audioClient = new LoggingSpeechToTextClient(innerClient, loggerFactory.CreateLogger(typeof(LoggingSpeechToTextClient))); + configure?.Invoke(audioClient); + return audioClient; + }); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs new file mode 100644 index 00000000000..15cacb65f24 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// A builder for creating pipelines of . +public sealed class SpeechToTextClientBuilder +{ + private readonly Func _innerClientFactory; + + /// The registered client factory instances. + private List>? _clientFactories; + + /// Initializes a new instance of the class. + /// The inner that represents the underlying backend. + public SpeechToTextClientBuilder(ISpeechToTextClient innerClient) + { + _ = Throw.IfNull(innerClient); + _innerClientFactory = _ => innerClient; + } + + /// Initializes a new instance of the class. + /// A callback that produces the inner that represents the underlying backend. + public SpeechToTextClientBuilder(Func innerClientFactory) + { + _innerClientFactory = Throw.IfNull(innerClientFactory); + } + + /// Builds an that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn. + /// + /// The that should provide services to the instances. + /// If null, an empty will be used. + /// + /// An instance of that represents the entire pipeline. + public ISpeechToTextClient Build(IServiceProvider? services = null) + { + services ??= EmptyServiceProvider.Instance; + var audioClient = _innerClientFactory(services); + + // To match intuitive expectations, apply the factories in reverse order, so that the first factory added is the outermost. + if (_clientFactories is not null) + { + for (var i = _clientFactories.Count - 1; i >= 0; i--) + { + audioClient = _clientFactories[i](audioClient, services) ?? + throw new InvalidOperationException( + $"The {nameof(SpeechToTextClientBuilder)} entry at index {i} returned null. " + + $"Ensure that the callbacks passed to {nameof(Use)} return non-null {nameof(ISpeechToTextClient)} instances."); + } + } + + return audioClient; + } + + /// Adds a factory for an intermediate audio transcription client to the audio transcription client pipeline. + /// The client factory function. + /// The updated instance. + public SpeechToTextClientBuilder Use(Func clientFactory) + { + _ = Throw.IfNull(clientFactory); + + return Use((innerClient, _) => clientFactory(innerClient)); + } + + /// Adds a factory for an intermediate audio transcription client to the audio transcription client pipeline. + /// The client factory function. + /// The updated instance. + public SpeechToTextClientBuilder Use(Func clientFactory) + { + _ = Throw.IfNull(clientFactory); + + (_clientFactories ??= []).Add(clientFactory); + return this; + } + + /// + /// Adds to the audio transcription client pipeline an anonymous delegating audio transcription client based on a delegate that provides + /// an implementation for both and . + /// + /// + /// A delegate that provides the implementation for both and + /// . In addition to the arguments for the operation, it's + /// provided with a delegate to the inner client that should be used to perform the operation on the inner client. + /// It will handle both the non-streaming and streaming cases. + /// + /// The updated instance. + /// + /// This overload may be used when the anonymous implementation needs to provide pre- and/or post-processing, but doesn't + /// need to interact with the results of the operation, which will come from the inner client. + /// + /// is . + public SpeechToTextClientBuilder Use(AnonymousDelegatingSpeechToTextClient.GetResponseSharedFunc sharedFunc) + { + _ = Throw.IfNull(sharedFunc); + + return Use((innerClient, _) => new AnonymousDelegatingSpeechToTextClient(innerClient, sharedFunc)); + } + + /// + /// Adds to the audio transcription client pipeline an anonymous delegating audio transcription client based on a delegate that provides + /// an implementation for both and . + /// + /// + /// A delegate that provides the implementation for . When , + /// must be non-null, and the implementation of + /// will use for the implementation. + /// + /// + /// A delegate that provides the implementation for . When , + /// must be non-null, and the implementation of + /// will use for the implementation. + /// + /// The updated instance. + /// + /// One or both delegates may be provided. If both are provided, they will be used for their respective methods: + /// will provide the implementation of , and + /// will provide the implementation of . + /// If only one of the delegates is provided, it will be used for both methods. That means that if + /// is supplied without , the implementation of + /// will employ limited streaming, as it will be operating on the batch output produced by . And if + /// is supplied without , the implementation of + /// will be implemented by combining the updates from . + /// + /// Both and are . + public SpeechToTextClientBuilder Use( + Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, Task>? transcribeFunc, + Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, + IAsyncEnumerable>? transcribeStreamingFunc) + { + AnonymousDelegatingSpeechToTextClient.ThrowIfBothDelegatesNull(transcribeFunc, transcribeStreamingFunc); + + return Use((innerClient, _) => new AnonymousDelegatingSpeechToTextClient(innerClient, transcribeFunc, transcribeStreamingFunc)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs new file mode 100644 index 00000000000..25c261fd66f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// Provides extension methods for registering with a . +public static class SpeechToTextClientBuilderServiceCollectionExtensions +{ + /// Registers a singleton in the . + /// The to which the client should be added. + /// The inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a singleton service. + public static SpeechToTextClientBuilder AddSpeechToTextClient( + this IServiceCollection serviceCollection, + ISpeechToTextClient innerClient, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddSpeechToTextClient(serviceCollection, _ => innerClient, lifetime); + + /// Registers a singleton in the . + /// The to which the client should be added. + /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a singleton service. + public static SpeechToTextClientBuilder AddSpeechToTextClient( + this IServiceCollection serviceCollection, + Func innerClientFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(innerClientFactory); + + var builder = new SpeechToTextClientBuilder(innerClientFactory); + serviceCollection.Add(new ServiceDescriptor(typeof(ISpeechToTextClient), builder.Build, lifetime)); + return builder; + } + + /// Registers a keyed singleton in the . + /// The to which the client should be added. + /// The key with which to associate the client. + /// The inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a scoped service. + public static SpeechToTextClientBuilder AddKeyedSpeechToTextClient( + this IServiceCollection serviceCollection, + object serviceKey, + ISpeechToTextClient innerClient, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddKeyedSpeechToTextClient(serviceCollection, serviceKey, _ => innerClient, lifetime); + + /// Registers a keyed singleton in the . + /// The to which the client should be added. + /// The key with which to associate the client. + /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . + /// A that can be used to build a pipeline around the inner client. + /// The client is registered as a scoped service. + public static SpeechToTextClientBuilder AddKeyedSpeechToTextClient( + this IServiceCollection serviceCollection, + object serviceKey, + Func innerClientFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(serviceKey); + _ = Throw.IfNull(innerClientFactory); + + var builder = new SpeechToTextClientBuilder(innerClientFactory); + serviceCollection.Add(new ServiceDescriptor(typeof(ISpeechToTextClient), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime)); + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs new file mode 100644 index 00000000000..929c8da526d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extension methods for working with in the context of . +public static class SpeechToTextClientBuilderSpeechToTextClientExtensions +{ + /// Creates a new using as its inner client. + /// The client to use as the inner client. + /// The new instance. + /// + /// This method is equivalent to using the constructor directly, + /// specifying as the inner client. + /// + public static SpeechToTextClientBuilder AsBuilder(this ISpeechToTextClient innerClient) + { + _ = Throw.IfNull(innerClient); + + return new SpeechToTextClientBuilder(innerClient); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ErrorContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ErrorContentTests.cs new file mode 100644 index 00000000000..2564f6bc2c9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ErrorContentTests.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ErrorContentTests +{ + [Fact] + public void Constructor_ShouldInitializeProperties() + { + // Arrange + string errorMessage = "Error occurred"; + string errorCode = "ERR001"; + string errorDetails = "Something went wrong"; + + // Act + var errorContent = new ErrorContent(errorMessage) + { + ErrorCode = errorCode, + Details = errorDetails + }; + + // Assert + Assert.Equal(errorMessage, errorContent.Message); + Assert.Equal(errorCode, errorContent.ErrorCode); + Assert.Equal(errorDetails, errorContent.Details); + } + + [Fact] + public void JsonSerialization_ShouldSerializeAndDeserializeCorrectly() + { + // Arrange + var errorContent = new ErrorContent("Error occurred") + { + ErrorCode = "ERR001", + Details = "Something went wrong" + }; + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Act + var json = JsonSerializer.Serialize(errorContent, options); + var deserializedErrorContent = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(deserializedErrorContent); + Assert.Equal(errorContent.Message, deserializedErrorContent!.Message); + Assert.Equal(errorContent.ErrorCode, deserializedErrorContent.ErrorCode); + Assert.Equal(errorContent.Details, deserializedErrorContent.Details); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs new file mode 100644 index 00000000000..19929db49f8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs @@ -0,0 +1,352 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class DataContentAsyncEnumerableStreamTests +{ + [Fact] + public void Constructor_InvalidArgs_Throws() + { + // Expect ArgumentNullException if source is null. + Assert.Throws("dataAsyncEnumerable", () => new DataContentAsyncEnumerableStreamImpl(null!)); + } + + [Fact] + public async Task ReadAsync_EmptySource_ReturnsEmpty() + { + var source = GetAsyncEnumerable(Array.Empty()); + using var stream = new DataContentAsyncEnumerableStreamImpl(source); + + byte[] buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(0, bytesRead); + } + + [Fact] + public async Task ReadAsync_SingleChunk_ReturnsChunk() + { + var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); + using var stream = new DataContentAsyncEnumerableStreamImpl(source); + + byte[] buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + + Assert.Equal(5, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 0, 0, 0, 0, 0 }, buffer); + } + + [Fact] + public async Task ReadAsync_MultipleChunks_ReturnsConcatenatedChunks() + { + var source = GetAsyncEnumerable(new[] + { + new byte[] { 1,2,3 }, + new byte[] { 4,5,6 }, + new byte[] { 7,8,9 } + }); + using var stream = new DataContentAsyncEnumerableStreamImpl(source); + + byte[] buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(9, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, buffer); + } + + [Fact] + public async Task ReadAsync_BufferTooSmall_ReturnsPartialChunk() + { + var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); + using var stream = new DataContentAsyncEnumerableStreamImpl(source); + + byte[] buffer = new byte[3]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3 }, buffer); + } + + [Fact] + public async Task ReadAsync_BufferTooSmall_MultipleReads() + { + var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); + using var stream = new DataContentAsyncEnumerableStreamImpl(source); + + byte[] buffer = new byte[3]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3 }, buffer); + + bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(2, bytesRead); + + // The third byte in the buffer remains from the previous read. + Assert.Equal(new byte[] { 4, 5, 3 }, buffer); + } + + [Fact] + public async Task ReadAsync_WithFirstDataContent_ReturnsFirstDataContentThenSource() + { + var firstDataContentBytes = new byte[] { 0, 0, 0, 0, 0 }; + var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); + + // Pass a firstDataContent as a DataContent instance. + using var stream = new DataContentAsyncEnumerableStreamImpl(source, new DataContent(firstDataContentBytes, "application/octet-stream")); + + byte[] buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(10, bytesRead); + Assert.Equal(new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5 }, buffer); + } + + [Fact] + public void Constructor_WithFirstDataContent_InitializesCorrectly() + { + var firstDataContentBytes = new byte[] { 0, 0, 0, 0, 0 }; + var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); + using var stream = new DataContentAsyncEnumerableStreamImpl(source, new DataContent(firstDataContentBytes, "application/octet-stream")); + Assert.NotNull(stream); + } + + [Fact] + public async Task ReadAsync_EmptySource_WithFirstDataContent_ReturnsFirstDataContent() + { + var firstDataContentBytes = new byte[] { 0, 0, 0, 0, 0 }; + var source = GetAsyncEnumerable(Array.Empty()); + using var stream = new DataContentAsyncEnumerableStreamImpl(source, new DataContent(firstDataContentBytes, "application/octet-stream")); + + byte[] buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(5, bytesRead); + Assert.Equal(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, buffer); + } + + private static async IAsyncEnumerable GetAsyncEnumerable(IEnumerable chunks) + { + foreach (var chunk in chunks) + { + await Task.Yield(); + yield return new DataContent(chunk, "application/octet-stream"); + } + } + + /// + /// Utility class to stream data content as a . + /// + /// The type of data content to stream. +#if !NET8_0_OR_GREATER + internal sealed class DataContentAsyncEnumerableStreamImpl : Stream, IAsyncDisposable +#else + internal sealed class DataContentAsyncEnumerableStreamImpl : Stream +#endif + where T : DataContent + { + private readonly IAsyncEnumerator _enumerator; + private bool _isCompleted; + private byte[] _remainingData; + private int _remainingDataOffset; + private long _position; + private T? _firstDataContent; + + /// + /// Initializes a new instance of the class, where T is . + /// + /// The async enumerable to stream. + /// The first chunk of data to reconsider when reading. + /// The to monitor for cancellation requests. The default is . + /// + /// needs to be considered back in the stream if was iterated before creating the stream. + /// This can happen to check if the first enumerable item contains data or is just a reference only content. + /// + internal DataContentAsyncEnumerableStreamImpl(IAsyncEnumerable dataAsyncEnumerable, T? firstDataContent = null, CancellationToken cancellationToken = default) + { + if (dataAsyncEnumerable is null) + { + throw new ArgumentNullException(nameof(dataAsyncEnumerable)); + } + + _enumerator = dataAsyncEnumerable.GetAsyncEnumerator(cancellationToken); + _remainingData = Array.Empty(); + _remainingDataOffset = 0; + _position = 0; + _firstDataContent = firstDataContent; + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + /// + public override void Flush() => throw new NotSupportedException(); + + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + /// + public override void SetLength(long value) => + throw new NotSupportedException(); + + /// + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("Use ReadAsync instead for asynchronous reading."); + } + + /// +#if NET8_0_OR_GREATER + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => ReadAsync(buffer, cancellationToken).AsTask(); +#else + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_isCompleted) + { + return 0; + } + + int bytesRead = 0; + + while (bytesRead < count) + { + if (_remainingDataOffset < _remainingData.Length) + { + int bytesToCopy = Math.Min(count - bytesRead, _remainingData.Length - _remainingDataOffset); + Array.Copy(_remainingData, _remainingDataOffset, buffer, offset + bytesRead, bytesToCopy); + _remainingDataOffset += bytesToCopy; + bytesRead += bytesToCopy; + _position += bytesToCopy; + } + else + { + // Special case when the first chunk was skipped and needs to be read + if (_position == 0 && _firstDataContent is not null) + { + _remainingData = _firstDataContent.Data.ToArray(); + _remainingDataOffset = 0; + + continue; + } + + if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + _isCompleted = true; + break; + } + + _remainingData = _enumerator.Current.Data.ToArray(); + _remainingDataOffset = 0; + } + } + + return bytesRead; + } +#endif + +#if NET8_0_OR_GREATER + /// + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (_isCompleted) + { + return 0; + } + + int bytesRead = 0; + int totalToRead = buffer.Length; + + while (bytesRead < totalToRead) + { + // If there's still data in the current chunk + if (_remainingDataOffset < _remainingData.Length) + { + int bytesToCopy = Math.Min(totalToRead - bytesRead, _remainingData.Length - _remainingDataOffset); + _remainingData.AsSpan(_remainingDataOffset, bytesToCopy) + .CopyTo(buffer.Span.Slice(bytesRead, bytesToCopy)); + + _remainingDataOffset += bytesToCopy; + bytesRead += bytesToCopy; + _position += bytesToCopy; + } + else + { + // If the first chunk was never read, attempt to read it now + if (_position == 0 && _firstDataContent is not null) + { + _remainingData = _firstDataContent.Data.ToArray(); + _remainingDataOffset = 0; + continue; + } + + // Move to the next chunk in the async enumerator + if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + _isCompleted = true; + break; + } + + _remainingData = _enumerator.Current.Data.ToArray(); + _remainingDataOffset = 0; + } + } + + return bytesRead; + } +#endif + +#if NET8_0_OR_GREATER + /// + public override async ValueTask DisposeAsync() + { + await _enumerator.DisposeAsync().ConfigureAwait(false); + + await base.DisposeAsync().ConfigureAwait(false); + } +#else + public async ValueTask DisposeAsync() + { + await _enumerator.DisposeAsync().ConfigureAwait(false); + + Dispose(); + } +#endif + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + var task = Task.Run(_enumerator.DisposeAsync); + } + + base.Dispose(disposing); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs new file mode 100644 index 00000000000..be083d8162b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class DelegatingSpeechToTextClientTests +{ + [Fact] + public void RequiresInnerSpeechToTextClient() + { + Assert.Throws("innerClient", () => new NoOpDelegatingSpeechToTextClient(null!)); + } + + [Fact] + public async Task GetResponseAsyncDefaultsToInnerClientAsync() + { + // Arrange + var expectedContents = new List>(); + var expectedOptions = new SpeechToTextOptions(); + var expectedCancellationToken = CancellationToken.None; + var expectedResult = new TaskCompletionSource(); + var expectedResponse = new SpeechToTextResponse([]); + using var inner = new TestSpeechToTextClient + { + GetResponseAsyncCallback = (speechContents, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContents); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCancellationToken, cancellationToken); + return expectedResult.Task; + } + }; + + using var delegating = new NoOpDelegatingSpeechToTextClient(inner); + + // Act + var resultTask = delegating.GetResponseAsync(expectedContents, expectedOptions, expectedCancellationToken); + + // Assert + Assert.False(resultTask.IsCompleted); + expectedResult.SetResult(expectedResponse); + Assert.True(resultTask.IsCompleted); + Assert.Same(expectedResponse, await resultTask); + } + + [Fact] + public async Task GetStreamingAsyncDefaultsToInnerClientAsync() + { + // Arrange + var expectedContents = new List>(); + var expectedOptions = new SpeechToTextOptions(); + var expectedCancellationToken = CancellationToken.None; + SpeechToTextResponseUpdate[] expectedResults = + [ + new() { Text = "Text update 1" }, + new() { Text = "Text update 2" } + ]; + + using var inner = new TestSpeechToTextClient + { + GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContents); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCancellationToken, cancellationToken); + return YieldAsync(expectedResults); + } + }; + + using var delegating = new NoOpDelegatingSpeechToTextClient(inner); + + // Act + var resultAsyncEnumerable = delegating.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCancellationToken); + + // Assert + var enumerator = resultAsyncEnumerable.GetAsyncEnumerator(); + Assert.True(await enumerator.MoveNextAsync()); + Assert.Same(expectedResults[0], enumerator.Current); + Assert.True(await enumerator.MoveNextAsync()); + Assert.Same(expectedResults[1], enumerator.Current); + Assert.False(await enumerator.MoveNextAsync()); + } + + [Fact] + public void GetServiceThrowsForNullType() + { + using var inner = new TestSpeechToTextClient(); + using var delegating = new NoOpDelegatingSpeechToTextClient(inner); + Assert.Throws("serviceType", () => delegating.GetService(null!)); + } + + [Fact] + public void GetServiceReturnsSelfIfCompatibleWithRequestAndKeyIsNull() + { + // Arrange + using var inner = new TestSpeechToTextClient(); + using var delegating = new NoOpDelegatingSpeechToTextClient(inner); + + // Act + var client = delegating.GetService(); + + // Assert + Assert.Same(delegating, client); + } + + [Fact] + public void GetServiceDelegatesToInnerIfKeyIsNotNull() + { + // Arrange + var expectedParam = new object(); + var expectedKey = new object(); + using var expectedResult = new TestSpeechToTextClient(); + using var inner = new TestSpeechToTextClient + { + GetServiceCallback = (_, _) => expectedResult + }; + using var delegating = new NoOpDelegatingSpeechToTextClient(inner); + + // Act + var client = delegating.GetService(expectedKey); + + // Assert + Assert.Same(expectedResult, client); + } + + [Fact] + public void GetServiceDelegatesToInnerIfNotCompatibleWithRequest() + { + // Arrange + var expectedParam = new object(); + var expectedResult = TimeZoneInfo.Local; + var expectedKey = new object(); + using var inner = new TestSpeechToTextClient + { + GetServiceCallback = (type, key) => type == expectedResult.GetType() && key == expectedKey + ? expectedResult + : throw new InvalidOperationException("Unexpected call") + }; + using var delegating = new NoOpDelegatingSpeechToTextClient(inner); + + // Act + var tzi = delegating.GetService(expectedKey); + + // Assert + Assert.Same(expectedResult, tzi); + } + + private static async IAsyncEnumerable YieldAsync(IEnumerable input) + { + await Task.Yield(); + foreach (var item in input) + { + yield return item; + } + } + + private sealed class NoOpDelegatingSpeechToTextClient(ISpeechToTextClient innerClient) + : DelegatingSpeechToTextClient(innerClient); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs new file mode 100644 index 00000000000..699b64e8bd4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextClientExtensionsTests +{ + [Fact] + public void GetService_InvalidArgs_Throws() + { + Assert.Throws("client", () => + { + _ = SpeechToTextClientExtensions.GetService(null!); + }); + } + + [Fact] + public void GetResponseAsync_InvalidArgs_Throws() + { + // Note: the extension method now requires a DataContent (not a string). + Assert.Throws("client", () => + { + _ = SpeechToTextClientExtensions.GetResponseAsync(null!, new DataContent("data:audio/wav;base64,AQIDBA==")); + }); + + Assert.Throws("speechContent", () => + { + _ = SpeechToTextClientExtensions.GetResponseAsync(new TestSpeechToTextClient(), (DataContent)null!); + }); + } + + [Fact] + public void GetStreamingResponseAsync_InvalidArgs_Throws() + { + Assert.Throws("client", () => + { + using var stream = new MemoryStream(); + _ = SpeechToTextClientExtensions.GetStreamingResponseAsync(client: null!, new DataContent("data:audio/wav;base64,AQIDBA==")); + }); + + Assert.Throws("speechContent", () => + { + _ = SpeechToTextClientExtensions.GetStreamingResponseAsync(new TestSpeechToTextClient(), speechContent: null!); + }); + } + + [Fact] + public async Task GetStreamingResponseAsync_CreatesTextMessageAsync() + { + // Arrange + var expectedOptions = new SpeechToTextOptions(); + using var cts = new CancellationTokenSource(); + + using TestSpeechToTextClient client = new() + { + GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => + { + Assert.Single(speechContents); + + // For testing, return an async enumerable yielding one streaming update with text "world". + return YieldAsync(new SpeechToTextResponseUpdate { Text = "world" }); + }, + }; + + int count = 0; + await foreach (var update in SpeechToTextClientExtensions.GetStreamingResponseAsync( + client, + new DataContent("data:audio/wav;base64,AQIDBA=="), + expectedOptions, + cts.Token)) + { + Assert.Equal(0, count); + Assert.Equal("world", update.Text); + count++; + } + + Assert.Equal(1, count); + } + + private static async IAsyncEnumerable YieldAsync(params SpeechToTextResponseUpdate[] updates) + { + await Task.Yield(); + foreach (var update in updates) + { + yield return update; + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs new file mode 100644 index 00000000000..da3b2272b05 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextClientMetadataTests +{ + [Fact] + public void Constructor_NullValues_AllowedAndRoundtrip() + { + SpeechToTextClientMetadata metadata = new(null, null, null); + Assert.Null(metadata.ProviderName); + Assert.Null(metadata.ProviderUri); + Assert.Null(metadata.ModelId); + } + + [Fact] + public void Constructor_Value_Roundtrips() + { + var uri = new Uri("https://example.com"); + SpeechToTextClientMetadata metadata = new("providerName", uri, "theModel"); + Assert.Equal("providerName", metadata.ProviderName); + Assert.Same(uri, metadata.ProviderUri); + Assert.Equal("theModel", metadata.ModelId); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs new file mode 100644 index 00000000000..40a9ac21eb9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextClientTests +{ + [Fact] + public async Task GetResponseAsync_CreatesTextMessageAsync() + { + // Arrange + var expectedResponse = new SpeechToTextResponse(new SpeechToTextMessage("hello")); + var expectedOptions = new SpeechToTextOptions(); + using var cts = new CancellationTokenSource(); + + using TestSpeechToTextClient client = new() + { + GetResponseAsyncCallback = (speechContents, options, cancellationToken) => + { + // In our simulated client, we expect a single async enumerable. + Assert.Single(speechContents); + + // For the purpose of the test, we assume that the underlying implementation converts the DataContent into a transcription choice. + // (In a real implementation, the speech audio data would be processed.) + SpeechToTextMessage choice = new("hello"); + return Task.FromResult(new SpeechToTextResponse(choice)); + }, + }; + + // Act – call the extension method with a valid DataContent. + SpeechToTextResponse response = await SpeechToTextClientExtensions.GetResponseAsync( + client, + new DataContent("data:audio/wav;base64,AQIDBA=="), + expectedOptions, + cts.Token); + + // Assert + Assert.Same(expectedResponse.Message.Text, response.Message.Text); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs new file mode 100644 index 00000000000..4df4f0f91ce --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextMessageTests +{ + [Fact] + public void Constructor_Parameterless_PropsDefaulted() + { + SpeechToTextMessage choice = new(); + Assert.Empty(choice.Contents); + Assert.Null(choice.Text); + Assert.NotNull(choice.Contents); + Assert.Same(choice.Contents, choice.Contents); + Assert.Empty(choice.Contents); + Assert.Null(choice.RawRepresentation); + Assert.Null(choice.AdditionalProperties); + Assert.Null(choice.StartTime); + Assert.Null(choice.EndTime); + Assert.Equal(string.Empty, choice.ToString()); + } + + [Theory] + [InlineData(null)] + [InlineData("text")] + public void Constructor_String_PropsRoundtrip(string? text) + { + SpeechToTextMessage choice = new(text); + + Assert.Same(choice.Contents, choice.Contents); + if (text is null) + { + Assert.Empty(choice.Contents); + } + else + { + Assert.Single(choice.Contents); + TextContent tc = Assert.IsType(choice.Contents[0]); + Assert.Equal(text, tc.Text); + } + + Assert.Null(choice.RawRepresentation); + Assert.Null(choice.AdditionalProperties); + Assert.Equal(text ?? string.Empty, choice.ToString()); + } + + [Fact] + public void Constructor_List_InvalidArgs_Throws() + { + Assert.Throws("contents", () => new SpeechToTextMessage((IList)null!)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Constructor_List_PropsRoundtrip(int choiceCount) + { + List content = []; + for (int i = 0; i < choiceCount; i++) + { + content.Add(new TextContent($"text-{i}")); + } + + SpeechToTextMessage choice = new(content); + + Assert.Same(choice.Contents, choice.Contents); + if (choiceCount == 0) + { + Assert.Empty(choice.Contents); + Assert.Null(choice.Text); + } + else + { + Assert.Equal(choiceCount, choice.Contents.Count); + for (int i = 0; i < choiceCount; i++) + { + TextContent tc = Assert.IsType(choice.Contents[i]); + Assert.Equal($"text-{i}", tc.Text); + } + + Assert.Equal("text-0", choice.Text); + Assert.Equal(string.Concat(Enumerable.Range(0, choiceCount).Select(i => $"text-{i}")), choice.ToString()); + } + + Assert.Null(choice.RawRepresentation); + Assert.Null(choice.AdditionalProperties); + } + + [Fact] + public void Text_GetSet_UsesFirstTextContent() + { + SpeechToTextMessage choice = new( + [ + new DataContent("data:audio/wav;base64,AQIDBA=="), + new DataContent("data:image/png;base64,AQIDBA=="), + new FunctionCallContent("callId1", "fc1"), + new TextContent("text-1"), + new TextContent("text-2"), + new FunctionResultContent("callId1", "result"), + ]); + + TextContent textContent = Assert.IsType(choice.Contents[3]); + Assert.Equal("text-1", textContent.Text); + Assert.Equal("text-1", choice.Text); + Assert.Equal("text-1text-2", choice.ToString()); + + choice.Text = "text-3"; + Assert.Equal("text-3", choice.Text); + Assert.Equal("text-3", choice.Text); + Assert.Same(textContent, choice.Contents[3]); + Assert.Equal("text-3text-2", choice.ToString()); + } + + [Fact] + public void Text_Set_AddsTextToEmptyList() + { + SpeechToTextMessage choice = new([]); + Assert.Empty(choice.Contents); + + choice.Text = "text-1"; + Assert.Equal("text-1", choice.Text); + + Assert.Single(choice.Contents); + TextContent textContent = Assert.IsType(choice.Contents[0]); + Assert.Equal("text-1", textContent.Text); + } + + [Fact] + public void Text_Set_AddsTextToListWithNoText() + { + SpeechToTextMessage choice = new( + [ + new DataContent("data:audio/wav;base64,AQIDBA=="), + new DataContent("data:image/png;base64,AQIDBA=="), + new FunctionCallContent("callId1", "fc1"), + ]); + Assert.Equal(3, choice.Contents.Count); + + choice.Text = "text-1"; + Assert.Equal("text-1", choice.Text); + Assert.Equal(4, choice.Contents.Count); + + choice.Text = "text-2"; + Assert.Equal("text-2", choice.Text); + Assert.Equal(4, choice.Contents.Count); + + choice.Contents.RemoveAt(3); + Assert.Equal(3, choice.Contents.Count); + + choice.Text = "text-3"; + Assert.Equal("text-3", choice.Text); + Assert.Equal(4, choice.Contents.Count); + } + + [Fact] + public void Contents_InitializesToList() + { + // This is an implementation detail, but if this test starts failing, we need to ensure + // tests are in place for whatever possibly-custom implementation of IList is being used. + Assert.IsType>(new SpeechToTextMessage().Contents); + } + + [Fact] + public void Contents_Roundtrips() + { + SpeechToTextMessage choice = new(); + Assert.Empty(choice.Contents); + + List contents = []; + choice.Contents = contents; + + Assert.Same(contents, choice.Contents); + + choice.Contents = contents; + Assert.Same(contents, choice.Contents); + + choice.Contents = null; + Assert.NotNull(choice.Contents); + Assert.NotSame(contents, choice.Contents); + Assert.Empty(choice.Contents); + } + + [Fact] + public void RawRepresentation_Roundtrips() + { + SpeechToTextMessage choice = new(); + Assert.Null(choice.RawRepresentation); + + object raw = new(); + + choice.RawRepresentation = raw; + Assert.Same(raw, choice.RawRepresentation); + + // Ensure the idempotency of setting the same value + choice.RawRepresentation = raw; + Assert.Same(raw, choice.RawRepresentation); + + choice.RawRepresentation = null; + Assert.Null(choice.RawRepresentation); + + choice.RawRepresentation = raw; + Assert.Same(raw, choice.RawRepresentation); + } + + [Fact] + public void AdditionalProperties_Roundtrips() + { + SpeechToTextMessage choice = new(); + Assert.Null(choice.RawRepresentation); + + AdditionalPropertiesDictionary props = []; + + choice.AdditionalProperties = props; + Assert.Same(props, choice.AdditionalProperties); + + // Ensure the idempotency of setting the same value + choice.AdditionalProperties = props; + Assert.Same(props, choice.AdditionalProperties); + + choice.AdditionalProperties = null; + Assert.Null(choice.AdditionalProperties); + + choice.AdditionalProperties = props; + Assert.Same(props, choice.AdditionalProperties); + } + + [Fact] + public void StartTime_Roundtrips() + { + SpeechToTextMessage choice = new(); + Assert.Null(choice.StartTime); + + TimeSpan startTime = TimeSpan.FromSeconds(10); + choice.StartTime = startTime; + Assert.Equal(startTime, choice.StartTime); + + choice.StartTime = null; + Assert.Null(choice.StartTime); + } + + [Fact] + public void EndTime_Roundtrips() + { + SpeechToTextMessage choice = new(); + Assert.Null(choice.EndTime); + + TimeSpan endTime = TimeSpan.FromSeconds(20); + choice.EndTime = endTime; + Assert.Equal(endTime, choice.EndTime); + + choice.EndTime = null; + Assert.Null(choice.EndTime); + } + + [Fact] + public void ItCanBeSerializeAndDeserialized() + { + // Arrange + IList items = + [ + new TextContent("content-1") + { + AdditionalProperties = new() { ["metadata-key-1"] = "metadata-value-1" } + }, + new DataContent(new Uri("data:audio/wav;base64,AQIDBA=="), "mime-type/2") + { + AdditionalProperties = new() { ["metadata-key-2"] = "metadata-value-2" } + }, + new DataContent(new BinaryData(new[] { 1, 2, 3 }, options: TestJsonSerializerContext.Default.Options), "mime-type/3") + { + AdditionalProperties = new() { ["metadata-key-3"] = "metadata-value-3" } + }, + new TextContent("content-4") + { + AdditionalProperties = new() { ["metadata-key-4"] = "metadata-value-4" } + }, + new FunctionCallContent("function-id", "plugin-name-function-name", new Dictionary { ["parameter"] = "argument" }), + new FunctionResultContent("function-id", "function-result"), + ]; + + // Act + var message = JsonSerializer.Serialize(new SpeechToTextMessage(contents: items) + { + Text = "content-1-override", // Override the content of the first text content item that has the "content-1" content + AdditionalProperties = new() { ["choice-metadata-key-1"] = "choice-metadata-value-1" }, + StartTime = TimeSpan.FromSeconds(10), + EndTime = TimeSpan.FromSeconds(20) + }, TestJsonSerializerContext.Default.Options); + + var deserializedChoice = JsonSerializer.Deserialize(message, TestJsonSerializerContext.Default.Options)!; + + // Assert + Assert.NotNull(deserializedChoice.AdditionalProperties); + Assert.Single(deserializedChoice.AdditionalProperties); + Assert.Equal("choice-metadata-value-1", deserializedChoice.AdditionalProperties["choice-metadata-key-1"]?.ToString()); + Assert.Equal(TimeSpan.FromSeconds(10), deserializedChoice.StartTime); + Assert.Equal(TimeSpan.FromSeconds(20), deserializedChoice.EndTime); + + Assert.NotNull(deserializedChoice.Contents); + Assert.Equal(items.Count, deserializedChoice.Contents.Count); + + var textContent = deserializedChoice.Contents[0] as TextContent; + Assert.NotNull(textContent); + Assert.Equal("content-1-override", textContent.Text); + Assert.NotNull(textContent.AdditionalProperties); + Assert.Single(textContent.AdditionalProperties); + Assert.Equal("metadata-value-1", textContent.AdditionalProperties["metadata-key-1"]?.ToString()); + + var dataContent = deserializedChoice.Contents[1] as DataContent; + Assert.NotNull(dataContent); + Assert.Equal("data:mime-type/2;base64,AQIDBA==", dataContent.Uri); + Assert.Equal("mime-type/2", dataContent.MediaType); + Assert.NotNull(dataContent.AdditionalProperties); + Assert.Single(dataContent.AdditionalProperties); + Assert.Equal("metadata-value-2", dataContent.AdditionalProperties["metadata-key-2"]?.ToString()); + + dataContent = deserializedChoice.Contents[2] as DataContent; + Assert.NotNull(dataContent); + Assert.True(dataContent.Data!.Span.SequenceEqual(new BinaryData(new[] { 1, 2, 3 }, TestJsonSerializerContext.Default.Options))); + Assert.Equal("mime-type/3", dataContent.MediaType); + Assert.NotNull(dataContent.AdditionalProperties); + Assert.Single(dataContent.AdditionalProperties); + Assert.Equal("metadata-value-3", dataContent.AdditionalProperties["metadata-key-3"]?.ToString()); + + textContent = deserializedChoice.Contents[3] as TextContent; + Assert.NotNull(textContent); + Assert.Equal("content-4", textContent.Text); + Assert.NotNull(textContent.AdditionalProperties); + Assert.Single(textContent.AdditionalProperties); + Assert.Equal("metadata-value-4", textContent.AdditionalProperties["metadata-key-4"]?.ToString()); + + var functionCallContent = deserializedChoice.Contents[4] as FunctionCallContent; + Assert.NotNull(functionCallContent); + Assert.Equal("plugin-name-function-name", functionCallContent.Name); + Assert.Equal("function-id", functionCallContent.CallId); + Assert.NotNull(functionCallContent.Arguments); + Assert.Single(functionCallContent.Arguments); + Assert.Equal("argument", functionCallContent.Arguments["parameter"]?.ToString()); + + var functionResultContent = deserializedChoice.Contents[5] as FunctionResultContent; + Assert.NotNull(functionResultContent); + Assert.Equal("function-result", functionResultContent.Result?.ToString()); + Assert.Equal("function-id", functionResultContent.CallId); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs new file mode 100644 index 00000000000..23e6ec86ab1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextOptionsTests +{ + [Fact] + public void Constructor_Parameterless_PropsDefaulted() + { + SpeechToTextOptions options = new(); + Assert.Null(options.ResponseId); + Assert.Null(options.ModelId); + Assert.Null(options.SpeechLanguage); + Assert.Null(options.SpeechSampleRate); + Assert.Null(options.AdditionalProperties); + + SpeechToTextOptions clone = options.Clone(); + Assert.Null(clone.ResponseId); + Assert.Null(clone.ModelId); + Assert.Null(clone.SpeechLanguage); + Assert.Null(clone.SpeechSampleRate); + Assert.Null(clone.AdditionalProperties); + } + + [Fact] + public void Properties_Roundtrip() + { + SpeechToTextOptions options = new(); + + AdditionalPropertiesDictionary additionalProps = new() + { + ["key"] = "value", + }; + + options.ResponseId = "completionId"; + options.ModelId = "modelId"; + options.SpeechLanguage = "en-US"; + options.SpeechSampleRate = 44100; + options.AdditionalProperties = additionalProps; + + Assert.Equal("completionId", options.ResponseId); + Assert.Equal("modelId", options.ModelId); + Assert.Equal("en-US", options.SpeechLanguage); + Assert.Equal(44100, options.SpeechSampleRate); + Assert.Same(additionalProps, options.AdditionalProperties); + + SpeechToTextOptions clone = options.Clone(); + Assert.Equal("completionId", clone.ResponseId); + Assert.Equal("modelId", clone.ModelId); + Assert.Equal("en-US", clone.SpeechLanguage); + Assert.Equal(44100, clone.SpeechSampleRate); + Assert.Equal(additionalProps, clone.AdditionalProperties); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + SpeechToTextOptions options = new(); + + AdditionalPropertiesDictionary additionalProps = new() + { + ["key"] = "value", + }; + + options.ResponseId = "completionId"; + options.ModelId = "modelId"; + options.SpeechLanguage = "en-US"; + options.SpeechSampleRate = 44100; + options.AdditionalProperties = additionalProps; + + string json = JsonSerializer.Serialize(options, TestJsonSerializerContext.Default.SpeechToTextOptions); + + SpeechToTextOptions? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextOptions); + Assert.NotNull(deserialized); + + Assert.Equal("completionId", deserialized.ResponseId); + Assert.Equal("modelId", deserialized.ModelId); + Assert.Equal("en-US", deserialized.SpeechLanguage); + Assert.Equal(44100, deserialized.SpeechSampleRate); + + Assert.NotNull(deserialized.AdditionalProperties); + Assert.Single(deserialized.AdditionalProperties); + Assert.True(deserialized.AdditionalProperties.TryGetValue("key", out object? value)); + Assert.IsType(value); + Assert.Equal("value", ((JsonElement)value!).GetString()); + } + + [Theory] + [InlineData(" ")] + [InlineData(" ")] + public void AudioLanguage_InvalidCulture_ThrowsCultureNotFoundException(string invalidCulture) + { + SpeechToTextOptions options = new(); + Assert.Throws(() => options.SpeechLanguage = invalidCulture); + } + + [Fact] + public void AudioLanguage_EmptyString_SetsInvariantCulture() + { + SpeechToTextOptions options = new() + { + SpeechLanguage = string.Empty, + }; + + // InvariantCulture's Name is returned when an empty string is used. + Assert.Equal(CultureInfo.InvariantCulture.Name, options.SpeechLanguage); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs new file mode 100644 index 00000000000..a2813932b91 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextResponseTests +{ + [Fact] + public void Constructor_InvalidArgs_Throws() + { + Assert.Throws("message", () => new SpeechToTextResponse((SpeechToTextMessage)null!)); + Assert.Throws("choices", () => new SpeechToTextResponse((IList)null!)); + } + + [Fact] + public void Constructor_Choice_Roundtrips() + { + SpeechToTextMessage choice = new(); + SpeechToTextResponse completion = new(choice); + + // The choice property returns the first (and only) choice. + Assert.Same(choice, completion.Message); + Assert.Same(choice, Assert.Single(completion.Choices)); + } + + [Fact] + public void Constructor_Choices_Roundtrips() + { + List choices = + [ + new SpeechToTextMessage(), + new SpeechToTextMessage(), + new SpeechToTextMessage(), + ]; + + SpeechToTextResponse completion = new(choices); + Assert.Same(choices, completion.Choices); + Assert.Equal(3, choices.Count); + } + + [Fact] + public void Response_EmptyChoices_Throws() + { + SpeechToTextResponse completion = new([]); + Assert.Empty(completion.Choices); + Assert.Throws(() => completion.Message); + } + + [Fact] + public void Response_SingleChoice_Returned() + { + SpeechToTextMessage choice = new(); + SpeechToTextResponse completion = new([choice]); + Assert.Same(choice, completion.Message); + Assert.Same(choice, completion.Choices[0]); + } + + [Fact] + public void Response_MultipleChoices_ReturnsFirst() + { + SpeechToTextMessage first = new(); + SpeechToTextResponse completion = new([ + first, + new SpeechToTextMessage(), + ]); + Assert.Same(first, completion.Message); + Assert.Same(first, completion.Choices[0]); + } + + [Fact] + public void Choices_SetNull_Throws() + { + SpeechToTextResponse completion = new([]); + Assert.Throws("value", () => completion.Choices = null!); + } + + [Fact] + public void Properties_Roundtrip() + { + SpeechToTextResponse completion = new([]); + Assert.Null(completion.ResponseId); + completion.ResponseId = "id"; + Assert.Equal("id", completion.ResponseId); + + Assert.Null(completion.ModelId); + completion.ModelId = "modelId"; + Assert.Equal("modelId", completion.ModelId); + + Assert.Null(completion.RawRepresentation); + object raw = new(); + completion.RawRepresentation = raw; + Assert.Same(raw, completion.RawRepresentation); + + Assert.Null(completion.AdditionalProperties); + AdditionalPropertiesDictionary additionalProps = []; + completion.AdditionalProperties = additionalProps; + Assert.Same(additionalProps, completion.AdditionalProperties); + + List newChoices = [new SpeechToTextMessage(), new SpeechToTextMessage()]; + completion.Choices = newChoices; + Assert.Same(newChoices, completion.Choices); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + SpeechToTextResponse original = new( + [ + new SpeechToTextMessage("Choice1"), + new SpeechToTextMessage("Choice2"), + new SpeechToTextMessage("Choice3"), + new SpeechToTextMessage("Choice4"), + ]) + { + ResponseId = "id", + ModelId = "modelId", + RawRepresentation = new(), + AdditionalProperties = new() { ["key"] = "value" }, + }; + + string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.SpeechToTextResponse); + + SpeechToTextResponse? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextResponse); + + Assert.NotNull(result); + Assert.Equal(4, result.Choices.Count); + + for (int i = 0; i < original.Choices.Count; i++) + { + Assert.Equal($"Choice{i + 1}", result.Choices[i].Text); + } + + Assert.Equal("id", result.ResponseId); + Assert.Equal("modelId", result.ModelId); + + Assert.NotNull(result.AdditionalProperties); + Assert.Single(result.AdditionalProperties); + Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value)); + Assert.IsType(value); + Assert.Equal("value", ((JsonElement)value!).GetString()); + } + + [Fact] + public void ToString_SingleChoice_OutputsChoiceText() + { + SpeechToTextResponse completion = new( + [ + new SpeechToTextMessage("This is a test." + Environment.NewLine + "It's multiple lines.") + ]); + Assert.Equal(completion.Choices[0].Text, completion.ToString()); + } + + [Fact] + public void ToString_MultipleChoices_OutputsAllChoicesWithPrefix() + { + SpeechToTextResponse completion = new( + [ + new SpeechToTextMessage("This is a test." + Environment.NewLine + "It's multiple lines."), + new SpeechToTextMessage("So is" + Environment.NewLine + " this."), + new SpeechToTextMessage("And this."), + ]); + + StringBuilder sb = new(); + + for (int i = 0; i < completion.Choices.Count; i++) + { + if (i > 0) + { + sb.AppendLine().AppendLine(); + } + + sb.Append("Choice ").Append(i).AppendLine(":").Append(completion.Choices[i].ToString()); + } + + string expected = sb.ToString(); + Assert.Equal(expected, completion.ToString()); + } + + [Fact] + public void ToSpeechToTextResponseUpdates_SingleChoice_ReturnsExpectedUpdates() + { + // Arrange: create a completion with one choice. + SpeechToTextMessage choice = new("Text") + { + InputIndex = 0, + StartTime = TimeSpan.FromSeconds(1), + EndTime = TimeSpan.FromSeconds(2) + }; + + SpeechToTextResponse completion = new(choice) + { + ResponseId = "12345", + ModelId = "someModel", + AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, + }; + + // Act: convert to streaming updates. + SpeechToTextResponseUpdate[] updates = completion.ToSpeechToTextResponseUpdates(); + + // Filter out any null entries (if any). + SpeechToTextResponseUpdate[] nonNullUpdates = updates.Where(u => u is not null).ToArray(); + + // Our implementation creates one update per choice plus an extra update if AdditionalProperties exists. + Assert.Equal(2, nonNullUpdates.Length); + + SpeechToTextResponseUpdate update0 = nonNullUpdates[0]; + Assert.Equal("12345", update0.ResponseId); + Assert.Equal("someModel", update0.ModelId); + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update0.Kind); + Assert.Equal(choice.Text, update0.Text); + Assert.Equal(choice.InputIndex, update0.InputIndex); + Assert.Equal(choice.StartTime, update0.StartTime); + Assert.Equal(choice.EndTime, update0.EndTime); + + SpeechToTextResponseUpdate updateExtra = nonNullUpdates[1]; + + // The extra update carries the AdditionalProperties from the completion. + Assert.Null(updateExtra.Text); + Assert.Equal("value1", updateExtra.AdditionalProperties?["key1"]); + Assert.Equal(42, updateExtra.AdditionalProperties?["key2"]); + } + + [Fact] + public void ToSpeechToTextResponseUpdates_MultiChoice_ReturnsExpectedUpdates() + { + // Arrange: create two choices. + SpeechToTextMessage choice1 = new( + [ + new TextContent("Hello, "), + new DataContent("data:image/png;base64,AQIDBA==", mediaType: "image/png"), + new TextContent("world!") + ]) + { + AdditionalProperties = new() { ["choice1Key"] = "choice1Value" }, + InputIndex = 0 + }; + + SpeechToTextMessage choice2 = new( + [ + new FunctionCallContent("call123", "name"), + new FunctionResultContent("call123", 42), + ]) + { + AdditionalProperties = new() { ["choice2Key"] = "choice2Value" }, + InputIndex = 1 + }; + + SpeechToTextResponse completion = new([choice1, choice2]) + { + ResponseId = "12345", + ModelId = "someModel", + AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, + }; + + // Act: convert to streaming updates. + SpeechToTextResponseUpdate[] updates = completion.ToSpeechToTextResponseUpdates(); + SpeechToTextResponseUpdate[] nonNullUpdates = updates.Where(u => u is not null).ToArray(); + + // Two choices plus an extra update should yield 3 updates. + Assert.Equal(3, nonNullUpdates.Length); + + // Validate update from first choice. + SpeechToTextResponseUpdate update0 = nonNullUpdates[0]; + Assert.Equal("12345", update0.ResponseId); + Assert.Equal("someModel", update0.ModelId); + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update0.Kind); + Assert.Equal("Hello, ", Assert.IsType(update0.Contents[0]).Text); + Assert.Equal("image/png", Assert.IsType(update0.Contents[1]).MediaType); + Assert.Equal("world!", Assert.IsType(update0.Contents[2]).Text); + Assert.Equal(choice1.InputIndex, update0.InputIndex); + Assert.Equal("choice1Value", update0.AdditionalProperties?["choice1Key"]); + + // Validate update from second choice. + SpeechToTextResponseUpdate update1 = nonNullUpdates[1]; + Assert.Equal("12345", update1.ResponseId); + Assert.Equal("someModel", update1.ModelId); + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update1.Kind); + + // For choice2 (function call and result), we do not expect a concatenated text. + Assert.True(update1.Contents.Count >= 2); + Assert.IsType(update1.Contents[0]); + Assert.IsType(update1.Contents[1]); + Assert.Equal("choice2Value", update1.AdditionalProperties?["choice2Key"]); + + // Validate the extra update. + SpeechToTextResponseUpdate updateExtra = nonNullUpdates[2]; + Assert.Null(updateExtra.Text); + Assert.Equal("value1", updateExtra.AdditionalProperties?["key1"]); + Assert.Equal(42, updateExtra.AdditionalProperties?["key2"]); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateKindTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateKindTests.cs new file mode 100644 index 00000000000..ddc72d076db --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateKindTests.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextResponseUpdateKindTests +{ + [Fact] + public void Constructor_Value_Roundtrips() + { + Assert.Equal("abc", new SpeechToTextResponseUpdateKind("abc").Value); + } + + [Fact] + public void Constructor_NullOrWhiteSpace_Throws() + { + Assert.Throws("value", () => new SpeechToTextResponseUpdateKind(null!)); + Assert.Throws("value", () => new SpeechToTextResponseUpdateKind(" ")); + } + + [Fact] + public void Equality_UsesOrdinalIgnoreCaseComparison() + { + var kind1 = new SpeechToTextResponseUpdateKind("abc"); + var kind2 = new SpeechToTextResponseUpdateKind("ABC"); + Assert.True(kind1.Equals(kind2)); + Assert.True(kind1.Equals((object)kind2)); + Assert.True(kind1 == kind2); + Assert.False(kind1 != kind2); + + var kind3 = new SpeechToTextResponseUpdateKind("def"); + Assert.False(kind1.Equals(kind3)); + Assert.False(kind1.Equals((object)kind3)); + Assert.False(kind1 == kind3); + Assert.True(kind1 != kind3); + + Assert.Equal(kind1.GetHashCode(), new SpeechToTextResponseUpdateKind("abc").GetHashCode()); + Assert.Equal(kind1.GetHashCode(), new SpeechToTextResponseUpdateKind("ABC").GetHashCode()); + } + + [Fact] + public void Singletons_UseKnownValues() + { + Assert.Equal(SpeechToTextResponseUpdateKind.SessionOpen.ToString(), SpeechToTextResponseUpdateKind.SessionOpen.Value); + Assert.Equal(SpeechToTextResponseUpdateKind.Error.ToString(), SpeechToTextResponseUpdateKind.Error.Value); + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdating.ToString(), SpeechToTextResponseUpdateKind.TextUpdating.Value); + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated.ToString(), SpeechToTextResponseUpdateKind.TextUpdated.Value); + Assert.Equal(SpeechToTextResponseUpdateKind.SessionClose.ToString(), SpeechToTextResponseUpdateKind.SessionClose.Value); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + var kind = new SpeechToTextResponseUpdateKind("abc"); + string json = JsonSerializer.Serialize(kind, TestJsonSerializerContext.Default.SpeechToTextResponseUpdateKind); + Assert.Equal("\"abc\"", json); + + var result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextResponseUpdateKind); + Assert.Equal(kind, result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs new file mode 100644 index 00000000000..64c557413b6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextResponseUpdateTests +{ + [Fact] + public void Constructor_PropsDefaulted() + { + SpeechToTextResponseUpdate update = new(); + + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdating, update.Kind); + Assert.Null(update.Text); + Assert.Empty(update.Contents); + Assert.Null(update.ResponseId); + Assert.Equal(0, update.ChoiceIndex); + Assert.Null(update.StartTime); + Assert.Null(update.EndTime); + Assert.Equal(string.Empty, update.ToString()); + } + + [Fact] + public void Properties_Roundtrip() + { + SpeechToTextResponseUpdate update = new() + { + InputIndex = 5, + ChoiceIndex = 42, + Kind = new SpeechToTextResponseUpdateKind("custom"), + }; + + Assert.Equal(5, update.InputIndex); + Assert.Equal(42, update.ChoiceIndex); + Assert.Equal("custom", update.Kind.Value); + + // Test the computed Text property + Assert.Null(update.Text); + update.Text = "sample text"; + Assert.Equal("sample text", update.Text); + + // Contents: assigning a new list then resetting to null should yield an empty list. + List newList = new(); + newList.Add(new TextContent("content1")); + update.Contents = newList; + Assert.Same(newList, update.Contents); + update.Contents = null; + Assert.NotNull(update.Contents); + Assert.Empty(update.Contents); + + update.ResponseId = "comp123"; + Assert.Equal("comp123", update.ResponseId); + + update.StartTime = TimeSpan.FromSeconds(10); + update.EndTime = TimeSpan.FromSeconds(20); + Assert.Equal(TimeSpan.FromSeconds(10), update.StartTime); + Assert.Equal(TimeSpan.FromSeconds(20), update.EndTime); + } + + [Fact] + public void Text_GetSet_UsesFirstTextContent() + { + SpeechToTextResponseUpdate update = new( + [ + new DataContent("data:audio/wav;base64,AQIDBA==", "application/octet-stream"), + new DataContent("data:image/wav;base64,AQIDBA==", "application/octet-stream"), + new FunctionCallContent("callId1", "fc1"), + new TextContent("text-1"), + new TextContent("text-2"), + new FunctionResultContent("callId1", "result"), + ]); + + // The getter returns the text of the first TextContent (which is at index 3). + TextContent textContent = Assert.IsType(update.Contents[3]); + Assert.Equal("text-1", textContent.Text); + Assert.Equal("text-1", update.Text); + + // Assume the ToString concatenates the text of all TextContent items. + Assert.Equal("text-1text-2", update.ToString()); + + update.Text = "text-3"; + Assert.Equal("text-3", update.Text); + + // The setter should update the first TextContent item. + Assert.Same(textContent, update.Contents[3]); + Assert.Equal("text-3text-2", update.ToString()); + } + + [Fact] + public void Text_Set_AddsTextMessageToEmptyList() + { + SpeechToTextResponseUpdate update = new(); + Assert.Empty(update.Contents); + + update.Text = "text-1"; + Assert.Equal("text-1", update.Text); + + Assert.Single(update.Contents); + TextContent textContent = Assert.IsType(update.Contents[0]); + Assert.Equal("text-1", textContent.Text); + } + + [Fact] + public void Text_Set_AddsTextMessageToListWithNoText() + { + SpeechToTextResponseUpdate update = new( + [ + new DataContent("data:image/wav;base64,AQIDBA==", "application/octet-stream"), + new DataContent("data:audio/wav;base64,AQIDBA==", "application/octet-stream"), + new FunctionCallContent("callId1", "fc1"), + ]); + Assert.Equal(3, update.Contents.Count); + + update.Text = "text-1"; + Assert.Equal("text-1", update.Text); + Assert.Equal(4, update.Contents.Count); + + update.Text = "text-2"; + Assert.Equal("text-2", update.Text); + Assert.Equal(4, update.Contents.Count); + + update.Contents.RemoveAt(3); + Assert.Equal(3, update.Contents.Count); + + update.Text = "text-3"; + Assert.Equal("text-3", update.Text); + Assert.Equal(4, update.Contents.Count); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + SpeechToTextResponseUpdate original = new() + { + InputIndex = 7, + ChoiceIndex = 3, + Kind = new SpeechToTextResponseUpdateKind("transcribed"), + ResponseId = "id123", + StartTime = TimeSpan.FromSeconds(5), + EndTime = TimeSpan.FromSeconds(10), + Contents = new List + { + new TextContent("text-1"), + new DataContent("data:audio/wav;base64,AQIDBA==", "application/octet-stream") + } + }; + + string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.SpeechToTextResponseUpdate); + SpeechToTextResponseUpdate? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextResponseUpdate); + Assert.NotNull(result); + + Assert.Equal(original.InputIndex, result.InputIndex); + Assert.Equal(original.ChoiceIndex, result.ChoiceIndex); + Assert.Equal(original.Kind, result.Kind); + Assert.Equal(original.ResponseId, result.ResponseId); + Assert.Equal(original.StartTime, result.StartTime); + Assert.Equal(original.EndTime, result.EndTime); + Assert.Equal(original.Contents.Count, result.Contents.Count); + for (int i = 0; i < original.Contents.Count; i++) + { + // Compare via string conversion. + Assert.Equal(original.Contents[i].ToString(), result.Contents[i].ToString()); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index 4af54d6cfd9..c18d6e75f9b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -16,6 +16,10 @@ namespace Microsoft.Extensions.AI; UseStringEnumConverter = true)] [JsonSerializable(typeof(ChatResponse))] [JsonSerializable(typeof(ChatResponseUpdate))] +[JsonSerializable(typeof(SpeechToTextResponse))] +[JsonSerializable(typeof(SpeechToTextResponseUpdate))] +[JsonSerializable(typeof(SpeechToTextResponseUpdateKind))] +[JsonSerializable(typeof(SpeechToTextOptions))] [JsonSerializable(typeof(ChatOptions))] [JsonSerializable(typeof(EmbeddingGenerationOptions))] [JsonSerializable(typeof(Dictionary))] diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs new file mode 100644 index 00000000000..8c83cfb8d48 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +public sealed class TestSpeechToTextClient : ISpeechToTextClient +{ + public TestSpeechToTextClient() + { + GetServiceCallback = DefaultGetServiceCallback; + } + + public IServiceProvider? Services { get; set; } + + // Callbacks for asynchronous operations. + public Func>, + SpeechToTextOptions?, + CancellationToken, + Task>? + GetResponseAsyncCallback + { get; set; } + + public Func>, + SpeechToTextOptions?, + CancellationToken, + IAsyncEnumerable>? + GetStreamingResponseAsyncCallback + { get; set; } + + public Func GetServiceCallback { get; set; } + + private object? DefaultGetServiceCallback(Type serviceType, object? serviceKey) + => serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; + + public Task GetResponseAsync( + IList> speechContents, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default) + => GetResponseAsyncCallback!.Invoke(speechContents, options, cancellationToken); + + public IAsyncEnumerable GetStreamingResponseAsync( + IList> speechContents, + SpeechToTextOptions? options = null, + CancellationToken cancellationToken = default) + => GetStreamingResponseAsyncCallback!.Invoke(speechContents, options, cancellationToken); + + public object? GetService(Type serviceType, object? serviceKey = null) + => GetServiceCallback!.Invoke(serviceType, serviceKey); + + public void Dispose() + { + // Dispose of resources if any. + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs new file mode 100644 index 00000000000..d0495795937 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// +/// An that checks the request body against an expected one +/// and sends back an expected response. +/// +public sealed class DelegatedHttpHandler( + Func> sendAsyncDelegate) : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => sendAsyncDelegate(request, cancellationToken); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index cf9f4d9703d..f3158568d69 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -17,11 +17,19 @@ - + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.wav b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.wav new file mode 100644 index 0000000000000000000000000000000000000000..f909b12aefdefb5fd63db03270fa25775851cb00 GIT binary patch literal 138248 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

LVrjptWZx4aaRrGvedqy@m+d{RJ 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 YBB~4(^hG||o?U|;vg-m=)b*7A2aOJK6#xJL literal 0 HcmV?d00001 diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio002.wav b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio002.wav new file mode 100644 index 0000000000000000000000000000000000000000..010e034775b486d0823ed5eb6fda18b978f6aa83 GIT binary patch literal 149768 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 P4fV-kmf`8=qxk$kE@De` literal 0 HcmV?d00001 diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/dotnet.png b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/dotnet.png similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Integration.Tests/dotnet.png rename to test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/dotnet.png diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs new file mode 100644 index 00000000000..f70e108ae78 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.TestUtilities; +using Xunit; + +#pragma warning disable CA2214 // Do not call overridable methods in constructors + +namespace Microsoft.Extensions.AI; + +public abstract class SpeechToTextClientIntegrationTests : IDisposable +{ + private readonly ISpeechToTextClient? _client; + + protected SpeechToTextClientIntegrationTests() + { + _client = CreateClient(); + } + + public void Dispose() + { + _client?.Dispose(); + GC.SuppressFinalize(this); + } + + protected abstract ISpeechToTextClient? CreateClient(); + + [ConditionalFact] + public virtual async Task GetResponseAsync_SingleAudioRequestMessage() + { + SkipIfNotEnabled(); + + using var audioStream = GetAudioStream("audio001.wav"); + var response = await _client.GetResponseAsync([audioStream.ToAsyncEnumerable()]); + + Assert.Contains("gym", response.Message.Text, StringComparison.OrdinalIgnoreCase); + } + + [ConditionalFact] + public virtual async Task GetResponseAsync_MultipleAudioRequestMessage() + { + SkipIfNotEnabled(); + + using var firstAudioStream = GetAudioStream("audio001.wav"); + using var secondAudioStream = GetAudioStream("audio002.wav"); + + var response = await _client.GetResponseAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()]); + + var firstFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 0)); + var secondFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 1)); + + Assert.Contains("gym", firstFileChoice.Text); + Assert.Contains("who", secondFileChoice.Text); + } + + [ConditionalFact] + public virtual async Task GetStreamingResponseAsync_SingleStreamingResponseChoice() + { + SkipIfNotEnabled(); + + using var audioStream = GetAudioStream("audio001.wav"); + + StringBuilder sb = new(); + await foreach (var chunk in _client.GetStreamingResponseAsync([audioStream.ToAsyncEnumerable()])) + { + sb.Append(chunk.Text); + } + + string responseText = sb.ToString(); + Assert.Contains("finally", responseText, StringComparison.OrdinalIgnoreCase); + Assert.Contains("gym", responseText, StringComparison.OrdinalIgnoreCase); + } + + [ConditionalFact] + public virtual async Task GetStreamingResponseAsync_MultipleStreamingResponseChoice() + { + SkipIfNotEnabled(); + + using var firstAudioStream = GetAudioStream("audio001.wav"); + using var secondAudioStream = GetAudioStream("audio002.wav"); + + StringBuilder firstSb = new(); + StringBuilder secondSb = new(); + await foreach (var chunk in _client.GetStreamingResponseAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()])) + { + if (chunk.InputIndex == 0) + { + firstSb.Append(chunk.Text); + } + else + { + secondSb.Append(chunk.Text); + } + } + + string firstTranscription = firstSb.ToString(); + Assert.Contains("finally", firstTranscription, StringComparison.OrdinalIgnoreCase); + Assert.Contains("gym", firstTranscription, StringComparison.OrdinalIgnoreCase); + + string secondTranscription = secondSb.ToString(); + Assert.Contains("who would", secondTranscription, StringComparison.OrdinalIgnoreCase); + Assert.Contains("go for", secondTranscription, StringComparison.OrdinalIgnoreCase); + } + + private static Stream GetAudioStream(string fileName) + { + using Stream? s = typeof(SpeechToTextClientIntegrationTests).Assembly.GetManifestResourceStream($"Microsoft.Extensions.AI.Resources.{fileName}"); + Assert.NotNull(s); + MemoryStream ms = new(); + s.CopyTo(ms); + + ms.Position = 0; + return ms; + } + + [MemberNotNull(nameof(_client))] + protected void SkipIfNotEnabled() + { + if (_client is null) + { + throw new SkipTestException("Client is not enabled."); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimMultiPartHttpHandler.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimMultiPartHttpHandler.cs new file mode 100644 index 00000000000..6b0374d70cd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimMultiPartHttpHandler.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +#pragma warning disable S3996 // URI properties should not be strings + +///

+/// An that checks the multi-part request body as a root +/// JSON structure of properties and sends back an expected JSON response. +/// +/// +/// The order of the properties does not affect the comparison. +/// +/// An expected input of { "name": "something" } will Assert for a multipart body that has +/// a name field with a value of something. +/// +/// +/// An expected input of { "multiple[]": ["one","two"] } will Assert for a multipart body that has +/// two multiple[] fields each having "one" and "two" value respectively. +/// +/// +/// +/// A JSON string representing the expected structure and values of the multipart request body to be verified. +/// For example, { "name": "something" } or { "multiple[]": ["one","two"] }. +/// +/// +/// A JSON string that will be returned as the response body when the request matches the expected input. +/// +public class VerbatimMultiPartHttpHandler(string expectedInput, string sentJsonOutput) : HttpClientHandler +{ + public string? ExpectedRequestUriContains { get; init; } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + Assert.NotNull(request.Content); + Assert.NotNull(request.Content.Headers.ContentType); + Assert.Equal("multipart/form-data", request.Content.Headers.ContentType.MediaType); + + Assert.NotNull(request.RequestUri); + if (!string.IsNullOrEmpty(ExpectedRequestUriContains)) + { + Assert.Contains(ExpectedRequestUriContains!, request.RequestUri!.ToString()); + } + + Dictionary parameters = []; + + // Extract the boundary + string? boundary = request.Content.Headers.ContentType.Parameters + .FirstOrDefault(p => p.Name == "boundary")?.Value; + + if (string.IsNullOrEmpty(boundary)) + { + throw new InvalidOperationException("Boundary not found."); + } + + string fullBoundary = $"--{boundary!.Trim('"')}"; + + // Read the entire body into memory (for simplicity; stream in production for large data) +#if NET + byte[] bodyBytes = await request.Content.ReadAsByteArrayAsync(cancellationToken); +#else + byte[] bodyBytes = await request.Content.ReadAsByteArrayAsync(); +#endif + using var stream = new MemoryStream(bodyBytes); + using var reader = new StreamReader(stream, Encoding.UTF8); +#if NET + + string bodyText = await reader.ReadToEndAsync(cancellationToken); +#else + string bodyText = await reader.ReadToEndAsync(); +#endif + + // Make it legible for debugging and splitting + bodyText = RemoveSpecialCharacters(bodyText); + + string[] parts = bodyText.Split(new string[] { fullBoundary }, StringSplitOptions.None); + + foreach (string part in parts) + { + if (part.Trim() == "--") + { + continue; // End boundary + } + + // Parse headers and body + int headerEnd = part.IndexOf("\r\n\r\n"); + if (headerEnd < 0) + { + continue; + } + + string headers = part.Substring(0, headerEnd).Trim(); + string rawValue = part.Substring(headerEnd + 4).TrimEnd('\r', '\n'); + + // Get the parameter name and value + if (headers.Contains("name=")) + { + // Text field + string name = ExtractNameFromHeaders(headers); + + // Skip file fields + if (!name.StartsWith("file")) + { + if (parameters.ContainsKey(name)) + { + ((List)parameters[name]).Add(ParseContentToJsonElement(rawValue)); + } + else + { + parameters.Add(name, new List { ParseContentToJsonElement(rawValue) }); + } + } + } + } + + // Transform one value lists into single values + foreach (var key in parameters.Keys.ToList()) + { + if (parameters[key] is List list && list.Count == 1) + { + parameters[key] = list[0]; + } + } + + var jsonParameters = JsonSerializer.Serialize(parameters); + Assert.NotNull(jsonParameters); + + AssertJsonEquals(expectedInput, jsonParameters); + + return new() { Content = new StringContent(sentJsonOutput, Encoding.UTF8, "application/json") }; + } + + private static string RemoveSpecialCharacters(string input) + { + return Regex.Replace(input, @"[^a-zA-Z0-9_ .,!?\r\n""=;\//\[\]-]", ""); + } + + private static JsonElement ParseContentToJsonElement(string content) + { + // Try parsing as a number + if (int.TryParse(content, out int intValue)) + { + return JsonSerializer.SerializeToElement(intValue); + } + + if (double.TryParse(content, out double doubleValue)) + { + return JsonSerializer.SerializeToElement(doubleValue); + } + + // Try parsing as a boolean + if (bool.TryParse(content, out bool boolValue)) + { + return JsonSerializer.SerializeToElement(boolValue); + } + + // Default to string + return JsonSerializer.SerializeToElement(content); + } + + private static string ExtractNameFromHeaders(string headers) + { + const string NamePrefix = "name="; + int start = headers.IndexOf(NamePrefix) + NamePrefix.Length; + int end = headers.IndexOf(";", start); + + if (end == -1) + { + end = headers.Length; + } + + return headers.Substring(start, end - start).Trim('"'); + } + + public static string? RemoveWhiteSpace(string? text) => + text is null ? null : + Regex.Replace(text, @"\s*", string.Empty); + + private static Dictionary? GetCharacterFrequencies(string text) + => RemoveWhiteSpace(text)?.GroupBy(c => c) + .ToDictionary(g => g.Key, g => g.Count()); + + private static void AssertJsonEquals(string expected, string actual) + { + var expectedFrequencies = GetCharacterFrequencies(expected); + var actualFrequencies = GetCharacterFrequencies(actual); + + Assert.NotNull(expectedFrequencies); + Assert.NotNull(actualFrequencies); + + foreach (var kvp in expectedFrequencies) + { + if (!actualFrequencies.ContainsKey(kvp.Key) || kvp.Value != actualFrequencies[kvp.Key]) + { + Assert.Fail($"Expected: {expected}, Actual: {actual}"); + } + + // Ensure the frequencies are equal during the test + Assert.Equal(kvp.Value, actualFrequencies[kvp.Key]); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 66412bfeace..2b7f238b445 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -14,6 +14,11 @@
+ + + + + diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs new file mode 100644 index 00000000000..a22db54a65d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegrationTests +{ + protected override ISpeechToTextClient? CreateClient() + => IntegrationTestHelpers.GetOpenAIClient() + ?.AsSpeechToTextClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1"); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs new file mode 100644 index 00000000000..b05f7fe7f3c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -0,0 +1,388 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Logging; +using Moq; +using OpenAI; +using OpenAI.Audio; +using Xunit; + +#pragma warning disable S103 // Lines should not be too long + +namespace Microsoft.Extensions.AI; + +public class OpenAISpeechToTextClientTests +{ + [Fact] + public void Ctor_InvalidArgs_Throws() + { + Assert.Throws("openAIClient", () => new OpenAISpeechToTextClient(null!, "model")); + Assert.Throws("audioClient", () => new OpenAISpeechToTextClient(null!)); + + OpenAIClient openAIClient = new("key"); + Assert.Throws("modelId", () => new OpenAISpeechToTextClient(openAIClient, null!)); + Assert.Throws("modelId", () => new OpenAISpeechToTextClient(openAIClient, "")); + Assert.Throws("modelId", () => new OpenAISpeechToTextClient(openAIClient, " ")); + } + + [Fact] + public void AsSpeechToTextClient_InvalidArgs_Throws() + { + Assert.Throws("openAIClient", () => ((OpenAIClient)null!).AsSpeechToTextClient("model")); + Assert.Throws("audioClient", () => ((AudioClient)null!).AsSpeechToTextClient()); + + OpenAIClient client = new("key"); + Assert.Throws("modelId", () => client.AsSpeechToTextClient(null!)); + Assert.Throws("modelId", () => client.AsSpeechToTextClient(" ")); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AsSpeechToTextClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + { + Uri endpoint = new("http://localhost/some/endpoint"); + string model = "amazingModel"; + + var openAIClient = useAzureOpenAI ? + new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : + new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + + ISpeechToTextClient client = openAIClient.AsSpeechToTextClient(model); + var metadata = client.GetService(); + Assert.NotNull(metadata); + Assert.Equal("openai", metadata.ProviderName); + Assert.Equal(endpoint, metadata.ProviderUri); + Assert.Equal(model, metadata.ModelId); + + client = openAIClient.GetAudioClient(model).AsSpeechToTextClient(); + metadata = client.GetService(); + Assert.NotNull(metadata); + Assert.Equal("openai", metadata.ProviderName); + Assert.Equal(endpoint, metadata.ProviderUri); + Assert.Equal(model, metadata.ModelId); + } + + [Fact] + public void GetService_OpenAIClient_SuccessfullyReturnsUnderlyingClient() + { + OpenAIClient openAIClient = new(new ApiKeyCredential("key")); + ISpeechToTextClient client = openAIClient.AsSpeechToTextClient("model"); + + Assert.Same(client, client.GetService()); + Assert.Same(client, client.GetService()); + + Assert.Same(openAIClient, client.GetService()); + + Assert.NotNull(client.GetService()); + var mockLoggerFactory = new Mock(); + mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny())).Returns(new Mock().Object); + + using ISpeechToTextClient pipeline = client + .AsBuilder() + .UseLogging(mockLoggerFactory.Object) + .Build(); + + Assert.NotNull(pipeline.GetService()); + + Assert.Same(openAIClient, pipeline.GetService()); + Assert.IsType(pipeline.GetService()); + } + + [Fact] + public void GetService_AudioClient_SuccessfullyReturnsUnderlyingClient() + { + AudioClient openAIClient = new OpenAIClient(new ApiKeyCredential("key")).GetAudioClient("model"); + ISpeechToTextClient audioClient = openAIClient.AsSpeechToTextClient(); + + Assert.Same(audioClient, audioClient.GetService()); + Assert.Same(openAIClient, audioClient.GetService()); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny())).Returns(new Mock().Object); + using ISpeechToTextClient pipeline = audioClient + .AsBuilder() + .UseLogging(mockLoggerFactory.Object) + .Build(); + + Assert.NotNull(pipeline.GetService()); + + Assert.Same(openAIClient, pipeline.GetService()); + Assert.IsType(pipeline.GetService()); + } + + [Theory] + [InlineData("pt", null)] + [InlineData("en", null)] + [InlineData("en", "en")] + [InlineData("pt", "pt")] + public async Task BasicTranscribeRequestResponse_NonStreaming(string? speechLanguage, string? textLanguage) + { + string input = $$""" + { + "model": "whisper-1", + "language": "{{speechLanguage}}" + } + """; + + const string Output = """ + { + "text":"I finally got back to the gym the other day." + } + """; + + using VerbatimMultiPartHttpHandler handler = new(input, Output) { ExpectedRequestUriContains = "audio/transcriptions" }; + using HttpClient httpClient = new(handler); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.wav"); + var response = await client.GetResponseAsync(fileStream, new SpeechToTextOptions + { + SpeechLanguage = speechLanguage, + TextLanguage = textLanguage + }); + + Assert.NotNull(response); + + Assert.Single(response.Choices); + Assert.Contains("I finally got back to the gym the other day", response.Message.Text); + + Assert.NotNull(response.Message.RawRepresentation); + Assert.IsType(response.Message.RawRepresentation); + } + + [Fact] + public async Task CancelledBasicTranscribeRequestResponse_NonStreaming_Throw() + { + using HttpClient httpClient = new(); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.wav"); + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + await Assert.ThrowsAsync(() + => client.GetResponseAsync(fileStream, cancellationToken: cancellationTokenSource.Token)); + } + + [Fact] + public async Task CancelledBasicTranscribeRequestResponse_Streaming_Throw() + { + using HttpClient httpClient = new(); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.wav"); + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + await Assert.ThrowsAsync(() + => client + .GetStreamingResponseAsync(fileStream, cancellationToken: cancellationTokenSource.Token) + .GetAsyncEnumerator() + .MoveNextAsync() + .AsTask()); + } + + [Theory] + [InlineData("pt", null)] + [InlineData("en", null)] + [InlineData("en", "en")] + [InlineData("pt", "pt")] + public async Task BasicTranscribeRequestResponse_Streaming(string? speechLanguage, string? textLanguage) + { + // There's no support for streaming audio in the OpenAI API, + // so we're just testing the client's ability to handle streaming responses. + + string input = $$""" + { + "model": "whisper-1", + "language": "{{speechLanguage}}" + } + """; + + const string Output = """ + { + "text":"I finally got back to the gym the other day." + } + """; + + using VerbatimMultiPartHttpHandler handler = new(input, Output) { ExpectedRequestUriContains = "audio/transcriptions" }; + using HttpClient httpClient = new(handler); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.mp3"); + await foreach (var update in client.GetStreamingResponseAsync(fileStream, new SpeechToTextOptions + { + SpeechLanguage = speechLanguage, + TextLanguage = textLanguage + })) + { + Assert.Contains("I finally got back to the gym the other day", update.Text); + Assert.NotNull(update.RawRepresentation); + Assert.IsType(update.RawRepresentation); + Assert.Equal(0, update.InputIndex); + } + } + + [Theory] + [InlineData(null, "pt")] + [InlineData(null, "it")] + [InlineData("en", "pt")] + public async Task NonSupportedTranslation_Streaming_Throws(string? speechLanguage, string? textLanguage) + { + using HttpClient httpClient = new(); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.mp3"); + var asyncEnumerator = client.GetStreamingResponseAsync(fileStream, new SpeechToTextOptions + { + SpeechLanguage = speechLanguage, + TextLanguage = textLanguage + }).GetAsyncEnumerator(); + + await Assert.ThrowsAsync(() => asyncEnumerator.MoveNextAsync().AsTask()); + } + + [Theory] + [InlineData(null, "pt")] + [InlineData(null, "it")] + [InlineData("en", "pt")] + public async Task NonSupportedTranslation_NonStreaming_Throws(string? speechLanguage, string? textLanguage) + { + using HttpClient httpClient = new(); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.mp3"); + + await Assert.ThrowsAsync(() => client.GetResponseAsync(fileStream, new SpeechToTextOptions + { + SpeechLanguage = speechLanguage, + TextLanguage = textLanguage + })); + } + + [Fact] + public async Task BasicTranslateRequestResponse_Streaming() + { + string textLanguage = "en"; + + // There's no support for non english translations, so no language is passed to the API. + const string Input = $$""" + { + "model": "whisper-1" + } + """; + + const string Output = """ + { + "text":"I finally got back to the gym the other day." + } + """; + + using VerbatimMultiPartHttpHandler handler = new(Input, Output) { ExpectedRequestUriContains = "audio/translations" }; + using HttpClient httpClient = new(handler); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.mp3"); + await foreach (var update in client.GetStreamingResponseAsync(fileStream, new SpeechToTextOptions + { + SpeechLanguage = "pt", + TextLanguage = textLanguage + })) + { + Assert.Contains("I finally got back to the gym the other day", update.Text); + Assert.NotNull(update.RawRepresentation); + Assert.IsType(update.RawRepresentation); + Assert.Equal(0, update.InputIndex); + } + } + + [Fact] + public async Task NonStronglyTypedOptions_AllSent() + { + const string Input = """ + { + "model": "whisper-1", + "prompt":"Hide any bad words with ", + "temperature": 0.5, + "response_format": "vtt", + "timestamp_granularities[]": ["word","segment"] + } + """; + + const string Output = """ + { + "text":"I finally got back to the gym the other day." + } + """; + + using VerbatimMultiPartHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.mp3"); + Assert.NotNull(await client.GetResponseAsync(fileStream, new() + { + AdditionalProperties = new() + { + ["SpeechLanguage"] = "pt", + ["Temperature"] = 0.5f, + ["TimestampGranularities"] = AudioTimestampGranularities.Segment | AudioTimestampGranularities.Word, + ["Prompt"] = "Hide any bad words with ***", + ["ResponseFormat"] = AudioTranscriptionFormat.Vtt, + }, + })); + } + + [Fact] + public async Task StronglyTypedOptions_AllSent() + { + const string Input = """ + { + "model": "whisper-1", + "language": "pt" + } + """; + + const string Output = """ + { + "text":"I finally got back to the gym the other day." + } + """; + + using VerbatimMultiPartHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + + using var fileStream = GetAudioStream("audio001.mp3"); + Assert.NotNull(await client.GetResponseAsync(fileStream, new() + { + SpeechLanguage = "pt", + })); + } + + private static Stream GetAudioStream(string fileName) +#pragma warning restore S125 // Sections of code should not be commented out + { + using Stream? s = typeof(OpenAISpeechToTextClientTests).Assembly.GetManifestResourceStream($"Microsoft.Extensions.AI.Resources.{fileName}"); + Assert.NotNull(s); + MemoryStream ms = new(); + s.CopyTo(ms); + + ms.Position = 0; + return ms; + } + + private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) => + new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) + .AsSpeechToTextClient(modelId); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.mp3 b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..9fbfb2bca1750ae56aef7a8074eb42780d1664ca GIT binary patch literal 13400 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 literal 0 HcmV?d00001 diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.wav b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.wav new file mode 100644 index 0000000000000000000000000000000000000000..f909b12aefdefb5fd63db03270fa25775851cb00 GIT binary patch literal 138248 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

LVrjptWZx4aaRrGvedqy@m+d{RJ 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 YBB~4(^hG||o?U|;vg-m=)b*7A2aOJK6#xJL literal 0 HcmV?d00001 diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj index 32589c430e0..9eeca0aac59 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs new file mode 100644 index 00000000000..c4c16be01d9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ConfigureOptionsSpeechToTextClientTests +{ + [Fact] + public void ConfigureOptionsSpeechToTextClient_InvalidArgs_Throws() + { + Assert.Throws("innerClient", () => new ConfigureOptionsSpeechToTextClient(null!, _ => { })); + Assert.Throws("configure", () => new ConfigureOptionsSpeechToTextClient(new TestSpeechToTextClient(), null!)); + } + + [Fact] + public void ConfigureOptions_InvalidArgs_Throws() + { + using var innerClient = new TestSpeechToTextClient(); + var builder = innerClient.AsBuilder(); + Assert.Throws("configure", () => builder.ConfigureOptions(null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullProvidedOptions) + { + SpeechToTextOptions? providedOptions = nullProvidedOptions ? null : new() { ModelId = "test" }; + SpeechToTextOptions? returnedOptions = null; + SpeechToTextResponse expectedResponse = new([]); + var expectedUpdates = Enumerable.Range(0, 3).Select(i => new SpeechToTextResponseUpdate()).ToArray(); + using CancellationTokenSource cts = new(); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetResponseAsyncCallback = (speechContents, options, cancellationToken) => + { + Assert.Same(returnedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return Task.FromResult(expectedResponse); + }, + + GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => + { + Assert.Same(returnedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return YieldUpdates(expectedUpdates); + }, + }; + + using var client = innerClient + .AsBuilder() + .ConfigureOptions(options => + { + Assert.NotSame(providedOptions, options); + if (nullProvidedOptions) + { + Assert.Null(options.ModelId); + } + else + { + Assert.Equal(providedOptions!.ModelId, options.ModelId); + } + + returnedOptions = options; + }) + .Build(); + + var response = await client.GetResponseAsync([], providedOptions, cts.Token); + Assert.Same(expectedResponse, response); + + int i = 0; + await using var e = client.GetStreamingResponseAsync([], providedOptions, cts.Token).GetAsyncEnumerator(); + while (i < expectedUpdates.Length) + { + Assert.True(await e.MoveNextAsync()); + Assert.Same(expectedUpdates[i++], e.Current); + } + + Assert.False(await e.MoveNextAsync()); + + static async IAsyncEnumerable YieldUpdates(SpeechToTextResponseUpdate[] updates) + { + foreach (var update in updates) + { + await Task.Yield(); + yield return update; + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs new file mode 100644 index 00000000000..07d54c4737a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class LoggingSpeechToTextClientTests +{ + [Fact] + public void LoggingSpeechToTextClient_InvalidArgs_Throws() + { + Assert.Throws("innerClient", () => new LoggingSpeechToTextClient(null!, NullLogger.Instance)); + Assert.Throws("logger", () => new LoggingSpeechToTextClient(new TestSpeechToTextClient(), null!)); + } + + [Fact] + public void UseLogging_AvoidsInjectingNopClient() + { + using var innerClient = new TestSpeechToTextClient(); + + Assert.Null(innerClient.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build().GetService(typeof(LoggingSpeechToTextClient))); + Assert.Same(innerClient, innerClient.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build().GetService(typeof(ISpeechToTextClient))); + + using var factory = LoggerFactory.Create(b => b.AddFakeLogging()); + Assert.NotNull(innerClient.AsBuilder().UseLogging(factory).Build().GetService(typeof(LoggingSpeechToTextClient))); + + ServiceCollection c = new(); + c.AddFakeLogging(); + var services = c.BuildServiceProvider(); + Assert.NotNull(innerClient.AsBuilder().UseLogging().Build(services).GetService(typeof(LoggingSpeechToTextClient))); + Assert.NotNull(innerClient.AsBuilder().UseLogging(null).Build(services).GetService(typeof(LoggingSpeechToTextClient))); + Assert.Null(innerClient.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build(services).GetService(typeof(LoggingSpeechToTextClient))); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public async Task GetResponseAsync_LogsResponseInvocationAndCompletion(LogLevel level) + { + var collector = new FakeLogCollector(); + + ServiceCollection c = new(); + c.AddLogging(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); + var services = c.BuildServiceProvider(); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + return Task.FromResult(new SpeechToTextResponse([new("blue whale")])); + }, + }; + + using ISpeechToTextClient client = innerClient + .AsBuilder() + .UseLogging() + .Build(services); + + await client.GetResponseAsync( + [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], + new SpeechToTextOptions { SpeechLanguage = "pt" }); + + var logs = collector.GetSnapshot(); + if (level is LogLevel.Trace) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains("GetResponseAsync invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains("GetResponseAsync completed:") && entry.Message.Contains("blue whale"))); + } + else if (level is LogLevel.Debug) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains("GetResponseAsync invoked.") && !entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains("GetResponseAsync completed.") && !entry.Message.Contains("blue whale"))); + } + else + { + Assert.Empty(logs); + } + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public async Task GetResponseStreamingStreamAsync_LogsUpdateReceived(LogLevel level) + { + var collector = new FakeLogCollector(); + using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => GetUpdatesAsync() + }; + + static async IAsyncEnumerable GetUpdatesAsync() + { + await Task.Yield(); + yield return new SpeechToTextResponseUpdate { Text = "blue " }; + yield return new SpeechToTextResponseUpdate { Text = "whale" }; + } + + using ISpeechToTextClient client = innerClient + .AsBuilder() + .UseLogging(loggerFactory) + .Build(); + + await foreach (var update in client.GetStreamingResponseAsync( + [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], + new SpeechToTextOptions { SpeechLanguage = "pt" })) + { + // nop + } + + var logs = collector.GetSnapshot(); + if (level is LogLevel.Trace) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update:") && entry.Message.Contains("blue")), + entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update:") && entry.Message.Contains("whale")), + entry => Assert.Contains("GetStreamingResponseAsync completed.", entry.Message)); + } + else if (level is LogLevel.Debug) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync invoked.") && !entry.Message.Contains("speechLanguage")), + entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update.") && !entry.Message.Contains("blue")), + entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update.") && !entry.Message.Contains("whale")), + entry => Assert.Contains("GetStreamingResponseAsync completed.", entry.Message)); + } + else + { + Assert.Empty(logs); + } + } + + private static async IAsyncEnumerable YieldAsync(IEnumerable input) + { + await Task.Yield(); + foreach (var item in input) + { + yield return item; + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SingletonSpeechToTextClientExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SingletonSpeechToTextClientExtensions.cs new file mode 100644 index 00000000000..5fc038f8147 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SingletonSpeechToTextClientExtensions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public static class SingletonSpeechToTextClientExtensions +{ + public static SpeechToTextClientBuilder UseSingletonMiddleware(this SpeechToTextClientBuilder builder) + => builder.Use((inner, services) + => new SpeechToTextClientDependencyInjectionPatterns.SingletonMiddleware(inner, services)); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs new file mode 100644 index 00000000000..07596a1bb6f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextClientDependencyInjectionPatterns +{ + private IServiceCollection ServiceCollection { get; } = new ServiceCollection(); + + [Fact] + public void CanRegisterSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddSpeechToTextClient(services => new TestSpeechToTextClient { Services = services }) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); + var instance2 = scope2.ServiceProvider.GetRequiredService(); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); + } + + [Fact] + public void CanRegisterSingletonUsingSharedInstance() + { + // Arrange/Act + using var singleton = new TestSpeechToTextClient(); + ServiceCollection.AddSpeechToTextClient(singleton) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); + var instance2 = scope2.ServiceProvider.GetRequiredService(); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddKeyedSpeechToTextClient("mykey", services => new TestSpeechToTextClient { Services = services }) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingSharedInstance() + { + // Arrange/Act + using var singleton = new TestSpeechToTextClient(); + ServiceCollection.AddKeyedSpeechToTextClient("mykey", singleton) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerClient); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddSpeechToTextClient_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + SpeechToTextClientBuilder builder = lifetime.HasValue + ? sc.AddSpeechToTextClient(services => new TestSpeechToTextClient(), lifetime.Value) + : sc.AddSpeechToTextClient(services => new TestSpeechToTextClient()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(ISpeechToTextClient), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddKeyedSpeechToTextClient_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + SpeechToTextClientBuilder builder = lifetime.HasValue + ? sc.AddKeyedSpeechToTextClient("key", services => new TestSpeechToTextClient(), lifetime.Value) + : sc.AddKeyedSpeechToTextClient("key", services => new TestSpeechToTextClient()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(ISpeechToTextClient), sd.ServiceType); + Assert.True(sd.IsKeyedService); + Assert.Equal("key", sd.ServiceKey); + Assert.Null(sd.KeyedImplementationInstance); + Assert.NotNull(sd.KeyedImplementationFactory); + Assert.IsType(sd.KeyedImplementationFactory(null!, null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + public class SingletonMiddleware(ISpeechToTextClient inner, IServiceProvider services) : DelegatingSpeechToTextClient(inner) + { + public new ISpeechToTextClient InnerClient => base.InnerClient; + public IServiceProvider Services => services; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs new file mode 100644 index 00000000000..eab1e993aad --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class UseDelegateSpeechToTextClientTests +{ + [Fact] + public void InvalidArgs_Throws() + { + using var client = new TestSpeechToTextClient(); + SpeechToTextClientBuilder builder = new(client); + + Assert.Throws("sharedFunc", () => + builder.Use((AnonymousDelegatingSpeechToTextClient.GetResponseSharedFunc)null!)); + + Assert.Throws("getResponseFunc", () => builder.Use(null!, null!)); + + Assert.Throws("innerClient", () => new AnonymousDelegatingSpeechToTextClient(null!, delegate { return Task.CompletedTask; })); + Assert.Throws("sharedFunc", () => new AnonymousDelegatingSpeechToTextClient(client, null!)); + + Assert.Throws("innerClient", () => new AnonymousDelegatingSpeechToTextClient(null!, null!, null!)); + Assert.Throws("getResponseFunc", () => new AnonymousDelegatingSpeechToTextClient(client, null!, null!)); + } + + [Fact] + public async Task Shared_ContextPropagated() + { + IList> expectedContents = []; + SpeechToTextOptions expectedOptions = new(); + using CancellationTokenSource expectedCts = new(); + + AsyncLocal asyncLocal = new(); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetResponseAsyncCallback = (speechContentsList, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + Assert.Equal(42, asyncLocal.Value); + return Task.FromResult(new SpeechToTextResponse(new SpeechToTextMessage { Text = "hello" })); + }, + + GetStreamingResponseAsyncCallback = (speechContentsList, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + Assert.Equal(42, asyncLocal.Value); + return YieldUpdates(new SpeechToTextResponseUpdate { Text = "world" }); + }, + }; + + using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) + .Use(async (chatMessages, options, next, cancellationToken) => + { + Assert.Same(expectedContents, chatMessages); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + asyncLocal.Value = 42; + await next(chatMessages, options, cancellationToken); + }) + .Build(); + + Assert.Equal(0, asyncLocal.Value); + SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); + Assert.Equal("hello", response.Message.Text); + + Assert.Equal(0, asyncLocal.Value); + response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); + Assert.Equal("world", response.Message.Text); + } + + [Fact] + public async Task GetResponseFunc_ContextPropagated() + { + IList> expectedContents = []; + SpeechToTextOptions expectedOptions = new(); + using CancellationTokenSource expectedCts = new(); + AsyncLocal asyncLocal = new(); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetResponseAsyncCallback = (speechContentsList, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + Assert.Equal(42, asyncLocal.Value); + return Task.FromResult(new SpeechToTextResponse(new SpeechToTextMessage { Text = "hello" })); + }, + }; + + using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) + .Use(async (speechContentsList, options, innerClient, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + asyncLocal.Value = 42; + var cc = await innerClient.GetResponseAsync(speechContentsList, options, cancellationToken); + cc.Choices[0].Text += " world"; + return cc; + }, null) + .Build(); + + Assert.Equal(0, asyncLocal.Value); + + SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); + Assert.Equal("hello world", response.Message.Text); + + response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); + Assert.Equal("hello world", response.Message.Text); + } + + [Fact] + public async Task GetStreamingResponseFunc_ContextPropagated() + { + IList> expectedContents = []; + SpeechToTextOptions expectedOptions = new(); + using CancellationTokenSource expectedCts = new(); + AsyncLocal asyncLocal = new(); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetStreamingResponseAsyncCallback = (speechContentsList, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + Assert.Equal(42, asyncLocal.Value); + return YieldUpdates(new SpeechToTextResponseUpdate { Text = "hello" }); + }, + }; + + using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) + .Use(null, (speechContentsList, options, innerClient, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + asyncLocal.Value = 42; + return Impl(speechContentsList, options, innerClient, cancellationToken); + + static async IAsyncEnumerable Impl( + IList> speechContentsList, + SpeechToTextOptions? options, + ISpeechToTextClient innerClient, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var update in innerClient.GetStreamingResponseAsync(speechContentsList, options, cancellationToken)) + { + yield return update; + } + + yield return new() { Text = " world" }; + } + }) + .Build(); + + Assert.Equal(0, asyncLocal.Value); + + SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); + Assert.Equal("hello world", response.Message.Text); + + response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); + Assert.Equal("hello world", response.Message.Text); + } + + [Fact] + public async Task BothGetResponseAndGetStreamingResponseFuncs_ContextPropagated() + { + IList> expectedContents = []; + SpeechToTextOptions expectedOptions = new(); + using CancellationTokenSource expectedCts = new(); + AsyncLocal asyncLocal = new(); + + using ISpeechToTextClient innerClient = new TestSpeechToTextClient + { + GetResponseAsyncCallback = (speechContentsList, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + Assert.Equal(42, asyncLocal.Value); + return Task.FromResult(new SpeechToTextResponse(new SpeechToTextMessage { Text = "non-streaming hello" })); + }, + + GetStreamingResponseAsyncCallback = (speechContentsList, options, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + Assert.Equal(42, asyncLocal.Value); + return YieldUpdates(new SpeechToTextResponseUpdate { Text = "streaming hello" }); + }, + }; + + using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) + .Use( + async (speechContentsList, options, innerClient, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + asyncLocal.Value = 42; + var cc = await innerClient.GetResponseAsync(speechContentsList, options, cancellationToken); + cc.Choices[0].Text += " world (non-streaming)"; + return cc; + }, + (speechContentsList, options, innerClient, cancellationToken) => + { + Assert.Same(expectedContents, speechContentsList); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCts.Token, cancellationToken); + asyncLocal.Value = 42; + return Impl(speechContentsList, options, innerClient, cancellationToken); + + static async IAsyncEnumerable Impl( + IList> speechContentsList, + SpeechToTextOptions? options, + ISpeechToTextClient innerClient, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var update in innerClient.GetStreamingResponseAsync(speechContentsList, options, cancellationToken)) + { + yield return update; + } + + yield return new() { Text = " world (streaming)" }; + } + }) + .Build(); + + Assert.Equal(0, asyncLocal.Value); + + SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); + Assert.Equal("non-streaming hello world (non-streaming)", response.Message.Text); + + response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); + Assert.Equal("streaming hello world (streaming)", response.Message.Text); + } + + private static async IAsyncEnumerable YieldUpdates(params SpeechToTextResponseUpdate[] updates) + { + foreach (var update in updates) + { + await Task.Yield(); + yield return update; + } + } +} From 4bdb7b9b824444901a6bf617c676ac5de69371cc Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 18 Mar 2025 18:23:40 -0400 Subject: [PATCH 02/27] Address some feedback (still more things to address) --- .../Contents/ErrorContent.cs | 38 ++- ...icrosoft.Extensions.AI.Abstractions.csproj | 4 +- .../DelegatingSpeechToTextClient.cs | 10 +- .../SpeechToText/ISpeechToTextClient.cs | 10 +- .../SpeechToTextClientExtensions.cs | 10 +- .../SpeechToTextClientMetadata.cs | 2 + .../SpeechToText/SpeechToTextMessage.cs | 1 + .../SpeechToText/SpeechToTextOptions.cs | 2 + .../SpeechToText/SpeechToTextResponse.cs | 2 + .../SpeechToTextResponseUpdate.cs | 1 + .../SpeechToTextResponseUpdateExtensions.cs | 16 +- .../Utilities/AIJsonUtilities.Defaults.cs | 10 +- .../DataContentAsyncEnumerableStream.cs | 197 ++++++------- .../Microsoft.Extensions.AI.OpenAI.csproj | 8 +- .../OpenAIClientExtensions.cs | 3 + .../OpenAISpeechToTextClient.cs | 10 +- .../Microsoft.Extensions.AI.csproj | 1 + .../AnonymousDelegatingSpeechToTextClient.cs | 217 --------------- .../ConfigureOptionsSpeechToTextClient.cs | 10 +- ...ionsSpeechToTextClientBuilderExtensions.cs | 2 + .../SpeechToText/LoggingSpeechToTextClient.cs | 36 +-- .../LoggingSpeechToTextClientExtensions.cs | 2 + .../SpeechToText/SpeechToTextClientBuilder.cs | 63 +---- ...lientBuilderServiceCollectionExtensions.cs | 2 + ...ientBuilderSpeechToTextClientExtensions.cs | 2 + ...ft.Extensions.AI.Abstractions.Tests.csproj | 1 + .../DelegatingSpeechToTextClientTests.cs | 4 +- .../TestSpeechToTextClient.cs | 4 +- ...oft.Extensions.AI.Integration.Tests.csproj | 1 + .../SpeechToTextClientIntegrationTests.cs | 8 +- ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 2 +- .../OpenAISpeechToTextClientTests.cs | 13 - .../Microsoft.Extensions.AI.Tests.csproj | 1 + ...ConfigureOptionsSpeechToTextClientTests.cs | 4 +- .../LoggingSpeechToTextClientTests.cs | 4 +- .../UseDelegateSpeechToTextClientTests.cs | 261 ------------------ 36 files changed, 212 insertions(+), 750 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs index 276c85f0a52..ceca3002f88 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs @@ -7,22 +7,31 @@ namespace Microsoft.Extensions.AI; -///

-/// Represents an error content. -/// +/// Represents an error. +/// +/// Typically, is used for non-fatal errors, where something went wrong +/// as part of the operation but the operation was still able to continue. +/// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class ErrorContent : AIContent { + /// The error message. + private string _message; + /// Initializes a new instance of the class with the specified message. /// The message to store in this content. [JsonConstructor] public ErrorContent(string message) { - Message = Throw.IfNull(message); + _message = Throw.IfNull(message); } /// Gets or sets the error message. - public string Message { get; set; } + public string Message + { + get => _message; + set => _message = Throw.IfNull(value); + } /// Gets or sets the error code. public string? ErrorCode { get; set; } @@ -32,19 +41,8 @@ public ErrorContent(string message) /// Gets a string representing this instance to display in the debugger. [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay - { - get - { - string display = $"Message = {Message} "; - - display += ErrorCode is not null ? - $", ErrorCode = {ErrorCode}" : string.Empty; - - display += Details is not null ? - $", Details = {Details}" : string.Empty; - - return display; - } - } + private string DebuggerDisplay => + $"Error = {Message}" + + (ErrorCode is not null ? $" ({ErrorCode})" : string.Empty) + + (Details is not null ? $" - {Details}" : string.Empty); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index 123a9a23334..29c6829219c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -17,16 +17,18 @@ $(TargetFrameworks);netstandard2.0 $(NoWarn);CA2227;CA1034;SA1316;S3253 + $(NoWarn);MEAI001 true true + true true + true true true true - true diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs index 11caeff92e8..7dce6c96572 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -16,6 +17,7 @@ namespace Microsoft.Extensions.AI; /// This is recommended as a base type when building clients that can be chained in any order around an underlying . /// The default implementation simply passes each call to the inner client instance. /// +[Experimental("MEAI001")] public class DelegatingSpeechToTextClient : ISpeechToTextClient { ///
@@ -38,17 +40,17 @@ public void Dispose() protected ISpeechToTextClient InnerClient { get; } /// - public virtual Task GetResponseAsync( + public virtual Task TranscribeAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.GetResponseAsync(speechContents, options, cancellationToken); + return InnerClient.TranscribeAudioAsync(speechContents, options, cancellationToken); } /// - public virtual IAsyncEnumerable GetStreamingResponseAsync( + public virtual IAsyncEnumerable TranscribeStreamingAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.GetStreamingResponseAsync(speechContents, options, cancellationToken); + return InnerClient.TranscribeStreamingAudioAsync(speechContents, options, cancellationToken); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs index b4370a22ddb..0398c699265 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -15,13 +16,14 @@ namespace Microsoft.Extensions.AI; /// It is expected that all implementations of support being used by multiple requests concurrently. /// /// -/// However, implementations of might mutate the arguments supplied to and -/// , such as by configuring the options instance. Thus, consumers of the interface either should avoid +/// However, implementations of might mutate the arguments supplied to and +/// , such as by configuring the options instance. Thus, consumers of the interface either should avoid /// using shared instances of these arguments for concurrent invocations or should otherwise ensure by construction that no /// instances are used which might employ such mutation. For example, the ConfigureOptions method be /// provided with a callback that could mutate the supplied options argument, and that should be avoided if using a singleton options instance. /// /// +[Experimental("MEAI001")] public interface ISpeechToTextClient : IDisposable { /// Sends speech speech audio contents to the model and returns the generated text. @@ -29,7 +31,7 @@ public interface ISpeechToTextClient : IDisposable /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - Task GetResponseAsync( + Task TranscribeAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); @@ -39,7 +41,7 @@ Task GetResponseAsync( /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. - IAsyncEnumerable GetStreamingResponseAsync( + IAsyncEnumerable TranscribeStreamingAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index 12ee9cb8085..d80b30983f0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,7 @@ namespace Microsoft.Extensions.AI; /// Extensions for . +[Experimental("MEAI001")] public static class SpeechToTextClientExtensions { /// Asks the for an object of type . @@ -42,7 +44,7 @@ public static Task GetResponseAsync( { IEnumerable speechContents = [Throw.IfNull(speechContent)]; return Throw.IfNull(client) - .GetResponseAsync( + .TranscribeAudioAsync( [speechContents.ToAsyncEnumerable()], options, cancellationToken); @@ -60,7 +62,7 @@ public static Task GetResponseAsync( SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) => Throw.IfNull(client) - .GetResponseAsync( + .TranscribeAudioAsync( [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], options, cancellationToken); @@ -77,7 +79,7 @@ public static IAsyncEnumerable GetStreamingResponseA SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) => Throw.IfNull(client) - .GetStreamingResponseAsync( + .TranscribeStreamingAudioAsync( [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], options, cancellationToken); @@ -96,7 +98,7 @@ public static IAsyncEnumerable GetStreamingResponseA { IEnumerable speechContents = [Throw.IfNull(speechContent)]; return Throw.IfNull(client) - .GetStreamingResponseAsync( + .TranscribeStreamingAudioAsync( [speechContents.ToAsyncEnumerable()], options, cancellationToken); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs index 49e35824757..165c2f3c470 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI; /// Provides metadata about an . +[Experimental("MEAI001")] public class SpeechToTextClientMetadata { /// Initializes a new instance of the class. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs index ccfdbb08622..01a1dd92801 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs @@ -11,6 +11,7 @@ namespace Microsoft.Extensions.AI; /// Represents a choice in an speech to text. +[Experimental("MEAI001")] public class SpeechToTextMessage { private IList? _contents; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 1c174eae051..0b7c9d713f6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Microsoft.Extensions.AI; /// Represents the options for an speech to text request. +[Experimental("MEAI001")] public class SpeechToTextOptions { private CultureInfo? _speechLanguage; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs index a4546633cd4..287ca33b66b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; @@ -10,6 +11,7 @@ namespace Microsoft.Extensions.AI; /// Represents the result of an speech to text request. +[Experimental("MEAI001")] public class SpeechToTextResponse { /// The list of choices in the generated text response. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs index e0b1521d9c7..9b39edef892 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -29,6 +29,7 @@ namespace Microsoft.Extensions.AI; /// only one slot for such an object available in . /// /// +[Experimental("MEAI001")] public class SpeechToTextResponseUpdate { private IList? _contents; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index 23f464ea265..7628c21ed1c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; #if NET using System.Runtime.InteropServices; @@ -15,6 +16,7 @@ namespace Microsoft.Extensions.AI; /// /// Provides extension methods for working with instances. /// +[Experimental("MEAI001")] public static class SpeechToTextResponseUpdateExtensions { /// Combines instances into a single . @@ -84,8 +86,15 @@ static async Task ToResponseAsync( /// The object whose properties should be updated based on . private static void ProcessUpdate(SpeechToTextResponseUpdate update, Dictionary choices, SpeechToTextResponse response) { - response.ResponseId ??= update.ResponseId; - response.ModelId ??= update.ModelId; + if (update.ResponseId is not null) + { + response.ResponseId = update.ResponseId; + } + + if (update.ModelId is not null) + { + response.ModelId = update.ModelId; + } #if NET SpeechToTextMessage choice = CollectionsMarshal.GetValueRefOrAddDefault(choices, update.ChoiceIndex, out _) ??= @@ -109,8 +118,7 @@ private static void ProcessUpdate(SpeechToTextResponseUpdate update, Dictionary< { foreach (var entry in update.AdditionalProperties) { - // Use first-wins behavior to match the behavior of the other properties. - _ = choice.AdditionalProperties.TryAdd(entry.Key, entry.Value); + choice.AdditionalProperties[entry.Key] = entry.Value; } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 2b3b4cf3082..0c47f790e88 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -77,19 +77,19 @@ private static JsonSerializerOptions CreateDefaultOptions() UseStringEnumConverter = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)] + [JsonSerializable(typeof(SpeechToTextOptions))] + [JsonSerializable(typeof(SpeechToTextClientMetadata))] + [JsonSerializable(typeof(SpeechToTextResponse))] + [JsonSerializable(typeof(SpeechToTextResponseUpdate))] + [JsonSerializable(typeof(IReadOnlyList))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(ChatOptions))] - [JsonSerializable(typeof(SpeechToTextOptions))] [JsonSerializable(typeof(EmbeddingGenerationOptions))] [JsonSerializable(typeof(ChatClientMetadata))] - [JsonSerializable(typeof(SpeechToTextClientMetadata))] [JsonSerializable(typeof(EmbeddingGeneratorMetadata))] [JsonSerializable(typeof(ChatResponse))] [JsonSerializable(typeof(ChatResponseUpdate))] [JsonSerializable(typeof(IReadOnlyList))] - [JsonSerializable(typeof(SpeechToTextResponse))] - [JsonSerializable(typeof(SpeechToTextResponseUpdate))] - [JsonSerializable(typeof(IReadOnlyList))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(JsonDocument))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs index ed9b0e031ec..398b74c46c0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs @@ -2,29 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Buffers; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits +#pragma warning disable SA1202 // Elements should be ordered by access + namespace Microsoft.Extensions.AI; /// /// Utility class to stream data content as a . /// -#if !NET8_0_OR_GREATER internal sealed class DataContentAsyncEnumerableStream : Stream, IAsyncDisposable -#else -internal sealed class DataContentAsyncEnumerableStream : Stream -#endif { private readonly IAsyncEnumerator _enumerator; - private bool _isCompleted; - private ReadOnlyMemory? _remainingData; - private int _remainingDataOffset; - private long _position; + private ReadOnlyMemory _current; private DataContent? _firstDataContent; /// @@ -37,13 +32,12 @@ internal sealed class DataContentAsyncEnumerableStream : Stream /// needs to be considered back in the stream if was iterated before creating the stream. /// This can happen to check if the first enumerable item contains data or is just a reference only content. /// - internal DataContentAsyncEnumerableStream(IAsyncEnumerable dataAsyncEnumerable, DataContent? firstDataContent = null, CancellationToken cancellationToken = default) + internal DataContentAsyncEnumerableStream( + IAsyncEnumerable dataAsyncEnumerable, DataContent? firstDataContent = null, CancellationToken cancellationToken = default) { _enumerator = Throw.IfNull(dataAsyncEnumerable).GetAsyncEnumerator(cancellationToken); - _remainingData = Memory.Empty; - _remainingDataOffset = 0; - _position = 0; _firstDataContent = firstDataContent; + _current = Memory.Empty; } /// @@ -59,147 +53,120 @@ internal DataContentAsyncEnumerableStream(IAsyncEnumerable dataAsyn public override long Length => throw new NotSupportedException(); /// - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + public override long Position { - _ = Throw.IfNull(destination); - - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - try - { - int bytesRead; - while ((bytesRead = await EnumeratorReadAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false)) != 0) - { -#if NET - await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); -#else - await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); -#endif - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } /// - public override long Position + public override void Flush() { - get => _position; - set => throw new NotSupportedException(); } + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// - public override void Flush() => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override long Seek(long offset, SeekOrigin origin) => - throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); /// - public override void SetLength(long value) => + public override int Read(byte[] buffer, int offset, int count) => + ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("Use ReadAsync instead for asynchronous reading."); - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); +#if NET /// - public override void Write(byte[] buffer, int offset, int count) + public override +#else + internal +#endif + async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - throw new NotSupportedException(); + if (buffer.IsEmpty) + { + return 0; + } + + while (_current.IsEmpty) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_firstDataContent is not null) + { + _current = _firstDataContent.Data; + _firstDataContent = null; + continue; + } + + if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + return 0; + } + + _current = _enumerator.Current.Data; + } + + int toCopy = Math.Min(buffer.Length, _current.Length); + _current.Slice(0, toCopy).CopyTo(buffer); + _current = _current.Slice(toCopy); + return toCopy; } +#if NET /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => EnumeratorReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + public override void CopyTo(Stream destination, int bufferSize) => + CopyToAsync(destination, bufferSize, CancellationToken.None).GetAwaiter().GetResult(); -#if NET8_0_OR_GREATER /// - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - => EnumeratorReadAsync(buffer, cancellationToken); + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + _ = Throw.IfNull(destination); + + if (!_current.IsEmpty) + { + await destination.WriteAsync(_current, cancellationToken).ConfigureAwait(false); + _current = Memory.Empty; + } + + if (_firstDataContent is not null) + { + await destination.WriteAsync(_firstDataContent.Data, cancellationToken).ConfigureAwait(false); + _firstDataContent = null; + } + + while (await _enumerator.MoveNextAsync().ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + await destination.WriteAsync(_enumerator.Current.Data, cancellationToken).ConfigureAwait(false); + } + } /// public override async ValueTask DisposeAsync() { await _enumerator.DisposeAsync().ConfigureAwait(false); - await base.DisposeAsync().ConfigureAwait(false); } #else /// - public async ValueTask DisposeAsync() - { - await _enumerator.DisposeAsync().ConfigureAwait(false); - } + public ValueTask DisposeAsync() => _enumerator.DisposeAsync(); -#pragma warning disable SA1202 // "protected" methods should come before "private" members -#pragma warning disable VSTHRD002 // Synchrnously waiting on tasks or awaiters may cause deadlocks. /// protected override void Dispose(bool disposing) { _enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); - base.Dispose(disposing); } -#pragma warning restore SA1202 // "protected" methods should come before "private" members -#pragma warning restore VSTHRD002 - #endif - - private async ValueTask EnumeratorReadAsync(Memory buffer, CancellationToken cancellationToken) - { - if (_isCompleted) - { - return 0; - } - - int bytesRead = 0; - int totalToRead = buffer.Length; - - while (bytesRead < totalToRead) - { - if (cancellationToken.IsCancellationRequested) - { - throw new OperationCanceledException("Operation was canceled by the caller.", cancellationToken); - } - - // If there's still data in the current iteration - if (_remainingData is not null && _remainingDataOffset < _remainingData.Value.Length) - { - int bytesToCopy = Math.Min(totalToRead - bytesRead, _remainingData.Value.Length - _remainingDataOffset); - _remainingData.Value.Slice(_remainingDataOffset, bytesToCopy) - .CopyTo(buffer.Slice(bytesRead, bytesToCopy)); - - _remainingDataOffset += bytesToCopy; - bytesRead += bytesToCopy; - _position += bytesToCopy; - } - else - { - // If the first data content was never read, attempt to read it now - if (_position == 0 && _firstDataContent is not null) - { - _remainingData = _firstDataContent.Data; - _remainingDataOffset = 0; - continue; - } - - // Move to the next data content in the async enumerator - if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) - { - _isCompleted = true; - break; - } - - _remainingData = _enumerator.Current.Data; - _remainingDataOffset = 0; - } - } - - return bytesRead; - } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj index f9e83e3ce88..57494264ad6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj @@ -17,17 +17,19 @@ $(TargetFrameworks);netstandard2.0 $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002;OPENAI002 + $(NoWarn);MEAI001 true true true + true + true + true true - true true - true - true + true diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 70d383b9bc6..fc1a1cc7998 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using OpenAI; using OpenAI.Assistants; using OpenAI.Audio; @@ -49,12 +50,14 @@ public static IChatClient AsChatClient(this AssistantClient assistantClient, str /// The client. /// The model. /// An that can be used to transcribe audio via the . + [Experimental("MEAI001")] public static ISpeechToTextClient AsSpeechToTextClient(this OpenAIClient openAIClient, string modelId) => new OpenAISpeechToTextClient(openAIClient, modelId); /// Gets an for use with this . /// The client. /// An that can be used to transcribe audio via the . + [Experimental("MEAI001")] public static ISpeechToTextClient AsSpeechToTextClient(this AudioClient audioClient) => new OpenAISpeechToTextClient(audioClient); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index da39e95681b..b3b7b0a0bc3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; @@ -18,7 +19,8 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . -public sealed class OpenAISpeechToTextClient : ISpeechToTextClient +[Experimental("MEAI001")] +internal sealed class OpenAISpeechToTextClient : ISpeechToTextClient { /// Default OpenAI endpoint. private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1"); @@ -88,7 +90,7 @@ public OpenAISpeechToTextClient(AudioClient audioClient) } /// - public async IAsyncEnumerable GetStreamingResponseAsync( + public async IAsyncEnumerable TranscribeStreamingAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { _ = Throw.IfNullOrEmpty(speechContents); @@ -98,7 +100,7 @@ public async IAsyncEnumerable GetStreamingResponseAs var speechContent = speechContents[inputIndex]; _ = Throw.IfNull(speechContent); - var speechResponse = await GetResponseAsync([speechContent], options, cancellationToken).ConfigureAwait(false); + var speechResponse = await TranscribeAudioAsync([speechContent], options, cancellationToken).ConfigureAwait(false); foreach (var choice in speechResponse.Choices) { @@ -113,7 +115,7 @@ public async IAsyncEnumerable GetStreamingResponseAs } /// - public async Task GetResponseAsync( + public async Task TranscribeAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNullOrEmpty(speechContents); diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 72bfb799ae7..a599edc2bf8 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -24,6 +24,7 @@ + true true true false diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs deleted file mode 100644 index f36fc1d69b0..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/AnonymousDelegatingSpeechToTextClient.cs +++ /dev/null @@ -1,217 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks - -namespace Microsoft.Extensions.AI; - -/// A delegating speech to text client that wraps an inner client with implementations provided by delegates. -public sealed class AnonymousDelegatingSpeechToTextClient : DelegatingSpeechToTextClient -{ - /// The delegate to use as the implementation of . - private readonly Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, Task>? _getResponseFunc; - - /// The delegate to use as the implementation of . - /// - /// When non-, this delegate is used as the implementation of and - /// will be invoked with the same arguments as the method itself, along with a reference to the inner client. - /// When , will delegate directly to the inner client. - /// - private readonly Func< - IList>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, IAsyncEnumerable>? _getStreamingResponseFunc; - - /// The delegate to use as the implementation of both and . - private readonly GetResponseSharedFunc? _sharedFunc; - - /// - /// Initializes a new instance of the class. - /// - /// The inner client. - /// - /// A delegate that provides the implementation for both and . - /// In addition to the arguments for the operation, it's provided with a delegate to the inner client that should be - /// used to perform the operation on the inner client. It will handle both the non-streaming and streaming cases. - /// - /// - /// This overload may be used when the anonymous implementation needs to provide pre- and/or post-processing, but doesn't - /// need to interact with the results of the operation, which will come from the inner client. - /// - /// is . - /// is . - public AnonymousDelegatingSpeechToTextClient(ISpeechToTextClient innerClient, GetResponseSharedFunc sharedFunc) - : base(innerClient) - { - _ = Throw.IfNull(sharedFunc); - - _sharedFunc = sharedFunc; - } - - /// - /// Initializes a new instance of the class. - /// - /// The inner client. - /// - /// A delegate that provides the implementation for . When , - /// must be non-null, and the implementation of - /// will use for the implementation. - /// - /// - /// A delegate that provides the implementation for . When , - /// must be non-null, and the implementation of - /// will use for the implementation. - /// - /// is . - /// Both and are . - public AnonymousDelegatingSpeechToTextClient( - ISpeechToTextClient innerClient, - Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, Task>? getResponseFunc, - Func< - IList>, - SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, IAsyncEnumerable>? getStreamingResponseFunc) - : base(innerClient) - { - ThrowIfBothDelegatesNull(getResponseFunc, getStreamingResponseFunc); - - _getResponseFunc = getResponseFunc; - _getStreamingResponseFunc = getStreamingResponseFunc; - } - - /// - public override Task GetResponseAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(speechContents); - - if (_sharedFunc is not null) - { - return RespondViaSharedAsync(speechContents, options, cancellationToken); - - async Task RespondViaSharedAsync( - IList> audioContents, SpeechToTextOptions? options, CancellationToken cancellationToken) - { - SpeechToTextResponse? completion = null; - await _sharedFunc(audioContents, options, async (audioContents, options, cancellationToken) => - { - completion = await InnerClient.GetResponseAsync(audioContents, options, cancellationToken).ConfigureAwait(false); - }, cancellationToken).ConfigureAwait(false); - - if (completion is null) - { - throw new InvalidOperationException("The wrapper completed successfully without producing a SpeechToTextResponse."); - } - - return completion; - } - } - else if (_getResponseFunc is not null) - { - return _getResponseFunc(speechContents, options, InnerClient, cancellationToken); - } - else - { - Debug.Assert(_getStreamingResponseFunc is not null, "Expected non-null streaming delegate."); - return _getStreamingResponseFunc!(speechContents, options, InnerClient, cancellationToken) - .ToSpeechToTextResponseAsync(coalesceContent: true, cancellationToken); - } - } - - /// - public override IAsyncEnumerable GetStreamingResponseAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(speechContents); - - if (_sharedFunc is not null) - { - var updates = Channel.CreateBounded(1); - -#pragma warning disable CA2016 // explicitly not forwarding the cancellation token, as we need to ensure the channel is always completed - _ = Task.Run(async () => -#pragma warning restore CA2016 - { - Exception? error = null; - try - { - await _sharedFunc(speechContents, options, async (speechContents, options, cancellationToken) => - { - await foreach (var update in InnerClient.GetStreamingResponseAsync(speechContents, options, cancellationToken).ConfigureAwait(false)) - { - await updates.Writer.WriteAsync(update, cancellationToken).ConfigureAwait(false); - } - }, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - error = ex; - throw; - } - finally - { - _ = updates.Writer.TryComplete(error); - } - }); - - return updates.Reader.ReadAllAsync(cancellationToken); - } - else if (_getStreamingResponseFunc is not null) - { - return _getStreamingResponseFunc(speechContents, options, InnerClient, cancellationToken); - } - else - { - Debug.Assert(_getResponseFunc is not null, "Expected non-null non-streaming delegate."); - return GetStreamingResponseAsyncViaGetResponseAsync(_getResponseFunc!(speechContents, options, InnerClient, cancellationToken)); - - static async IAsyncEnumerable GetStreamingResponseAsyncViaGetResponseAsync(Task task) - { - SpeechToTextResponse completion = await task.ConfigureAwait(false); - foreach (var update in completion.ToSpeechToTextResponseUpdates()) - { - yield return update; - } - } - } - } - - /// Throws an exception if both of the specified delegates are null. - /// Both and are . - internal static void ThrowIfBothDelegatesNull(object? getResponseFunc, object? getStreamingResponseFunc) - { - if (getResponseFunc is null && getStreamingResponseFunc is null) - { - Throw.ArgumentNullException(nameof(getResponseFunc), $"At least one of the {nameof(getResponseFunc)} or {nameof(getStreamingResponseFunc)} delegates must be non-null."); - } - } - - // Design note: - // The following delegate could juse use Func<...>, but it's defined as a custom delegate type - // in order to provide better discoverability / documentation / usability around its complicated - // signature with the nextAsync delegate parameter. - - /// - /// Represents a method used to call or . - /// - /// The audio contents to send. - /// The speech to text options to configure the request. - /// - /// A delegate that provides the implementation for the inner client's or - /// . It should be invoked to continue the pipeline. It accepts - /// the audio contents, options, and cancellation token, which are typically the same instances as provided to this method - /// but need not be. - /// - /// The to monitor for cancellation requests. The default is . - /// A that represents the completion of the operation. - public delegate Task GetResponseSharedFunc( - IList> speechContents, - SpeechToTextOptions? options, - Func>, SpeechToTextOptions?, CancellationToken, Task> nextAsync, - CancellationToken cancellationToken); -} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs index a05ac872d1b..8df34ac03ea 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -10,6 +11,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that configures a instance used by the remainder of the pipeline. +[Experimental("MEAI001")] public sealed class ConfigureOptionsSpeechToTextClient : DelegatingSpeechToTextClient { /// The callback delegate used to configure options. @@ -33,17 +35,17 @@ public ConfigureOptionsSpeechToTextClient(ISpeechToTextClient innerClient, Actio } /// - public override Task GetResponseAsync( + public override Task TranscribeAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetResponseAsync(speechContents, Configure(options), cancellationToken); + return base.TranscribeAudioAsync(speechContents, Configure(options), cancellationToken); } /// - public override IAsyncEnumerable GetStreamingResponseAsync( + public override IAsyncEnumerable TranscribeStreamingAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetStreamingResponseAsync(speechContents, Configure(options), cancellationToken); + return base.TranscribeStreamingAudioAsync(speechContents, Configure(options), cancellationToken); } /// Creates and configures the to pass along to the inner client. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs index bbba647d782..037d25a14d5 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClientBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.Diagnostics; #pragma warning disable SA1629 // Documentation text should end with a period @@ -9,6 +10,7 @@ namespace Microsoft.Extensions.AI; /// Provides extensions for configuring instances. +[Experimental("MEAI001")] public static class ConfigureOptionsSpeechToTextClientBuilderExtensions { /// diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index c9c08532fba..4e68bb73e6e 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; @@ -17,6 +18,7 @@ namespace Microsoft.Extensions.AI; /// The provided implementation of is thread-safe for concurrent use so long as the /// employed is also thread-safe for concurrent use. /// +[Experimental("MEAI001")] public partial class LoggingSpeechToTextClient : DelegatingSpeechToTextClient { /// An instance used for all logging. @@ -43,34 +45,34 @@ public JsonSerializerOptions JsonSerializerOptions } /// - public override async Task GetResponseAsync( + public override async Task TranscribeAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetResponseAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(TranscribeAudioAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); } else { - LogInvoked(nameof(GetResponseAsync)); + LogInvoked(nameof(TranscribeAudioAsync)); } } try { - var completion = await base.GetResponseAsync(speechContents, options, cancellationToken).ConfigureAwait(false); + var completion = await base.TranscribeAudioAsync(speechContents, options, cancellationToken).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogCompletedSensitive(nameof(GetResponseAsync), AsJson(completion)); + LogCompletedSensitive(nameof(TranscribeAudioAsync), AsJson(completion)); } else { - LogCompleted(nameof(GetResponseAsync)); + LogCompleted(nameof(TranscribeAudioAsync)); } } @@ -78,45 +80,45 @@ public override async Task GetResponseAsync( } catch (OperationCanceledException) { - LogInvocationCanceled(nameof(GetResponseAsync)); + LogInvocationCanceled(nameof(TranscribeAudioAsync)); throw; } catch (Exception ex) { - LogInvocationFailed(nameof(GetResponseAsync), ex); + LogInvocationFailed(nameof(TranscribeAudioAsync), ex); throw; } } /// - public override async IAsyncEnumerable GetStreamingResponseAsync( + public override async IAsyncEnumerable TranscribeStreamingAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetStreamingResponseAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(TranscribeStreamingAudioAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); } else { - LogInvoked(nameof(GetStreamingResponseAsync)); + LogInvoked(nameof(TranscribeStreamingAudioAsync)); } } IAsyncEnumerator e; try { - e = base.GetStreamingResponseAsync(speechContents, options, cancellationToken).GetAsyncEnumerator(cancellationToken); + e = base.TranscribeStreamingAudioAsync(speechContents, options, cancellationToken).GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) { - LogInvocationCanceled(nameof(GetStreamingResponseAsync)); + LogInvocationCanceled(nameof(TranscribeStreamingAudioAsync)); throw; } catch (Exception ex) { - LogInvocationFailed(nameof(GetStreamingResponseAsync), ex); + LogInvocationFailed(nameof(TranscribeStreamingAudioAsync), ex); throw; } @@ -136,12 +138,12 @@ public override async IAsyncEnumerable GetStreamingR } catch (OperationCanceledException) { - LogInvocationCanceled(nameof(GetStreamingResponseAsync)); + LogInvocationCanceled(nameof(TranscribeStreamingAudioAsync)); throw; } catch (Exception ex) { - LogInvocationFailed(nameof(GetStreamingResponseAsync), ex); + LogInvocationFailed(nameof(TranscribeStreamingAudioAsync), ex); throw; } @@ -160,7 +162,7 @@ public override async IAsyncEnumerable GetStreamingR yield return update; } - LogCompleted(nameof(GetStreamingResponseAsync)); + LogCompleted(nameof(TranscribeStreamingAudioAsync)); } finally { diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs index 55e38382854..ccbe4c28b34 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -10,6 +11,7 @@ namespace Microsoft.Extensions.AI; /// Provides extensions for configuring instances. +[Experimental("MEAI001")] public static class LoggingSpeechToTextClientExtensions { /// Adds logging to the audio transcription client pipeline. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs index 15cacb65f24..dae4224a94d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs @@ -3,13 +3,13 @@ using System; using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// A builder for creating pipelines of . +[Experimental("MEAI001")] public sealed class SpeechToTextClientBuilder { private readonly Func _innerClientFactory; @@ -78,63 +78,4 @@ public SpeechToTextClientBuilder Use(Func - /// Adds to the audio transcription client pipeline an anonymous delegating audio transcription client based on a delegate that provides - /// an implementation for both and . - /// - /// - /// A delegate that provides the implementation for both and - /// . In addition to the arguments for the operation, it's - /// provided with a delegate to the inner client that should be used to perform the operation on the inner client. - /// It will handle both the non-streaming and streaming cases. - /// - /// The updated instance. - /// - /// This overload may be used when the anonymous implementation needs to provide pre- and/or post-processing, but doesn't - /// need to interact with the results of the operation, which will come from the inner client. - /// - /// is . - public SpeechToTextClientBuilder Use(AnonymousDelegatingSpeechToTextClient.GetResponseSharedFunc sharedFunc) - { - _ = Throw.IfNull(sharedFunc); - - return Use((innerClient, _) => new AnonymousDelegatingSpeechToTextClient(innerClient, sharedFunc)); - } - - /// - /// Adds to the audio transcription client pipeline an anonymous delegating audio transcription client based on a delegate that provides - /// an implementation for both and . - /// - /// - /// A delegate that provides the implementation for . When , - /// must be non-null, and the implementation of - /// will use for the implementation. - /// - /// - /// A delegate that provides the implementation for . When , - /// must be non-null, and the implementation of - /// will use for the implementation. - /// - /// The updated instance. - /// - /// One or both delegates may be provided. If both are provided, they will be used for their respective methods: - /// will provide the implementation of , and - /// will provide the implementation of . - /// If only one of the delegates is provided, it will be used for both methods. That means that if - /// is supplied without , the implementation of - /// will employ limited streaming, as it will be operating on the batch output produced by . And if - /// is supplied without , the implementation of - /// will be implemented by combining the updates from . - /// - /// Both and are . - public SpeechToTextClientBuilder Use( - Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, Task>? transcribeFunc, - Func>, SpeechToTextOptions?, ISpeechToTextClient, CancellationToken, - IAsyncEnumerable>? transcribeStreamingFunc) - { - AnonymousDelegatingSpeechToTextClient.ThrowIfBothDelegatesNull(transcribeFunc, transcribeStreamingFunc); - - return Use((innerClient, _) => new AnonymousDelegatingSpeechToTextClient(innerClient, transcribeFunc, transcribeStreamingFunc)); - } } diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs index 25c261fd66f..5ef54e8db26 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; /// Provides extension methods for registering with a . +[Experimental("MEAI001")] public static class SpeechToTextClientBuilderServiceCollectionExtensions { /// Registers a singleton in the . diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs index 929c8da526d..29569c55207 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderSpeechToTextClientExtensions.cs @@ -1,12 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// Provides extension methods for working with in the context of . +[Experimental("MEAI001")] public static class SpeechToTextClientBuilderSpeechToTextClientExtensions { /// Creates a new using as its inner client. diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj index b22bdc9fdde..f7b3a0154e5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj @@ -6,6 +6,7 @@ $(NoWarn);CA1063;CA1861;CA2201;VSTHRD003;S104 + $(NoWarn);MEAI001 true diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs index be083d8162b..9da2de9777b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs @@ -40,7 +40,7 @@ public async Task GetResponseAsyncDefaultsToInnerClientAsync() using var delegating = new NoOpDelegatingSpeechToTextClient(inner); // Act - var resultTask = delegating.GetResponseAsync(expectedContents, expectedOptions, expectedCancellationToken); + var resultTask = delegating.TranscribeAudioAsync(expectedContents, expectedOptions, expectedCancellationToken); // Assert Assert.False(resultTask.IsCompleted); @@ -76,7 +76,7 @@ public async Task GetStreamingAsyncDefaultsToInnerClientAsync() using var delegating = new NoOpDelegatingSpeechToTextClient(inner); // Act - var resultAsyncEnumerable = delegating.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCancellationToken); + var resultAsyncEnumerable = delegating.TranscribeStreamingAudioAsync(expectedContents, expectedOptions, expectedCancellationToken); // Assert var enumerator = resultAsyncEnumerable.GetAsyncEnumerator(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs index 8c83cfb8d48..c67b3c094c0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs @@ -38,13 +38,13 @@ public Func serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; - public Task GetResponseAsync( + public Task TranscribeAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) => GetResponseAsyncCallback!.Invoke(speechContents, options, cancellationToken); - public IAsyncEnumerable GetStreamingResponseAsync( + public IAsyncEnumerable TranscribeStreamingAudioAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index f3158568d69..d2c12ac907f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -7,6 +7,7 @@ $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 + $(NoWarn);MEAI001 true diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index f70e108ae78..a333b9b9235 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -37,7 +37,7 @@ public virtual async Task GetResponseAsync_SingleAudioRequestMessage() SkipIfNotEnabled(); using var audioStream = GetAudioStream("audio001.wav"); - var response = await _client.GetResponseAsync([audioStream.ToAsyncEnumerable()]); + var response = await _client.TranscribeAudioAsync([audioStream.ToAsyncEnumerable()]); Assert.Contains("gym", response.Message.Text, StringComparison.OrdinalIgnoreCase); } @@ -50,7 +50,7 @@ public virtual async Task GetResponseAsync_MultipleAudioRequestMessage() using var firstAudioStream = GetAudioStream("audio001.wav"); using var secondAudioStream = GetAudioStream("audio002.wav"); - var response = await _client.GetResponseAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()]); + var response = await _client.TranscribeAudioAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()]); var firstFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 0)); var secondFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 1)); @@ -67,7 +67,7 @@ public virtual async Task GetStreamingResponseAsync_SingleStreamingResponseChoic using var audioStream = GetAudioStream("audio001.wav"); StringBuilder sb = new(); - await foreach (var chunk in _client.GetStreamingResponseAsync([audioStream.ToAsyncEnumerable()])) + await foreach (var chunk in _client.TranscribeStreamingAudioAsync([audioStream.ToAsyncEnumerable()])) { sb.Append(chunk.Text); } @@ -87,7 +87,7 @@ public virtual async Task GetStreamingResponseAsync_MultipleStreamingResponseCho StringBuilder firstSb = new(); StringBuilder secondSb = new(); - await foreach (var chunk in _client.GetStreamingResponseAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()])) + await foreach (var chunk in _client.TranscribeStreamingAudioAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()])) { if (chunk.InputIndex == 0) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 2b7f238b445..178afa087be 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -2,7 +2,7 @@ Microsoft.Extensions.AI Unit tests for Microsoft.Extensions.AI.OpenAI - $(NoWarn);OPENAI002 + $(NoWarn);OPENAI002;MEAI001 diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index b05f7fe7f3c..41b1bfbd284 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -22,18 +22,6 @@ namespace Microsoft.Extensions.AI; public class OpenAISpeechToTextClientTests { - [Fact] - public void Ctor_InvalidArgs_Throws() - { - Assert.Throws("openAIClient", () => new OpenAISpeechToTextClient(null!, "model")); - Assert.Throws("audioClient", () => new OpenAISpeechToTextClient(null!)); - - OpenAIClient openAIClient = new("key"); - Assert.Throws("modelId", () => new OpenAISpeechToTextClient(openAIClient, null!)); - Assert.Throws("modelId", () => new OpenAISpeechToTextClient(openAIClient, "")); - Assert.Throws("modelId", () => new OpenAISpeechToTextClient(openAIClient, " ")); - } - [Fact] public void AsSpeechToTextClient_InvalidArgs_Throws() { @@ -79,7 +67,6 @@ public void GetService_OpenAIClient_SuccessfullyReturnsUnderlyingClient() ISpeechToTextClient client = openAIClient.AsSpeechToTextClient("model"); Assert.Same(client, client.GetService()); - Assert.Same(client, client.GetService()); Assert.Same(openAIClient, client.GetService()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj index 9eeca0aac59..9b8967a37ce 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj @@ -6,6 +6,7 @@ $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 + $(NoWarn);MEAI001 true diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs index c4c16be01d9..a671fce2bc4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs @@ -73,11 +73,11 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP }) .Build(); - var response = await client.GetResponseAsync([], providedOptions, cts.Token); + var response = await client.TranscribeAudioAsync([], providedOptions, cts.Token); Assert.Same(expectedResponse, response); int i = 0; - await using var e = client.GetStreamingResponseAsync([], providedOptions, cts.Token).GetAsyncEnumerator(); + await using var e = client.TranscribeStreamingAudioAsync([], providedOptions, cts.Token).GetAsyncEnumerator(); while (i < expectedUpdates.Length) { Assert.True(await e.MoveNextAsync()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index 07d54c4737a..c1089df4586 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -65,7 +65,7 @@ public async Task GetResponseAsync_LogsResponseInvocationAndCompletion(LogLevel .UseLogging() .Build(services); - await client.GetResponseAsync( + await client.TranscribeAudioAsync( [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], new SpeechToTextOptions { SpeechLanguage = "pt" }); @@ -114,7 +114,7 @@ static async IAsyncEnumerable GetUpdatesAsync() .UseLogging(loggerFactory) .Build(); - await foreach (var update in client.GetStreamingResponseAsync( + await foreach (var update in client.TranscribeStreamingAudioAsync( [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], new SpeechToTextOptions { SpeechLanguage = "pt" })) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs deleted file mode 100644 index eab1e993aad..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/UseDelegateSpeechToTextClientTests.cs +++ /dev/null @@ -1,261 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class UseDelegateSpeechToTextClientTests -{ - [Fact] - public void InvalidArgs_Throws() - { - using var client = new TestSpeechToTextClient(); - SpeechToTextClientBuilder builder = new(client); - - Assert.Throws("sharedFunc", () => - builder.Use((AnonymousDelegatingSpeechToTextClient.GetResponseSharedFunc)null!)); - - Assert.Throws("getResponseFunc", () => builder.Use(null!, null!)); - - Assert.Throws("innerClient", () => new AnonymousDelegatingSpeechToTextClient(null!, delegate { return Task.CompletedTask; })); - Assert.Throws("sharedFunc", () => new AnonymousDelegatingSpeechToTextClient(client, null!)); - - Assert.Throws("innerClient", () => new AnonymousDelegatingSpeechToTextClient(null!, null!, null!)); - Assert.Throws("getResponseFunc", () => new AnonymousDelegatingSpeechToTextClient(client, null!, null!)); - } - - [Fact] - public async Task Shared_ContextPropagated() - { - IList> expectedContents = []; - SpeechToTextOptions expectedOptions = new(); - using CancellationTokenSource expectedCts = new(); - - AsyncLocal asyncLocal = new(); - - using ISpeechToTextClient innerClient = new TestSpeechToTextClient - { - GetResponseAsyncCallback = (speechContentsList, options, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - Assert.Equal(42, asyncLocal.Value); - return Task.FromResult(new SpeechToTextResponse(new SpeechToTextMessage { Text = "hello" })); - }, - - GetStreamingResponseAsyncCallback = (speechContentsList, options, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - Assert.Equal(42, asyncLocal.Value); - return YieldUpdates(new SpeechToTextResponseUpdate { Text = "world" }); - }, - }; - - using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) - .Use(async (chatMessages, options, next, cancellationToken) => - { - Assert.Same(expectedContents, chatMessages); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - asyncLocal.Value = 42; - await next(chatMessages, options, cancellationToken); - }) - .Build(); - - Assert.Equal(0, asyncLocal.Value); - SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); - Assert.Equal("hello", response.Message.Text); - - Assert.Equal(0, asyncLocal.Value); - response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); - Assert.Equal("world", response.Message.Text); - } - - [Fact] - public async Task GetResponseFunc_ContextPropagated() - { - IList> expectedContents = []; - SpeechToTextOptions expectedOptions = new(); - using CancellationTokenSource expectedCts = new(); - AsyncLocal asyncLocal = new(); - - using ISpeechToTextClient innerClient = new TestSpeechToTextClient - { - GetResponseAsyncCallback = (speechContentsList, options, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - Assert.Equal(42, asyncLocal.Value); - return Task.FromResult(new SpeechToTextResponse(new SpeechToTextMessage { Text = "hello" })); - }, - }; - - using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) - .Use(async (speechContentsList, options, innerClient, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - asyncLocal.Value = 42; - var cc = await innerClient.GetResponseAsync(speechContentsList, options, cancellationToken); - cc.Choices[0].Text += " world"; - return cc; - }, null) - .Build(); - - Assert.Equal(0, asyncLocal.Value); - - SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); - Assert.Equal("hello world", response.Message.Text); - - response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); - Assert.Equal("hello world", response.Message.Text); - } - - [Fact] - public async Task GetStreamingResponseFunc_ContextPropagated() - { - IList> expectedContents = []; - SpeechToTextOptions expectedOptions = new(); - using CancellationTokenSource expectedCts = new(); - AsyncLocal asyncLocal = new(); - - using ISpeechToTextClient innerClient = new TestSpeechToTextClient - { - GetStreamingResponseAsyncCallback = (speechContentsList, options, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - Assert.Equal(42, asyncLocal.Value); - return YieldUpdates(new SpeechToTextResponseUpdate { Text = "hello" }); - }, - }; - - using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) - .Use(null, (speechContentsList, options, innerClient, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - asyncLocal.Value = 42; - return Impl(speechContentsList, options, innerClient, cancellationToken); - - static async IAsyncEnumerable Impl( - IList> speechContentsList, - SpeechToTextOptions? options, - ISpeechToTextClient innerClient, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var update in innerClient.GetStreamingResponseAsync(speechContentsList, options, cancellationToken)) - { - yield return update; - } - - yield return new() { Text = " world" }; - } - }) - .Build(); - - Assert.Equal(0, asyncLocal.Value); - - SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); - Assert.Equal("hello world", response.Message.Text); - - response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); - Assert.Equal("hello world", response.Message.Text); - } - - [Fact] - public async Task BothGetResponseAndGetStreamingResponseFuncs_ContextPropagated() - { - IList> expectedContents = []; - SpeechToTextOptions expectedOptions = new(); - using CancellationTokenSource expectedCts = new(); - AsyncLocal asyncLocal = new(); - - using ISpeechToTextClient innerClient = new TestSpeechToTextClient - { - GetResponseAsyncCallback = (speechContentsList, options, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - Assert.Equal(42, asyncLocal.Value); - return Task.FromResult(new SpeechToTextResponse(new SpeechToTextMessage { Text = "non-streaming hello" })); - }, - - GetStreamingResponseAsyncCallback = (speechContentsList, options, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - Assert.Equal(42, asyncLocal.Value); - return YieldUpdates(new SpeechToTextResponseUpdate { Text = "streaming hello" }); - }, - }; - - using ISpeechToTextClient client = new SpeechToTextClientBuilder(innerClient) - .Use( - async (speechContentsList, options, innerClient, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - asyncLocal.Value = 42; - var cc = await innerClient.GetResponseAsync(speechContentsList, options, cancellationToken); - cc.Choices[0].Text += " world (non-streaming)"; - return cc; - }, - (speechContentsList, options, innerClient, cancellationToken) => - { - Assert.Same(expectedContents, speechContentsList); - Assert.Same(expectedOptions, options); - Assert.Equal(expectedCts.Token, cancellationToken); - asyncLocal.Value = 42; - return Impl(speechContentsList, options, innerClient, cancellationToken); - - static async IAsyncEnumerable Impl( - IList> speechContentsList, - SpeechToTextOptions? options, - ISpeechToTextClient innerClient, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var update in innerClient.GetStreamingResponseAsync(speechContentsList, options, cancellationToken)) - { - yield return update; - } - - yield return new() { Text = " world (streaming)" }; - } - }) - .Build(); - - Assert.Equal(0, asyncLocal.Value); - - SpeechToTextResponse response = await client.GetResponseAsync(expectedContents, expectedOptions, expectedCts.Token); - Assert.Equal("non-streaming hello world (non-streaming)", response.Message.Text); - - response = await client.GetStreamingResponseAsync(expectedContents, expectedOptions, expectedCts.Token).ToSpeechToTextResponseAsync(); - Assert.Equal("streaming hello world (streaming)", response.Message.Text); - } - - private static async IAsyncEnumerable YieldUpdates(params SpeechToTextResponseUpdate[] updates) - { - foreach (var update in updates) - { - await Task.Yield(); - yield return update; - } - } -} From fad8017f86916a89e0fb7544bd7a80be1a194578 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:34:21 +0000 Subject: [PATCH 03/27] Resolve conflict --- global.json | 4 +- .../OpenAIClientExtensions.cs | 2 +- .../OpenAIModelMapper.AudioTranscription.cs | 127 --------------- .../OpenAIModelMapper.AudioTranslation.cs | 103 ------------ .../OpenAISpeechToTextClient.cs | 146 +++++++++++++++++- 5 files changed, 144 insertions(+), 238 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs diff --git a/global.json b/global.json index e13e112eb61..20085c8d34e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,8 @@ { "sdk": { - "version": "9.0.104" + "version": "9.0.104", + "rollForward": "latestFeature", + "allowPrerelease": true }, "tools": { "dotnet": "9.0.104", diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 621af6935b0..8d906cea2b4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,8 +3,8 @@ using System; using System.ComponentModel; -using Microsoft.Shared.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; using OpenAI; using OpenAI.Audio; using OpenAI.Chat; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs deleted file mode 100644 index 258ebe17df9..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranscription.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.Shared.Diagnostics; -using OpenAI.Audio; - -#pragma warning disable S3440 // Variables should not be checked against the values they're about to be assigned - -namespace Microsoft.Extensions.AI; - -internal static partial class OpenAIModelMappers -{ - public static SpeechToTextMessage FromOpenAIAudioTranscription(OpenAI.Audio.AudioTranscription audioTranscription, int inputIndex) - { - _ = Throw.IfNull(audioTranscription); - - var segmentCount = audioTranscription.Segments.Count; - var wordCount = audioTranscription.Words.Count; - - TimeSpan? endTime = null; - TimeSpan? startTime = null; - if (segmentCount > 0) - { - endTime = audioTranscription.Segments[segmentCount - 1].EndTime; - startTime = audioTranscription.Segments[0].StartTime; - } - else if (wordCount > 0) - { - endTime = audioTranscription.Words[wordCount - 1].EndTime; - startTime = audioTranscription.Words[0].StartTime; - } - - // Create the return choice. - return new SpeechToTextMessage - { - RawRepresentation = audioTranscription, - InputIndex = inputIndex, - Text = audioTranscription.Text, - StartTime = startTime, - EndTime = endTime, - AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranscription.Language)] = audioTranscription.Language, - [nameof(audioTranscription.Duration)] = audioTranscription.Duration - }, - }; - } - - public static SpeechToTextOptions FromOpenAITranscriptionOptions(OpenAI.Audio.AudioTranscriptionOptions options) - { - SpeechToTextOptions result = new(); - - if (options is not null) - { - result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString() switch - { - null or "" => null, - var modelId => modelId, - }; - - result.SpeechLanguage = options.Language; - - if (options.Temperature is float temperature) - { - (result.AdditionalProperties ??= [])[nameof(options.Temperature)] = temperature; - } - - if (options.TimestampGranularities is AudioTimestampGranularities timestampGranularities) - { - (result.AdditionalProperties ??= [])[nameof(options.TimestampGranularities)] = timestampGranularities; - } - - if (options.Prompt is string prompt) - { - (result.AdditionalProperties ??= [])[nameof(options.Prompt)] = prompt; - } - - if (options.ResponseFormat is AudioTranscriptionFormat jsonFormat) - { - (result.AdditionalProperties ??= [])[nameof(options.ResponseFormat)] = jsonFormat; - } - } - - return result; - } - - /// Converts an extensions options instance to an OpenAI options instance. - public static OpenAI.Audio.AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) - { - OpenAI.Audio.AudioTranscriptionOptions result = new(); - - if (options is not null) - { - if (options.SpeechLanguage is not null) - { - result.Language = options.SpeechLanguage; - } - - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) - { - if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) - { - result.Temperature = temperature; - } - - if (additionalProperties.TryGetValue(nameof(result.TimestampGranularities), out object? timestampGranularities)) - { - result.TimestampGranularities = timestampGranularities is AudioTimestampGranularities granularities ? granularities : default; - } - - if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) - { - result.Prompt = prompt; - } - - if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranscriptionFormat? responseFormat)) - { - result.ResponseFormat = responseFormat; - } - } - } - - return result; - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs deleted file mode 100644 index 00ba0da6d56..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.AudioTranslation.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Shared.Diagnostics; -using OpenAI.Audio; - -#pragma warning disable S3440 // Variables should not be checked against the values they're about to be assigned - -namespace Microsoft.Extensions.AI; - -internal static partial class OpenAIModelMappers -{ - public static SpeechToTextMessage FromOpenAIAudioTranslation(OpenAI.Audio.AudioTranslation audioTranslation, int inputIndex) - { - _ = Throw.IfNull(audioTranslation); - - var segmentCount = audioTranslation.Segments.Count; - - TimeSpan? endTime = null; - TimeSpan? startTime = null; - if (segmentCount > 0) - { - endTime = audioTranslation.Segments[segmentCount - 1].EndTime; - startTime = audioTranslation.Segments[0].StartTime; - } - - // Create the return choice. - return new SpeechToTextMessage - { - RawRepresentation = audioTranslation, - InputIndex = inputIndex, - Text = audioTranslation.Text, - StartTime = startTime, - EndTime = endTime, - AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranslation.Language)] = audioTranslation.Language, - [nameof(audioTranslation.Duration)] = audioTranslation.Duration - }, - }; - } - - public static SpeechToTextOptions FromOpenAITranslationOptions(OpenAI.Audio.AudioTranslationOptions options) - { - SpeechToTextOptions result = new(); - - if (options is not null) - { - result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString() switch - { - null or "" => null, - var modelId => modelId, - }; - - if (options.Temperature is float temperature) - { - (result.AdditionalProperties ??= [])[nameof(options.Temperature)] = temperature; - } - - if (options.Prompt is string prompt) - { - (result.AdditionalProperties ??= [])[nameof(options.Prompt)] = prompt; - } - - if (options.ResponseFormat is AudioTranslationFormat jsonFormat) - { - (result.AdditionalProperties ??= [])[nameof(options.ResponseFormat)] = jsonFormat; - } - } - - return result; - } - - /// Converts an extensions options instance to an OpenAI options instance. - public static OpenAI.Audio.AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) - { - OpenAI.Audio.AudioTranslationOptions result = new(); - - if (options is not null) - { - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) - { - if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) - { - result.Temperature = temperature; - } - - if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) - { - result.Prompt = prompt; - } - - if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranslationFormat? responseFormat)) - { - result.ResponseFormat = responseFormat; - } - } - } - - return result; - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index b3b7b0a0bc3..c9dab980516 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -152,17 +152,17 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) AudioTranslation translationResult = await GetTranslationResultAsync(options, speechContent, firstChunk, cancellationToken).ConfigureAwait(false); - var choice = OpenAIModelMappers.FromOpenAIAudioTranslation(translationResult, inputIndex); + var choice = FromOpenAIAudioTranslation(translationResult, inputIndex); choices.Add(choice); } else { - var openAIOptions = OpenAIModelMappers.ToOpenAITranscriptionOptions(options); + var openAIOptions = ToOpenAITranscriptionOptions(options); // Transcription request AudioTranscription transcriptionResult = await GetTranscriptionResultAsync(speechContent, firstChunk, openAIOptions, cancellationToken).ConfigureAwait(false); - var choice = OpenAIModelMappers.FromOpenAIAudioTranscription(transcriptionResult, inputIndex); + var choice = FromOpenAIAudioTranscription(transcriptionResult, inputIndex); choices.Add(choice); } } @@ -176,10 +176,144 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. } + private static SpeechToTextMessage FromOpenAIAudioTranscription(AudioTranscription audioTranscription, int inputIndex) + { + _ = Throw.IfNull(audioTranscription); + + var segmentCount = audioTranscription.Segments.Count; + var wordCount = audioTranscription.Words.Count; + + TimeSpan? endTime = null; + TimeSpan? startTime = null; + if (segmentCount > 0) + { + endTime = audioTranscription.Segments[segmentCount - 1].EndTime; + startTime = audioTranscription.Segments[0].StartTime; + } + else if (wordCount > 0) + { + endTime = audioTranscription.Words[wordCount - 1].EndTime; + startTime = audioTranscription.Words[0].StartTime; + } + + // Create the return choice. + return new SpeechToTextMessage + { + RawRepresentation = audioTranscription, + InputIndex = inputIndex, + Text = audioTranscription.Text, + StartTime = startTime, + EndTime = endTime, + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(audioTranscription.Language)] = audioTranscription.Language, + [nameof(audioTranscription.Duration)] = audioTranscription.Duration + }, + }; + } + + /// Converts an extensions options instance to an OpenAI options instance. + private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) + { + AudioTranscriptionOptions result = new(); + + if (options is not null) + { + if (options.SpeechLanguage is not null) + { + result.Language = options.SpeechLanguage; + } + + if (options.AdditionalProperties is { Count: > 0 } additionalProperties) + { + if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) + { + result.Temperature = temperature; + } + + if (additionalProperties.TryGetValue(nameof(result.TimestampGranularities), out object? timestampGranularities)) + { + result.TimestampGranularities = timestampGranularities is AudioTimestampGranularities granularities ? granularities : default; + } + + if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) + { + result.Prompt = prompt; + } + + if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranscriptionFormat? responseFormat)) + { + result.ResponseFormat = responseFormat; + } + } + } + + return result; + } + + private static SpeechToTextMessage FromOpenAIAudioTranslation(AudioTranslation audioTranslation, int inputIndex) + { + _ = Throw.IfNull(audioTranslation); + + var segmentCount = audioTranslation.Segments.Count; + + TimeSpan? endTime = null; + TimeSpan? startTime = null; + if (segmentCount > 0) + { + endTime = audioTranslation.Segments[segmentCount - 1].EndTime; + startTime = audioTranslation.Segments[0].StartTime; + } + + // Create the return choice. + return new SpeechToTextMessage + { + RawRepresentation = audioTranslation, + InputIndex = inputIndex, + Text = audioTranslation.Text, + StartTime = startTime, + EndTime = endTime, + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(audioTranslation.Language)] = audioTranslation.Language, + [nameof(audioTranslation.Duration)] = audioTranslation.Duration + }, + }; + } + + /// Converts an extensions options instance to an OpenAI options instance. + private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) + { + AudioTranslationOptions result = new(); + + if (options is not null) + { + if (options.AdditionalProperties is { Count: > 0 } additionalProperties) + { + if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) + { + result.Temperature = temperature; + } + + if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) + { + result.Prompt = prompt; + } + + if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranslationFormat? responseFormat)) + { + result.ResponseFormat = responseFormat; + } + } + } + + return result; + } + private async Task GetTranscriptionResultAsync( IAsyncEnumerable speechContent, DataContent firstChunk, AudioTranscriptionOptions openAIOptions, CancellationToken cancellationToken) { - OpenAI.Audio.AudioTranscription transcriptionResult; + AudioTranscription transcriptionResult; var audioFileStream = speechContent.ToStream(firstChunk, cancellationToken); #if NET @@ -200,8 +334,8 @@ private async Task GetTranscriptionResultAsync( private async Task GetTranslationResultAsync( SpeechToTextOptions? options, IAsyncEnumerable speechContent, DataContent firstChunk, CancellationToken cancellationToken) { - var openAIOptions = OpenAIModelMappers.ToOpenAITranslationOptions(options); - OpenAI.Audio.AudioTranslation translationResult; + var openAIOptions = ToOpenAITranslationOptions(options); + AudioTranslation translationResult; var audioFileStream = speechContent.ToStream(firstChunk, cancellationToken); #if NET From 3448daa16d69a049507dbac9ce73b553468f7661 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:58:55 +0000 Subject: [PATCH 04/27] Ensure UT are working before further changes --- .../SpeechToText/LoggingSpeechToTextClient.cs | 8 +++--- .../LoggingSpeechToTextClientTests.cs | 25 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index 4e68bb73e6e..d69bc2df74a 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -175,19 +175,19 @@ public override async IAsyncEnumerable TranscribeStr [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] private partial void LogInvoked(string methodName); - [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: Audio contents: {AudioContents}. Options: {SpeechToTextOptions}. Metadata: {SpeechToTextClientMetadata}.")] + [LoggerMessage(LogLevel.Trace, $"{{MethodName}} invoked: Audio contents: {{AudioContents}}. Options: {{{nameof(AI.SpeechToTextOptions)}}}. Metadata: {{{nameof(AI.SpeechToTextClientMetadata)}}}.")] private partial void LogInvokedSensitive(string methodName, string audioContents, string SpeechToTextOptions, string SpeechToTextClientMetadata); [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] private partial void LogCompleted(string methodName); - [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {SpeechToTextResponse}.")] + [LoggerMessage(LogLevel.Trace, $"{{MethodName}} completed: {{{nameof(AI.SpeechToTextResponse)}}}.")] private partial void LogCompletedSensitive(string methodName, string SpeechToTextResponse); - [LoggerMessage(LogLevel.Debug, "GetStreamingResponseAsync received update.")] + [LoggerMessage(LogLevel.Debug, $"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update.")] private partial void LogStreamingUpdate(); - [LoggerMessage(LogLevel.Trace, "GetStreamingResponseAsync received update: {SpeechToTextResponseUpdate}")] + [LoggerMessage(LogLevel.Trace, $"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update: {{{nameof(SpeechToTextResponseUpdate)}}}")] private partial void LogStreamingUpdateSensitive(string speechToTextResponseUpdate); [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index c1089df4586..2788af86795 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -73,14 +74,14 @@ await client.TranscribeAudioAsync( if (level is LogLevel.Trace) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains("GetResponseAsync invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), - entry => Assert.True(entry.Message.Contains("GetResponseAsync completed:") && entry.Message.Contains("blue whale"))); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} completed:") && entry.Message.Contains("blue whale"))); } else if (level is LogLevel.Debug) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains("GetResponseAsync invoked.") && !entry.Message.Contains("\"speechLanguage\": \"pt\"")), - entry => Assert.True(entry.Message.Contains("GetResponseAsync completed.") && !entry.Message.Contains("blue whale"))); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} invoked.") && !entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} completed.") && !entry.Message.Contains("blue whale"))); } else { @@ -125,18 +126,18 @@ static async IAsyncEnumerable GetUpdatesAsync() if (level is LogLevel.Trace) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update:") && entry.Message.Contains("blue")), - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update:") && entry.Message.Contains("whale")), - entry => Assert.Contains("GetStreamingResponseAsync completed.", entry.Message)); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update:") && entry.Message.Contains("blue")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update:") && entry.Message.Contains("whale")), + entry => Assert.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} completed.", entry.Message)); } else if (level is LogLevel.Debug) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync invoked.") && !entry.Message.Contains("speechLanguage")), - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update.") && !entry.Message.Contains("blue")), - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update.") && !entry.Message.Contains("whale")), - entry => Assert.Contains("GetStreamingResponseAsync completed.", entry.Message)); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} invoked.") && !entry.Message.Contains("speechLanguage")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update.") && !entry.Message.Contains("blue")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update.") && !entry.Message.Contains("whale")), + entry => Assert.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} completed.", entry.Message)); } else { From ef93211442d685431122407e264c6d78113fe100 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 16:54:39 +0000 Subject: [PATCH 05/27] Update method names Transcribe / Response to GetText --- .../DelegatingSpeechToTextClient.cs | 8 ++-- .../SpeechToText/ISpeechToTextClient.cs | 8 ++-- .../SpeechToTextClientExtensions.cs | 16 ++++---- .../OpenAISpeechToTextClient.cs | 6 +-- .../ConfigureOptionsSpeechToTextClient.cs | 8 ++-- .../SpeechToText/LoggingSpeechToTextClient.cs | 38 +++++++++---------- ...=> SpeechToTextClientBuilderExtensions.cs} | 2 +- .../SpeechToTextClientExtensionsTests.cs | 10 ++--- .../SpeechToText/SpeechToTextClientTests.cs | 2 +- .../TestSpeechToTextClient.cs | 4 +- .../SpeechToTextClientIntegrationTests.cs | 8 ++-- .../OpenAISpeechToTextClientTests.cs | 18 ++++----- ...ConfigureOptionsSpeechToTextClientTests.cs | 4 +- .../LoggingSpeechToTextClientTests.cs | 28 +++++++------- 14 files changed, 80 insertions(+), 80 deletions(-) rename src/Libraries/Microsoft.Extensions.AI/SpeechToText/{LoggingSpeechToTextClientExtensions.cs => SpeechToTextClientBuilderExtensions.cs} (97%) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs index 7dce6c96572..d02999f58e4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs @@ -40,17 +40,17 @@ public void Dispose() protected ISpeechToTextClient InnerClient { get; } /// - public virtual Task TranscribeAudioAsync( + public virtual Task GetTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.TranscribeAudioAsync(speechContents, options, cancellationToken); + return InnerClient.GetTextAsync(speechContents, options, cancellationToken); } /// - public virtual IAsyncEnumerable TranscribeStreamingAudioAsync( + public virtual IAsyncEnumerable GetStreamingTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.TranscribeStreamingAudioAsync(speechContents, options, cancellationToken); + return InnerClient.GetStreamingTextAsync(speechContents, options, cancellationToken); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs index 0398c699265..a12c63fd8c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -16,8 +16,8 @@ namespace Microsoft.Extensions.AI; /// It is expected that all implementations of support being used by multiple requests concurrently. /// /// -/// However, implementations of might mutate the arguments supplied to and -/// , such as by configuring the options instance. Thus, consumers of the interface either should avoid +/// However, implementations of might mutate the arguments supplied to and +/// , such as by configuring the options instance. Thus, consumers of the interface either should avoid /// using shared instances of these arguments for concurrent invocations or should otherwise ensure by construction that no /// instances are used which might employ such mutation. For example, the ConfigureOptions method be /// provided with a callback that could mutate the supplied options argument, and that should be avoided if using a singleton options instance. @@ -31,7 +31,7 @@ public interface ISpeechToTextClient : IDisposable /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - Task TranscribeAudioAsync( + Task GetTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); @@ -41,7 +41,7 @@ Task TranscribeAudioAsync( /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. - IAsyncEnumerable TranscribeStreamingAudioAsync( + IAsyncEnumerable GetStreamingTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index d80b30983f0..48db2c53b7d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -36,7 +36,7 @@ public static class SpeechToTextClientExtensions /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static Task GetResponseAsync( + public static Task GetTextAsync( this ISpeechToTextClient client, DataContent speechContent, SpeechToTextOptions? options = null, @@ -44,7 +44,7 @@ public static Task GetResponseAsync( { IEnumerable speechContents = [Throw.IfNull(speechContent)]; return Throw.IfNull(client) - .TranscribeAudioAsync( + .GetTextAsync( [speechContents.ToAsyncEnumerable()], options, cancellationToken); @@ -56,13 +56,13 @@ public static Task GetResponseAsync( /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static Task GetResponseAsync( + public static Task GetTextAsync( this ISpeechToTextClient client, Stream speechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) => Throw.IfNull(client) - .TranscribeAudioAsync( + .GetTextAsync( [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], options, cancellationToken); @@ -73,13 +73,13 @@ public static Task GetResponseAsync( /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static IAsyncEnumerable GetStreamingResponseAsync( + public static IAsyncEnumerable GetStreamingTextAsync( this ISpeechToTextClient client, Stream speechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) => Throw.IfNull(client) - .TranscribeStreamingAudioAsync( + .GetStreamingTextAsync( [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], options, cancellationToken); @@ -90,7 +90,7 @@ public static IAsyncEnumerable GetStreamingResponseA /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static IAsyncEnumerable GetStreamingResponseAsync( + public static IAsyncEnumerable GetStreamingTextAsync( this ISpeechToTextClient client, DataContent speechContent, SpeechToTextOptions? options = null, @@ -98,7 +98,7 @@ public static IAsyncEnumerable GetStreamingResponseA { IEnumerable speechContents = [Throw.IfNull(speechContent)]; return Throw.IfNull(client) - .TranscribeStreamingAudioAsync( + .GetStreamingTextAsync( [speechContents.ToAsyncEnumerable()], options, cancellationToken); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index c9dab980516..7431924dda2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -90,7 +90,7 @@ public OpenAISpeechToTextClient(AudioClient audioClient) } /// - public async IAsyncEnumerable TranscribeStreamingAudioAsync( + public async IAsyncEnumerable GetStreamingTextAsync( IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { _ = Throw.IfNullOrEmpty(speechContents); @@ -100,7 +100,7 @@ public async IAsyncEnumerable TranscribeStreamingAud var speechContent = speechContents[inputIndex]; _ = Throw.IfNull(speechContent); - var speechResponse = await TranscribeAudioAsync([speechContent], options, cancellationToken).ConfigureAwait(false); + var speechResponse = await GetTextAsync([speechContent], options, cancellationToken).ConfigureAwait(false); foreach (var choice in speechResponse.Choices) { @@ -115,7 +115,7 @@ public async IAsyncEnumerable TranscribeStreamingAud } /// - public async Task TranscribeAudioAsync( + public async Task GetTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNullOrEmpty(speechContents); diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs index 8df34ac03ea..04f874bb570 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs @@ -35,17 +35,17 @@ public ConfigureOptionsSpeechToTextClient(ISpeechToTextClient innerClient, Actio } /// - public override Task TranscribeAudioAsync( + public override Task GetTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.TranscribeAudioAsync(speechContents, Configure(options), cancellationToken); + return base.GetTextAsync(speechContents, Configure(options), cancellationToken); } /// - public override IAsyncEnumerable TranscribeStreamingAudioAsync( + public override IAsyncEnumerable GetStreamingTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.TranscribeStreamingAudioAsync(speechContents, Configure(options), cancellationToken); + return base.GetStreamingTextAsync(speechContents, Configure(options), cancellationToken); } /// Creates and configures the to pass along to the inner client. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index d69bc2df74a..4fe85f3a7c9 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -45,34 +45,34 @@ public JsonSerializerOptions JsonSerializerOptions } /// - public override async Task TranscribeAudioAsync( + public override async Task GetTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(TranscribeAudioAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetTextAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); } else { - LogInvoked(nameof(TranscribeAudioAsync)); + LogInvoked(nameof(GetTextAsync)); } } try { - var completion = await base.TranscribeAudioAsync(speechContents, options, cancellationToken).ConfigureAwait(false); + var completion = await base.GetTextAsync(speechContents, options, cancellationToken).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogCompletedSensitive(nameof(TranscribeAudioAsync), AsJson(completion)); + LogCompletedSensitive(nameof(GetTextAsync), AsJson(completion)); } else { - LogCompleted(nameof(TranscribeAudioAsync)); + LogCompleted(nameof(GetTextAsync)); } } @@ -80,45 +80,45 @@ public override async Task TranscribeAudioAsync( } catch (OperationCanceledException) { - LogInvocationCanceled(nameof(TranscribeAudioAsync)); + LogInvocationCanceled(nameof(GetTextAsync)); throw; } catch (Exception ex) { - LogInvocationFailed(nameof(TranscribeAudioAsync), ex); + LogInvocationFailed(nameof(GetTextAsync), ex); throw; } } /// - public override async IAsyncEnumerable TranscribeStreamingAudioAsync( + public override async IAsyncEnumerable GetStreamingTextAsync( IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(TranscribeStreamingAudioAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetStreamingTextAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); } else { - LogInvoked(nameof(TranscribeStreamingAudioAsync)); + LogInvoked(nameof(GetStreamingTextAsync)); } } IAsyncEnumerator e; try { - e = base.TranscribeStreamingAudioAsync(speechContents, options, cancellationToken).GetAsyncEnumerator(cancellationToken); + e = base.GetStreamingTextAsync(speechContents, options, cancellationToken).GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) { - LogInvocationCanceled(nameof(TranscribeStreamingAudioAsync)); + LogInvocationCanceled(nameof(GetStreamingTextAsync)); throw; } catch (Exception ex) { - LogInvocationFailed(nameof(TranscribeStreamingAudioAsync), ex); + LogInvocationFailed(nameof(GetStreamingTextAsync), ex); throw; } @@ -138,12 +138,12 @@ public override async IAsyncEnumerable TranscribeStr } catch (OperationCanceledException) { - LogInvocationCanceled(nameof(TranscribeStreamingAudioAsync)); + LogInvocationCanceled(nameof(GetStreamingTextAsync)); throw; } catch (Exception ex) { - LogInvocationFailed(nameof(TranscribeStreamingAudioAsync), ex); + LogInvocationFailed(nameof(GetStreamingTextAsync), ex); throw; } @@ -162,7 +162,7 @@ public override async IAsyncEnumerable TranscribeStr yield return update; } - LogCompleted(nameof(TranscribeStreamingAudioAsync)); + LogCompleted(nameof(GetStreamingTextAsync)); } finally { @@ -184,10 +184,10 @@ public override async IAsyncEnumerable TranscribeStr [LoggerMessage(LogLevel.Trace, $"{{MethodName}} completed: {{{nameof(AI.SpeechToTextResponse)}}}.")] private partial void LogCompletedSensitive(string methodName, string SpeechToTextResponse); - [LoggerMessage(LogLevel.Debug, $"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update.")] + [LoggerMessage(LogLevel.Debug, $"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update.")] private partial void LogStreamingUpdate(); - [LoggerMessage(LogLevel.Trace, $"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update: {{{nameof(SpeechToTextResponseUpdate)}}}")] + [LoggerMessage(LogLevel.Trace, $"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update: {{{nameof(SpeechToTextResponseUpdate)}}}")] private partial void LogStreamingUpdateSensitive(string speechToTextResponseUpdate); [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderExtensions.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs rename to src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderExtensions.cs index ccbe4c28b34..7ce2b19ac37 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.AI; /// Provides extensions for configuring instances. [Experimental("MEAI001")] -public static class LoggingSpeechToTextClientExtensions +public static class SpeechToTextClientBuilderExtensions { /// Adds logging to the audio transcription client pipeline. /// The . diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs index 699b64e8bd4..10e7e6f0856 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs @@ -27,12 +27,12 @@ public void GetResponseAsync_InvalidArgs_Throws() // Note: the extension method now requires a DataContent (not a string). Assert.Throws("client", () => { - _ = SpeechToTextClientExtensions.GetResponseAsync(null!, new DataContent("data:audio/wav;base64,AQIDBA==")); + _ = SpeechToTextClientExtensions.GetTextAsync(null!, new DataContent("data:audio/wav;base64,AQIDBA==")); }); Assert.Throws("speechContent", () => { - _ = SpeechToTextClientExtensions.GetResponseAsync(new TestSpeechToTextClient(), (DataContent)null!); + _ = SpeechToTextClientExtensions.GetTextAsync(new TestSpeechToTextClient(), (DataContent)null!); }); } @@ -42,12 +42,12 @@ public void GetStreamingResponseAsync_InvalidArgs_Throws() Assert.Throws("client", () => { using var stream = new MemoryStream(); - _ = SpeechToTextClientExtensions.GetStreamingResponseAsync(client: null!, new DataContent("data:audio/wav;base64,AQIDBA==")); + _ = SpeechToTextClientExtensions.GetStreamingTextAsync(client: null!, new DataContent("data:audio/wav;base64,AQIDBA==")); }); Assert.Throws("speechContent", () => { - _ = SpeechToTextClientExtensions.GetStreamingResponseAsync(new TestSpeechToTextClient(), speechContent: null!); + _ = SpeechToTextClientExtensions.GetStreamingTextAsync(new TestSpeechToTextClient(), speechContent: null!); }); } @@ -70,7 +70,7 @@ public async Task GetStreamingResponseAsync_CreatesTextMessageAsync() }; int count = 0; - await foreach (var update in SpeechToTextClientExtensions.GetStreamingResponseAsync( + await foreach (var update in SpeechToTextClientExtensions.GetStreamingTextAsync( client, new DataContent("data:audio/wav;base64,AQIDBA=="), expectedOptions, diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs index 40a9ac21eb9..392d1c46722 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -32,7 +32,7 @@ public async Task GetResponseAsync_CreatesTextMessageAsync() }; // Act – call the extension method with a valid DataContent. - SpeechToTextResponse response = await SpeechToTextClientExtensions.GetResponseAsync( + SpeechToTextResponse response = await SpeechToTextClientExtensions.GetTextAsync( client, new DataContent("data:audio/wav;base64,AQIDBA=="), expectedOptions, diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs index c67b3c094c0..9e0b04cd442 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs @@ -38,13 +38,13 @@ public Func serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; - public Task TranscribeAudioAsync( + public Task GetTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) => GetResponseAsyncCallback!.Invoke(speechContents, options, cancellationToken); - public IAsyncEnumerable TranscribeStreamingAudioAsync( + public IAsyncEnumerable GetStreamingTextAsync( IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index a333b9b9235..d7deed443bd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -37,7 +37,7 @@ public virtual async Task GetResponseAsync_SingleAudioRequestMessage() SkipIfNotEnabled(); using var audioStream = GetAudioStream("audio001.wav"); - var response = await _client.TranscribeAudioAsync([audioStream.ToAsyncEnumerable()]); + var response = await _client.GetTextAsync([audioStream.ToAsyncEnumerable()]); Assert.Contains("gym", response.Message.Text, StringComparison.OrdinalIgnoreCase); } @@ -50,7 +50,7 @@ public virtual async Task GetResponseAsync_MultipleAudioRequestMessage() using var firstAudioStream = GetAudioStream("audio001.wav"); using var secondAudioStream = GetAudioStream("audio002.wav"); - var response = await _client.TranscribeAudioAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()]); + var response = await _client.GetTextAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()]); var firstFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 0)); var secondFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 1)); @@ -67,7 +67,7 @@ public virtual async Task GetStreamingResponseAsync_SingleStreamingResponseChoic using var audioStream = GetAudioStream("audio001.wav"); StringBuilder sb = new(); - await foreach (var chunk in _client.TranscribeStreamingAudioAsync([audioStream.ToAsyncEnumerable()])) + await foreach (var chunk in _client.GetStreamingTextAsync([audioStream.ToAsyncEnumerable()])) { sb.Append(chunk.Text); } @@ -87,7 +87,7 @@ public virtual async Task GetStreamingResponseAsync_MultipleStreamingResponseCho StringBuilder firstSb = new(); StringBuilder secondSb = new(); - await foreach (var chunk in _client.TranscribeStreamingAudioAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()])) + await foreach (var chunk in _client.GetStreamingTextAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()])) { if (chunk.InputIndex == 0) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 41b1bfbd284..e85ee58a8ce 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -132,7 +132,7 @@ public async Task BasicTranscribeRequestResponse_NonStreaming(string? speechLang using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); using var fileStream = GetAudioStream("audio001.wav"); - var response = await client.GetResponseAsync(fileStream, new SpeechToTextOptions + var response = await client.GetTextAsync(fileStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -158,7 +158,7 @@ public async Task CancelledBasicTranscribeRequestResponse_NonStreaming_Throw() cancellationTokenSource.Cancel(); await Assert.ThrowsAsync(() - => client.GetResponseAsync(fileStream, cancellationToken: cancellationTokenSource.Token)); + => client.GetTextAsync(fileStream, cancellationToken: cancellationTokenSource.Token)); } [Fact] @@ -173,7 +173,7 @@ public async Task CancelledBasicTranscribeRequestResponse_Streaming_Throw() await Assert.ThrowsAsync(() => client - .GetStreamingResponseAsync(fileStream, cancellationToken: cancellationTokenSource.Token) + .GetStreamingTextAsync(fileStream, cancellationToken: cancellationTokenSource.Token) .GetAsyncEnumerator() .MoveNextAsync() .AsTask()); @@ -207,7 +207,7 @@ public async Task BasicTranscribeRequestResponse_Streaming(string? speechLanguag using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); using var fileStream = GetAudioStream("audio001.mp3"); - await foreach (var update in client.GetStreamingResponseAsync(fileStream, new SpeechToTextOptions + await foreach (var update in client.GetStreamingTextAsync(fileStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -230,7 +230,7 @@ public async Task NonSupportedTranslation_Streaming_Throws(string? speechLanguag using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); using var fileStream = GetAudioStream("audio001.mp3"); - var asyncEnumerator = client.GetStreamingResponseAsync(fileStream, new SpeechToTextOptions + var asyncEnumerator = client.GetStreamingTextAsync(fileStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -250,7 +250,7 @@ public async Task NonSupportedTranslation_NonStreaming_Throws(string? speechLang using var fileStream = GetAudioStream("audio001.mp3"); - await Assert.ThrowsAsync(() => client.GetResponseAsync(fileStream, new SpeechToTextOptions + await Assert.ThrowsAsync(() => client.GetTextAsync(fileStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -280,7 +280,7 @@ public async Task BasicTranslateRequestResponse_Streaming() using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); using var fileStream = GetAudioStream("audio001.mp3"); - await foreach (var update in client.GetStreamingResponseAsync(fileStream, new SpeechToTextOptions + await foreach (var update in client.GetStreamingTextAsync(fileStream, new SpeechToTextOptions { SpeechLanguage = "pt", TextLanguage = textLanguage @@ -317,7 +317,7 @@ public async Task NonStronglyTypedOptions_AllSent() using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); using var fileStream = GetAudioStream("audio001.mp3"); - Assert.NotNull(await client.GetResponseAsync(fileStream, new() + Assert.NotNull(await client.GetTextAsync(fileStream, new() { AdditionalProperties = new() { @@ -351,7 +351,7 @@ public async Task StronglyTypedOptions_AllSent() using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); using var fileStream = GetAudioStream("audio001.mp3"); - Assert.NotNull(await client.GetResponseAsync(fileStream, new() + Assert.NotNull(await client.GetTextAsync(fileStream, new() { SpeechLanguage = "pt", })); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs index a671fce2bc4..f27b4ca7be3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs @@ -73,11 +73,11 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP }) .Build(); - var response = await client.TranscribeAudioAsync([], providedOptions, cts.Token); + var response = await client.GetTextAsync([], providedOptions, cts.Token); Assert.Same(expectedResponse, response); int i = 0; - await using var e = client.TranscribeStreamingAudioAsync([], providedOptions, cts.Token).GetAsyncEnumerator(); + await using var e = client.GetStreamingTextAsync([], providedOptions, cts.Token).GetAsyncEnumerator(); while (i < expectedUpdates.Length) { Assert.True(await e.MoveNextAsync()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index 2788af86795..3e28dbf6302 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -66,7 +66,7 @@ public async Task GetResponseAsync_LogsResponseInvocationAndCompletion(LogLevel .UseLogging() .Build(services); - await client.TranscribeAudioAsync( + await client.GetTextAsync( [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], new SpeechToTextOptions { SpeechLanguage = "pt" }); @@ -74,14 +74,14 @@ await client.TranscribeAudioAsync( if (level is LogLevel.Trace) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} completed:") && entry.Message.Contains("blue whale"))); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetTextAsync)} invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetTextAsync)} completed:") && entry.Message.Contains("blue whale"))); } else if (level is LogLevel.Debug) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} invoked.") && !entry.Message.Contains("\"speechLanguage\": \"pt\"")), - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeAudioAsync)} completed.") && !entry.Message.Contains("blue whale"))); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetTextAsync)} invoked.") && !entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetTextAsync)} completed.") && !entry.Message.Contains("blue whale"))); } else { @@ -115,7 +115,7 @@ static async IAsyncEnumerable GetUpdatesAsync() .UseLogging(loggerFactory) .Build(); - await foreach (var update in client.TranscribeStreamingAudioAsync( + await foreach (var update in client.GetStreamingTextAsync( [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], new SpeechToTextOptions { SpeechLanguage = "pt" })) { @@ -126,18 +126,18 @@ static async IAsyncEnumerable GetUpdatesAsync() if (level is LogLevel.Trace) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update:") && entry.Message.Contains("blue")), - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update:") && entry.Message.Contains("whale")), - entry => Assert.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} completed.", entry.Message)); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} invoked:") && entry.Message.Contains("\"speechLanguage\": \"pt\"")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update:") && entry.Message.Contains("blue")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update:") && entry.Message.Contains("whale")), + entry => Assert.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} completed.", entry.Message)); } else if (level is LogLevel.Debug) { Assert.Collection(logs, - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} invoked.") && !entry.Message.Contains("speechLanguage")), - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update.") && !entry.Message.Contains("blue")), - entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} received update.") && !entry.Message.Contains("whale")), - entry => Assert.Contains($"{nameof(ISpeechToTextClient.TranscribeStreamingAudioAsync)} completed.", entry.Message)); + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} invoked.") && !entry.Message.Contains("speechLanguage")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update.") && !entry.Message.Contains("blue")), + entry => Assert.True(entry.Message.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update.") && !entry.Message.Contains("whale")), + entry => Assert.Contains($"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} completed.", entry.Message)); } else { From 43d610c6564e77c5ba9c2b54056e5b06806069e6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:02:47 +0000 Subject: [PATCH 06/27] Update Test Names to new Method names --- .../SpeechToText/ISpeechToTextClient.cs | 8 ++++---- .../SpeechToTextClientExtensionsTests.cs | 6 +++--- .../SpeechToText/SpeechToTextClientTests.cs | 2 +- .../SpeechToTextClientIntegrationTests.cs | 8 ++++---- .../OpenAISpeechToTextClientTests.cs | 18 +++++++++--------- .../LoggingSpeechToTextClientTests.cs | 4 ++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs index a12c63fd8c8..96c5c794d71 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -26,8 +26,8 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public interface ISpeechToTextClient : IDisposable { - /// Sends speech speech audio contents to the model and returns the generated text. - /// The list of speech speech audio contents to send. + /// Sends speech audio contents to the model and returns the generated text. + /// The list of speech audio contents to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. @@ -36,8 +36,8 @@ Task GetTextAsync( SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); - /// Sends speech speech audio contents to the model and streams back the generated text. - /// The list of speech speech audio contents to send. + /// Sends speech audio contents to the model and streams back the generated text. + /// The list of speech audio contents to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs index 10e7e6f0856..88d0836d58b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs @@ -22,7 +22,7 @@ public void GetService_InvalidArgs_Throws() } [Fact] - public void GetResponseAsync_InvalidArgs_Throws() + public void GetTextAsync_InvalidArgs_Throws() { // Note: the extension method now requires a DataContent (not a string). Assert.Throws("client", () => @@ -37,7 +37,7 @@ public void GetResponseAsync_InvalidArgs_Throws() } [Fact] - public void GetStreamingResponseAsync_InvalidArgs_Throws() + public void GetStreamingTextAsync_InvalidArgs_Throws() { Assert.Throws("client", () => { @@ -52,7 +52,7 @@ public void GetStreamingResponseAsync_InvalidArgs_Throws() } [Fact] - public async Task GetStreamingResponseAsync_CreatesTextMessageAsync() + public async Task GetStreamingTextAsync_CreatesTextMessageAsync() { // Arrange var expectedOptions = new SpeechToTextOptions(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs index 392d1c46722..afd58865bb7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI; public class SpeechToTextClientTests { [Fact] - public async Task GetResponseAsync_CreatesTextMessageAsync() + public async Task GetTextAsync_CreatesTextMessageAsync() { // Arrange var expectedResponse = new SpeechToTextResponse(new SpeechToTextMessage("hello")); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index d7deed443bd..992440240cd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -32,7 +32,7 @@ public void Dispose() protected abstract ISpeechToTextClient? CreateClient(); [ConditionalFact] - public virtual async Task GetResponseAsync_SingleAudioRequestMessage() + public virtual async Task GetTextAsync_SingleAudioRequestMessage() { SkipIfNotEnabled(); @@ -43,7 +43,7 @@ public virtual async Task GetResponseAsync_SingleAudioRequestMessage() } [ConditionalFact] - public virtual async Task GetResponseAsync_MultipleAudioRequestMessage() + public virtual async Task GetTextAsync_MultipleAudioRequestMessage() { SkipIfNotEnabled(); @@ -60,7 +60,7 @@ public virtual async Task GetResponseAsync_MultipleAudioRequestMessage() } [ConditionalFact] - public virtual async Task GetStreamingResponseAsync_SingleStreamingResponseChoice() + public virtual async Task GetStreamingTextAsync_SingleStreamingResponseChoice() { SkipIfNotEnabled(); @@ -78,7 +78,7 @@ public virtual async Task GetStreamingResponseAsync_SingleStreamingResponseChoic } [ConditionalFact] - public virtual async Task GetStreamingResponseAsync_MultipleStreamingResponseChoice() + public virtual async Task GetStreamingTextAsync_MultipleStreamingResponseChoice() { SkipIfNotEnabled(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index e85ee58a8ce..c6644036b20 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -112,7 +112,7 @@ public void GetService_AudioClient_SuccessfullyReturnsUnderlyingClient() [InlineData("en", null)] [InlineData("en", "en")] [InlineData("pt", "pt")] - public async Task BasicTranscribeRequestResponse_NonStreaming(string? speechLanguage, string? textLanguage) + public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, string? textLanguage) { string input = $$""" { @@ -148,7 +148,7 @@ public async Task BasicTranscribeRequestResponse_NonStreaming(string? speechLang } [Fact] - public async Task CancelledBasicTranscribeRequestResponse_NonStreaming_Throw() + public async Task GetTextAsync_Cancelled_Throws() { using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); @@ -162,7 +162,7 @@ await Assert.ThrowsAsync(() } [Fact] - public async Task CancelledBasicTranscribeRequestResponse_Streaming_Throw() + public async Task GetStreamingTextAsync_Cancelled_Throws() { using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); @@ -184,7 +184,7 @@ await Assert.ThrowsAsync(() [InlineData("en", null)] [InlineData("en", "en")] [InlineData("pt", "pt")] - public async Task BasicTranscribeRequestResponse_Streaming(string? speechLanguage, string? textLanguage) + public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLanguage, string? textLanguage) { // There's no support for streaming audio in the OpenAI API, // so we're just testing the client's ability to handle streaming responses. @@ -224,7 +224,7 @@ public async Task BasicTranscribeRequestResponse_Streaming(string? speechLanguag [InlineData(null, "pt")] [InlineData(null, "it")] [InlineData("en", "pt")] - public async Task NonSupportedTranslation_Streaming_Throws(string? speechLanguage, string? textLanguage) + public async Task GetStreamingTextAsync_NonSupportedTranslation_Throws(string? speechLanguage, string? textLanguage) { using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); @@ -243,7 +243,7 @@ public async Task NonSupportedTranslation_Streaming_Throws(string? speechLanguag [InlineData(null, "pt")] [InlineData(null, "it")] [InlineData("en", "pt")] - public async Task NonSupportedTranslation_NonStreaming_Throws(string? speechLanguage, string? textLanguage) + public async Task GetTextAsync_NonSupportedTranslation_Throws(string? speechLanguage, string? textLanguage) { using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); @@ -258,7 +258,7 @@ public async Task NonSupportedTranslation_NonStreaming_Throws(string? speechLang } [Fact] - public async Task BasicTranslateRequestResponse_Streaming() + public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() { string textLanguage = "en"; @@ -294,7 +294,7 @@ public async Task BasicTranslateRequestResponse_Streaming() } [Fact] - public async Task NonStronglyTypedOptions_AllSent() + public async Task GetTextAsync_NonStronglyTypedOptions_AllSent() { const string Input = """ { @@ -331,7 +331,7 @@ public async Task NonStronglyTypedOptions_AllSent() } [Fact] - public async Task StronglyTypedOptions_AllSent() + public async Task GetTextAsync_StronglyTypedOptions_AllSent() { const string Input = """ { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index 3e28dbf6302..c323e44094a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -45,7 +45,7 @@ public void UseLogging_AvoidsInjectingNopClient() [InlineData(LogLevel.Trace)] [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Information)] - public async Task GetResponseAsync_LogsResponseInvocationAndCompletion(LogLevel level) + public async Task GetTextAsync_LogsResponseInvocationAndCompletion(LogLevel level) { var collector = new FakeLogCollector(); @@ -93,7 +93,7 @@ await client.GetTextAsync( [InlineData(LogLevel.Trace)] [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Information)] - public async Task GetResponseStreamingStreamAsync_LogsUpdateReceived(LogLevel level) + public async Task GetStreamingTextAsync_LogsUpdateReceived(LogLevel level) { var collector = new FakeLogCollector(); using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); From ff4ae4a33b6c82a43db61e729d264c2b15eab021 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 18:24:36 +0000 Subject: [PATCH 07/27] Change interface from IList to one stream item at a time --- .../DelegatingSpeechToTextClient.cs | 9 +- .../SpeechToText/ISpeechToTextClient.cs | 14 +- .../SpeechToTextClientExtensions.cs | 74 ++-------- .../OpenAISpeechToTextClient.cs | 137 +++++++----------- .../ConfigureOptionsSpeechToTextClient.cs | 9 +- .../SpeechToText/LoggingSpeechToTextClient.cs | 13 +- .../DelegatingSpeechToTextClientTests.cs | 21 +-- .../SpeechToTextClientExtensionsTests.cs | 4 +- .../SpeechToText/SpeechToTextClientTests.cs | 8 +- .../TestSpeechToTextClient.cs | 14 +- .../SpeechToTextClientIntegrationTests.cs | 53 +------ ...ConfigureOptionsSpeechToTextClientTests.cs | 11 +- .../LoggingSpeechToTextClientTests.cs | 20 +-- 13 files changed, 128 insertions(+), 259 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs index d02999f58e4..49f95b05847 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -41,16 +42,16 @@ public void Dispose() /// public virtual Task GetTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.GetTextAsync(speechContents, options, cancellationToken); + return InnerClient.GetTextAsync(audioStream, options, cancellationToken); } /// public virtual IAsyncEnumerable GetStreamingTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.GetStreamingTextAsync(speechContents, options, cancellationToken); + return InnerClient.GetStreamingTextAsync(audioStream, options, cancellationToken); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs index 96c5c794d71..36c01afad09 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -21,28 +22,29 @@ namespace Microsoft.Extensions.AI; /// using shared instances of these arguments for concurrent invocations or should otherwise ensure by construction that no /// instances are used which might employ such mutation. For example, the ConfigureOptions method be /// provided with a callback that could mutate the supplied options argument, and that should be avoided if using a singleton options instance. +/// The audio stream passed to these methods will not be closed or disposed by the implementation. /// /// [Experimental("MEAI001")] public interface ISpeechToTextClient : IDisposable { - /// Sends speech audio contents to the model and returns the generated text. - /// The list of speech audio contents to send. + /// Sends speech audio content to the model and returns the generated text. + /// The audio stream to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. Task GetTextAsync( - IList> speechContents, + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); - /// Sends speech audio contents to the model and streams back the generated text. - /// The list of speech audio contents to send. + /// Sends speech audio content to the model and streams back the generated text. + /// The audio stream to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. IAsyncEnumerable GetStreamingTextAsync( - IList> speechContents, + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index 48db2c53b7d..3ac86d5e11f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -36,53 +37,18 @@ public static class SpeechToTextClientExtensions /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static Task GetTextAsync( + public static async Task GetTextAsync( this ISpeechToTextClient client, DataContent speechContent, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - IEnumerable speechContents = [Throw.IfNull(speechContent)]; - return Throw.IfNull(client) - .GetTextAsync( - [speechContents.ToAsyncEnumerable()], - options, - cancellationToken); - } - - /// Generates text from speech providing a single speech audio . - /// The client. - /// The single speech audio stream. - /// The speech to text options to configure the request. - /// The to monitor for cancellation requests. The default is . - /// The text generated by the client. - public static Task GetTextAsync( - this ISpeechToTextClient client, - Stream speechStream, - SpeechToTextOptions? options = null, - CancellationToken cancellationToken = default) - => Throw.IfNull(client) - .GetTextAsync( - [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], - options, - cancellationToken); + _ = Throw.IfNull(speechContent); + _ = Throw.IfNull(client); - /// Generates text from speech providing a single speech audio . - /// The client. - /// The single speech audio stream. - /// The speech to text options to configure the request. - /// The to monitor for cancellation requests. The default is . - /// The text generated by the client. - public static IAsyncEnumerable GetStreamingTextAsync( - this ISpeechToTextClient client, - Stream speechStream, - SpeechToTextOptions? options = null, - CancellationToken cancellationToken = default) - => Throw.IfNull(client) - .GetStreamingTextAsync( - [speechStream.ToAsyncEnumerable(cancellationToken: cancellationToken)], - options, - cancellationToken); + using var memoryStream = new MemoryStream(speechContent.Data.ToArray()); + return await client.GetTextAsync(memoryStream, options, cancellationToken).ConfigureAwait(false); + } /// Generates text from speech providing a single speech audio . /// The client. @@ -90,29 +56,19 @@ public static IAsyncEnumerable GetStreamingTextAsync /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static IAsyncEnumerable GetStreamingTextAsync( + public static async IAsyncEnumerable GetStreamingTextAsync( this ISpeechToTextClient client, DataContent speechContent, SpeechToTextOptions? options = null, - CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - IEnumerable speechContents = [Throw.IfNull(speechContent)]; - return Throw.IfNull(client) - .GetStreamingTextAsync( - [speechContents.ToAsyncEnumerable()], - options, - cancellationToken); - } + _ = Throw.IfNull(speechContent); + _ = Throw.IfNull(client); -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - private static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) - { - foreach (var item in source) + using var memoryStream = new MemoryStream(speechContent.Data.ToArray()); + await foreach (var update in client.GetStreamingTextAsync(memoryStream, options, cancellationToken).ConfigureAwait(false)) { - yield return item; + yield return update; } } -#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods -#pragma warning restore CS1998 // Unused private types or members should be removed } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 7431924dda2..d4680cce5ed 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; @@ -91,34 +92,28 @@ public OpenAISpeechToTextClient(AudioClient audioClient) /// public async IAsyncEnumerable GetStreamingTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - _ = Throw.IfNullOrEmpty(speechContents); + _ = Throw.IfNull(audioStream); - for (var inputIndex = 0; inputIndex < speechContents.Count; inputIndex++) - { - var speechContent = speechContents[inputIndex]; - _ = Throw.IfNull(speechContent); - - var speechResponse = await GetTextAsync([speechContent], options, cancellationToken).ConfigureAwait(false); + var speechResponse = await GetTextAsync(audioStream, options, cancellationToken).ConfigureAwait(false); - foreach (var choice in speechResponse.Choices) + foreach (var choice in speechResponse.Choices) + { + yield return new SpeechToTextResponseUpdate(choice.Contents) { - yield return new SpeechToTextResponseUpdate(choice.Contents) - { - InputIndex = inputIndex, - Kind = SpeechToTextResponseUpdateKind.TextUpdated, - RawRepresentation = choice.RawRepresentation - }; - } + InputIndex = 0, + Kind = SpeechToTextResponseUpdateKind.TextUpdated, + RawRepresentation = choice.RawRepresentation + }; } } /// public async Task GetTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNullOrEmpty(speechContents); + _ = Throw.IfNull(audioStream); List choices = []; @@ -127,44 +122,55 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) => options is not null && options.TextLanguage is not null && (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage); - for (var inputIndex = 0; inputIndex < speechContents.Count; inputIndex++) + if (IsTranslationRequest(options)) { - var speechContent = speechContents[inputIndex]; - _ = Throw.IfNull(speechContent); + _ = Throw.IfNull(options); - var enumerator = speechContent.GetAsyncEnumerator(cancellationToken); - if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) + // Translation request will be triggered whenever the source language is not specified and a target text language is and different from the output text language + if (CultureInfo.GetCultureInfo(options.TextLanguage!).TwoLetterISOLanguageName != "en") { - throw new InvalidOperationException($"The audio content provided in the index: {inputIndex} is empty."); + throw new NotSupportedException($"Only translation to english is supported."); } - var firstChunk = enumerator.Current; + var openAIOptions = ToOpenAITranslationOptions(options); + AudioTranslation translationResult; - if (IsTranslationRequest(options)) +#if NET + await using (audioStream.ConfigureAwait(false)) +#else + using (audioStream) +#endif { - _ = Throw.IfNull(options); + translationResult = (await _audioClient.TranslateAudioAsync( + audioStream, + "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. + openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + } - // Translation request will be triggered whenever the source language is not specified and a target text language is and different from the output text language - if (CultureInfo.GetCultureInfo(options.TextLanguage!).TwoLetterISOLanguageName != "en") - { - throw new NotSupportedException($"Only translation to english is supported."); - } + var choice = FromOpenAIAudioTranslation(translationResult, 0); + choices.Add(choice); + } + else + { + var openAIOptions = ToOpenAITranscriptionOptions(options); - AudioTranslation translationResult = await GetTranslationResultAsync(options, speechContent, firstChunk, cancellationToken).ConfigureAwait(false); + // Transcription request + AudioTranscription transcriptionResult; - var choice = FromOpenAIAudioTranslation(translationResult, inputIndex); - choices.Add(choice); - } - else +#if NET + await using (audioStream.ConfigureAwait(false)) +#else + using (audioStream) +#endif { - var openAIOptions = ToOpenAITranscriptionOptions(options); - - // Transcription request - AudioTranscription transcriptionResult = await GetTranscriptionResultAsync(speechContent, firstChunk, openAIOptions, cancellationToken).ConfigureAwait(false); - - var choice = FromOpenAIAudioTranscription(transcriptionResult, inputIndex); - choices.Add(choice); + transcriptionResult = (await _audioClient.TranscribeAudioAsync( + audioStream, + "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. + openAIOptions, cancellationToken).ConfigureAwait(false)).Value; } + + var choice = FromOpenAIAudioTranscription(transcriptionResult, 0); + choices.Add(choice); } return new SpeechToTextResponse(choices); @@ -309,48 +315,5 @@ private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOp return result; } - - private async Task GetTranscriptionResultAsync( - IAsyncEnumerable speechContent, DataContent firstChunk, AudioTranscriptionOptions openAIOptions, CancellationToken cancellationToken) - { - AudioTranscription transcriptionResult; - - var audioFileStream = speechContent.ToStream(firstChunk, cancellationToken); -#if NET - await using (audioFileStream.ConfigureAwait(false)) -#else - using (audioFileStream) -#endif - { - transcriptionResult = (await _audioClient.TranscribeAudioAsync( - audioFileStream, - "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. - openAIOptions, cancellationToken).ConfigureAwait(false)).Value; - } - - return transcriptionResult; - } - - private async Task GetTranslationResultAsync( - SpeechToTextOptions? options, IAsyncEnumerable speechContent, DataContent firstChunk, CancellationToken cancellationToken) - { - var openAIOptions = ToOpenAITranslationOptions(options); - AudioTranslation translationResult; - - var audioFileStream = speechContent.ToStream(firstChunk, cancellationToken); -#if NET - await using (audioFileStream.ConfigureAwait(false)) -#else - using (audioFileStream) -#endif - { - translationResult = (await _audioClient.TranslateAudioAsync( - audioFileStream, - "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. - openAIOptions, cancellationToken).ConfigureAwait(false)).Value; - } - - return translationResult; - } } diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs index 04f874bb570..954413cce4a 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -36,16 +37,16 @@ public ConfigureOptionsSpeechToTextClient(ISpeechToTextClient innerClient, Actio /// public override Task GetTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetTextAsync(speechContents, Configure(options), cancellationToken); + return base.GetTextAsync(audioStream, Configure(options), cancellationToken); } /// public override IAsyncEnumerable GetStreamingTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetStreamingTextAsync(speechContents, Configure(options), cancellationToken); + return base.GetStreamingTextAsync(audioStream, Configure(options), cancellationToken); } /// Creates and configures the to pass along to the inner client. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index 4fe85f3a7c9..86d0ea84fbd 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; @@ -46,13 +47,13 @@ public JsonSerializerOptions JsonSerializerOptions /// public override async Task GetTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetTextAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetTextAsync), "[audio stream]", AsJson(options), AsJson(this.GetService())); } else { @@ -62,7 +63,7 @@ public override async Task GetTextAsync( try { - var completion = await base.GetTextAsync(speechContents, options, cancellationToken).ConfigureAwait(false); + var completion = await base.GetTextAsync(audioStream, options, cancellationToken).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -92,13 +93,13 @@ public override async Task GetTextAsync( /// public override async IAsyncEnumerable GetStreamingTextAsync( - IList> speechContents, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + Stream audioStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetStreamingTextAsync), AsJson(speechContents), AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetStreamingTextAsync), "[audio stream]", AsJson(options), AsJson(this.GetService())); } else { @@ -109,7 +110,7 @@ public override async IAsyncEnumerable GetStreamingT IAsyncEnumerator e; try { - e = base.GetStreamingTextAsync(speechContents, options, cancellationToken).GetAsyncEnumerator(cancellationToken); + e = base.GetStreamingTextAsync(audioStream, options, cancellationToken).GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs index 9da2de9777b..89e13048136 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -18,19 +19,19 @@ public void RequiresInnerSpeechToTextClient() } [Fact] - public async Task GetResponseAsyncDefaultsToInnerClientAsync() + public async Task GetTextAsyncDefaultsToInnerClientAsync() { // Arrange - var expectedContents = new List>(); + using var expectedStream = new MemoryStream(); var expectedOptions = new SpeechToTextOptions(); var expectedCancellationToken = CancellationToken.None; var expectedResult = new TaskCompletionSource(); var expectedResponse = new SpeechToTextResponse([]); using var inner = new TestSpeechToTextClient { - GetResponseAsyncCallback = (speechContents, options, cancellationToken) => + GetResponseAsyncCallback = (audioStream, options, cancellationToken) => { - Assert.Same(expectedContents, speechContents); + Assert.Same(expectedStream, audioStream); Assert.Same(expectedOptions, options); Assert.Equal(expectedCancellationToken, cancellationToken); return expectedResult.Task; @@ -40,7 +41,7 @@ public async Task GetResponseAsyncDefaultsToInnerClientAsync() using var delegating = new NoOpDelegatingSpeechToTextClient(inner); // Act - var resultTask = delegating.TranscribeAudioAsync(expectedContents, expectedOptions, expectedCancellationToken); + var resultTask = delegating.GetTextAsync(expectedStream, expectedOptions, expectedCancellationToken); // Assert Assert.False(resultTask.IsCompleted); @@ -50,10 +51,10 @@ public async Task GetResponseAsyncDefaultsToInnerClientAsync() } [Fact] - public async Task GetStreamingAsyncDefaultsToInnerClientAsync() + public async Task GetStreamingTextAsyncDefaultsToInnerClientAsync() { // Arrange - var expectedContents = new List>(); + using var expectedStream = new MemoryStream(); var expectedOptions = new SpeechToTextOptions(); var expectedCancellationToken = CancellationToken.None; SpeechToTextResponseUpdate[] expectedResults = @@ -64,9 +65,9 @@ public async Task GetStreamingAsyncDefaultsToInnerClientAsync() using var inner = new TestSpeechToTextClient { - GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => + GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => { - Assert.Same(expectedContents, speechContents); + Assert.Same(expectedStream, audioStream); Assert.Same(expectedOptions, options); Assert.Equal(expectedCancellationToken, cancellationToken); return YieldAsync(expectedResults); @@ -76,7 +77,7 @@ public async Task GetStreamingAsyncDefaultsToInnerClientAsync() using var delegating = new NoOpDelegatingSpeechToTextClient(inner); // Act - var resultAsyncEnumerable = delegating.TranscribeStreamingAudioAsync(expectedContents, expectedOptions, expectedCancellationToken); + var resultAsyncEnumerable = delegating.GetStreamingTextAsync(expectedStream, expectedOptions, expectedCancellationToken); // Assert var enumerator = resultAsyncEnumerable.GetAsyncEnumerator(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs index 88d0836d58b..fea28e84051 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs @@ -60,10 +60,8 @@ public async Task GetStreamingTextAsync_CreatesTextMessageAsync() using TestSpeechToTextClient client = new() { - GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => + GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => { - Assert.Single(speechContents); - // For testing, return an async enumerable yielding one streaming update with text "world". return YieldAsync(new SpeechToTextResponseUpdate { Text = "world" }); }, diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs index afd58865bb7..ac3d74f88af 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -19,12 +20,9 @@ public async Task GetTextAsync_CreatesTextMessageAsync() using TestSpeechToTextClient client = new() { - GetResponseAsyncCallback = (speechContents, options, cancellationToken) => + GetResponseAsyncCallback = (audioStream, options, cancellationToken) => { - // In our simulated client, we expect a single async enumerable. - Assert.Single(speechContents); - - // For the purpose of the test, we assume that the underlying implementation converts the DataContent into a transcription choice. + // For the purpose of the test, we assume that the underlying implementation converts the audio stream into a transcription choice. // (In a real implementation, the speech audio data would be processed.) SpeechToTextMessage choice = new("hello"); return Task.FromResult(new SpeechToTextResponse(choice)); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs index 9e0b04cd442..9401319f141 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -18,15 +19,14 @@ public TestSpeechToTextClient() public IServiceProvider? Services { get; set; } // Callbacks for asynchronous operations. - public Func>, + public Func>? GetResponseAsyncCallback { get; set; } - public Func>, + public Func>? @@ -39,16 +39,16 @@ public Func serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; public Task GetTextAsync( - IList> speechContents, + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) - => GetResponseAsyncCallback!.Invoke(speechContents, options, cancellationToken); + => GetResponseAsyncCallback!.Invoke(audioStream, options, cancellationToken); public IAsyncEnumerable GetStreamingTextAsync( - IList> speechContents, + Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) - => GetStreamingResponseAsyncCallback!.Invoke(speechContents, options, cancellationToken); + => GetStreamingResponseAsyncCallback!.Invoke(audioStream, options, cancellationToken); public object? GetService(Type serviceType, object? serviceKey = null) => GetServiceCallback!.Invoke(serviceType, serviceKey); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index 992440240cd..93b46046580 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.TestUtilities; @@ -37,28 +36,11 @@ public virtual async Task GetTextAsync_SingleAudioRequestMessage() SkipIfNotEnabled(); using var audioStream = GetAudioStream("audio001.wav"); - var response = await _client.GetTextAsync([audioStream.ToAsyncEnumerable()]); + var response = await _client.GetTextAsync(audioStream); Assert.Contains("gym", response.Message.Text, StringComparison.OrdinalIgnoreCase); } - [ConditionalFact] - public virtual async Task GetTextAsync_MultipleAudioRequestMessage() - { - SkipIfNotEnabled(); - - using var firstAudioStream = GetAudioStream("audio001.wav"); - using var secondAudioStream = GetAudioStream("audio002.wav"); - - var response = await _client.GetTextAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()]); - - var firstFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 0)); - var secondFileChoice = Assert.Single(response.Choices.Where(c => c.InputIndex == 1)); - - Assert.Contains("gym", firstFileChoice.Text); - Assert.Contains("who", secondFileChoice.Text); - } - [ConditionalFact] public virtual async Task GetStreamingTextAsync_SingleStreamingResponseChoice() { @@ -67,7 +49,7 @@ public virtual async Task GetStreamingTextAsync_SingleStreamingResponseChoice() using var audioStream = GetAudioStream("audio001.wav"); StringBuilder sb = new(); - await foreach (var chunk in _client.GetStreamingTextAsync([audioStream.ToAsyncEnumerable()])) + await foreach (var chunk in _client.GetStreamingTextAsync(audioStream)) { sb.Append(chunk.Text); } @@ -77,37 +59,6 @@ public virtual async Task GetStreamingTextAsync_SingleStreamingResponseChoice() Assert.Contains("gym", responseText, StringComparison.OrdinalIgnoreCase); } - [ConditionalFact] - public virtual async Task GetStreamingTextAsync_MultipleStreamingResponseChoice() - { - SkipIfNotEnabled(); - - using var firstAudioStream = GetAudioStream("audio001.wav"); - using var secondAudioStream = GetAudioStream("audio002.wav"); - - StringBuilder firstSb = new(); - StringBuilder secondSb = new(); - await foreach (var chunk in _client.GetStreamingTextAsync([firstAudioStream.ToAsyncEnumerable(), secondAudioStream.ToAsyncEnumerable()])) - { - if (chunk.InputIndex == 0) - { - firstSb.Append(chunk.Text); - } - else - { - secondSb.Append(chunk.Text); - } - } - - string firstTranscription = firstSb.ToString(); - Assert.Contains("finally", firstTranscription, StringComparison.OrdinalIgnoreCase); - Assert.Contains("gym", firstTranscription, StringComparison.OrdinalIgnoreCase); - - string secondTranscription = secondSb.ToString(); - Assert.Contains("who would", secondTranscription, StringComparison.OrdinalIgnoreCase); - Assert.Contains("go for", secondTranscription, StringComparison.OrdinalIgnoreCase); - } - private static Stream GetAudioStream(string fileName) { using Stream? s = typeof(SpeechToTextClientIntegrationTests).Assembly.GetManifestResourceStream($"Microsoft.Extensions.AI.Resources.{fileName}"); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs index f27b4ca7be3..a71e337eff2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -40,14 +41,14 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP using ISpeechToTextClient innerClient = new TestSpeechToTextClient { - GetResponseAsyncCallback = (speechContents, options, cancellationToken) => + GetResponseAsyncCallback = (audioStream, options, cancellationToken) => { Assert.Same(returnedOptions, options); Assert.Equal(cts.Token, cancellationToken); return Task.FromResult(expectedResponse); }, - GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => + GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => { Assert.Same(returnedOptions, options); Assert.Equal(cts.Token, cancellationToken); @@ -73,11 +74,13 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP }) .Build(); - var response = await client.GetTextAsync([], providedOptions, cts.Token); + using var memoryStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + var response = await client.GetTextAsync(memoryStream, providedOptions, cts.Token); Assert.Same(expectedResponse, response); int i = 0; - await using var e = client.GetStreamingTextAsync([], providedOptions, cts.Token).GetAsyncEnumerator(); + using var memoryStream2 = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + await using var e = client.GetStreamingTextAsync(memoryStream2, providedOptions, cts.Token).GetAsyncEnumerator(); while (i < expectedUpdates.Length) { Assert.True(await e.MoveNextAsync()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index c323e44094a..d5691ff2ce0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -55,7 +56,7 @@ public async Task GetTextAsync_LogsResponseInvocationAndCompletion(LogLevel leve using ISpeechToTextClient innerClient = new TestSpeechToTextClient { - GetResponseAsyncCallback = (messages, options, cancellationToken) => + GetResponseAsyncCallback = (audioStream, options, cancellationToken) => { return Task.FromResult(new SpeechToTextResponse([new("blue whale")])); }, @@ -66,8 +67,9 @@ public async Task GetTextAsync_LogsResponseInvocationAndCompletion(LogLevel leve .UseLogging() .Build(services); + using var memoryStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); await client.GetTextAsync( - [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], + memoryStream, new SpeechToTextOptions { SpeechLanguage = "pt" }); var logs = collector.GetSnapshot(); @@ -100,7 +102,7 @@ public async Task GetStreamingTextAsync_LogsUpdateReceived(LogLevel level) using ISpeechToTextClient innerClient = new TestSpeechToTextClient { - GetStreamingResponseAsyncCallback = (speechContents, options, cancellationToken) => GetUpdatesAsync() + GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => GetUpdatesAsync() }; static async IAsyncEnumerable GetUpdatesAsync() @@ -115,8 +117,9 @@ static async IAsyncEnumerable GetUpdatesAsync() .UseLogging(loggerFactory) .Build(); + using var memoryStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); await foreach (var update in client.GetStreamingTextAsync( - [YieldAsync([new DataContent("data:audio/wav;base64,AQIDBA==")])], + memoryStream, new SpeechToTextOptions { SpeechLanguage = "pt" })) { // nop @@ -144,13 +147,4 @@ static async IAsyncEnumerable GetUpdatesAsync() Assert.Empty(logs); } } - - private static async IAsyncEnumerable YieldAsync(IEnumerable input) - { - await Task.Yield(); - foreach (var item in input) - { - yield return item; - } - } } From 08310002f2128c0a89b0d71cf8539aa2aa41c113 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 20:13:07 +0000 Subject: [PATCH 08/27] Update XmlDocs with corret definition, ensure correct naming --- .../DelegatingSpeechToTextClient.cs | 8 ++-- .../SpeechToText/ISpeechToTextClient.cs | 14 +++--- .../SpeechToTextClientExtensions.cs | 24 +++++----- .../SpeechToText/SpeechToTextMessage.cs | 4 +- .../SpeechToTextResponseUpdate.cs | 8 ++-- .../OpenAISpeechToTextClient.cs | 22 ++++----- .../ConfigureOptionsSpeechToTextClient.cs | 8 ++-- .../SpeechToText/LoggingSpeechToTextClient.cs | 12 ++--- .../DelegatingSpeechToTextClientTests.cs | 16 +++---- .../SpeechToTextClientExtensionsTests.cs | 10 ++-- .../SpeechToText/SpeechToTextClientTests.cs | 7 ++- .../TestSpeechToTextClient.cs | 12 ++--- ...oft.Extensions.AI.Integration.Tests.csproj | 8 ++-- .../Resources/audio001.mp3 | Bin .../Resources/audio001.wav | Bin 138248 -> 0 bytes .../Resources/audio002.wav | Bin 149768 -> 0 bytes .../SpeechToTextClientIntegrationTests.cs | 8 ++-- ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 5 -- .../OpenAISpeechToTextClientTests.cs | 45 +++++++----------- .../Resources/audio001.wav | Bin 138248 -> 0 bytes ...ConfigureOptionsSpeechToTextClientTests.cs | 12 ++--- .../LoggingSpeechToTextClientTests.cs | 12 ++--- 22 files changed, 110 insertions(+), 125 deletions(-) rename test/Libraries/{Microsoft.Extensions.AI.OpenAI.Tests => Microsoft.Extensions.AI.Integration.Tests}/Resources/audio001.mp3 (100%) delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.wav delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio002.wav delete mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.wav diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs index 49f95b05847..6cbe2392e4c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/DelegatingSpeechToTextClient.cs @@ -42,16 +42,16 @@ public void Dispose() /// public virtual Task GetTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.GetTextAsync(audioStream, options, cancellationToken); + return InnerClient.GetTextAsync(audioSpeechStream, options, cancellationToken); } /// public virtual IAsyncEnumerable GetStreamingTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return InnerClient.GetStreamingTextAsync(audioStream, options, cancellationToken); + return InnerClient.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs index 36c01afad09..4c7173e2d58 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -22,29 +22,29 @@ namespace Microsoft.Extensions.AI; /// using shared instances of these arguments for concurrent invocations or should otherwise ensure by construction that no /// instances are used which might employ such mutation. For example, the ConfigureOptions method be /// provided with a callback that could mutate the supplied options argument, and that should be avoided if using a singleton options instance. -/// The audio stream passed to these methods will not be closed or disposed by the implementation. +/// The audio speech stream passed to these methods will not be closed or disposed by the implementation. /// /// [Experimental("MEAI001")] public interface ISpeechToTextClient : IDisposable { - /// Sends speech audio content to the model and returns the generated text. - /// The audio stream to send. + /// Sends audio speech content to the model and returns the generated text. + /// The audio speech stream to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. Task GetTextAsync( - Stream audioStream, + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); - /// Sends speech audio content to the model and streams back the generated text. - /// The audio stream to send. + /// Sends audio speech content to the model and streams back the generated text. + /// The audio speech stream to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. IAsyncEnumerable GetStreamingTextAsync( - Stream audioStream, + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index 3ac86d5e11f..945cc0e533e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -31,42 +31,42 @@ public static class SpeechToTextClientExtensions return (TService?)client.GetService(typeof(TService), serviceKey); } - /// Generates text from speech providing a single speech audio . + /// Generates text from speech providing a single audio speech . /// The client. - /// The single speech audio content. + /// The single audio speech content. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. public static async Task GetTextAsync( this ISpeechToTextClient client, - DataContent speechContent, + DataContent audioSpeechContent, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNull(speechContent); + _ = Throw.IfNull(audioSpeechContent); _ = Throw.IfNull(client); - using var memoryStream = new MemoryStream(speechContent.Data.ToArray()); - return await client.GetTextAsync(memoryStream, options, cancellationToken).ConfigureAwait(false); + using var audioSpeechStream = new MemoryStream(audioSpeechContent.Data.ToArray()); + return await client.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); } - /// Generates text from speech providing a single speech audio . + /// Generates text from speech providing a single audio speech . /// The client. - /// The single speech audio content. + /// The single audio speech content. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. public static async IAsyncEnumerable GetStreamingTextAsync( this ISpeechToTextClient client, - DataContent speechContent, + DataContent audioSpeechContent, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - _ = Throw.IfNull(speechContent); + _ = Throw.IfNull(audioSpeechContent); _ = Throw.IfNull(client); - using var memoryStream = new MemoryStream(speechContent.Data.ToArray()); - await foreach (var update in client.GetStreamingTextAsync(memoryStream, options, cancellationToken).ConfigureAwait(false)) + using var audioSpeechStream = new MemoryStream(audioSpeechContent.Data.ToArray()); + await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)) { yield return update; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs index 01a1dd92801..18dc4ee5fa1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs @@ -38,11 +38,11 @@ public SpeechToTextMessage( } /// Gets or sets the start time of the speech to text choice. - /// This represents the start of the generated text in relation to the original speech audio source length. + /// This represents the start of the generated text in relation to the original audio speech source length. public TimeSpan? StartTime { get; set; } /// Gets or sets the end time of the speech to text choice. - /// This represents the end of the generated text in relation to the original speech audio source length. + /// This represents the end of the generated text in relation to the original audio speech source length. public TimeSpan? EndTime { get; set; } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs index 9b39edef892..2c2532dbe22 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -16,8 +16,8 @@ namespace Microsoft.Extensions.AI; /// /// is so named because it represents streaming updates /// to an speech to text generation. As such, it is considered erroneous for multiple updates that are part -/// of the same speech audio to contain competing values. For example, some updates that are part of -/// the same speech audio may have a value, and others may have a non- value, +/// of the same audio speech to contain competing values. For example, some updates that are part of +/// the same audio speech may have a value, and others may have a non- value, /// but all of those with a non- value must have the same value (e.g. ). /// /// @@ -66,10 +66,10 @@ public SpeechToTextResponseUpdate(string? content) /// Gets or sets the ID of the generated text response of which this update is a part. public string? ResponseId { get; set; } - /// Gets or sets the start time of the text segment associated with this update in relation to the full speech audio length. + /// Gets or sets the start time of the text segment associated with this update in relation to the full audio speech length. public TimeSpan? StartTime { get; set; } - /// Gets or sets the end time of the text segment associated with this update in relation to the full speech audio length. + /// Gets or sets the end time of the text segment associated with this update in relation to the full audio speech length. public TimeSpan? EndTime { get; set; } /// Gets or sets the model ID using in the creation of the speech to text of which this update is a part. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index d4680cce5ed..492b5baf50c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -92,11 +92,11 @@ public OpenAISpeechToTextClient(AudioClient audioClient) /// public async IAsyncEnumerable GetStreamingTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - _ = Throw.IfNull(audioStream); + _ = Throw.IfNull(audioSpeechStream); - var speechResponse = await GetTextAsync(audioStream, options, cancellationToken).ConfigureAwait(false); + var speechResponse = await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); foreach (var choice in speechResponse.Choices) { @@ -111,9 +111,9 @@ public async IAsyncEnumerable GetStreamingTextAsync( /// public async Task GetTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNull(audioStream); + _ = Throw.IfNull(audioSpeechStream); List choices = []; @@ -136,13 +136,13 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) AudioTranslation translationResult; #if NET - await using (audioStream.ConfigureAwait(false)) + await using (audioSpeechStream.ConfigureAwait(false)) #else - using (audioStream) + using (audioSpeechStream) #endif { translationResult = (await _audioClient.TranslateAudioAsync( - audioStream, + audioSpeechStream, "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. openAIOptions, cancellationToken).ConfigureAwait(false)).Value; } @@ -158,13 +158,13 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) AudioTranscription transcriptionResult; #if NET - await using (audioStream.ConfigureAwait(false)) + await using (audioSpeechStream.ConfigureAwait(false)) #else - using (audioStream) + using (audioSpeechStream) #endif { transcriptionResult = (await _audioClient.TranscribeAudioAsync( - audioStream, + audioSpeechStream, "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. openAIOptions, cancellationToken).ConfigureAwait(false)).Value; } diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs index 954413cce4a..2e768a58e6c 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs @@ -37,16 +37,16 @@ public ConfigureOptionsSpeechToTextClient(ISpeechToTextClient innerClient, Actio /// public override Task GetTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetTextAsync(audioStream, Configure(options), cancellationToken); + return base.GetTextAsync(audioSpeechStream, Configure(options), cancellationToken); } /// public override IAsyncEnumerable GetStreamingTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetStreamingTextAsync(audioStream, Configure(options), cancellationToken); + return base.GetStreamingTextAsync(audioSpeechStream, Configure(options), cancellationToken); } /// Creates and configures the to pass along to the inner client. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index 86d0ea84fbd..52f690169ff 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -47,13 +47,13 @@ public JsonSerializerOptions JsonSerializerOptions /// public override async Task GetTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetTextAsync), "[audio stream]", AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetTextAsync), "[audio speech stream]", AsJson(options), AsJson(this.GetService())); } else { @@ -63,7 +63,7 @@ public override async Task GetTextAsync( try { - var completion = await base.GetTextAsync(audioStream, options, cancellationToken).ConfigureAwait(false); + var completion = await base.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -93,13 +93,13 @@ public override async Task GetTextAsync( /// public override async IAsyncEnumerable GetStreamingTextAsync( - Stream audioStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetStreamingTextAsync), "[audio stream]", AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetStreamingTextAsync), "[audio speech stream]", AsJson(options), AsJson(this.GetService())); } else { @@ -110,7 +110,7 @@ public override async IAsyncEnumerable GetStreamingT IAsyncEnumerator e; try { - e = base.GetStreamingTextAsync(audioStream, options, cancellationToken).GetAsyncEnumerator(cancellationToken); + e = base.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken).GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs index 89e13048136..c9535b4fd81 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs @@ -22,16 +22,16 @@ public void RequiresInnerSpeechToTextClient() public async Task GetTextAsyncDefaultsToInnerClientAsync() { // Arrange - using var expectedStream = new MemoryStream(); + using var expectedAudioSpeechStream = new MemoryStream(); var expectedOptions = new SpeechToTextOptions(); var expectedCancellationToken = CancellationToken.None; var expectedResult = new TaskCompletionSource(); var expectedResponse = new SpeechToTextResponse([]); using var inner = new TestSpeechToTextClient { - GetResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { - Assert.Same(expectedStream, audioStream); + Assert.Same(expectedAudioSpeechStream, audioSpeechStream); Assert.Same(expectedOptions, options); Assert.Equal(expectedCancellationToken, cancellationToken); return expectedResult.Task; @@ -41,7 +41,7 @@ public async Task GetTextAsyncDefaultsToInnerClientAsync() using var delegating = new NoOpDelegatingSpeechToTextClient(inner); // Act - var resultTask = delegating.GetTextAsync(expectedStream, expectedOptions, expectedCancellationToken); + var resultTask = delegating.GetTextAsync(expectedAudioSpeechStream, expectedOptions, expectedCancellationToken); // Assert Assert.False(resultTask.IsCompleted); @@ -54,7 +54,7 @@ public async Task GetTextAsyncDefaultsToInnerClientAsync() public async Task GetStreamingTextAsyncDefaultsToInnerClientAsync() { // Arrange - using var expectedStream = new MemoryStream(); + using var expectedAudioSpeechStream = new MemoryStream(); var expectedOptions = new SpeechToTextOptions(); var expectedCancellationToken = CancellationToken.None; SpeechToTextResponseUpdate[] expectedResults = @@ -65,9 +65,9 @@ public async Task GetStreamingTextAsyncDefaultsToInnerClientAsync() using var inner = new TestSpeechToTextClient { - GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetStreamingTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { - Assert.Same(expectedStream, audioStream); + Assert.Same(expectedAudioSpeechStream, audioSpeechStream); Assert.Same(expectedOptions, options); Assert.Equal(expectedCancellationToken, cancellationToken); return YieldAsync(expectedResults); @@ -77,7 +77,7 @@ public async Task GetStreamingTextAsyncDefaultsToInnerClientAsync() using var delegating = new NoOpDelegatingSpeechToTextClient(inner); // Act - var resultAsyncEnumerable = delegating.GetStreamingTextAsync(expectedStream, expectedOptions, expectedCancellationToken); + var resultAsyncEnumerable = delegating.GetStreamingTextAsync(expectedAudioSpeechStream, expectedOptions, expectedCancellationToken); // Assert var enumerator = resultAsyncEnumerable.GetAsyncEnumerator(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs index fea28e84051..dc3fb283325 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs @@ -30,9 +30,9 @@ public void GetTextAsync_InvalidArgs_Throws() _ = SpeechToTextClientExtensions.GetTextAsync(null!, new DataContent("data:audio/wav;base64,AQIDBA==")); }); - Assert.Throws("speechContent", () => + Assert.Throws("audioSpeechContent", () => { - _ = SpeechToTextClientExtensions.GetTextAsync(new TestSpeechToTextClient(), (DataContent)null!); + _ = SpeechToTextClientExtensions.GetTextAsync(new TestSpeechToTextClient(), null!); }); } @@ -45,9 +45,9 @@ public void GetStreamingTextAsync_InvalidArgs_Throws() _ = SpeechToTextClientExtensions.GetStreamingTextAsync(client: null!, new DataContent("data:audio/wav;base64,AQIDBA==")); }); - Assert.Throws("speechContent", () => + Assert.Throws("audioSpeechContent", () => { - _ = SpeechToTextClientExtensions.GetStreamingTextAsync(new TestSpeechToTextClient(), speechContent: null!); + _ = SpeechToTextClientExtensions.GetStreamingTextAsync(new TestSpeechToTextClient(), audioSpeechContent: null!); }); } @@ -60,7 +60,7 @@ public async Task GetStreamingTextAsync_CreatesTextMessageAsync() using TestSpeechToTextClient client = new() { - GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetStreamingTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { // For testing, return an async enumerable yielding one streaming update with text "world". return YieldAsync(new SpeechToTextResponseUpdate { Text = "world" }); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs index ac3d74f88af..962c546aeed 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -20,10 +19,10 @@ public async Task GetTextAsync_CreatesTextMessageAsync() using TestSpeechToTextClient client = new() { - GetResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { - // For the purpose of the test, we assume that the underlying implementation converts the audio stream into a transcription choice. - // (In a real implementation, the speech audio data would be processed.) + // For the purpose of the test, we assume that the underlying implementation converts the audio speech stream into a transcription choice. + // (In a real implementation, the audio speech data would be processed.) SpeechToTextMessage choice = new("hello"); return Task.FromResult(new SpeechToTextResponse(choice)); }, diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs index 9401319f141..44e1d739533 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestSpeechToTextClient.cs @@ -23,14 +23,14 @@ public TestSpeechToTextClient() SpeechToTextOptions?, CancellationToken, Task>? - GetResponseAsyncCallback + GetTextAsyncCallback { get; set; } public Func>? - GetStreamingResponseAsyncCallback + GetStreamingTextAsyncCallback { get; set; } public Func GetServiceCallback { get; set; } @@ -39,16 +39,16 @@ public TestSpeechToTextClient() => serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; public Task GetTextAsync( - Stream audioStream, + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) - => GetResponseAsyncCallback!.Invoke(audioStream, options, cancellationToken); + => GetTextAsyncCallback!.Invoke(audioSpeechStream, options, cancellationToken); public IAsyncEnumerable GetStreamingTextAsync( - Stream audioStream, + Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) - => GetStreamingResponseAsyncCallback!.Invoke(audioStream, options, cancellationToken); + => GetStreamingTextAsyncCallback!.Invoke(audioSpeechStream, options, cancellationToken); public object? GetService(Type serviceType, object? serviceKey = null) => GetServiceCallback!.Invoke(serviceType, serviceKey); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index d2c12ac907f..ec925a15309 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -18,13 +18,13 @@ - - + - - + + Never + diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.mp3 b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.mp3 similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.mp3 rename to test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.mp3 diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.wav b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio001.wav deleted file mode 100644 index f909b12aefdefb5fd63db03270fa25775851cb00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138248 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

LVrjptWZx4aaRrGvedqy@m+d{RJ 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 YBB~4(^hG||o?U|;vg-m=)b*7A2aOJK6#xJL diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio002.wav b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Resources/audio002.wav deleted file mode 100644 index 010e034775b486d0823ed5eb6fda18b978f6aa83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149768 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 P4fV-kmf`8=qxk$kE@De` diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index 93b46046580..ceb4a540162 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -35,8 +35,8 @@ public virtual async Task GetTextAsync_SingleAudioRequestMessage() { SkipIfNotEnabled(); - using var audioStream = GetAudioStream("audio001.wav"); - var response = await _client.GetTextAsync(audioStream); + using var audioSpeechStream = GetAudioStream("audio001.mp3"); + var response = await _client.GetTextAsync(audioSpeechStream); Assert.Contains("gym", response.Message.Text, StringComparison.OrdinalIgnoreCase); } @@ -46,10 +46,10 @@ public virtual async Task GetStreamingTextAsync_SingleStreamingResponseChoice() { SkipIfNotEnabled(); - using var audioStream = GetAudioStream("audio001.wav"); + using var audioSpeechStream = GetAudioStream("audio001.mp3"); StringBuilder sb = new(); - await foreach (var chunk in _client.GetStreamingTextAsync(audioStream)) + await foreach (var chunk in _client.GetStreamingTextAsync(audioSpeechStream)) { sb.Append(chunk.Text); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 178afa087be..5626f4f207e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -14,11 +14,6 @@ - - - - - diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index c6644036b20..aa25422d584 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -131,8 +131,8 @@ public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, stri using HttpClient httpClient = new(handler); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.wav"); - var response = await client.GetTextAsync(fileStream, new SpeechToTextOptions + using var audioSpeechStream = GetAudioStream(); + var response = await client.GetTextAsync(audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -153,7 +153,7 @@ public async Task GetTextAsync_Cancelled_Throws() using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.wav"); + using var fileStream = GetAudioStream(); using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); @@ -167,7 +167,7 @@ public async Task GetStreamingTextAsync_Cancelled_Throws() using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.wav"); + using var fileStream = GetAudioStream(); using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); @@ -206,8 +206,8 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu using HttpClient httpClient = new(handler); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.mp3"); - await foreach (var update in client.GetStreamingTextAsync(fileStream, new SpeechToTextOptions + using var audioSpeechStream = GetAudioStream(); + await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -229,8 +229,8 @@ public async Task GetStreamingTextAsync_NonSupportedTranslation_Throws(string? s using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.mp3"); - var asyncEnumerator = client.GetStreamingTextAsync(fileStream, new SpeechToTextOptions + using var audioSpeechStream = GetAudioStream(); + var asyncEnumerator = client.GetStreamingTextAsync(audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -248,9 +248,9 @@ public async Task GetTextAsync_NonSupportedTranslation_Throws(string? speechLang using HttpClient httpClient = new(); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.mp3"); + using var audioSpeechStream = GetAudioStream(); - await Assert.ThrowsAsync(() => client.GetTextAsync(fileStream, new SpeechToTextOptions + await Assert.ThrowsAsync(() => client.GetTextAsync(audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = speechLanguage, TextLanguage = textLanguage @@ -279,8 +279,8 @@ public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() using HttpClient httpClient = new(handler); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.mp3"); - await foreach (var update in client.GetStreamingTextAsync(fileStream, new SpeechToTextOptions + using var audioSpeechStream = GetAudioStream(); + await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = "pt", TextLanguage = textLanguage @@ -316,8 +316,8 @@ public async Task GetTextAsync_NonStronglyTypedOptions_AllSent() using HttpClient httpClient = new(handler); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.mp3"); - Assert.NotNull(await client.GetTextAsync(fileStream, new() + using var audioSpeechStream = GetAudioStream(); + Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() { AdditionalProperties = new() { @@ -350,24 +350,15 @@ public async Task GetTextAsync_StronglyTypedOptions_AllSent() using HttpClient httpClient = new(handler); using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - using var fileStream = GetAudioStream("audio001.mp3"); - Assert.NotNull(await client.GetTextAsync(fileStream, new() + using var audioSpeechStream = GetAudioStream(); + Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() { SpeechLanguage = "pt", })); } - private static Stream GetAudioStream(string fileName) -#pragma warning restore S125 // Sections of code should not be commented out - { - using Stream? s = typeof(OpenAISpeechToTextClientTests).Assembly.GetManifestResourceStream($"Microsoft.Extensions.AI.Resources.{fileName}"); - Assert.NotNull(s); - MemoryStream ms = new(); - s.CopyTo(ms); - - ms.Position = 0; - return ms; - } + private static Stream GetAudioStream() + => new MemoryStream([0x01, 0x02]); private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) => new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.wav b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Resources/audio001.wav deleted file mode 100644 index f909b12aefdefb5fd63db03270fa25775851cb00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138248 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

LVrjptWZx4aaRrGvedqy@m+d{RJ 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 YBB~4(^hG||o?U|;vg-m=)b*7A2aOJK6#xJL diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs index a71e337eff2..6140b7ed354 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/ConfigureOptionsSpeechToTextClientTests.cs @@ -41,14 +41,14 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP using ISpeechToTextClient innerClient = new TestSpeechToTextClient { - GetResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { Assert.Same(returnedOptions, options); Assert.Equal(cts.Token, cancellationToken); return Task.FromResult(expectedResponse); }, - GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetStreamingTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { Assert.Same(returnedOptions, options); Assert.Equal(cts.Token, cancellationToken); @@ -74,13 +74,13 @@ public async Task ConfigureOptions_ReturnedInstancePassedToNextClient(bool nullP }) .Build(); - using var memoryStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); - var response = await client.GetTextAsync(memoryStream, providedOptions, cts.Token); + using var audioSpeechStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + var response = await client.GetTextAsync(audioSpeechStream, providedOptions, cts.Token); Assert.Same(expectedResponse, response); int i = 0; - using var memoryStream2 = new MemoryStream(new byte[] { 1, 2, 3, 4 }); - await using var e = client.GetStreamingTextAsync(memoryStream2, providedOptions, cts.Token).GetAsyncEnumerator(); + using var audioSpeechStream2 = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + await using var e = client.GetStreamingTextAsync(audioSpeechStream2, providedOptions, cts.Token).GetAsyncEnumerator(); while (i < expectedUpdates.Length) { Assert.True(await e.MoveNextAsync()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index d5691ff2ce0..1c3c72ae202 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -56,7 +56,7 @@ public async Task GetTextAsync_LogsResponseInvocationAndCompletion(LogLevel leve using ISpeechToTextClient innerClient = new TestSpeechToTextClient { - GetResponseAsyncCallback = (audioStream, options, cancellationToken) => + GetTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { return Task.FromResult(new SpeechToTextResponse([new("blue whale")])); }, @@ -67,9 +67,9 @@ public async Task GetTextAsync_LogsResponseInvocationAndCompletion(LogLevel leve .UseLogging() .Build(services); - using var memoryStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + using var audioSpeechStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); await client.GetTextAsync( - memoryStream, + audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = "pt" }); var logs = collector.GetSnapshot(); @@ -102,7 +102,7 @@ public async Task GetStreamingTextAsync_LogsUpdateReceived(LogLevel level) using ISpeechToTextClient innerClient = new TestSpeechToTextClient { - GetStreamingResponseAsyncCallback = (audioStream, options, cancellationToken) => GetUpdatesAsync() + GetStreamingTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => GetUpdatesAsync() }; static async IAsyncEnumerable GetUpdatesAsync() @@ -117,9 +117,9 @@ static async IAsyncEnumerable GetUpdatesAsync() .UseLogging(loggerFactory) .Build(); - using var memoryStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + using var audioSpeechStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); await foreach (var update in client.GetStreamingTextAsync( - memoryStream, + audioSpeechStream, new SpeechToTextOptions { SpeechLanguage = "pt" })) { // nop From 8c893a9d5821baba07547313b24f2875cf1a2274 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 29 Mar 2025 22:30:58 +0000 Subject: [PATCH 09/27] Dropping the Choice / Message concept, flattering the Message with the Response --- .../SpeechToTextClientExtensions.cs | 4 +- .../SpeechToText/SpeechToTextMessage.cs | 97 ----- .../SpeechToText/SpeechToTextResponse.cs | 139 +++---- .../SpeechToTextResponseUpdate.cs | 6 - .../SpeechToTextResponseUpdateExtensions.cs | 142 +++---- .../OpenAISpeechToTextClient.cs | 76 ++-- .../SpeechToTextClientExtensionsTests.cs | 42 +-- .../SpeechToText/SpeechToTextClientTests.cs | 7 +- .../SpeechToText/SpeechToTextMessageTests.cs | 353 ------------------ .../SpeechToText/SpeechToTextResponseTests.cs | 343 +++++++---------- .../SpeechToTextResponseUpdateTests.cs | 9 - .../SpeechToTextClientIntegrationTests.cs | 2 +- .../OpenAISpeechToTextClientTests.cs | 11 +- .../LoggingSpeechToTextClientTests.cs | 2 +- 14 files changed, 321 insertions(+), 912 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index 945cc0e533e..64f43b7b713 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -43,8 +43,8 @@ public static async Task GetTextAsync( SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNull(audioSpeechContent); _ = Throw.IfNull(client); + _ = Throw.IfNull(audioSpeechContent); using var audioSpeechStream = new MemoryStream(audioSpeechContent.Data.ToArray()); return await client.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); @@ -62,8 +62,8 @@ public static async IAsyncEnumerable GetStreamingTex SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - _ = Throw.IfNull(audioSpeechContent); _ = Throw.IfNull(client); + _ = Throw.IfNull(audioSpeechContent); using var audioSpeechStream = new MemoryStream(audioSpeechContent.Data.ToArray()); await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs deleted file mode 100644 index 18dc4ee5fa1..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextMessage.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.Json.Serialization; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -///

Represents a choice in an speech to text. -[Experimental("MEAI001")] -public class SpeechToTextMessage -{ - private IList? _contents; - - /// Initializes a new instance of the class. - [JsonConstructor] - public SpeechToTextMessage() - { - } - - /// Initializes a new instance of the class. - /// Content of the message. - public SpeechToTextMessage(string? content) - : this(content is null ? [] : [new TextContent(content)]) - { - } - - /// Initializes a new instance of the class. - /// The contents for this message. - public SpeechToTextMessage( - IList contents) - { - _contents = Throw.IfNull(contents); - } - - /// Gets or sets the start time of the speech to text choice. - /// This represents the start of the generated text in relation to the original audio speech source length. - public TimeSpan? StartTime { get; set; } - - /// Gets or sets the end time of the speech to text choice. - /// This represents the end of the generated text in relation to the original audio speech source length. - public TimeSpan? EndTime { get; set; } - - /// - /// Gets or sets the text of the first instance in . - /// - /// - /// If there is no instance in , then the getter returns , - /// and the setter adds a new instance with the provided value. - /// - [JsonIgnore] - public string? Text - { - get => Contents.OfType().FirstOrDefault()?.Text; - set - { - if (Contents.OfType().FirstOrDefault() is { } textContent) - { - textContent.Text = value; - } - else if (value is not null) - { - Contents.Add(new TextContent(value)); - } - } - } - - /// Gets or sets the generated content items. - [AllowNull] - public IList Contents - { - get => _contents ??= []; - set => _contents = value; - } - - /// Gets or sets the zero-based index of the input list with which this choice is associated. - public int InputIndex { get; set; } - - /// Gets or sets the raw representation of the speech to text choice from an underlying implementation. - /// - /// If a is created to represent some underlying object from another object - /// model, this property can be used to store that original object. This can be useful for debugging or - /// for enabling a consumer to access the underlying object model if needed. - /// - [JsonIgnore] - public object? RawRepresentation { get; set; } - - /// Gets or sets any additional properties associated with the message. - public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } - - /// - public override string ToString() => Contents.ConcatText(); -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs index 287ca33b66b..7de22b777b2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text; +using System.Linq; + using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; @@ -14,43 +15,35 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class SpeechToTextResponse { - /// The list of choices in the generated text response. - private IList _choices; + /// The content items in the generated text response. + private IList? _contents; /// Initializes a new instance of the class. - /// the generated text representing the singular choice message in the response. - public SpeechToTextResponse(SpeechToTextMessage message) - : this([Throw.IfNull(message)]) + [JsonConstructor] + public SpeechToTextResponse() { } /// Initializes a new instance of the class. - /// The list of choices in the response, one message per choice. - [JsonConstructor] - public SpeechToTextResponse(IList choices) + /// The contents for this response. + public SpeechToTextResponse(IList contents) { - _choices = Throw.IfNull(choices); + _contents = Throw.IfNull(contents); } - /// Gets the speech to text message details. - /// - /// If no speech to text was generated, this property will throw. - /// - [JsonIgnore] - public SpeechToTextMessage Message + /// Initializes a new instance of the class. + /// Content of the response. + public SpeechToTextResponse(string? content) + : this(content is null ? [] : [new TextContent(content)]) { - get - { - var choices = Choices; - if (choices.Count == 0) - { - throw new InvalidOperationException($"The {nameof(SpeechToTextResponse)} instance does not contain any {nameof(SpeechToTextMessage)} choices."); - } - - return choices[0]; - } } + /// Gets or sets the start time of the text segment in relation to the full audio speech length. + public TimeSpan? StartTime { get; set; } + + /// Gets or sets the end time of the text segment in relation to the full audio speech length. + public TimeSpan? EndTime { get; set; } + /// Gets or sets the ID of the speech to text response. public string? ResponseId { get; set; } @@ -69,77 +62,57 @@ public SpeechToTextMessage Message /// Gets or sets any additional properties associated with the speech to text completion. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } - /// - public override string ToString() + /// + /// Gets or sets the text of the first instance in . + /// + /// + /// If there is no instance in , then the getter returns , + /// and the setter adds a new instance with the provided value. + /// + [JsonIgnore] + public string? Text { - if (Choices.Count == 1) - { - return Choices[0].ToString(); - } - - StringBuilder sb = new(); - for (int i = 0; i < Choices.Count; i++) + get => Contents.OfType().FirstOrDefault()?.Text; + set { - if (i > 0) + if (Contents.OfType().FirstOrDefault() is { } textContent) { - _ = sb.AppendLine().AppendLine(); + textContent.Text = value; + } + else if (value is not null) + { + Contents.Add(new TextContent(value)); } - - _ = sb.Append("Choice ").Append(i).AppendLine(":").Append(Choices[i]); } - - return sb.ToString(); } + /// + public override string ToString() => Contents.ConcatText(); + /// Creates an array of instances that represent this . /// An array of instances that may be used to represent this . public SpeechToTextResponseUpdate[] ToSpeechToTextResponseUpdates() { - SpeechToTextResponseUpdate? extra = null; - if (AdditionalProperties is not null) + SpeechToTextResponseUpdate update = new SpeechToTextResponseUpdate { - extra = new SpeechToTextResponseUpdate - { - Kind = SpeechToTextResponseUpdateKind.TextUpdated, - AdditionalProperties = AdditionalProperties, - }; - } - - int choicesCount = Choices.Count; - var updates = new SpeechToTextResponseUpdate[choicesCount + (extra is null ? 0 : 1)]; - - for (int choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) - { - SpeechToTextMessage choice = Choices[choiceIndex]; - updates[choiceIndex] = new SpeechToTextResponseUpdate - { - ChoiceIndex = choiceIndex, - InputIndex = choice.InputIndex, - - AdditionalProperties = choice.AdditionalProperties, - Contents = choice.Contents, - RawRepresentation = choice.RawRepresentation, - StartTime = choice.StartTime, - EndTime = choice.EndTime, - - Kind = SpeechToTextResponseUpdateKind.TextUpdated, - ResponseId = ResponseId, - ModelId = ModelId, - }; - } - - if (extra is not null) - { - updates[choicesCount] = extra; - } - - return updates; + Contents = Contents, + AdditionalProperties = AdditionalProperties, + RawRepresentation = RawRepresentation, + StartTime = StartTime, + EndTime = EndTime, + Kind = SpeechToTextResponseUpdateKind.TextUpdated, + ResponseId = ResponseId, + ModelId = ModelId, + }; + + return [update]; } - /// Gets or sets the list of speech to text choices. - public IList Choices + /// Gets or sets the generated content items. + [AllowNull] + public IList Contents { - get => _choices; - set => _choices = Throw.IfNull(value); + get => _contents ??= []; + set => _contents = value; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs index 2c2532dbe22..eefba114228 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -54,12 +54,6 @@ public SpeechToTextResponseUpdate(string? content) { } - /// Gets or sets the zero-based index of the input list with which this update is associated in the streaming sequence. - public int InputIndex { get; set; } - - /// Gets or sets the zero-based index of the resulting choice with which this update is associated in the streaming sequence. - public int ChoiceIndex { get; set; } - /// Gets or sets the kind of the generated text update. public SpeechToTextResponseUpdateKind Kind { get; set; } = SpeechToTextResponseUpdateKind.TextUpdating; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index 7628c21ed1c..6ecb53b8db2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -3,10 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -#if NET -using System.Runtime.InteropServices; -#endif + using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -23,8 +20,7 @@ public static class SpeechToTextResponseUpdateExtensions /// The updates to be combined. /// /// to attempt to coalesce contiguous items, where applicable, - /// into a single , in order to reduce the number of individual content items that are included in - /// the manufactured instances. When , the original content items are used. + /// into a single . When , the original content items are used. /// The default is . /// /// The combined . @@ -33,15 +29,28 @@ public static SpeechToTextResponse ToSpeechToTextResponse( { _ = Throw.IfNull(updates); - SpeechToTextResponse response = new([]); - Dictionary choices = []; + SpeechToTextResponse response = new(); + List contents = []; + string? responseId = null; + string? modelId = null; + object? rawRepresentation = null; + AdditionalPropertiesDictionary? additionalProperties = null; foreach (var update in updates) { - ProcessUpdate(update, choices, response); + ProcessUpdate(update, contents, ref responseId, ref modelId, ref rawRepresentation, ref additionalProperties); + } + + if (coalesceContent) + { + ChatResponseExtensions.CoalesceTextContent(contents); } - AddChoicesToCompletion(choices, response, coalesceContent); + response.Contents = contents; + response.ResponseId = responseId; + response.ModelId = modelId; + response.RawRepresentation = rawRepresentation; + response.AdditionalProperties = additionalProperties; return response; } @@ -50,8 +59,7 @@ public static SpeechToTextResponse ToSpeechToTextResponse( /// The updates to be combined. /// /// to attempt to coalesce contiguous items, where applicable, - /// into a single , in order to reduce the number of individual content items that are included in - /// the manufactured instances. When , the original content items are used. + /// into a single . When , the original content items are used. /// The default is . /// /// The to monitor for cancellation requests. The default is . @@ -66,110 +74,80 @@ public static Task ToSpeechToTextResponseAsync( static async Task ToResponseAsync( IAsyncEnumerable updates, bool coalesceContent, CancellationToken cancellationToken) { - SpeechToTextResponse response = new([]); - Dictionary choices = []; + SpeechToTextResponse response = new(); + List contents = []; + string? responseId = null; + string? modelId = null; + object? rawRepresentation = null; + AdditionalPropertiesDictionary? additionalProperties = null; await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) { - ProcessUpdate(update, choices, response); + ProcessUpdate(update, contents, ref responseId, ref modelId, ref rawRepresentation, ref additionalProperties); + } + + if (coalesceContent) + { + ChatResponseExtensions.CoalesceTextContent(contents); } - AddChoicesToCompletion(choices, response, coalesceContent); + response.Contents = contents; + response.ResponseId = responseId; + response.ModelId = modelId; + response.RawRepresentation = rawRepresentation; + response.AdditionalProperties = additionalProperties; return response; } } - /// Processes the , incorporating its contents into and . + /// Processes the , incorporating its contents and properties. /// The update to process. - /// The dictionary mapping to the being built for that choice. - /// The object whose properties should be updated based on . - private static void ProcessUpdate(SpeechToTextResponseUpdate update, Dictionary choices, SpeechToTextResponse response) + /// The list of content items being accumulated. + /// The response ID to update if the update has one. + /// The model ID to update if the update has one. + /// The raw representation to update if the update has one. + /// The additional properties to update if the update has any. +#pragma warning disable S4047 // Generics should be used when appropriate + private static void ProcessUpdate( + SpeechToTextResponseUpdate update, + List contents, + ref string? responseId, + ref string? modelId, + ref object? rawRepresentation, + ref AdditionalPropertiesDictionary? additionalProperties) { if (update.ResponseId is not null) { - response.ResponseId = update.ResponseId; + responseId = update.ResponseId; } if (update.ModelId is not null) { - response.ModelId = update.ModelId; + modelId = update.ModelId; } -#if NET - SpeechToTextMessage choice = CollectionsMarshal.GetValueRefOrAddDefault(choices, update.ChoiceIndex, out _) ??= - new(new List()); -#else - if (!choices.TryGetValue(update.ChoiceIndex, out SpeechToTextMessage? choice)) + if (update.RawRepresentation is not null) { - choices[update.ChoiceIndex] = choice = new(new List()); + rawRepresentation = update.RawRepresentation; } -#endif - ((List)choice.Contents).AddRange(update.Contents); + contents.AddRange(update.Contents); if (update.AdditionalProperties is not null) { - if (choice.AdditionalProperties is null) + if (additionalProperties is null) { - choice.AdditionalProperties = new(update.AdditionalProperties); + additionalProperties = new(update.AdditionalProperties); } else { foreach (var entry in update.AdditionalProperties) { - choice.AdditionalProperties[entry.Key] = entry.Value; + additionalProperties[entry.Key] = entry.Value; } } } } - - /// Finalizes the object by transferring the into it. - /// The messages to process further and transfer into . - /// The result being built. - /// The corresponding option value provided to or . - private static void AddChoicesToCompletion(Dictionary choices, SpeechToTextResponse response, bool coalesceContent) - { - if (choices.Count <= 1) - { - // Add the single message if there is one. - foreach (var entry in choices) - { - AddChoice(response, coalesceContent, entry); - } - - // In the vast majority case where there's only one choice, promote any additional properties - // from the single choice to the speech to text response, making them more discoverable and more similar - // to how they're typically surfaced from non-streaming services. - if (response.Choices.Count == 1 && - response.Choices[0].AdditionalProperties is { } messageProps) - { - response.Choices[0].AdditionalProperties = null; - response.AdditionalProperties = messageProps; - } - } - else - { - // Add all of the messages, sorted by choice index. - foreach (var entry in choices.OrderBy(entry => entry.Key)) - { - AddChoice(response, coalesceContent, entry); - } - - // If there are multiple choices, we don't promote additional properties from the individual messages. - // At a minimum, we'd want to know which choice the additional properties applied to, and if there were - // conflicting values across the choices, it would be unclear which one should be used. - } - - static void AddChoice(SpeechToTextResponse response, bool coalesceContent, KeyValuePair entry) - { - if (coalesceContent) - { - // Consider moving to a utility method. - ChatResponseExtensions.CoalesceTextContent((List)entry.Value.Contents); - } - - response.Choices.Add(entry.Value); - } - } +#pragma warning restore S4047 // Generics should be used when appropriate } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 492b5baf50c..e86aa39ce7a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -98,15 +98,15 @@ public async IAsyncEnumerable GetStreamingTextAsync( var speechResponse = await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); - foreach (var choice in speechResponse.Choices) + yield return new SpeechToTextResponseUpdate(speechResponse.Contents) { - yield return new SpeechToTextResponseUpdate(choice.Contents) - { - InputIndex = 0, - Kind = SpeechToTextResponseUpdateKind.TextUpdated, - RawRepresentation = choice.RawRepresentation - }; - } + Kind = SpeechToTextResponseUpdateKind.TextUpdated, + RawRepresentation = speechResponse.RawRepresentation, + StartTime = speechResponse.StartTime, + EndTime = speechResponse.EndTime, + ResponseId = speechResponse.ResponseId, + ModelId = speechResponse.ModelId + }; } /// @@ -115,7 +115,7 @@ public async Task GetTextAsync( { _ = Throw.IfNull(audioSpeechStream); - List choices = []; + SpeechToTextResponse response = new(); // A translation is triggered when the target text language is specified and the source language is not provided or different. static bool IsTranslationRequest(SpeechToTextOptions? options) @@ -147,8 +147,7 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) openAIOptions, cancellationToken).ConfigureAwait(false)).Value; } - var choice = FromOpenAIAudioTranslation(translationResult, 0); - choices.Add(choice); + UpdateResponseFromOpenAIAudioTranslation(response, translationResult); } else { @@ -169,11 +168,10 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) openAIOptions, cancellationToken).ConfigureAwait(false)).Value; } - var choice = FromOpenAIAudioTranscription(transcriptionResult, 0); - choices.Add(choice); + UpdateResponseFromOpenAIAudioTranscription(response, transcriptionResult); } - return new SpeechToTextResponse(choices); + return response; } /// @@ -182,7 +180,10 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. } - private static SpeechToTextMessage FromOpenAIAudioTranscription(AudioTranscription audioTranscription, int inputIndex) + /// Updates a from an OpenAI . + /// The response to update. + /// The OpenAI audio transcription. + private static void UpdateResponseFromOpenAIAudioTranscription(SpeechToTextResponse response, AudioTranscription audioTranscription) { _ = Throw.IfNull(audioTranscription); @@ -202,19 +203,15 @@ private static SpeechToTextMessage FromOpenAIAudioTranscription(AudioTranscripti startTime = audioTranscription.Words[0].StartTime; } - // Create the return choice. - return new SpeechToTextMessage + // Update the response + response.RawRepresentation = audioTranscription; + response.Text = audioTranscription.Text; + response.StartTime = startTime; + response.EndTime = endTime; + response.AdditionalProperties = new AdditionalPropertiesDictionary { - RawRepresentation = audioTranscription, - InputIndex = inputIndex, - Text = audioTranscription.Text, - StartTime = startTime, - EndTime = endTime, - AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranscription.Language)] = audioTranscription.Language, - [nameof(audioTranscription.Duration)] = audioTranscription.Duration - }, + [nameof(audioTranscription.Language)] = audioTranscription.Language, + [nameof(audioTranscription.Duration)] = audioTranscription.Duration }; } @@ -257,7 +254,10 @@ private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTe return result; } - private static SpeechToTextMessage FromOpenAIAudioTranslation(AudioTranslation audioTranslation, int inputIndex) + /// Updates a from an OpenAI . + /// The response to update. + /// The OpenAI audio translation. + private static void UpdateResponseFromOpenAIAudioTranslation(SpeechToTextResponse response, AudioTranslation audioTranslation) { _ = Throw.IfNull(audioTranslation); @@ -271,19 +271,15 @@ private static SpeechToTextMessage FromOpenAIAudioTranslation(AudioTranslation a startTime = audioTranslation.Segments[0].StartTime; } - // Create the return choice. - return new SpeechToTextMessage + // Update the response + response.RawRepresentation = audioTranslation; + response.Text = audioTranslation.Text; + response.StartTime = startTime; + response.EndTime = endTime; + response.AdditionalProperties = new AdditionalPropertiesDictionary { - RawRepresentation = audioTranslation, - InputIndex = inputIndex, - Text = audioTranslation.Text, - StartTime = startTime, - EndTime = endTime, - AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranslation.Language)] = audioTranslation.Language, - [nameof(audioTranslation.Duration)] = audioTranslation.Duration - }, + [nameof(audioTranslation.Language)] = audioTranslation.Language, + [nameof(audioTranslation.Duration)] = audioTranslation.Duration }; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs index dc3fb283325..d39c73fc0c6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientExtensionsTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -22,33 +21,32 @@ public void GetService_InvalidArgs_Throws() } [Fact] - public void GetTextAsync_InvalidArgs_Throws() + public async Task GetTextAsync_InvalidArgs_Throws() { // Note: the extension method now requires a DataContent (not a string). - Assert.Throws("client", () => - { - _ = SpeechToTextClientExtensions.GetTextAsync(null!, new DataContent("data:audio/wav;base64,AQIDBA==")); - }); + ISpeechToTextClient? client = null; + var content = new DataContent("data:audio/wav;base64,AQIDBA=="); + var ex1 = await Assert.ThrowsAsync(() => SpeechToTextClientExtensions.GetTextAsync(client!, content)); + Assert.Equal("client", ex1.ParamName); - Assert.Throws("audioSpeechContent", () => - { - _ = SpeechToTextClientExtensions.GetTextAsync(new TestSpeechToTextClient(), null!); - }); + using var testClient = new TestSpeechToTextClient(); + DataContent? nullContent = null; + var ex2 = await Assert.ThrowsAsync(() => SpeechToTextClientExtensions.GetTextAsync(testClient, nullContent!)); + Assert.Equal("audioSpeechContent", ex2.ParamName); } [Fact] - public void GetStreamingTextAsync_InvalidArgs_Throws() + public async Task GetStreamingTextAsync_InvalidArgs_Throws() { - Assert.Throws("client", () => - { - using var stream = new MemoryStream(); - _ = SpeechToTextClientExtensions.GetStreamingTextAsync(client: null!, new DataContent("data:audio/wav;base64,AQIDBA==")); - }); + ISpeechToTextClient? client = null; + var content = new DataContent("data:audio/wav;base64,AQIDBA=="); + var ex1 = await Assert.ThrowsAsync(() => SpeechToTextClientExtensions.GetStreamingTextAsync(client!, content).GetAsyncEnumerator().MoveNextAsync().AsTask()); + Assert.Equal("client", ex1.ParamName); - Assert.Throws("audioSpeechContent", () => - { - _ = SpeechToTextClientExtensions.GetStreamingTextAsync(new TestSpeechToTextClient(), audioSpeechContent: null!); - }); + using var testClient = new TestSpeechToTextClient(); + DataContent? nullContent = null; + var ex2 = await Assert.ThrowsAsync(() => SpeechToTextClientExtensions.GetStreamingTextAsync(testClient, nullContent!).GetAsyncEnumerator().MoveNextAsync().AsTask()); + Assert.Equal("audioSpeechContent", ex2.ParamName); } [Fact] @@ -63,7 +61,9 @@ public async Task GetStreamingTextAsync_CreatesTextMessageAsync() GetStreamingTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { // For testing, return an async enumerable yielding one streaming update with text "world". - return YieldAsync(new SpeechToTextResponseUpdate { Text = "world" }); + var update = new SpeechToTextResponseUpdate(); + update.Contents.Add(new TextContent("world")); + return YieldAsync(update); }, }; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs index 962c546aeed..f635b42ae0a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -13,7 +13,7 @@ public class SpeechToTextClientTests public async Task GetTextAsync_CreatesTextMessageAsync() { // Arrange - var expectedResponse = new SpeechToTextResponse(new SpeechToTextMessage("hello")); + var expectedResponse = new SpeechToTextResponse("hello"); var expectedOptions = new SpeechToTextOptions(); using var cts = new CancellationTokenSource(); @@ -23,8 +23,7 @@ public async Task GetTextAsync_CreatesTextMessageAsync() { // For the purpose of the test, we assume that the underlying implementation converts the audio speech stream into a transcription choice. // (In a real implementation, the audio speech data would be processed.) - SpeechToTextMessage choice = new("hello"); - return Task.FromResult(new SpeechToTextResponse(choice)); + return Task.FromResult(new SpeechToTextResponse("hello")); }, }; @@ -36,6 +35,6 @@ public async Task GetTextAsync_CreatesTextMessageAsync() cts.Token); // Assert - Assert.Same(expectedResponse.Message.Text, response.Message.Text); + Assert.Equal(expectedResponse.Text, response.Text); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs deleted file mode 100644 index 4df4f0f91ce..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextMessageTests.cs +++ /dev/null @@ -1,353 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class SpeechToTextMessageTests -{ - [Fact] - public void Constructor_Parameterless_PropsDefaulted() - { - SpeechToTextMessage choice = new(); - Assert.Empty(choice.Contents); - Assert.Null(choice.Text); - Assert.NotNull(choice.Contents); - Assert.Same(choice.Contents, choice.Contents); - Assert.Empty(choice.Contents); - Assert.Null(choice.RawRepresentation); - Assert.Null(choice.AdditionalProperties); - Assert.Null(choice.StartTime); - Assert.Null(choice.EndTime); - Assert.Equal(string.Empty, choice.ToString()); - } - - [Theory] - [InlineData(null)] - [InlineData("text")] - public void Constructor_String_PropsRoundtrip(string? text) - { - SpeechToTextMessage choice = new(text); - - Assert.Same(choice.Contents, choice.Contents); - if (text is null) - { - Assert.Empty(choice.Contents); - } - else - { - Assert.Single(choice.Contents); - TextContent tc = Assert.IsType(choice.Contents[0]); - Assert.Equal(text, tc.Text); - } - - Assert.Null(choice.RawRepresentation); - Assert.Null(choice.AdditionalProperties); - Assert.Equal(text ?? string.Empty, choice.ToString()); - } - - [Fact] - public void Constructor_List_InvalidArgs_Throws() - { - Assert.Throws("contents", () => new SpeechToTextMessage((IList)null!)); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - public void Constructor_List_PropsRoundtrip(int choiceCount) - { - List content = []; - for (int i = 0; i < choiceCount; i++) - { - content.Add(new TextContent($"text-{i}")); - } - - SpeechToTextMessage choice = new(content); - - Assert.Same(choice.Contents, choice.Contents); - if (choiceCount == 0) - { - Assert.Empty(choice.Contents); - Assert.Null(choice.Text); - } - else - { - Assert.Equal(choiceCount, choice.Contents.Count); - for (int i = 0; i < choiceCount; i++) - { - TextContent tc = Assert.IsType(choice.Contents[i]); - Assert.Equal($"text-{i}", tc.Text); - } - - Assert.Equal("text-0", choice.Text); - Assert.Equal(string.Concat(Enumerable.Range(0, choiceCount).Select(i => $"text-{i}")), choice.ToString()); - } - - Assert.Null(choice.RawRepresentation); - Assert.Null(choice.AdditionalProperties); - } - - [Fact] - public void Text_GetSet_UsesFirstTextContent() - { - SpeechToTextMessage choice = new( - [ - new DataContent("data:audio/wav;base64,AQIDBA=="), - new DataContent("data:image/png;base64,AQIDBA=="), - new FunctionCallContent("callId1", "fc1"), - new TextContent("text-1"), - new TextContent("text-2"), - new FunctionResultContent("callId1", "result"), - ]); - - TextContent textContent = Assert.IsType(choice.Contents[3]); - Assert.Equal("text-1", textContent.Text); - Assert.Equal("text-1", choice.Text); - Assert.Equal("text-1text-2", choice.ToString()); - - choice.Text = "text-3"; - Assert.Equal("text-3", choice.Text); - Assert.Equal("text-3", choice.Text); - Assert.Same(textContent, choice.Contents[3]); - Assert.Equal("text-3text-2", choice.ToString()); - } - - [Fact] - public void Text_Set_AddsTextToEmptyList() - { - SpeechToTextMessage choice = new([]); - Assert.Empty(choice.Contents); - - choice.Text = "text-1"; - Assert.Equal("text-1", choice.Text); - - Assert.Single(choice.Contents); - TextContent textContent = Assert.IsType(choice.Contents[0]); - Assert.Equal("text-1", textContent.Text); - } - - [Fact] - public void Text_Set_AddsTextToListWithNoText() - { - SpeechToTextMessage choice = new( - [ - new DataContent("data:audio/wav;base64,AQIDBA=="), - new DataContent("data:image/png;base64,AQIDBA=="), - new FunctionCallContent("callId1", "fc1"), - ]); - Assert.Equal(3, choice.Contents.Count); - - choice.Text = "text-1"; - Assert.Equal("text-1", choice.Text); - Assert.Equal(4, choice.Contents.Count); - - choice.Text = "text-2"; - Assert.Equal("text-2", choice.Text); - Assert.Equal(4, choice.Contents.Count); - - choice.Contents.RemoveAt(3); - Assert.Equal(3, choice.Contents.Count); - - choice.Text = "text-3"; - Assert.Equal("text-3", choice.Text); - Assert.Equal(4, choice.Contents.Count); - } - - [Fact] - public void Contents_InitializesToList() - { - // This is an implementation detail, but if this test starts failing, we need to ensure - // tests are in place for whatever possibly-custom implementation of IList is being used. - Assert.IsType>(new SpeechToTextMessage().Contents); - } - - [Fact] - public void Contents_Roundtrips() - { - SpeechToTextMessage choice = new(); - Assert.Empty(choice.Contents); - - List contents = []; - choice.Contents = contents; - - Assert.Same(contents, choice.Contents); - - choice.Contents = contents; - Assert.Same(contents, choice.Contents); - - choice.Contents = null; - Assert.NotNull(choice.Contents); - Assert.NotSame(contents, choice.Contents); - Assert.Empty(choice.Contents); - } - - [Fact] - public void RawRepresentation_Roundtrips() - { - SpeechToTextMessage choice = new(); - Assert.Null(choice.RawRepresentation); - - object raw = new(); - - choice.RawRepresentation = raw; - Assert.Same(raw, choice.RawRepresentation); - - // Ensure the idempotency of setting the same value - choice.RawRepresentation = raw; - Assert.Same(raw, choice.RawRepresentation); - - choice.RawRepresentation = null; - Assert.Null(choice.RawRepresentation); - - choice.RawRepresentation = raw; - Assert.Same(raw, choice.RawRepresentation); - } - - [Fact] - public void AdditionalProperties_Roundtrips() - { - SpeechToTextMessage choice = new(); - Assert.Null(choice.RawRepresentation); - - AdditionalPropertiesDictionary props = []; - - choice.AdditionalProperties = props; - Assert.Same(props, choice.AdditionalProperties); - - // Ensure the idempotency of setting the same value - choice.AdditionalProperties = props; - Assert.Same(props, choice.AdditionalProperties); - - choice.AdditionalProperties = null; - Assert.Null(choice.AdditionalProperties); - - choice.AdditionalProperties = props; - Assert.Same(props, choice.AdditionalProperties); - } - - [Fact] - public void StartTime_Roundtrips() - { - SpeechToTextMessage choice = new(); - Assert.Null(choice.StartTime); - - TimeSpan startTime = TimeSpan.FromSeconds(10); - choice.StartTime = startTime; - Assert.Equal(startTime, choice.StartTime); - - choice.StartTime = null; - Assert.Null(choice.StartTime); - } - - [Fact] - public void EndTime_Roundtrips() - { - SpeechToTextMessage choice = new(); - Assert.Null(choice.EndTime); - - TimeSpan endTime = TimeSpan.FromSeconds(20); - choice.EndTime = endTime; - Assert.Equal(endTime, choice.EndTime); - - choice.EndTime = null; - Assert.Null(choice.EndTime); - } - - [Fact] - public void ItCanBeSerializeAndDeserialized() - { - // Arrange - IList items = - [ - new TextContent("content-1") - { - AdditionalProperties = new() { ["metadata-key-1"] = "metadata-value-1" } - }, - new DataContent(new Uri("data:audio/wav;base64,AQIDBA=="), "mime-type/2") - { - AdditionalProperties = new() { ["metadata-key-2"] = "metadata-value-2" } - }, - new DataContent(new BinaryData(new[] { 1, 2, 3 }, options: TestJsonSerializerContext.Default.Options), "mime-type/3") - { - AdditionalProperties = new() { ["metadata-key-3"] = "metadata-value-3" } - }, - new TextContent("content-4") - { - AdditionalProperties = new() { ["metadata-key-4"] = "metadata-value-4" } - }, - new FunctionCallContent("function-id", "plugin-name-function-name", new Dictionary { ["parameter"] = "argument" }), - new FunctionResultContent("function-id", "function-result"), - ]; - - // Act - var message = JsonSerializer.Serialize(new SpeechToTextMessage(contents: items) - { - Text = "content-1-override", // Override the content of the first text content item that has the "content-1" content - AdditionalProperties = new() { ["choice-metadata-key-1"] = "choice-metadata-value-1" }, - StartTime = TimeSpan.FromSeconds(10), - EndTime = TimeSpan.FromSeconds(20) - }, TestJsonSerializerContext.Default.Options); - - var deserializedChoice = JsonSerializer.Deserialize(message, TestJsonSerializerContext.Default.Options)!; - - // Assert - Assert.NotNull(deserializedChoice.AdditionalProperties); - Assert.Single(deserializedChoice.AdditionalProperties); - Assert.Equal("choice-metadata-value-1", deserializedChoice.AdditionalProperties["choice-metadata-key-1"]?.ToString()); - Assert.Equal(TimeSpan.FromSeconds(10), deserializedChoice.StartTime); - Assert.Equal(TimeSpan.FromSeconds(20), deserializedChoice.EndTime); - - Assert.NotNull(deserializedChoice.Contents); - Assert.Equal(items.Count, deserializedChoice.Contents.Count); - - var textContent = deserializedChoice.Contents[0] as TextContent; - Assert.NotNull(textContent); - Assert.Equal("content-1-override", textContent.Text); - Assert.NotNull(textContent.AdditionalProperties); - Assert.Single(textContent.AdditionalProperties); - Assert.Equal("metadata-value-1", textContent.AdditionalProperties["metadata-key-1"]?.ToString()); - - var dataContent = deserializedChoice.Contents[1] as DataContent; - Assert.NotNull(dataContent); - Assert.Equal("data:mime-type/2;base64,AQIDBA==", dataContent.Uri); - Assert.Equal("mime-type/2", dataContent.MediaType); - Assert.NotNull(dataContent.AdditionalProperties); - Assert.Single(dataContent.AdditionalProperties); - Assert.Equal("metadata-value-2", dataContent.AdditionalProperties["metadata-key-2"]?.ToString()); - - dataContent = deserializedChoice.Contents[2] as DataContent; - Assert.NotNull(dataContent); - Assert.True(dataContent.Data!.Span.SequenceEqual(new BinaryData(new[] { 1, 2, 3 }, TestJsonSerializerContext.Default.Options))); - Assert.Equal("mime-type/3", dataContent.MediaType); - Assert.NotNull(dataContent.AdditionalProperties); - Assert.Single(dataContent.AdditionalProperties); - Assert.Equal("metadata-value-3", dataContent.AdditionalProperties["metadata-key-3"]?.ToString()); - - textContent = deserializedChoice.Contents[3] as TextContent; - Assert.NotNull(textContent); - Assert.Equal("content-4", textContent.Text); - Assert.NotNull(textContent.AdditionalProperties); - Assert.Single(textContent.AdditionalProperties); - Assert.Equal("metadata-value-4", textContent.AdditionalProperties["metadata-key-4"]?.ToString()); - - var functionCallContent = deserializedChoice.Contents[4] as FunctionCallContent; - Assert.NotNull(functionCallContent); - Assert.Equal("plugin-name-function-name", functionCallContent.Name); - Assert.Equal("function-id", functionCallContent.CallId); - Assert.NotNull(functionCallContent.Arguments); - Assert.Single(functionCallContent.Arguments); - Assert.Equal("argument", functionCallContent.Arguments["parameter"]?.ToString()); - - var functionResultContent = deserializedChoice.Contents[5] as FunctionResultContent; - Assert.NotNull(functionResultContent); - Assert.Equal("function-result", functionResultContent.Result?.ToString()); - Assert.Equal("function-id", functionResultContent.CallId); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs index a2813932b91..4221965bb23 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Text.Json; using Xunit; @@ -15,112 +14,142 @@ public class SpeechToTextResponseTests [Fact] public void Constructor_InvalidArgs_Throws() { - Assert.Throws("message", () => new SpeechToTextResponse((SpeechToTextMessage)null!)); - Assert.Throws("choices", () => new SpeechToTextResponse((IList)null!)); + Assert.Throws("contents", () => new SpeechToTextResponse((IList)null!)); } [Fact] - public void Constructor_Choice_Roundtrips() + public void Constructor_Parameterless_PropsDefaulted() { - SpeechToTextMessage choice = new(); - SpeechToTextResponse completion = new(choice); - - // The choice property returns the first (and only) choice. - Assert.Same(choice, completion.Message); - Assert.Same(choice, Assert.Single(completion.Choices)); + SpeechToTextResponse response = new(); + Assert.Empty(response.Contents); + Assert.Null(response.Text); + Assert.NotNull(response.Contents); + Assert.Same(response.Contents, response.Contents); + Assert.Empty(response.Contents); + Assert.Null(response.RawRepresentation); + Assert.Null(response.AdditionalProperties); + Assert.Null(response.StartTime); + Assert.Null(response.EndTime); + Assert.Equal(string.Empty, response.ToString()); } - [Fact] - public void Constructor_Choices_Roundtrips() + [Theory] + [InlineData(null)] + [InlineData("text")] + public void Constructor_String_PropsRoundtrip(string? text) { - List choices = - [ - new SpeechToTextMessage(), - new SpeechToTextMessage(), - new SpeechToTextMessage(), - ]; - - SpeechToTextResponse completion = new(choices); - Assert.Same(choices, completion.Choices); - Assert.Equal(3, choices.Count); - } + SpeechToTextResponse response = new(text); - [Fact] - public void Response_EmptyChoices_Throws() - { - SpeechToTextResponse completion = new([]); - Assert.Empty(completion.Choices); - Assert.Throws(() => completion.Message); - } + Assert.Same(response.Contents, response.Contents); + if (text is null) + { + Assert.Empty(response.Contents); + } + else + { + Assert.Single(response.Contents); + TextContent tc = Assert.IsType(response.Contents[0]); + Assert.Equal(text, tc.Text); + } - [Fact] - public void Response_SingleChoice_Returned() - { - SpeechToTextMessage choice = new(); - SpeechToTextResponse completion = new([choice]); - Assert.Same(choice, completion.Message); - Assert.Same(choice, completion.Choices[0]); + Assert.Null(response.RawRepresentation); + Assert.Null(response.AdditionalProperties); + Assert.Equal(text ?? string.Empty, response.ToString()); } [Fact] - public void Response_MultipleChoices_ReturnsFirst() + public void Constructor_List_InvalidArgs_Throws() { - SpeechToTextMessage first = new(); - SpeechToTextResponse completion = new([ - first, - new SpeechToTextMessage(), - ]); - Assert.Same(first, completion.Message); - Assert.Same(first, completion.Choices[0]); + Assert.Throws("contents", () => new SpeechToTextResponse((IList)null!)); } - [Fact] - public void Choices_SetNull_Throws() + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Constructor_List_PropsRoundtrip(int contentCount) { - SpeechToTextResponse completion = new([]); - Assert.Throws("value", () => completion.Choices = null!); + List content = []; + for (int i = 0; i < contentCount; i++) + { + content.Add(new TextContent($"text-{i}")); + } + + SpeechToTextResponse response = new(content); + + Assert.Same(response.Contents, response.Contents); + if (contentCount == 0) + { + Assert.Empty(response.Contents); + Assert.Null(response.Text); + } + else + { + Assert.Equal(contentCount, response.Contents.Count); + for (int i = 0; i < contentCount; i++) + { + TextContent tc = Assert.IsType(response.Contents[i]); + Assert.Equal($"text-{i}", tc.Text); + } + + Assert.Equal("text-0", response.Text); + Assert.Equal(string.Concat(Enumerable.Range(0, contentCount).Select(i => $"text-{i}")), response.ToString()); + } } [Fact] public void Properties_Roundtrip() { - SpeechToTextResponse completion = new([]); - Assert.Null(completion.ResponseId); - completion.ResponseId = "id"; - Assert.Equal("id", completion.ResponseId); + SpeechToTextResponse response = new(); + Assert.Null(response.ResponseId); + response.ResponseId = "id"; + Assert.Equal("id", response.ResponseId); - Assert.Null(completion.ModelId); - completion.ModelId = "modelId"; - Assert.Equal("modelId", completion.ModelId); + Assert.Null(response.ModelId); + response.ModelId = "modelId"; + Assert.Equal("modelId", response.ModelId); - Assert.Null(completion.RawRepresentation); + Assert.Null(response.RawRepresentation); object raw = new(); - completion.RawRepresentation = raw; - Assert.Same(raw, completion.RawRepresentation); + response.RawRepresentation = raw; + Assert.Same(raw, response.RawRepresentation); - Assert.Null(completion.AdditionalProperties); + Assert.Null(response.AdditionalProperties); AdditionalPropertiesDictionary additionalProps = []; - completion.AdditionalProperties = additionalProps; - Assert.Same(additionalProps, completion.AdditionalProperties); - - List newChoices = [new SpeechToTextMessage(), new SpeechToTextMessage()]; - completion.Choices = newChoices; - Assert.Same(newChoices, completion.Choices); + response.AdditionalProperties = additionalProps; + Assert.Same(additionalProps, response.AdditionalProperties); + + Assert.Null(response.StartTime); + TimeSpan startTime = TimeSpan.FromSeconds(1); + response.StartTime = startTime; + Assert.Equal(startTime, response.StartTime); + + Assert.Null(response.EndTime); + TimeSpan endTime = TimeSpan.FromSeconds(2); + response.EndTime = endTime; + Assert.Equal(endTime, response.EndTime); + + List newContents = [new TextContent("text1"), new TextContent("text2")]; + response.Contents = newContents; + Assert.Same(newContents, response.Contents); } [Fact] public void JsonSerialization_Roundtrips() { - SpeechToTextResponse original = new( - [ - new SpeechToTextMessage("Choice1"), - new SpeechToTextMessage("Choice2"), - new SpeechToTextMessage("Choice3"), - new SpeechToTextMessage("Choice4"), - ]) + SpeechToTextResponse original = new() { + Contents = + [ + new TextContent("Text1"), + new TextContent("Text2"), + new TextContent("Text3"), + new TextContent("Text4"), + ], ResponseId = "id", ModelId = "modelId", + StartTime = TimeSpan.FromSeconds(1), + EndTime = TimeSpan.FromSeconds(2), RawRepresentation = new(), AdditionalProperties = new() { ["key"] = "value" }, }; @@ -130,15 +159,17 @@ public void JsonSerialization_Roundtrips() SpeechToTextResponse? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextResponse); Assert.NotNull(result); - Assert.Equal(4, result.Choices.Count); + Assert.Equal(4, result.Contents.Count); - for (int i = 0; i < original.Choices.Count; i++) + for (int i = 0; i < original.Contents.Count; i++) { - Assert.Equal($"Choice{i + 1}", result.Choices[i].Text); + Assert.Equal($"Text{i + 1}", ((TextContent)result.Contents[i]).Text); } Assert.Equal("id", result.ResponseId); Assert.Equal("modelId", result.ModelId); + Assert.Equal(TimeSpan.FromSeconds(1), result.StartTime); + Assert.Equal(TimeSpan.FromSeconds(2), result.EndTime); Assert.NotNull(result.AdditionalProperties); Assert.Single(result.AdditionalProperties); @@ -148,151 +179,51 @@ public void JsonSerialization_Roundtrips() } [Fact] - public void ToString_SingleChoice_OutputsChoiceText() - { - SpeechToTextResponse completion = new( - [ - new SpeechToTextMessage("This is a test." + Environment.NewLine + "It's multiple lines.") - ]); - Assert.Equal(completion.Choices[0].Text, completion.ToString()); - } - - [Fact] - public void ToString_MultipleChoices_OutputsAllChoicesWithPrefix() + public void ToString_OutputsText() { - SpeechToTextResponse completion = new( - [ - new SpeechToTextMessage("This is a test." + Environment.NewLine + "It's multiple lines."), - new SpeechToTextMessage("So is" + Environment.NewLine + " this."), - new SpeechToTextMessage("And this."), - ]); - - StringBuilder sb = new(); - - for (int i = 0; i < completion.Choices.Count; i++) - { - if (i > 0) - { - sb.AppendLine().AppendLine(); - } - - sb.Append("Choice ").Append(i).AppendLine(":").Append(completion.Choices[i].ToString()); - } - - string expected = sb.ToString(); - Assert.Equal(expected, completion.ToString()); + SpeechToTextResponse response = new("This is a test." + Environment.NewLine + "It's multiple lines."); + Assert.Equal("This is a test." + Environment.NewLine + "It's multiple lines.", response.ToString()); } [Fact] - public void ToSpeechToTextResponseUpdates_SingleChoice_ReturnsExpectedUpdates() + public void ToSpeechToTextResponseUpdates_ReturnsExpectedUpdate() { - // Arrange: create a completion with one choice. - SpeechToTextMessage choice = new("Text") + // Arrange: create a response with contents + SpeechToTextResponse response = new() { - InputIndex = 0, + Contents = + [ + new TextContent("Hello, "), + new DataContent("data:image/png;base64,AQIDBA==", mediaType: "image/png"), + new TextContent("world!") + ], StartTime = TimeSpan.FromSeconds(1), - EndTime = TimeSpan.FromSeconds(2) - }; - - SpeechToTextResponse completion = new(choice) - { + EndTime = TimeSpan.FromSeconds(2), ResponseId = "12345", ModelId = "someModel", AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, }; - // Act: convert to streaming updates. - SpeechToTextResponseUpdate[] updates = completion.ToSpeechToTextResponseUpdates(); + // Act: convert to streaming updates + SpeechToTextResponseUpdate[] updates = response.ToSpeechToTextResponseUpdates(); - // Filter out any null entries (if any). - SpeechToTextResponseUpdate[] nonNullUpdates = updates.Where(u => u is not null).ToArray(); + // Assert: should be a single update with all properties + Assert.Single(updates); - // Our implementation creates one update per choice plus an extra update if AdditionalProperties exists. - Assert.Equal(2, nonNullUpdates.Length); + SpeechToTextResponseUpdate update = updates[0]; + Assert.Equal("12345", update.ResponseId); + Assert.Equal("someModel", update.ModelId); + Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update.Kind); + Assert.Equal(TimeSpan.FromSeconds(1), update.StartTime); + Assert.Equal(TimeSpan.FromSeconds(2), update.EndTime); - SpeechToTextResponseUpdate update0 = nonNullUpdates[0]; - Assert.Equal("12345", update0.ResponseId); - Assert.Equal("someModel", update0.ModelId); - Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update0.Kind); - Assert.Equal(choice.Text, update0.Text); - Assert.Equal(choice.InputIndex, update0.InputIndex); - Assert.Equal(choice.StartTime, update0.StartTime); - Assert.Equal(choice.EndTime, update0.EndTime); - - SpeechToTextResponseUpdate updateExtra = nonNullUpdates[1]; - - // The extra update carries the AdditionalProperties from the completion. - Assert.Null(updateExtra.Text); - Assert.Equal("value1", updateExtra.AdditionalProperties?["key1"]); - Assert.Equal(42, updateExtra.AdditionalProperties?["key2"]); - } - - [Fact] - public void ToSpeechToTextResponseUpdates_MultiChoice_ReturnsExpectedUpdates() - { - // Arrange: create two choices. - SpeechToTextMessage choice1 = new( - [ - new TextContent("Hello, "), - new DataContent("data:image/png;base64,AQIDBA==", mediaType: "image/png"), - new TextContent("world!") - ]) - { - AdditionalProperties = new() { ["choice1Key"] = "choice1Value" }, - InputIndex = 0 - }; - - SpeechToTextMessage choice2 = new( - [ - new FunctionCallContent("call123", "name"), - new FunctionResultContent("call123", 42), - ]) - { - AdditionalProperties = new() { ["choice2Key"] = "choice2Value" }, - InputIndex = 1 - }; - - SpeechToTextResponse completion = new([choice1, choice2]) - { - ResponseId = "12345", - ModelId = "someModel", - AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, - }; + Assert.Equal(3, update.Contents.Count); + Assert.Equal("Hello, ", Assert.IsType(update.Contents[0]).Text); + Assert.Equal("image/png", Assert.IsType(update.Contents[1]).MediaType); + Assert.Equal("world!", Assert.IsType(update.Contents[2]).Text); - // Act: convert to streaming updates. - SpeechToTextResponseUpdate[] updates = completion.ToSpeechToTextResponseUpdates(); - SpeechToTextResponseUpdate[] nonNullUpdates = updates.Where(u => u is not null).ToArray(); - - // Two choices plus an extra update should yield 3 updates. - Assert.Equal(3, nonNullUpdates.Length); - - // Validate update from first choice. - SpeechToTextResponseUpdate update0 = nonNullUpdates[0]; - Assert.Equal("12345", update0.ResponseId); - Assert.Equal("someModel", update0.ModelId); - Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update0.Kind); - Assert.Equal("Hello, ", Assert.IsType(update0.Contents[0]).Text); - Assert.Equal("image/png", Assert.IsType(update0.Contents[1]).MediaType); - Assert.Equal("world!", Assert.IsType(update0.Contents[2]).Text); - Assert.Equal(choice1.InputIndex, update0.InputIndex); - Assert.Equal("choice1Value", update0.AdditionalProperties?["choice1Key"]); - - // Validate update from second choice. - SpeechToTextResponseUpdate update1 = nonNullUpdates[1]; - Assert.Equal("12345", update1.ResponseId); - Assert.Equal("someModel", update1.ModelId); - Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdated, update1.Kind); - - // For choice2 (function call and result), we do not expect a concatenated text. - Assert.True(update1.Contents.Count >= 2); - Assert.IsType(update1.Contents[0]); - Assert.IsType(update1.Contents[1]); - Assert.Equal("choice2Value", update1.AdditionalProperties?["choice2Key"]); - - // Validate the extra update. - SpeechToTextResponseUpdate updateExtra = nonNullUpdates[2]; - Assert.Null(updateExtra.Text); - Assert.Equal("value1", updateExtra.AdditionalProperties?["key1"]); - Assert.Equal(42, updateExtra.AdditionalProperties?["key2"]); + Assert.NotNull(update.AdditionalProperties); + Assert.Equal("value1", update.AdditionalProperties["key1"]); + Assert.Equal(42, update.AdditionalProperties["key2"]); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs index 64c557413b6..b2184870fdd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs @@ -19,7 +19,6 @@ public void Constructor_PropsDefaulted() Assert.Null(update.Text); Assert.Empty(update.Contents); Assert.Null(update.ResponseId); - Assert.Equal(0, update.ChoiceIndex); Assert.Null(update.StartTime); Assert.Null(update.EndTime); Assert.Equal(string.Empty, update.ToString()); @@ -30,13 +29,9 @@ public void Properties_Roundtrip() { SpeechToTextResponseUpdate update = new() { - InputIndex = 5, - ChoiceIndex = 42, Kind = new SpeechToTextResponseUpdateKind("custom"), }; - Assert.Equal(5, update.InputIndex); - Assert.Equal(42, update.ChoiceIndex); Assert.Equal("custom", update.Kind.Value); // Test the computed Text property @@ -137,8 +132,6 @@ public void JsonSerialization_Roundtrips() { SpeechToTextResponseUpdate original = new() { - InputIndex = 7, - ChoiceIndex = 3, Kind = new SpeechToTextResponseUpdateKind("transcribed"), ResponseId = "id123", StartTime = TimeSpan.FromSeconds(5), @@ -154,8 +147,6 @@ public void JsonSerialization_Roundtrips() SpeechToTextResponseUpdate? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextResponseUpdate); Assert.NotNull(result); - Assert.Equal(original.InputIndex, result.InputIndex); - Assert.Equal(original.ChoiceIndex, result.ChoiceIndex); Assert.Equal(original.Kind, result.Kind); Assert.Equal(original.ResponseId, result.ResponseId); Assert.Equal(original.StartTime, result.StartTime); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index ceb4a540162..f0ae6392350 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -38,7 +38,7 @@ public virtual async Task GetTextAsync_SingleAudioRequestMessage() using var audioSpeechStream = GetAudioStream("audio001.mp3"); var response = await _client.GetTextAsync(audioSpeechStream); - Assert.Contains("gym", response.Message.Text, StringComparison.OrdinalIgnoreCase); + Assert.Contains("gym", response.Text, StringComparison.OrdinalIgnoreCase); } [ConditionalFact] diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index aa25422d584..55a49e16499 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -140,11 +140,10 @@ public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, stri Assert.NotNull(response); - Assert.Single(response.Choices); - Assert.Contains("I finally got back to the gym the other day", response.Message.Text); + Assert.Contains("I finally got back to the gym the other day", response.Text); - Assert.NotNull(response.Message.RawRepresentation); - Assert.IsType(response.Message.RawRepresentation); + Assert.NotNull(response.RawRepresentation); + Assert.IsType(response.RawRepresentation); } [Fact] @@ -216,7 +215,6 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu Assert.Contains("I finally got back to the gym the other day", update.Text); Assert.NotNull(update.RawRepresentation); Assert.IsType(update.RawRepresentation); - Assert.Equal(0, update.InputIndex); } } @@ -288,8 +286,7 @@ public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() { Assert.Contains("I finally got back to the gym the other day", update.Text); Assert.NotNull(update.RawRepresentation); - Assert.IsType(update.RawRepresentation); - Assert.Equal(0, update.InputIndex); + Assert.IsType(update.RawRepresentation); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index 1c3c72ae202..0fd9d5174ae 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -58,7 +58,7 @@ public async Task GetTextAsync_LogsResponseInvocationAndCompletion(LogLevel leve { GetTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => { - return Task.FromResult(new SpeechToTextResponse([new("blue whale")])); + return Task.FromResult(new SpeechToTextResponse("blue whale")); }, }; From 3d91982016fae3a039a5df1772873a23de23a025 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:01:34 +0000 Subject: [PATCH 10/27] Remove CultureInfo complexity from language properties --- .../SpeechToText/SpeechToTextOptions.cs | 21 ++++------- .../OpenAISpeechToTextClient.cs | 7 ---- .../SpeechToText/SpeechToTextOptionsTests.cs | 22 ----------- .../ChatClientIntegrationTests.cs | 4 +- .../SpeechToTextClientIntegrationTests.cs | 4 +- .../IntegrationTestHelpers.cs | 1 + .../OpenAISpeechToTextClientTests.cs | 37 ------------------- 7 files changed, 14 insertions(+), 82 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 0b7c9d713f6..7cdd2351218 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Globalization; namespace Microsoft.Extensions.AI; @@ -10,9 +9,6 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class SpeechToTextOptions { - private CultureInfo? _speechLanguage; - private CultureInfo? _textLanguage; - /// Gets or sets the ID for the speech to text. /// Long running jobs may use this ID for status pooling. public string? ResponseId { get; set; } @@ -21,18 +17,15 @@ public class SpeechToTextOptions public string? ModelId { get; set; } /// Gets or sets the language of source speech. - public string? SpeechLanguage - { - get => _speechLanguage?.Name; - set => _speechLanguage = value is null ? null : CultureInfo.GetCultureInfo(value); - } + public string? SpeechLanguage { get; set; } /// Gets or sets the language for the target generated text. - public string? TextLanguage - { - get => _textLanguage?.Name; - set => _textLanguage = value is null ? null : CultureInfo.GetCultureInfo(value); - } + public string? TextLanguage { get; set; } + + /// + /// Gets or sets the prompt to be used for the speech to text request. + /// + public string? Prompt { get; set; } /// Gets or sets the sample rate of the speech input audio. public int? SpeechSampleRate { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index e86aa39ce7a..2e7905a3803 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -126,12 +125,6 @@ static bool IsTranslationRequest(SpeechToTextOptions? options) { _ = Throw.IfNull(options); - // Translation request will be triggered whenever the source language is not specified and a target text language is and different from the output text language - if (CultureInfo.GetCultureInfo(options.TextLanguage!).TwoLetterISOLanguageName != "en") - { - throw new NotSupportedException($"Only translation to english is supported."); - } - var openAIOptions = ToOpenAITranslationOptions(options); AudioTranslation translationResult; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs index 23e6ec86ab1..78a629c7902 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using System.Text.Json; using Xunit; @@ -89,25 +88,4 @@ public void JsonSerialization_Roundtrips() Assert.IsType(value); Assert.Equal("value", ((JsonElement)value!).GetString()); } - - [Theory] - [InlineData(" ")] - [InlineData(" ")] - public void AudioLanguage_InvalidCulture_ThrowsCultureNotFoundException(string invalidCulture) - { - SpeechToTextOptions options = new(); - Assert.Throws(() => options.SpeechLanguage = invalidCulture); - } - - [Fact] - public void AudioLanguage_EmptyString_SetsInvariantCulture() - { - SpeechToTextOptions options = new() - { - SpeechLanguage = string.Empty, - }; - - // InvariantCulture's Name is returned when an empty string is used. - Assert.Equal(CultureInfo.InvariantCulture.Name, options.SpeechLanguage); - } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 6b06b3c0ed2..5f00c9b9c44 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -967,7 +967,9 @@ private static Uri GetImageDataUri() [MemberNotNull(nameof(_chatClient))] protected void SkipIfNotEnabled() { - if (_chatClient is null) + string? skipIntegration = TestRunnerConfiguration.Instance["SkipIntegrationTests"]; + + if (skipIntegration is not null || _chatClient is null) { throw new SkipTestException("Client is not enabled."); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs index f0ae6392350..f0ea6c1790e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/SpeechToTextClientIntegrationTests.cs @@ -73,7 +73,9 @@ private static Stream GetAudioStream(string fileName) [MemberNotNull(nameof(_client))] protected void SkipIfNotEnabled() { - if (_client is null) + string? skipIntegration = TestRunnerConfiguration.Instance["SkipIntegrationTests"]; + + if (skipIntegration is not null || _client is null) { throw new SkipTestException("Client is not enabled."); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs index 4b7252965f0..2a20b121ab0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs @@ -16,6 +16,7 @@ internal static class IntegrationTestHelpers public static OpenAIClient? GetOpenAIClient() { var configuration = TestRunnerConfiguration.Instance; + string? apiKey = configuration["OpenAI:Key"]; if (apiKey is not null) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 55a49e16499..a55a413025c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -218,43 +218,6 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu } } - [Theory] - [InlineData(null, "pt")] - [InlineData(null, "it")] - [InlineData("en", "pt")] - public async Task GetStreamingTextAsync_NonSupportedTranslation_Throws(string? speechLanguage, string? textLanguage) - { - using HttpClient httpClient = new(); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - - using var audioSpeechStream = GetAudioStream(); - var asyncEnumerator = client.GetStreamingTextAsync(audioSpeechStream, new SpeechToTextOptions - { - SpeechLanguage = speechLanguage, - TextLanguage = textLanguage - }).GetAsyncEnumerator(); - - await Assert.ThrowsAsync(() => asyncEnumerator.MoveNextAsync().AsTask()); - } - - [Theory] - [InlineData(null, "pt")] - [InlineData(null, "it")] - [InlineData("en", "pt")] - public async Task GetTextAsync_NonSupportedTranslation_Throws(string? speechLanguage, string? textLanguage) - { - using HttpClient httpClient = new(); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); - - using var audioSpeechStream = GetAudioStream(); - - await Assert.ThrowsAsync(() => client.GetTextAsync(audioSpeechStream, new SpeechToTextOptions - { - SpeechLanguage = speechLanguage, - TextLanguage = textLanguage - })); - } - [Fact] public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() { From 009eecab5e0fd311fd3870b7f55ad5368558e060 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:20:35 +0000 Subject: [PATCH 11/27] Adding Prompt property to options + UT --- .../SpeechToText/SpeechToTextOptions.cs | 5 +-- .../OpenAISpeechToTextClient.cs | 20 ++++----- .../SpeechToText/SpeechToTextClientTests.cs | 45 +++++++++++++++++++ .../SpeechToText/SpeechToTextOptionsTests.cs | 7 +++ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 7cdd2351218..ba41731cceb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -22,9 +22,7 @@ public class SpeechToTextOptions /// Gets or sets the language for the target generated text. public string? TextLanguage { get; set; } - /// - /// Gets or sets the prompt to be used for the speech to text request. - /// + /// Gets or sets the prompt to be used for the speech to text request. public string? Prompt { get; set; } /// Gets or sets the sample rate of the speech input audio. @@ -44,6 +42,7 @@ public virtual SpeechToTextOptions Clone() SpeechLanguage = SpeechLanguage, TextLanguage = TextLanguage, SpeechSampleRate = SpeechSampleRate, + Prompt = Prompt, AdditionalProperties = AdditionalProperties?.Clone(), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 2e7905a3803..c09efe09acf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -215,6 +215,11 @@ private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTe if (options is not null) { + if (options.Prompt is not null) + { + result.Prompt = options.Prompt; + } + if (options.SpeechLanguage is not null) { result.Language = options.SpeechLanguage; @@ -232,11 +237,6 @@ private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTe result.TimestampGranularities = timestampGranularities is AudioTimestampGranularities granularities ? granularities : default; } - if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) - { - result.Prompt = prompt; - } - if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranscriptionFormat? responseFormat)) { result.ResponseFormat = responseFormat; @@ -283,6 +283,11 @@ private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOp if (options is not null) { + if (options.Prompt is not null) + { + result.Prompt = options.Prompt; + } + if (options.AdditionalProperties is { Count: > 0 } additionalProperties) { if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) @@ -290,11 +295,6 @@ private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOp result.Temperature = temperature; } - if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) - { - result.Prompt = prompt; - } - if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranslationFormat? responseFormat)) { result.ResponseFormat = responseFormat; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs index f635b42ae0a..092ad57b2c2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -37,4 +38,48 @@ public async Task GetTextAsync_CreatesTextMessageAsync() // Assert Assert.Equal(expectedResponse.Text, response.Text); } + + [Fact] + public async Task GetStreamingTextAsync_CreatesStreamingUpdatesAsync() + { + // Arrange + var expectedOptions = new SpeechToTextOptions(); + using var cts = new CancellationTokenSource(); + + using TestSpeechToTextClient client = new() + { + GetStreamingTextAsyncCallback = (audioSpeechStream, options, cancellationToken) => + { + // For the purpose of the test, we simulate a streaming response with multiple updates + return GetStreamingUpdatesAsync(); + }, + }; + + // Act – call the extension method with a valid DataContent + List updates = []; + await foreach (var update in SpeechToTextClientExtensions.GetStreamingTextAsync( + client, + new DataContent("data:audio/wav;base64,AQIDBA=="), + expectedOptions, + cts.Token)) + { + updates.Add(update); + } + + // Assert + Assert.Equal(3, updates.Count); + Assert.Equal("hello ", updates[0].Text); + Assert.Equal("world ", updates[1].Text); + Assert.Equal("!", updates[2].Text); + } + + // Helper method to simulate streaming updates +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + private static async IAsyncEnumerable GetStreamingUpdatesAsync() + { + yield return new("hello "); + yield return new("world "); + yield return new("!"); + } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs index 78a629c7902..b48af2bdca7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs @@ -17,6 +17,7 @@ public void Constructor_Parameterless_PropsDefaulted() Assert.Null(options.SpeechLanguage); Assert.Null(options.SpeechSampleRate); Assert.Null(options.AdditionalProperties); + Assert.Null(options.Prompt); SpeechToTextOptions clone = options.Clone(); Assert.Null(clone.ResponseId); @@ -24,6 +25,7 @@ public void Constructor_Parameterless_PropsDefaulted() Assert.Null(clone.SpeechLanguage); Assert.Null(clone.SpeechSampleRate); Assert.Null(clone.AdditionalProperties); + Assert.Null(clone.Prompt); } [Fact] @@ -40,11 +42,13 @@ public void Properties_Roundtrip() options.ModelId = "modelId"; options.SpeechLanguage = "en-US"; options.SpeechSampleRate = 44100; + options.Prompt = "prompt"; options.AdditionalProperties = additionalProps; Assert.Equal("completionId", options.ResponseId); Assert.Equal("modelId", options.ModelId); Assert.Equal("en-US", options.SpeechLanguage); + Assert.Equal("prompt", options.Prompt); Assert.Equal(44100, options.SpeechSampleRate); Assert.Same(additionalProps, options.AdditionalProperties); @@ -52,6 +56,7 @@ public void Properties_Roundtrip() Assert.Equal("completionId", clone.ResponseId); Assert.Equal("modelId", clone.ModelId); Assert.Equal("en-US", clone.SpeechLanguage); + Assert.Equal("prompt", clone.Prompt); Assert.Equal(44100, clone.SpeechSampleRate); Assert.Equal(additionalProps, clone.AdditionalProperties); } @@ -70,6 +75,7 @@ public void JsonSerialization_Roundtrips() options.ModelId = "modelId"; options.SpeechLanguage = "en-US"; options.SpeechSampleRate = 44100; + options.Prompt = "prompt"; options.AdditionalProperties = additionalProps; string json = JsonSerializer.Serialize(options, TestJsonSerializerContext.Default.SpeechToTextOptions); @@ -81,6 +87,7 @@ public void JsonSerialization_Roundtrips() Assert.Equal("modelId", deserialized.ModelId); Assert.Equal("en-US", deserialized.SpeechLanguage); Assert.Equal(44100, deserialized.SpeechSampleRate); + Assert.Equal("prompt", deserialized.Prompt); Assert.NotNull(deserialized.AdditionalProperties); Assert.Single(deserialized.AdditionalProperties); From 305e7e46497d07439749e65b89dd0d8f4c95e93e Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:41:54 +0000 Subject: [PATCH 12/27] Revert global.json changes --- global.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/global.json b/global.json index 20085c8d34e..e13e112eb61 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,6 @@ { "sdk": { - "version": "9.0.104", - "rollForward": "latestFeature", - "allowPrerelease": true + "version": "9.0.104" }, "tools": { "dotnet": "9.0.104", From 1feac6dff9284758967577594325ae7b1e25b129 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:49:26 +0000 Subject: [PATCH 13/27] Add missing experimental --- .../SpeechToText/SpeechToTextResponseUpdateKind.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs index cfae35f94dd..1a3d7b0a474 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateKind.cs @@ -13,6 +13,7 @@ namespace Microsoft.Extensions.AI; /// /// Describes the intended purpose of a specific update during streaming of speech to text updates. /// +[Experimental("MEAI001")] [JsonConverter(typeof(Converter))] public readonly struct SpeechToTextResponseUpdateKind : IEquatable { From 956097d1b559cdc6c7be90c8c560488e46080338 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 30 Mar 2025 02:51:30 +0100 Subject: [PATCH 14/27] Fix UT --- .../OpenAISpeechToTextClientTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index a55a413025c..10ac0a729c3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -279,12 +279,12 @@ public async Task GetTextAsync_NonStronglyTypedOptions_AllSent() using var audioSpeechStream = GetAudioStream(); Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() { + Prompt = "Hide any bad words with ", AdditionalProperties = new() { ["SpeechLanguage"] = "pt", ["Temperature"] = 0.5f, ["TimestampGranularities"] = AudioTimestampGranularities.Segment | AudioTimestampGranularities.Word, - ["Prompt"] = "Hide any bad words with ***", ["ResponseFormat"] = AudioTranscriptionFormat.Vtt, }, })); From 0830a514428856a0d199094efa67cf11ef4ddd0f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 31 Mar 2025 21:58:12 +0100 Subject: [PATCH 15/27] Address PR comments --- .../SpeechToTextClientExtensions.cs | 11 +- .../SpeechToTextClientMetadata.cs | 10 +- .../SpeechToText/SpeechToTextOptions.cs | 9 - .../SpeechToText/SpeechToTextResponse.cs | 27 +- .../SpeechToTextResponseUpdate.cs | 26 +- .../SpeechToTextResponseUpdateExtensions.cs | 28 +- .../Utilities/StreamExtensions.cs | 47 --- .../AsyncEnumerableExtensions.cs | 25 -- .../OpenAIClientExtensions.cs | 17 - .../OpenAISpeechToTextClient.cs | 35 +- .../ConfigureOptionsSpeechToTextClient.cs | 14 +- .../SpeechToText/LoggingSpeechToTextClient.cs | 20 +- .../DataContentAsyncEnumerableStreamTests.cs | 352 ------------------ .../SpeechToTextClientMetadataTests.cs | 4 +- .../SpeechToText/SpeechToTextOptionsTests.cs | 14 - .../DelegatedHttpHandler.cs | 20 - .../OpenAISpeechToTextClientTests.cs | 6 +- 17 files changed, 69 insertions(+), 596 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index 64f43b7b713..e1a4c07638a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -46,7 +47,10 @@ public static async Task GetTextAsync( _ = Throw.IfNull(client); _ = Throw.IfNull(audioSpeechContent); - using var audioSpeechStream = new MemoryStream(audioSpeechContent.Data.ToArray()); + using var audioSpeechStream = MemoryMarshal.TryGetArray(audioSpeechContent.Data, out var array) ? + new MemoryStream(array.Array!, array.Offset, array.Count) : + new MemoryStream(audioSpeechContent.Data.ToArray()); + return await client.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); } @@ -65,7 +69,10 @@ public static async IAsyncEnumerable GetStreamingTex _ = Throw.IfNull(client); _ = Throw.IfNull(audioSpeechContent); - using var audioSpeechStream = new MemoryStream(audioSpeechContent.Data.ToArray()); + using var audioSpeechStream = MemoryMarshal.TryGetArray(audioSpeechContent.Data, out var array) ? + new MemoryStream(array.Array!, array.Offset, array.Count) : + new MemoryStream(audioSpeechContent.Data.ToArray()); + await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)) { yield return update; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs index 165c2f3c470..df39fb7facc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientMetadata.cs @@ -16,10 +16,10 @@ public class SpeechToTextClientMetadata /// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems. /// /// The URL for accessing the speech to text provider, if applicable. - /// The ID of the speech to text model used, if applicable. - public SpeechToTextClientMetadata(string? providerName = null, Uri? providerUri = null, string? modelId = null) + /// The ID of the speech to text used by default, if applicable. + public SpeechToTextClientMetadata(string? providerName = null, Uri? providerUri = null, string? defaultModelId = null) { - ModelId = modelId; + DefaultModelId = defaultModelId; ProviderName = providerName; ProviderUri = providerUri; } @@ -34,10 +34,10 @@ public SpeechToTextClientMetadata(string? providerName = null, Uri? providerUri /// Gets the URL for accessing the speech to text provider. public Uri? ProviderUri { get; } - /// Gets the ID of the model used by this speech to text provider. + /// Gets the ID of the default model used by this speech to text client. /// /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance. /// An individual request may override this value via . /// - public string? ModelId { get; } + public string? DefaultModelId { get; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index ba41731cceb..cb196a4c91c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -9,10 +9,6 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class SpeechToTextOptions { - /// Gets or sets the ID for the speech to text. - /// Long running jobs may use this ID for status pooling. - public string? ResponseId { get; set; } - /// Gets or sets the model ID for the speech to text. public string? ModelId { get; set; } @@ -22,9 +18,6 @@ public class SpeechToTextOptions /// Gets or sets the language for the target generated text. public string? TextLanguage { get; set; } - /// Gets or sets the prompt to be used for the speech to text request. - public string? Prompt { get; set; } - /// Gets or sets the sample rate of the speech input audio. public int? SpeechSampleRate { get; set; } @@ -37,12 +30,10 @@ public virtual SpeechToTextOptions Clone() { SpeechToTextOptions options = new() { - ResponseId = ResponseId, ModelId = ModelId, SpeechLanguage = SpeechLanguage, TextLanguage = TextLanguage, SpeechSampleRate = SpeechSampleRate, - Prompt = Prompt, AdditionalProperties = AdditionalProperties?.Clone(), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs index 7de22b777b2..49ef9df414a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; - using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; +#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators + namespace Microsoft.Extensions.AI; /// Represents the result of an speech to text request. @@ -62,29 +62,12 @@ public SpeechToTextResponse(string? content) /// Gets or sets any additional properties associated with the speech to text completion. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } - /// - /// Gets or sets the text of the first instance in . - /// + /// Gets the text of this speech to text response. /// - /// If there is no instance in , then the getter returns , - /// and the setter adds a new instance with the provided value. + /// This property concatenates the text of all objects in . /// [JsonIgnore] - public string? Text - { - get => Contents.OfType().FirstOrDefault()?.Text; - set - { - if (Contents.OfType().FirstOrDefault() is { } textContent) - { - textContent.Text = value; - } - else if (value is not null) - { - Contents.Add(new TextContent(value)); - } - } - } + public string Text => Contents?.ConcatText() ?? string.Empty; /// public override string ToString() => Contents.ConcatText(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs index eefba114228..004a49278d5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -4,10 +4,11 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; +#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators + namespace Microsoft.Extensions.AI; /// @@ -81,29 +82,12 @@ public SpeechToTextResponseUpdate(string? content) /// Gets or sets additional properties for the update. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } - /// - /// Gets or sets the text of the first instance in . - /// + /// Gets the text of this speech to text response. /// - /// If there is no instance in , then the getter returns , - /// and the setter adds a new instance with the provided value. + /// This property concatenates the text of all objects in . /// [JsonIgnore] - public string? Text - { - get => Contents.OfType().FirstOrDefault()?.Text; - set - { - if (Contents.OfType().FirstOrDefault() is { } textContent) - { - textContent.Text = value; - } - else if (value is not null) - { - Contents.Add(new TextContent(value)); - } - } - } + public string Text => Contents?.ConcatText() ?? string.Empty; /// Gets or sets the generated content items. [AllowNull] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index 6ecb53b8db2..f5d71a2ef14 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -18,14 +18,9 @@ public static class SpeechToTextResponseUpdateExtensions { /// Combines instances into a single . /// The updates to be combined. - /// - /// to attempt to coalesce contiguous items, where applicable, - /// into a single . When , the original content items are used. - /// The default is . - /// /// The combined . public static SpeechToTextResponse ToSpeechToTextResponse( - this IEnumerable updates, bool coalesceContent = true) + this IEnumerable updates) { _ = Throw.IfNull(updates); @@ -41,10 +36,7 @@ public static SpeechToTextResponse ToSpeechToTextResponse( ProcessUpdate(update, contents, ref responseId, ref modelId, ref rawRepresentation, ref additionalProperties); } - if (coalesceContent) - { - ChatResponseExtensions.CoalesceTextContent(contents); - } + ChatResponseExtensions.CoalesceTextContent(contents); response.Contents = contents; response.ResponseId = responseId; @@ -57,22 +49,17 @@ public static SpeechToTextResponse ToSpeechToTextResponse( /// Combines instances into a single . /// The updates to be combined. - /// - /// to attempt to coalesce contiguous items, where applicable, - /// into a single . When , the original content items are used. - /// The default is . - /// /// The to monitor for cancellation requests. The default is . /// The combined . public static Task ToSpeechToTextResponseAsync( - this IAsyncEnumerable updates, bool coalesceContent = true, CancellationToken cancellationToken = default) + this IAsyncEnumerable updates, CancellationToken cancellationToken = default) { _ = Throw.IfNull(updates); - return ToResponseAsync(updates, coalesceContent, cancellationToken); + return ToResponseAsync(updates, cancellationToken); static async Task ToResponseAsync( - IAsyncEnumerable updates, bool coalesceContent, CancellationToken cancellationToken) + IAsyncEnumerable updates, CancellationToken cancellationToken) { SpeechToTextResponse response = new(); List contents = []; @@ -86,10 +73,7 @@ static async Task ToResponseAsync( ProcessUpdate(update, contents, ref responseId, ref modelId, ref rawRepresentation, ref additionalProperties); } - if (coalesceContent) - { - ChatResponseExtensions.CoalesceTextContent(contents); - } + ChatResponseExtensions.CoalesceTextContent(contents); response.Contents = contents; response.ResponseId = responseId; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs deleted file mode 100644 index 98f402a722f..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/StreamExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - -namespace Microsoft.Extensions.AI; - -/// Provides extension methods for . -public static class StreamExtensions -{ - /// Converts a to an . - /// The to convert. - /// The optional media type of the audio stream. - /// The optional buffer size to use when reading from the audio stream. The default is 4096. - /// The to monitor for cancellation requests. The default is . - /// An . - public static async IAsyncEnumerable ToAsyncEnumerable( - this Stream stream, - string mediaType = "audio/*", - int bufferSize = 4096, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - int bytesRead; -#if NET8_0_OR_GREATER - Memory buffer = new byte[bufferSize]; - while ((bytesRead = await Throw.IfNull(stream).ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) - { - yield return new DataContent(buffer.Slice(0, bytesRead), mediaType)!; - } -#else - var buffer = new byte[bufferSize]; - while ((bytesRead = await Throw.IfNull(stream).ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) - { - byte[] chunk = new byte[bytesRead]; - Array.Copy(buffer, 0, chunk, 0, bytesRead); - yield return new DataContent((ReadOnlyMemory)chunk, mediaType)!; - } -#endif - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs deleted file mode 100644 index ed8c2c0738a..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/AsyncEnumerableExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// Provides extension methods for . -internal static class AsyncEnumerableExtensions -{ - /// Converts an to a . - /// The data content async enumerable to convert. - /// The first data content chunk to write back to the stream. - /// The to monitor for cancellation requests. The default is . - /// The stream containing the data content. - /// - /// needs to be considered back in the stream if was iterated before creating the stream. - /// This can happen to check if the first enumerable item contains data or is just a reference only content. - /// - internal static Stream ToStream(this IAsyncEnumerable dataAsyncEnumerable, DataContent? firstDataContent = null, CancellationToken cancellationToken = default) - => new DataContentAsyncEnumerableStream(Throw.IfNull(dataAsyncEnumerable), firstDataContent, cancellationToken); -} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 8d906cea2b4..6b330e4da00 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,10 +3,8 @@ using System; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.Diagnostics; using OpenAI; -using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; using OpenAI.Responses; @@ -37,21 +35,6 @@ public static IChatClient AsIChatClient(this ChatClient chatClient) => public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => new OpenAIResponseChatClient(responseClient); - /// Gets an for use with this . - /// The client. - /// The model. - /// An that can be used to transcribe audio via the . - [Experimental("MEAI001")] - public static ISpeechToTextClient AsSpeechToTextClient(this OpenAIClient openAIClient, string modelId) => - new OpenAISpeechToTextClient(openAIClient, modelId); - - /// Gets an for use with this . - /// The client. - /// An that can be used to transcribe audio via the . - [Experimental("MEAI001")] - public static ISpeechToTextClient AsSpeechToTextClient(this AudioClient audioClient) => - new OpenAISpeechToTextClient(audioClient); - /// Gets an for use with this . /// The client. /// The model to use. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index c09efe09acf..766644421e4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -97,15 +97,10 @@ public async IAsyncEnumerable GetStreamingTextAsync( var speechResponse = await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); - yield return new SpeechToTextResponseUpdate(speechResponse.Contents) + foreach (var update in speechResponse.ToSpeechToTextResponseUpdates()) { - Kind = SpeechToTextResponseUpdateKind.TextUpdated, - RawRepresentation = speechResponse.RawRepresentation, - StartTime = speechResponse.StartTime, - EndTime = speechResponse.EndTime, - ResponseId = speechResponse.ResponseId, - ModelId = speechResponse.ModelId - }; + yield return update; + } } /// @@ -198,7 +193,7 @@ private static void UpdateResponseFromOpenAIAudioTranscription(SpeechToTextRespo // Update the response response.RawRepresentation = audioTranscription; - response.Text = audioTranscription.Text; + response.Contents = [new TextContent(audioTranscription.Text)]; response.StartTime = startTime; response.EndTime = endTime; response.AdditionalProperties = new AdditionalPropertiesDictionary @@ -215,11 +210,6 @@ private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTe if (options is not null) { - if (options.Prompt is not null) - { - result.Prompt = options.Prompt; - } - if (options.SpeechLanguage is not null) { result.Language = options.SpeechLanguage; @@ -241,6 +231,11 @@ private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTe { result.ResponseFormat = responseFormat; } + + if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) + { + result.Prompt = prompt; + } } } @@ -266,7 +261,7 @@ private static void UpdateResponseFromOpenAIAudioTranslation(SpeechToTextRespons // Update the response response.RawRepresentation = audioTranslation; - response.Text = audioTranslation.Text; + response.Contents = [new TextContent(audioTranslation.Text)]; response.StartTime = startTime; response.EndTime = endTime; response.AdditionalProperties = new AdditionalPropertiesDictionary @@ -283,11 +278,6 @@ private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOp if (options is not null) { - if (options.Prompt is not null) - { - result.Prompt = options.Prompt; - } - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) { if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) @@ -299,6 +289,11 @@ private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOp { result.ResponseFormat = responseFormat; } + + if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) + { + result.Prompt = prompt; + } } } diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs index 2e768a58e6c..85833a3c171 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/ConfigureOptionsSpeechToTextClient.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -36,17 +37,20 @@ public ConfigureOptionsSpeechToTextClient(ISpeechToTextClient innerClient, Actio } /// - public override Task GetTextAsync( + public override async Task GetTextAsync( Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) { - return base.GetTextAsync(audioSpeechStream, Configure(options), cancellationToken); + return await base.GetTextAsync(audioSpeechStream, Configure(options), cancellationToken).ConfigureAwait(false); } /// - public override IAsyncEnumerable GetStreamingTextAsync( - Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + public override async IAsyncEnumerable GetStreamingTextAsync( + Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - return base.GetStreamingTextAsync(audioSpeechStream, Configure(options), cancellationToken); + await foreach (var update in base.GetStreamingTextAsync(audioSpeechStream, Configure(options), cancellationToken).ConfigureAwait(false)) + { + yield return update; + } } /// Creates and configures the to pass along to the inner client. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index 52f690169ff..6299d38663e 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -53,7 +53,7 @@ public override async Task GetTextAsync( { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetTextAsync), "[audio speech stream]", AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetTextAsync), AsJson(options), AsJson(this.GetService())); } else { @@ -63,13 +63,13 @@ public override async Task GetTextAsync( try { - var completion = await base.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); + var response = await base.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { - LogCompletedSensitive(nameof(GetTextAsync), AsJson(completion)); + LogCompletedSensitive(nameof(GetTextAsync), AsJson(response)); } else { @@ -77,7 +77,7 @@ public override async Task GetTextAsync( } } - return completion; + return response; } catch (OperationCanceledException) { @@ -99,7 +99,7 @@ public override async IAsyncEnumerable GetStreamingT { if (_logger.IsEnabled(LogLevel.Trace)) { - LogInvokedSensitive(nameof(GetStreamingTextAsync), "[audio speech stream]", AsJson(options), AsJson(this.GetService())); + LogInvokedSensitive(nameof(GetStreamingTextAsync), AsJson(options), AsJson(this.GetService())); } else { @@ -176,19 +176,19 @@ public override async IAsyncEnumerable GetStreamingT [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] private partial void LogInvoked(string methodName); - [LoggerMessage(LogLevel.Trace, $"{{MethodName}} invoked: Audio contents: {{AudioContents}}. Options: {{{nameof(AI.SpeechToTextOptions)}}}. Metadata: {{{nameof(AI.SpeechToTextClientMetadata)}}}.")] - private partial void LogInvokedSensitive(string methodName, string audioContents, string SpeechToTextOptions, string SpeechToTextClientMetadata); + [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: Options: {SpeechToTextOptions}. Metadata: {SpeechToTextClientMetadata}.")] + private partial void LogInvokedSensitive(string methodName, string speechToTextOptions, string speechToTextClientMetadata); [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] private partial void LogCompleted(string methodName); - [LoggerMessage(LogLevel.Trace, $"{{MethodName}} completed: {{{nameof(AI.SpeechToTextResponse)}}}.")] + [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {SpeechToTextResponse}.")] private partial void LogCompletedSensitive(string methodName, string SpeechToTextResponse); - [LoggerMessage(LogLevel.Debug, $"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update.")] + [LoggerMessage(LogLevel.Debug, "GetStreamingTextAsync received update.")] private partial void LogStreamingUpdate(); - [LoggerMessage(LogLevel.Trace, $"{nameof(ISpeechToTextClient.GetStreamingTextAsync)} received update: {{{nameof(SpeechToTextResponseUpdate)}}}")] + [LoggerMessage(LogLevel.Trace, "GetStreamingTextAsync received update: {SpeechToTextResponseUpdate}")] private partial void LogStreamingUpdateSensitive(string speechToTextResponseUpdate); [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs deleted file mode 100644 index 19929db49f8..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/DataContentAsyncEnumerableStreamTests.cs +++ /dev/null @@ -1,352 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class DataContentAsyncEnumerableStreamTests -{ - [Fact] - public void Constructor_InvalidArgs_Throws() - { - // Expect ArgumentNullException if source is null. - Assert.Throws("dataAsyncEnumerable", () => new DataContentAsyncEnumerableStreamImpl(null!)); - } - - [Fact] - public async Task ReadAsync_EmptySource_ReturnsEmpty() - { - var source = GetAsyncEnumerable(Array.Empty()); - using var stream = new DataContentAsyncEnumerableStreamImpl(source); - - byte[] buffer = new byte[10]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(0, bytesRead); - } - - [Fact] - public async Task ReadAsync_SingleChunk_ReturnsChunk() - { - var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); - using var stream = new DataContentAsyncEnumerableStreamImpl(source); - - byte[] buffer = new byte[10]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - - Assert.Equal(5, bytesRead); - Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 0, 0, 0, 0, 0 }, buffer); - } - - [Fact] - public async Task ReadAsync_MultipleChunks_ReturnsConcatenatedChunks() - { - var source = GetAsyncEnumerable(new[] - { - new byte[] { 1,2,3 }, - new byte[] { 4,5,6 }, - new byte[] { 7,8,9 } - }); - using var stream = new DataContentAsyncEnumerableStreamImpl(source); - - byte[] buffer = new byte[10]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(9, bytesRead); - Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, buffer); - } - - [Fact] - public async Task ReadAsync_BufferTooSmall_ReturnsPartialChunk() - { - var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); - using var stream = new DataContentAsyncEnumerableStreamImpl(source); - - byte[] buffer = new byte[3]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 1, 2, 3 }, buffer); - } - - [Fact] - public async Task ReadAsync_BufferTooSmall_MultipleReads() - { - var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); - using var stream = new DataContentAsyncEnumerableStreamImpl(source); - - byte[] buffer = new byte[3]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 1, 2, 3 }, buffer); - - bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(2, bytesRead); - - // The third byte in the buffer remains from the previous read. - Assert.Equal(new byte[] { 4, 5, 3 }, buffer); - } - - [Fact] - public async Task ReadAsync_WithFirstDataContent_ReturnsFirstDataContentThenSource() - { - var firstDataContentBytes = new byte[] { 0, 0, 0, 0, 0 }; - var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); - - // Pass a firstDataContent as a DataContent instance. - using var stream = new DataContentAsyncEnumerableStreamImpl(source, new DataContent(firstDataContentBytes, "application/octet-stream")); - - byte[] buffer = new byte[10]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(10, bytesRead); - Assert.Equal(new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5 }, buffer); - } - - [Fact] - public void Constructor_WithFirstDataContent_InitializesCorrectly() - { - var firstDataContentBytes = new byte[] { 0, 0, 0, 0, 0 }; - var source = GetAsyncEnumerable(new[] { new byte[] { 1, 2, 3, 4, 5 } }); - using var stream = new DataContentAsyncEnumerableStreamImpl(source, new DataContent(firstDataContentBytes, "application/octet-stream")); - Assert.NotNull(stream); - } - - [Fact] - public async Task ReadAsync_EmptySource_WithFirstDataContent_ReturnsFirstDataContent() - { - var firstDataContentBytes = new byte[] { 0, 0, 0, 0, 0 }; - var source = GetAsyncEnumerable(Array.Empty()); - using var stream = new DataContentAsyncEnumerableStreamImpl(source, new DataContent(firstDataContentBytes, "application/octet-stream")); - - byte[] buffer = new byte[10]; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - Assert.Equal(5, bytesRead); - Assert.Equal(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, buffer); - } - - private static async IAsyncEnumerable GetAsyncEnumerable(IEnumerable chunks) - { - foreach (var chunk in chunks) - { - await Task.Yield(); - yield return new DataContent(chunk, "application/octet-stream"); - } - } - - /// - /// Utility class to stream data content as a . - /// - /// The type of data content to stream. -#if !NET8_0_OR_GREATER - internal sealed class DataContentAsyncEnumerableStreamImpl : Stream, IAsyncDisposable -#else - internal sealed class DataContentAsyncEnumerableStreamImpl : Stream -#endif - where T : DataContent - { - private readonly IAsyncEnumerator _enumerator; - private bool _isCompleted; - private byte[] _remainingData; - private int _remainingDataOffset; - private long _position; - private T? _firstDataContent; - - /// - /// Initializes a new instance of the class, where T is . - /// - /// The async enumerable to stream. - /// The first chunk of data to reconsider when reading. - /// The to monitor for cancellation requests. The default is . - /// - /// needs to be considered back in the stream if was iterated before creating the stream. - /// This can happen to check if the first enumerable item contains data or is just a reference only content. - /// - internal DataContentAsyncEnumerableStreamImpl(IAsyncEnumerable dataAsyncEnumerable, T? firstDataContent = null, CancellationToken cancellationToken = default) - { - if (dataAsyncEnumerable is null) - { - throw new ArgumentNullException(nameof(dataAsyncEnumerable)); - } - - _enumerator = dataAsyncEnumerable.GetAsyncEnumerator(cancellationToken); - _remainingData = Array.Empty(); - _remainingDataOffset = 0; - _position = 0; - _firstDataContent = firstDataContent; - } - - /// - public override bool CanRead => true; - - /// - public override bool CanSeek => false; - - /// - public override bool CanWrite => false; - - /// - public override long Length => throw new NotSupportedException(); - - /// - public override long Position - { - get => _position; - set => throw new NotSupportedException(); - } - - /// - public override void Flush() => throw new NotSupportedException(); - - /// - public override long Seek(long offset, SeekOrigin origin) => - throw new NotSupportedException(); - - /// - public override void SetLength(long value) => - throw new NotSupportedException(); - - /// - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("Use ReadAsync instead for asynchronous reading."); - } - - /// -#if NET8_0_OR_GREATER - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => ReadAsync(buffer, cancellationToken).AsTask(); -#else - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (_isCompleted) - { - return 0; - } - - int bytesRead = 0; - - while (bytesRead < count) - { - if (_remainingDataOffset < _remainingData.Length) - { - int bytesToCopy = Math.Min(count - bytesRead, _remainingData.Length - _remainingDataOffset); - Array.Copy(_remainingData, _remainingDataOffset, buffer, offset + bytesRead, bytesToCopy); - _remainingDataOffset += bytesToCopy; - bytesRead += bytesToCopy; - _position += bytesToCopy; - } - else - { - // Special case when the first chunk was skipped and needs to be read - if (_position == 0 && _firstDataContent is not null) - { - _remainingData = _firstDataContent.Data.ToArray(); - _remainingDataOffset = 0; - - continue; - } - - if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) - { - _isCompleted = true; - break; - } - - _remainingData = _enumerator.Current.Data.ToArray(); - _remainingDataOffset = 0; - } - } - - return bytesRead; - } -#endif - -#if NET8_0_OR_GREATER - /// - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - if (_isCompleted) - { - return 0; - } - - int bytesRead = 0; - int totalToRead = buffer.Length; - - while (bytesRead < totalToRead) - { - // If there's still data in the current chunk - if (_remainingDataOffset < _remainingData.Length) - { - int bytesToCopy = Math.Min(totalToRead - bytesRead, _remainingData.Length - _remainingDataOffset); - _remainingData.AsSpan(_remainingDataOffset, bytesToCopy) - .CopyTo(buffer.Span.Slice(bytesRead, bytesToCopy)); - - _remainingDataOffset += bytesToCopy; - bytesRead += bytesToCopy; - _position += bytesToCopy; - } - else - { - // If the first chunk was never read, attempt to read it now - if (_position == 0 && _firstDataContent is not null) - { - _remainingData = _firstDataContent.Data.ToArray(); - _remainingDataOffset = 0; - continue; - } - - // Move to the next chunk in the async enumerator - if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) - { - _isCompleted = true; - break; - } - - _remainingData = _enumerator.Current.Data.ToArray(); - _remainingDataOffset = 0; - } - } - - return bytesRead; - } -#endif - -#if NET8_0_OR_GREATER - /// - public override async ValueTask DisposeAsync() - { - await _enumerator.DisposeAsync().ConfigureAwait(false); - - await base.DisposeAsync().ConfigureAwait(false); - } -#else - public async ValueTask DisposeAsync() - { - await _enumerator.DisposeAsync().ConfigureAwait(false); - - Dispose(); - } -#endif - - /// - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - var task = Task.Run(_enumerator.DisposeAsync); - } - - base.Dispose(disposing); - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs index da3b2272b05..c9081d0adb6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextClientMetadataTests.cs @@ -14,7 +14,7 @@ public void Constructor_NullValues_AllowedAndRoundtrip() SpeechToTextClientMetadata metadata = new(null, null, null); Assert.Null(metadata.ProviderName); Assert.Null(metadata.ProviderUri); - Assert.Null(metadata.ModelId); + Assert.Null(metadata.DefaultModelId); } [Fact] @@ -24,6 +24,6 @@ public void Constructor_Value_Roundtrips() SpeechToTextClientMetadata metadata = new("providerName", uri, "theModel"); Assert.Equal("providerName", metadata.ProviderName); Assert.Same(uri, metadata.ProviderUri); - Assert.Equal("theModel", metadata.ModelId); + Assert.Equal("theModel", metadata.DefaultModelId); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs index b48af2bdca7..20936fd4517 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs @@ -12,20 +12,16 @@ public class SpeechToTextOptionsTests public void Constructor_Parameterless_PropsDefaulted() { SpeechToTextOptions options = new(); - Assert.Null(options.ResponseId); Assert.Null(options.ModelId); Assert.Null(options.SpeechLanguage); Assert.Null(options.SpeechSampleRate); Assert.Null(options.AdditionalProperties); - Assert.Null(options.Prompt); SpeechToTextOptions clone = options.Clone(); - Assert.Null(clone.ResponseId); Assert.Null(clone.ModelId); Assert.Null(clone.SpeechLanguage); Assert.Null(clone.SpeechSampleRate); Assert.Null(clone.AdditionalProperties); - Assert.Null(clone.Prompt); } [Fact] @@ -38,25 +34,19 @@ public void Properties_Roundtrip() ["key"] = "value", }; - options.ResponseId = "completionId"; options.ModelId = "modelId"; options.SpeechLanguage = "en-US"; options.SpeechSampleRate = 44100; - options.Prompt = "prompt"; options.AdditionalProperties = additionalProps; - Assert.Equal("completionId", options.ResponseId); Assert.Equal("modelId", options.ModelId); Assert.Equal("en-US", options.SpeechLanguage); - Assert.Equal("prompt", options.Prompt); Assert.Equal(44100, options.SpeechSampleRate); Assert.Same(additionalProps, options.AdditionalProperties); SpeechToTextOptions clone = options.Clone(); - Assert.Equal("completionId", clone.ResponseId); Assert.Equal("modelId", clone.ModelId); Assert.Equal("en-US", clone.SpeechLanguage); - Assert.Equal("prompt", clone.Prompt); Assert.Equal(44100, clone.SpeechSampleRate); Assert.Equal(additionalProps, clone.AdditionalProperties); } @@ -71,11 +61,9 @@ public void JsonSerialization_Roundtrips() ["key"] = "value", }; - options.ResponseId = "completionId"; options.ModelId = "modelId"; options.SpeechLanguage = "en-US"; options.SpeechSampleRate = 44100; - options.Prompt = "prompt"; options.AdditionalProperties = additionalProps; string json = JsonSerializer.Serialize(options, TestJsonSerializerContext.Default.SpeechToTextOptions); @@ -83,11 +71,9 @@ public void JsonSerialization_Roundtrips() SpeechToTextOptions? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.SpeechToTextOptions); Assert.NotNull(deserialized); - Assert.Equal("completionId", deserialized.ResponseId); Assert.Equal("modelId", deserialized.ModelId); Assert.Equal("en-US", deserialized.SpeechLanguage); Assert.Equal(44100, deserialized.SpeechSampleRate); - Assert.Equal("prompt", deserialized.Prompt); Assert.NotNull(deserialized.AdditionalProperties); Assert.Single(deserialized.AdditionalProperties); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs deleted file mode 100644 index d0495795937..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/DelegatedHttpHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI; - -/// -/// An that checks the request body against an expected one -/// and sends back an expected response. -/// -public sealed class DelegatedHttpHandler( - Func> sendAsyncDelegate) : HttpMessageHandler -{ - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - => sendAsyncDelegate(request, cancellationToken); -} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 10ac0a729c3..1d204053389 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -50,14 +50,14 @@ public void AsSpeechToTextClient_OpenAIClient_ProducesExpectedMetadata(bool useA Assert.NotNull(metadata); Assert.Equal("openai", metadata.ProviderName); Assert.Equal(endpoint, metadata.ProviderUri); - Assert.Equal(model, metadata.ModelId); + Assert.Equal(model, metadata.DefaultModelId); client = openAIClient.GetAudioClient(model).AsSpeechToTextClient(); metadata = client.GetService(); Assert.NotNull(metadata); Assert.Equal("openai", metadata.ProviderName); Assert.Equal(endpoint, metadata.ProviderUri); - Assert.Equal(model, metadata.ModelId); + Assert.Equal(model, metadata.DefaultModelId); } [Fact] @@ -279,9 +279,9 @@ public async Task GetTextAsync_NonStronglyTypedOptions_AllSent() using var audioSpeechStream = GetAudioStream(); Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() { - Prompt = "Hide any bad words with ", AdditionalProperties = new() { + ["Prompt"] = "Hide any bad words with ", ["SpeechLanguage"] = "pt", ["Temperature"] = 0.5f, ["TimestampGranularities"] = AudioTimestampGranularities.Segment | AudioTimestampGranularities.Word, From 72407f228775b06304d9c7501ed275671a27e20e Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:49:01 +0100 Subject: [PATCH 16/27] Fix unit tests --- .../DelegatingSpeechToTextClientTests.cs | 4 +- .../SpeechToText/SpeechToTextResponseTests.cs | 6 +- .../SpeechToTextResponseUpdateTests.cs | 55 ++----------------- 3 files changed, 9 insertions(+), 56 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs index c9535b4fd81..ef4da7f94bd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/DelegatingSpeechToTextClientTests.cs @@ -59,8 +59,8 @@ public async Task GetStreamingTextAsyncDefaultsToInnerClientAsync() var expectedCancellationToken = CancellationToken.None; SpeechToTextResponseUpdate[] expectedResults = [ - new() { Text = "Text update 1" }, - new() { Text = "Text update 2" } + new("Text update 1"), + new("Text update 2") ]; using var inner = new TestSpeechToTextClient diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs index 4221965bb23..33b27b01291 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs @@ -22,7 +22,7 @@ public void Constructor_Parameterless_PropsDefaulted() { SpeechToTextResponse response = new(); Assert.Empty(response.Contents); - Assert.Null(response.Text); + Assert.Empty(response.Text); Assert.NotNull(response.Contents); Assert.Same(response.Contents, response.Contents); Assert.Empty(response.Contents); @@ -81,7 +81,7 @@ public void Constructor_List_PropsRoundtrip(int contentCount) if (contentCount == 0) { Assert.Empty(response.Contents); - Assert.Null(response.Text); + Assert.Empty(response.Text); } else { @@ -92,7 +92,7 @@ public void Constructor_List_PropsRoundtrip(int contentCount) Assert.Equal($"text-{i}", tc.Text); } - Assert.Equal("text-0", response.Text); + Assert.Equal(string.Concat(Enumerable.Range(0, contentCount).Select(i => $"text-{i}")), response.Text); Assert.Equal(string.Concat(Enumerable.Range(0, contentCount).Select(i => $"text-{i}")), response.ToString()); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs index b2184870fdd..0eae376070e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateTests.cs @@ -16,7 +16,7 @@ public void Constructor_PropsDefaulted() SpeechToTextResponseUpdate update = new(); Assert.Equal(SpeechToTextResponseUpdateKind.TextUpdating, update.Kind); - Assert.Null(update.Text); + Assert.Empty(update.Text); Assert.Empty(update.Contents); Assert.Null(update.ResponseId); Assert.Null(update.StartTime); @@ -35,9 +35,7 @@ public void Properties_Roundtrip() Assert.Equal("custom", update.Kind.Value); // Test the computed Text property - Assert.Null(update.Text); - update.Text = "sample text"; - Assert.Equal("sample text", update.Text); + Assert.Empty(update.Text); // Contents: assigning a new list then resetting to null should yield an empty list. List newList = new(); @@ -58,7 +56,7 @@ public void Properties_Roundtrip() } [Fact] - public void Text_GetSet_UsesFirstTextContent() + public void Text_Get_UsesFirstTextContent() { SpeechToTextResponseUpdate update = new( [ @@ -73,58 +71,13 @@ public void Text_GetSet_UsesFirstTextContent() // The getter returns the text of the first TextContent (which is at index 3). TextContent textContent = Assert.IsType(update.Contents[3]); Assert.Equal("text-1", textContent.Text); - Assert.Equal("text-1", update.Text); + Assert.Equal("text-1text-2", update.Text); // Assume the ToString concatenates the text of all TextContent items. Assert.Equal("text-1text-2", update.ToString()); - update.Text = "text-3"; - Assert.Equal("text-3", update.Text); - // The setter should update the first TextContent item. Assert.Same(textContent, update.Contents[3]); - Assert.Equal("text-3text-2", update.ToString()); - } - - [Fact] - public void Text_Set_AddsTextMessageToEmptyList() - { - SpeechToTextResponseUpdate update = new(); - Assert.Empty(update.Contents); - - update.Text = "text-1"; - Assert.Equal("text-1", update.Text); - - Assert.Single(update.Contents); - TextContent textContent = Assert.IsType(update.Contents[0]); - Assert.Equal("text-1", textContent.Text); - } - - [Fact] - public void Text_Set_AddsTextMessageToListWithNoText() - { - SpeechToTextResponseUpdate update = new( - [ - new DataContent("data:image/wav;base64,AQIDBA==", "application/octet-stream"), - new DataContent("data:audio/wav;base64,AQIDBA==", "application/octet-stream"), - new FunctionCallContent("callId1", "fc1"), - ]); - Assert.Equal(3, update.Contents.Count); - - update.Text = "text-1"; - Assert.Equal("text-1", update.Text); - Assert.Equal(4, update.Contents.Count); - - update.Text = "text-2"; - Assert.Equal("text-2", update.Text); - Assert.Equal(4, update.Contents.Count); - - update.Contents.RemoveAt(3); - Assert.Equal(3, update.Contents.Count); - - update.Text = "text-3"; - Assert.Equal("text-3", update.Text); - Assert.Equal(4, update.Contents.Count); } [Fact] From 3c7e4ae1a68f4e833331601b3a71e757695de245 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:08:04 +0100 Subject: [PATCH 17/27] Fix UT --- .../OpenAISpeechToTextClient.cs | 2 +- ...penAISpeechToTextClientIntegrationTests.cs | 14 ++- .../OpenAISpeechToTextClientTests.cs | 95 +------------------ .../LoggingSpeechToTextClientTests.cs | 4 +- 4 files changed, 17 insertions(+), 98 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 766644421e4..fe2d2497ec5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -20,7 +20,7 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . [Experimental("MEAI001")] -internal sealed class OpenAISpeechToTextClient : ISpeechToTextClient +public sealed class OpenAISpeechToTextClient : ISpeechToTextClient { /// Default OpenAI endpoint. private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1"); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs index a22db54a65d..a385c6db536 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs @@ -6,6 +6,16 @@ namespace Microsoft.Extensions.AI; public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegrationTests { protected override ISpeechToTextClient? CreateClient() - => IntegrationTestHelpers.GetOpenAIClient() - ?.AsSpeechToTextClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1"); + { + var openAIClient = IntegrationTestHelpers.GetOpenAIClient(); + + if (openAIClient is null) + { + return null; + } + + return new OpenAISpeechToTextClient( + openAIClient: openAIClient, + modelId: TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1"); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 1d204053389..a5d5bddc7ba 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -1,17 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.ClientModel; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Moq; using OpenAI; using OpenAI.Audio; using Xunit; @@ -22,91 +17,6 @@ namespace Microsoft.Extensions.AI; public class OpenAISpeechToTextClientTests { - [Fact] - public void AsSpeechToTextClient_InvalidArgs_Throws() - { - Assert.Throws("openAIClient", () => ((OpenAIClient)null!).AsSpeechToTextClient("model")); - Assert.Throws("audioClient", () => ((AudioClient)null!).AsSpeechToTextClient()); - - OpenAIClient client = new("key"); - Assert.Throws("modelId", () => client.AsSpeechToTextClient(null!)); - Assert.Throws("modelId", () => client.AsSpeechToTextClient(" ")); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsSpeechToTextClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - var openAIClient = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); - - ISpeechToTextClient client = openAIClient.AsSpeechToTextClient(model); - var metadata = client.GetService(); - Assert.NotNull(metadata); - Assert.Equal("openai", metadata.ProviderName); - Assert.Equal(endpoint, metadata.ProviderUri); - Assert.Equal(model, metadata.DefaultModelId); - - client = openAIClient.GetAudioClient(model).AsSpeechToTextClient(); - metadata = client.GetService(); - Assert.NotNull(metadata); - Assert.Equal("openai", metadata.ProviderName); - Assert.Equal(endpoint, metadata.ProviderUri); - Assert.Equal(model, metadata.DefaultModelId); - } - - [Fact] - public void GetService_OpenAIClient_SuccessfullyReturnsUnderlyingClient() - { - OpenAIClient openAIClient = new(new ApiKeyCredential("key")); - ISpeechToTextClient client = openAIClient.AsSpeechToTextClient("model"); - - Assert.Same(client, client.GetService()); - - Assert.Same(openAIClient, client.GetService()); - - Assert.NotNull(client.GetService()); - var mockLoggerFactory = new Mock(); - mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny())).Returns(new Mock().Object); - - using ISpeechToTextClient pipeline = client - .AsBuilder() - .UseLogging(mockLoggerFactory.Object) - .Build(); - - Assert.NotNull(pipeline.GetService()); - - Assert.Same(openAIClient, pipeline.GetService()); - Assert.IsType(pipeline.GetService()); - } - - [Fact] - public void GetService_AudioClient_SuccessfullyReturnsUnderlyingClient() - { - AudioClient openAIClient = new OpenAIClient(new ApiKeyCredential("key")).GetAudioClient("model"); - ISpeechToTextClient audioClient = openAIClient.AsSpeechToTextClient(); - - Assert.Same(audioClient, audioClient.GetService()); - Assert.Same(openAIClient, audioClient.GetService()); - - var mockLoggerFactory = new Mock(); - mockLoggerFactory.Setup(f => f.CreateLogger(It.IsAny())).Returns(new Mock().Object); - using ISpeechToTextClient pipeline = audioClient - .AsBuilder() - .UseLogging(mockLoggerFactory.Object) - .Build(); - - Assert.NotNull(pipeline.GetService()); - - Assert.Same(openAIClient, pipeline.GetService()); - Assert.IsType(pipeline.GetService()); - } - [Theory] [InlineData("pt", null)] [InlineData("en", null)] @@ -320,7 +230,6 @@ public async Task GetTextAsync_StronglyTypedOptions_AllSent() private static Stream GetAudioStream() => new MemoryStream([0x01, 0x02]); - private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) => - new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) - .AsSpeechToTextClient(modelId); + private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) + => new OpenAISpeechToTextClient(new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }), modelId); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs index 0fd9d5174ae..79c09dd5c6f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/LoggingSpeechToTextClientTests.cs @@ -108,8 +108,8 @@ public async Task GetStreamingTextAsync_LogsUpdateReceived(LogLevel level) static async IAsyncEnumerable GetUpdatesAsync() { await Task.Yield(); - yield return new SpeechToTextResponseUpdate { Text = "blue " }; - yield return new SpeechToTextResponseUpdate { Text = "whale" }; + yield return new SpeechToTextResponseUpdate("blue "); + yield return new SpeechToTextResponseUpdate("whale"); } using ISpeechToTextClient client = innerClient From c6c016e5029a147cc8661f877506bdaea777a2e5 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 1 Apr 2025 19:04:12 +0100 Subject: [PATCH 18/27] Address PR comments --- .../SpeechToText/ISpeechToTextClient.cs | 4 +- .../DataContentAsyncEnumerableStream.cs | 172 ------------------ .../OpenAIClientExtensions.cs | 9 + .../OpenAISpeechToTextClient.cs | 2 +- ...penAISpeechToTextClientIntegrationTests.cs | 15 +- .../OpenAISpeechToTextClientTests.cs | 6 +- 6 files changed, 19 insertions(+), 189 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs index 4c7173e2d58..65458d6602c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/ISpeechToTextClient.cs @@ -32,7 +32,7 @@ public interface ISpeechToTextClient : IDisposable /// The audio speech stream to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . - /// The text generated by the client. + /// The text generated. Task GetTextAsync( Stream audioSpeechStream, SpeechToTextOptions? options = null, @@ -42,7 +42,7 @@ Task GetTextAsync( /// The audio speech stream to send. /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . - /// The response messages generated by the client. + /// The text updates representing the streamed output. IAsyncEnumerable GetStreamingTextAsync( Stream audioSpeechStream, SpeechToTextOptions? options = null, diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs deleted file mode 100644 index 398b74c46c0..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/DataContentAsyncEnumerableStream.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits -#pragma warning disable SA1202 // Elements should be ordered by access - -namespace Microsoft.Extensions.AI; - -/// -/// Utility class to stream data content as a . -/// -internal sealed class DataContentAsyncEnumerableStream : Stream, IAsyncDisposable -{ - private readonly IAsyncEnumerator _enumerator; - private ReadOnlyMemory _current; - private DataContent? _firstDataContent; - - /// - /// Initializes a new instance of the class/>. - /// - /// The async enumerable to stream. - /// The first chunk of data to reconsider when reading. - /// The to monitor for cancellation requests. The default is . - /// - /// needs to be considered back in the stream if was iterated before creating the stream. - /// This can happen to check if the first enumerable item contains data or is just a reference only content. - /// - internal DataContentAsyncEnumerableStream( - IAsyncEnumerable dataAsyncEnumerable, DataContent? firstDataContent = null, CancellationToken cancellationToken = default) - { - _enumerator = Throw.IfNull(dataAsyncEnumerable).GetAsyncEnumerator(cancellationToken); - _firstDataContent = firstDataContent; - _current = Memory.Empty; - } - - /// - public override bool CanRead => true; - - /// - public override bool CanSeek => false; - - /// - public override bool CanWrite => false; - - /// - public override long Length => throw new NotSupportedException(); - - /// - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - /// - public override void Flush() - { - } - - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - /// - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - - /// - public override void SetLength(long value) => throw new NotSupportedException(); - - /// - public override int Read(byte[] buffer, int offset, int count) => - ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); - - /// - public override void Write(byte[] buffer, int offset, int count) => - throw new NotSupportedException(); - - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => - ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - -#if NET - /// - public override -#else - internal -#endif - async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - if (buffer.IsEmpty) - { - return 0; - } - - while (_current.IsEmpty) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_firstDataContent is not null) - { - _current = _firstDataContent.Data; - _firstDataContent = null; - continue; - } - - if (!await _enumerator.MoveNextAsync().ConfigureAwait(false)) - { - return 0; - } - - _current = _enumerator.Current.Data; - } - - int toCopy = Math.Min(buffer.Length, _current.Length); - _current.Slice(0, toCopy).CopyTo(buffer); - _current = _current.Slice(toCopy); - return toCopy; - } - -#if NET - /// - public override void CopyTo(Stream destination, int bufferSize) => - CopyToAsync(destination, bufferSize, CancellationToken.None).GetAwaiter().GetResult(); - - /// - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - _ = Throw.IfNull(destination); - - if (!_current.IsEmpty) - { - await destination.WriteAsync(_current, cancellationToken).ConfigureAwait(false); - _current = Memory.Empty; - } - - if (_firstDataContent is not null) - { - await destination.WriteAsync(_firstDataContent.Data, cancellationToken).ConfigureAwait(false); - _firstDataContent = null; - } - - while (await _enumerator.MoveNextAsync().ConfigureAwait(false)) - { - cancellationToken.ThrowIfCancellationRequested(); - await destination.WriteAsync(_enumerator.Current.Data, cancellationToken).ConfigureAwait(false); - } - } - - /// - public override async ValueTask DisposeAsync() - { - await _enumerator.DisposeAsync().ConfigureAwait(false); - await base.DisposeAsync().ConfigureAwait(false); - } -#else - /// - public ValueTask DisposeAsync() => _enumerator.DisposeAsync(); - - /// - protected override void Dispose(bool disposing) - { - _enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); - base.Dispose(disposing); - } -#endif -} - diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 6b330e4da00..44fa6de1f17 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,8 +3,10 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.Diagnostics; using OpenAI; +using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; using OpenAI.Responses; @@ -35,6 +37,13 @@ public static IChatClient AsIChatClient(this ChatClient chatClient) => public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => new OpenAIResponseChatClient(responseClient); + /// Gets an for use with this . + /// The client. + /// An that can be used to transcribe audio via the . + [Experimental("MEAI001")] + public static ISpeechToTextClient AsSpeechToTextClient(this AudioClient audioClient) => + new OpenAISpeechToTextClient(audioClient); + /// Gets an for use with this . /// The client. /// The model to use. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index fe2d2497ec5..766644421e4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -20,7 +20,7 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . [Experimental("MEAI001")] -public sealed class OpenAISpeechToTextClient : ISpeechToTextClient +internal sealed class OpenAISpeechToTextClient : ISpeechToTextClient { /// Default OpenAI endpoint. private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1"); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs index a385c6db536..eed2b4cacd8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs @@ -6,16 +6,7 @@ namespace Microsoft.Extensions.AI; public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegrationTests { protected override ISpeechToTextClient? CreateClient() - { - var openAIClient = IntegrationTestHelpers.GetOpenAIClient(); - - if (openAIClient is null) - { - return null; - } - - return new OpenAISpeechToTextClient( - openAIClient: openAIClient, - modelId: TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1"); - } + => IntegrationTestHelpers.GetOpenAIClient()? + .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1") + .AsSpeechToTextClient(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index a5d5bddc7ba..133310f5ea5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -230,6 +230,8 @@ public async Task GetTextAsync_StronglyTypedOptions_AllSent() private static Stream GetAudioStream() => new MemoryStream([0x01, 0x02]); - private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) - => new OpenAISpeechToTextClient(new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }), modelId); + private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) => + new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) + .GetAudioClient(modelId) + .AsSpeechToTextClient(); } From ca1338b793572f92c1da35577d383c4372515195 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:42:02 +0100 Subject: [PATCH 19/27] Remove async wrapping --- .../SpeechToTextClientExtensions.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs index e1a4c07638a..d8ca62f34ea 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextClientExtensions.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -38,7 +37,7 @@ public static class SpeechToTextClientExtensions /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static async Task GetTextAsync( + public static Task GetTextAsync( this ISpeechToTextClient client, DataContent audioSpeechContent, SpeechToTextOptions? options = null, @@ -47,11 +46,11 @@ public static async Task GetTextAsync( _ = Throw.IfNull(client); _ = Throw.IfNull(audioSpeechContent); - using var audioSpeechStream = MemoryMarshal.TryGetArray(audioSpeechContent.Data, out var array) ? + var audioSpeechStream = MemoryMarshal.TryGetArray(audioSpeechContent.Data, out var array) ? new MemoryStream(array.Array!, array.Offset, array.Count) : new MemoryStream(audioSpeechContent.Data.ToArray()); - return await client.GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); + return client.GetTextAsync(audioSpeechStream, options, cancellationToken); } /// Generates text from speech providing a single audio speech . @@ -60,22 +59,19 @@ public static async Task GetTextAsync( /// The speech to text options to configure the request. /// The to monitor for cancellation requests. The default is . /// The text generated by the client. - public static async IAsyncEnumerable GetStreamingTextAsync( + public static IAsyncEnumerable GetStreamingTextAsync( this ISpeechToTextClient client, DataContent audioSpeechContent, SpeechToTextOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default) { _ = Throw.IfNull(client); _ = Throw.IfNull(audioSpeechContent); - using var audioSpeechStream = MemoryMarshal.TryGetArray(audioSpeechContent.Data, out var array) ? + var audioSpeechStream = MemoryMarshal.TryGetArray(audioSpeechContent.Data, out var array) ? new MemoryStream(array.Array!, array.Offset, array.Count) : new MemoryStream(audioSpeechContent.Data.ToArray()); - await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } + return client.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken); } } From d3a14c9d6e918bfceb505c2031a8fc0bfb5c3725 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:44:37 +0100 Subject: [PATCH 20/27] Adjusting concat / text fields --- .../SpeechToText/SpeechToTextResponse.cs | 4 ++-- .../SpeechToText/SpeechToTextResponseUpdate.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs index 49ef9df414a..24fa20a11ed 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -67,10 +67,10 @@ public SpeechToTextResponse(string? content) /// This property concatenates the text of all objects in . /// [JsonIgnore] - public string Text => Contents?.ConcatText() ?? string.Empty; + public string Text => _contents?.ConcatText() ?? string.Empty; /// - public override string ToString() => Contents.ConcatText(); + public override string ToString() => Text; /// Creates an array of instances that represent this . /// An array of instances that may be used to represent this . diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs index 004a49278d5..24b7f079302 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -87,7 +87,7 @@ public SpeechToTextResponseUpdate(string? content) /// This property concatenates the text of all objects in . /// [JsonIgnore] - public string Text => Contents?.ConcatText() ?? string.Empty; + public string Text => _contents?.ConcatText() ?? string.Empty; /// Gets or sets the generated content items. [AllowNull] @@ -98,5 +98,5 @@ public IList Contents } /// - public override string ToString() => Contents.ConcatText(); + public override string ToString() => Text; } From 263f0e09a7aeabe54bac3c5c8a7e4dccb3fad24d Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:02:41 +0100 Subject: [PATCH 21/27] Start time and end time added to update + UT covering --- .../SpeechToTextResponseUpdateExtensions.cs | 54 ++++-- ...eechToTextResponseUpdateExtensionsTests.cs | 155 ++++++++++++++++++ 2 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index f5d71a2ef14..ce5a6684daf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; - using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -28,20 +28,34 @@ public static SpeechToTextResponse ToSpeechToTextResponse( List contents = []; string? responseId = null; string? modelId = null; - object? rawRepresentation = null; + List? rawRepresentations = null; AdditionalPropertiesDictionary? additionalProperties = null; + TimeSpan? endTime = null; foreach (var update in updates) { - ProcessUpdate(update, contents, ref responseId, ref modelId, ref rawRepresentation, ref additionalProperties); + // Track the first start time provided by the updates + response.StartTime ??= update.StartTime; + + // Track the last end time provided by the updates + if (update.EndTime is not null) + { + endTime = update.EndTime; + } + + // Add the list of the all raw representation updates + rawRepresentations ??= []; + rawRepresentations.Add(update.RawRepresentation); + + ProcessUpdate(update, contents, ref responseId, ref modelId, ref additionalProperties); } ChatResponseExtensions.CoalesceTextContent(contents); - + response.EndTime = endTime; response.Contents = contents; response.ResponseId = responseId; response.ModelId = modelId; - response.RawRepresentation = rawRepresentation; + response.RawRepresentation = rawRepresentations; response.AdditionalProperties = additionalProperties; return response; @@ -65,20 +79,35 @@ static async Task ToResponseAsync( List contents = []; string? responseId = null; string? modelId = null; - object? rawRepresentation = null; + List? rawRepresentations = null; AdditionalPropertiesDictionary? additionalProperties = null; + TimeSpan? endTime = null; await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) { - ProcessUpdate(update, contents, ref responseId, ref modelId, ref rawRepresentation, ref additionalProperties); + // Track the first start time provided by the updates + response.StartTime ??= update.StartTime; + + // Track the last end time provided by the updates + if (update.EndTime is not null) + { + endTime = update.EndTime; + } + + // Add the list of the all raw representation updates + rawRepresentations ??= []; + rawRepresentations.Add(update.RawRepresentation); + + ProcessUpdate(update, contents, ref responseId, ref modelId, ref additionalProperties); } ChatResponseExtensions.CoalesceTextContent(contents); + response.EndTime = endTime; response.Contents = contents; response.ResponseId = responseId; response.ModelId = modelId; - response.RawRepresentation = rawRepresentation; + response.RawRepresentation = rawRepresentations; response.AdditionalProperties = additionalProperties; return response; @@ -90,15 +119,12 @@ static async Task ToResponseAsync( /// The list of content items being accumulated. /// The response ID to update if the update has one. /// The model ID to update if the update has one. - /// The raw representation to update if the update has one. /// The additional properties to update if the update has any. -#pragma warning disable S4047 // Generics should be used when appropriate private static void ProcessUpdate( SpeechToTextResponseUpdate update, List contents, ref string? responseId, ref string? modelId, - ref object? rawRepresentation, ref AdditionalPropertiesDictionary? additionalProperties) { if (update.ResponseId is not null) @@ -111,11 +137,6 @@ private static void ProcessUpdate( modelId = update.ModelId; } - if (update.RawRepresentation is not null) - { - rawRepresentation = update.RawRepresentation; - } - contents.AddRange(update.Contents); if (update.AdditionalProperties is not null) @@ -133,5 +154,4 @@ private static void ProcessUpdate( } } } -#pragma warning restore S4047 // Generics should be used when appropriate } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs new file mode 100644 index 00000000000..e22a061c714 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SpeechToTextResponseUpdateExtensionsTests +{ + public static IEnumerable ToSpeechToTextResponse_Coalescing_VariousSequenceAndGapLengths_MemberData() + { + foreach (bool useAsync in new[] { false, true }) + { + for (int numSequences = 1; numSequences <= 3; numSequences++) + { + for (int sequenceLength = 1; sequenceLength <= 3; sequenceLength++) + { + for (int gapLength = 1; gapLength <= 3; gapLength++) + { + foreach (bool gapBeginningEnd in new[] { false, true }) + { + yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false }; + } + } + } + } + } + } + + [Fact] + public void InvalidArgs_Throws() + { + Assert.Throws("updates", () => ((List)null!).ToSpeechToTextResponse()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToSpeechToTextResponse_SuccessfullyCreatesResponse(bool useAsync) + { + object[] rawRepresentations = [new(), new(), new(), new(), new(), new()]; + SpeechToTextResponseUpdate[] updates = + [ + new("Hello ") { ModelId = "model123", RawRepresentation = rawRepresentations[0], StartTime = null, AdditionalProperties = new() { ["a"] = "b" } }, + new("human, ") { ModelId = "model123", RawRepresentation = rawRepresentations[1], StartTime = TimeSpan.FromSeconds(10), EndTime = TimeSpan.FromSeconds(20) }, + new("How ") { ModelId = "model123", RawRepresentation = rawRepresentations[2], StartTime = TimeSpan.FromSeconds(22), EndTime = TimeSpan.FromSeconds(23) }, + new("are ") { ModelId = "model123", RawRepresentation = rawRepresentations[3], StartTime = TimeSpan.FromSeconds(23), EndTime = TimeSpan.FromSeconds(24) }, + new([new TextContent("You?")]) + { + ModelId = "model123", RawRepresentation = rawRepresentations[4], StartTime = TimeSpan.FromSeconds(24), EndTime = TimeSpan.FromSeconds(25), + AdditionalProperties = new() { ["c"] = "d" } + }, + new() { ResponseId = "someResponse", ModelId = "model123", RawRepresentation = rawRepresentations[5], StartTime = TimeSpan.FromSeconds(25), EndTime = TimeSpan.FromSeconds(35) }, + ]; + + SpeechToTextResponse response = useAsync ? + updates.ToSpeechToTextResponse() : + await YieldAsync(updates).ToSpeechToTextResponseAsync(); + + Assert.NotNull(response); + + Assert.NotNull(response.RawRepresentation); + + Assert.Equal("someResponse", response.ResponseId); + Assert.Equal(TimeSpan.FromSeconds(10), response.StartTime); + Assert.Equal(TimeSpan.FromSeconds(35), response.EndTime); + Assert.Equal("model123", response.ModelId); + var responseRawRepresentation = Assert.IsAssignableFrom>(response.RawRepresentation); + Assert.Collection(responseRawRepresentation, + response => Assert.Equal(rawRepresentations[0], response), + response => Assert.Equal(rawRepresentations[1], response), + response => Assert.Equal(rawRepresentations[2], response), + response => Assert.Equal(rawRepresentations[3], response), + response => Assert.Equal(rawRepresentations[4], response), + response => Assert.Equal(rawRepresentations[5], response)); + + Assert.NotNull(response.AdditionalProperties); + Assert.Equal(2, response.AdditionalProperties.Count); + Assert.Equal("b", response.AdditionalProperties["a"]); + Assert.Equal("d", response.AdditionalProperties["c"]); + + Assert.Equal("Hello human, How are You?", response.Text); + } + + [Theory] + [MemberData(nameof(ToSpeechToTextResponse_Coalescing_VariousSequenceAndGapLengths_MemberData))] + public async Task ToSpeechToTextResponse_Coalescing_VariousSequenceAndGapLengths(bool useAsync, int numSequences, int sequenceLength, int gapLength, bool gapBeginningEnd) + { + List updates = []; + + List expected = []; + + if (gapBeginningEnd) + { + AddGap(); + } + + for (int sequenceNum = 0; sequenceNum < numSequences; sequenceNum++) + { + StringBuilder sb = new(); + for (int i = 0; i < sequenceLength; i++) + { + string text = $"{(char)('A' + sequenceNum)}{i}"; + updates.Add(new(text)); + sb.Append(text); + } + + expected.Add(sb.ToString()); + + if (sequenceNum < numSequences - 1) + { + AddGap(); + } + } + + if (gapBeginningEnd) + { + AddGap(); + } + + void AddGap() + { + for (int i = 0; i < gapLength; i++) + { + updates.Add(new() { Contents = [new DataContent("data:image/png;base64,aGVsbG8=")] }); + } + } + + SpeechToTextResponse response = useAsync ? await YieldAsync(updates).ToSpeechToTextResponseAsync() : updates.ToSpeechToTextResponse(); + Assert.NotNull(response); + + Assert.Equal(expected.Count + (gapLength * ((numSequences - 1) + (gapBeginningEnd ? 2 : 0))), response.Contents.Count); + + TextContent[] contents = response.Contents.OfType().ToArray(); + Assert.Equal(expected.Count, contents.Length); + for (int i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i], contents[i].Text); + } + } + + private static async IAsyncEnumerable YieldAsync(IEnumerable updates) + { + foreach (SpeechToTextResponseUpdate update in updates) + { + await Task.Yield(); + yield return update; + } + } +} From dd5ec14e65724be5e813d856a1dda1fee3d75855 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:04:25 +0100 Subject: [PATCH 22/27] AsISpeechToText renaming --- .../Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs | 2 +- .../OpenAISpeechToTextClientIntegrationTests.cs | 2 +- .../OpenAISpeechToTextClientTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 44fa6de1f17..c2753379974 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -41,7 +41,7 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient /// The client. /// An that can be used to transcribe audio via the . [Experimental("MEAI001")] - public static ISpeechToTextClient AsSpeechToTextClient(this AudioClient audioClient) => + public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioClient) => new OpenAISpeechToTextClient(audioClient); /// Gets an for use with this . diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs index eed2b4cacd8..c80b37c865e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs @@ -8,5 +8,5 @@ public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegr protected override ISpeechToTextClient? CreateClient() => IntegrationTestHelpers.GetOpenAIClient()? .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1") - .AsSpeechToTextClient(); + .AsISpeechToTextClient(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 133310f5ea5..fb4329e7fb3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -233,5 +233,5 @@ private static Stream GetAudioStream() private static ISpeechToTextClient CreateSpeechToTextClient(HttpClient httpClient, string modelId) => new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) .GetAudioClient(modelId) - .AsSpeechToTextClient(); + .AsISpeechToTextClient(); } From 9eabb982682e268991549cc1d5daa79975c4f1f4 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:10:27 +0100 Subject: [PATCH 23/27] Remove OpenAIClient ctor + small fixes --- .../OpenAISpeechToTextClient.cs | 25 ------------------- .../SpeechToText/LoggingSpeechToTextClient.cs | 2 +- .../OpenAISpeechToTextClientTests.cs | 2 +- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 766644421e4..78fe00a8377 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -28,33 +28,9 @@ internal sealed class OpenAISpeechToTextClient : ISpeechToTextClient /// Metadata about the client. private readonly SpeechToTextClientMetadata _metadata; - /// The underlying . - private readonly OpenAIClient? _openAIClient; - /// The underlying . private readonly AudioClient _audioClient; - /// Initializes a new instance of the class for the specified . - /// The underlying client. - /// The model to use. - public OpenAISpeechToTextClient(OpenAIClient openAIClient, string modelId) - { - _ = Throw.IfNull(openAIClient); - _ = Throw.IfNullOrWhitespace(modelId); - - _openAIClient = openAIClient; - _audioClient = openAIClient.GetAudioClient(modelId); - - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages - // implement the abstractions directly rather than providing adapters on top of the public APIs, - // the package can provide such implementations separate from what's exposed in the public API. - Uri providerUrl = typeof(OpenAIClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(openAIClient) as Uri ?? _defaultOpenAIEndpoint; - - _metadata = new("openai", providerUrl, modelId); - } - /// Initializes a new instance of the class for the specified . /// The underlying client. public OpenAISpeechToTextClient(AudioClient audioClient) @@ -83,7 +59,6 @@ public OpenAISpeechToTextClient(AudioClient audioClient) return serviceKey is not null ? null : serviceType == typeof(SpeechToTextClientMetadata) ? _metadata : - serviceType == typeof(OpenAIClient) ? _openAIClient : serviceType == typeof(AudioClient) ? _audioClient : serviceType.IsInstanceOfType(this) ? this : null; diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index 6299d38663e..4494d319dc0 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -183,7 +183,7 @@ public override async IAsyncEnumerable GetStreamingT private partial void LogCompleted(string methodName); [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {SpeechToTextResponse}.")] - private partial void LogCompletedSensitive(string methodName, string SpeechToTextResponse); + private partial void LogCompletedSensitive(string methodName, string speechToTextResponse); [LoggerMessage(LogLevel.Debug, "GetStreamingTextAsync received update.")] private partial void LogStreamingUpdate(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index fb4329e7fb3..50a8dc10f73 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -53,7 +53,7 @@ public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, stri Assert.Contains("I finally got back to the gym the other day", response.Text); Assert.NotNull(response.RawRepresentation); - Assert.IsType(response.RawRepresentation); + Assert.IsType(response.RawRepresentation); } [Fact] From 78e4ebb2840e1e7747783ded97a2f72a54797e29 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:28:03 +0100 Subject: [PATCH 24/27] Removing rawrepresentation impl from Update -> Response --- .../SpeechToTextResponseUpdateExtensions.cs | 12 --------- ...eechToTextResponseUpdateExtensionsTests.cs | 27 +++++-------------- 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index ce5a6684daf..230ec838ba3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -28,7 +28,6 @@ public static SpeechToTextResponse ToSpeechToTextResponse( List contents = []; string? responseId = null; string? modelId = null; - List? rawRepresentations = null; AdditionalPropertiesDictionary? additionalProperties = null; TimeSpan? endTime = null; @@ -43,10 +42,6 @@ public static SpeechToTextResponse ToSpeechToTextResponse( endTime = update.EndTime; } - // Add the list of the all raw representation updates - rawRepresentations ??= []; - rawRepresentations.Add(update.RawRepresentation); - ProcessUpdate(update, contents, ref responseId, ref modelId, ref additionalProperties); } @@ -55,7 +50,6 @@ public static SpeechToTextResponse ToSpeechToTextResponse( response.Contents = contents; response.ResponseId = responseId; response.ModelId = modelId; - response.RawRepresentation = rawRepresentations; response.AdditionalProperties = additionalProperties; return response; @@ -79,7 +73,6 @@ static async Task ToResponseAsync( List contents = []; string? responseId = null; string? modelId = null; - List? rawRepresentations = null; AdditionalPropertiesDictionary? additionalProperties = null; TimeSpan? endTime = null; @@ -94,10 +87,6 @@ static async Task ToResponseAsync( endTime = update.EndTime; } - // Add the list of the all raw representation updates - rawRepresentations ??= []; - rawRepresentations.Add(update.RawRepresentation); - ProcessUpdate(update, contents, ref responseId, ref modelId, ref additionalProperties); } @@ -107,7 +96,6 @@ static async Task ToResponseAsync( response.Contents = contents; response.ResponseId = responseId; response.ModelId = modelId; - response.RawRepresentation = rawRepresentations; response.AdditionalProperties = additionalProperties; return response; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs index e22a061c714..f0a2f08ab13 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs @@ -43,19 +43,14 @@ public void InvalidArgs_Throws() [InlineData(true)] public async Task ToSpeechToTextResponse_SuccessfullyCreatesResponse(bool useAsync) { - object[] rawRepresentations = [new(), new(), new(), new(), new(), new()]; SpeechToTextResponseUpdate[] updates = [ - new("Hello ") { ModelId = "model123", RawRepresentation = rawRepresentations[0], StartTime = null, AdditionalProperties = new() { ["a"] = "b" } }, - new("human, ") { ModelId = "model123", RawRepresentation = rawRepresentations[1], StartTime = TimeSpan.FromSeconds(10), EndTime = TimeSpan.FromSeconds(20) }, - new("How ") { ModelId = "model123", RawRepresentation = rawRepresentations[2], StartTime = TimeSpan.FromSeconds(22), EndTime = TimeSpan.FromSeconds(23) }, - new("are ") { ModelId = "model123", RawRepresentation = rawRepresentations[3], StartTime = TimeSpan.FromSeconds(23), EndTime = TimeSpan.FromSeconds(24) }, - new([new TextContent("You?")]) - { - ModelId = "model123", RawRepresentation = rawRepresentations[4], StartTime = TimeSpan.FromSeconds(24), EndTime = TimeSpan.FromSeconds(25), - AdditionalProperties = new() { ["c"] = "d" } - }, - new() { ResponseId = "someResponse", ModelId = "model123", RawRepresentation = rawRepresentations[5], StartTime = TimeSpan.FromSeconds(25), EndTime = TimeSpan.FromSeconds(35) }, + new("Hello ") { ModelId = "model123", StartTime = null, AdditionalProperties = new() { ["a"] = "b" } }, + new("human, ") { ModelId = "model123", StartTime = TimeSpan.FromSeconds(10), EndTime = TimeSpan.FromSeconds(20) }, + new("How ") { ModelId = "model123", StartTime = TimeSpan.FromSeconds(22), EndTime = TimeSpan.FromSeconds(23) }, + new("are ") { ModelId = "model123", StartTime = TimeSpan.FromSeconds(23), EndTime = TimeSpan.FromSeconds(24) }, + new([new TextContent("You?")]) { ModelId = "model123", StartTime = TimeSpan.FromSeconds(24), EndTime = TimeSpan.FromSeconds(25), AdditionalProperties = new() { ["c"] = "d" } }, + new() { ResponseId = "someResponse", ModelId = "model123", StartTime = TimeSpan.FromSeconds(25), EndTime = TimeSpan.FromSeconds(35) }, ]; SpeechToTextResponse response = useAsync ? @@ -64,20 +59,10 @@ public async Task ToSpeechToTextResponse_SuccessfullyCreatesResponse(bool useAsy Assert.NotNull(response); - Assert.NotNull(response.RawRepresentation); - Assert.Equal("someResponse", response.ResponseId); Assert.Equal(TimeSpan.FromSeconds(10), response.StartTime); Assert.Equal(TimeSpan.FromSeconds(35), response.EndTime); Assert.Equal("model123", response.ModelId); - var responseRawRepresentation = Assert.IsAssignableFrom>(response.RawRepresentation); - Assert.Collection(responseRawRepresentation, - response => Assert.Equal(rawRepresentations[0], response), - response => Assert.Equal(rawRepresentations[1], response), - response => Assert.Equal(rawRepresentations[2], response), - response => Assert.Equal(rawRepresentations[3], response), - response => Assert.Equal(rawRepresentations[4], response), - response => Assert.Equal(rawRepresentations[5], response)); Assert.NotNull(response.AdditionalProperties); Assert.Equal(2, response.AdditionalProperties.Count); From 8bf3389e015ad5df1bcdd4d998088ac85e74d099 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:42:04 +0100 Subject: [PATCH 25/27] Add missing AsISpeechToText UT --- .../OpenAISpeechToTextClientTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 50a8dc10f73..50f103ec82d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -1,12 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.ClientModel; using System.ClientModel.Primitives; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Azure.AI.OpenAI; using OpenAI; using OpenAI.Audio; using Xunit; @@ -17,6 +19,31 @@ namespace Microsoft.Extensions.AI; public class OpenAISpeechToTextClientTests { + [Fact] + public void AsISpeechToTextClient_InvalidArgs_Throws() + { + Assert.Throws("audioClient", () => ((AudioClient)null!).AsISpeechToTextClient()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata(bool useAzureOpenAI) + { + Uri endpoint = new("http://localhost/some/endpoint"); + string model = "amazingModel"; + + var client = useAzureOpenAI ? + new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : + new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + + ISpeechToTextClient speechToTextClient = client.GetAudioClient(model).AsISpeechToTextClient(); + var metadata = speechToTextClient.GetService(); + Assert.Equal("openai", metadata?.ProviderName); + Assert.Equal(endpoint, metadata?.ProviderUri); + Assert.Equal(model, metadata?.DefaultModelId); + } + [Theory] [InlineData("pt", null)] [InlineData("en", null)] From c5c6e893e47390359945fc8d62de905ba99f6496 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:50:11 +0100 Subject: [PATCH 26/27] Add GetService UT --- .../OpenAISpeechToTextClientTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 50f103ec82d..c376f24be31 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -9,8 +9,12 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.OpenAI; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using OpenAI; using OpenAI.Audio; +using OpenAI.Chat; using Xunit; #pragma warning disable S103 // Lines should not be too long @@ -44,6 +48,25 @@ public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata(bool useA Assert.Equal(model, metadata?.DefaultModelId); } + [Fact] + public void GetService_AudioClient_SuccessfullyReturnsUnderlyingClient() + { + AudioClient audioClient = new OpenAIClient(new ApiKeyCredential("key")).GetAudioClient("model"); + ISpeechToTextClient speechToTextClient = audioClient.AsISpeechToTextClient(); + Assert.Same(speechToTextClient, speechToTextClient.GetService()); + Assert.Same(audioClient, speechToTextClient.GetService()); + using var factory = LoggerFactory.Create(b => b.AddFakeLogging()); + using ISpeechToTextClient pipeline = speechToTextClient + .AsBuilder() + .UseLogging(factory) + .Build(); + + Assert.NotNull(pipeline.GetService()); + + Assert.Same(audioClient, pipeline.GetService()); + Assert.IsType(pipeline.GetService()); + } + [Theory] [InlineData("pt", null)] [InlineData("en", null)] From 977a0e5c987d6dda3ab41685af878cfd6d5ae17a Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:51:29 +0100 Subject: [PATCH 27/27] Warning fix --- .../OpenAISpeechToTextClientTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index c376f24be31..4587c3a5524 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -9,12 +9,9 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.OpenAI; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using OpenAI; using OpenAI.Audio; -using OpenAI.Chat; using Xunit; #pragma warning disable S103 // Lines should not be too long