From 4e00a491133f7ac5c04ba0472eb031e5c09cea39 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 1 Sep 2024 23:41:14 -0400 Subject: [PATCH] . --- .gitignore | 1 + .../TestFixture_08_DubbingEndpoint.cs | 36 ++++++-- ElevenLabs-DotNet/Dubbing/DubbingEndpoint.cs | 90 +++++++++---------- .../Dubbing/DubbingProjectMetadata.cs | 7 ++ ElevenLabs-DotNet/Dubbing/DubbingResponse.cs | 2 + 5 files changed, 80 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 2e31c64..b1d6357 100644 --- a/.gitignore +++ b/.gitignore @@ -339,3 +339,4 @@ ASALocalRun/ # BeatPulse healthcheck temp database healthchecksdb .vscode +*.dubbed.* diff --git a/ElevenLabs-DotNet-Tests/TestFixture_08_DubbingEndpoint.cs b/ElevenLabs-DotNet-Tests/TestFixture_08_DubbingEndpoint.cs index b3c1044..421bd44 100644 --- a/ElevenLabs-DotNet-Tests/TestFixture_08_DubbingEndpoint.cs +++ b/ElevenLabs-DotNet-Tests/TestFixture_08_DubbingEndpoint.cs @@ -16,11 +16,23 @@ public async Task Test_01_Dubbing_File() Assert.NotNull(ElevenLabsClient.DubbingEndpoint); var filePath = Path.GetFullPath("../../../Assets/test_sample_01.ogg"); var request = new DubbingRequest(filePath, "es", "en", 1); - var response = await ElevenLabsClient.DubbingEndpoint.DubAsync(request); + var response = await ElevenLabsClient.DubbingEndpoint.DubAsync(request, progress: new Progress(metadata => + { + switch (metadata.Status) + { + case "dubbing": + Console.WriteLine($"Dubbing for {metadata.DubbingId} in progress... Expected Duration: {metadata.ExpectedDurationSeconds:0.00} seconds"); + break; + case "dubbed": + Console.WriteLine($"Dubbing for {metadata.DubbingId} complete in {metadata.TimeCompleted.TotalSeconds:0.00} seconds!"); + break; + default: + Console.WriteLine($"Status: {metadata.Status}"); + break; + } + })); Assert.IsFalse(string.IsNullOrEmpty(response.DubbingId)); Assert.IsTrue(response.ExpectedDurationSeconds > 0); - Console.WriteLine($"Expected Duration: {response.ExpectedDurationSeconds:0.00} seconds"); - Assert.IsTrue(await ElevenLabsClient.DubbingEndpoint.WaitForDubbingCompletionAsync(response.DubbingId, progress: new Progress(Console.WriteLine))); var srcFile = new FileInfo(filePath); var dubbedPath = new FileInfo($"{srcFile.FullName}.dubbed.{request.TargetLanguage}{srcFile.Extension}"); @@ -52,11 +64,23 @@ public async Task Test_02_Dubbing_Url() var uri = new Uri("https://youtu.be/Zo5-rhYOlNk"); var request = new DubbingRequest(uri, "ja", "en", 1, true); - var response = await ElevenLabsClient.DubbingEndpoint.DubAsync(request); + var response = await ElevenLabsClient.DubbingEndpoint.DubAsync(request, progress: new Progress(metadata => + { + switch (metadata.Status) + { + case "dubbing": + Console.WriteLine($"Dubbing for {metadata.DubbingId} in progress... Expected Duration: {metadata.ExpectedDurationSeconds:0.00} seconds"); + break; + case "dubbed": + Console.WriteLine($"Dubbing for {metadata.DubbingId} complete in {metadata.TimeCompleted.TotalSeconds:0.00} seconds!"); + break; + default: + Console.WriteLine($"Status: {metadata.Status}"); + break; + } + })); Assert.IsFalse(string.IsNullOrEmpty(response.DubbingId)); Assert.IsTrue(response.ExpectedDurationSeconds > 0); - Console.WriteLine($"Expected Duration: {response.ExpectedDurationSeconds:0.00} seconds"); - Assert.IsTrue(await ElevenLabsClient.DubbingEndpoint.WaitForDubbingCompletionAsync(response.DubbingId, progress: new Progress(Console.WriteLine))); var assetsDir = Path.GetFullPath("../../../Assets"); var dubbedPath = new FileInfo(Path.Combine(assetsDir, $"online.dubbed.{request.TargetLanguage}.mp4")); diff --git a/ElevenLabs-DotNet/Dubbing/DubbingEndpoint.cs b/ElevenLabs-DotNet/Dubbing/DubbingEndpoint.cs index 6a9292e..c9d0e98 100644 --- a/ElevenLabs-DotNet/Dubbing/DubbingEndpoint.cs +++ b/ElevenLabs-DotNet/Dubbing/DubbingEndpoint.cs @@ -3,6 +3,7 @@ using ElevenLabs.Extensions; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Net.Http; using System.Runtime.CompilerServices; @@ -20,25 +21,18 @@ public sealed class DubbingEndpoint(ElevenLabsClient client) : ElevenLabsBaseEnd private const string DubbingId = "dubbing_id"; private const string ExpectedDurationSecs = "expected_duration_sec"; - /// - /// Gets or sets the maximum number of retry attempts to wait for the dubbing completion status. - /// - public int DefaultMaxRetries { get; set; } = 30; - - /// - /// Gets or sets the timeout interval for waiting between dubbing status checks. - /// - public TimeSpan DefaultTimeoutInterval { get; set; } = TimeSpan.FromSeconds(10); - protected override string Root => "dubbing"; /// /// Dubs provided audio or video file into given language. /// /// The containing dubbing configuration and files. + /// /// Optional, . - /// . - public async Task DubAsync(DubbingRequest request, CancellationToken cancellationToken = default) + /// + /// + /// . + public async Task DubAsync(DubbingRequest request, int? maxRetries = null, TimeSpan? pollingInterval = null, IProgress progress = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); using var payload = new MultipartFormDataContent(); @@ -101,53 +95,52 @@ public async Task DubAsync(DubbingRequest request, Cancellation } 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); - return await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken).ConfigureAwait(false); + var responseBody = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + var dubResponse = JsonSerializer.Deserialize(responseBody); + var metadata = await WaitForDubbingCompletionAsync(dubResponse, maxRetries ?? 60, pollingInterval ?? TimeSpan.FromSeconds(dubResponse.ExpectedDurationSeconds), pollingInterval == null, progress, cancellationToken); + return metadata; } - /// - /// Waits asynchronously for a dubbing operation to complete. This method polls the dubbing status at regular intervals, - /// reporting progress updates if a progress reporter is provided. - /// - /// The ID of the dubbing project. - /// The maximum number of retries for checking the dubbing completion status. If not specified, a default value is used. - /// The time to wait between each status check. If not specified, a default interval is used. - /// An optional implementation to report progress updates, such as status messages and errors. - /// A to cancel the waiting operation. - /// - /// A task that represents the asynchronous wait operation. The task result is - /// if the dubbing completes successfully within the specified number of retries and timeout interval; otherwise, . - /// - /// - /// This method checks the dubbing status by sending requests to the dubbing service at intervals defined by the parameter. - /// If the dubbing status is "dubbed", the method returns . If the dubbing fails or the specified number of is reached without successful completion, the method returns . - /// - public async Task WaitForDubbingCompletionAsync(string dubbingId, int? maxRetries = null, TimeSpan? timeoutInterval = null, IProgress progress = null, CancellationToken cancellationToken = default) + private async Task WaitForDubbingCompletionAsync(DubbingResponse dubbingResponse, int maxRetries, TimeSpan pollingInterval, bool adjustInterval, IProgress progress = null, CancellationToken cancellationToken = default) { - maxRetries ??= DefaultMaxRetries; - timeoutInterval ??= DefaultTimeoutInterval; + var stopwatch = Stopwatch.StartNew(); - for (var i = 0; i < maxRetries; i++) + for (var i = 1; i < maxRetries + 1; i++) { - var metadata = await GetDubbingProjectMetadataAsync(dubbingId, cancellationToken).ConfigureAwait(false); + var metadata = await GetDubbingProjectMetadataAsync(dubbingResponse, cancellationToken).ConfigureAwait(false); + metadata.ExpectedDurationSeconds = dubbingResponse.ExpectedDurationSeconds; + + if (metadata.Status.Equals("dubbed", StringComparison.Ordinal)) + { + stopwatch.Stop(); + metadata.TimeCompleted = stopwatch.Elapsed; + progress?.Report(metadata); + return metadata; + } - if (metadata.Status.Equals("dubbed", StringComparison.Ordinal)) { return true; } + progress?.Report(metadata); if (metadata.Status.Equals("dubbing", StringComparison.Ordinal)) { - progress?.Report($"Dubbing for {dubbingId} in progress... Will check status again in {timeoutInterval.Value.TotalSeconds} seconds."); - await Task.Delay(timeoutInterval.Value, cancellationToken).ConfigureAwait(false); + if (EnableDebug) + { + Console.WriteLine($"Dubbing for {dubbingResponse.DubbingId} in progress... Will check status again in {pollingInterval.TotalSeconds} seconds."); + } + + if (adjustInterval) + { + pollingInterval = TimeSpan.FromSeconds(dubbingResponse.ExpectedDurationSeconds / Math.Pow(2, i)); + } + + await Task.Delay(pollingInterval, cancellationToken).ConfigureAwait(false); } else { - progress?.Report($"Dubbing for {dubbingId} failed: {metadata.Error}"); - return false; + throw new Exception($"Dubbing for {dubbingResponse.DubbingId} failed: {metadata.Error}"); } } - progress?.Report($"Dubbing for {dubbingId} timed out or exceeded expected duration."); - return false; + throw new TimeoutException($"Dubbing for {dubbingResponse.DubbingId} timed out or exceeded expected duration."); } /// @@ -158,9 +151,8 @@ public async Task WaitForDubbingCompletionAsync(string dubbingId, int? max /// . public async Task GetDubbingProjectMetadataAsync(string dubbingId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{dubbingId}"), cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - var responseBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{dubbingId}"), cancellationToken).ConfigureAwait(false); + var responseBody = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseBody); } @@ -182,8 +174,7 @@ public async Task GetTranscriptForDubAsync(string dubbingId, string lang { var @params = new Dictionary { { "format_type", formatType.ToString().ToLower() } }; using var response = await client.Client.GetAsync(GetUrl($"/{dubbingId}/transcript/{languageCode}", @params), cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + return await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); } /// @@ -205,7 +196,6 @@ public async IAsyncEnumerable GetDubbedFileAsync(string dubbingId, strin { using var response = await client.Client.GetAsync(GetUrl($"/{dubbingId}/audio/{languageCode}"), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var buffer = new byte[bufferSize]; int bytesRead; diff --git a/ElevenLabs-DotNet/Dubbing/DubbingProjectMetadata.cs b/ElevenLabs-DotNet/Dubbing/DubbingProjectMetadata.cs index 0097681..556bc77 100644 --- a/ElevenLabs-DotNet/Dubbing/DubbingProjectMetadata.cs +++ b/ElevenLabs-DotNet/Dubbing/DubbingProjectMetadata.cs @@ -1,5 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -26,5 +27,11 @@ public sealed class DubbingProjectMetadata [JsonInclude] [JsonPropertyName("error")] public string Error { get; private set; } + + [JsonIgnore] + public float ExpectedDurationSeconds { get; internal set; } + + [JsonIgnore] + public TimeSpan TimeCompleted { get; internal set; } } } diff --git a/ElevenLabs-DotNet/Dubbing/DubbingResponse.cs b/ElevenLabs-DotNet/Dubbing/DubbingResponse.cs index 1336be4..0b18f5d 100644 --- a/ElevenLabs-DotNet/Dubbing/DubbingResponse.cs +++ b/ElevenLabs-DotNet/Dubbing/DubbingResponse.cs @@ -13,5 +13,7 @@ public sealed class DubbingResponse [JsonInclude] [JsonPropertyName("expected_duration_sec")] public float ExpectedDurationSeconds { get; private set; } + + public static implicit operator string(DubbingResponse response) => response?.DubbingId; } } \ No newline at end of file