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

ElevenLabs-DotNet 2.2.1 #43

Merged
merged 1 commit into from
May 9, 2024
Merged
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
7 changes: 5 additions & 2 deletions ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
<PackageTags>ElevenLabs, AI, ML, API, api-proxy, proxy, gateway</PackageTags>
<Title>ElevenLabs API Proxy</Title>
<PackageId>ElevenLabs-DotNet-Proxy</PackageId>
<Version>1.2.0</Version>
<Version>2.2.1</Version>
<RootNamespace>ElevenLabs.Proxy</RootNamespace>
<PackageReleaseNotes>Initial Release!</PackageReleaseNotes>
<PackageReleaseNotes>Version 2.2.1
- Refactor with modern WebApplication builder
- Added ElevenLabs.Proxy.EndpointRouteBuilder
</PackageReleaseNotes>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<PackageReadmeFile>Readme.md</PackageReadmeFile>
Expand Down
116 changes: 13 additions & 103 deletions ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Authentication;
using System.Threading.Tasks;
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;

namespace ElevenLabs.Proxy
{
Expand All @@ -26,37 +19,18 @@ public class ElevenLabsProxyStartup
private ElevenLabsClient elevenLabsClient;
private IAuthenticationFilter authenticationFilter;

// Copied from https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83
private static readonly HashSet<string> excludedHeaders = new()
{
HeaderNames.Connection,
HeaderNames.TransferEncoding,
HeaderNames.KeepAlive,
HeaderNames.Upgrade,
"Proxy-Connection",
"Proxy-Authenticate",
"Proxy-Authentication-Info",
"Proxy-Authorization",
"Proxy-Features",
"Proxy-Instruction",
"Security-Scheme",
"ALPN",
"Close",
HeaderNames.TE,
#if NET
HeaderNames.AltSvc,
#else
"Alt-Svc",
#endif
};

/// <summary>
/// Configures the <see cref="elevenLabsClient"/> and <see cref="IAuthenticationFilter"/> services.
/// Configures the <see cref="ElevenLabsClient"/> and <see cref="IAuthenticationFilter"/> services.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
=> SetupServices(services.BuildServiceProvider());

/// <summary>
/// Configures the <see cref="IApplicationBuilder"/> to handle requests and forward them to OpenAI API.
/// </summary>
/// <param name="app"><see cref="IApplicationBuilder"/>.</param>
/// <param name="env"><see cref="IWebHostEnvironment"/>.</param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
Expand All @@ -71,7 +45,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/health", HealthEndpoint);
endpoints.Map($"{elevenLabsClient.ElevenLabsClientSettings.BaseRequest}{{**endpoint}}", HandleRequest);
endpoints.MapElevenLabsEndpoints(elevenLabsClient, authenticationFilter);
});
}

Expand All @@ -94,6 +68,12 @@ public static IHost CreateDefaultHost<T>(string[] args, ElevenLabsClient elevenL
services.AddSingleton<IAuthenticationFilter, T>();
}).Build();

/// <summary>
/// Creates a new <see cref="WebApplication"/> that acts as a proxy web api for OpenAI.
/// </summary>
/// <typeparam name="T"><see cref="IAuthenticationFilter"/> type to use to validate your custom issued tokens.</typeparam>
/// <param name="args">Startup args.</param>
/// <param name="openAIClient"><see cref="OpenAIClient"/> with configured <see cref="OpenAIAuthentication"/> and <see cref="OpenAIClientSettings"/>.</param>
public static WebApplication CreateWebApplication<T>(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter
{
var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -130,75 +110,5 @@ private static async Task HealthEndpoint(HttpContext context)
const string content = "OK";
await context.Response.WriteAsync(content);
}

/// <summary>
/// Handles incoming requests, validates authentication, and forwards the request to ElevenLabs API
/// </summary>
private async Task HandleRequest(HttpContext httpContext, string endpoint)
{
try
{
// ReSharper disable once MethodHasAsyncOverload
// just in case either method is implemented we call it twice.
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers);

var method = new HttpMethod(httpContext.Request.Method);
var uri = new Uri(string.Format(elevenLabsClient.ElevenLabsClientSettings.BaseRequestUrlFormat, $"{endpoint}{httpContext.Request.QueryString}"));
using var request = new HttpRequestMessage(method, uri);

request.Content = new StreamContent(httpContext.Request.Body);

if (httpContext.Request.ContentType != null)
{
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType);
}

var proxyResponse = await elevenLabsClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
httpContext.Response.StatusCode = (int)proxyResponse.StatusCode;

foreach (var (key, value) in proxyResponse.Headers)
{
if (excludedHeaders.Contains(key)) { continue; }
httpContext.Response.Headers[key] = value.ToArray();
}

foreach (var (key, value) in proxyResponse.Content.Headers)
{
if (excludedHeaders.Contains(key)) { continue; }
httpContext.Response.Headers[key] = value.ToArray();
}

httpContext.Response.ContentType = proxyResponse.Content.Headers.ContentType?.ToString() ?? string.Empty;
const string streamingContent = "text/event-stream";

if (httpContext.Response.ContentType.Equals(streamingContent))
{
var stream = await proxyResponse.Content.ReadAsStreamAsync();
await WriteServerStreamEventsAsync(httpContext, stream);
}
else
{
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body);
}
}
catch (AuthenticationException authenticationException)
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
await httpContext.Response.WriteAsync(authenticationException.Message);
}
catch (Exception e)
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsync(e.Message);
}
}

private static async Task WriteServerStreamEventsAsync(HttpContext httpContext, Stream contentStream)
{
var responseStream = httpContext.Response.Body;
await contentStream.CopyToAsync(responseStream, httpContext.RequestAborted);
await responseStream.FlushAsync(httpContext.RequestAborted);
}
}
}
129 changes: 129 additions & 0 deletions ElevenLabs-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Authentication;
using System.Text.Json;
using System.Threading.Tasks;

namespace ElevenLabs.Proxy
{
public static class EndpointRouteBuilder
{
// Copied from https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83
private static readonly HashSet<string> excludedHeaders = new()
{
HeaderNames.Connection,
HeaderNames.TransferEncoding,
HeaderNames.KeepAlive,
HeaderNames.Upgrade,
"Proxy-Connection",
"Proxy-Authenticate",
"Proxy-Authentication-Info",
"Proxy-Authorization",
"Proxy-Features",
"Proxy-Instruction",
"Security-Scheme",
"ALPN",
"Close",
"Set-Cookie",
HeaderNames.TE,
#if NET
HeaderNames.AltSvc,
#else
"Alt-Svc",
#endif
};

public static void MapElevenLabsEndpoints(this IEndpointRouteBuilder endpoints,
ElevenLabsClient elevenLabsClient, IAuthenticationFilter authenticationFilter)
{
endpoints.Map($"{elevenLabsClient.ElevenLabsClientSettings.BaseRequest}{{**endpoint}}", HandleRequest);

async Task HandleRequest(HttpContext httpContext, string endpoint)
{
try
{
// ReSharper disable once MethodHasAsyncOverload
// just in case either method is implemented we call it twice.
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers);

var method = new HttpMethod(httpContext.Request.Method);
var uri = new Uri(string.Format(
elevenLabsClient.ElevenLabsClientSettings.BaseRequestUrlFormat,
$"{endpoint}{httpContext.Request.QueryString}"
));
using var request = new HttpRequestMessage(method, uri);
request.Content = new StreamContent(httpContext.Request.Body);

if (httpContext.Request.ContentType != null)
{
request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(httpContext.Request.ContentType);
}

var proxyResponse = await elevenLabsClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
httpContext.Response.StatusCode = (int)proxyResponse.StatusCode;

foreach (var (key, value) in proxyResponse.Headers)
{
if (excludedHeaders.Contains(key))
{
continue;
}

httpContext.Response.Headers[key] = value.ToArray();
}

foreach (var (key, value) in proxyResponse.Content.Headers)
{
if (excludedHeaders.Contains(key))
{
continue;
}

httpContext.Response.Headers[key] = value.ToArray();
}

httpContext.Response.ContentType = proxyResponse.Content.Headers.ContentType?.ToString() ?? string.Empty;
const string streamingContent = "text/event-stream";

if (httpContext.Response.ContentType.Equals(streamingContent))
{
var stream = await proxyResponse.Content.ReadAsStreamAsync();
await WriteServerStreamEventsAsync(httpContext, stream);
}
else
{
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body);
}
}
catch (AuthenticationException authenticationException)
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
await httpContext.Response.WriteAsync(authenticationException.Message);
}
catch (Exception e)
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = JsonSerializer.Serialize(new { error = new { e.Message, e.StackTrace } });
await httpContext.Response.WriteAsync(response);
}

static async Task WriteServerStreamEventsAsync(HttpContext httpContext, Stream contentStream)
{
var responseStream = httpContext.Response.Body;
await contentStream.CopyToAsync(responseStream, httpContext.RequestAborted);
await responseStream.FlushAsync(httpContext.RequestAborted);
}
}
}
}
}
16 changes: 7 additions & 9 deletions ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task Test_03_GetVoice()
var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync();
Assert.NotNull(results);
Assert.IsNotEmpty(results);
var voiceToGet = results.OrderBy(voice => voice.Name).FirstOrDefault();
var voiceToGet = results.MinBy(voice => voice.Name);
var result = await ElevenLabsClient.VoicesEndpoint.GetVoiceAsync(voiceToGet);
Assert.NotNull(result);
Console.WriteLine($"{result.Id} | {result.Name} | {result.PreviewUrl}");
Expand Down Expand Up @@ -94,7 +94,7 @@ public async Task Test_06_AddVoiceFromByteArray()
{ "accent", "american" }
};
var clipPath = Path.GetFullPath("../../../Assets/test_sample_01.ogg");
byte[] clipData = await File.ReadAllBytesAsync(clipPath);
var clipData = await File.ReadAllBytesAsync(clipPath);
var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { clipData }, testLabels);
Assert.NotNull(result);
Console.WriteLine($"{result.Name}");
Expand All @@ -112,13 +112,11 @@ public async Task Test_07_AddVoiceFromStream()
};
var clipPath = Path.GetFullPath("../../../Assets/test_sample_01.ogg");

using (FileStream fs = File.OpenRead(clipPath))
{
var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { fs }, testLabels);
Assert.NotNull(result);
Console.WriteLine($"{result.Name}");
Assert.IsNotEmpty(result.Samples);
}
await using var fs = File.OpenRead(clipPath);
var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { fs }, testLabels);
Assert.NotNull(result);
Console.WriteLine($"{result.Name}");
Assert.IsNotEmpty(result.Samples);
}

[Test]
Expand Down
6 changes: 4 additions & 2 deletions ElevenLabs-DotNet/ElevenLabs-DotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
<Company>RageAgainstThePixel</Company>
<Copyright>2024</Copyright>
<PackageTags>ElevenLabs, AI, ML, Voice, TTS</PackageTags>
<Version>2.2.0</Version>
<PackageReleaseNotes>Version 2.2.0
<Version>2.2.1</Version>
<PackageReleaseNotes>Version 2.2.1
- Misc formatting changes
Version 2.2.0
- Changed ElevenLabsClient to be IDisposable
- The ElevenLabsClient must now be disposed if you do not pass your own HttpClient
- Updated ElevenLabsClientSettings to accept custom domains
Expand Down
4 changes: 1 addition & 3 deletions ElevenLabs-DotNet/Voices/VoicesEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ public VoicesEndpoint(ElevenLabsClient client) : base(client) { }
/// <param name="cancellationToken"></param>
/// <returns><see cref="IReadOnlyList{T}"/> of <see cref="Voice"/>s.</returns>
public Task<IReadOnlyList<Voice>> GetAllVoicesAsync(CancellationToken cancellationToken = default)
{
return GetAllVoicesAsync(true, cancellationToken);
}
=> GetAllVoicesAsync(true, cancellationToken);

/// <summary>
/// Gets a list of all available voices for a user.
Expand Down
Loading