Skip to content

Commit

Permalink
ElevenLabs-DotNet 2.2.1
Browse files Browse the repository at this point in the history
- misc formatting changes

ElevenLabs-DotNet-Proxy 2.2.1
- Refactor with modern WebApplication builder
- Added ElevenLabs.Proxy.EndpointRouteBuilder
  • Loading branch information
StephenHodgson committed May 9, 2024
1 parent 9caa655 commit f7d6269
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 119 deletions.
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

0 comments on commit f7d6269

Please sign in to comment.