diff --git a/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj b/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj index 27bbe56..d8a3b42 100644 --- a/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj +++ b/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj @@ -24,6 +24,9 @@ + + + diff --git a/CloudBuilder.AI.Service/Utility/SaveAudioToFileHelper.cs b/CloudBuilder.AI.Service/Utility/SaveAudioToFileHelper.cs new file mode 100644 index 0000000..4b3e2fe --- /dev/null +++ b/CloudBuilder.AI.Service/Utility/SaveAudioToFileHelper.cs @@ -0,0 +1,225 @@ +using CloudBuilder.AI.Policy; +using CloudBuilder.Core.DatabaseAccessor.Entity; +using DocumentFormat.OpenXml.Spreadsheet; +using EdgeTtsSharp; +using Google.Protobuf.WellKnownTypes; +using SherpaOnnx; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace CloudBuilder.AI.Service.Utility +{ + public class SaveAudioToFileHelper + { + private IApplicationService service; + private OfflineTts offlineTts1; + private OfflineTts offlineTts2; + private OfflineTts offlineTts3; + private OfflineTts offlineTts4; + + public SaveAudioToFileHelper(IApplicationService service) + { + this.service = service; + + InitOfflineTtsConfig(DeclaimerPolicy.VITS_ZH_HF_THERESA); + + InitOfflineTtsConfig(DeclaimerPolicy.VITS_ZH_HF_FANCHEN_C); + + InitOfflineTtsConfig(DeclaimerPolicy.SHERPA_ONNX_VITS_ZH_LL); + + InitOfflineTtsConfig(DeclaimerPolicy.VITS_MELO_TTS_ZH_EN); + } + + private void InitOfflineTtsConfig(string ttsId) + { + var config = new OfflineTtsConfig(); + config.Model.Provider = "cpu"; + config.Model.NumThreads = 2; + config.MaxNumSentences = 1; + + switch (ttsId) + { + case DeclaimerPolicy.VITS_ZH_HF_THERESA: + config.Model.Vits.Model = "tts-models/vits-zh-hf-theresa/theresa.onnx"; + config.Model.Vits.Tokens = "tts-models/vits-zh-hf-theresa/tokens.txt"; + config.Model.Vits.Lexicon = "tts-models/vits-zh-hf-theresa/lexicon.txt"; + offlineTts1 = new OfflineTts(config); + break; + case DeclaimerPolicy.VITS_ZH_HF_FANCHEN_C: + config.Model.Vits.Model = "tts-models/vits-zh-hf-fanchen-C/vits-zh-hf-fanchen-C.onnx"; + config.Model.Vits.Tokens = "tts-models/vits-zh-hf-fanchen-C/tokens.txt"; + config.Model.Vits.Lexicon = "tts-models/vits-zh-hf-fanchen-C/lexicon.txt"; + config.RuleFars = "tts-models/vits-zh-hf-fanchen-C/phone.fst,tts-models/vits-zh-hf-fanchen-C/number.fst"; + offlineTts2 = new OfflineTts(config); + break; + case DeclaimerPolicy.SHERPA_ONNX_VITS_ZH_LL: + config.Model.Vits.Model = "tts-models/sherpa-onnx-vits-zh-ll/model.onnx"; + config.Model.Vits.Tokens = "tts-models/sherpa-onnx-vits-zh-ll/tokens.txt"; + config.Model.Vits.Lexicon = "tts-models/sherpa-onnx-vits-zh-ll/lexicon.txt"; + config.RuleFars = "tts-models/sherpa-onnx-vits-zh-ll/phone.fst,tts-models/sherpa-onnx-vits-zh-ll/number.fst"; + offlineTts3 = new OfflineTts(config); + break; + default://DeclaimerPolicy.VITS_MELO_TTS_ZH_EN + config.Model.Vits.Model = "tts-models/vits-melo-tts-zh_en/model.onnx"; + config.Model.Vits.Tokens = "tts-models/vits-melo-tts-zh_en/tokens.txt"; + config.Model.Vits.Lexicon = "tts-models/vits-melo-tts-zh_en/lexicon.txt"; + offlineTts4 = new OfflineTts(config); + break; + } + + // --- 2. 初始化引擎 --- + offlineTts1 = new OfflineTts(config); + } + + public void AudioToFile(string text, string saveFilePath) + { + AudioToFile(DeclaimerPolicy.VITS_MELO_TTS_ZH_EN, 0, text, saveFilePath); + } + + public void AudioToFile(string ttsId, int sid, string text, string saveFilePath) + { + float speed = 1.0f; + OfflineTtsGeneratedAudio audio; + switch (ttsId) + { + case DeclaimerPolicy.VITS_ZH_HF_THERESA: + audio = offlineTts1.Generate(text, speed, sid); + break; + case DeclaimerPolicy.VITS_ZH_HF_FANCHEN_C: + audio = offlineTts2.Generate(text, speed, sid); + break; + case DeclaimerPolicy.SHERPA_ONNX_VITS_ZH_LL: + audio = offlineTts3.Generate(text, speed, sid); + break; + default://DeclaimerPolicy.VITS_MELO_TTS_ZH_EN + audio = offlineTts4.Generate(text, speed, sid); + break; + } + audio.SaveToWaveFile(saveFilePath); + } + + public static void AudioToFile(ChineseTtsVoice actorName, string text, string saveFilePath) + { + // 步骤1:同步获取异步语音对象 + var voiceTask = EdgeTts.GetVoice(actorName.GetVoiceId()); + var voice = voiceTask.GetAwaiter().GetResult(); // 替代await,同步阻塞 + + // 步骤2:同步保存音频文件 + var saveTask = voice.SaveAudioToFile(text, saveFilePath); + saveTask.GetAwaiter().GetResult(); // 同步等待保存完成 + } + } + /// + /// 微软Edge TTS中文语音音色枚举 + /// 对应官方Neural语音标识,覆盖港澳台及大陆各地区音色 + /// + public enum ChineseTtsVoice + { + /// + /// 粤语(香港)- 喜佳(女性) + /// + [System.ComponentModel.Description("zh-HK-HiuGaaiNeural")] + ZhHkHiuGaaiNeural, + + /// + /// 粤语(香港)- 喜文(女性) + /// + [System.ComponentModel.Description("zh-HK-HiuMaanNeural")] + ZhHkHiuMaanNeural, + + /// + /// 粤语(香港)- 云龙(男性) + /// + [System.ComponentModel.Description("zh-HK-WanLungNeural")] + ZhHkWanLungNeural, + + /// + /// 普通话(中国大陆)- 晓晓(女性) + /// + [System.ComponentModel.Description("zh-CN-XiaoxiaoNeural")] + ZhCnXiaoxiaoNeural, + + /// + /// 普通话(中国大陆)- 小艺(女性) + /// + [System.ComponentModel.Description("zh-CN-XiaoyiNeural")] + ZhCnXiaoyiNeural, + + /// + /// 普通话(中国大陆)- 云健(男性) + /// + [System.ComponentModel.Description("zh-CN-YunjianNeural")] + ZhCnYunjianNeural, + + /// + /// 普通话(中国大陆)- 云希(男性) + /// + [System.ComponentModel.Description("zh-CN-YunxiNeural")] + ZhCnYunxiNeural, + + /// + /// 普通话(中国大陆)- 云夏(女性) + /// + [System.ComponentModel.Description("zh-CN-YunxiaNeural")] + ZhCnYunxiaNeural, + + /// + /// 普通话(中国大陆)- 云阳(男性) + /// + [System.ComponentModel.Description("zh-CN-YunyangNeural")] + ZhCnYunyangNeural, + + /// + /// 普通话(中国大陆-辽宁)- 小北(女性) + /// + [System.ComponentModel.Description("zh-CN-liaoning-XiaobeiNeural")] + ZhCnLiaoningXiaobeiNeural, + + /// + /// 普通话(台湾)- 小珍(女性) + /// + [System.ComponentModel.Description("zh-TW-HsiaoChenNeural")] + ZhTwHsiaoChenNeural, + + /// + /// 普通话(台湾)- 云哲(男性) + /// + [System.ComponentModel.Description("zh-TW-YunJheNeural")] + ZhTwYunJheNeural, + + /// + /// 普通话(台湾)- 小瑜(女性) + /// + [System.ComponentModel.Description("zh-TW-HsiaoYuNeural")] + ZhTwHsiaoYuNeural, + + /// + /// 普通话(中国大陆-陕西)- 小妮(女性) + /// + [System.ComponentModel.Description("zh-CN-shaanxi-XiaoniNeural")] + ZhCnShaanxiXiaoniNeural + } + + public static class ChineseTtsVoiceExtensions + { + /// + /// 获取枚举值对应的Description特性值 + /// + /// 语音枚举值 + /// 描述字符串(无特性时返回枚举名称) + public static string GetVoiceId(this ChineseTtsVoice voice) + { + // 获取枚举成员的特性 + var field = voice.GetType().GetField(voice.ToString()); + var attribute = field?.GetCustomAttribute(); + + // 有特性返回描述,无则返回枚举名 + return attribute?.Description ?? voice.ToString(); + } + } +} diff --git a/CloudBuilder.AI/Policy/DeclaimerPolicy.cs b/CloudBuilder.AI/Policy/DeclaimerPolicy.cs new file mode 100644 index 0000000..5dc1949 --- /dev/null +++ b/CloudBuilder.AI/Policy/DeclaimerPolicy.cs @@ -0,0 +1,44 @@ +using CloudBuilder.Core.DependencyInjection.Policy; +using CloudBuilder.Core.DependencyInjection.Request; +using CloudBuilder.Core.Policy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudBuilder.AI.Policy +{ + public class DeclaimerPolicy : LocalePolicy, IPolicy + { + public const string ZhHkHiuGaaiNeural = "ZhHkHiuGaaiNeural"; + public const string ZhHkHiuMaanNeural = "ZhHkHiuMaanNeural"; + public const string ZhHkWanLungNeural = "ZhHkWanLungNeural"; + public const string ZhCnXiaoxiaoNeural = "ZhCnXiaoxiaoNeural"; + public const string ZhCnXiaoyiNeural = "ZhCnXiaoyiNeural"; + public const string ZhCnYunjianNeural = "ZhCnYunjianNeural"; + public const string ZhCnYunxiNeural = "ZhCnYunxiNeural"; + public const string ZhCnYunxiaNeural = "ZhCnYunxiaNeural"; + public const string ZhCnYunyangNeural = "ZhCnYunyangNeural"; + public const string ZhCnLiaoningXiaobeiNeural = "ZhCnLiaoningXiaobeiNeural"; + public const string ZhTwHsiaoChenNeural = "ZhTwHsiaoChenNeural"; + public const string ZhTwYunJheNeural = "ZhTwYunJheNeural"; + public const string ZhTwHsiaoYuNeural = "ZhTwHsiaoYuNeural"; + public const string ZhCnShaanxiXiaoniNeural = "ZhCnShaanxiXiaoniNeural"; + public const string VITS_MELO_TTS_ZH_EN = "vits-melo-tts-zh_en"; + public const string VITS_ZH_HF_THERESA = "vits-zh-hf-theresa"; + public const string VITS_ZH_HF_FANCHEN_C = "vits-zh-hf-fanchen-C"; + public const string SHERPA_ONNX_VITS_ZH_LL = "sherpa-onnx-vits-zh-ll"; + + private readonly IContext context; + private readonly ILocaleMessageManager localeMessageManager; + + public DeclaimerPolicy(IContext context, ILocaleMessageManager localeMessageManager) : base(context, localeMessageManager) + { + this.context = context; + this.localeMessageManager = localeMessageManager; + + this.Name = typeof(DeclaimerPolicy).Name; + } + } +} diff --git a/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj b/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj index 3682b51..8a85c29 100644 --- a/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj +++ b/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj @@ -28,6 +28,9 @@ + + +