From f7d626960f52af05f03c0221e166bc4b187529f2 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 8 May 2024 21:26:28 -0400 Subject: [PATCH] ElevenLabs-DotNet 2.2.1 - misc formatting changes ElevenLabs-DotNet-Proxy 2.2.1 - Refactor with modern WebApplication builder - Added ElevenLabs.Proxy.EndpointRouteBuilder --- .../ElevenLabs-DotNet-Proxy.csproj | 7 +- .../Proxy/ElevenLabsProxyStartup.cs | 116 ++-------------- .../Proxy/EndpointRouteBuilder.cs | 129 ++++++++++++++++++ .../Test_Fixture_02_VoicesEndpoint.cs | 16 +-- ElevenLabs-DotNet/ElevenLabs-DotNet.csproj | 6 +- ElevenLabs-DotNet/Voices/VoicesEndpoint.cs | 4 +- 6 files changed, 159 insertions(+), 119 deletions(-) create mode 100644 ElevenLabs-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs diff --git a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj index 87f53ab..5d2c963 100644 --- a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj +++ b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj @@ -15,9 +15,12 @@ ElevenLabs, AI, ML, API, api-proxy, proxy, gateway ElevenLabs API Proxy ElevenLabs-DotNet-Proxy - 1.2.0 + 2.2.1 ElevenLabs.Proxy - Initial Release! + Version 2.2.1 +- Refactor with modern WebApplication builder +- Added ElevenLabs.Proxy.EndpointRouteBuilder + True false Readme.md diff --git a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs index 12a14e4..0b2a68e 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs @@ -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 { @@ -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 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 - }; - /// - /// Configures the and services. + /// Configures the and services. /// /// public void ConfigureServices(IServiceCollection services) => SetupServices(services.BuildServiceProvider()); + /// + /// Configures the to handle requests and forward them to OpenAI API. + /// + /// . + /// . public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) @@ -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); }); } @@ -94,6 +68,12 @@ public static IHost CreateDefaultHost(string[] args, ElevenLabsClient elevenL services.AddSingleton(); }).Build(); + /// + /// Creates a new that acts as a proxy web api for OpenAI. + /// + /// type to use to validate your custom issued tokens. + /// Startup args. + /// with configured and . public static WebApplication CreateWebApplication(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter { var builder = WebApplication.CreateBuilder(args); @@ -130,75 +110,5 @@ private static async Task HealthEndpoint(HttpContext context) const string content = "OK"; await context.Response.WriteAsync(content); } - - /// - /// Handles incoming requests, validates authentication, and forwards the request to ElevenLabs API - /// - 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); - } } } diff --git a/ElevenLabs-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs b/ElevenLabs-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs new file mode 100644 index 0000000..181f68d --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs @@ -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 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); + } + } + } + } +} diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs index c577290..9d0f4e4 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs @@ -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}"); @@ -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}"); @@ -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] diff --git a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj index fb235ad..6c7771d 100644 --- a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj +++ b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj @@ -14,8 +14,10 @@ RageAgainstThePixel 2024 ElevenLabs, AI, ML, Voice, TTS - 2.2.0 - Version 2.2.0 + 2.2.1 + 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 diff --git a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs index acdd203..252aab8 100644 --- a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs +++ b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs @@ -42,9 +42,7 @@ public VoicesEndpoint(ElevenLabsClient client) : base(client) { } /// /// of s. public Task> GetAllVoicesAsync(CancellationToken cancellationToken = default) - { - return GetAllVoicesAsync(true, cancellationToken); - } + => GetAllVoicesAsync(true, cancellationToken); /// /// Gets a list of all available voices for a user.