Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenHodgson committed Sep 2, 2024
1 parent 1f38b03 commit 4e00a49
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 56 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,4 @@ ASALocalRun/
# BeatPulse healthcheck temp database
healthchecksdb
.vscode
*.dubbed.*
36 changes: 30 additions & 6 deletions ElevenLabs-DotNet-Tests/TestFixture_08_DubbingEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DubbingProjectMetadata>(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<string>(Console.WriteLine)));

var srcFile = new FileInfo(filePath);
var dubbedPath = new FileInfo($"{srcFile.FullName}.dubbed.{request.TargetLanguage}{srcFile.Extension}");
Expand Down Expand Up @@ -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<DubbingProjectMetadata>(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<string>(Console.WriteLine)));

var assetsDir = Path.GetFullPath("../../../Assets");
var dubbedPath = new FileInfo(Path.Combine(assetsDir, $"online.dubbed.{request.TargetLanguage}.mp4"));
Expand Down
90 changes: 40 additions & 50 deletions ElevenLabs-DotNet/Dubbing/DubbingEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,25 +21,18 @@ public sealed class DubbingEndpoint(ElevenLabsClient client) : ElevenLabsBaseEnd
private const string DubbingId = "dubbing_id";
private const string ExpectedDurationSecs = "expected_duration_sec";

/// <summary>
/// Gets or sets the maximum number of retry attempts to wait for the dubbing completion status.
/// </summary>
public int DefaultMaxRetries { get; set; } = 30;

/// <summary>
/// Gets or sets the timeout interval for waiting between dubbing status checks.
/// </summary>
public TimeSpan DefaultTimeoutInterval { get; set; } = TimeSpan.FromSeconds(10);

protected override string Root => "dubbing";

/// <summary>
/// Dubs provided audio or video file into given language.
/// </summary>
/// <param name="request">The <see cref="DubbingRequest"/> containing dubbing configuration and files.</param>
/// <param name="progress"></param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns> <see cref="DubbingResponse"/>.</returns>
public async Task<DubbingResponse> DubAsync(DubbingRequest request, CancellationToken cancellationToken = default)
/// <param name="maxRetries"></param>
/// <param name="pollingInterval"></param>
/// <returns> <see cref="DubbingProjectMetadata"/>.</returns>
public async Task<DubbingProjectMetadata> DubAsync(DubbingRequest request, int? maxRetries = null, TimeSpan? pollingInterval = null, IProgress<DubbingProjectMetadata> progress = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
using var payload = new MultipartFormDataContent();
Expand Down Expand Up @@ -101,53 +95,52 @@ public async Task<DubbingResponse> 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<DubbingResponse>(responseStream, cancellationToken: cancellationToken).ConfigureAwait(false);
var responseBody = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
var dubResponse = JsonSerializer.Deserialize<DubbingResponse>(responseBody);
var metadata = await WaitForDubbingCompletionAsync(dubResponse, maxRetries ?? 60, pollingInterval ?? TimeSpan.FromSeconds(dubResponse.ExpectedDurationSeconds), pollingInterval == null, progress, cancellationToken);
return metadata;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="dubbingId">The ID of the dubbing project.</param>
/// <param name="maxRetries">The maximum number of retries for checking the dubbing completion status. If not specified, a default value is used.</param>
/// <param name="timeoutInterval">The time to wait between each status check. If not specified, a default interval is used.</param>
/// <param name="progress">An optional <see cref="IProgress{T}"/> implementation to report progress updates, such as status messages and errors.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the waiting operation.</param>
/// <returns>
/// A task that represents the asynchronous wait operation. The task result is <see langword="true"/>
/// if the dubbing completes successfully within the specified number of retries and timeout interval; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// This method checks the dubbing status by sending requests to the dubbing service at intervals defined by the <paramref name="timeoutInterval"/> parameter.
/// If the dubbing status is "dubbed", the method returns <see langword="true"/>. If the dubbing fails or the specified number of <paramref name="maxRetries"/> is reached without successful completion, the method returns <see langword="false"/>.
/// </remarks>
public async Task<bool> WaitForDubbingCompletionAsync(string dubbingId, int? maxRetries = null, TimeSpan? timeoutInterval = null, IProgress<string> progress = null, CancellationToken cancellationToken = default)
private async Task<DubbingProjectMetadata> WaitForDubbingCompletionAsync(DubbingResponse dubbingResponse, int maxRetries, TimeSpan pollingInterval, bool adjustInterval, IProgress<DubbingProjectMetadata> 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.");
}

/// <summary>
Expand All @@ -158,9 +151,8 @@ public async Task<bool> WaitForDubbingCompletionAsync(string dubbingId, int? max
/// <returns><see cref="DubbingProjectMetadata"/>.</returns>
public async Task<DubbingProjectMetadata> 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<DubbingProjectMetadata>(responseBody);
}

Expand All @@ -182,8 +174,7 @@ public async Task<string> GetTranscriptForDubAsync(string dubbingId, string lang
{
var @params = new Dictionary<string, string> { { "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);
}

/// <summary>
Expand All @@ -205,7 +196,6 @@ public async IAsyncEnumerable<byte[]> 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;
Expand Down
7 changes: 7 additions & 0 deletions ElevenLabs-DotNet/Dubbing/DubbingProjectMetadata.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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; }
}
}
2 changes: 2 additions & 0 deletions ElevenLabs-DotNet/Dubbing/DubbingResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

0 comments on commit 4e00a49

Please sign in to comment.