diff --git a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj index 44aba86..87f53ab 100644 --- a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj +++ b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj @@ -1,48 +1,48 @@  - - net6.0 - disable - disable - True - false - false - Stephen Hodgson - ElevenLabs-DotNet-Proxy - A simple Proxy API gateway for ElevenLabs-DotNet to make authenticated requests from a front end application without exposing your API keys. - 2023 - https://github.com/RageAgainstThePixel/ElevenLabs-DotNet - https://github.com/RageAgainstThePixel/ElevenLabs-DotNet - ElevenLabs, AI, ML, API, api-proxy, proxy, gateway - ElevenLabs API Proxy - ElevenLabs-DotNet-Proxy - 1.0.1 - ElevenLabs.Proxy - Initial Release! - True - false - Readme.md - True - ElevenLabsIcon.png - LICENSE - - - - - - - - True - \ - - - True - \ - - - - - True - \ - - + + net6.0 + disable + disable + True + false + false + Stephen Hodgson + ElevenLabs-DotNet-Proxy + A simple Proxy API gateway for ElevenLabs-DotNet to make authenticated requests from a front end application without exposing your API keys. + 2024 + https://github.com/RageAgainstThePixel/ElevenLabs-DotNet + https://github.com/RageAgainstThePixel/ElevenLabs-DotNet + ElevenLabs, AI, ML, API, api-proxy, proxy, gateway + ElevenLabs API Proxy + ElevenLabs-DotNet-Proxy + 1.2.0 + ElevenLabs.Proxy + Initial Release! + True + false + Readme.md + True + ElevenLabsIcon.png + LICENSE + + + + + + + + True + \ + + + True + \ + + + + + True + \ + + diff --git a/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs b/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs index 6323cf3..37a4f12 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs @@ -1,5 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace ElevenLabs.Proxy @@ -9,5 +10,8 @@ public abstract class AbstractAuthenticationFilter : IAuthenticationFilter { /// public abstract void ValidateAuthentication(IHeaderDictionary request); + + /// + public abstract Task ValidateAuthenticationAsync(IHeaderDictionary request); } } diff --git a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs index af4f5a7..12a14e4 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Net.Http.Headers; @@ -26,7 +27,7 @@ public class ElevenLabsProxyStartup 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 HashSet() + private static readonly HashSet excludedHeaders = new() { HeaderNames.Connection, HeaderNames.TransferEncoding, @@ -49,7 +50,12 @@ public class ElevenLabsProxyStartup #endif }; - public void ConfigureServices(IServiceCollection services) { } + /// + /// Configures the and services. + /// + /// + public void ConfigureServices(IServiceCollection services) + => SetupServices(services.BuildServiceProvider()); public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { @@ -58,8 +64,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } - elevenLabsClient = app.ApplicationServices.GetRequiredService(); - authenticationFilter = app.ApplicationServices.GetRequiredService(); + SetupServices(app.ApplicationServices); app.UseHttpsRedirection(); app.UseRouting(); @@ -77,25 +82,43 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) /// Startup args. /// with configured and . public static IHost CreateDefaultHost(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter - { - return Host.CreateDefaultBuilder(args) + => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - webBuilder.ConfigureKestrel(options => - { - options.AllowSynchronousIO = false; - options.Limits.MinRequestBodyDataRate = null; - options.Limits.MinResponseDataRate = null; - options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); - options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2); - }); + webBuilder.ConfigureKestrel(ConfigureKestrel); }) .ConfigureServices(services => { services.AddSingleton(elevenLabsClient); services.AddSingleton(); }).Build(); + + public static WebApplication CreateWebApplication(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter + { + var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel(ConfigureKestrel); + builder.Services.AddSingleton(elevenLabsClient); + builder.Services.AddSingleton(); + var app = builder.Build(); + var startup = new ElevenLabsProxyStartup(); + startup.Configure(app, app.Environment); + return app; + } + + private static void ConfigureKestrel(KestrelServerOptions options) + { + options.AllowSynchronousIO = false; + options.Limits.MinRequestBodyDataRate = null; + options.Limits.MinResponseDataRate = null; + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2); + } + + private void SetupServices(IServiceProvider serviceProvider) + { + elevenLabsClient = serviceProvider.GetRequiredService(); + authenticationFilter = serviceProvider.GetRequiredService(); } private static async Task HealthEndpoint(HttpContext context) @@ -115,31 +138,34 @@ 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}")); - var elevenLabsRequest = new HttpRequestMessage(method, uri); + using var request = new HttpRequestMessage(method, uri); - elevenLabsRequest.Content = new StreamContent(httpContext.Request.Body); + request.Content = new StreamContent(httpContext.Request.Body); if (httpContext.Request.ContentType != null) { - elevenLabsRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType); } - var proxyResponse = await elevenLabsClient.Client.SendAsync(elevenLabsRequest, HttpCompletionOption.ResponseHeadersRead); + 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; } + if (excludedHeaders.Contains(key)) { continue; } httpContext.Response.Headers[key] = value.ToArray(); } foreach (var (key, value) in proxyResponse.Content.Headers) { - if (ExcludedHeaders.Contains(key)) { continue; } + if (excludedHeaders.Contains(key)) { continue; } httpContext.Response.Headers[key] = value.ToArray(); } diff --git a/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs b/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs index d568b07..2b692e8 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs @@ -1,7 +1,8 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Security.Authentication; using Microsoft.AspNetCore.Http; +using System.Security.Authentication; +using System.Threading.Tasks; namespace ElevenLabs.Proxy { @@ -17,5 +18,13 @@ public interface IAuthenticationFilter /// /// void ValidateAuthentication(IHeaderDictionary request); + + /// + /// Checks the headers for your user issued token. + /// If it's not valid, then throw . + /// + /// + /// + Task ValidateAuthenticationAsync(IHeaderDictionary request); } } diff --git a/ElevenLabs-DotNet-Proxy/Readme.md b/ElevenLabs-DotNet-Proxy/Readme.md index 2319007..8aa3ae8 100644 --- a/ElevenLabs-DotNet-Proxy/Readme.md +++ b/ElevenLabs-DotNet-Proxy/Readme.md @@ -51,7 +51,7 @@ In this example, we demonstrate how to set up and use `ElevenLabsProxyStartup` i - Powershell install: `Install-Package ElevenLabs-DotNet-Proxy` - Manually editing .csproj: `` 3. Create a new class that inherits from `AbstractAuthenticationFilter` and override the `ValidateAuthentication` method. This will implement the `IAuthenticationFilter` that you will use to check user session token against your internal server. -4. In `Program.cs`, create a new proxy web application by calling `ElevenLabsProxyStartup.CreateDefaultHost` method, passing your custom `AuthenticationFilter` as a type argument. +4. In `Program.cs`, create a new proxy web application by calling `ElevenLabsProxyStartup.CreateWebApplication` method, passing your custom `AuthenticationFilter` as a type argument. 5. Create `ElevenLabsAuthentication` and `ElevenLabsClientSettings` as you would normally with your API keys, org id, or Azure settings. ```csharp @@ -63,7 +63,19 @@ public partial class Program { // You will need to implement your own class to properly test // custom issued tokens you've setup for your end users. - if (!request["xi-api-key"].ToString().Contains(userToken)) + if (!request["xi-api-key"].ToString().Contains(TestUserToken)) + { + throw new AuthenticationException("User is not authorized"); + } + } + + public override async Task ValidateAuthenticationAsync(IHeaderDictionary request) + { + await Task.CompletedTask; // remote resource call + + // You will need to implement your own class to properly test + // custom issued tokens you've setup for your end users. + if (!request["xi-api-key"].ToString().Contains(TestUserToken)) { throw new AuthenticationException("User is not authorized"); } @@ -72,9 +84,9 @@ public partial class Program public static void Main(string[] args) { - var client = new ElevenLabsClient(); - var proxy = ElevenLabsProxyStartup.CreateDefaultHost(args, client); - proxy.Run(); + var auth = ElevenLabsAuthentication.LoadFromEnv(); + var client = new ElevenLabsClient(auth); + ElevenLabsProxyStartup.CreateWebApplication(args, client).Run(); } } ``` diff --git a/ElevenLabs-DotNet-Tests-Proxy/Program.cs b/ElevenLabs-DotNet-Tests-Proxy/Program.cs index d959642..37d5d1d 100644 --- a/ElevenLabs-DotNet-Tests-Proxy/Program.cs +++ b/ElevenLabs-DotNet-Tests-Proxy/Program.cs @@ -2,8 +2,8 @@ using ElevenLabs.Proxy; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Hosting; using System.Security.Authentication; +using System.Threading.Tasks; namespace ElevenLabs.Tests.Proxy { @@ -27,14 +27,25 @@ public override void ValidateAuthentication(IHeaderDictionary request) throw new AuthenticationException("User is not authorized"); } } + + public override async Task ValidateAuthenticationAsync(IHeaderDictionary request) + { + await Task.CompletedTask; // remote resource call + + // You will need to implement your own class to properly test + // custom issued tokens you've setup for your end users. + if (!request["xi-api-key"].ToString().Contains(TestUserToken)) + { + throw new AuthenticationException("User is not authorized"); + } + } } public static void Main(string[] args) { var auth = ElevenLabsAuthentication.LoadFromEnv(); var client = new ElevenLabsClient(auth); - var proxy = ElevenLabsProxyStartup.CreateDefaultHost(args, client); - proxy.Run(); + ElevenLabsProxyStartup.CreateWebApplication(args, client).Run(); } } } \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs index af1d3be..c577290 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs @@ -86,7 +86,43 @@ public async Task Test_05_AddVoice() } [Test] - public async Task Test_06_EditVoice() + public async Task Test_06_AddVoiceFromByteArray() + { + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var testLabels = new Dictionary + { + { "accent", "american" } + }; + var clipPath = Path.GetFullPath("../../../Assets/test_sample_01.ogg"); + byte[] clipData = await File.ReadAllBytesAsync(clipPath); + var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { clipData }, testLabels); + Assert.NotNull(result); + Console.WriteLine($"{result.Name}"); + Assert.IsNotEmpty(result.Samples); + } + + + [Test] + public async Task Test_07_AddVoiceFromStream() + { + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var testLabels = new Dictionary + { + { "accent", "american" } + }; + 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); + } + } + + [Test] + public async Task Test_08_EditVoice() { Assert.NotNull(ElevenLabsClient.VoicesEndpoint); var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); @@ -106,7 +142,7 @@ public async Task Test_06_EditVoice() } [Test] - public async Task Test_07_GetVoiceSample() + public async Task Test_09_GetVoiceSample() { Assert.NotNull(ElevenLabsClient.VoicesEndpoint); var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); @@ -124,7 +160,7 @@ public async Task Test_07_GetVoiceSample() } [Test] - public async Task Test_08_DeleteVoiceSample() + public async Task Test_10_DeleteVoiceSample() { Assert.NotNull(ElevenLabsClient.VoicesEndpoint); var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); @@ -143,7 +179,7 @@ public async Task Test_08_DeleteVoiceSample() } [Test] - public async Task Test_09_DeleteVoice() + public async Task Test_11_DeleteVoice() { Assert.NotNull(ElevenLabsClient.VoicesEndpoint); var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); diff --git a/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs b/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs index d33bc86..18ecbf1 100644 --- a/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs +++ b/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs @@ -51,9 +51,9 @@ public static ElevenLabsAuthentication Default return cachedDefault; } - var auth = (LoadFromDirectory()) ?? - LoadFromDirectory(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)) ?? - LoadFromEnv(); + var auth = LoadFromDirectory() ?? + LoadFromDirectory(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)) ?? + LoadFromEnv(); cachedDefault = auth; return auth; } diff --git a/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs b/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs index 715e202..c876e39 100644 --- a/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs +++ b/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs @@ -6,8 +6,9 @@ namespace ElevenLabs { public sealed class ElevenLabsClientSettings { - internal const string ElevenLabsDomain = "api.elevenlabs.io"; + internal const string Https = "https://"; internal const string DefaultApiVersion = "v1"; + internal const string ElevenLabsDomain = "api.elevenlabs.io"; /// /// Creates a new instance of for use with ElevenLabs API. @@ -17,7 +18,7 @@ public ElevenLabsClientSettings() Domain = ElevenLabsDomain; ApiVersion = "v1"; BaseRequest = $"/{ApiVersion}/"; - BaseRequestUrlFormat = $"https://{Domain}{BaseRequest}{{0}}"; + BaseRequestUrlFormat = $"{Https}{Domain}{BaseRequest}{{0}}"; } /// @@ -32,8 +33,8 @@ public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVer domain = ElevenLabsDomain; } - if (!domain.Contains(".") && - !domain.Contains(":")) + if (!domain.Contains('.') && + !domain.Contains(':')) { throw new ArgumentException($"You're attempting to pass a \"resourceName\" parameter to \"{nameof(domain)}\". Please specify \"resourceName:\" for this parameter in constructor."); } @@ -43,10 +44,10 @@ public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVer apiVersion = DefaultApiVersion; } - Domain = domain; + Domain = domain.Contains("http") ? domain : $"{Https}{domain}"; ApiVersion = apiVersion; BaseRequest = $"/{ApiVersion}/"; - BaseRequestUrlFormat = $"https://{Domain}{BaseRequest}{{0}}"; + BaseRequestUrlFormat = $"{Domain}{BaseRequest}{{0}}"; } public string Domain { get; } @@ -57,6 +58,6 @@ public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVer public string BaseRequestUrlFormat { get; } - public static ElevenLabsClientSettings Default { get; } = new ElevenLabsClientSettings(); + public static ElevenLabsClientSettings Default { get; } = new(); } } diff --git a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj index 831b40f..fb235ad 100644 --- a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj +++ b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj @@ -12,10 +12,15 @@ Stephen Hodgson ElevenLabs-DotNet RageAgainstThePixel - 2023 + 2024 ElevenLabs, AI, ML, Voice, TTS - 2.1.1 - Version 2.1.1 + 2.2.0 + 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 +- Added filesystemless overloads for uploading audio clips +Version 2.1.1 - Added VoicesEndpoint.GetAllVoicesAsync overload that allows skipping downloading the voice settings Version 2.1.0 - Added ElevenLabsClient.EnableDebug option to enable and disable for all endpoints diff --git a/ElevenLabs-DotNet/ElevenLabsClient.cs b/ElevenLabs-DotNet/ElevenLabsClient.cs index aa33a62..a064398 100644 --- a/ElevenLabs-DotNet/ElevenLabsClient.cs +++ b/ElevenLabs-DotNet/ElevenLabsClient.cs @@ -6,6 +6,7 @@ using ElevenLabs.User; using ElevenLabs.VoiceGeneration; using ElevenLabs.Voices; +using System; using System.Net.Http; using System.Security.Authentication; using System.Text.Json; @@ -13,7 +14,7 @@ namespace ElevenLabs { - public sealed class ElevenLabsClient + public sealed class ElevenLabsClient : IDisposable { /// /// Creates a new client for the Eleven Labs API, handling auth and allowing for access to various API endpoints. @@ -27,6 +28,12 @@ public sealed class ElevenLabsClient /// /// Optional, . /// Raised when authentication details are missing or invalid. + /// implements to manage the lifecycle of the resources it uses, including . + /// + /// When you initialize , it will create an internal instance if one is not provided. + /// This internal HttpClient is disposed of when ElevenLabsClient is disposed of. + /// If you provide an external HttpClient instance to ElevenLabsClient, you are responsible for managing its disposal. + /// public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null, ElevenLabsClientSettings clientSettings = null, HttpClient httpClient = null) { ElevenLabsAuthentication = elevenLabsAuthentication ?? ElevenLabsAuthentication.Default; @@ -37,7 +44,19 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/RageAgainstThePixel/ElevenLabs-DotNet#authentication for details."); } - Client = httpClient ?? new HttpClient(); + if (httpClient == null) + { + httpClient = new HttpClient(new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15) + }); + } + else + { + isCustomClient = true; + } + + Client = httpClient; Client.DefaultRequestHeaders.Add("User-Agent", "ElevenLabs-DotNet"); Client.DefaultRequestHeaders.Add("xi-api-key", ElevenLabsAuthentication.ApiKey); @@ -49,6 +68,38 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null VoiceGenerationEndpoint = new VoiceGenerationEndpoint(this); } + ~ElevenLabsClient() + { + Dispose(false); + } + + #region IDisposable + + private bool isDisposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!isDisposed && disposing) + { + if (!isCustomClient) + { + Client?.Dispose(); + } + + isDisposed = true; + } + } + + #endregion IDisposable + + private bool isCustomClient; + /// /// to use when making calls to the API. /// @@ -57,7 +108,7 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null /// /// The to use when making calls to the API. /// - internal static JsonSerializerOptions JsonSerializationOptions { get; } = new JsonSerializerOptions + internal static JsonSerializerOptions JsonSerializationOptions { get; } = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; diff --git a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs index cdbeaad..acdd203 100644 --- a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs +++ b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs @@ -130,7 +130,7 @@ public async Task GetVoiceAsync(string voiceId, bool withSettings = false /// /// Edit your settings for a specific voice. /// - /// Id of the voice settings to edit. + /// The id of the voice settings to edit. /// . /// Optional, . /// True, if voice settings was successfully edited. @@ -203,6 +203,101 @@ public async Task AddVoiceAsync(string name, IEnumerable samplePa return await GetVoiceAsync(voiceResponse.VoiceId, cancellationToken: cancellationToken).ConfigureAwait(false); } + /// + /// Add a new voice to your collection of voices in VoiceLab from a stream + /// + /// Name of the voice you want to add. + /// Collection of samples as an array of bytes to be used for the new voice + /// Optional, labels for the new voice. + /// Optional, . + public async Task AddVoiceAsync(string name, IEnumerable samples, IReadOnlyDictionary labels = null, CancellationToken cancellationToken = default) + { + var form = new MultipartFormDataContent(); + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (samples == null) + { + throw new ArgumentNullException(nameof(samples)); + } + + form.Add(new StringContent(name), "name"); + + var fileItr = 0; + + foreach (var content in samples) + { + try + { + form.Add(new ByteArrayContent(content), "files", $"file-{fileItr++}"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + if (labels != null) + { + form.Add(new StringContent(JsonSerializer.Serialize(labels)), "labels"); + } + + var response = await client.Client.PostAsync(GetUrl("/add"), form, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + var voiceResponse = JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); + return await GetVoiceAsync(voiceResponse.VoiceId, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Add a new voice to your collection of voices in VoiceLab from a stream + /// + /// Name of the voice you want to add. + /// Collection of samples as a stream to be used for the new voice + /// Optional, labels for the new voice. + /// Optional, . + public async Task AddVoiceAsync(string name, IEnumerable sampleStreams, IReadOnlyDictionary labels = null, CancellationToken cancellationToken = default) + { + var form = new MultipartFormDataContent(); + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (sampleStreams == null) + { + throw new ArgumentNullException(nameof(sampleStreams)); + } + + form.Add(new StringContent(name), "name"); + var fileItr = 0; + + foreach (var voiceStream in sampleStreams) + { + try + { + form.Add(new StreamContent(voiceStream), "files", $"file-{fileItr++}"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + if (labels != null) + { + form.Add(new StringContent(JsonSerializer.Serialize(labels)), "labels"); + } + + var response = await client.Client.PostAsync(GetUrl("/add"), form, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + var voiceResponse = JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); + return await GetVoiceAsync(voiceResponse.VoiceId, cancellationToken: cancellationToken).ConfigureAwait(false); + } + /// /// Edit a voice created by you. /// diff --git a/README.md b/README.md index f70299e..848f51c 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ In this example, we demonstrate how to set up and use `ElevenLabsProxyStartup` i - Powershell install: `Install-Package ElevenLabs-DotNet-Proxy` - Manually editing .csproj: `` 3. Create a new class that inherits from `AbstractAuthenticationFilter` and override the `ValidateAuthentication` method. This will implement the `IAuthenticationFilter` that you will use to check user session token against your internal server. -4. In `Program.cs`, create a new proxy web application by calling `ElevenLabsProxyStartup.CreateDefaultHost` method, passing your custom `AuthenticationFilter` as a type argument. +4. In `Program.cs`, create a new proxy web application by calling `ElevenLabsProxyStartup.CreateWebApplication` method, passing your custom `AuthenticationFilter` as a type argument. 5. Create `ElevenLabsAuthentication` and `ElevenLabsClientSettings` as you would normally with your API keys, org id, or Azure settings. ```csharp @@ -156,7 +156,19 @@ public partial class Program { // You will need to implement your own class to properly test // custom issued tokens you've setup for your end users. - if (!request["xi-api-key"].ToString().Contains(userToken)) + if (!request["xi-api-key"].ToString().Contains(TestUserToken)) + { + throw new AuthenticationException("User is not authorized"); + } + } + + public override async Task ValidateAuthenticationAsync(IHeaderDictionary request) + { + await Task.CompletedTask; // remote resource call + + // You will need to implement your own class to properly test + // custom issued tokens you've setup for your end users. + if (!request["xi-api-key"].ToString().Contains(TestUserToken)) { throw new AuthenticationException("User is not authorized"); } @@ -165,9 +177,9 @@ public partial class Program public static void Main(string[] args) { - var client = new ElevenLabsClient(); - var proxy = ElevenLabsProxyStartup.CreateDefaultHost(args, client); - proxy.Run(); + var auth = ElevenLabsAuthentication.LoadFromEnv(); + var client = new ElevenLabsClient(auth); + ElevenLabsProxyStartup.CreateWebApplication(args, client).Run(); } } ```