diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs
deleted file mode 100644
index d810b2d2a470..000000000000
--- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.ClientModel;
-using System.ClientModel.Primitives;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
-
-namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core;
-
-///
-/// Unit tests for class.
-///
-public sealed class ClientResultExceptionExtensionsTests
-{
- [Fact]
- public void ToHttpOperationExceptionWithContentReturnsValidException()
- {
- // Arrange
- using var response = new FakeResponse("Response Content", 500);
- var exception = new ClientResultException(response);
-
- // Act
- var actualException = exception.ToHttpOperationException();
-
- // Assert
- Assert.IsType(actualException);
- Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode);
- Assert.Equal("Response Content", actualException.ResponseContent);
- Assert.Same(exception, actualException.InnerException);
- }
-
- #region private
-
- private sealed class FakeResponse(string responseContent, int status) : PipelineResponse
- {
- private readonly string _responseContent = responseContent;
- public override BinaryData Content => BinaryData.FromString(this._responseContent);
- public override int Status { get; } = status;
- public override string ReasonPhrase => "Reason Phrase";
- public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); }
- protected override PipelineResponseHeaders HeadersCore => throw new NotImplementedException();
- public override BinaryData BufferContent(CancellationToken cancellationToken = default) => new(this._responseContent);
- public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
- public override void Dispose() { }
- }
-
- #endregion
-}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj
index 0a100b3c13a6..80e71aa16760 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs
index f296000c5245..bfa71f7e5ab3 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.Services;
+using Microsoft.SemanticKernel.TextToAudio;
using Microsoft.SemanticKernel.TextToImage;
using OpenAI;
using Xunit;
@@ -70,4 +72,64 @@ public void ItCanAddTextToImageServiceWithOpenAIClient()
// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}
+
+ [Fact]
+ public void ItCanAddTextToAudioService()
+ {
+ // Arrange
+ var sut = Kernel.CreateBuilder();
+
+ // Act
+ var service = sut.AddOpenAITextToAudio("model", "key")
+ .Build()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public void ItCanAddTextToAudioServiceWithOpenAIClient()
+ {
+ // Arrange
+ var sut = Kernel.CreateBuilder();
+
+ // Act
+ var service = sut.AddOpenAITextToAudio("model", new OpenAIClient("key"))
+ .Build()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public void ItCanAddAudioToTextService()
+ {
+ // Arrange
+ var sut = Kernel.CreateBuilder();
+
+ // Act
+ var service = sut.AddOpenAIAudioToText("model", "key")
+ .Build()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public void ItCanAddAudioToTextServiceWithOpenAIClient()
+ {
+ // Arrange
+ var sut = Kernel.CreateBuilder();
+
+ // Act
+ var service = sut.AddOpenAIAudioToText("model", new OpenAIClient("key"))
+ .Build()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs
index 65db68eea180..79c8024bb93f 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs
@@ -2,8 +2,10 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.Services;
+using Microsoft.SemanticKernel.TextToAudio;
using Microsoft.SemanticKernel.TextToImage;
using OpenAI;
using Xunit;
@@ -71,4 +73,64 @@ public void ItCanAddImageToTextServiceWithOpenAIClient()
// Assert
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}
+
+ [Fact]
+ public void ItCanAddTextToAudioService()
+ {
+ // Arrange
+ var sut = new ServiceCollection();
+
+ // Act
+ var service = sut.AddOpenAITextToAudio("model", "key")
+ .BuildServiceProvider()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public void ItCanAddTextToAudioServiceWithOpenAIClient()
+ {
+ // Arrange
+ var sut = new ServiceCollection();
+
+ // Act
+ var service = sut.AddOpenAITextToAudio("model", new OpenAIClient("key"))
+ .BuildServiceProvider()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public void ItCanAddAudioToTextService()
+ {
+ // Arrange
+ var sut = new ServiceCollection();
+
+ // Act
+ var service = sut.AddOpenAIAudioToText("model", "key")
+ .BuildServiceProvider()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public void ItCanAddAudioToTextServiceWithOpenAIClient()
+ {
+ // Arrange
+ var sut = new ServiceCollection();
+
+ // Act
+ var service = sut.AddOpenAIAudioToText("model", new OpenAIClient("key"))
+ .BuildServiceProvider()
+ .GetRequiredService();
+
+ // Assert
+ Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs
new file mode 100644
index 000000000000..9648670d3de5
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Moq;
+using OpenAI;
+using Xunit;
+using static Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services;
+
+///
+/// Unit tests for class.
+///
+public sealed class OpenAIAudioToTextServiceTests : IDisposable
+{
+ private readonly HttpMessageHandlerStub _messageHandlerStub;
+ private readonly HttpClient _httpClient;
+ private readonly Mock _mockLoggerFactory;
+
+ public OpenAIAudioToTextServiceTests()
+ {
+ this._messageHandlerStub = new HttpMessageHandlerStub();
+ this._httpClient = new HttpClient(this._messageHandlerStub, false);
+ this._mockLoggerFactory = new Mock();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory)
+ {
+ // Arrange & Act
+ var service = includeLoggerFactory ?
+ new OpenAIAudioToTextService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) :
+ new OpenAIAudioToTextService("model-id", "api-key", "organization");
+
+ // Assert
+ Assert.NotNull(service);
+ Assert.Equal("model-id", service.Attributes["ModelId"]);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory)
+ {
+ // Arrange & Act
+ var client = new OpenAIClient("key");
+ var service = includeLoggerFactory ?
+ new OpenAIAudioToTextService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) :
+ new OpenAIAudioToTextService("model-id", client);
+
+ // Assert
+ Assert.NotNull(service);
+ Assert.Equal("model-id", service.Attributes["ModelId"]);
+ }
+
+ [Theory]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default }, "0")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word }, "word")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment }, "segment")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Word }, "word", "segment")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Segment }, "word", "segment")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Word }, "word", "0")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Default }, "word", "0")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Segment }, "segment", "0")]
+ [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Default }, "segment", "0")]
+ public async Task GetTextContentGranularitiesWorksAsync(TimeStampGranularities[] granularities, params string[] expectedGranularities)
+ {
+ // Arrange
+ var service = new OpenAIAudioToTextService("model-id", "api-key", httpClient: this._httpClient);
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new StringContent("Test audio-to-text response")
+ };
+
+ // Act
+ var settings = new OpenAIAudioToTextExecutionSettings("file.mp3") { Granularities = granularities };
+ var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings);
+
+ // Assert
+ Assert.NotNull(this._messageHandlerStub.RequestContent);
+ Assert.NotNull(result);
+
+ var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!);
+ var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase));
+
+ foreach (var granularity in expectedGranularities)
+ {
+ var expectedMultipart = $"{granularity}\r\n{multiPartBreak}";
+ Assert.Contains(expectedMultipart, multiPartData);
+ }
+ }
+
+ [Fact]
+ public async Task GetTextContentByDefaultWorksCorrectlyAsync()
+ {
+ // Arrange
+ var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient);
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new StringContent("Test audio-to-text response")
+ };
+
+ // Act
+ var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("file.mp3"));
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("Test audio-to-text response", result[0].Text);
+ }
+
+ [Fact]
+ public async Task GetTextContentsDoesLogActionAsync()
+ {
+ // Assert
+ var modelId = "whisper-1";
+ var logger = new Mock>();
+ logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true);
+
+ this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object);
+
+ // Arrange
+ var sut = new OpenAIAudioToTextService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object);
+
+ // Act
+ await sut.GetTextContentsAsync(new(new byte[] { 0x01, 0x02 }, "text/plain"));
+
+ // Assert
+ logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIAudioToTextService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once());
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this._messageHandlerStub.Dispose();
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs
new file mode 100644
index 000000000000..e8fdb7b46b1e
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs
@@ -0,0 +1,205 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services;
+
+///
+/// Unit tests for class.
+///
+public sealed class OpenAITextToAudioServiceTests : IDisposable
+{
+ private readonly HttpMessageHandlerStub _messageHandlerStub;
+ private readonly HttpClient _httpClient;
+ private readonly Mock _mockLoggerFactory;
+
+ public OpenAITextToAudioServiceTests()
+ {
+ this._messageHandlerStub = new HttpMessageHandlerStub();
+ this._httpClient = new HttpClient(this._messageHandlerStub, false);
+ this._mockLoggerFactory = new Mock();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory)
+ {
+ // Arrange & Act
+ var service = includeLoggerFactory ?
+ new OpenAITextToAudioService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) :
+ new OpenAITextToAudioService("model-id", "api-key", "organization");
+
+ // Assert
+ Assert.NotNull(service);
+ Assert.Equal("model-id", service.Attributes["ModelId"]);
+ }
+
+ [Theory]
+ [MemberData(nameof(ExecutionSettings))]
+ public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAITextToAudioExecutionSettings? settings, Type expectedExceptionType)
+ {
+ // Arrange
+ var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient);
+ await using var stream = new MemoryStream([0x00, 0x00, 0xFF, 0x7F]);
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StreamContent(stream)
+ };
+
+ // Act
+ var exception = await Assert.ThrowsAnyAsync(async () => await service.GetAudioContentsAsync("Some text", settings));
+
+ // Assert
+ Assert.NotNull(exception);
+ Assert.IsType(expectedExceptionType, exception);
+ }
+
+ [Fact]
+ public async Task GetAudioContentByDefaultWorksCorrectlyAsync()
+ {
+ // Arrange
+ byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F];
+
+ var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient);
+ await using var stream = new MemoryStream(expectedByteArray);
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StreamContent(stream)
+ };
+
+ // Act
+ var result = await service.GetAudioContentsAsync("Some text");
+
+ // Assert
+ var audioData = result[0].Data!.Value;
+ Assert.False(audioData.IsEmpty);
+ Assert.True(audioData.Span.SequenceEqual(expectedByteArray));
+ }
+
+ [Theory]
+ [InlineData("echo", "wav")]
+ [InlineData("fable", "opus")]
+ [InlineData("onyx", "flac")]
+ [InlineData("nova", "aac")]
+ [InlineData("shimmer", "pcm")]
+ public async Task GetAudioContentVoicesWorksCorrectlyAsync(string voice, string format)
+ {
+ // Arrange
+ byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F];
+
+ var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient);
+ await using var stream = new MemoryStream(expectedByteArray);
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StreamContent(stream)
+ };
+
+ // Act
+ var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings(voice) { ResponseFormat = format });
+
+ // Assert
+ var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!);
+ var audioData = result[0].Data!.Value;
+ Assert.Contains($"\"voice\":\"{voice}\"", requestBody);
+ Assert.Contains($"\"response_format\":\"{format}\"", requestBody);
+ Assert.False(audioData.IsEmpty);
+ Assert.True(audioData.Span.SequenceEqual(expectedByteArray));
+ }
+
+ [Fact]
+ public async Task GetAudioContentThrowsWhenVoiceIsNotSupportedAsync()
+ {
+ // Arrange
+ byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F];
+
+ var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice")));
+ }
+
+ [Fact]
+ public async Task GetAudioContentThrowsWhenFormatIsNotSupportedAsync()
+ {
+ // Arrange
+ byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F];
+
+ var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings() { ResponseFormat = "not supported" }));
+ }
+
+ [Theory]
+ [InlineData(true, "http://local-endpoint")]
+ [InlineData(false, "https://api.openai.com")]
+ public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAddress, string expectedBaseAddress)
+ {
+ // Arrange
+ byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F];
+
+ if (useHttpClientBaseAddress)
+ {
+ this._httpClient.BaseAddress = new Uri("http://local-endpoint");
+ }
+
+ var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient);
+ await using var stream = new MemoryStream(expectedByteArray);
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StreamContent(stream)
+ };
+
+ // Act
+ var result = await service.GetAudioContentsAsync("Some text");
+
+ // Assert
+ Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture);
+ }
+
+ [Fact]
+ public async Task GetAudioContentDoesLogActionAsync()
+ {
+ // Assert
+ var modelId = "whisper-1";
+ var logger = new Mock>();
+ logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true);
+
+ this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object);
+
+ // Arrange
+ var sut = new OpenAITextToAudioService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object);
+
+ // Act
+ await sut.GetAudioContentsAsync("description");
+
+ // Assert
+ logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextToAudioService.GetAudioContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once());
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this._messageHandlerStub.Dispose();
+ }
+
+ public static TheoryData ExecutionSettings => new()
+ {
+ { new OpenAITextToAudioExecutionSettings("invalid"), typeof(NotSupportedException) },
+ };
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs
index 919b864327e8..f449059e8ab5 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs
@@ -11,7 +11,7 @@
using OpenAI;
using Xunit;
-namespace SemanticKernel.Connectors.UnitTests.OpenAI.Services;
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services;
///
/// Unit tests for class.
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs
new file mode 100644
index 000000000000..e01345c82f03
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UniTests.Settings;
+
+///
+/// Unit tests for class.
+///
+public sealed class OpenAIAudioToTextExecutionSettingsTests
+{
+ [Fact]
+ public void ItReturnsDefaultSettingsWhenSettingsAreNull()
+ {
+ Assert.NotNull(OpenAIAudioToTextExecutionSettings.FromExecutionSettings(null));
+ }
+
+ [Fact]
+ public void ItReturnsValidOpenAIAudioToTextExecutionSettings()
+ {
+ // Arrange
+ var audioToTextSettings = new OpenAIAudioToTextExecutionSettings("file.mp3")
+ {
+ ModelId = "model_id",
+ Language = "en",
+ Prompt = "prompt",
+ ResponseFormat = "text",
+ Temperature = 0.2f
+ };
+
+ // Act
+ var settings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(audioToTextSettings);
+
+ // Assert
+ Assert.Same(audioToTextSettings, settings);
+ }
+
+ [Fact]
+ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson()
+ {
+ // Arrange
+ var json = """
+ {
+ "model_id": "model_id",
+ "language": "en",
+ "filename": "file.mp3",
+ "prompt": "prompt",
+ "response_format": "text",
+ "temperature": 0.2
+ }
+ """;
+
+ var executionSettings = JsonSerializer.Deserialize(json);
+
+ // Act
+ var settings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings);
+
+ // Assert
+ Assert.NotNull(settings);
+ Assert.Equal("model_id", settings.ModelId);
+ Assert.Equal("en", settings.Language);
+ Assert.Equal("file.mp3", settings.Filename);
+ Assert.Equal("prompt", settings.Prompt);
+ Assert.Equal("text", settings.ResponseFormat);
+ Assert.Equal(0.2f, settings.Temperature);
+ }
+
+ [Fact]
+ public void ItClonesAllProperties()
+ {
+ var settings = new OpenAIAudioToTextExecutionSettings()
+ {
+ ModelId = "model_id",
+ Language = "en",
+ Prompt = "prompt",
+ ResponseFormat = "text",
+ Temperature = 0.2f,
+ Filename = "something.mp3",
+ };
+
+ var clone = (OpenAIAudioToTextExecutionSettings)settings.Clone();
+ Assert.NotSame(settings, clone);
+
+ Assert.Equal("model_id", clone.ModelId);
+ Assert.Equal("en", clone.Language);
+ Assert.Equal("prompt", clone.Prompt);
+ Assert.Equal("text", clone.ResponseFormat);
+ Assert.Equal(0.2f, clone.Temperature);
+ Assert.Equal("something.mp3", clone.Filename);
+ }
+
+ [Fact]
+ public void ItFreezesAndPreventsMutation()
+ {
+ var settings = new OpenAIAudioToTextExecutionSettings()
+ {
+ ModelId = "model_id",
+ Language = "en",
+ Prompt = "prompt",
+ ResponseFormat = "text",
+ Temperature = 0.2f,
+ Filename = "something.mp3",
+ };
+
+ settings.Freeze();
+ Assert.True(settings.IsFrozen);
+
+ Assert.Throws(() => settings.ModelId = "new_model");
+ Assert.Throws(() => settings.Language = "some_format");
+ Assert.Throws(() => settings.Prompt = "prompt");
+ Assert.Throws(() => settings.ResponseFormat = "something");
+ Assert.Throws(() => settings.Temperature = 0.2f);
+ Assert.Throws(() => settings.Filename = "something");
+
+ settings.Freeze(); // idempotent
+ Assert.True(settings.IsFrozen);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs
new file mode 100644
index 000000000000..f30478e15acf
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UniTests.Settings;
+
+///
+/// Unit tests for class.
+///
+public sealed class OpenAITextToAudioExecutionSettingsTests
+{
+ [Fact]
+ public void ItReturnsDefaultSettingsWhenSettingsAreNull()
+ {
+ Assert.NotNull(OpenAITextToAudioExecutionSettings.FromExecutionSettings(null));
+ }
+
+ [Fact]
+ public void ItReturnsValidOpenAITextToAudioExecutionSettings()
+ {
+ // Arrange
+ var textToAudioSettings = new OpenAITextToAudioExecutionSettings("voice")
+ {
+ ModelId = "model_id",
+ ResponseFormat = "mp3",
+ Speed = 1.0f
+ };
+
+ // Act
+ var settings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(textToAudioSettings);
+
+ // Assert
+ Assert.Same(textToAudioSettings, settings);
+ }
+
+ [Fact]
+ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson()
+ {
+ // Arrange
+ var json = """
+ {
+ "model_id": "model_id",
+ "voice": "voice",
+ "response_format": "mp3",
+ "speed": 1.2
+ }
+ """;
+
+ var executionSettings = JsonSerializer.Deserialize(json);
+
+ // Act
+ var settings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings);
+
+ // Assert
+ Assert.NotNull(settings);
+ Assert.Equal("model_id", settings.ModelId);
+ Assert.Equal("voice", settings.Voice);
+ Assert.Equal("mp3", settings.ResponseFormat);
+ Assert.Equal(1.2f, settings.Speed);
+ }
+
+ [Fact]
+ public void ItClonesAllProperties()
+ {
+ var textToAudioSettings = new OpenAITextToAudioExecutionSettings()
+ {
+ ModelId = "some_model",
+ ResponseFormat = "some_format",
+ Speed = 3.14f,
+ Voice = "something"
+ };
+
+ var clone = (OpenAITextToAudioExecutionSettings)textToAudioSettings.Clone();
+ Assert.NotSame(textToAudioSettings, clone);
+
+ Assert.Equal("some_model", clone.ModelId);
+ Assert.Equal("some_format", clone.ResponseFormat);
+ Assert.Equal(3.14f, clone.Speed);
+ Assert.Equal("something", clone.Voice);
+ }
+
+ [Fact]
+ public void ItFreezesAndPreventsMutation()
+ {
+ var textToAudioSettings = new OpenAITextToAudioExecutionSettings()
+ {
+ ModelId = "some_model",
+ ResponseFormat = "some_format",
+ Speed = 3.14f,
+ Voice = "something"
+ };
+
+ textToAudioSettings.Freeze();
+ Assert.True(textToAudioSettings.IsFrozen);
+
+ Assert.Throws(() => textToAudioSettings.ModelId = "new_model");
+ Assert.Throws(() => textToAudioSettings.ResponseFormat = "some_format");
+ Assert.Throws(() => textToAudioSettings.Speed = 3.14f);
+ Assert.Throws(() => textToAudioSettings.Voice = "something");
+
+ textToAudioSettings.Freeze(); // idempotent
+ Assert.True(textToAudioSettings.IsFrozen);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs
new file mode 100644
index 000000000000..77ec85fe9c10
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenAI.Audio;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Base class for AI clients that provides common functionality for interacting with OpenAI services.
+///
+internal partial class ClientCore
+{
+ ///
+ /// Generates an image with the provided configuration.
+ ///
+ /// Input audio to generate the text
+ /// Audio-to-text execution settings for the prompt
+ /// The to monitor for cancellation requests. The default is .
+ /// Url of the generated image
+ internal async Task> GetTextFromAudioContentsAsync(
+ AudioContent input,
+ PromptExecutionSettings? executionSettings,
+ CancellationToken cancellationToken)
+ {
+ if (!input.CanRead)
+ {
+ throw new ArgumentException("The input audio content is not readable.", nameof(input));
+ }
+
+ OpenAIAudioToTextExecutionSettings audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!;
+ AudioTranscriptionOptions? audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings);
+
+ Verify.ValidFilename(audioExecutionSettings?.Filename);
+
+ using var memoryStream = new MemoryStream(input.Data!.Value.ToArray());
+
+ AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value;
+
+ return [new(responseData.Text, this.ModelId, metadata: GetResponseMetadata(responseData))];
+ }
+
+ ///
+ /// Converts to type.
+ ///
+ /// Instance of .
+ /// Instance of .
+ private static AudioTranscriptionOptions? AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings)
+ => new()
+ {
+ Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities),
+ Language = executionSettings.Language,
+ Prompt = executionSettings.Prompt,
+ Temperature = executionSettings.Temperature
+ };
+
+ private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities)
+ {
+ AudioTimestampGranularities result = AudioTimestampGranularities.Default;
+
+ if (granularities is not null)
+ {
+ foreach (var granularity in granularities)
+ {
+ var openAIGranularity = granularity switch
+ {
+ OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word,
+ OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment,
+ _ => AudioTimestampGranularities.Default
+ };
+
+ result |= openAIGranularity;
+ }
+ }
+
+ return result;
+ }
+
+ private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription)
+ => new(3)
+ {
+ [nameof(audioTranscription.Language)] = audioTranscription.Language,
+ [nameof(audioTranscription.Duration)] = audioTranscription.Duration,
+ [nameof(audioTranscription.Segments)] = audioTranscription.Segments
+ };
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs
new file mode 100644
index 000000000000..75e484a489aa
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenAI.Audio;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Base class for AI clients that provides common functionality for interacting with OpenAI services.
+///
+internal partial class ClientCore
+{
+ ///
+ /// Generates an image with the provided configuration.
+ ///
+ /// Prompt to generate the image
+ /// Text to Audio execution settings for the prompt
+ /// The to monitor for cancellation requests. The default is .
+ /// Url of the generated image
+ internal async Task> GetAudioContentsAsync(
+ string prompt,
+ PromptExecutionSettings? executionSettings,
+ CancellationToken cancellationToken)
+ {
+ Verify.NotNullOrWhiteSpace(prompt);
+
+ OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings);
+ var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings?.ResponseFormat);
+ SpeechGenerationOptions options = new()
+ {
+ ResponseFormat = responseFormat,
+ Speed = audioExecutionSettings?.Speed,
+ };
+
+ ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false);
+
+ return [new AudioContent(response.Value.ToArray(), mimeType)];
+ }
+
+ private static GeneratedSpeechVoice GetGeneratedSpeechVoice(string? voice)
+ => voice?.ToUpperInvariant() switch
+ {
+ "ALLOY" => GeneratedSpeechVoice.Alloy,
+ "ECHO" => GeneratedSpeechVoice.Echo,
+ "FABLE" => GeneratedSpeechVoice.Fable,
+ "ONYX" => GeneratedSpeechVoice.Onyx,
+ "NOVA" => GeneratedSpeechVoice.Nova,
+ "SHIMMER" => GeneratedSpeechVoice.Shimmer,
+ _ => throw new NotSupportedException($"The voice '{voice}' is not supported."),
+ };
+
+ private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeechFormatAndMimeType(string? format)
+ => format?.ToUpperInvariant() switch
+ {
+ "WAV" => (GeneratedSpeechFormat.Wav, "audio/wav"),
+ "MP3" => (GeneratedSpeechFormat.Mp3, "audio/mpeg"),
+ "OPUS" => (GeneratedSpeechFormat.Opus, "audio/opus"),
+ "FLAC" => (GeneratedSpeechFormat.Flac, "audio/flac"),
+ "AAC" => (GeneratedSpeechFormat.Aac, "audio/aac"),
+ "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"),
+ _ => throw new NotSupportedException($"The format '{format}' is not supported.")
+ };
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs
index 567d82726e4b..ce4a4d9866e0 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs
@@ -1,13 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
+/* Phase 4
+- Added missing OpenAIClient extensions for audio
+- Updated the Experimental attribute to the correct value 0001 -> 0010 (Connector)
+ */
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.Http;
+using Microsoft.SemanticKernel.TextToAudio;
using Microsoft.SemanticKernel.TextToImage;
using OpenAI;
@@ -89,7 +95,7 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration(
#region Text to Image
///
- /// Add the OpenAI Dall-E text to image service to the list
+ /// Add the OpenAI text-to-image service to the list
///
/// The instance to augment.
/// OpenAI model name, see https://platform.openai.com/docs/models
@@ -115,7 +121,7 @@ public static IKernelBuilder AddOpenAITextToImage(
}
///
- /// Add the OpenAI Dall-E text to image service to the list
+ /// Add the OpenAI text-to-image service to the list
///
/// The instance to augment.
/// The model to use for image generation.
@@ -149,4 +155,136 @@ public static IKernelBuilder AddOpenAITextToImage(
return builder;
}
#endregion
+
+ #region Text to Audio
+
+ ///
+ /// Adds the OpenAI text-to-audio service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model name, see https://platform.openai.com/docs/models
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// Non-default endpoint for the OpenAI API.
+ /// The HttpClient to use with this service.
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IKernelBuilder AddOpenAITextToAudio(
+ this IKernelBuilder builder,
+ string modelId,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null,
+ Uri? endpoint = null,
+ HttpClient? httpClient = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAITextToAudioService(
+ modelId,
+ apiKey,
+ orgId,
+ endpoint,
+ HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
+ serviceProvider.GetService()));
+
+ return builder;
+ }
+
+ ///
+ /// Add the OpenAI text-to-audio service to the list
+ ///
+ /// The instance to augment.
+ /// OpenAI model name, see https://platform.openai.com/docs/models
+ /// to use for the service. If null, one must be available in the service provider when this service is resolved.
+ /// A local identifier for the given AI service
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IKernelBuilder AddOpenAITextToAudio(
+ this IKernelBuilder builder,
+ string modelId,
+ OpenAIClient? openAIClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAITextToAudioService(
+ modelId,
+ openAIClient ?? serviceProvider.GetRequiredService(),
+ serviceProvider.GetService()));
+
+ return builder;
+ }
+
+ #endregion
+
+ #region Audio-to-Text
+
+ ///
+ /// Adds the OpenAI audio-to-text service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model name, see https://platform.openai.com/docs/models
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// Non-default endpoint for the OpenAI API.
+ /// The HttpClient to use with this service.
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IKernelBuilder AddOpenAIAudioToText(
+ this IKernelBuilder builder,
+ string modelId,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null,
+ Uri? endpoint = null,
+ HttpClient? httpClient = null)
+ {
+ Verify.NotNull(builder);
+
+ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) =>
+ new(modelId,
+ apiKey,
+ orgId,
+ endpoint,
+ HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
+ serviceProvider.GetService());
+
+ builder.Services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return builder;
+ }
+
+ ///
+ /// Adds the OpenAI audio-to-text service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model id
+ /// to use for the service. If null, one must be available in the service provider when this service is resolved.
+ /// A local identifier for the given AI service
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IKernelBuilder AddOpenAIAudioToText(
+ this IKernelBuilder builder,
+ string modelId,
+ OpenAIClient? openAIClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(builder);
+
+ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) =>
+ new(modelId,
+ openAIClient ?? serviceProvider.GetRequiredService(),
+ serviceProvider.GetService());
+
+ builder.Services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return builder;
+ }
+
+ #endregion
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs
index 77355de7f24e..769634c1cea7 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs
@@ -4,9 +4,11 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.Http;
+using Microsoft.SemanticKernel.TextToAudio;
using Microsoft.SemanticKernel.TextToImage;
using OpenAI;
@@ -89,7 +91,7 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceC
#region Text to Image
///
- /// Add the OpenAI Dall-E text to image service to the list
+ /// Add the OpenAI text-to-image service to the list
///
/// The instance to augment.
/// The model to use for image generation.
@@ -143,4 +145,125 @@ public static IServiceCollection AddOpenAITextToImage(this IServiceCollection se
serviceProvider.GetService()));
}
#endregion
+
+ #region Text to Audio
+
+ ///
+ /// Adds the OpenAI text-to-audio service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model name, see https://platform.openai.com/docs/models
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// Non-default endpoint for the OpenAI API.
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IServiceCollection AddOpenAITextToAudio(
+ this IServiceCollection services,
+ string modelId,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null,
+ Uri? endpoint = null)
+ {
+ Verify.NotNull(services);
+
+ return services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAITextToAudioService(
+ modelId,
+ apiKey,
+ orgId,
+ endpoint,
+ HttpClientProvider.GetHttpClient(serviceProvider),
+ serviceProvider.GetService()));
+ }
+
+ ///
+ /// Adds the OpenAI text-to-audio service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model name, see https://platform.openai.com/docs/models
+ /// to use for the service. If null, one must be available in the service provider when this service is resolved.
+ /// A local identifier for the given AI service
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IServiceCollection AddOpenAITextToAudio(
+ this IServiceCollection services,
+ string modelId,
+ OpenAIClient? openAIClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+
+ return services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ new OpenAITextToAudioService(
+ modelId,
+ openAIClient ?? serviceProvider.GetRequiredService(),
+ serviceProvider.GetService()));
+ }
+
+ #endregion
+
+ #region Audio-to-Text
+
+ ///
+ /// Adds the OpenAI audio-to-text service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model name, see https://platform.openai.com/docs/models
+ /// OpenAI API key, see https://platform.openai.com/account/api-keys
+ /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.
+ /// A local identifier for the given AI service
+ /// Non-default endpoint for the OpenAI API.
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IServiceCollection AddOpenAIAudioToText(
+ this IServiceCollection services,
+ string modelId,
+ string apiKey,
+ string? orgId = null,
+ string? serviceId = null,
+ Uri? endpoint = null)
+ {
+ Verify.NotNull(services);
+
+ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) =>
+ new(modelId,
+ apiKey,
+ orgId,
+ endpoint,
+ HttpClientProvider.GetHttpClient(serviceProvider),
+ serviceProvider.GetService());
+
+ services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return services;
+ }
+
+ ///
+ /// Adds the OpenAI audio-to-text service to the list.
+ ///
+ /// The instance to augment.
+ /// OpenAI model id
+ /// to use for the service. If null, one must be available in the service provider when this service is resolved.
+ /// A local identifier for the given AI service
+ /// The same instance as .
+ [Experimental("SKEXP0010")]
+ public static IServiceCollection AddOpenAIAudioToText(
+ this IServiceCollection services,
+ string modelId,
+ OpenAIClient? openAIClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+
+ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) =>
+ new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService());
+
+ services.AddKeyedSingleton(serviceId, (Func)Factory);
+
+ return services;
+ }
+ #endregion
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs
new file mode 100644
index 000000000000..a226d6c59040
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.AudioToText;
+using Microsoft.SemanticKernel.Services;
+using OpenAI;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// OpenAI text-to-audio service.
+///
+[Experimental("SKEXP0010")]
+public sealed class OpenAIAudioToTextService : IAudioToTextService
+{
+ ///
+ /// OpenAI text-to-audio client for HTTP operations.
+ ///
+ private readonly ClientCore _client;
+
+ ///
+ /// Gets the attribute name used to store the organization in the dictionary.
+ ///
+ public static string OrganizationKey => "Organization";
+
+ ///
+ public IReadOnlyDictionary Attributes => this._client.Attributes;
+
+ ///
+ /// Creates an instance of the with API key auth.
+ ///
+ /// Model name
+ /// OpenAI API Key
+ /// OpenAI Organization Id (usually optional)
+ /// Non-default endpoint for the OpenAI API.
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ public OpenAIAudioToTextService(
+ string modelId,
+ string apiKey,
+ string? organization = null,
+ Uri? endpoint = null,
+ HttpClient? httpClient = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService)));
+ }
+
+ ///
+ /// Creates an instance of the with API key auth.
+ ///
+ /// Model name
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ public OpenAIAudioToTextService(
+ string modelId,
+ OpenAIClient openAIClient,
+ ILoggerFactory? loggerFactory = null)
+ {
+ this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService)));
+ }
+
+ ///
+ public Task> GetTextContentsAsync(
+ AudioContent content,
+ PromptExecutionSettings? executionSettings = null,
+ Kernel? kernel = null,
+ CancellationToken cancellationToken = default)
+ {
+ this._client.LogActionDetails();
+ return this._client.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs
index a4dd48ba75e3..ea607b2565b3 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs
@@ -22,7 +22,7 @@ Adding the non-default endpoint parameter to the constructor.
[Experimental("SKEXP0010")]
public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService
{
- private readonly ClientCore _core;
+ private readonly ClientCore _client;
private readonly int? _dimensions;
///
@@ -44,7 +44,7 @@ public OpenAITextEmbeddingGenerationService(
ILoggerFactory? loggerFactory = null,
int? dimensions = null)
{
- this._core = new(
+ this._client = new(
modelId: modelId,
apiKey: apiKey,
endpoint: endpoint,
@@ -68,12 +68,12 @@ public OpenAITextEmbeddingGenerationService(
ILoggerFactory? loggerFactory = null,
int? dimensions = null)
{
- this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
+ this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
this._dimensions = dimensions;
}
///
- public IReadOnlyDictionary Attributes => this._core.Attributes;
+ public IReadOnlyDictionary Attributes => this._client.Attributes;
///
public Task>> GenerateEmbeddingsAsync(
@@ -81,7 +81,7 @@ public Task>> GenerateEmbeddingsAsync(
Kernel? kernel = null,
CancellationToken cancellationToken = default)
{
- this._core.LogActionDetails();
- return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken);
+ this._client.LogActionDetails();
+ return this._client.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken);
}
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs
new file mode 100644
index 000000000000..87346eefb1b5
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Services;
+using Microsoft.SemanticKernel.TextToAudio;
+using OpenAI;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// OpenAI text-to-audio service.
+///
+[Experimental("SKEXP0010")]
+public sealed class OpenAITextToAudioService : ITextToAudioService
+{
+ ///
+ /// OpenAI text-to-audio client for HTTP operations.
+ ///
+ private readonly ClientCore _client;
+
+ ///
+ /// Gets the attribute name used to store the organization in the dictionary.
+ ///
+ public static string OrganizationKey => "Organization";
+
+ ///
+ public IReadOnlyDictionary Attributes => this._client.Attributes;
+
+ ///
+ /// Creates an instance of the with API key auth.
+ ///
+ /// Model name
+ /// OpenAI API Key
+ /// OpenAI Organization Id (usually optional)
+ /// Non-default endpoint for the OpenAI API.
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ public OpenAITextToAudioService(
+ string modelId,
+ string apiKey,
+ string? organization = null,
+ Uri? endpoint = null,
+ HttpClient? httpClient = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService)));
+ }
+
+ ///
+ /// Creates an instance of the with API key auth.
+ ///
+ /// Model name
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ public OpenAITextToAudioService(
+ string modelId,
+ OpenAIClient openAIClient,
+ ILoggerFactory? loggerFactory = null)
+ {
+ this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService)));
+ }
+
+ ///
+ public Task> GetAudioContentsAsync(
+ string text,
+ PromptExecutionSettings? executionSettings = null,
+ Kernel? kernel = null,
+ CancellationToken cancellationToken = default)
+ {
+ this._client.LogActionDetails();
+ return this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs
index 55eca0e112eb..1a6038aa3f43 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs
@@ -28,10 +28,10 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI;
[Experimental("SKEXP0010")]
public class OpenAITextToImageService : ITextToImageService
{
- private readonly ClientCore _core;
+ private readonly ClientCore _client;
///
- public IReadOnlyDictionary Attributes => this._core.Attributes;
+ public IReadOnlyDictionary Attributes => this._client.Attributes;
///
/// Initializes a new instance of the class.
@@ -50,7 +50,7 @@ public OpenAITextToImageService(
HttpClient? httpClient = null,
ILoggerFactory? loggerFactory = null)
{
- this._core = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType()));
+ this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType()));
}
///
@@ -64,13 +64,13 @@ public OpenAITextToImageService(
OpenAIClient openAIClient,
ILoggerFactory? loggerFactory = null)
{
- this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
+ this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
}
///
public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default)
{
- this._core.LogActionDetails();
- return this._core.GenerateImageAsync(description, width, height, cancellationToken);
+ this._client.LogActionDetails();
+ return this._client.GenerateImageAsync(description, width, height, cancellationToken);
}
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs
new file mode 100644
index 000000000000..5d87768c5ddd
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.SemanticKernel.Text;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Execution settings for OpenAI audio-to-text request.
+///
+[Experimental("SKEXP0010")]
+public sealed class OpenAIAudioToTextExecutionSettings : PromptExecutionSettings
+{
+ ///
+ /// Filename or identifier associated with audio data.
+ /// Should be in format {filename}.{extension}
+ ///
+ [JsonPropertyName("filename")]
+ public string Filename
+ {
+ get => this._filename;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._filename = value;
+ }
+ }
+
+ ///
+ /// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es').
+ ///
+ [JsonPropertyName("language")]
+ public string? Language
+ {
+ get => this._language;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._language = value;
+ }
+ }
+
+ ///
+ /// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.
+ ///
+ [JsonPropertyName("prompt")]
+ public string? Prompt
+ {
+ get => this._prompt;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._prompt = value;
+ }
+ }
+
+ ///
+ /// The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. Default is 'json'.
+ ///
+ [JsonPropertyName("response_format")]
+ public string ResponseFormat
+ {
+ get => this._responseFormat;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._responseFormat = value;
+ }
+ }
+
+ ///
+ /// The sampling temperature, between 0 and 1.
+ /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
+ /// If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit.
+ /// Default is 0.
+ ///
+ [JsonPropertyName("temperature")]
+ public float Temperature
+ {
+ get => this._temperature;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._temperature = value;
+ }
+ }
+
+ ///
+ /// The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment.
+ ///
+ [JsonPropertyName("granularities")]
+ public IReadOnlyList? Granularities { get; set; }
+
+ ///
+ /// Creates an instance of class with default filename - "file.mp3".
+ ///
+ public OpenAIAudioToTextExecutionSettings()
+ : this(DefaultFilename)
+ {
+ }
+
+ ///
+ /// Creates an instance of class.
+ ///
+ /// Filename or identifier associated with audio data. Should be in format {filename}.{extension}
+ public OpenAIAudioToTextExecutionSettings(string filename)
+ {
+ this._filename = filename;
+ }
+
+ ///
+ public override PromptExecutionSettings Clone()
+ {
+ return new OpenAIAudioToTextExecutionSettings(this.Filename)
+ {
+ ModelId = this.ModelId,
+ ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null,
+ Temperature = this.Temperature,
+ ResponseFormat = this.ResponseFormat,
+ Language = this.Language,
+ Prompt = this.Prompt
+ };
+ }
+
+ ///
+ /// Converts to derived type.
+ ///
+ /// Instance of .
+ /// Instance of .
+ public static OpenAIAudioToTextExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings)
+ {
+ if (executionSettings is null)
+ {
+ return new OpenAIAudioToTextExecutionSettings();
+ }
+
+ if (executionSettings is OpenAIAudioToTextExecutionSettings settings)
+ {
+ return settings;
+ }
+
+ var json = JsonSerializer.Serialize(executionSettings);
+
+ var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive);
+
+ return openAIExecutionSettings!;
+ }
+
+ ///
+ /// The timestamp granularities available to populate transcriptions.
+ ///
+ public enum TimeStampGranularities
+ {
+ ///
+ /// Not specified.
+ ///
+ Default = 0,
+
+ ///
+ /// The transcription is segmented by word.
+ ///
+ Word = 1,
+
+ ///
+ /// The timestamp of transcription is by segment.
+ ///
+ Segment = 2,
+ }
+
+ #region private ================================================================================
+
+ private const string DefaultFilename = "file.mp3";
+
+ private float _temperature = 0;
+ private string _responseFormat = "json";
+ private string _filename;
+ private string? _language;
+ private string? _prompt;
+
+ #endregion
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs
new file mode 100644
index 000000000000..8fca703901eb
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/* Phase 4
+Bringing the OpenAITextToAudioExecutionSettings class to the OpenAIV2 connector as is
+
+*/
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.SemanticKernel.Text;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Execution settings for OpenAI text-to-audio request.
+///
+[Experimental("SKEXP0001")]
+public sealed class OpenAITextToAudioExecutionSettings : PromptExecutionSettings
+{
+ ///
+ /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer.
+ ///
+ [JsonPropertyName("voice")]
+ public string Voice
+ {
+ get => this._voice;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._voice = value;
+ }
+ }
+
+ ///
+ /// The format to audio in. Supported formats are mp3, opus, aac, and flac.
+ ///
+ [JsonPropertyName("response_format")]
+ public string ResponseFormat
+ {
+ get => this._responseFormat;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._responseFormat = value;
+ }
+ }
+
+ ///
+ /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default.
+ ///
+ [JsonPropertyName("speed")]
+ public float Speed
+ {
+ get => this._speed;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._speed = value;
+ }
+ }
+
+ ///
+ /// Creates an instance of class with default voice - "alloy".
+ ///
+ public OpenAITextToAudioExecutionSettings()
+ : this(DefaultVoice)
+ {
+ }
+
+ ///
+ /// Creates an instance of class.
+ ///
+ /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer.
+ public OpenAITextToAudioExecutionSettings(string? voice)
+ {
+ this._voice = voice ?? DefaultVoice;
+ }
+
+ ///
+ public override PromptExecutionSettings Clone()
+ {
+ return new OpenAITextToAudioExecutionSettings(this.Voice)
+ {
+ ModelId = this.ModelId,
+ ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null,
+ Speed = this.Speed,
+ ResponseFormat = this.ResponseFormat
+ };
+ }
+
+ ///
+ /// Converts to derived type.
+ ///
+ /// Instance of .
+ /// Instance of .
+ public static OpenAITextToAudioExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings)
+ {
+ if (executionSettings is null)
+ {
+ return new OpenAITextToAudioExecutionSettings();
+ }
+
+ if (executionSettings is OpenAITextToAudioExecutionSettings settings)
+ {
+ return settings;
+ }
+
+ var json = JsonSerializer.Serialize(executionSettings);
+
+ var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive);
+
+ return openAIExecutionSettings!;
+ }
+
+ #region private ================================================================================
+
+ private const string DefaultVoice = "alloy";
+
+ private float _speed = 1.0f;
+ private string _responseFormat = "mp3";
+ private string _voice;
+
+ #endregion
+}
diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs
new file mode 100644
index 000000000000..f1ead5f9b9c5
--- /dev/null
+++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.AudioToText;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using SemanticKernel.IntegrationTests.TestSettings;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Connectors.OpenAI;
+
+public sealed class OpenAIAudioToTextTests()
+{
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ [Fact]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")]
+ public async Task OpenAIAudioToTextTestAsync()
+ {
+ // Arrange
+ const string Filename = "test_audio.wav";
+
+ OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIAudioToText").Get();
+ Assert.NotNull(openAIConfiguration);
+
+ var kernel = Kernel.CreateBuilder()
+ .AddOpenAIAudioToText(openAIConfiguration.ModelId, openAIConfiguration.ApiKey)
+ .Build();
+
+ var service = kernel.GetRequiredService();
+
+ await using Stream audio = File.OpenRead($"./TestData/{Filename}");
+ var audioData = await BinaryData.FromStreamAsync(audio);
+
+ // Act
+ var result = await service.GetTextContentAsync(new AudioContent(audioData, mimeType: "audio/wav"), new OpenAIAudioToTextExecutionSettings(Filename));
+
+ // Assert
+ Assert.Contains("The sun rises in the east and sets in the west.", result.Text, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs
new file mode 100644
index 000000000000..c2818abe2502
--- /dev/null
+++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.TextToAudio;
+using SemanticKernel.IntegrationTests.TestSettings;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Connectors.OpenAI;
+
+public sealed class OpenAITextToAudioTests
+{
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ [Fact]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")]
+ public async Task OpenAITextToAudioTestAsync()
+ {
+ // Arrange
+ OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToAudio").Get();
+ Assert.NotNull(openAIConfiguration);
+
+ var kernel = Kernel.CreateBuilder()
+ .AddOpenAITextToAudio(openAIConfiguration.ModelId, openAIConfiguration.ApiKey)
+ .Build();
+
+ var service = kernel.GetRequiredService();
+
+ // Act
+ var result = await service.GetAudioContentAsync("The sun rises in the east and sets in the west.");
+
+ // Assert
+ var audioData = result.Data!.Value;
+ Assert.False(audioData.IsEmpty);
+ }
+}
diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav b/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav
new file mode 100644
index 000000000000..c6d0edd9a931
Binary files /dev/null and b/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav differ
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs b/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs
similarity index 94%
rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs
rename to dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs
index 7da92e5826ba..75cc074b862d 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs
+++ b/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs
@@ -7,13 +7,14 @@ Preserved the logic as is.
*/
using System.ClientModel;
+using System.Diagnostics.CodeAnalysis;
using System.Net;
-
-namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel;
///
/// Provides extension methods for the class.
///
+[ExcludeFromCodeCoverage]
internal static class ClientResultExceptionExtensions
{
///
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
similarity index 95%
rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
rename to dotnet/src/SemanticKernel.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
index 0b95f904d893..f7a4e947ec38 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
+++ b/dotnet/src/SemanticKernel.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
@@ -2,10 +2,9 @@
using System.ClientModel;
using System.ClientModel.Primitives;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
using Xunit;
-namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions;
+namespace SemanticKernel.UnitTests.Utilities.OpenAI;
public class ClientResultExceptionExtensionsTests
{
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs
similarity index 98%
rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs
rename to dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs
index 6fe18b9c1684..2e254c53d04e 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs
+++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs
@@ -12,7 +12,7 @@ This class was imported and adapted from the System.ClientModel Unit Tests.
using System.Threading;
using System.Threading.Tasks;
-namespace SemanticKernel.Connectors.OpenAI.UnitTests;
+namespace SemanticKernel.UnitTests.Utilities.OpenAI;
public class MockPipelineResponse : PipelineResponse
{
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs
similarity index 94%
rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs
rename to dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs
index fceef64e4bae..97c9776b4b25 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs
+++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs
@@ -9,7 +9,7 @@ This class was imported and adapted from the System.ClientModel Unit Tests.
using System.ClientModel.Primitives;
using System.Collections.Generic;
-namespace SemanticKernel.Connectors.OpenAI.UnitTests;
+namespace SemanticKernel.UnitTests.Utilities.OpenAI;
public class MockResponseHeaders : PipelineResponseHeaders
{