Skip to content

feat: support cosy voice #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Cnblogs.DashScope.Sdk.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002EAI_002EUnitTests_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002ESample_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002ESdk_002ESnapshotGenerator_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002ESdk_002EUnitTests_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002ESdk_002EUnitTests_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002ETests_002EShared_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002EWebSample_002EClient_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Cnblogs_002EDashScope_002EWebSample_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
38 changes: 19 additions & 19 deletions sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Cnblogs.DashScope.Sdk\Cnblogs.DashScope.Sdk.csproj" />
<ProjectReference Include="..\..\src\Cnblogs.DashScope.AI\Cnblogs.DashScope.AI.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Cnblogs.DashScope.Sdk\Cnblogs.DashScope.Sdk.csproj"/>
<ProjectReference Include="..\..\src\Cnblogs.DashScope.AI\Cnblogs.DashScope.AI.csproj"/>
</ItemGroup>

<ItemGroup>
<None Update="test.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="test.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="9.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="9.5.0"/>
</ItemGroup>

</Project>
32 changes: 31 additions & 1 deletion sample/Cnblogs.DashScope.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
using Microsoft.Extensions.AI;

Console.WriteLine("Reading key from environment variable DASHSCOPE_KEY");
var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY");
var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_KEY", EnvironmentVariableTarget.Process)
?? Environment.GetEnvironmentVariable("DASHSCOPE_KEY", EnvironmentVariableTarget.User);
if (string.IsNullOrEmpty(apiKey))
{
Console.Write("ApiKey > ");
Expand Down Expand Up @@ -63,6 +64,35 @@
userInput = Console.ReadLine()!;
await ApplicationCallAsync(applicationId, userInput);
break;
case SampleType.TextToSpeech:
{
using var tts = await dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync("cosyvoice-v2");
var taskId = await tts.RunTaskAsync(
new SpeechSynthesizerParameters { Voice = "longxiaochun_v2", Format = "mp3" });
await tts.ContinueTaskAsync(taskId, "博客园");
await tts.ContinueTaskAsync(taskId, "代码改变世界");
await tts.FinishTaskAsync(taskId);
var file = new FileInfo("tts.mp3");
var writer = file.OpenWrite();
await foreach (var b in tts.GetAudioAsync())
{
writer.WriteByte(b);
}

writer.Close();

var tokenUsage = 0;
await foreach (var message in tts.GetMessagesAsync())
{
if (message.Payload.Usage?.Characters > tokenUsage)
{
tokenUsage = message.Payload.Usage.Characters;
}
}

Console.WriteLine($"audio saved to {file.FullName}, token usage: {tokenUsage}");
break;
}
}

return;
Expand Down
4 changes: 3 additions & 1 deletion sample/Cnblogs.DashScope.Sample/SampleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ public enum SampleType

MicrosoftExtensionsAiToolCall,

ApplicationCall
ApplicationCall,

TextToSpeech,
}
1 change: 1 addition & 0 deletions sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static string GetDescription(this SampleType sampleType)
SampleType.MicrosoftExtensionsAi => "Use with Microsoft.Extensions.AI",
SampleType.MicrosoftExtensionsAiToolCall => "Use tool call with Microsoft.Extensions.AI interfaces",
SampleType.ApplicationCall => "Call pre-defined application",
SampleType.TextToSpeech => "TTS task",
_ => throw new ArgumentOutOfRangeException(nameof(sampleType), sampleType, "Unsupported sample option")
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="JsonSchema.Net.Generation" Version="5.0.2" />
<PackageReference Include="JsonSchema.Net.Generation" Version="5.0.3" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.5.0" />
</ItemGroup>

Expand Down
4 changes: 4 additions & 0 deletions src/Cnblogs.DashScope.AspNetCore/Assembly.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

[assembly:InternalsVisibleTo("Cnblogs.DashScope.Sdk.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<PropertyGroup>
<Product>Cnblogs.DashScopeSDK</Product>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>Cnblogs;Dashscope;AI;Sdk;Embedding;AspNetCore</PackageTags>
<PackageTags>Cnblogs;Dashscope;AI;Sdk;Embedding;AspNetCore;Bailian</PackageTags>
<RootNamespace>Cnblogs.DashScope.AspNetCore</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Cnblogs.DashScope.AspNetCore;

internal static class DashScopeAspNetCoreDefaults
{
public const string DefaultHttpClientName = "Cnblogs.DashScope.Http";
}
20 changes: 20 additions & 0 deletions src/Cnblogs.DashScope.AspNetCore/DashScopeClientAspNetCore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Cnblogs.DashScope.Core;

namespace Cnblogs.DashScope.AspNetCore;

/// <summary>
/// The <see cref="DashScopeClientCore"/> with DI and options pattern support.
/// </summary>
public class DashScopeClientAspNetCore
: DashScopeClientCore
{
/// <summary>
/// The <see cref="DashScopeClientCore"/> with DI and options pattern support.
/// </summary>
/// <param name="factory">The factory to create <see cref="HttpClient"/>.</param>
/// <param name="pool">The socket pool for WebSocket API calls.</param>
public DashScopeClientAspNetCore(IHttpClientFactory factory, DashScopeClientWebSocketPool pool)
: base(factory.CreateClient(DashScopeAspNetCoreDefaults.DefaultHttpClientName), pool)
{
}
}
47 changes: 42 additions & 5 deletions src/Cnblogs.DashScope.AspNetCore/ServiceCollectionInjector.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Net.Http.Headers;
using Cnblogs.DashScope.AspNetCore;
using Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Core.Internals;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -37,9 +40,10 @@ public static IHttpClientBuilder AddDashScopeClient(this IServiceCollection serv
{
var apiKey = section["apiKey"]
?? throw new InvalidOperationException("There is no apiKey provided in given section");
var baseAddress = section["baseAddress"];
var baseAddress = section["baseAddress"] ?? DashScopeDefaults.HttpApiBaseAddress;
var workspaceId = section["workspaceId"];
return services.AddDashScopeClient(apiKey, baseAddress, workspaceId);
services.Configure<DashScopeOptions>(section);
return services.AddDashScopeHttpClient(apiKey, baseAddress, workspaceId);
}

/// <summary>
Expand All @@ -48,16 +52,49 @@ public static IHttpClientBuilder AddDashScopeClient(this IServiceCollection serv
/// <param name="services">The service collection to add service to.</param>
/// <param name="apiKey">The DashScope api key.</param>
/// <param name="baseAddress">The DashScope api base address, you may change this value if you are using proxy.</param>
/// <param name="baseWebsocketAddress">The DashScope websocket base address, you may want to change this value if use are using proxy.</param>
/// <param name="workspaceId">Default workspace id to use.</param>
/// <returns></returns>
public static IHttpClientBuilder AddDashScopeClient(
this IServiceCollection services,
string apiKey,
string? baseAddress = null,
string? baseWebsocketAddress = null,
string? workspaceId = null)
{
baseAddress ??= "https://dashscope.aliyuncs.com/api/v1/";
return services.AddHttpClient<IDashScopeClient, DashScopeClientCore>(
services.Configure<DashScopeOptions>(o =>
{
o.ApiKey = apiKey;
if (baseAddress != null)
{
o.BaseAddress = baseAddress;
}

if (baseWebsocketAddress != null)
{
o.WebsocketBaseAddress = baseWebsocketAddress;
}

o.WorkspaceId = workspaceId;
});

return services.AddDashScopeHttpClient(apiKey, baseAddress, workspaceId);
}

private static IHttpClientBuilder AddDashScopeHttpClient(
this IServiceCollection services,
string apiKey,
string? baseAddress,
string? workspaceId)
{
services.AddSingleton<IDashScopeClientWebSocketFactory, DashScopeClientWebSocketFactory>();
services.AddSingleton<DashScopeClientWebSocketPool>(sp
=> new DashScopeClientWebSocketPool(
sp.GetRequiredService<IDashScopeClientWebSocketFactory>(),
sp.GetRequiredService<IOptions<DashScopeOptions>>().Value));
services.AddScoped<IDashScopeClient, DashScopeClientAspNetCore>();
return services.AddHttpClient(
DashScopeAspNetCoreDefaults.DefaultHttpClientName,
h =>
{
h.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
Expand All @@ -66,7 +103,7 @@ public static IHttpClientBuilder AddDashScopeClient(
h.DefaultRequestHeaders.Add("X-DashScope-WorkSpace", workspaceId);
}

h.BaseAddress = new Uri(baseAddress);
h.BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.HttpApiBaseAddress);
});
}
}
4 changes: 2 additions & 2 deletions src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Product>Cnblogs.DashScopeSDK</Product>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>Cnblogs;Dashscope;AI;Sdk;Embedding;</PackageTags>
<PackageTags>Cnblogs;Dashscope;AI;Sdk;Embedding;Bailian;</PackageTags>
<Description>Provide pure api access to DashScope without extra references. Cnblogs.DashScope.Sdk should be used for general purpose.</Description>
</PropertyGroup>

Expand All @@ -15,5 +15,5 @@
<PackageReference Include="Microsoft.DeepDev.TokenizerLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

</Project>
43 changes: 39 additions & 4 deletions src/Cnblogs.DashScope.Core/DashScopeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,60 @@ namespace Cnblogs.DashScope.Core;
public class DashScopeClient : DashScopeClientCore
{
private static readonly Dictionary<string, HttpClient> ClientPools = new();
private static readonly Dictionary<string, DashScopeClientWebSocketPool> SocketPools = new();

/// <summary>
/// Creates a DashScopeClient for further api call.
/// </summary>
/// <param name="apiKey">The DashScope api key.</param>
/// <param name="timeout">The timeout for internal http client, defaults to 2 minute.</param>
/// <param name="baseAddress">The base address for dashscope api call.</param>
/// <param name="baseAddress">The base address for DashScope api call.</param>
/// <param name="baseWebsocketAddress">The base address for DashScope websocket api call.</param>
/// <param name="workspaceId">The workspace id.</param>
/// <param name="socketPoolSize">Maximum size of socket pool.</param>
/// <remarks>
/// The underlying httpclient is cached by constructor parameter list.
/// Client created with same parameter value will share same underlying <see cref="HttpClient"/> instance.
/// </remarks>
public DashScopeClient(
string apiKey,
TimeSpan? timeout = null,
string? baseAddress = null,
string baseAddress = DashScopeDefaults.HttpApiBaseAddress,
string baseWebsocketAddress = DashScopeDefaults.WebsocketApiBaseAddress,
string? workspaceId = null,
int socketPoolSize = 32)
: base(
GetConfiguredClient(apiKey, timeout, baseAddress, workspaceId),
GetConfiguredSocketPool(apiKey, baseWebsocketAddress, socketPoolSize, workspaceId))
{
}

private static DashScopeClientWebSocketPool GetConfiguredSocketPool(
string apiKey,
string baseAddress,
int socketPoolSize,
string? workspaceId = null)
: base(GetConfiguredClient(apiKey, timeout, baseAddress, workspaceId))
{
var key = GetCacheKey();

var pool = SocketPools.GetValueOrDefault(key);
if (pool is null)
{
pool = new DashScopeClientWebSocketPool(
new DashScopeClientWebSocketFactory(),
new DashScopeOptions
{
ApiKey = apiKey,
WebsocketBaseAddress = baseAddress,
SocketPoolSize = socketPoolSize,
WorkspaceId = workspaceId
});
SocketPools.Add(key, pool);
}

return pool;

string GetCacheKey() => $"{apiKey}-{socketPoolSize}-{baseAddress}-{workspaceId}";
}

private static HttpClient GetConfiguredClient(
Expand All @@ -41,7 +76,7 @@ private static HttpClient GetConfiguredClient(
{
client = new HttpClient
{
BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.DashScopeApiBaseAddress),
BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.HttpApiBaseAddress),
Timeout = timeout ?? TimeSpan.FromMinutes(2)
};

Expand Down
Loading