diff --git a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj index 8265666..cd8ed77 100644 --- a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj +++ b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj @@ -3,32 +3,33 @@ net6.0 disable disable - True - false - false Stephen Hodgson + ElevenLabs API Proxy ElevenLabs-DotNet-Proxy + ElevenLabs-DotNet-Proxy + ElevenLabs.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 - 2.2.2 - ElevenLabs.Proxy - Version 2.2.2 + ElevenLabsIcon.png + Readme.md + LICENSE + true + true + false + false + 3.0.0 + +Version 3.0.0 +- Renamed ElevenLabsProxyStartup to ElevenLabsProxy +Version 2.2.2 - Updated EndpointRouteBuilder with optional route prefix parameter Version 2.2.1 - Refactor with modern WebApplication builder - Added ElevenLabs.Proxy.EndpointRouteBuilder - True - false - Readme.md - True - ElevenLabsIcon.png - LICENSE @@ -36,17 +37,17 @@ Version 2.2.1 - True + true \ - True + true \ - True + true \ diff --git a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxy.cs b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxy.cs new file mode 100644 index 0000000..89014eb --- /dev/null +++ b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxy.cs @@ -0,0 +1,114 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +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 System; +using System.Threading.Tasks; + +namespace ElevenLabs.Proxy +{ + /// + /// Used in ASP.NET Core WebApps to start your own ElevenLabs web api proxy. + /// + public class ElevenLabsProxy + { + private ElevenLabsClient elevenLabsClient; + private IAuthenticationFilter authenticationFilter; + + /// + /// Configures the and services. + /// + /// + public void ConfigureServices(IServiceCollection services) + => SetupServices(services.BuildServiceProvider()); + + /// + /// Configures the to handle requests and forward them to OpenAI API. + /// + /// . + /// . + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + SetupServices(app.ApplicationServices); + + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/health", HealthEndpoint); + endpoints.MapElevenLabsEndpoints(elevenLabsClient, authenticationFilter); + }); + } + + /// + /// 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 + => Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.ConfigureKestrel(ConfigureKestrel); + }) + .ConfigureServices(services => + { + services.AddSingleton(elevenLabsClient); + services.AddSingleton(); + }).Build(); + + /// + /// Creates a new that acts as a proxy web api for OpenAI. + /// + /// type to use to validate your custom issued tokens. + /// Startup args. + /// with configured and . + public static WebApplication CreateWebApplication(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter + { + var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel(ConfigureKestrel); + builder.Services.AddSingleton(elevenLabsClient); + builder.Services.AddSingleton(); + var app = builder.Build(); + var startup = new ElevenLabsProxy(); + 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) + { + // 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); + } + } +} diff --git a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs index e8fab3f..1ec478f 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs @@ -2,113 +2,22 @@ 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 System; -using System.Threading.Tasks; namespace ElevenLabs.Proxy { - /// - /// Used in ASP.NET Core WebApps to start your own ElevenLabs web api proxy. - /// + [Obsolete("Use ElevenLabsProxy")] public class ElevenLabsProxyStartup { - private ElevenLabsClient elevenLabsClient; - private IAuthenticationFilter authenticationFilter; + public void ConfigureServices(IServiceCollection services) { } + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { } - /// - /// Configures the and services. - /// - /// - public void ConfigureServices(IServiceCollection services) - => SetupServices(services.BuildServiceProvider()); + public static IHost CreateDefaultHost(string[] args, ElevenLabsClient elevenLabsClient) + where T : class, IAuthenticationFilter => null; - /// - /// Configures the to handle requests and forward them to OpenAI API. - /// - /// . - /// . - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - SetupServices(app.ApplicationServices); - - app.UseHttpsRedirection(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapGet("/health", HealthEndpoint); - endpoints.MapElevenLabsEndpoints(elevenLabsClient, authenticationFilter); - }); - } - - /// - /// 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 - => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.ConfigureKestrel(ConfigureKestrel); - }) - .ConfigureServices(services => - { - services.AddSingleton(elevenLabsClient); - services.AddSingleton(); - }).Build(); - - /// - /// Creates a new that acts as a proxy web api for OpenAI. - /// - /// type to use to validate your custom issued tokens. - /// Startup args. - /// with configured and . - public static WebApplication CreateWebApplication(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter - { - var builder = WebApplication.CreateBuilder(args); - 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) - { - // 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); - } + public static WebApplication CreateWebApplication(string[] args, ElevenLabsClient elevenLabsClient) + where T : class, IAuthenticationFilter => null; } -} +} \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests-Proxy/Program.cs b/ElevenLabs-DotNet-Tests-Proxy/Program.cs index 37d5d1d..97bfc42 100644 --- a/ElevenLabs-DotNet-Tests-Proxy/Program.cs +++ b/ElevenLabs-DotNet-Tests-Proxy/Program.cs @@ -43,9 +43,9 @@ public override async Task ValidateAuthenticationAsync(IHeaderDictionary request public static void Main(string[] args) { - var auth = ElevenLabsAuthentication.LoadFromEnv(); + var auth = ElevenLabsAuthentication.LoadFromEnvironment(); var client = new ElevenLabsClient(auth); - ElevenLabsProxyStartup.CreateWebApplication(args, client).Run(); + ElevenLabsProxy.CreateWebApplication(args, client).Run(); } } } \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs b/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs index 8681f7f..61b7217 100644 --- a/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs +++ b/ElevenLabs-DotNet-Tests/AbstractTestFixture.cs @@ -31,6 +31,7 @@ protected AbstractTestFixture() var settings = new ElevenLabsClientSettings(domain: domain); var auth = new ElevenLabsAuthentication(TestUserToken); ElevenLabsClient = new ElevenLabsClient(auth, settings, HttpClient); + ElevenLabsClient.EnableDebug = true; } } } \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs b/ElevenLabs-DotNet-Tests/TestFixture_000_Proxy.cs similarity index 96% rename from ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs rename to ElevenLabs-DotNet-Tests/TestFixture_000_Proxy.cs index 6bbd79d..a9590b3 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_000_Proxy.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_000_Proxy.cs @@ -8,7 +8,7 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_000_Proxy : AbstractTestFixture + internal class TestFixture_000_Proxy : AbstractTestFixture { [Test] public async Task Test_01_Health() diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs b/ElevenLabs-DotNet-Tests/TestFixture_00_Authentication.cs similarity index 87% rename from ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs rename to ElevenLabs-DotNet-Tests/TestFixture_00_Authentication.cs index 51f6c68..e068dce 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_00_Authentication.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_00_Authentication.cs @@ -8,29 +8,29 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_00_Authentication + internal class TestFixture_00_Authentication { [SetUp] public void Setup() { var authJson = new AuthInfo("key-test12"); var authText = JsonSerializer.Serialize(authJson); - File.WriteAllText(".elevenlabs", authText); + File.WriteAllText(ElevenLabsAuthentication.CONFIG_FILE, authText); } [Test] - public void Test_01_GetAuthFromEnv() + public void Test_01_GetAuthFromEnvironment() { - var auth = ElevenLabsAuthentication.LoadFromEnv(); + var auth = ElevenLabsAuthentication.LoadFromEnvironment(); Assert.IsNotNull(auth); Assert.IsNotNull(auth.ApiKey); Assert.IsNotEmpty(auth.ApiKey); } [Test] - public void Test_02_GetAuthFromFile() + public void Test_02_GetAuthFromPath() { - var auth = ElevenLabsAuthentication.LoadFromDirectory(); + var auth = ElevenLabsAuthentication.LoadFromPath(ElevenLabsAuthentication.CONFIG_FILE); Assert.IsNotNull(auth); Assert.IsNotNull(auth.ApiKey); Assert.AreEqual("key-test12", auth.ApiKey); @@ -119,9 +119,9 @@ public void Test_09_CustomDomainConfigurationSettings() [TearDown] public void TearDown() { - if (File.Exists(".elevenlabs")) + if (File.Exists(ElevenLabsAuthentication.CONFIG_FILE)) { - File.Delete(".elevenlabs"); + File.Delete(ElevenLabsAuthentication.CONFIG_FILE); } } } diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs b/ElevenLabs-DotNet-Tests/TestFixture_01_UserEndpoint.cs similarity index 91% rename from ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs rename to ElevenLabs-DotNet-Tests/TestFixture_01_UserEndpoint.cs index 82609d4..b4fc2d6 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_01_UserEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_01_UserEndpoint.cs @@ -5,7 +5,7 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_01_UserEndpoint : AbstractTestFixture + internal class TestFixture_01_UserEndpoint : AbstractTestFixture { [Test] public async Task Test_01_GetUserInfo() diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_06_Models.cs b/ElevenLabs-DotNet-Tests/TestFixture_02_Models.cs similarity index 92% rename from ElevenLabs-DotNet-Tests/Test_Fixture_06_Models.cs rename to ElevenLabs-DotNet-Tests/TestFixture_02_Models.cs index 4114ba3..2d9b8a2 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_06_Models.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_02_Models.cs @@ -6,7 +6,7 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_06_Models : AbstractTestFixture + internal class TestFixture_02_Models : AbstractTestFixture { [Test] public async Task Test_01_GetModels() diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs b/ElevenLabs-DotNet-Tests/TestFixture_03_VoicesEndpoint.cs similarity index 92% rename from ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs rename to ElevenLabs-DotNet-Tests/TestFixture_03_VoicesEndpoint.cs index 9d0f4e4..a3cbaf0 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_03_VoicesEndpoint.cs @@ -10,10 +10,10 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_02_VoicesEndpoint : AbstractTestFixture + internal class TestFixture_03_VoicesEndpoint : AbstractTestFixture { [Test] - public async Task Test_01_GetVoices() + public async Task Test_01_01_GetVoices() { Assert.NotNull(ElevenLabsClient.VoicesEndpoint); var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync(); @@ -26,6 +26,20 @@ public async Task Test_01_GetVoices() } } + [Test] + public async Task Test_01_02_GetSharedVoices() + { + Assert.NotNull(ElevenLabsClient.SharedVoicesEndpoint); + var results = await ElevenLabsClient.SharedVoicesEndpoint.GetSharedVoicesAsync(); + Assert.NotNull(results); + Assert.IsNotEmpty(results.Voices); + + foreach (var voice in results.Voices) + { + Console.WriteLine($"{voice.OwnerId} | {voice.VoiceId} | {voice.Date} | {voice.Name}"); + } + } + [Test] public async Task Test_02_GetDefaultVoiceSettings() { diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs b/ElevenLabs-DotNet-Tests/TestFixture_04_TextToSpeechEndpoint.cs similarity index 95% rename from ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs rename to ElevenLabs-DotNet-Tests/TestFixture_04_TextToSpeechEndpoint.cs index dbd31d8..545f487 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_03_TextToSpeechEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_04_TextToSpeechEndpoint.cs @@ -8,7 +8,7 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_03_TextToSpeechEndpoint : AbstractTestFixture + internal class TestFixture_04_TextToSpeechEndpoint : AbstractTestFixture { [Test] public async Task Test_01_TextToSpeech() diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs b/ElevenLabs-DotNet-Tests/TestFixture_05_VoiceGeneration.cs similarity index 96% rename from ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs rename to ElevenLabs-DotNet-Tests/TestFixture_05_VoiceGeneration.cs index dd67898..87ce52d 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_05_VoiceGeneration.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_05_VoiceGeneration.cs @@ -9,7 +9,7 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_05_VoiceGeneration : AbstractTestFixture + internal class TestFixture_05_VoiceGeneration : AbstractTestFixture { [Test] public async Task Test_01_GetVoiceGenerationOptions() diff --git a/ElevenLabs-DotNet-Tests/TestFixture_06_SoundGeneration.cs b/ElevenLabs-DotNet-Tests/TestFixture_06_SoundGeneration.cs new file mode 100644 index 0000000..472f5b2 --- /dev/null +++ b/ElevenLabs-DotNet-Tests/TestFixture_06_SoundGeneration.cs @@ -0,0 +1,96 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using ElevenLabs.SoundGeneration; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace ElevenLabs.Tests +{ + internal class TestFixture_06_SoundGeneration : AbstractTestFixture + { + [Test] + public async Task Test_01_GenerateSound() + { + Assert.NotNull(ElevenLabsClient.SoundGenerationEndpoint); + var request = new SoundGenerationRequest("Star Wars Light Saber parry"); + var clip = await ElevenLabsClient.SoundGenerationEndpoint.GenerateSoundAsync(request); + Assert.NotNull(clip); + Assert.IsFalse(clip.ClipData.IsEmpty); + Assert.IsFalse(string.IsNullOrWhiteSpace(clip.Text)); + } + + //[Test] + //public async Task Test_02_01_GetSoundGenerationHistory() + //{ + // Assert.NotNull(ElevenLabsClient.SoundGenerationEndpoint); + // var historyInfo = await ElevenLabsClient.SoundGenerationEndpoint.GetHistoryAsync(pageSize: 20); + // Assert.NotNull(historyInfo); + // Assert.IsNotEmpty(historyInfo.HistoryItems); + + // foreach (var item in historyInfo.HistoryItems) + // { + // Console.WriteLine($"{item.Id} | {item.Text} | {item.CreatedAt}"); + // } + //} + + //[Test] + //public async Task Test_02_02_GetHistoryAudio() + //{ + // Assert.NotNull(ElevenLabsClient.SoundGenerationEndpoint); + // var historyInfo = await ElevenLabsClient.SoundGenerationEndpoint.GetHistoryAsync(pageSize: 20); + // Assert.NotNull(historyInfo); + // Assert.IsNotEmpty(historyInfo.HistoryItems); + // var downloadItem = historyInfo.HistoryItems.OrderByDescending(item => item.CreatedAt).FirstOrDefault(); + // Assert.NotNull(downloadItem); + // Console.WriteLine($"Downloading {downloadItem!.Id}..."); + // var soundClip = await ElevenLabsClient.SoundGenerationEndpoint.DownloadSoundAudioAsync(downloadItem); + // Assert.NotNull(soundClip); + // Assert.IsFalse(soundClip.ClipData.IsEmpty); + //} + + //[Test] + //public async Task Test_02_03_DownloadAllHistoryItems() + //{ + // Assert.NotNull(ElevenLabsClient.SoundGenerationEndpoint); + // var historyInfo = await ElevenLabsClient.SoundGenerationEndpoint.GetHistoryAsync(pageSize: 20); + // Assert.NotNull(historyInfo); + // Assert.IsNotEmpty(historyInfo.HistoryItems); + // var singleItem = historyInfo.HistoryItems.FirstOrDefault(); + // var singleItemResult = await ElevenLabsClient.SoundGenerationEndpoint.DownloadHistoryItemsAsync(new List { singleItem }); + // Assert.NotNull(singleItemResult); + // Assert.IsNotEmpty(singleItemResult); + // var downloadItems = historyInfo.HistoryItems.Select(item => item.Id).ToList(); + // var soundClips = await ElevenLabsClient.SoundGenerationEndpoint.DownloadHistoryItemsAsync(downloadItems); + // Assert.NotNull(soundClips); + // Assert.IsNotEmpty(soundClips); + //} + + //[Test] + //public async Task Test_02_04_DeleteHistoryItem() + //{ + // Assert.NotNull(ElevenLabsClient.SoundGenerationEndpoint); + // var historyInfo = await ElevenLabsClient.SoundGenerationEndpoint.GetHistoryAsync(pageSize: 20); + // Assert.NotNull(historyInfo); + // Assert.IsNotEmpty(historyInfo.HistoryItems); + // var itemsToDelete = historyInfo.HistoryItems.Where(item => item.Text.Contains("Star Wars Light Saber parry")).ToList(); + // Assert.NotNull(itemsToDelete); + // Assert.IsNotEmpty(itemsToDelete); + + // foreach (var historyItem in itemsToDelete) + // { + // Console.WriteLine($"Deleting {historyItem.Id}..."); + // var result = await ElevenLabsClient.SoundGenerationEndpoint.DeleteHistoryItemAsync(historyItem.Id); + // Assert.IsTrue(result); + // } + + // var updatedHistoryInfo = await ElevenLabsClient.SoundGenerationEndpoint.GetHistoryAsync(); + // Assert.NotNull(updatedHistoryInfo); + // Assert.That(updatedHistoryInfo.HistoryItems, Has.None.EqualTo(itemsToDelete)); + + // foreach (var item in updatedHistoryInfo.HistoryItems.OrderBy(item => item.CreatedAt)) + // { + // Console.WriteLine($"[{item.Id}] {item.CreatedAt} | {item.Text}"); + // } + //} + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs b/ElevenLabs-DotNet-Tests/TestFixture_07_HistoryEndpoint.cs similarity index 96% rename from ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs rename to ElevenLabs-DotNet-Tests/TestFixture_07_HistoryEndpoint.cs index 8356833..49fe437 100644 --- a/ElevenLabs-DotNet-Tests/Test_Fixture_04_HistoryEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_07_HistoryEndpoint.cs @@ -8,7 +8,7 @@ namespace ElevenLabs.Tests { - internal class Test_Fixture_04_HistoryEndpoint : AbstractTestFixture + internal class TestFixture_07_HistoryEndpoint : AbstractTestFixture { [Test] public async Task Test_01_GetHistory() @@ -36,6 +36,7 @@ public async Task Test_02_GetHistoryAudio() Console.WriteLine($"Downloading {downloadItem!.Id}..."); var voiceClip = await ElevenLabsClient.HistoryEndpoint.DownloadHistoryAudioAsync(downloadItem); Assert.NotNull(voiceClip); + Assert.IsFalse(voiceClip.ClipData.IsEmpty); } [Test] @@ -64,7 +65,6 @@ public async Task Test_04_DeleteHistoryItem() Assert.IsNotEmpty(historyInfo.HistoryItems); var itemsToDelete = historyInfo.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) { diff --git a/ElevenLabs-DotNet/AuthInfo.cs b/ElevenLabs-DotNet/Authentication/AuthInfo.cs similarity index 100% rename from ElevenLabs-DotNet/AuthInfo.cs rename to ElevenLabs-DotNet/Authentication/AuthInfo.cs diff --git a/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs b/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs index 18ecbf1..687461e 100644 --- a/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs +++ b/ElevenLabs-DotNet/Authentication/ElevenLabsAuthentication.cs @@ -12,6 +12,7 @@ namespace ElevenLabs public sealed class ElevenLabsAuthentication { internal const string CONFIG_FILE = ".elevenlabs"; + private const string ELEVENLABS_API_KEY = nameof(ELEVENLABS_API_KEY); private const string ELEVEN_LABS_API_KEY = nameof(ELEVEN_LABS_API_KEY); private readonly AuthInfo authInfo; @@ -53,13 +54,16 @@ public static ElevenLabsAuthentication Default var auth = LoadFromDirectory() ?? LoadFromDirectory(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)) ?? - LoadFromEnv(); + LoadFromEnvironment(); cachedDefault = auth; return auth; } internal set => cachedDefault = value; } + [Obsolete("Use LoadFromEnvironment")] + public static ElevenLabsAuthentication LoadFromEnv() => LoadFromEnvironment(); + /// /// Attempts to load api keys from environment variables, as "ELEVEN_LABS_API_KEY" /// @@ -67,10 +71,15 @@ public static ElevenLabsAuthentication Default /// Returns the loaded any api keys were found, /// or if there were no matching environment vars. /// - public static ElevenLabsAuthentication LoadFromEnv() + public static ElevenLabsAuthentication LoadFromEnvironment() { var apiKey = Environment.GetEnvironmentVariable(ELEVEN_LABS_API_KEY); + if (string.IsNullOrWhiteSpace(apiKey)) + { + apiKey = Environment.GetEnvironmentVariable(ELEVENLABS_API_KEY); + } + return string.IsNullOrEmpty(apiKey) ? null : new ElevenLabsAuthentication(apiKey); } @@ -150,6 +159,7 @@ public static ElevenLabsAuthentication LoadFromDirectory(string directory = null switch (part) { + case ELEVENLABS_API_KEY: case ELEVEN_LABS_API_KEY: apiKey = nextPart.Trim(); break; diff --git a/ElevenLabs-DotNet/Common/ElevenLabsBaseEndPoint.cs b/ElevenLabs-DotNet/Common/ElevenLabsBaseEndPoint.cs index 1c950f1..94807c3 100644 --- a/ElevenLabs-DotNet/Common/ElevenLabsBaseEndPoint.cs +++ b/ElevenLabs-DotNet/Common/ElevenLabsBaseEndPoint.cs @@ -28,7 +28,7 @@ protected string GetUrl(string endpoint = "", Dictionary queryPa if (queryParameters is { Count: not 0 }) { - result += $"?{string.Join("&", queryParameters.Select(parameter => $"{parameter.Key}={parameter.Value}"))}"; + result += $"?{string.Join('&', queryParameters.Select(parameter => $"{parameter.Key}={parameter.Value}"))}"; } return result; diff --git a/ElevenLabs-DotNet/Common/GeneratedClip.cs b/ElevenLabs-DotNet/Common/GeneratedClip.cs new file mode 100644 index 0000000..d450ee9 --- /dev/null +++ b/ElevenLabs-DotNet/Common/GeneratedClip.cs @@ -0,0 +1,38 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using ElevenLabs.Extensions; +using System; + +namespace ElevenLabs +{ + public class GeneratedClip + { + internal GeneratedClip(string id, string text, ReadOnlyMemory clipData) + { + Id = id; + Text = text; + TextHash = $"{id}{text}".GenerateGuid().ToString(); + ClipData = clipData; + } + + /// + /// The unique id of this clip. + /// + public string Id { get; } + + /// + /// The text input that generated this clip. + /// + public string Text { get; } + + /// + /// Hash string of id and text. + /// + public string TextHash { get; } + + /// + /// The ray clip data. + /// + public ReadOnlyMemory ClipData { get; } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/Common/VoiceClip.cs b/ElevenLabs-DotNet/Common/VoiceClip.cs index 3f76866..8f75dbd 100644 --- a/ElevenLabs-DotNet/Common/VoiceClip.cs +++ b/ElevenLabs-DotNet/Common/VoiceClip.cs @@ -1,30 +1,17 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using ElevenLabs.Extensions; using ElevenLabs.Voices; using System; namespace ElevenLabs { - public sealed class VoiceClip + public sealed class VoiceClip : GeneratedClip { - internal VoiceClip(string id, string text, Voice voice, ReadOnlyMemory clipData) + internal VoiceClip(string id, string text, Voice voice, ReadOnlyMemory clipData) : base(id, text, clipData) { - Id = id; - Text = text; Voice = voice; - TextHash = $"{id}{text}".GenerateGuid().ToString(); - ClipData = clipData; } - public string Id { get; } - - public string Text { get; } - public Voice Voice { get; } - - public string TextHash { get; } - - public ReadOnlyMemory ClipData { get; } } } diff --git a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj index 6c7771d..1168dc2 100644 --- a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj +++ b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj @@ -2,20 +2,37 @@ net6.0 latest - true - ElevenLabs - disable - disable - https://github.com/RageAgainstThePixel/ElevenLabs-DotNet - README.md - https://github.com/RageAgainstThePixel/ElevenLabs-DotNet - Stephen Hodgson ElevenLabs-DotNet + ElevenLabs-DotNet + ElevenLabs-DotNet + ElevenLabs RageAgainstThePixel + Stephen Hodgson + A non-official Eleven Labs voice synthesis RESTful client. +I am not affiliated with Eleven Labs and an account with api access is required. +All copyrights, trademarks, logos, and assets are the property of their respective owners. + 2024 + true + https://github.com/RageAgainstThePixel/ElevenLabs-DotNet + https://github.com/RageAgainstThePixel/ElevenLabs-DotNet ElevenLabs, AI, ML, Voice, TTS - 2.2.1 - Version 2.2.1 + false + README.md + LICENSE + Assets\ElevenLabsIcon.png + true + false + true + true + 3.0.0 + +Version 3.0.0 +- Added ability to specify fully customizable domain proxies +- Added environment variable parsing for ELEVENLABS_API_KEY +- Added SoundEffects API endpoints +- Updated default models +Version 2.2.1 - Misc formatting changes Version 2.2.0 - Changed ElevenLabsClient to be IDisposable @@ -110,24 +127,18 @@ Version 1.0.1 Version 1.0.0 - 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. -All copyrights, trademarks, logos, and assets are the property of their respective owners. - ElevenLabsIcon.png - True - - True + + true \ - True + true \ - True + true \ diff --git a/ElevenLabs-DotNet/ElevenLabsClient.cs b/ElevenLabs-DotNet/ElevenLabsClient.cs index a064398..09113bc 100644 --- a/ElevenLabs-DotNet/ElevenLabsClient.cs +++ b/ElevenLabs-DotNet/ElevenLabsClient.cs @@ -2,6 +2,7 @@ using ElevenLabs.History; using ElevenLabs.Models; +using ElevenLabs.SoundGeneration; using ElevenLabs.TextToSpeech; using ElevenLabs.User; using ElevenLabs.VoiceGeneration; @@ -19,11 +20,11 @@ public sealed class ElevenLabsClient : IDisposable /// /// Creates a new client for the Eleven Labs API, handling auth and allowing for access to various API endpoints. /// - /// The API authentication information to use for API calls, + /// 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, . @@ -34,10 +35,10 @@ public sealed class ElevenLabsClient : IDisposable /// 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) + public ElevenLabsClient(ElevenLabsAuthentication authentication = null, ElevenLabsClientSettings settings = null, HttpClient httpClient = null) { - ElevenLabsAuthentication = elevenLabsAuthentication ?? ElevenLabsAuthentication.Default; - ElevenLabsClientSettings = clientSettings ?? ElevenLabsClientSettings.Default; + ElevenLabsAuthentication = authentication ?? ElevenLabsAuthentication.Default; + ElevenLabsClientSettings = settings ?? ElevenLabsClientSettings.Default; if (string.IsNullOrWhiteSpace(ElevenLabsAuthentication?.ApiKey)) { @@ -62,10 +63,12 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null UserEndpoint = new UserEndpoint(this); VoicesEndpoint = new VoicesEndpoint(this); + SharedVoicesEndpoint = new SharedVoicesEndpoint(this); ModelsEndpoint = new ModelsEndpoint(this); HistoryEndpoint = new HistoryEndpoint(this); TextToSpeechEndpoint = new TextToSpeechEndpoint(this); VoiceGenerationEndpoint = new VoiceGenerationEndpoint(this); + SoundGenerationEndpoint = new SoundGenerationEndpoint(this); } ~ElevenLabsClient() @@ -129,6 +132,8 @@ private void Dispose(bool disposing) public VoicesEndpoint VoicesEndpoint { get; } + public SharedVoicesEndpoint SharedVoicesEndpoint { get; } + public ModelsEndpoint ModelsEndpoint { get; } public HistoryEndpoint HistoryEndpoint { get; } @@ -136,5 +141,7 @@ private void Dispose(bool disposing) public TextToSpeechEndpoint TextToSpeechEndpoint { get; } public VoiceGenerationEndpoint VoiceGenerationEndpoint { get; } + + public SoundGenerationEndpoint SoundGenerationEndpoint { get; } } } diff --git a/ElevenLabs-DotNet/Extensions/HttpResponseMessageExtensions.cs b/ElevenLabs-DotNet/Extensions/HttpResponseMessageExtensions.cs index 2b4e7fb..e66275f 100644 --- a/ElevenLabs-DotNet/Extensions/HttpResponseMessageExtensions.cs +++ b/ElevenLabs-DotNet/Extensions/HttpResponseMessageExtensions.cs @@ -1,8 +1,15 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; @@ -10,30 +17,162 @@ namespace ElevenLabs.Extensions { internal static class HttpResponseMessageExtensions { - public static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse = false, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) + private static readonly JsonSerializerOptions debugJsonOptions = new() { - var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; - if (!response.IsSuccessStatusCode) + internal static async Task CheckResponseAsync(this HttpResponseMessage response, bool debug, CancellationToken cancellationToken, [CallerMemberName] string methodName = null) + { + if (!response.IsSuccessStatusCode || debug) { - throw new HttpRequestException($"{methodName} Failed!\n{response.RequestMessage}\n[{response.StatusCode}] {responseAsString}", null, response.StatusCode); + await response.ReadAsStringAsync(debug, null, null, cancellationToken, methodName).ConfigureAwait(false); } + } - if (debugResponse) + internal static async Task CheckResponseAsync(this HttpResponseMessage response, bool debug, HttpContent requestContent, CancellationToken cancellationToken, [CallerMemberName] string methodName = null) + { + if (!response.IsSuccessStatusCode || debug) { - Console.WriteLine($"{response.RequestMessage}\n[{response.StatusCode}] {responseAsString}"); + await response.ReadAsStringAsync(debug, requestContent, null, cancellationToken, methodName).ConfigureAwait(false); } + } - return responseAsString; + internal static async Task CheckResponseAsync(this HttpResponseMessage response, bool debug, HttpContent requestContent, MemoryStream responseStream, CancellationToken cancellationToken, [CallerMemberName] string methodName = null) + { + if (!response.IsSuccessStatusCode || debug) + { + await response.ReadAsStringAsync(debug, requestContent, responseStream, cancellationToken, methodName).ConfigureAwait(false); + } } - internal static async Task CheckResponseAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) + internal static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse, HttpContent requestContent, CancellationToken cancellationToken, [CallerMemberName] string methodName = null) + => await response.ReadAsStringAsync(debugResponse, requestContent, null, cancellationToken, methodName).ConfigureAwait(false); + + internal static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse, CancellationToken cancellationToken, [CallerMemberName] string methodName = null) + => await response.ReadAsStringAsync(debugResponse, null, null, cancellationToken, methodName).ConfigureAwait(false); + + internal static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse, HttpContent requestContent, MemoryStream responseStream, CancellationToken cancellationToken, [CallerMemberName] string methodName = null) { + var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var debugMessage = new StringBuilder(); + + if (!response.IsSuccessStatusCode || debugResponse) + { + if (!string.IsNullOrWhiteSpace(methodName)) + { + debugMessage.Append($"{methodName} -> "); + } + + var debugMessageObject = new Dictionary>(); + + if (response.RequestMessage != null) + { + debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n"); + + debugMessageObject["Request"] = new Dictionary + { + ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), + }; + } + + if (requestContent != null) + { + debugMessageObject["Request"]["Body-Headers"] = requestContent.Headers.ToDictionary(pair => pair.Key, pair => pair.Value); + string requestAsString; + + if (requestContent is MultipartFormDataContent multipartFormData) + { + var stringContents = multipartFormData.Select(content => + { + var headers = content.Headers.ToDictionary(pair => pair.Key, pair => pair.Value); + switch (content) + { + case StringContent stringContent: + var valueAsString = stringContent.ReadAsStringAsync(cancellationToken).Result; + object value; + + try + { + value = JsonNode.Parse(valueAsString); + } + catch + { + value = valueAsString; + } + + return new { headers, value }; + default: + return new { headers }; + } + }); + requestAsString = JsonSerializer.Serialize(stringContents); + } + else + { + requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + + if (!string.IsNullOrWhiteSpace(requestAsString)) + { + try + { + debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString); + } + catch + { + debugMessageObject["Request"]["Body"] = requestAsString; + } + } + } + + debugMessageObject["Response"] = new() + { + ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), + }; + + if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString)) + { + debugMessageObject["Response"]["Body"] = new Dictionary(); + } + + if (responseStream != null) + { + var body = Encoding.UTF8.GetString(responseStream.ToArray()); + + try + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Events"] = JsonNode.Parse(body); + } + catch + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Events"] = body; + } + } + + if (!string.IsNullOrWhiteSpace(responseAsString)) + { + try + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString); + } + catch + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Content"] = responseAsString; + } + } + + debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, debugJsonOptions)); + Console.WriteLine(debugMessage.ToString()); + } + if (!response.IsSuccessStatusCode) { - var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken); - throw new HttpRequestException($"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, response.StatusCode); + throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); } + + return responseAsString; } } } diff --git a/ElevenLabs-DotNet/History/HistoryEndpoint.cs b/ElevenLabs-DotNet/History/HistoryEndpoint.cs index a97b0a6..b4f8106 100644 --- a/ElevenLabs-DotNet/History/HistoryEndpoint.cs +++ b/ElevenLabs-DotNet/History/HistoryEndpoint.cs @@ -30,8 +30,8 @@ public HistoryEndpoint(ElevenLabsClient client) : base(client) { } /// /// Optional, the id of the item to start after. /// Optional, . - /// . - public async Task GetHistoryAsync(int? pageSize = null, string startAfterId = null, CancellationToken cancellationToken = default) + /// . + public async Task> GetHistoryAsync(int? pageSize = null, string startAfterId = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); @@ -45,9 +45,9 @@ public async Task GetHistoryAsync(int? pageSize = null, string star parameters.Add("start_after_history_item_id", startAfterId); } - var response = await client.Client.GetAsync(GetUrl(queryParameters: parameters), cancellationToken); + using var response = await client.Client.GetAsync(GetUrl(queryParameters: parameters), cancellationToken); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken); - return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); + return JsonSerializer.Deserialize>(responseAsString, ElevenLabsClient.JsonSerializationOptions); } /// @@ -58,11 +58,23 @@ public async Task GetHistoryAsync(int? pageSize = null, string star /// public async Task GetHistoryItemAsync(string id, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{id}"), cancellationToken); + using var response = await client.Client.GetAsync(GetUrl($"/{id}"), cancellationToken); var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken); return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); } + /// + /// Download audio of a history item. + /// + /// or . + /// Optional, . + /// . + public async Task DownloadHistoryAudioAsync(string id, CancellationToken cancellationToken = default) + { + var historyItem = await GetHistoryItemAsync(id, cancellationToken).ConfigureAwait(false); + return await DownloadHistoryAudioAsync(historyItem, cancellationToken).ConfigureAwait(false); + } + /// /// Download audio of a history item. /// @@ -72,8 +84,8 @@ public async Task GetHistoryItemAsync(string id, CancellationToken public async Task DownloadHistoryAudioAsync(HistoryItem historyItem, CancellationToken cancellationToken = default) { var voice = await client.VoicesEndpoint.GetVoiceAsync(historyItem.VoiceId, cancellationToken: cancellationToken).ConfigureAwait(false); - var response = await client.Client.GetAsync(GetUrl($"/{historyItem.Id}/audio"), cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken); + using var response = await client.Client.GetAsync(GetUrl($"/{historyItem.Id}/audio"), cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, cancellationToken); var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var memoryStream = new MemoryStream(); byte[] clipData; @@ -100,7 +112,7 @@ public async Task DownloadHistoryAudioAsync(HistoryItem historyItem, /// True, if history item was successfully deleted. public async Task DeleteHistoryItemAsync(string id, CancellationToken cancellationToken = default) { - var response = await client.Client.DeleteAsync(GetUrl($"/{id}"), cancellationToken); + using var response = await client.Client.DeleteAsync(GetUrl($"/{id}"), cancellationToken); await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken); return response.IsSuccessStatusCode; } @@ -117,15 +129,15 @@ public async Task DeleteHistoryItemAsync(string id, CancellationToken canc public async Task> DownloadHistoryItemsAsync(List historyItemIds = null, CancellationToken cancellationToken = default) { historyItemIds ??= (await GetHistoryAsync(cancellationToken: cancellationToken)).HistoryItems.Select(item => item.Id).ToList(); - var voiceClips = new ConcurrentBag(); + var clips = new ConcurrentBag(); - async Task DownloadItem(string historyItemId) + async Task DownloadItem(string id) { try { - var historyItem = await GetHistoryItemAsync(historyItemId, cancellationToken).ConfigureAwait(false); - var voiceClip = await DownloadHistoryAudioAsync(historyItem, cancellationToken).ConfigureAwait(false); - voiceClips.Add(voiceClip); + var historyItem = await GetHistoryItemAsync(id, cancellationToken).ConfigureAwait(false); + var clip = await DownloadHistoryAudioAsync(historyItem, cancellationToken).ConfigureAwait(false); + clips.Add(clip); } catch (Exception e) { @@ -134,7 +146,7 @@ async Task DownloadItem(string historyItemId) } await Task.WhenAll(historyItemIds.Select(DownloadItem)).ConfigureAwait(false); - return voiceClips.ToList(); + return clips.ToList(); } } } diff --git a/ElevenLabs-DotNet/History/HistoryInfo.cs b/ElevenLabs-DotNet/History/HistoryInfo.cs index 3657c2d..af17474 100644 --- a/ElevenLabs-DotNet/History/HistoryInfo.cs +++ b/ElevenLabs-DotNet/History/HistoryInfo.cs @@ -5,11 +5,11 @@ namespace ElevenLabs.History { - public sealed class HistoryInfo + public sealed class HistoryInfo { [JsonInclude] [JsonPropertyName("history")] - public IReadOnlyList HistoryItems { get; private set; } + public IReadOnlyList HistoryItems { get; private set; } [JsonInclude] [JsonPropertyName("last_history_item_id")] diff --git a/ElevenLabs-DotNet/History/HistoryItem.cs b/ElevenLabs-DotNet/History/HistoryItem.cs index 6aa81f1..5db2e8f 100644 --- a/ElevenLabs-DotNet/History/HistoryItem.cs +++ b/ElevenLabs-DotNet/History/HistoryItem.cs @@ -7,8 +7,6 @@ namespace ElevenLabs.History { public sealed class HistoryItem { - public static implicit operator string(HistoryItem historyItem) => historyItem.Id; - [JsonInclude] [JsonPropertyName("history_item_id")] public string Id { get; private set; } @@ -47,5 +45,7 @@ public sealed class HistoryItem [JsonInclude] [JsonPropertyName("state")] public string State { get; private set; } + + public static implicit operator string(HistoryItem historyItem) => historyItem?.Id; } } diff --git a/ElevenLabs-DotNet/Models/Model.cs b/ElevenLabs-DotNet/Models/Model.cs index 06b965c..41a3d1f 100644 --- a/ElevenLabs-DotNet/Models/Model.cs +++ b/ElevenLabs-DotNet/Models/Model.cs @@ -51,13 +51,22 @@ public Model(string id) #region Predefined Models [JsonIgnore] - public static Model MonoLingualV1 { get; } = new Model("eleven_monolingual_v1"); + public static Model MonoLingualV1 { get; } = new("eleven_monolingual_v1"); [JsonIgnore] - public static Model MultiLingualV1 { get; } = new Model("eleven_multilingual_v1"); + public static Model MultiLingualV1 { get; } = new("eleven_multilingual_v1"); [JsonIgnore] - public static Model MultiLingualV2 { get; } = new Model("eleven_multilingual_v2"); + public static Model MultiLingualV2 { get; } = new("eleven_multilingual_v2"); + + [JsonIgnore] + public static Model TurboV2 { get; } = new("eleven_turbo_v2"); + + [JsonIgnore] + public static Model EnglishSpeechToSpeechV2 { get; } = new("eleven_english_sts_v2"); + + [JsonIgnore] + public static Model MultilingualSpeechToSpeechV2 { get; } = new("eleven_multilingual_sts_v2"); #endregion Predefined Models } diff --git a/ElevenLabs-DotNet/Models/ModelsEndpoint.cs b/ElevenLabs-DotNet/Models/ModelsEndpoint.cs index de49022..0e91727 100644 --- a/ElevenLabs-DotNet/Models/ModelsEndpoint.cs +++ b/ElevenLabs-DotNet/Models/ModelsEndpoint.cs @@ -3,6 +3,7 @@ using ElevenLabs.Extensions; using System.Collections.Generic; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace ElevenLabs.Models @@ -17,10 +18,10 @@ public ModelsEndpoint(ElevenLabsClient client) : base(client) { } /// Access the different models available to the platform. /// /// A list of s you can use. - public async Task> GetModelsAsync() + public async Task> GetModelsAsync(CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl()); - var responseAsString = await response.ReadAsStringAsync(EnableDebug); + var response = await client.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize>(responseAsString, ElevenLabsClient.JsonSerializationOptions); } } diff --git a/ElevenLabs-DotNet/SoundGeneration/GenerationSettings.cs b/ElevenLabs-DotNet/SoundGeneration/GenerationSettings.cs new file mode 100644 index 0000000..c6b22b3 --- /dev/null +++ b/ElevenLabs-DotNet/SoundGeneration/GenerationSettings.cs @@ -0,0 +1,17 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text.Json.Serialization; + +namespace ElevenLabs.SoundGeneration +{ + public sealed class GenerationSettings + { + [JsonInclude] + [JsonPropertyName("duration_seconds")] + public float? Duration { get; private set; } + + [JsonInclude] + [JsonPropertyName("prompt_influence")] + public float PromptInfluence { get; private set; } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/SoundGeneration/SoundGenerationConfig.cs b/ElevenLabs-DotNet/SoundGeneration/SoundGenerationConfig.cs new file mode 100644 index 0000000..4a5a74d --- /dev/null +++ b/ElevenLabs-DotNet/SoundGeneration/SoundGenerationConfig.cs @@ -0,0 +1,21 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Text.Json.Serialization; + +namespace ElevenLabs.SoundGeneration +{ + public sealed class SoundGenerationConfig + { + [JsonInclude] + [JsonPropertyName("number_of_generations")] + public int NumberOfGenerations { get; private set; } + + [JsonInclude] + [JsonPropertyName("generation_settings")] + public GenerationSettings GenerationSettings { get; private set; } + + [JsonInclude] + [JsonPropertyName("text")] + public string Text { get; private set; } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/SoundGeneration/SoundGenerationEndpoint.cs b/ElevenLabs-DotNet/SoundGeneration/SoundGenerationEndpoint.cs new file mode 100644 index 0000000..5b5f408 --- /dev/null +++ b/ElevenLabs-DotNet/SoundGeneration/SoundGenerationEndpoint.cs @@ -0,0 +1,163 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using ElevenLabs.Extensions; +using System; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ElevenLabs.SoundGeneration +{ + public sealed class SoundGenerationEndpoint : ElevenLabsBaseEndPoint + { + public SoundGenerationEndpoint(ElevenLabsClient client) : base(client) { } + + protected override string Root => "sound-generation"; + + /// + /// converts text into sounds & uses the most advanced AI audio model ever. + /// Create sound effects for your videos, voice-overs or video games. + /// + /// . + /// Optional, . + /// . + public async Task GenerateSoundAsync(SoundGenerationRequest request, CancellationToken cancellationToken = default) + { + using var payload = JsonSerializer.Serialize(request, ElevenLabsClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl(), payload, cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, payload, cancellationToken).ConfigureAwait(false); + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using var memoryStream = new MemoryStream(); + await responseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + return new GeneratedClip(Guid.NewGuid().ToString(), request.Text, memoryStream.ToArray()); + } + + ///// + ///// Get metadata about all your generated sounds. + ///// + ///// + ///// Optional, number of items to return. Cannot exceed 1000.
+ ///// Default: 100 + ///// + ///// Optional, the id of the item to start after. + ///// Optional, . + ///// . + //public async Task> GetHistoryAsync(int? pageSize = null, string startAfterId = null, CancellationToken cancellationToken = default) + //{ + // var parameters = new Dictionary(); + + // if (pageSize.HasValue) + // { + // parameters.Add("page_size", pageSize.ToString()); + // } + + // if (!string.IsNullOrWhiteSpace(startAfterId)) + // { + // parameters.Add("start_after_history_item_id", startAfterId); + // } + + // var response = await client.Client.GetAsync(GetUrl("/history", queryParameters: parameters), cancellationToken).ConfigureAwait(false); + // var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); + // return JsonSerializer.Deserialize>(responseAsString, ElevenLabsClient.JsonSerializationOptions); + //} + + ///// + ///// Get a sound by history item id. + ///// + ///// or . + ///// Optional, . + ///// . + //public async Task GetSoundAsync(string id, CancellationToken cancellationToken = default) + //{ + // var response = await client.Client.GetAsync(GetUrl($"/history/{id}"), cancellationToken).ConfigureAwait(false); + // var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); + // return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); + //} + + ///// + ///// Download audio of a sound history item. + ///// + ///// or . + ///// Optional, . + ///// . + //public async Task DownloadSoundAudioAsync(string id, CancellationToken cancellationToken = default) + //{ + // var historyItem = await GetSoundAsync(id, cancellationToken).ConfigureAwait(false); + // return await DownloadSoundAudioAsync(historyItem, cancellationToken).ConfigureAwait(false); + //} + + ///// + ///// Download audio of a sound history item. + ///// + ///// . + ///// Optional, . + ///// . + //public async Task DownloadSoundAudioAsync(SoundHistoryItem historyItem, CancellationToken cancellationToken = default) + //{ + // using var response = await client.Client.GetAsync(GetUrl($"/history/{historyItem.Id}/audio"), cancellationToken).ConfigureAwait(false); + // await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + // var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + // var memoryStream = new MemoryStream(); + // byte[] clipData; + + // try + // { + // await responseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + // clipData = memoryStream.ToArray(); + // } + // finally + // { + // await responseStream.DisposeAsync().ConfigureAwait(false); + // await memoryStream.DisposeAsync().ConfigureAwait(false); + // } + + // return new GeneratedClip(historyItem.Id, historyItem.Text, clipData); + //} + + ///// + ///// Delete a history item by its id. + ///// + ///// or . + ///// Optional, . + ///// True, if history item was successfully deleted. + //public async Task DeleteHistoryItemAsync(string id, CancellationToken cancellationToken = default) + //{ + // using var response = await client.Client.DeleteAsync(GetUrl($"/history/{id}"), cancellationToken).ConfigureAwait(false); + // await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + // return response.IsSuccessStatusCode; + //} + + ///// + ///// Download one or more history items.
+ ///// If no ids are specified, then the last 100 history items are downloaded.
+ ///// If one history item id is provided, we will return a single audio file.
+ ///// If more than one history item ids are provided multiple audio files will be downloaded. + /////
+ ///// Optional, One or more history item ids queued for download. + ///// Optional, . + ///// A list of voice clips downloaded by the request. + //public async Task> DownloadHistoryItemsAsync(List historyItemIds = null, CancellationToken cancellationToken = default) + //{ + // historyItemIds ??= (await GetHistoryAsync(cancellationToken: cancellationToken)).HistoryItems.Select(item => item.Id).ToList(); + //var clips = new ConcurrentBag(); + + //async Task DownloadItem(string id) + //{ + // try + // { + // var historyItem = await GetSoundAsync(id, cancellationToken).ConfigureAwait(false); + // var clip = await DownloadSoundAudioAsync(historyItem, cancellationToken).ConfigureAwait(false); + // clips.Add(clip); + // } + // catch (Exception e) + // { + // Console.WriteLine(e); + // } + //} + + //await Task.WhenAll(historyItemIds.Select(DownloadItem)).ConfigureAwait(false); + // return clips.ToList(); + //} + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/SoundGeneration/SoundGenerationRequest.cs b/ElevenLabs-DotNet/SoundGeneration/SoundGenerationRequest.cs new file mode 100644 index 0000000..9bf41ec --- /dev/null +++ b/ElevenLabs-DotNet/SoundGeneration/SoundGenerationRequest.cs @@ -0,0 +1,69 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Text.Json.Serialization; + +namespace ElevenLabs.SoundGeneration +{ + public sealed class SoundGenerationRequest + { + /// + /// + /// + /// + /// The text that will get converted into a sound effect. + /// + /// + /// The duration of the sound which will be generated in seconds. + /// Must be at least 0.5 and at most 22. + /// If set to None we will guess the optimal duration using the prompt. + /// Defaults to None. + /// + /// + /// A higher prompt influence makes your generation follow the prompt more closely while also making generations less variable. + /// Must be a value between 0 and 1. + /// Defaults to 0.3. + /// + public SoundGenerationRequest(string text, float? duration = null, float? promptInfluence = null) + { + Text = text; + + if (duration is > 22f or < 0.5f) + { + throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be a value between 0.5 and 22."); + } + + Duration = duration; + + if (promptInfluence is > 1f or < 0f) + { + throw new ArgumentOutOfRangeException(nameof(promptInfluence), "Prompt influence must be a value between 0 and 1."); + } + + PromptInfluence = promptInfluence; + } + + /// + /// The text that will get converted into a sound effect. + /// + [JsonPropertyName("text")] + public string Text { get; } + + /// + /// The duration of the sound which will be generated in seconds. + /// Must be at least 0.5 and at most 22. + /// If set to None we will guess the optimal duration using the prompt. + /// Defaults to None. + /// + [JsonPropertyName("duration_seconds")] + public float? Duration { get; } + + /// + /// A higher prompt influence makes your generation follow the prompt more closely while also making generations less variable. + /// Must be a value between 0 and 1. + /// Defaults to 0.3. + /// + [JsonPropertyName("prompt_influence")] + public float? PromptInfluence { get; } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/SoundGeneration/SoundHistoryItem.cs b/ElevenLabs-DotNet/SoundGeneration/SoundHistoryItem.cs new file mode 100644 index 0000000..8a2d73c --- /dev/null +++ b/ElevenLabs-DotNet/SoundGeneration/SoundHistoryItem.cs @@ -0,0 +1,35 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Text.Json.Serialization; + +namespace ElevenLabs.SoundGeneration +{ + public sealed class SoundHistoryItem + { + [JsonInclude] + [JsonPropertyName("sound_generation_history_item_id")] + public string Id { get; private set; } + + [JsonInclude] + [JsonPropertyName("text")] + public string Text { get; private set; } + + [JsonInclude] + [JsonPropertyName("created_at_unix")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + [JsonInclude] + [JsonPropertyName("content_type")] + public string ContentType { get; private set; } + + [JsonInclude] + [JsonPropertyName("generation_config")] + public SoundGenerationConfig SoundGenerationConfig { get; private set; } + + public static implicit operator string(SoundHistoryItem item) => item?.Id; + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs b/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs index c32b5fc..ad2a9af 100644 --- a/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs +++ b/ElevenLabs-DotNet/TextToSpeech/TextToSpeechEndpoint.cs @@ -94,7 +94,7 @@ public async Task TextToSpeechAsync(string text, Voice voice, VoiceSe ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead; var response = await client.Client.SendAsync(postRequest, requestOption, cancellationToken); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, payload, cancellationToken).ConfigureAwait(false); var clipId = response.Headers.GetValues(HistoryItemId).FirstOrDefault(); if (string.IsNullOrWhiteSpace(clipId)) diff --git a/ElevenLabs-DotNet/Users/UserEndpoint.cs b/ElevenLabs-DotNet/Users/UserEndpoint.cs index bb0451f..59dc0b5 100644 --- a/ElevenLabs-DotNet/Users/UserEndpoint.cs +++ b/ElevenLabs-DotNet/Users/UserEndpoint.cs @@ -2,6 +2,7 @@ using ElevenLabs.Extensions; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace ElevenLabs.User @@ -18,20 +19,20 @@ public UserEndpoint(ElevenLabsClient client) : base(client) { } /// /// Gets information about your user account. /// - public async Task GetUserInfoAsync() + public async Task GetUserInfoAsync(CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl()); - var responseAsString = await response.ReadAsStringAsync(EnableDebug); + var response = await client.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); } /// /// Gets your subscription info. /// - public async Task GetSubscriptionInfoAsync() + public async Task GetSubscriptionInfoAsync(CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl("/subscription")); - var responseAsString = await response.ReadAsStringAsync(EnableDebug); + var response = await client.Client.GetAsync(GetUrl("/subscription"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); } } diff --git a/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs b/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs index e4ec9bb..cb191df 100644 --- a/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs +++ b/ElevenLabs-DotNet/VoiceGeneration/VoiceGenerationEndpoint.cs @@ -24,8 +24,8 @@ public VoiceGenerationEndpoint(ElevenLabsClient client) : base(client) { } /// . public async Task GetVoiceGenerationOptionsAsync(CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl("/generate-voice/parameters"), cancellationToken); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken); + var response = await client.Client.GetAsync(GetUrl("/generate-voice/parameters"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); } @@ -38,8 +38,8 @@ public async Task GetVoiceGenerationOptionsAsync(Cancella public async Task>> GenerateVoicePreviewAsync(GeneratedVoicePreviewRequest generatedVoicePreviewRequest, CancellationToken cancellationToken = default) { var payload = JsonSerializer.Serialize(generatedVoicePreviewRequest, ElevenLabsClient.JsonSerializationOptions).ToJsonStringContent(); - var response = await client.Client.PostAsync(GetUrl("/generate-voice"), payload, cancellationToken); - await response.CheckResponseAsync(cancellationToken); + var response = await client.Client.PostAsync(GetUrl("/generate-voice"), payload, cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, payload, cancellationToken).ConfigureAwait(false); var generatedVoiceId = response.Headers.FirstOrDefault(pair => pair.Key == "generated_voice_id").Value.FirstOrDefault(); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var memoryStream = new MemoryStream(); @@ -65,8 +65,8 @@ public async Task>> GenerateVoicePreviewAsync public async Task CreateVoiceAsync(CreateVoiceRequest createVoiceRequest, CancellationToken cancellationToken = default) { var payload = JsonSerializer.Serialize(createVoiceRequest).ToJsonStringContent(); - var response = await client.Client.PostAsync(GetUrl("/create-voice"), payload, cancellationToken); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken); + var response = await client.Client.PostAsync(GetUrl("/create-voice"), payload, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); } } diff --git a/ElevenLabs-DotNet/Voices/SharedVoiceInfo.cs b/ElevenLabs-DotNet/Voices/SharedVoiceInfo.cs new file mode 100644 index 0000000..fd4098e --- /dev/null +++ b/ElevenLabs-DotNet/Voices/SharedVoiceInfo.cs @@ -0,0 +1,121 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Text.Json.Serialization; + +namespace ElevenLabs.Voices +{ + public sealed class SharedVoiceInfo + { + [JsonInclude] + [JsonPropertyName("public_owner_id")] + public string OwnerId { get; private set; } + + [JsonInclude] + [JsonPropertyName("voice_id")] + public string VoiceId { get; private set; } + + [JsonInclude] + [JsonPropertyName("date_unix")] + public int DateUnix { get; private set; } + + [JsonIgnore] + public DateTime Date => DateTimeOffset.FromUnixTimeSeconds(DateUnix).DateTime; + + [JsonInclude] + [JsonPropertyName("name")] + public string Name { get; private set; } + + [JsonInclude] + [JsonPropertyName("accent")] + public string Accent { get; private set; } + + [JsonInclude] + [JsonPropertyName("gender")] + public string Gender { get; private set; } + + [JsonInclude] + [JsonPropertyName("age")] + public string Age { get; private set; } + + [JsonInclude] + [JsonPropertyName("descriptive")] + public string Descriptive { get; private set; } + + [JsonInclude] + [JsonPropertyName("use_case")] + public string UseCase { get; private set; } + + [JsonInclude] + [JsonPropertyName("category")] + public string Category { get; private set; } + + [JsonInclude] + [JsonPropertyName("language")] + public string Language { get; private set; } + + [JsonInclude] + [JsonPropertyName("description")] + public string Description { get; private set; } + + [JsonInclude] + [JsonPropertyName("preview_url")] + public string PreviewUrl { get; private set; } + + [JsonInclude] + [JsonPropertyName("usage_character_count_1y")] + public int UsageCharacterCount1Y { get; private set; } + + [JsonInclude] + [JsonPropertyName("usage_character_count_7d")] + public int UsageCharacterCount7D { get; private set; } + + [JsonInclude] + [JsonPropertyName("play_api_usage_character_count_1y")] + public int PlayApiUsageCharacterCount1Y { get; private set; } + + [JsonInclude] + [JsonPropertyName("cloned_by_count")] + public int ClonedByCount { get; private set; } + + [JsonInclude] + [JsonPropertyName("rate")] + public float Rate { get; private set; } + + [JsonInclude] + [JsonPropertyName("free_users_allowed")] + public bool FreeUsersAllowed { get; private set; } + + [JsonInclude] + [JsonPropertyName("live_moderation_enabled")] + public bool LiveModerationEnabled { get; private set; } + + [JsonInclude] + [JsonPropertyName("featured")] + public bool Featured { get; private set; } + + [JsonInclude] + [JsonPropertyName("notice_period")] + public int? NoticePeriod { get; private set; } + + [JsonInclude] + [JsonPropertyName("instagram_username")] + public string InstagramUsername { get; private set; } + + [JsonInclude] + [JsonPropertyName("twitter_username")] + public string TwitterUsername { get; private set; } + + [JsonInclude] + [JsonPropertyName("youtube_username")] + public string YoutubeUsername { get; private set; } + + [JsonInclude] + [JsonPropertyName("tiktok_username")] + public string TikTokUsername { get; private set; } + + [JsonInclude] + [JsonPropertyName("image_url")] + public string ImageUrl { get; private set; } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/Voices/SharedVoiceList.cs b/ElevenLabs-DotNet/Voices/SharedVoiceList.cs new file mode 100644 index 0000000..fa3c34c --- /dev/null +++ b/ElevenLabs-DotNet/Voices/SharedVoiceList.cs @@ -0,0 +1,22 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ElevenLabs.Voices +{ + public sealed class SharedVoiceList + { + [JsonInclude] + [JsonPropertyName("voices")] + public IReadOnlyList Voices { get; private set; } + + [JsonInclude] + [JsonPropertyName("has_more")] + public bool HasMore { get; private set; } + + [JsonInclude] + [JsonPropertyName("last_sort_id")] + public string LastId { get; private set; } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/Voices/SharedVoiceQuery.cs b/ElevenLabs-DotNet/Voices/SharedVoiceQuery.cs new file mode 100644 index 0000000..bf27a15 --- /dev/null +++ b/ElevenLabs-DotNet/Voices/SharedVoiceQuery.cs @@ -0,0 +1,114 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections.Generic; + +namespace ElevenLabs.Voices +{ + public sealed class SharedVoiceQuery + { + public int? PageSize { get; set; } = null; + + public string Category { get; set; } = null; + + public string Gender { get; set; } = null; + + public string Age { get; set; } = null; + + public string Accent { get; set; } = null; + + public string Language { get; set; } = null; + + public string SearchTerms { get; set; } = null; + + public List UseCases { get; set; } = null; + + public List Descriptives { get; set; } = null; + + public bool? Featured { get; set; } = null; + + public bool? ReaderAppEnabled { get; set; } = null; + + public string OwnerId { get; set; } = null; + + public string Sort { get; set; } = null; + + public int? Page { get; set; } = null; + + public Dictionary ToQueryParams() + { + var parameters = new Dictionary(); + + if (PageSize.HasValue) + { + parameters.Add("page_size", PageSize.Value.ToString()); + } + + if (!string.IsNullOrWhiteSpace(Category)) + { + parameters.Add("category", Category); + } + + if (!string.IsNullOrWhiteSpace(Gender)) + { + parameters.Add("gender", Gender); + } + + if (!string.IsNullOrWhiteSpace(Age)) + { + parameters.Add("age", Age); + } + + if (!string.IsNullOrWhiteSpace(Accent)) + { + parameters.Add("accent", Accent); + } + + if (!string.IsNullOrWhiteSpace(Language)) + { + parameters.Add("language", Language); + } + + if (!string.IsNullOrWhiteSpace(SearchTerms)) + { + parameters.Add("search", SearchTerms); + } + + if (UseCases is { Count: > 0 }) + { + parameters.Add("use_cases", string.Join(',', UseCases)); + } + + if (Descriptives is { Count: > 0 }) + { + parameters.Add("descriptives", string.Join(',', Descriptives)); + } + + if (Featured.HasValue) + { + parameters.Add("featured", Featured.Value.ToString()); + } + + if (ReaderAppEnabled.HasValue) + { + parameters.Add("reader_app_enabled", ReaderAppEnabled.Value.ToString()); + } + + if (!string.IsNullOrWhiteSpace(OwnerId)) + { + parameters.Add("owner_id", OwnerId); + } + + if (!string.IsNullOrWhiteSpace(Sort)) + { + parameters.Add("sort", Sort); + } + + if (Page.HasValue) + { + parameters.Add("page", Page.Value.ToString()); + } + + return parameters; + } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/Voices/SharedVoicesEndpoint.cs b/ElevenLabs-DotNet/Voices/SharedVoicesEndpoint.cs new file mode 100644 index 0000000..de8ade0 --- /dev/null +++ b/ElevenLabs-DotNet/Voices/SharedVoicesEndpoint.cs @@ -0,0 +1,29 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using ElevenLabs.Extensions; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ElevenLabs.Voices +{ + public sealed class SharedVoicesEndpoint : ElevenLabsBaseEndPoint + { + public SharedVoicesEndpoint(ElevenLabsClient client) : base(client) { } + + protected override string Root => "shared-voices"; + + /// + /// Gets a list of shared voices. + /// + /// Optional, . + /// Optional, . + /// . + public async Task GetSharedVoicesAsync(SharedVoiceQuery query = null, CancellationToken cancellationToken = default) + { + using var response = await client.Client.GetAsync(GetUrl(queryParameters: query?.ToQueryParams()), cancellationToken); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken); + return JsonSerializer.Deserialize(responseAsString, ElevenLabsClient.JsonSerializationOptions); + } + } +} \ No newline at end of file diff --git a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs index 252aab8..3b7e61f 100644 --- a/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs +++ b/ElevenLabs-DotNet/Voices/VoicesEndpoint.cs @@ -41,8 +41,8 @@ public VoicesEndpoint(ElevenLabsClient client) : base(client) { } /// /// /// of s. - public Task> GetAllVoicesAsync(CancellationToken cancellationToken = default) - => GetAllVoicesAsync(true, cancellationToken); + public async Task> GetAllVoicesAsync(CancellationToken cancellationToken = default) + => await GetAllVoicesAsync(true, cancellationToken).ConfigureAwait(false); /// /// Gets a list of all available voices for a user. @@ -62,7 +62,7 @@ public async Task> GetAllVoicesAsync(bool downloadSettings, foreach (var voice in voices) { - voiceSettingsTasks.Add(Task.Run(LocalGetVoiceSettingsAsync, cancellationToken)); + voiceSettingsTasks.Add(LocalGetVoiceSettingsAsync()); async Task LocalGetVoiceSettingsAsync() { @@ -348,7 +348,7 @@ public async Task EditVoiceAsync(Voice voice, IEnumerable samplePa } var response = await client.Client.PostAsync(GetUrl($"/{voice.Id}/edit"), form, cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, form, cancellationToken).ConfigureAwait(false); return response.IsSuccessStatusCode; } @@ -366,7 +366,7 @@ public async Task DeleteVoiceAsync(string voiceId, CancellationToken cance } var response = await client.Client.DeleteAsync(GetUrl($"/{voiceId}"), cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return response.IsSuccessStatusCode; } @@ -394,7 +394,7 @@ public async Task DownloadVoiceSampleAudioAsync(Voice voice, Sample s } var response = await client.Client.GetAsync(GetUrl($"/{voice.Id}/samples/{sample.Id}/audio"), cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var memoryStream = new MemoryStream(); await responseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); @@ -420,8 +420,8 @@ public async Task DeleteVoiceSampleAsync(string voiceId, string sampleId, throw new ArgumentNullException(nameof(sampleId)); } - var response = await client.Client.DeleteAsync(GetUrl($"/{voiceId}/samples/{sampleId}"), cancellationToken); - await response.CheckResponseAsync(cancellationToken); + var response = await client.Client.DeleteAsync(GetUrl($"/{voiceId}/samples/{sampleId}"), cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return response.IsSuccessStatusCode; } diff --git a/LICENSE b/LICENSE index 6a82cb0..029dfaf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Rage Against The Pixel +Copyright (c) 2024 Rage Against The Pixel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal