Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions dotnet/samples/Concepts/AudioToText/OpenAI_AudioToText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,26 @@ public async Task AudioToTextAsync()
// Output the transcribed text
Console.WriteLine(textContent.Text);
}

[Fact(Skip = "Requires a local OpenAI-compatible audio-to-text server, such as FunASR.")]
public async Task OpenAICompatibleAudioToTextAsync()
{
// FunASR exposes the OpenAI-compatible endpoint at /v1/audio/transcriptions.
var kernel = Kernel.CreateBuilder()
.AddOpenAIAudioToText(
modelId: "sensevoice",
endpoint: new Uri("http://localhost:8000/v1"))
.Build();

var audioToTextService = kernel.GetRequiredService<IAudioToTextService>();
await using var audioFileStream = EmbeddedResource.ReadStream(AudioFilename);
var audioFileBinaryData = await BinaryData.FromStreamAsync(audioFileStream!);
AudioContent audioContent = new(audioFileBinaryData, mimeType: null);

var textContent = await audioToTextService.GetTextContentAsync(
audioContent,
new OpenAIAudioToTextExecutionSettings(AudioFilename));

Console.WriteLine(textContent.Text);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ public void ItCanAddAudioToTextService()
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddAudioToTextServiceWithCustomEndpoint()
{
// Arrange
var endpoint = new Uri("http://localhost:10095/v1");
var sut = Kernel.CreateBuilder();

// Act
var service = sut.AddOpenAIAudioToText("model", endpoint)
.Build()
.GetRequiredService<IAudioToTextService>();

// Assert
Assert.Equal(endpoint.ToString(), service.Attributes[AIServiceExtensions.EndpointKey]);
}

[Fact]
public void ItThrowsWhenAddingAudioToTextServiceWithoutCustomEndpoint()
{
// Arrange
var sut = Kernel.CreateBuilder();

// Act & Assert
Assert.Throws<ArgumentNullException>(() => sut.AddOpenAIAudioToText("model", endpoint: null!));
}

[Fact]
public void ItCanAddAudioToTextServiceWithOpenAIClient()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,32 @@ public void ItCanAddAudioToTextService()
Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]);
}

[Fact]
public void ItCanAddAudioToTextServiceWithCustomEndpoint()
{
// Arrange
var endpoint = new Uri("http://localhost:10095/v1");
var sut = new ServiceCollection();

// Act
var service = sut.AddOpenAIAudioToText("model", endpoint)
.BuildServiceProvider()
.GetRequiredService<IAudioToTextService>();

// Assert
Assert.Equal(endpoint.ToString(), service.Attributes[AIServiceExtensions.EndpointKey]);
}

[Fact]
public void ItThrowsWhenAddingAudioToTextServiceWithoutCustomEndpoint()
{
// Arrange
var sut = new ServiceCollection();

// Act & Assert
Assert.Throws<ArgumentNullException>(() => sut.AddOpenAIAudioToText("model", endpoint: null!));
}

[Fact]
public void ItCanAddAudioToTextServiceWithOpenAIClient()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory)
Assert.Equal("model-id", service.Attributes["ModelId"]);
}

[Fact]
public async Task ItUsesCustomEndpointWithoutApiKeyAsync()
{
// Arrange
var endpoint = new Uri("http://localhost:10095/v1");
var service = new OpenAIAudioToTextService("model-id", endpoint, httpClient: this._httpClient);
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("Test audio-to-text response")
};

// Act
await service.GetTextContentsAsync(
new AudioContent(new BinaryData("data"), mimeType: null),
new OpenAIAudioToTextExecutionSettings("file.mp3"));

// Assert
Assert.Equal("http://localhost:10095/v1/audio/transcriptions", this._messageHandlerStub.RequestUri!.ToString());
Assert.Equal(endpoint.ToString(), service.Attributes["Endpoint"]);
}

[Fact]
public void ItThrowsIfCustomEndpointIsNotProvided()
{
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenAIAudioToTextService("model-id", endpoint: null!));
Assert.Equal("endpoint", exception.ParamName);
}

[Fact]
public void ItThrowsIfModelIdIsNotProvided()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,44 @@ public static IKernelBuilder AddOpenAIAudioToText(
return builder;
}

/// <summary>
/// Adds an OpenAI-compatible audio-to-text service with a custom endpoint to the list.
/// </summary>
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
/// <param name="modelId">Model name.</param>
/// <param name="endpoint">OpenAI-compatible API endpoint.</param>
/// <param name="apiKey">Optional API key.</param>
/// <param name="orgId">OpenAI organization id.</param>
/// <param name="serviceId">A local identifier for the given AI service.</param>
/// <param name="httpClient">The HttpClient to use with this service.</param>
/// <returns>The same instance as <paramref name="builder"/>.</returns>
[Experimental("SKEXP0010")]
public static IKernelBuilder AddOpenAIAudioToText(
this IKernelBuilder builder,
string modelId,
Uri endpoint,
string? apiKey = null,
string? orgId = null,
string? serviceId = null,
HttpClient? httpClient = null)
{
Verify.NotNull(builder);
Verify.NotNullOrWhiteSpace(modelId);
Verify.NotNull(endpoint);

Func<IServiceProvider, object?, OpenAIAudioToTextService> factory = (serviceProvider, _) =>
new(modelId,
endpoint,
apiKey,
orgId,
HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
serviceProvider.GetService<ILoggerFactory>());

builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);

return builder;
}

/// <summary>
/// Adds the OpenAI audio-to-text service to the list.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,42 @@ public static IServiceCollection AddOpenAIAudioToText(
return services;
}

/// <summary>
/// Adds an OpenAI-compatible audio-to-text service with a custom endpoint to the list.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
/// <param name="modelId">Model name.</param>
/// <param name="endpoint">OpenAI-compatible API endpoint.</param>
/// <param name="apiKey">Optional API key.</param>
/// <param name="orgId">OpenAI organization id.</param>
/// <param name="serviceId">A local identifier for the given AI service.</param>
/// <returns>The same instance as <paramref name="services"/>.</returns>
[Experimental("SKEXP0010")]
public static IServiceCollection AddOpenAIAudioToText(
this IServiceCollection services,
string modelId,
Uri endpoint,
string? apiKey = null,
string? orgId = null,
string? serviceId = null)
{
Verify.NotNull(services);
Verify.NotNullOrWhiteSpace(modelId);
Verify.NotNull(endpoint);

Func<IServiceProvider, object?, OpenAIAudioToTextService> factory = (serviceProvider, _) =>
new(modelId,
endpoint,
apiKey,
orgId,
HttpClientProvider.GetHttpClient(serviceProvider),
serviceProvider.GetService<ILoggerFactory>());

services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);

return services;
}

/// <summary>
/// Adds the OpenAI audio-to-text service to the list.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
Expand All @@ -12,13 +13,13 @@
namespace Microsoft.SemanticKernel.Connectors.OpenAI;

/// <summary>
/// OpenAI text-to-audio service.
/// OpenAI audio-to-text service.
/// </summary>
[Experimental("SKEXP0010")]
public sealed class OpenAIAudioToTextService : IAudioToTextService
{
/// <summary>
/// OpenAI text-to-audio client for HTTP operations.
/// OpenAI audio-to-text client for HTTP operations.
/// </summary>
private readonly ClientCore _client;

Expand All @@ -44,6 +45,28 @@ public OpenAIAudioToTextService(
this._client = new(modelId, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService)));
}

/// <summary>
/// Initializes a new instance of the <see cref="OpenAIAudioToTextService"/> class for a custom OpenAI-compatible endpoint.
/// </summary>
/// <param name="modelId">Model name.</param>
/// <param name="endpoint">OpenAI-compatible API endpoint.</param>
/// <param name="apiKey">Optional API key.</param>
/// <param name="organization">OpenAI Organization Id (usually optional).</param>
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
public OpenAIAudioToTextService(
string modelId,
Uri endpoint,
string? apiKey = null,
string? organization = null,
HttpClient? httpClient = null,
ILoggerFactory? loggerFactory = null)
{
Verify.NotNullOrWhiteSpace(modelId, nameof(modelId));
Verify.NotNull(endpoint);
this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService)));
}

/// <summary>
/// Initializes a new instance of the <see cref="OpenAIAudioToTextService"/> class.
/// </summary>
Expand Down
Loading