Skip to content

Commit a259145

Browse files
committed
feat: initial implementation of SpeechSynthesizer
1 parent 3693cbb commit a259145

32 files changed

+936
-37
lines changed

sample/Cnblogs.DashScope.Sample/Program.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@
6363
userInput = Console.ReadLine()!;
6464
await ApplicationCallAsync(applicationId, userInput);
6565
break;
66+
case SampleType.TextToSpeech:
67+
var tts = await dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync("cosyvoice-v2");
68+
var taskId = await tts.RunTaskAsync(
69+
new SpeechSynthesizerParameters() { Voice = "longxiaochun_v2", Format = "mp3" });
70+
await tts.ContinueTaskAsync(taskId, "博客园");
71+
await tts.ContinueTaskAsync(taskId, "代码改变世界");
72+
await tts.FinishTaskAsync(taskId);
73+
var file = new FileInfo("tts.mp3");
74+
var writer = file.OpenWrite();
75+
await foreach (var b in tts.GetAudioAsync())
76+
{
77+
writer.WriteByte(b);
78+
}
79+
80+
writer.Close();
81+
82+
Console.WriteLine($"audio saved to {file.FullName}");
83+
break;
6684
}
6785

6886
return;

sample/Cnblogs.DashScope.Sample/SampleType.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ public enum SampleType
1616

1717
MicrosoftExtensionsAiToolCall,
1818

19-
ApplicationCall
19+
ApplicationCall,
20+
21+
TextToSpeech,
2022
}

sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public static string GetDescription(this SampleType sampleType)
1414
SampleType.MicrosoftExtensionsAi => "Use with Microsoft.Extensions.AI",
1515
SampleType.MicrosoftExtensionsAiToolCall => "Use tool call with Microsoft.Extensions.AI interfaces",
1616
SampleType.ApplicationCall => "Call pre-defined application",
17+
SampleType.TextToSpeech => "TTS task",
1718
_ => throw new ArgumentOutOfRangeException(nameof(sampleType), sampleType, "Unsupported sample option")
1819
};
1920
}

src/Cnblogs.DashScope.AspNetCore/Cnblogs.DashScope.AspNetCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<Product>Cnblogs.DashScopeSDK</Product>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
66
<PackageTags>Cnblogs;Dashscope;AI;Sdk;Embedding;AspNetCore</PackageTags>
7+
<RootNamespace>Cnblogs.DashScope.AspNetCore</RootNamespace>
78
</PropertyGroup>
89

910
<ItemGroup>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Cnblogs.DashScope.AspNetCore;
2+
3+
internal static class DashScopeAspNetCoreDefaults
4+
{
5+
public const string DefaultHttpClientName = "Cnblogs.DashScope.Http";
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Cnblogs.DashScope.Core;
2+
using Microsoft.Extensions.Options;
3+
4+
namespace Cnblogs.DashScope.AspNetCore;
5+
6+
/// <summary>
7+
/// The <see cref="DashScopeClientCore"/> with DI and options pattern support.
8+
/// </summary>
9+
public class DashScopeClientAspNetCore(IHttpClientFactory factory, DashScopeClientWebSocketPool pool)
10+
: DashScopeClientCore(factory.CreateClient(DashScopeAspNetCoreDefaults.DefaultHttpClientName), pool);

src/Cnblogs.DashScope.AspNetCore/ServiceCollectionInjector.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System.Net.Http.Headers;
2+
using Cnblogs.DashScope.AspNetCore;
23
using Cnblogs.DashScope.Core;
4+
using Cnblogs.DashScope.Core.Internals;
35
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Options;
47

58
// ReSharper disable once CheckNamespace
69
namespace Microsoft.Extensions.DependencyInjection;
@@ -37,9 +40,10 @@ public static IHttpClientBuilder AddDashScopeClient(this IServiceCollection serv
3740
{
3841
var apiKey = section["apiKey"]
3942
?? throw new InvalidOperationException("There is no apiKey provided in given section");
40-
var baseAddress = section["baseAddress"];
43+
var baseAddress = section["baseAddress"] ?? DashScopeDefaults.DashScopeHttpApiBaseAddress;
4144
var workspaceId = section["workspaceId"];
42-
return services.AddDashScopeClient(apiKey, baseAddress, workspaceId);
45+
services.Configure<DashScopeOptions>(section);
46+
return services.AddDashScopeHttpClient(apiKey, baseAddress, workspaceId);
4347
}
4448

4549
/// <summary>
@@ -48,16 +52,46 @@ public static IHttpClientBuilder AddDashScopeClient(this IServiceCollection serv
4852
/// <param name="services">The service collection to add service to.</param>
4953
/// <param name="apiKey">The DashScope api key.</param>
5054
/// <param name="baseAddress">The DashScope api base address, you may change this value if you are using proxy.</param>
55+
/// <param name="baseWebsocketAddress">The DashScope websocket base address, you may want to change this value if use are using proxy.</param>
5156
/// <param name="workspaceId">Default workspace id to use.</param>
5257
/// <returns></returns>
5358
public static IHttpClientBuilder AddDashScopeClient(
5459
this IServiceCollection services,
5560
string apiKey,
5661
string? baseAddress = null,
62+
string? baseWebsocketAddress = null,
5763
string? workspaceId = null)
5864
{
59-
baseAddress ??= "https://dashscope.aliyuncs.com/api/v1/";
60-
return services.AddHttpClient<IDashScopeClient, DashScopeClientCore>(
65+
services.Configure<DashScopeOptions>(o =>
66+
{
67+
o.ApiKey = apiKey;
68+
if (baseAddress != null)
69+
{
70+
o.BaseAddress = baseAddress;
71+
}
72+
73+
if (baseWebsocketAddress != null)
74+
{
75+
o.BaseWebsocketAddress = baseWebsocketAddress;
76+
}
77+
78+
o.WorkspaceId = workspaceId;
79+
});
80+
81+
return services.AddDashScopeHttpClient(apiKey, baseAddress, workspaceId);
82+
}
83+
84+
private static IHttpClientBuilder AddDashScopeHttpClient(
85+
this IServiceCollection services,
86+
string apiKey,
87+
string baseAddress,
88+
string? workspaceId)
89+
{
90+
services.AddSingleton<DashScopeClientWebSocketPool>(sp
91+
=> new DashScopeClientWebSocketPool(sp.GetRequiredService<IOptions<DashScopeOptions>>().Value));
92+
services.AddScoped<IDashScopeClient, DashScopeClientAspNetCore>();
93+
return services.AddHttpClient(
94+
DashScopeAspNetCoreDefaults.DefaultHttpClientName,
6195
h =>
6296
{
6397
h.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);

src/Cnblogs.DashScope.Core/DashScopeClient.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,59 @@ namespace Cnblogs.DashScope.Core;
99
public class DashScopeClient : DashScopeClientCore
1010
{
1111
private static readonly Dictionary<string, HttpClient> ClientPools = new();
12+
private static readonly Dictionary<string, DashScopeClientWebSocketPool> SocketPools = new();
1213

1314
/// <summary>
1415
/// Creates a DashScopeClient for further api call.
1516
/// </summary>
1617
/// <param name="apiKey">The DashScope api key.</param>
1718
/// <param name="timeout">The timeout for internal http client, defaults to 2 minute.</param>
18-
/// <param name="baseAddress">The base address for dashscope api call.</param>
19+
/// <param name="baseAddress">The base address for DashScope api call.</param>
20+
/// <param name="baseWebsocketAddress">The base address for DashScope websocket api call.</param>
1921
/// <param name="workspaceId">The workspace id.</param>
22+
/// <param name="socketPoolSize">Maximum size of socket pool.</param>
2023
/// <remarks>
2124
/// The underlying httpclient is cached by constructor parameter list.
2225
/// Client created with same parameter value will share same underlying <see cref="HttpClient"/> instance.
2326
/// </remarks>
2427
public DashScopeClient(
2528
string apiKey,
2629
TimeSpan? timeout = null,
27-
string? baseAddress = null,
30+
string baseAddress = DashScopeDefaults.DashScopeHttpApiBaseAddress,
31+
string baseWebsocketAddress = DashScopeDefaults.DashScopeWebsocketApiBaseAddress,
32+
string? workspaceId = null,
33+
int socketPoolSize = 5)
34+
: base(
35+
GetConfiguredClient(apiKey, timeout, baseAddress, workspaceId),
36+
GetConfiguredSocketPool(apiKey, baseWebsocketAddress, socketPoolSize, workspaceId))
37+
{
38+
}
39+
40+
private static DashScopeClientWebSocketPool GetConfiguredSocketPool(
41+
string apiKey,
42+
string baseAddress,
43+
int socketPoolSize = 5,
2844
string? workspaceId = null)
29-
: base(GetConfiguredClient(apiKey, timeout, baseAddress, workspaceId))
3045
{
46+
var key = GetCacheKey();
47+
48+
var pool = SocketPools.GetValueOrDefault(key);
49+
if (pool is null)
50+
{
51+
pool = new DashScopeClientWebSocketPool(
52+
new DashScopeOptions()
53+
{
54+
ApiKey = apiKey,
55+
BaseWebsocketAddress = baseAddress,
56+
SocketPoolSize = socketPoolSize,
57+
WorkspaceId = workspaceId
58+
});
59+
SocketPools.Add(key, pool);
60+
}
61+
62+
return pool;
63+
64+
string GetCacheKey() => $"{apiKey}-{socketPoolSize}-{baseAddress}-{workspaceId}";
3165
}
3266

3367
private static HttpClient GetConfiguredClient(
@@ -41,7 +75,7 @@ private static HttpClient GetConfiguredClient(
4175
{
4276
client = new HttpClient
4377
{
44-
BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.DashScopeApiBaseAddress),
78+
BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.DashScopeHttpApiBaseAddress),
4579
Timeout = timeout ?? TimeSpan.FromMinutes(2)
4680
};
4781

src/Cnblogs.DashScope.Core/DashScopeClientCore.cs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Runtime.CompilerServices;
55
using System.Text;
66
using System.Text.Json;
7-
using System.Text.Json.Serialization;
87
using Cnblogs.DashScope.Core.Internals;
98

109
namespace Cnblogs.DashScope.Core;
@@ -14,22 +13,18 @@ namespace Cnblogs.DashScope.Core;
1413
/// </summary>
1514
public class DashScopeClientCore : IDashScopeClient
1615
{
17-
private static readonly JsonSerializerOptions SerializationOptions =
18-
new()
19-
{
20-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
21-
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
22-
};
23-
2416
private readonly HttpClient _httpClient;
17+
private readonly DashScopeClientWebSocketPool _socketPool;
2518

2619
/// <summary>
2720
/// For DI container to inject pre-configured httpclient.
2821
/// </summary>
2922
/// <param name="httpClient">Pre-configured httpclient.</param>
30-
public DashScopeClientCore(HttpClient httpClient)
23+
/// <param name="pool">Websocket pool.</param>
24+
public DashScopeClientCore(HttpClient httpClient, DashScopeClientWebSocketPool pool)
3125
{
3226
_httpClient = httpClient;
27+
_socketPool = pool;
3328
}
3429

3530
/// <inheritdoc />
@@ -283,6 +278,15 @@ public async Task<DashScopeDeleteFileResult> DeleteFileAsync(
283278
return (await SendCompatibleAsync<DashScopeDeleteFileResult>(request, cancellationToken))!;
284279
}
285280

281+
/// <inheritdoc />
282+
public async Task<SpeechSynthesizerSocketSession> CreateSpeechSynthesizerSocketSessionAsync(
283+
string modelId,
284+
CancellationToken cancellationToken = default)
285+
{
286+
var socket = await _socketPool.RentSocketAsync<SpeechSynthesizerOutput>(cancellationToken);
287+
return new SpeechSynthesizerSocketSession(socket, modelId);
288+
}
289+
286290
private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
287291
where TPayload : class
288292
{
@@ -304,7 +308,9 @@ private static HttpRequestMessage BuildRequest<TPayload>(
304308
{
305309
var message = new HttpRequestMessage(method, url)
306310
{
307-
Content = payload != null ? JsonContent.Create(payload, options: SerializationOptions) : null
311+
Content = payload != null
312+
? JsonContent.Create(payload, options: DashScopeDefaults.SerializationOptions)
313+
: null
308314
};
309315

310316
if (sse)
@@ -340,7 +346,9 @@ private static HttpRequestMessage BuildRequest<TPayload>(
340346
},
341347
HttpCompletionOption.ResponseContentRead,
342348
cancellationToken);
343-
return await response.Content.ReadFromJsonAsync<TResponse>(SerializationOptions, cancellationToken);
349+
return await response.Content.ReadFromJsonAsync<TResponse>(
350+
DashScopeDefaults.SerializationOptions,
351+
cancellationToken);
344352
}
345353

346354
private async Task<TResponse?> SendAsync<TResponse>(HttpRequestMessage message, CancellationToken cancellationToken)
@@ -350,7 +358,9 @@ private static HttpRequestMessage BuildRequest<TPayload>(
350358
message,
351359
HttpCompletionOption.ResponseContentRead,
352360
cancellationToken);
353-
return await response.Content.ReadFromJsonAsync<TResponse>(SerializationOptions, cancellationToken);
361+
return await response.Content.ReadFromJsonAsync<TResponse>(
362+
DashScopeDefaults.SerializationOptions,
363+
cancellationToken);
354364
}
355365

356366
private async IAsyncEnumerable<TResponse> StreamAsync<TResponse>(
@@ -373,15 +383,16 @@ private async IAsyncEnumerable<TResponse> StreamAsync<TResponse>(
373383
var data = line["data:".Length..];
374384
if (data.StartsWith("{\"code\":"))
375385
{
376-
var error = JsonSerializer.Deserialize<DashScopeError>(data, SerializationOptions)!;
386+
var error =
387+
JsonSerializer.Deserialize<DashScopeError>(data, DashScopeDefaults.SerializationOptions)!;
377388
throw new DashScopeException(
378389
message.RequestUri?.ToString(),
379390
(int)response.StatusCode,
380391
error,
381392
error.Message);
382393
}
383394

384-
yield return JsonSerializer.Deserialize<TResponse>(data, SerializationOptions)!;
395+
yield return JsonSerializer.Deserialize<TResponse>(data, DashScopeDefaults.SerializationOptions)!;
385396
}
386397
}
387398
}
@@ -418,7 +429,9 @@ private async Task<HttpResponseMessage> GetSuccessResponseAsync<TError>(
418429
DashScopeError? error = null;
419430
try
420431
{
421-
var r = await response.Content.ReadFromJsonAsync<TError>(SerializationOptions, cancellationToken);
432+
var r = await response.Content.ReadFromJsonAsync<TError>(
433+
DashScopeDefaults.SerializationOptions,
434+
cancellationToken);
422435
error = r == null ? null : errorMapper.Invoke(r);
423436
}
424437
catch (Exception)

0 commit comments

Comments
 (0)