From ca3accf3a482b3539ad809f59da8f4591bf0e960 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 25 Mar 2023 22:19:13 -0400 Subject: [PATCH] ElevenLabs-DotNet 1.1.0 (#10) - Added ElevenLabs-DotNet-Proxy project - Added support for proxy api gateway - Updated unit tests --- .../ElevenLabs-DotNet-Proxy.csproj | 48 +++++ .../Proxy/AbstractAuthenticationFilter.cs | 13 ++ .../Proxy/ElevenLabsProxyStartup.cs | 179 ++++++++++++++++++ .../Proxy/IAuthenticationFilter.cs | 21 ++ ElevenLabs-DotNet-Proxy/Readme.md | 82 ++++++++ .../ElevenLabs-DotNet-Tests-Proxy.csproj | 11 ++ ElevenLabs-DotNet-Tests-Proxy/Program.cs | 40 ++++ .../Properties/launchSettings.json | 31 +++ .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../AbstractTestFixture.cs | 36 ++++ .../ElevenLabs-DotNet-Tests.csproj | 39 ++-- .../Properties/launchSettings.json | 12 ++ .../Test_Fixture_000_Proxy.cs | 59 ++++++ .../Test_Fixture_00_Authentication.cs | 128 +++++++++++++ .../Test_Fixture_01_UserEndpoint.cs | 14 +- .../Test_Fixture_02_VoicesEndpoint.cs | 70 +++---- .../Test_Fixture_03_TextToSpeechEndpoint.cs | 15 +- .../Test_Fixture_04_HistoryEndpoint.cs | 53 +++--- .../Test_Fixture_05_VoiceGeneration.cs | 20 +- ElevenLabs-DotNet.sln | 16 +- ElevenLabs-DotNet/AssemblyInfo.cs | 6 + ElevenLabs-DotNet/BaseEndPoint.cs | 14 +- ElevenLabs-DotNet/ElevenLabs-DotNet.csproj | 7 +- ElevenLabs-DotNet/ElevenLabsClient.cs | 32 +--- ElevenLabs-DotNet/ElevenLabsClientSettings.cs | 62 ++++++ ElevenLabs-DotNet/History/HistoryEndpoint.cs | 13 +- .../HttpResponseMessageExtensions.cs | 4 +- .../TextToSpeech/TextToSpeechEndpoint.cs | 5 +- ElevenLabs-DotNet/Users/UserEndpoint.cs | 8 +- .../VoiceGenerationEndpoint.cs | 9 +- ElevenLabs-DotNet/Voices/VoicesEndpoint.cs | 23 ++- README.md | 69 +++++++ 33 files changed, 974 insertions(+), 182 deletions(-) create mode 100644 ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj create mode 100644 ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs create mode 100644 ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs create mode 100644 ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs create mode 100644 ElevenLabs-DotNet-Proxy/Readme.md create mode 100644 ElevenLabs-DotNet-Tests-Proxy/ElevenLabs-DotNet-Tests-Proxy.csproj create mode 100644 ElevenLabs-DotNet-Tests-Proxy/Program.cs create mode 100644 ElevenLabs-DotNet-Tests-Proxy/Properties/launchSettings.json create mode 100644 ElevenLabs-DotNet-Tests-Proxy/appsettings.Development.json create mode 100644 ElevenLabs-DotNet-Tests-Proxy/appsettings.json create mode 100644 ElevenLabs-DotNet-Tests/AbstractTestFixture.cs create mode 100644 ElevenLabs-DotNet-Tests/Properties/launchSettings.json create mode 100644 ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs create mode 100644 ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs create mode 100644 ElevenLabs-DotNet/AssemblyInfo.cs create mode 100644 ElevenLabs-DotNet/ElevenLabsClientSettings.cs diff --git a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj new file mode 100644 index 0000000..2ff533d --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj @@ -0,0 +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.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 new file mode 100644 index 0000000..6323cf3 --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs @@ -0,0 +1,13 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace ElevenLabs.Proxy +{ + /// + public abstract class AbstractAuthenticationFilter : IAuthenticationFilter + { + /// + public abstract void ValidateAuthentication(IHeaderDictionary request); + } +} diff --git a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs new file mode 100644 index 0000000..d781972 --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs @@ -0,0 +1,179 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using ElevenLabs.Proxy; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +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 +{ + /// + /// Used in ASP.NET Core WebApps to start your own ElevenLabs web api proxy. + /// + 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 HashSet() + { + 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 + }; + + public void ConfigureServices(IServiceCollection services) { } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + elevenLabsClient = app.ApplicationServices.GetRequiredService(); + authenticationFilter = app.ApplicationServices.GetRequiredService(); + + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/health", HealthEndpoint); + endpoints.Map($"{elevenLabsClient.ElevenLabsClientSettings.BaseRequest}{{**endpoint}}", HandleRequest); + }); + } + + /// + /// Creates a new that acts as a proxy web api for ElevenLabs. + /// + /// type to use to validate your custom issued tokens. + /// Startup args. + /// with configured and . + public static IHost CreateDefaultHost(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter + { + return 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); + }); + }) + .ConfigureServices(services => + { + services.AddSingleton(elevenLabsClient); + services.AddSingleton(); + }).Build(); + } + + private static async Task HealthEndpoint(HttpContext context) + { + // Respond with a 200 OK status code and a plain text message + context.Response.StatusCode = StatusCodes.Status200OK; + const string contentType = "text/plain"; + context.Response.ContentType = contentType; + 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 + { + authenticationFilter.ValidateAuthentication(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); + + elevenLabsRequest.Content = new StreamContent(httpContext.Request.Body); + + if (httpContext.Request.ContentType != null) + { + elevenLabsRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType); + } + + var proxyResponse = await elevenLabsClient.Client.SendAsync(elevenLabsRequest, 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/IAuthenticationFilter.cs b/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs new file mode 100644 index 0000000..d568b07 --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs @@ -0,0 +1,21 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Security.Authentication; +using Microsoft.AspNetCore.Http; + +namespace ElevenLabs.Proxy +{ + /// + /// Filters headers to ensure your users have the correct access. + /// + public interface IAuthenticationFilter + { + /// + /// Checks the headers for your user issued token. + /// If it's not valid, then throw . + /// + /// + /// + void ValidateAuthentication(IHeaderDictionary request); + } +} diff --git a/ElevenLabs-DotNet-Proxy/Readme.md b/ElevenLabs-DotNet-Proxy/Readme.md new file mode 100644 index 0000000..2319007 --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/Readme.md @@ -0,0 +1,82 @@ + +# ElevenLabs-DotNet-Proxy + +[![NuGet version (ElevenLabs-DotNet-Proxy)](https://img.shields.io/nuget/v/ElevenLabs-DotNet-Proxy.svg?label=ElevenLabs-DotNet-Proxy&logo=nuget)](https://www.nuget.org/packages/ElevenLabs-DotNet-Proxy/) + +A simple Proxy API gateway for [ElevenLabs-DotNet](https://github.com/RageAgainstThePixel/ElevenLabs-DotNet) to make authenticated requests from a front end application without exposing your API keys. + +## Getting started + +### Install from NuGet + +Install package [`ElevenLabs-DotNet-Proxy` from Nuget](https://www.nuget.org/packages/ElevenLabs-DotNet-Proxy/). Here's how via command line: + +```powershell +Install-Package ElevenLabs-DotNet-Proxy +``` + +## Documentation + +Using either the [ElevenLabs-DotNet](https://github.com/RageAgainstThePixel/ElevenLabs-DotNet) or [com.rest.elevenlabs](https://github.com/RageAgainstThePixel/com.rest.elevenlabs) packages directly in your front-end app may expose your API keys and other sensitive information. To mitigate this risk, it is recommended to set up an intermediate API that makes requests to ElevenLabs on behalf of your front-end app. This library can be utilized for both front-end and intermediary host configurations, ensuring secure communication with the ElevenLabs API. + +### Front End Example + +In the front end example, you will need to securely authenticate your users using your preferred OAuth provider. Once the user is authenticated, exchange your custom auth token with your API key on the backend. + +Follow these steps: + +1. Setup a new project using either the [ElevenLabs-DotNet](https://github.com/RageAgainstThePixel/ElevenLabs-DotNet) or [com.rest.elevenlabs](https://github.com/RageAgainstThePixel/com.rest.elevenlabs) packages. +2. Authenticate users with your OAuth provider. +3. After successful authentication, create a new `ElevenLabsAuthentication` object and pass in the custom token. +4. Create a new `ElevenLabsClientSettings` object and specify the domain where your intermediate API is located. +5. Pass your new `auth` and `settings` objects to the `ElevenLabsClient` constructor when you create the client instance. + +Here's an example of how to set up the front end: + +```csharp +var authToken = await LoginAsync(); +var auth = new ElevenLabsAuthentication(authToken); +var settings = new ElevenLabsClientSettings(domain: "api.your-custom-domain.com"); +var api = new ElevenLabsClient(auth, settings); +``` + +This setup allows your front end application to securely communicate with your backend that will be using the ElevenLabs-DotNet-Proxy, which then forwards requests to the ElevenLabs API. This ensures that your ElevenLabs API keys and other sensitive information remain secure throughout the process. + +### Back End Example + +In this example, we demonstrate how to set up and use `ElevenLabsProxyStartup` in a new ASP.NET Core web app. The proxy server will handle authentication and forward requests to the ElevenLabs API, ensuring that your API keys and other sensitive information remain secure. + +1. Create a new [ASP.NET Core minimal web API](https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api?view=aspnetcore-6.0) project. +2. Add the ElevenLabs-DotNet nuget package to your project. + - 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. +5. Create `ElevenLabsAuthentication` and `ElevenLabsClientSettings` as you would normally with your API keys, org id, or Azure settings. + +```csharp +public partial class Program +{ + private class AuthenticationFilter : AbstractAuthenticationFilter + { + public override void ValidateAuthentication(IHeaderDictionary request) + { + // 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)) + { + throw new AuthenticationException("User is not authorized"); + } + } + } + + public static void Main(string[] args) + { + var client = new ElevenLabsClient(); + var proxy = ElevenLabsProxyStartup.CreateDefaultHost(args, client); + proxy.Run(); + } +} +``` + +Once you have set up your proxy server, your end users can now make authenticated requests to your proxy api instead of directly to the ElevenLabs API. The proxy server will handle authentication and forward requests to the ElevenLabs API, ensuring that your API keys and other sensitive information remain secure. diff --git a/ElevenLabs-DotNet-Tests-Proxy/ElevenLabs-DotNet-Tests-Proxy.csproj b/ElevenLabs-DotNet-Tests-Proxy/ElevenLabs-DotNet-Tests-Proxy.csproj new file mode 100644 index 0000000..d69cf52 --- /dev/null +++ b/ElevenLabs-DotNet-Tests-Proxy/ElevenLabs-DotNet-Tests-Proxy.csproj @@ -0,0 +1,11 @@ + + + net6.0 + disable + disable + ElevenLabs.Tests.Proxy + + + + + diff --git a/ElevenLabs-DotNet-Tests-Proxy/Program.cs b/ElevenLabs-DotNet-Tests-Proxy/Program.cs new file mode 100644 index 0000000..d959642 --- /dev/null +++ b/ElevenLabs-DotNet-Tests-Proxy/Program.cs @@ -0,0 +1,40 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using ElevenLabs.Proxy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using System.Security.Authentication; + +namespace ElevenLabs.Tests.Proxy +{ + /// + /// Example Web App Proxy API. + /// + // ReSharper disable once PartialTypeWithSinglePart + public partial class Program + { + private const string TestUserToken = "aAbBcCdDeE123456789"; + + // ReSharper disable once ClassNeverInstantiated.Local + private class AuthenticationFilter : AbstractAuthenticationFilter + { + public override void ValidateAuthentication(IHeaderDictionary request) + { + // 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(); + } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests-Proxy/Properties/launchSettings.json b/ElevenLabs-DotNet-Tests-Proxy/Properties/launchSettings.json new file mode 100644 index 0000000..7dea5f2 --- /dev/null +++ b/ElevenLabs-DotNet-Tests-Proxy/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16952", + "sslPort": 44310 + } + }, + "profiles": { + "ElevenLabs_DotNet_Tests_Proxy": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "health", + "applicationUrl": "https://localhost:7096;http://localhost:5244", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "health", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ElevenLabs-DotNet-Tests-Proxy/appsettings.Development.json b/ElevenLabs-DotNet-Tests-Proxy/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ElevenLabs-DotNet-Tests-Proxy/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ElevenLabs-DotNet-Tests-Proxy/appsettings.json b/ElevenLabs-DotNet-Tests-Proxy/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/ElevenLabs-DotNet-Tests-Proxy/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs b/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs new file mode 100644 index 0000000..8681f7f --- /dev/null +++ b/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs @@ -0,0 +1,36 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http; + +namespace ElevenLabs.Tests +{ + internal abstract class AbstractTestFixture + { + protected class TestProxyFactory : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Development"); + base.ConfigureWebHost(builder); + } + } + + internal const string TestUserToken = "aAbBcCdDeE123456789"; + + protected readonly HttpClient HttpClient; + + protected readonly ElevenLabsClient ElevenLabsClient; + + protected AbstractTestFixture() + { + var webApplicationFactory = new TestProxyFactory(); + HttpClient = webApplicationFactory.CreateClient(); + var domain = $"{HttpClient.BaseAddress?.Authority}:{HttpClient.BaseAddress?.Port}"; + var settings = new ElevenLabsClientSettings(domain: domain); + var auth = new ElevenLabsAuthentication(TestUserToken); + ElevenLabsClient = new ElevenLabsClient(auth, settings, HttpClient); + } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/ElevenLabs-DotNet-Tests.csproj b/ElevenLabs-DotNet-Tests/ElevenLabs-DotNet-Tests.csproj index 8128d46..870b1e7 100644 --- a/ElevenLabs-DotNet-Tests/ElevenLabs-DotNet-Tests.csproj +++ b/ElevenLabs-DotNet-Tests/ElevenLabs-DotNet-Tests.csproj @@ -1,23 +1,18 @@ - - - net6.0 - ElevenLabs - disable - disable - false - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + net6.0 + ElevenLabs + disable + disable + false + True + + + + + + + + + diff --git a/ElevenLabs-DotNet-Tests/Properties/launchSettings.json b/ElevenLabs-DotNet-Tests/Properties/launchSettings.json new file mode 100644 index 0000000..160a69f --- /dev/null +++ b/ElevenLabs-DotNet-Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ElevenLabs-DotNet-Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61238;http://localhost:61239" + } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs new file mode 100644 index 0000000..6bbd79d --- /dev/null +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs @@ -0,0 +1,59 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace ElevenLabs.Tests +{ + internal class Test_Fixture_000_Proxy : AbstractTestFixture + { + [Test] + public async Task Test_01_Health() + { + var response = await HttpClient.GetAsync("/health"); + var responseAsString = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"[{response.StatusCode}] {responseAsString}"); + Assert.IsTrue(HttpStatusCode.OK == response.StatusCode); + } + + [Test] + public async Task Test_02_Client_Authenticated() + { + var voices = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); + Assert.IsNotNull(voices); + Assert.IsNotEmpty(voices); + + foreach (var model in voices) + { + Console.WriteLine(model); + } + } + + [Test] + public async Task Test_03_Client_Unauthenticated() + { + var webApplicationFactory = new TestProxyFactory(); + var httpClient = webApplicationFactory.CreateClient(); + var settings = new ElevenLabsClientSettings(domain: "localhost:7096"); + var auth = new ElevenLabsAuthentication("invalid-token"); + var elevenLabsClient = new ElevenLabsClient(auth, settings, httpClient); + + try + { + await elevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); + } + catch (HttpRequestException httpRequestException) + { + // System.Net.Http.HttpRequestException : GetModelsAsync Failed! HTTP status code: Unauthorized | Response body: User is not authorized + Assert.IsTrue(httpRequestException.StatusCode == HttpStatusCode.Unauthorized); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs new file mode 100644 index 0000000..51f6c68 --- /dev/null +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs @@ -0,0 +1,128 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; +using System; +using System.IO; +using System.Security.Authentication; +using System.Text.Json; + +namespace ElevenLabs.Tests +{ + internal class Test_Fixture_00_Authentication + { + [SetUp] + public void Setup() + { + var authJson = new AuthInfo("key-test12"); + var authText = JsonSerializer.Serialize(authJson); + File.WriteAllText(".elevenlabs", authText); + } + + [Test] + public void Test_01_GetAuthFromEnv() + { + var auth = ElevenLabsAuthentication.LoadFromEnv(); + Assert.IsNotNull(auth); + Assert.IsNotNull(auth.ApiKey); + Assert.IsNotEmpty(auth.ApiKey); + } + + [Test] + public void Test_02_GetAuthFromFile() + { + var auth = ElevenLabsAuthentication.LoadFromDirectory(); + Assert.IsNotNull(auth); + Assert.IsNotNull(auth.ApiKey); + Assert.AreEqual("key-test12", auth.ApiKey); + } + + [Test] + public void Test_03_GetAuthFromNonExistentFile() + { + var auth = ElevenLabsAuthentication.LoadFromDirectory(filename: "bad.config"); + Assert.IsNull(auth); + } + + [Test] + public void Test_05_Authentication() + { + var defaultAuth = ElevenLabsAuthentication.Default; + var manualAuth = new ElevenLabsAuthentication("key-testAA"); + var api = new ElevenLabsClient(); + var shouldBeDefaultAuth = api.ElevenLabsAuthentication; + Assert.IsNotNull(shouldBeDefaultAuth); + Assert.IsNotNull(shouldBeDefaultAuth.ApiKey); + Assert.AreEqual(defaultAuth.ApiKey, shouldBeDefaultAuth.ApiKey); + + ElevenLabsAuthentication.Default = new ElevenLabsAuthentication("key-testAA"); + api = new ElevenLabsClient(); + var shouldBeManualAuth = api.ElevenLabsAuthentication; + Assert.IsNotNull(shouldBeManualAuth); + Assert.IsNotNull(shouldBeManualAuth.ApiKey); + Assert.AreEqual(manualAuth.ApiKey, shouldBeManualAuth.ApiKey); + + ElevenLabsAuthentication.Default = defaultAuth; + } + + [Test] + public void Test_06_GetKey() + { + var auth = new ElevenLabsAuthentication("key-testAA"); + Assert.IsNotNull(auth.ApiKey); + Assert.AreEqual("key-testAA", auth.ApiKey); + } + + [Test] + public void Test_07_GetKeyFailed() + { + ElevenLabsAuthentication auth = null; + + try + { + auth = new ElevenLabsAuthentication("fail-key"); + } + catch (InvalidCredentialException) + { + Assert.IsNull(auth); + } + catch (Exception e) + { + Assert.IsTrue(false, $"Expected exception {nameof(InvalidCredentialException)} but got {e.GetType().Name}"); + } + } + + [Test] + public void Test_08_ParseKey() + { + var auth = new ElevenLabsAuthentication("key-testAA"); + Assert.IsNotNull(auth.ApiKey); + Assert.AreEqual("key-testAA", auth.ApiKey); + auth = "key-testCC"; + Assert.IsNotNull(auth.ApiKey); + Assert.AreEqual("key-testCC", auth.ApiKey); + + auth = new ElevenLabsAuthentication("key-testBB"); + Assert.IsNotNull(auth.ApiKey); + Assert.AreEqual("key-testBB", auth.ApiKey); + } + + [Test] + public void Test_09_CustomDomainConfigurationSettings() + { + var auth = new ElevenLabsAuthentication("customIssuedToken"); + var settings = new ElevenLabsClientSettings(domain: "api.your-custom-domain.com"); + var api = new ElevenLabsClient(auth, settings); + Console.WriteLine(api.ElevenLabsClientSettings.BaseRequest); + Console.WriteLine(api.ElevenLabsClientSettings.BaseRequestUrlFormat); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(".elevenlabs")) + { + File.Delete(".elevenlabs"); + } + } + } +} diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs index 019ff90..82609d4 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs @@ -3,25 +3,23 @@ using NUnit.Framework; using System.Threading.Tasks; -namespace ElevenLabs.Voice.Tests +namespace ElevenLabs.Tests { - internal class Test_Fixture_01_UserEndpoint + internal class Test_Fixture_01_UserEndpoint : AbstractTestFixture { [Test] public async Task Test_01_GetUserInfo() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.UserEndpoint); - var result = await api.UserEndpoint.GetUserInfoAsync(); + Assert.NotNull(ElevenLabsClient.UserEndpoint); + var result = await ElevenLabsClient.UserEndpoint.GetUserInfoAsync(); Assert.NotNull(result); } [Test] public async Task Test_02_GetSubscriptionInfo() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.UserEndpoint); - var result = await api.UserEndpoint.GetSubscriptionInfoAsync(); + Assert.NotNull(ElevenLabsClient.UserEndpoint); + var result = await ElevenLabsClient.UserEndpoint.GetSubscriptionInfoAsync(); Assert.NotNull(result); } } diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs index fbddaa3..2e7cb93 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs @@ -8,16 +8,15 @@ using System.Linq; using System.Threading.Tasks; -namespace ElevenLabs.Voice.Tests +namespace ElevenLabs.Tests { - internal class Test_Fixture_02_VoicesEndpoint + internal class Test_Fixture_02_VoicesEndpoint : AbstractTestFixture { [Test] public async Task Test_01_GetVoices() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); @@ -30,9 +29,8 @@ public async Task Test_01_GetVoices() [Test] public async Task Test_02_GetDefaultVoiceSettings() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var result = await api.VoicesEndpoint.GetDefaultVoiceSettingsAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var result = await ElevenLabsClient.VoicesEndpoint.GetDefaultVoiceSettingsAsync(); Assert.NotNull(result); Console.WriteLine($"stability: {result.Stability} | similarity boost: {result.SimilarityBoost}"); } @@ -41,12 +39,12 @@ public async Task Test_02_GetDefaultVoiceSettings() public async Task Test_03_GetVoice() { var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); var voiceToGet = results.MinBy(voice => voice.Name); - var result = await api.VoicesEndpoint.GetVoiceAsync(voiceToGet); + var result = await ElevenLabsClient.VoicesEndpoint.GetVoiceAsync(voiceToGet); Assert.NotNull(result); Console.WriteLine($"{result.Id} | {result.Name} | {result.PreviewUrl}"); } @@ -54,21 +52,20 @@ public async Task Test_03_GetVoice() [Test] public async Task Test_04_EditVoiceSettings() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); var voice = results.FirstOrDefault(); - var result = await api.VoicesEndpoint.EditVoiceSettingsAsync(voice, new VoiceSettings(0.7f, 0.7f)); + var result = await ElevenLabsClient.VoicesEndpoint.EditVoiceSettingsAsync(voice, new VoiceSettings(0.7f, 0.7f)); Assert.NotNull(result); Assert.IsTrue(result); - var updatedVoice = await api.VoicesEndpoint.GetVoiceAsync(voice); + var updatedVoice = await ElevenLabsClient.VoicesEndpoint.GetVoiceAsync(voice); Assert.NotNull(updatedVoice); Console.WriteLine($"{updatedVoice.Id} | similarity boost: {updatedVoice.Settings?.SimilarityBoost} | stability: {updatedVoice.Settings?.Stability}"); - var defaultVoiceSettings = await api.VoicesEndpoint.GetDefaultVoiceSettingsAsync(); + var defaultVoiceSettings = await ElevenLabsClient.VoicesEndpoint.GetDefaultVoiceSettingsAsync(); Assert.NotNull(defaultVoiceSettings); - var defaultResult = await api.VoicesEndpoint.EditVoiceSettingsAsync(voice, defaultVoiceSettings); + var defaultResult = await ElevenLabsClient.VoicesEndpoint.EditVoiceSettingsAsync(voice, defaultVoiceSettings); Assert.NotNull(defaultResult); Assert.IsTrue(defaultResult); } @@ -76,14 +73,13 @@ public async Task Test_04_EditVoiceSettings() [Test] public async Task Test_05_AddVoice() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); var testLabels = new Dictionary { { "accent", "american" } }; var clipPath = Path.GetFullPath("..\\..\\..\\Assets\\test_sample_01.ogg"); - var result = await api.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { clipPath }, testLabels); + var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { clipPath }, testLabels); Assert.NotNull(result); Console.WriteLine($"{result.Name}"); Assert.IsNotEmpty(result.Samples); @@ -92,9 +88,8 @@ public async Task Test_05_AddVoice() [Test] public async Task Test_06_EditVoice() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); var voiceToEdit = results.FirstOrDefault(voice => voice.Name.Contains("Test Voice")); @@ -105,7 +100,7 @@ public async Task Test_06_EditVoice() { "key", "value" } }; var clipPath = Path.GetFullPath("..\\..\\..\\Assets\\test_sample_01.ogg"); - var result = await api.VoicesEndpoint.EditVoiceAsync(voiceToEdit, new[] { clipPath }, testLabels); + var result = await ElevenLabsClient.VoicesEndpoint.EditVoiceAsync(voiceToEdit, new[] { clipPath }, testLabels); Assert.NotNull(result); Assert.IsTrue(result); } @@ -113,38 +108,36 @@ public async Task Test_06_EditVoice() [Test] public async Task Test_07_GetVoiceSample() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); var voice = results.FirstOrDefault(voice => voice.Name.Contains("Test Voice")); Assert.NotNull(voice); - var updatedVoice = await api.VoicesEndpoint.GetVoiceAsync(voice); + var updatedVoice = await ElevenLabsClient.VoicesEndpoint.GetVoiceAsync(voice); Assert.NotNull(updatedVoice); Assert.IsNotEmpty(updatedVoice.Samples); var sample = updatedVoice.Samples.FirstOrDefault(); Assert.NotNull(sample); - var result = await api.VoicesEndpoint.GetVoiceSampleAsync(updatedVoice, updatedVoice.Samples.FirstOrDefault()); + var result = await ElevenLabsClient.VoicesEndpoint.GetVoiceSampleAsync(updatedVoice, updatedVoice.Samples.FirstOrDefault()); Assert.NotNull(result); } [Test] public async Task Test_08_DeleteVoiceSample() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); var voice = results.FirstOrDefault(voice => voice.Name.Contains("Test Voice")); Assert.NotNull(voice); - var updatedVoice = await api.VoicesEndpoint.GetVoiceAsync(voice); + var updatedVoice = await ElevenLabsClient.VoicesEndpoint.GetVoiceAsync(voice); Assert.NotNull(updatedVoice); Assert.IsNotEmpty(updatedVoice.Samples); var sample = updatedVoice.Samples.FirstOrDefault(); Assert.NotNull(sample); - var result = await api.VoicesEndpoint.DeleteVoiceSampleAsync(updatedVoice, sample); + var result = await ElevenLabsClient.VoicesEndpoint.DeleteVoiceSampleAsync(updatedVoice, sample); Assert.NotNull(result); Assert.IsTrue(result); } @@ -152,9 +145,8 @@ public async Task Test_08_DeleteVoiceSample() [Test] public async Task Test_09_DeleteVoice() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.VoicesEndpoint); - var results = await api.VoicesEndpoint.GetAllVoicesAsync(); + Assert.NotNull(ElevenLabsClient.VoicesEndpoint); + var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); var voicesToDelete = results.Where(voice => voice.Name.Contains("Test Voice")).ToList(); @@ -163,7 +155,7 @@ public async Task Test_09_DeleteVoice() foreach (var voice in voicesToDelete) { - var result = await api.VoicesEndpoint.DeleteVoiceAsync(voice); + var result = await ElevenLabsClient.VoicesEndpoint.DeleteVoiceAsync(voice); Assert.NotNull(result); Assert.IsTrue(result); } diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs index fc006f6..a134ee0 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs @@ -1,23 +1,22 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using NUnit.Framework; +using System; using System.Linq; using System.Threading.Tasks; -namespace ElevenLabs.Voice.Tests +namespace ElevenLabs.Tests { - internal class Test_Fixture_03_TextToSpeechEndpoint + internal class Test_Fixture_03_TextToSpeechEndpoint : AbstractTestFixture { [Test] public async Task Test_01_TextToSpeech() { - var api = new ElevenLabsClient(ElevenLabsAuthentication.LoadFromEnv()); - Assert.NotNull(api.TextToSpeechEndpoint); - var voice = (await api.VoicesEndpoint.GetAllVoicesAsync()).FirstOrDefault(); + Assert.NotNull(ElevenLabsClient.TextToSpeechEndpoint); + var voice = (await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync()).FirstOrDefault(); Assert.NotNull(voice); - var defaultVoiceSettings = await api.VoicesEndpoint.GetDefaultVoiceSettingsAsync(); - var clipPath = await api.TextToSpeechEndpoint.TextToSpeechAsync("The quick brown fox jumps over the lazy dog.", voice, defaultVoiceSettings); + var defaultVoiceSettings = await ElevenLabsClient.VoicesEndpoint.GetDefaultVoiceSettingsAsync(); + var clipPath = await ElevenLabsClient.TextToSpeechEndpoint.TextToSpeechAsync("The quick brown fox jumps over the lazy dog.", voice, defaultVoiceSettings); Assert.NotNull(clipPath); Console.WriteLine(clipPath); } diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs index 398b540..15c54ea 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs @@ -6,16 +6,15 @@ using System.Linq; using System.Threading.Tasks; -namespace ElevenLabs.Voice.Tests +namespace ElevenLabs.Tests { - internal class Test_Fixture_04_HistoryEndpoint + internal class Test_Fixture_04_HistoryEndpoint : AbstractTestFixture { [Test] public async Task Test_01_GetHistory() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.HistoryEndpoint); - var results = await api.HistoryEndpoint.GetHistoryAsync(); + Assert.NotNull(ElevenLabsClient.HistoryEndpoint); + var results = await ElevenLabsClient.HistoryEndpoint.GetHistoryAsync(); Assert.NotNull(results); Assert.IsNotEmpty(results); @@ -28,32 +27,30 @@ public async Task Test_01_GetHistory() [Test] public async Task Test_02_GetHistoryAudio() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.HistoryEndpoint); - var historyItems = await api.HistoryEndpoint.GetHistoryAsync(); + Assert.NotNull(ElevenLabsClient.HistoryEndpoint); + var historyItems = await ElevenLabsClient.HistoryEndpoint.GetHistoryAsync(); Assert.NotNull(historyItems); Assert.IsNotEmpty(historyItems); var downloadItem = historyItems.MaxBy(item => item.Date); Assert.NotNull(downloadItem); Console.WriteLine($"Downloading {downloadItem!.Id}..."); - var result = await api.HistoryEndpoint.GetHistoryAudioAsync(downloadItem); + var result = await ElevenLabsClient.HistoryEndpoint.GetHistoryAudioAsync(downloadItem); Assert.NotNull(result); } [Test] public async Task Test_03_DownloadAllHistoryItems() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.HistoryEndpoint); - var historyItems = await api.HistoryEndpoint.GetHistoryAsync(); + Assert.NotNull(ElevenLabsClient.HistoryEndpoint); + var historyItems = await ElevenLabsClient.HistoryEndpoint.GetHistoryAsync(); Assert.NotNull(historyItems); Assert.IsNotEmpty(historyItems); var singleItem = historyItems.FirstOrDefault(); - var singleItemResult = await api.HistoryEndpoint.DownloadHistoryItemsAsync(new List { singleItem }); + var singleItemResult = await ElevenLabsClient.HistoryEndpoint.DownloadHistoryItemsAsync(new List { singleItem }); Assert.NotNull(singleItemResult); Assert.IsNotEmpty(singleItemResult); var downloadItems = historyItems.Select(item => item.Id).ToList(); - var results = await api.HistoryEndpoint.DownloadHistoryItemsAsync(downloadItems); + var results = await ElevenLabsClient.HistoryEndpoint.DownloadHistoryItemsAsync(downloadItems); Assert.NotNull(results); Assert.IsNotEmpty(results); } @@ -61,21 +58,25 @@ public async Task Test_03_DownloadAllHistoryItems() [Test] public async Task Test_04_DeleteHistoryItem() { - var api = new ElevenLabsClient(); - Assert.NotNull(api.HistoryEndpoint); - var historyItems = await api.HistoryEndpoint.GetHistoryAsync(); + Assert.NotNull(ElevenLabsClient.HistoryEndpoint); + var historyItems = await ElevenLabsClient.HistoryEndpoint.GetHistoryAsync(); Assert.NotNull(historyItems); Assert.IsNotEmpty(historyItems); - var itemToDelete = historyItems.FirstOrDefault(item => item.Text.Contains("The quick brown fox jumps over the lazy dog.")); - Assert.NotNull(itemToDelete); - Console.WriteLine($"Deleting {itemToDelete!.Id}..."); - var result = await api.HistoryEndpoint.DeleteHistoryItemAsync(itemToDelete); - Assert.NotNull(result); - Assert.IsTrue(result); - var updatedItems = await api.HistoryEndpoint.GetHistoryAsync(); + var itemsToDelete = historyItems.Where(item => item.Text.Contains("The quick brown fox jumps over the lazy dog.")).ToList(); + Assert.NotNull(itemsToDelete); + Assert.IsNotEmpty(itemsToDelete); + + foreach (var historyItem in itemsToDelete) + { + Console.WriteLine($"Deleting {historyItem.Id}..."); + var result = await ElevenLabsClient.HistoryEndpoint.DeleteHistoryItemAsync(historyItem); + Assert.NotNull(result); + Assert.IsTrue(result); + } + + var updatedItems = await ElevenLabsClient.HistoryEndpoint.GetHistoryAsync(); Assert.NotNull(updatedItems); - var isDeleted = updatedItems.All(item => item.Id != itemToDelete.Id); - Assert.IsTrue(isDeleted); + Assert.That(updatedItems, Has.None.EqualTo(itemsToDelete)); foreach (var item in updatedItems.OrderBy(item => item.Date)) { diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs b/ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs index 97b0546..c8ef871 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs +++ b/ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs @@ -8,16 +8,15 @@ using System.Text.Json; using System.Threading.Tasks; -namespace ElevenLabs.Voice.Tests +namespace ElevenLabs.Tests { - internal class Test_Fixture_05_VoiceGeneration + internal class Test_Fixture_05_VoiceGeneration : AbstractTestFixture { [Test] public async Task Test_01_GetVoiceGenerationOptions() { - var api = new ElevenLabsClient(ElevenLabsAuthentication.LoadFromEnv()); - Assert.NotNull(api.VoiceGenerationEndpoint); - var options = await api.VoiceGenerationEndpoint.GetVoiceGenerationOptionsAsync(); + Assert.NotNull(ElevenLabsClient.VoiceGenerationEndpoint); + var options = await ElevenLabsClient.VoiceGenerationEndpoint.GetVoiceGenerationOptionsAsync(); Assert.NotNull(options); Console.WriteLine(JsonSerializer.Serialize(options)); } @@ -25,20 +24,19 @@ public async Task Test_01_GetVoiceGenerationOptions() [Test] public async Task Test_02_GenerateVoice() { - var api = new ElevenLabsClient(ElevenLabsAuthentication.LoadFromEnv()); - Assert.NotNull(api.VoiceGenerationEndpoint); - var options = await api.VoiceGenerationEndpoint.GetVoiceGenerationOptionsAsync(); + Assert.NotNull(ElevenLabsClient.VoiceGenerationEndpoint); + var options = await ElevenLabsClient.VoiceGenerationEndpoint.GetVoiceGenerationOptionsAsync(); var generateRequest = new GeneratedVoiceRequest("First we thought the PC was a calculator. Then we found out how to turn numbers into letters and we thought it was a typewriter.", options.Genders.FirstOrDefault(), options.Accents.FirstOrDefault(), options.Ages.FirstOrDefault()); - var (generatedVoiceId, audioFilePath) = await api.VoiceGenerationEndpoint.GenerateVoiceAsync(generateRequest); + var (generatedVoiceId, audioFilePath) = await ElevenLabsClient.VoiceGenerationEndpoint.GenerateVoiceAsync(generateRequest); Console.WriteLine(generatedVoiceId); Console.WriteLine(audioFilePath); var createVoiceRequest = new CreateVoiceRequest("Test Voice Lab Create Voice", generatedVoiceId); File.Delete(audioFilePath); Assert.NotNull(createVoiceRequest); - var result = await api.VoiceGenerationEndpoint.CreateVoiceAsync(createVoiceRequest); + var result = await ElevenLabsClient.VoiceGenerationEndpoint.CreateVoiceAsync(createVoiceRequest); Assert.NotNull(result); Console.WriteLine(result.Id); - var deleteResult = await api.VoicesEndpoint.DeleteVoiceAsync(result.Id); + var deleteResult = await ElevenLabsClient.VoicesEndpoint.DeleteVoiceAsync(result.Id); Assert.NotNull(deleteResult); Assert.IsTrue(deleteResult); } diff --git a/ElevenLabs-DotNet.sln b/ElevenLabs-DotNet.sln index b6f168d..2a8058e 100644 --- a/ElevenLabs-DotNet.sln +++ b/ElevenLabs-DotNet.sln @@ -3,9 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElevenLabs-DotNet", "ElevenLabs-DotNet\ElevenLabs-DotNet.csproj", "{D49BE322-BA1F-4D5F-85B6-33A639F7E97C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElevenLabs-DotNet", "ElevenLabs-DotNet\ElevenLabs-DotNet.csproj", "{D49BE322-BA1F-4D5F-85B6-33A639F7E97C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElevenLabs-DotNet-Tests", "ElevenLabs-DotNet-Tests\ElevenLabs-DotNet-Tests.csproj", "{CCF51E5C-9798-4DE3-B83D-3E0F0848B84F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElevenLabs-DotNet-Proxy", "ElevenLabs-DotNet-Proxy\ElevenLabs-DotNet-Proxy.csproj", "{AAD60297-D6D6-47BE-B59D-05AFA9B2F4A1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElevenLabs-DotNet-Tests", "ElevenLabs-DotNet-Tests\ElevenLabs-DotNet-Tests.csproj", "{CCF51E5C-9798-4DE3-B83D-3E0F0848B84F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElevenLabs-DotNet-Tests-Proxy", "ElevenLabs-DotNet-Tests-Proxy\ElevenLabs-DotNet-Tests-Proxy.csproj", "{6E728878-5704-4E6C-92CB-575156FDCDD1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,10 +21,18 @@ Global {D49BE322-BA1F-4D5F-85B6-33A639F7E97C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D49BE322-BA1F-4D5F-85B6-33A639F7E97C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D49BE322-BA1F-4D5F-85B6-33A639F7E97C}.Release|Any CPU.Build.0 = Release|Any CPU + {AAD60297-D6D6-47BE-B59D-05AFA9B2F4A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAD60297-D6D6-47BE-B59D-05AFA9B2F4A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAD60297-D6D6-47BE-B59D-05AFA9B2F4A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAD60297-D6D6-47BE-B59D-05AFA9B2F4A1}.Release|Any CPU.Build.0 = Release|Any CPU {CCF51E5C-9798-4DE3-B83D-3E0F0848B84F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCF51E5C-9798-4DE3-B83D-3E0F0848B84F}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCF51E5C-9798-4DE3-B83D-3E0F0848B84F}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCF51E5C-9798-4DE3-B83D-3E0F0848B84F}.Release|Any CPU.Build.0 = Release|Any CPU + {6E728878-5704-4E6C-92CB-575156FDCDD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E728878-5704-4E6C-92CB-575156FDCDD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E728878-5704-4E6C-92CB-575156FDCDD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E728878-5704-4E6C-92CB-575156FDCDD1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ElevenLabs-DotNet/AssemblyInfo.cs b/ElevenLabs-DotNet/AssemblyInfo.cs new file mode 100644 index 0000000..b0e4a3b --- /dev/null +++ b/ElevenLabs-DotNet/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ElevenLabs-DotNet-Proxy")] +[assembly: InternalsVisibleTo("ElevenLabs-DotNet-Tests")] \ No newline at end of file diff --git a/ElevenLabs-DotNet/BaseEndPoint.cs b/ElevenLabs-DotNet/BaseEndPoint.cs index c3fe5e5..c31e412 100644 --- a/ElevenLabs-DotNet/BaseEndPoint.cs +++ b/ElevenLabs-DotNet/BaseEndPoint.cs @@ -4,18 +4,20 @@ namespace ElevenLabs { public abstract class BaseEndPoint { + internal BaseEndPoint(ElevenLabsClient api) => Api = api; + protected readonly ElevenLabsClient Api; /// - /// Constructor of the api endpoint. - /// Rather than instantiating this yourself, access it through an instance of . + /// The root endpoint address. /// - internal BaseEndPoint(ElevenLabsClient api) => Api = api; + protected abstract string Root { get; } /// - /// Gets the basic endpoint url for the API + /// Gets the full formatted url for the API endpoint. /// - /// The completed basic url for the endpoint. - protected abstract string GetEndpoint(); + /// The endpoint url. + protected string GetUrl(string endpoint = "") + => string.Format(Api.ElevenLabsClientSettings.BaseRequestUrlFormat, $"{Root}{endpoint}"); } } diff --git a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj index 8e772d0..ae0e204 100644 --- a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj +++ b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj @@ -15,7 +15,9 @@ 1.0.4 2023 ElevenLabs, AI, ML, Voice, TTS - Release 1.0.4 + Release 1.1.0 +- Added support for ElevenLabs-DotNet-Proxy +Release 1.0.4 - Updated docs - Removed exception when sample item path is null or whitespace Release 1.0.3 @@ -28,7 +30,8 @@ Release 1.0.2 Release 1.0.1 - Updated docs Release 1.0.0 -- Initial Release! +- Initial Release! + LICENSE A non-official Eleven Labs voice synthesis RESTful client. I am not affiliated with Eleven Labs and an account with api access is required. diff --git a/ElevenLabs-DotNet/ElevenLabsClient.cs b/ElevenLabs-DotNet/ElevenLabsClient.cs index c25a103..2b2d2f7 100644 --- a/ElevenLabs-DotNet/ElevenLabsClient.cs +++ b/ElevenLabs-DotNet/ElevenLabsClient.cs @@ -20,21 +20,23 @@ public sealed class ElevenLabsClient /// The API authentication information to use for API calls, /// or to attempt to use the , /// potentially loading from environment vars or from a config file. + /// Optional, for specifying a proxy domain. + /// Optional, . /// Raised when authentication details are missing or invalid. - public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null) + public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null, ElevenLabsClientSettings clientSettings = null, HttpClient httpClient = null) { ElevenLabsAuthentication = elevenLabsAuthentication ?? ElevenLabsAuthentication.Default; + ElevenLabsClientSettings = clientSettings ?? ElevenLabsClientSettings.Default; if (string.IsNullOrWhiteSpace(ElevenLabsAuthentication?.ApiKey)) { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/RageAgainstThePixel/com.rest.elevenlabs#authentication for details."); + throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/RageAgainstThePixel/ElevenLabs-DotNet#authentication for details."); } - Client = new HttpClient(); - Client.DefaultRequestHeaders.Add("User-Agent", "com.rest.elevenlabs"); + Client = httpClient ?? new HttpClient(); + Client.DefaultRequestHeaders.Add("User-Agent", "ElevenLabs-DotNet"); Client.DefaultRequestHeaders.Add("xi-api-key", ElevenLabsAuthentication.ApiKey); - Version = 1; JsonSerializationOptions = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull @@ -62,25 +64,7 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null /// public ElevenLabsAuthentication ElevenLabsAuthentication { get; } - private int version; - - /// - /// Specifies which version of the API to use. - /// - public int Version - { - get => version; - set - { - version = value; - BaseUrl = $"https://api.elevenlabs.io/v{version}/"; - } - } - - /// - /// The base url to use when making calls to the API. - /// - internal string BaseUrl { get; private set; } + internal ElevenLabsClientSettings ElevenLabsClientSettings { get; } public UserEndpoint UserEndpoint { get; } diff --git a/ElevenLabs-DotNet/ElevenLabsClientSettings.cs b/ElevenLabs-DotNet/ElevenLabsClientSettings.cs new file mode 100644 index 0000000..715e202 --- /dev/null +++ b/ElevenLabs-DotNet/ElevenLabsClientSettings.cs @@ -0,0 +1,62 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +namespace ElevenLabs +{ + public sealed class ElevenLabsClientSettings + { + internal const string ElevenLabsDomain = "api.elevenlabs.io"; + internal const string DefaultApiVersion = "v1"; + + /// + /// Creates a new instance of for use with ElevenLabs API. + /// + public ElevenLabsClientSettings() + { + Domain = ElevenLabsDomain; + ApiVersion = "v1"; + BaseRequest = $"/{ApiVersion}/"; + BaseRequestUrlFormat = $"https://{Domain}{BaseRequest}{{0}}"; + } + + /// + /// Creates a new instance of for use with ElevenLabs API. + /// + /// Base api domain. + /// The version of the ElevenLabs api you want to use. + public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVersion) + { + if (string.IsNullOrWhiteSpace(domain)) + { + domain = ElevenLabsDomain; + } + + 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."); + } + + if (string.IsNullOrWhiteSpace(apiVersion)) + { + apiVersion = DefaultApiVersion; + } + + Domain = domain; + ApiVersion = apiVersion; + BaseRequest = $"/{ApiVersion}/"; + BaseRequestUrlFormat = $"https://{Domain}{BaseRequest}{{0}}"; + } + + public string Domain { get; } + + public string ApiVersion { get; } + + public string BaseRequest { get; } + + public string BaseRequestUrlFormat { get; } + + public static ElevenLabsClientSettings Default { get; } = new ElevenLabsClientSettings(); + } +} diff --git a/ElevenLabs-DotNet/History/HistoryEndpoint.cs b/ElevenLabs-DotNet/History/HistoryEndpoint.cs index 1e84b0c..ec953b7 100644 --- a/ElevenLabs-DotNet/History/HistoryEndpoint.cs +++ b/ElevenLabs-DotNet/History/HistoryEndpoint.cs @@ -1,7 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Compression; @@ -27,8 +25,7 @@ private class HistoryInfo public HistoryEndpoint(ElevenLabsClient api) : base(api) { } - protected override string GetEndpoint() - => $"{Api.BaseUrl}history"; + protected override string Root => "history"; /// /// Get metadata about all your generated audio. @@ -37,7 +34,7 @@ protected override string GetEndpoint() /// A list of history items containing metadata about generated audio. public async Task> GetHistoryAsync(CancellationToken cancellationToken = default) { - var result = await Api.Client.GetAsync($"{GetEndpoint()}", cancellationToken); + var result = await Api.Client.GetAsync(GetUrl(), cancellationToken); var resultAsString = await result.ReadAsStringAsync(); return JsonSerializer.Deserialize(resultAsString, Api.JsonSerializationOptions)?.History; } @@ -61,7 +58,7 @@ public async Task GetHistoryAudioAsync(HistoryItem historyItem, string s File.Delete(filePath); } - var response = await Api.Client.GetAsync($"{GetEndpoint()}/{historyItem.Id}/audio", cancellationToken); + var response = await Api.Client.GetAsync(GetUrl($"/{historyItem.Id}/audio"), cancellationToken); await response.CheckResponseAsync(cancellationToken); var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); @@ -97,7 +94,7 @@ public async Task GetHistoryAudioAsync(HistoryItem historyItem, string s /// True, if history item was successfully deleted. public async Task DeleteHistoryItemAsync(string historyId, CancellationToken cancellationToken = default) { - var response = await Api.Client.DeleteAsync($"{GetEndpoint()}/{historyId}", cancellationToken); + var response = await Api.Client.DeleteAsync(GetUrl($"/{historyId}"), cancellationToken); await response.ReadAsStringAsync(); return response.IsSuccessStatusCode; } @@ -127,7 +124,7 @@ public async Task> DownloadHistoryItemsAsync(List else { var jsonContent = $"{{\"history_item_ids\":[\"{string.Join("\",\"", historyItemIds)}\"]}}".ToJsonStringContent(); - var response = await Api.Client.PostAsync($"{GetEndpoint()}/download", jsonContent, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl("/download"), jsonContent, cancellationToken); await response.CheckResponseAsync(cancellationToken); var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); diff --git a/ElevenLabs-DotNet/HttpResponseMessageExtensions.cs b/ElevenLabs-DotNet/HttpResponseMessageExtensions.cs index 5c0c823..965b4a4 100644 --- a/ElevenLabs-DotNet/HttpResponseMessageExtensions.cs +++ b/ElevenLabs-DotNet/HttpResponseMessageExtensions.cs @@ -16,7 +16,7 @@ public static async Task ReadAsStringAsync(this HttpResponseMessage resp if (!response.IsSuccessStatusCode) { - throw new HttpRequestException($"{methodName} Failed!\n{response.RequestMessage}\n[{response.StatusCode}] {responseAsString}"); + throw new HttpRequestException($"{methodName} Failed!\n{response.RequestMessage}\n[{response.StatusCode}] {responseAsString}", null, response.StatusCode); } if (debugResponse) @@ -32,7 +32,7 @@ internal static async Task CheckResponseAsync(this HttpResponseMessage response, if (!response.IsSuccessStatusCode) { var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken); - throw new HttpRequestException($"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}"); + throw new HttpRequestException($"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, response.StatusCode); } } } diff --git a/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs b/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs index f61596d..40266c6 100644 --- a/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs +++ b/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs @@ -16,8 +16,7 @@ public sealed class TextToSpeechEndpoint : BaseEndPoint { public TextToSpeechEndpoint(ElevenLabsClient api) : base(api) { } - protected override string GetEndpoint() - => $"{Api.BaseUrl}text-to-speech"; + protected override string Root => "text-to-speech"; /// /// Converts text into speech using a voice of your choice and returns audio. @@ -44,7 +43,7 @@ public async Task TextToSpeechAsync(string text, Voice voice, VoiceSetti { var defaultVoiceSettings = voiceSettings ?? voice.Settings ?? await Api.VoicesEndpoint.GetDefaultVoiceSettingsAsync(cancellationToken); var payload = JsonSerializer.Serialize(new TextToSpeechRequest(text, defaultVoiceSettings)).ToJsonStringContent(); - var response = await Api.Client.PostAsync($"{GetEndpoint()}/{voice.Id}", payload, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl($"/{voice.Id}"), payload, cancellationToken); await response.CheckResponseAsync(cancellationToken); var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); diff --git a/ElevenLabs-DotNet/Users/UserEndpoint.cs b/ElevenLabs-DotNet/Users/UserEndpoint.cs index 6cbb1ad..9cb13fa 100644 --- a/ElevenLabs-DotNet/Users/UserEndpoint.cs +++ b/ElevenLabs-DotNet/Users/UserEndpoint.cs @@ -12,15 +12,15 @@ public sealed class UserEndpoint : BaseEndPoint { public UserEndpoint(ElevenLabsClient api) : base(api) { } - protected override string GetEndpoint() - => $"{Api.BaseUrl}user"; + /// + protected override string Root => "user"; /// /// Gets information about your user account. /// public async Task GetUserInfoAsync() { - var response = await Api.Client.GetAsync($"{GetEndpoint()}"); + var response = await Api.Client.GetAsync(GetUrl()); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } @@ -30,7 +30,7 @@ public async Task GetUserInfoAsync() /// public async Task GetSubscriptionInfoAsync() { - var response = await Api.Client.GetAsync($"{GetEndpoint()}/subscription"); + var response = await Api.Client.GetAsync(GetUrl("/subscription")); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } diff --git a/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs b/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs index fc60cd8..3e358d5 100644 --- a/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs +++ b/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs @@ -14,8 +14,7 @@ public sealed class VoiceGenerationEndpoint : BaseEndPoint { public VoiceGenerationEndpoint(ElevenLabsClient api) : base(api) { } - protected override string GetEndpoint() - => $"{Api.BaseUrl}voice-generation"; + protected override string Root => "voice-generation"; /// /// Gets the available voice generation options. @@ -24,7 +23,7 @@ protected override string GetEndpoint() /// . public async Task GetVoiceGenerationOptionsAsync(CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync($"{GetEndpoint()}/generate-voice/parameters", cancellationToken); + var response = await Api.Client.GetAsync(GetUrl("/generate-voice/parameters"), cancellationToken); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } @@ -39,7 +38,7 @@ public async Task GetVoiceGenerationOptionsAsync(Cancella public async Task> GenerateVoiceAsync(GeneratedVoiceRequest generatedVoiceRequest, string saveDirectory = null, CancellationToken cancellationToken = default) { var payload = JsonSerializer.Serialize(generatedVoiceRequest, Api.JsonSerializationOptions).ToJsonStringContent(); - var response = await Api.Client.PostAsync($"{GetEndpoint()}/generate-voice", payload, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl("/generate-voice"), payload, cancellationToken); await response.CheckResponseAsync(cancellationToken); var generatedVoiceId = response.Headers.FirstOrDefault(pair => pair.Key == "generated_voice_id").Value.FirstOrDefault(); @@ -86,7 +85,7 @@ public async Task> GenerateVoiceAsync(GeneratedVoiceReques public async Task CreateVoiceAsync(CreateVoiceRequest createVoiceRequest, CancellationToken cancellationToken = default) { var payload = JsonSerializer.Serialize(createVoiceRequest).ToJsonStringContent(); - var response = await Api.Client.PostAsync($"{GetEndpoint()}/create-voice", payload, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl("/create-voice"), payload, cancellationToken); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } diff --git a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs index 8e49870..f2719f0 100644 --- a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs +++ b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs @@ -33,8 +33,7 @@ private class VoiceResponse public VoicesEndpoint(ElevenLabsClient api) : base(api) { } - protected override string GetEndpoint() - => $"{Api.BaseUrl}voices"; + protected override string Root => "voices"; /// /// Gets a list of all available voices for a user. @@ -43,7 +42,7 @@ protected override string GetEndpoint() /// of s. public async Task> GetAllVoicesAsync(CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync(GetEndpoint(), cancellationToken); + var response = await Api.Client.GetAsync(GetUrl(), cancellationToken); var responseAsString = await response.ReadAsStringAsync(); var voices = JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions).Voices; var voiceSettingsTasks = new List(); @@ -69,7 +68,7 @@ async Task LocalGetVoiceSettings() /// . public async Task GetDefaultVoiceSettingsAsync(CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync($"{GetEndpoint()}/settings/default", cancellationToken); + var response = await Api.Client.GetAsync(GetUrl("/settings/default"), cancellationToken); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } @@ -82,7 +81,7 @@ public async Task GetDefaultVoiceSettingsAsync(CancellationToken /// . public async Task GetVoiceSettingsAsync(string voiceId, CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync($"{GetEndpoint()}/{voiceId}/settings", cancellationToken); + var response = await Api.Client.GetAsync(GetUrl($"/{voiceId}/settings"), cancellationToken); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } @@ -96,7 +95,7 @@ public async Task GetVoiceSettingsAsync(string voiceId, Cancellat /// . public async Task GetVoiceAsync(string voiceId, bool withSettings = true, CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync($"{GetEndpoint()}/{voiceId}?with_settings={withSettings}", cancellationToken); + var response = await Api.Client.GetAsync(GetUrl($"/{voiceId}?with_settings={withSettings}"), cancellationToken); var responseAsString = await response.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); } @@ -111,7 +110,7 @@ public async Task GetVoiceAsync(string voiceId, bool withSettings = true, public async Task EditVoiceSettingsAsync(string voiceId, VoiceSettings voiceSettings, CancellationToken cancellationToken = default) { var payload = JsonSerializer.Serialize(voiceSettings).ToJsonStringContent(); - var response = await Api.Client.PostAsync($"{GetEndpoint()}/{voiceId}/settings/edit", payload, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl($"/{voiceId}/settings/edit"), payload, cancellationToken); await response.ReadAsStringAsync(); return response.IsSuccessStatusCode; } @@ -162,7 +161,7 @@ public async Task AddVoiceAsync(string name, IEnumerable samplePa form.Add(new StringContent(JsonSerializer.Serialize(labels)), "labels"); } - var response = await Api.Client.PostAsync($"{GetEndpoint()}/add", form, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl("/add"), form, cancellationToken); var responseAsString = await response.ReadAsStringAsync(); var voiceResponse = JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); var voice = await GetVoiceAsync(voiceResponse.VoiceId, cancellationToken: cancellationToken); @@ -216,7 +215,7 @@ public async Task EditVoiceAsync(Voice voice, IEnumerable samplePa form.Add(new StringContent(JsonSerializer.Serialize(labels)), "labels"); } - var response = await Api.Client.PostAsync($"{GetEndpoint()}/{voice.Id}/edit", form, cancellationToken); + var response = await Api.Client.PostAsync(GetUrl($"/{voice.Id}/edit"), form, cancellationToken); await response.CheckResponseAsync(cancellationToken); return response.IsSuccessStatusCode; } @@ -229,7 +228,7 @@ public async Task EditVoiceAsync(Voice voice, IEnumerable samplePa /// True, if voice was successfully deleted. public async Task DeleteVoiceAsync(string voiceId, CancellationToken cancellationToken = default) { - var response = await Api.Client.DeleteAsync($"{GetEndpoint()}/{voiceId}", cancellationToken); + var response = await Api.Client.DeleteAsync(GetUrl($"/{voiceId}"), cancellationToken); await response.CheckResponseAsync(cancellationToken); return response.IsSuccessStatusCode; } @@ -245,7 +244,7 @@ public async Task DeleteVoiceAsync(string voiceId, CancellationToken cance /// Optional, . public async Task GetVoiceSampleAsync(string voiceId, string sampleId, string saveDirectory = null, CancellationToken cancellationToken = default) { - var response = await Api.Client.GetAsync($"{GetEndpoint()}/{voiceId}/samples/{sampleId}/audio", cancellationToken); + var response = await Api.Client.GetAsync(GetUrl($"/{voiceId}/samples/{sampleId}/audio"), cancellationToken); await response.CheckResponseAsync(cancellationToken); var rootDirectory = (saveDirectory ?? Directory.GetCurrentDirectory()).CreateNewDirectory(nameof(ElevenLabs)); @@ -291,7 +290,7 @@ public async Task GetVoiceSampleAsync(string voiceId, string sampleId, s /// True, if was successfully deleted. public async Task DeleteVoiceSampleAsync(string voiceId, string sampleId, CancellationToken cancellationToken = default) { - var response = await Api.Client.DeleteAsync($"{GetEndpoint()}/{voiceId}/samples/{sampleId}", cancellationToken); + var response = await Api.Client.DeleteAsync(GetUrl($"/{voiceId}/samples/{sampleId}"), cancellationToken); await response.CheckResponseAsync(cancellationToken); return response.IsSuccessStatusCode; } diff --git a/README.md b/README.md index c12f0b2..8e9cde8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Install-Package ElevenLabs-DotNet ### Table of Contents - [Authentication](#authentication) +- [API Proxy](#api-proxy) :new: - [Text to Speech](#text-to-speech) - [Voices](#voices) - [Get All Voices](#get-all-voices) @@ -102,6 +103,74 @@ Use your system's environment variables specify an api key to use. var api = new ElevenLabsClient(ElevenLabsAuthentication.LoadFromEnv()); ``` +### [API Proxy](ElevenLabs-DotNet-Proxy/Readme.md) + +[![NuGet version (ElevenLabs-DotNet-Proxy)](https://img.shields.io/nuget/v/ElevenLabs-DotNet-Proxy.svg?label=ElevenLabs-DotNet-Proxy&logo=nuget)](https://www.nuget.org/packages/ElevenLabs-DotNet-Proxy/) + +Using either the [ElevenLabs-DotNet](https://github.com/RageAgainstThePixel/ElevenLabs-DotNet) or [com.rest.elevenlabs](https://github.com/RageAgainstThePixel/com.rest.elevenlabs) packages directly in your front-end app may expose your API keys and other sensitive information. To mitigate this risk, it is recommended to set up an intermediate API that makes requests to ElevenLabs on behalf of your front-end app. This library can be utilized for both front-end and intermediary host configurations, ensuring secure communication with the ElevenLabs API. + +### Front End Example + +In the front end example, you will need to securely authenticate your users using your preferred OAuth provider. Once the user is authenticated, exchange your custom auth token with your API key on the backend. + +Follow these steps: + +1. Setup a new project using either the [ElevenLabs-DotNet](https://github.com/RageAgainstThePixel/ElevenLabs-DotNet) or [com.rest.elevenlabs](https://github.com/RageAgainstThePixel/com.rest.elevenlabs) packages. +2. Authenticate users with your OAuth provider. +3. After successful authentication, create a new `ElevenLabsAuthentication` object and pass in the custom token. +4. Create a new `ElevenLabsClientSettings` object and specify the domain where your intermediate API is located. +5. Pass your new `auth` and `settings` objects to the `ElevenLabsClient` constructor when you create the client instance. + +Here's an example of how to set up the front end: + +```csharp +var authToken = await LoginAsync(); +var auth = new ElevenLabsAuthentication(authToken); +var settings = new ElevenLabsClientSettings(domain: "api.your-custom-domain.com"); +var api = new ElevenLabsClient(auth, settings); +``` + +This setup allows your front end application to securely communicate with your backend that will be using the ElevenLabs-DotNet-Proxy, which then forwards requests to the ElevenLabs API. This ensures that your ElevenLabs API keys and other sensitive information remain secure throughout the process. + +### Back End Example + +In this example, we demonstrate how to set up and use `ElevenLabsProxyStartup` in a new ASP.NET Core web app. The proxy server will handle authentication and forward requests to the ElevenLabs API, ensuring that your API keys and other sensitive information remain secure. + +1. Create a new [ASP.NET Core minimal web API](https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api?view=aspnetcore-6.0) project. +2. Add the ElevenLabs-DotNet nuget package to your project. + - 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. +5. Create `ElevenLabsAuthentication` and `ElevenLabsClientSettings` as you would normally with your API keys, org id, or Azure settings. + +```csharp +public partial class Program +{ + private class AuthenticationFilter : AbstractAuthenticationFilter + { + public override void ValidateAuthentication(IHeaderDictionary request) + { + // 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)) + { + throw new AuthenticationException("User is not authorized"); + } + } + } + + public static void Main(string[] args) + { + var client = new ElevenLabsClient(); + var proxy = ElevenLabsProxyStartup.CreateDefaultHost(args, client); + proxy.Run(); + } +} +``` + +Once you have set up your proxy server, your end users can now make authenticated requests to your proxy api instead of directly to the ElevenLabs API. The proxy server will handle authentication and forward requests to the ElevenLabs API, ensuring that your API keys and other sensitive information remain secure. + ### [Text to Speech](https://api.elevenlabs.io/docs#/text-to-speech) Convert text to speech.