Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

M.E.AI.Abstractions - Speech to Text Abstraction #5838

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a8dd64f
Latests adjustments
RogerBarreto Jan 31, 2025
6986a9f
Final adjustments before UT and IT, building + xml docs
RogerBarreto Feb 3, 2025
3782f17
Add unit tests for AudioTranscription
RogerBarreto Feb 4, 2025
2f380cb
Update Unit Tests until compile
RogerBarreto Feb 4, 2025
b64edff
Update Unit Tests
RogerBarreto Feb 5, 2025
11508fd
Remove culture info
RogerBarreto Feb 5, 2025
2fd2390
Merge pull request #3 from RogerBarreto/add-audio-transcription-tests
RogerBarreto Feb 5, 2025
e74ffc8
Fix warnings + UT
RogerBarreto Feb 6, 2025
aaff261
Adding missing components for IT
RogerBarreto Feb 8, 2025
c40099c
Adding the TranscriptionBuilder
RogerBarreto Feb 9, 2025
f6146dc
Adding concrete OpenAI builder, logging client and OpenAI UT IT
RogerBarreto Feb 9, 2025
924e816
Merge branch 'main' of https://github.com/dotnet/extensions into audi…
RogerBarreto Feb 14, 2025
7dc52b3
Conflict and Merge fixes
RogerBarreto Feb 14, 2025
86d0e35
Address PR comments
RogerBarreto Feb 17, 2025
fec0953
Fix UT
RogerBarreto Feb 17, 2025
e3c5779
Renaming Abstractions to Speech to Text
RogerBarreto Feb 19, 2025
3a252b6
Adding CopyToAsync override
RogerBarreto Feb 19, 2025
8cc9160
minor update
RogerBarreto Feb 19, 2025
bad57dd
Merge branch 'main' into audio-transcription-abstraction
RogerBarreto Feb 21, 2025
b6f1968
Adding response UT checks for audio transcription
RogerBarreto Feb 23, 2025
ec7773c
Add Translation Implementationand UT
RogerBarreto Feb 23, 2025
5253a15
Added more UT and coverage
RogerBarreto Feb 25, 2025
6bfb26b
Add SpeechToText Builder + UT
RogerBarreto Feb 25, 2025
4377a29
Add DependencyInjection Patterns
RogerBarreto Feb 25, 2025
7a531cd
Add Logging SpeechToText Client UT + Updates
RogerBarreto Feb 25, 2025
cba7a36
Merge branch 'main' into audio-transcription-abstraction
RogerBarreto Feb 25, 2025
319d283
Fix descriptions
RogerBarreto Feb 25, 2025
f72497f
Merge branch 'main' into audio-transcription-abstraction
RogerBarreto Feb 26, 2025
d82f394
Merge branch 'main' into audio-transcription-abstraction
RogerBarreto Feb 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Adding the TranscriptionBuilder
RogerBarreto committed Feb 9, 2025
commit c40099c4675bb0bbe4dbe6ddcf6603f136f54dc7
Original file line number Diff line number Diff line change
@@ -51,14 +51,14 @@ protected virtual void Dispose(bool disposing)
public virtual AudioTranscriptionClientMetadata Metadata => InnerClient.Metadata;

/// <inheritdoc />
public virtual Task<AudioTranscriptionCompletion> TranscribeAsync(IReadOnlyList<
public virtual Task<AudioTranscriptionCompletion> TranscribeAsync(IList<
IAsyncEnumerable<DataContent>> audioContents, AudioTranscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
return InnerClient.TranscribeAsync(audioContents, options, cancellationToken);
}

/// <inheritdoc />
public virtual IAsyncEnumerable<StreamingAudioTranscriptionUpdate> TranscribeStreamingAsync(IReadOnlyList<
public virtual IAsyncEnumerable<StreamingAudioTranscriptionUpdate> TranscribeStreamingAsync(IList<
IAsyncEnumerable<DataContent>> audioContents, AudioTranscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
return InnerClient.TranscribeStreamingAsync(audioContents, options, cancellationToken);
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ public interface IAudioTranscriptionClient : IDisposable
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The transcriptions generated by the client.</returns>
Task<AudioTranscriptionCompletion> TranscribeAsync(
IReadOnlyList<IAsyncEnumerable<DataContent>> audioContents,
IList<IAsyncEnumerable<DataContent>> audioContents,
AudioTranscriptionOptions? options = null,
CancellationToken cancellationToken = default);

@@ -40,7 +40,7 @@ Task<AudioTranscriptionCompletion> TranscribeAsync(
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The response messages generated by the client.</returns>
IAsyncEnumerable<StreamingAudioTranscriptionUpdate> TranscribeStreamingAsync(
IReadOnlyList<IAsyncEnumerable<DataContent>> audioContents,
IList<IAsyncEnumerable<DataContent>> audioContents,
AudioTranscriptionOptions? options = null,
CancellationToken cancellationToken = default);

Original file line number Diff line number Diff line change
@@ -39,8 +39,4 @@
<ProjectReference Include="../Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="ChatCompletion\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -164,7 +164,7 @@ public async IAsyncEnumerable<StreamingAudioTranscriptionUpdate> TranscribeStrea
{
yield return new StreamingAudioTranscriptionUpdate(choice.Contents)
{
ChoiceIndex = inputIndex,
InputIndex = inputIndex,
Kind = AudioTranscriptionUpdateKind.Transcribed,
RawRepresentation = choice.RawRepresentation
};
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using OpenAI;
using OpenAI.Audio;
using OpenAI.Chat;
using OpenAI.Embeddings;

@@ -23,6 +24,19 @@ public static IChatClient AsChatClient(this OpenAIClient openAIClient, string mo
public static IChatClient AsChatClient(this ChatClient chatClient) =>
new OpenAIChatClient(chatClient);

/// <summary>Gets an <see cref="IAudioTranscriptionClient"/> for use with this <see cref="OpenAIClient"/>.</summary>
/// <param name="openAIClient">The client.</param>
/// <param name="modelId">The model.</param>
/// <returns>An <see cref="IAudioTranscriptionClient"/> that can be used to transcribe audio via the <see cref="OpenAIClient"/>.</returns>
public static IAudioTranscriptionClient AsAudioTranscriptionClient(this OpenAIClient openAIClient, string modelId) =>
new OpenAIAudioTranscriptionClient(openAIClient, modelId);

/// <summary>Gets an <see cref="IAudioTranscriptionClient"/> for use with this <see cref="AudioClient"/>.</summary>
/// <param name="audioClient">The client.</param>
/// <returns>An <see cref="IAudioTranscriptionClient"/> that can be used to transcribe audio via the <see cref="AudioClient"/>.</returns>
public static IAudioTranscriptionClient AsAudioTranscriptionClient(this AudioClient audioClient) =>
new OpenAIAudioTranscriptionClient(audioClient);

/// <summary>Gets an <see cref="IEmbeddingGenerator{String, Single}"/> for use with this <see cref="OpenAIClient"/>.</summary>
/// <param name="openAIClient">The client.</param>
/// <param name="modelId">The model to use.</param>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>A delegating audio transcription client that wraps an inner client with implementations provided by delegates.</summary>
public sealed class AnonymousDelegatingAudioTranscriptionClient : DelegatingAudioTranscriptionClient
{
/// <summary>The delegate to use as the implementation of <see cref="TranscribeAsync"/>.</summary>
private readonly Func<IList<IAsyncEnumerable<DataContent>>, AudioTranscriptionOptions?, IAudioTranscriptionClient, CancellationToken, Task<AudioTranscriptionCompletion>>? _transcribeFunc;

/// <summary>The delegate to use as the implementation of <see cref="TranscribeStreamingAsync"/>.</summary>
/// <remarks>
/// When non-<see langword="null"/>, this delegate is used as the implementation of <see cref="TranscribeStreamingAsync"/> and
/// will be invoked with the same arguments as the method itself, along with a reference to the inner client.
/// When <see langword="null"/>, <see cref="TranscribeStreamingAsync"/> will delegate directly to the inner client.
/// </remarks>
private readonly Func<
IList<IAsyncEnumerable<DataContent>>, AudioTranscriptionOptions?, IAudioTranscriptionClient, CancellationToken, IAsyncEnumerable<StreamingAudioTranscriptionUpdate>>? _transcribeStreamingFunc;

/// <summary>The delegate to use as the implementation of both <see cref="TranscribeAsync"/> and <see cref="TranscribeStreamingAsync"/>.</summary>
private readonly TranscribeSharedFunc? _sharedFunc;

/// <summary>
/// Initializes a new instance of the <see cref="AnonymousDelegatingAudioTranscriptionClient"/> class.
/// </summary>
/// <param name="innerClient">The inner client.</param>
/// <param name="sharedFunc">
/// A delegate that provides the implementation for both <see cref="TranscribeAsync"/> and <see cref="TranscribeStreamingAsync"/>.
/// 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.
/// </param>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="innerClient"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="sharedFunc"/> is <see langword="null"/>.</exception>
public AnonymousDelegatingAudioTranscriptionClient(IAudioTranscriptionClient innerClient, TranscribeSharedFunc sharedFunc)
: base(innerClient)
{
_ = Throw.IfNull(sharedFunc);

_sharedFunc = sharedFunc;
}

/// <summary>
/// Initializes a new instance of the <see cref="AnonymousDelegatingAudioTranscriptionClient"/> class.
/// </summary>
/// <param name="innerClient">The inner client.</param>
/// <param name="transcribeFunc">
/// A delegate that provides the implementation for <see cref="TranscribeAsync"/>. When <see langword="null"/>,
/// <paramref name="transcribeStreamingFunc"/> must be non-null, and the implementation of <see cref="TranscribeAsync"/>
/// will use <paramref name="transcribeStreamingFunc"/> for the implementation.
/// </param>
/// <param name="transcribeStreamingFunc">
/// A delegate that provides the implementation for <see cref="TranscribeStreamingAsync"/>. When <see langword="null"/>,
/// <paramref name="transcribeFunc"/> must be non-null, and the implementation of <see cref="TranscribeStreamingAsync"/>
/// will use <paramref name="transcribeFunc"/> for the implementation.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="innerClient"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException">Both <paramref name="transcribeFunc"/> and <paramref name="transcribeStreamingFunc"/> are <see langword="null"/>.</exception>
public AnonymousDelegatingAudioTranscriptionClient(
IAudioTranscriptionClient innerClient,
Func<IList<IAsyncEnumerable<DataContent>>, AudioTranscriptionOptions?, IAudioTranscriptionClient, CancellationToken, Task<AudioTranscriptionCompletion>>? transcribeFunc,
Func<
IList<IAsyncEnumerable<DataContent>>,
AudioTranscriptionOptions?, IAudioTranscriptionClient, CancellationToken, IAsyncEnumerable<StreamingAudioTranscriptionUpdate>>? transcribeStreamingFunc)
: base(innerClient)
{
ThrowIfBothDelegatesNull(transcribeFunc, transcribeStreamingFunc);

_transcribeFunc = transcribeFunc;
_transcribeStreamingFunc = transcribeStreamingFunc;
}

/// <inheritdoc/>
public override Task<AudioTranscriptionCompletion> TranscribeAsync(
IList<IAsyncEnumerable<DataContent>> audioContents, AudioTranscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(audioContents);

if (_sharedFunc is not null)
{
return TranscribeViaSharedAsync(audioContents, options, cancellationToken);

async Task<AudioTranscriptionCompletion> TranscribeViaSharedAsync(
IList<IAsyncEnumerable<DataContent>> audioContents, AudioTranscriptionOptions? options, CancellationToken cancellationToken)
{
AudioTranscriptionCompletion? completion = null;
await _sharedFunc(audioContents, options, async (audioContents, options, cancellationToken) =>
{
completion = await InnerClient.TranscribeAsync(audioContents, options, cancellationToken).ConfigureAwait(false);
}, cancellationToken).ConfigureAwait(false);

if (completion is null)
{
throw new InvalidOperationException("The wrapper completed successfully without producing a AudioTranscriptionCompletion.");
}

return completion;
}
}
else if (_transcribeFunc is not null)
{
return _transcribeFunc(audioContents, options, InnerClient, cancellationToken);
}
else
{
Debug.Assert(_transcribeStreamingFunc is not null, "Expected non-null streaming delegate.");
return _transcribeStreamingFunc!(audioContents, options, InnerClient, cancellationToken)
.ToAudioTranscriptionCompletionAsync(coalesceContent: true, cancellationToken);
}
}

/// <inheritdoc/>
public override IAsyncEnumerable<StreamingAudioTranscriptionUpdate> TranscribeStreamingAsync(
IList<IAsyncEnumerable<DataContent>> audioContents, AudioTranscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(audioContents);

if (_sharedFunc is not null)
{
var updates = Channel.CreateBounded<StreamingAudioTranscriptionUpdate>(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(audioContents, options, async (audioContents, options, cancellationToken) =>
{
await foreach (var update in InnerClient.TranscribeStreamingAsync(audioContents, 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 (_transcribeStreamingFunc is not null)
{
return _transcribeStreamingFunc(audioContents, options, InnerClient, cancellationToken);
}
else
{
Debug.Assert(_transcribeFunc is not null, "Expected non-null non-streaming delegate.");
return TranscribeStreamingAsyncViaTranscribeAsync(_transcribeFunc!(audioContents, options, InnerClient, cancellationToken));

static async IAsyncEnumerable<StreamingAudioTranscriptionUpdate> TranscribeStreamingAsyncViaTranscribeAsync(Task<AudioTranscriptionCompletion> task)
{
AudioTranscriptionCompletion completion = await task.ConfigureAwait(false);
foreach (var update in completion.ToStreamingAudioTranscriptionUpdates())
{
yield return update;
}
}
}
}

/// <summary>Throws an exception if both of the specified delegates are null.</summary>
/// <exception cref="ArgumentNullException">Both <paramref name="transcribeFunc"/> and <paramref name="transcribeStreamingFunc"/> are <see langword="null"/>.</exception>
internal static void ThrowIfBothDelegatesNull(object? transcribeFunc, object? transcribeStreamingFunc)
{
if (transcribeFunc is null && transcribeStreamingFunc is null)
{
Throw.ArgumentNullException(nameof(transcribeFunc), $"At least one of the {nameof(transcribeFunc)} or {nameof(transcribeStreamingFunc)} 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.

/// <summary>
/// Represents a method used to call <see cref="IAudioTranscriptionClient.TranscribeAsync"/> or <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>.
/// </summary>
/// <param name="audioContents">The audio contents to send.</param>
/// <param name="options">The audio transcription options to configure the request.</param>
/// <param name="nextAsync">
/// A delegate that provides the implementation for the inner client's <see cref="IAudioTranscriptionClient.TranscribeAsync"/> or
/// <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>. 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.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="Task"/> that represents the completion of the operation.</returns>
public delegate Task TranscribeSharedFunc(
IList<IAsyncEnumerable<DataContent>> audioContents,
AudioTranscriptionOptions? options,
Func<IList<IAsyncEnumerable<DataContent>>, AudioTranscriptionOptions?, CancellationToken, Task> nextAsync,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>A builder for creating pipelines of <see cref="IAudioTranscriptionClient"/>.</summary>
public sealed class AudioTranscriptionClientBuilder
{
private readonly Func<IServiceProvider, IAudioTranscriptionClient> _innerClientFactory;

/// <summary>The registered client factory instances.</summary>
private List<Func<IAudioTranscriptionClient, IServiceProvider, IAudioTranscriptionClient>>? _clientFactories;

/// <summary>Initializes a new instance of the <see cref="AudioTranscriptionClientBuilder"/> class.</summary>
/// <param name="innerClient">The inner <see cref="IAudioTranscriptionClient"/> that represents the underlying backend.</param>
public AudioTranscriptionClientBuilder(IAudioTranscriptionClient innerClient)
{
_ = Throw.IfNull(innerClient);
_innerClientFactory = _ => innerClient;
}

/// <summary>Initializes a new instance of the <see cref="AudioTranscriptionClientBuilder"/> class.</summary>
/// <param name="innerClientFactory">A callback that produces the inner <see cref="IAudioTranscriptionClient"/> that represents the underlying backend.</param>
public AudioTranscriptionClientBuilder(Func<IServiceProvider, IAudioTranscriptionClient> innerClientFactory)
{
_innerClientFactory = Throw.IfNull(innerClientFactory);
}

/// <summary>Builds an <see cref="IAudioTranscriptionClient"/> that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn.</summary>
/// <param name="services">
/// The <see cref="IServiceProvider"/> that should provide services to the <see cref="IAudioTranscriptionClient"/> instances.
/// If null, an empty <see cref="IServiceProvider"/> will be used.
/// </param>
/// <returns>An instance of <see cref="IAudioTranscriptionClient"/> that represents the entire pipeline.</returns>
public IAudioTranscriptionClient 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(AudioTranscriptionClientBuilder)} entry at index {i} returned null. " +
$"Ensure that the callbacks passed to {nameof(Use)} return non-null {nameof(IAudioTranscriptionClient)} instances.");
}
}

return audioClient;
}

/// <summary>Adds a factory for an intermediate audio transcription client to the audio transcription client pipeline.</summary>
/// <param name="clientFactory">The client factory function.</param>
/// <returns>The updated <see cref="AudioTranscriptionClientBuilder"/> instance.</returns>
public AudioTranscriptionClientBuilder Use(Func<IAudioTranscriptionClient, IAudioTranscriptionClient> clientFactory)
{
_ = Throw.IfNull(clientFactory);

return Use((innerClient, _) => clientFactory(innerClient));
}

/// <summary>Adds a factory for an intermediate audio transcription client to the audio transcription client pipeline.</summary>
/// <param name="clientFactory">The client factory function.</param>
/// <returns>The updated <see cref="AudioTranscriptionClientBuilder"/> instance.</returns>
public AudioTranscriptionClientBuilder Use(Func<IAudioTranscriptionClient, IServiceProvider, IAudioTranscriptionClient> clientFactory)
{
_ = Throw.IfNull(clientFactory);

(_clientFactories ??= []).Add(clientFactory);
return this;
}

/// <summary>
/// Adds to the audio transcription client pipeline an anonymous delegating audio transcription client based on a delegate that provides
/// an implementation for both <see cref="IAudioTranscriptionClient.TranscribeAsync"/> and <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>.
/// </summary>
/// <param name="sharedFunc">
/// A delegate that provides the implementation for both <see cref="IAudioTranscriptionClient.TranscribeAsync"/> and
/// <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>. 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.
/// </param>
/// <returns>The updated <see cref="AudioTranscriptionClientBuilder"/> instance.</returns>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="sharedFunc"/> is <see langword="null"/>.</exception>
public AudioTranscriptionClientBuilder Use(AnonymousDelegatingAudioTranscriptionClient.TranscribeSharedFunc sharedFunc)
{
_ = Throw.IfNull(sharedFunc);

return Use((innerClient, _) => new AnonymousDelegatingAudioTranscriptionClient(innerClient, sharedFunc));
}

/// <summary>
/// Adds to the audio transcription client pipeline an anonymous delegating audio transcription client based on a delegate that provides
/// an implementation for both <see cref="IAudioTranscriptionClient.TranscribeAsync"/> and <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>.
/// </summary>
/// <param name="transcribeFunc">
/// A delegate that provides the implementation for <see cref="IAudioTranscriptionClient.TranscribeAsync"/>. When <see langword="null"/>,
/// <paramref name="transcribeStreamingFunc"/> must be non-null, and the implementation of <see cref="IAudioTranscriptionClient.TranscribeAsync"/>
/// will use <paramref name="transcribeStreamingFunc"/> for the implementation.
/// </param>
/// <param name="transcribeStreamingFunc">
/// A delegate that provides the implementation for <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>. When <see langword="null"/>,
/// <paramref name="transcribeFunc"/> must be non-null, and the implementation of <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>
/// will use <paramref name="transcribeFunc"/> for the implementation.
/// </param>
/// <returns>The updated <see cref="AudioTranscriptionClientBuilder"/> instance.</returns>
/// <remarks>
/// One or both delegates may be provided. If both are provided, they will be used for their respective methods:
/// <paramref name="transcribeFunc"/> will provide the implementation of <see cref="IAudioTranscriptionClient.TranscribeAsync"/>, and
/// <paramref name="transcribeStreamingFunc"/> will provide the implementation of <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>.
/// If only one of the delegates is provided, it will be used for both methods. That means that if <paramref name="transcribeFunc"/>
/// is supplied without <paramref name="transcribeStreamingFunc"/>, the implementation of <see cref="IAudioTranscriptionClient.TranscribeStreamingAsync"/>
/// will employ limited streaming, as it will be operating on the batch output produced by <paramref name="transcribeFunc"/>. And if
/// <paramref name="transcribeStreamingFunc"/> is supplied without <paramref name="transcribeFunc"/>, the implementation of
/// <see cref="IAudioTranscriptionClient.TranscribeAsync"/> will be implemented by combining the updates from <paramref name="transcribeStreamingFunc"/>.
/// </remarks>
/// <exception cref="ArgumentNullException">Both <paramref name="transcribeFunc"/> and <paramref name="transcribeStreamingFunc"/> are <see langword="null"/>.</exception>
public AudioTranscriptionClientBuilder Use(
Func<IList<IAsyncEnumerable<DataContent>>, AudioTranscriptionOptions?, IAudioTranscriptionClient, CancellationToken, Task<AudioTranscriptionCompletion>>? transcribeFunc,
Func<IList<IAsyncEnumerable<DataContent>>, AudioTranscriptionOptions?, IAudioTranscriptionClient, CancellationToken,
IAsyncEnumerable<StreamingAudioTranscriptionUpdate>>? transcribeStreamingFunc)
{
AnonymousDelegatingAudioTranscriptionClient.ThrowIfBothDelegatesNull(transcribeFunc, transcribeStreamingFunc);

return Use((innerClient, _) => new AnonymousDelegatingAudioTranscriptionClient(innerClient, transcribeFunc, transcribeStreamingFunc));
}
}
Original file line number Diff line number Diff line change
@@ -100,13 +100,13 @@ public virtual async Task TranscribeStreamingAsync_MultipleStreamingResponseChoi
}
}

string responseText = firstSb.ToString();
Assert.Contains("finally", responseText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("gym", responseText, StringComparison.OrdinalIgnoreCase);
string firstTranscription = firstSb.ToString();
Assert.Contains("finally", firstTranscription, StringComparison.OrdinalIgnoreCase);
Assert.Contains("gym", firstTranscription, StringComparison.OrdinalIgnoreCase);

responseText = secondSb.ToString();
Assert.Contains("who would", responseText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("go for", responseText, 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)
Original file line number Diff line number Diff line change
@@ -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 OpenAIAudioTranscriptionClientIntegrationTests : AudioTranscriptionClientIntegrationTests
{
protected override IAudioTranscriptionClient? CreateClient()
=> IntegrationTestHelpers.GetOpenAIClient()
?.AsAudioTranscriptionClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1");
}

Large diffs are not rendered by default.