diff --git a/CloudBuilder.AI.Gui/AiBookOperationPage.Designer.cs b/CloudBuilder.AI.Gui/AiBookOperationPage.Designer.cs index 751c83e..7123863 100644 --- a/CloudBuilder.AI.Gui/AiBookOperationPage.Designer.cs +++ b/CloudBuilder.AI.Gui/AiBookOperationPage.Designer.cs @@ -307,6 +307,7 @@ namespace CloudBuilder.AI.Gui tableLayoutPanel6.SetRowSpan(sentenceGv, 2); sentenceGv.Size = new Size(687, 349); sentenceGv.TabIndex = 4; + sentenceGv.CellDoubleClick += sentenceGv_CellDoubleClick; // // sentenceIndexCol // diff --git a/CloudBuilder.AI.Gui/AiBookOperationPage.cs b/CloudBuilder.AI.Gui/AiBookOperationPage.cs index ed5c4c6..f5778bf 100644 --- a/CloudBuilder.AI.Gui/AiBookOperationPage.cs +++ b/CloudBuilder.AI.Gui/AiBookOperationPage.cs @@ -4,6 +4,7 @@ using CloudBuilder.AI.Gui.Request; using CloudBuilder.AI.Linkup; using CloudBuilder.AI.Policy; using CloudBuilder.AI.SearchCriteria; +using CloudBuilder.Core.Request; using CloudBuilder.Core.Service; using CloudBuilder.Gui; using CloudBuilder.Request.AI; @@ -15,10 +16,13 @@ using CloudBuilder.Security.Policy; using DocumentFormat.OpenXml.Wordprocessing; using Krypton.Toolkit; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NAudio.Wave; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Security.Permissions; @@ -106,7 +110,7 @@ namespace CloudBuilder.AI.Gui private void InitGridViewPagination() { searchCriteria = new AiChapterSearchCriteria(); - gridViewPagination.SetLimit(searchCriteria.limit); + gridViewPagination.SetLimit(searchCriteria.limit); gridViewPagination.OnPaginationChanged += SearchChapter; } @@ -260,7 +264,7 @@ namespace CloudBuilder.AI.Gui guiOperateHelper.EnableControls(false, contentPanel); //查詢有記錄時,修改等控件有效 if (aiBookEntity != null) - guiOperateHelper.EnableControls(true, bottomPanel, processPanel,gridViewPagination); + guiOperateHelper.EnableControls(true, bottomPanel, processPanel, gridViewPagination); else guiOperateHelper.EnableControls(false, bottomPanel, processPanel, gridViewPagination); } @@ -269,7 +273,7 @@ namespace CloudBuilder.AI.Gui savePanel.Visible = true; operationPanel.Visible = false; - + guiOperateHelper.EnableControls(true, bottomPanel, contentPanel); guiOperateHelper.EnableControls(false, topPanel, processPanel, gridViewPagination); @@ -802,5 +806,114 @@ namespace CloudBuilder.AI.Gui ShowMessageBox.ShowError(ex.Message); } } + + private void sentenceGv_CellDoubleClick(object sender, DataGridViewCellEventArgs e) + { + try + { + if (sentenceGv.RowCount <= 0 || sentenceGv.SelectedCells.Count <= 0) + { + ShowMessageBox.ShowInfo(securityMessagePolicy.ComposeMessage(SecurityMessagePolicy.GUI_WS_RECORD_NOT_FOUND)); + return; + } + + AiSentenceViewEntity sentence = sentenceGv.GetRowData(sentenceGv.SelectedCells[0].RowIndex); + + using (Stream audioStream = ServiceHelper.GetService()!.PostStreamAsync("/AiBook/GetTtsStream?text=" + sentence.Content, null, string.Empty).GetAwaiter().GetResult()) + { + PlayMp3Stream(audioStream); + } + } + catch (ValidatedException ex) + { + ShowMessageBox.ShowInfo(ex.Message); + } + catch (ServiceErrorException ex) + { + ShowMessageBox.ShowError(ex.Message); + } + catch (Exception ex) + { + ShowMessageBox.ShowError(ex.Message); + } + } + + public void PlayMp3Stream(Stream audioStream) + { + string tempFile = Path.Combine(Path.GetTempPath(), "temp_audio.mp3"); + + // 保存流到临时文件 + using (var fs = new FileStream(tempFile, FileMode.Create)) + { + audioStream.Position = 0; + audioStream.CopyTo(fs); + } + + // 直接调用你已经能播放的方法!!! + //PlayAudioFile(tempFile); + Task.Run(() => + { + try + { + PlayAudioFile(tempFile); // 后台播放 + } + finally + { + File.Delete(tempFile); // 播完删除 + } + }); + } + + public static void PlayAudioFile(string filePath) + { + // 必用这种写法,绝对不会提前释放 + var reader = new AudioFileReader(filePath); + + var waveOut = new WaveOutEvent(); + + waveOut.Init(reader); + waveOut.Play(); + + // 同步等待播放完成(这是最稳、最不会出错的) + while (waveOut.PlaybackState == NAudio.Wave.PlaybackState.Playing) + { + System.Threading.Thread.Sleep(100); + } + + // 播放完再释放 + waveOut.Dispose(); + reader.Dispose(); + } + + + + public void DownloadFile(int fileId, string saveFilePath) + { + try + { + ServiceHelper.GetService()!.DownloadFile("/DmsFile/DownloadFile?fileId=" + fileId, saveFilePath, null, OpenFile); + } + catch (ValidatedException ex) + { + ShowMessageBox.ShowInfo(ex.Message); + } + catch (ServiceErrorException ex) + { + ShowMessageBox.ShowError(ex.Message); + } + catch (Exception ex) + { + ShowMessageBox.ShowError(ex.Message); + } + } + + private void OpenFile(string fileFullName) + { + Process.Start(new ProcessStartInfo + { + FileName = fileFullName, + UseShellExecute = true // 使用Windows Shell执行 + }); + } } } diff --git a/CloudBuilder.AI.Gui/CloudBuilder.AI.Gui.csproj b/CloudBuilder.AI.Gui/CloudBuilder.AI.Gui.csproj index c95c6b2..150df8b 100644 --- a/CloudBuilder.AI.Gui/CloudBuilder.AI.Gui.csproj +++ b/CloudBuilder.AI.Gui/CloudBuilder.AI.Gui.csproj @@ -48,6 +48,8 @@ + + diff --git a/CloudBuilder.AI.Service/AiBookService.auto.cs b/CloudBuilder.AI.Service/AiBookService.auto.cs index d235334..f8e0e4e 100644 --- a/CloudBuilder.AI.Service/AiBookService.auto.cs +++ b/CloudBuilder.AI.Service/AiBookService.auto.cs @@ -1,4 +1,5 @@ -using CloudBuilder.AI.Data; +using CliWrap; +using CloudBuilder.AI.Data; using CloudBuilder.AI.Entity; using CloudBuilder.AI.Policy; using CloudBuilder.AI.SearchCriteria; @@ -14,8 +15,10 @@ using CloudBuilder.Security.Utility; using DocumentFormat.OpenXml.ExtendedProperties; using DocumentFormat.OpenXml.Presentation; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics.Contracts; +using System.IO; using System.Text; using System.Transactions; namespace CloudBuilder.AI.Service @@ -291,7 +294,7 @@ namespace CloudBuilder.AI.Service foreach (var name in names) { entity = new AiChapterPersonEntity(); - entity.Guid= Guid.NewGuid().ToString(); + entity.Guid = Guid.NewGuid().ToString(); entity.ChapterGuid = paragraph.ChapterGuid; entity.ParagraphGuid = paragraph.Guid; entity.ParagraphIndex = paragraph.ParagraphIndex; @@ -304,5 +307,76 @@ namespace CloudBuilder.AI.Service if (aiChapterPersonEntities != null && aiChapterPersonEntities.Count() > 0) repositoryAiChapterPersonEntity.InsertNow(aiChapterPersonEntities); } + + [PermissionOperate(name: AIPermissionPolicy.AiBook, operate: Operate.Find)] + public IActionResult GetTtsStream(string text) + { + string fileDownloadName = Path.Combine(service.Configuration["FileServiceSettings:BackupDmsFileDirectory"], @"voice", DateTime.Now.ToString("yyyyMMddHHmmss") + ".mp3"); + + string param = PackParameters(ChineseTtsVoice.Sherpa2.ToString(), fileDownloadName, text); + RunMyProcess(service.Configuration["CloudBuilder.Topshelf:Path"], param); + + var stream = System.IO.File.OpenRead(fileDownloadName); + + string contentType = "application/octet-stream"; + + return new FileStreamResult(stream, contentType) { FileDownloadName = fileDownloadName }; + } + private string PackParameters(string voice, string save_path, string text) + { + List para = new List(); + para.Add("task:" + "TtsTask"); + para.Add("voice:" + voice); + para.Add("save_path:" + save_path); + para.Add("text:" + text.Replace(":", ":")); + return para.ToArray().ToDelimiteredList(' '); + } + + + /// + /// 执行cmd指令(同步等待,await要求等待返回,转码太耗时) + /// + /// + public void RunMyProcess(string exePath, string Parameters) + { + try + { + if (string.IsNullOrEmpty(exePath)) return; + var result = Cli.Wrap(exePath) + .WithWorkingDirectory(Path.GetDirectoryName(exePath)) + .WithArguments(Parameters) + .ExecuteAsync().GetAwaiter().GetResult(); + + Thread.Sleep(300); + } + catch (Exception ex) + { + throw new ValidatedException(ex.Message); + } + + } + + /// + /// 执行cmd指令(异步不等待,await要求等待返回,转码太耗时) + /// + /// + public void RunMyProcessAsync(string exePath, string Parameters) + { + try + { + if (string.IsNullOrEmpty(exePath)) return; + Cli.Wrap(exePath) + .WithWorkingDirectory(Path.GetDirectoryName(exePath)) + .WithArguments(Parameters) + .ExecuteAsync(); + + Thread.Sleep(300); + } + catch (Exception ex) + { + throw new ValidatedException(ex.Message); + } + + } } } diff --git a/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj b/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj index d8a3b42..25e70b1 100644 --- a/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj +++ b/CloudBuilder.AI.Service/CloudBuilder.AI.Service.csproj @@ -7,6 +7,7 @@ + @@ -24,9 +25,7 @@ - - - + diff --git a/CloudBuilder.AI.Service/Utility/SaveAudioToFileHelper.cs b/CloudBuilder.AI.Service/Utility/SaveAudioToFileHelper.cs deleted file mode 100644 index 4b3e2fe..0000000 --- a/CloudBuilder.AI.Service/Utility/SaveAudioToFileHelper.cs +++ /dev/null @@ -1,225 +0,0 @@ -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/ChineseTtsVoicePolicy.cs b/CloudBuilder.AI/Policy/ChineseTtsVoicePolicy.cs new file mode 100644 index 0000000..d6321f4 --- /dev/null +++ b/CloudBuilder.AI/Policy/ChineseTtsVoicePolicy.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudBuilder.AI.Policy +{ + public enum ChineseTtsVoice + { + /// + /// 普通话(中国大陆) 娇柔(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-0")] + Sherpa0, + /// + /// 普通话(中国大陆)- 柔美(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 柔美(女性)-2")] + Sherpa2, + /// + /// 普通话(中国大陆)- 知性(男性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 知性(男性)-4")] + Sherpa4, + /// + /// 普通话(中国大陆)- 知青(男性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 知青(男性)-3")] + Sherpa3, + /// + /// 普通话(中国大陆)- 中青(男性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 中青(男性)-1")] + Sherpa1, + + + /// + /// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快 + /// + [System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-6")] + Aaishe6, + + /// + /// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快 + /// + [System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-14")] + Aaishe14, + + /// + /// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快 + /// + [System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-18")] + Aaishe18, + + /// + /// 普通话(中国大陆)- 云健(男性)- 文件小速度快 + /// + [System.ComponentModel.Description("普通话(中国大陆) 云健(男性)-10")] + Aaishe10, + + /// + /// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快 + /// + [System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-1")] + Aaishe1, + + /// + /// 普通话(中国大陆)- 云健(男性)- 文件小速度快 + /// + [System.ComponentModel.Description("普通话(中国大陆) 云健(男性)-15")] + Aaishe15, + + /// 普通话(中国大陆) 云阳(男性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 云阳(男性)-10")] + Fanchen10, + /// + /// 普通话(中国大陆) 云阳(男性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 云阳(男性)-110")] + Fanchen110, + /// 普通话(中国大陆) 喜佳(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 云夏(女性)-6")] + Fanchen6, + /// + /// 普通话(中国大陆) 云阳(男性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 云阳(男性)-15")] + Fanchen15, + /// + /// 普通话(中国大陆) 喜佳(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-2")] + Fanchen2, + /// + /// 普通话(中国大陆) 喜佳(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-14")] + Fanchen14, + /// + /// 普通话(中国大陆) 喜佳(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-20")] + Fanchen20, + /// + /// 普通话(中国大陆) 喜佳(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-36")] + Fanchen36, + + /// + /// 粤语(香港)- 女性(男性)- 文件大 + /// + [System.ComponentModel.Description("粤语(香港) 梅隆 (女性)-0")] + Melo, + + /// + /// 普通话(中国大陆)- 神精女(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 神精女(女性)-0")] + Eula, + + /// + /// 普通话(中国大陆)- 小神精女(女性) + /// + [System.ComponentModel.Description("普通话(中国大陆) 小神精女(女性)-27")] + Theresa, + } +} diff --git a/CloudBuilder.AI/Service/IAiBookService.auto.cs b/CloudBuilder.AI/Service/IAiBookService.auto.cs index 37191a3..be6279b 100644 --- a/CloudBuilder.AI/Service/IAiBookService.auto.cs +++ b/CloudBuilder.AI/Service/IAiBookService.auto.cs @@ -5,6 +5,7 @@ using CloudBuilder.AI.SearchCriteria; using CloudBuilder.Core.Service; using CloudBuilder.Security.Data; using CloudBuilder.Security.SearchCriteria; +using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.AI.Service { @@ -23,5 +24,7 @@ namespace CloudBuilder.AI.Service void ExtractNames(string bookGuid); void ExtractNamesByChapterGuid(string bookGuid, string chapterGuid); + + IActionResult GetTtsStream(string text); } } diff --git a/CloudBuilder.Core/Request/IServiceRequest.cs b/CloudBuilder.Core/Request/IServiceRequest.cs index 9a1cb8e..75ffa61 100644 --- a/CloudBuilder.Core/Request/IServiceRequest.cs +++ b/CloudBuilder.Core/Request/IServiceRequest.cs @@ -39,6 +39,8 @@ namespace CloudBuilder.Core.Request Task PostAsync(string api, MultipartFormDataContent data); + Task PostStreamAsync(string api, Dictionary headers, T data); + void AddHeaders(string name, string? value); void SetContext(IContext loginContext); HttpClient GetHttpClient(); diff --git a/CloudBuilder.Core/Request/ServiceRequest.cs b/CloudBuilder.Core/Request/ServiceRequest.cs index 1672fbb..9b6ec9b 100644 --- a/CloudBuilder.Core/Request/ServiceRequest.cs +++ b/CloudBuilder.Core/Request/ServiceRequest.cs @@ -361,6 +361,56 @@ namespace CloudBuilder.Core.Request if (completedCallback != null) completedCallback(filePath); } + public async Task PostStreamAsync(string api, Dictionary headers, T data) + { + if (headers != null && headers.Count > 0) + { + foreach (var header in headers) + { + if (request.DefaultRequestHeaders.Contains(header.Key)) request.DefaultRequestHeaders.Remove(header.Key); + request.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + + this.request.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginContext.GetToken()); + + if (request.DefaultRequestHeaders.Contains(ClaimPolicy.HEADER_LOCALE)) + request.DefaultRequestHeaders.Remove(ClaimPolicy.HEADER_LOCALE); + request.DefaultRequestHeaders.Add(ClaimPolicy.HEADER_LOCALE, loginContext.GetCulture()); + + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + try + { + var response = await this.request.PostAsJsonAsync(api, data, options).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + //{"errors":{},"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"00-64ad066978f4471cb3c9ce219c213da9-bccca8afd14676b3-00"} + var responseContext = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string errors = string.Empty; + if (responseContext != null && responseContext.StartsWith("{\"errors\"")) errors = responseContext.Substring(11, responseContext.IndexOf("}") - 11); + + if (response.ReasonPhrase == "Forbidden") + throw new Exception(string.Format("{0}", CoreMessagePolicy.GetInstance().ComposeMessage(CoreMessagePolicy.NO_PERMISSION))); + + throw new Exception(string.Format("{0}{1}", response.ReasonPhrase, string.IsNullOrEmpty(errors) ? "" : "\r\n" + errors)); + } + + response.EnsureSuccessStatusCode(); + + + return await response.Content.ReadAsStreamAsync(); + } + catch (Exception ex ) + { + + throw ex; + } + } public async Task PostAsync(string api, MultipartFormDataContent data) { diff --git a/CloudBuilder.Request/AI/AiBookRequest.auto.cs b/CloudBuilder.Request/AI/AiBookRequest.auto.cs index 00c54b0..d1bddf5 100644 --- a/CloudBuilder.Request/AI/AiBookRequest.auto.cs +++ b/CloudBuilder.Request/AI/AiBookRequest.auto.cs @@ -9,6 +9,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Request.AI { @@ -75,6 +76,10 @@ var compositionData = new ProcessBookCompositionData3{ key = key, encoding = e { var compositionData = new ExtractNamesByChapterGuidCompositionData2{ bookGuid = bookGuid, chapterGuid = chapterGuid }; Task.Run(() => serviceRequest.PostAsJsonAsync("/AiBook/ExtractNamesByChapterGuid", compositionData)).GetAwaiter().GetResult(); } + public IActionResult GetTtsStream(string text) + { +return Task.Run(() => serviceRequest.PostAsJsonAsync("/AiBook/GetTtsStream?text=" + text.ToString())).GetAwaiter().GetResult(); } + } } \ No newline at end of file diff --git a/CloudBuilder.Request/AI/AiChapterPersonRequest.auto.cs b/CloudBuilder.Request/AI/AiChapterPersonRequest.auto.cs index 64c9172..24931cf 100644 --- a/CloudBuilder.Request/AI/AiChapterPersonRequest.auto.cs +++ b/CloudBuilder.Request/AI/AiChapterPersonRequest.auto.cs @@ -9,6 +9,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Request.AI { diff --git a/CloudBuilder.Request/AI/AiChapterRequest.auto.cs b/CloudBuilder.Request/AI/AiChapterRequest.auto.cs index c6ea1a3..b3ec5d6 100644 --- a/CloudBuilder.Request/AI/AiChapterRequest.auto.cs +++ b/CloudBuilder.Request/AI/AiChapterRequest.auto.cs @@ -9,6 +9,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Request.AI { diff --git a/CloudBuilder.Request/AI/AiParagraphRequest.auto.cs b/CloudBuilder.Request/AI/AiParagraphRequest.auto.cs index f369450..c7746ba 100644 --- a/CloudBuilder.Request/AI/AiParagraphRequest.auto.cs +++ b/CloudBuilder.Request/AI/AiParagraphRequest.auto.cs @@ -9,6 +9,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Request.AI { diff --git a/CloudBuilder.Request/AI/AiSentenceRequest.auto.cs b/CloudBuilder.Request/AI/AiSentenceRequest.auto.cs index 79a9523..0674bbd 100644 --- a/CloudBuilder.Request/AI/AiSentenceRequest.auto.cs +++ b/CloudBuilder.Request/AI/AiSentenceRequest.auto.cs @@ -9,6 +9,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Request.AI { diff --git a/CloudBuilder.Request/AI/DmsFileRequest.auto.cs b/CloudBuilder.Request/AI/DmsFileRequest.auto.cs index 82f1e7c..802ae89 100644 --- a/CloudBuilder.Request/AI/DmsFileRequest.auto.cs +++ b/CloudBuilder.Request/AI/DmsFileRequest.auto.cs @@ -9,6 +9,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; namespace CloudBuilder.Request.AI diff --git a/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj b/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj index 8a85c29..9962079 100644 --- a/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj +++ b/CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj @@ -28,9 +28,7 @@ - - - + diff --git a/CloudBuilder.Web.Entry/Controllers/Ai/AiBookController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Ai/AiBookController.auto.cs index c78b99f..14fd3ba 100644 --- a/CloudBuilder.Web.Entry/Controllers/Ai/AiBookController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Ai/AiBookController.auto.cs @@ -11,6 +11,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Web.Entry.Controllers.Ai { @@ -246,6 +247,26 @@ this.aiBookService= aiBookService; } } + [PermissionOperate(name: "AiBook", operate: "Find")] + [HttpPost] + public dynamic GetTtsStream(string text) + { + try + { + + var result = aiBookService.GetTtsStream(text); + return result; + } + catch (ValidatedException vex) + { + return vex; + } + catch (Exception ex) + { + return new ServiceErrorException(ex); + } + } + } } \ No newline at end of file diff --git a/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterController.auto.cs index ca64d7d..f56c9e8 100644 --- a/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterController.auto.cs @@ -11,6 +11,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Web.Entry.Controllers.Ai { diff --git a/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterPersonController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterPersonController.auto.cs index fa1f071..5bd3f5a 100644 --- a/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterPersonController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Ai/AiChapterPersonController.auto.cs @@ -11,6 +11,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Web.Entry.Controllers.Ai { diff --git a/CloudBuilder.Web.Entry/Controllers/Ai/AiParagraphController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Ai/AiParagraphController.auto.cs index 3f3504b..f795b36 100644 --- a/CloudBuilder.Web.Entry/Controllers/Ai/AiParagraphController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Ai/AiParagraphController.auto.cs @@ -11,6 +11,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Web.Entry.Controllers.Ai { diff --git a/CloudBuilder.Web.Entry/Controllers/Ai/AiSentenceController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Ai/AiSentenceController.auto.cs index c04e6ea..c22ba15 100644 --- a/CloudBuilder.Web.Entry/Controllers/Ai/AiSentenceController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Ai/AiSentenceController.auto.cs @@ -11,6 +11,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; namespace CloudBuilder.Web.Entry.Controllers.Ai { diff --git a/CloudBuilder.Web.Entry/Controllers/Ai/DmsFileController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Ai/DmsFileController.auto.cs index c90e7c0..aa992f5 100644 --- a/CloudBuilder.Web.Entry/Controllers/Ai/DmsFileController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Ai/DmsFileController.auto.cs @@ -11,6 +11,7 @@ using CloudBuilder.AI.Service; using System; using CloudBuilder.AI.Data; using CloudBuilder.AI.SearchCriteria; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; namespace CloudBuilder.Web.Entry.Controllers.Ai diff --git a/CloudBuilder.Web.Entry/Controllers/Security/AuthController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/AuthController.auto.cs index 268a7b6..6abcf1e 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/AuthController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/AuthController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/LocaleController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/LocaleController.auto.cs index 68107de..04ec67a 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/LocaleController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/LocaleController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityActorController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityActorController.auto.cs index d44b993..46e2962 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityActorController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityActorController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityFunctionController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityFunctionController.auto.cs index a122043..b1ee59a 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityFunctionController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityFunctionController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityGeneratorNumberController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityGeneratorNumberController.auto.cs index b25d391..a6e75d0 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityGeneratorNumberController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityGeneratorNumberController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityGroupController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityGroupController.auto.cs index 264f540..9beae52 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityGroupController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityGroupController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityLockController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityLockController.auto.cs index 61ceb5c..dfc27f3 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityLockController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityLockController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityMasterController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityMasterController.auto.cs index c04c823..caa28e1 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityMasterController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityMasterController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityPermissionController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityPermissionController.auto.cs index 63bca5f..18032c7 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityPermissionController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityPermissionController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserController.auto.cs index f329c0f..0ea68ba 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserPreferenceController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserPreferenceController.auto.cs index 8c9f2b5..c9fa37b 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserPreferenceController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/SecurityUserPreferenceController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/Controllers/Security/YesNoPolicyController.auto.cs b/CloudBuilder.Web.Entry/Controllers/Security/YesNoPolicyController.auto.cs index 53c5308..3a93cd3 100644 --- a/CloudBuilder.Web.Entry/Controllers/Security/YesNoPolicyController.auto.cs +++ b/CloudBuilder.Web.Entry/Controllers/Security/YesNoPolicyController.auto.cs @@ -1,3 +1,4 @@ + using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.Policy; using Microsoft.AspNetCore.Authorization; diff --git a/CloudBuilder.Web.Entry/appsettings.json b/CloudBuilder.Web.Entry/appsettings.json index 6907d20..ff9bced 100644 --- a/CloudBuilder.Web.Entry/appsettings.json +++ b/CloudBuilder.Web.Entry/appsettings.json @@ -62,7 +62,7 @@ "Arguments": " -i {0} -c:v libx264 -vf scale=1280:-1 -start_number 0 -hls_time {1} -hls_list_size 0 -f hls {2}" //0-源路径,1-目标路径,2-存放多少秒 1280×720,852*480 }, "CloudBuilder.Topshelf": { - "Path": "D:\\NET8\\CloudBuilder.Topshelf\\bin\\Debug\\net8.0\\CloudBuilder.Topshelf.exe" //图片压缩 + "Path": "D:\\MAFAI\\CloudBuilder.Topshelf\\bin\\Debug\\net8.0\\CloudBuilder.Topshelf.exe" //图片压缩 }, "SecuritySettings": { "RsaPublicKey": "-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8HMr2CBpoZPm3t9tCVlrKtTmI4jNJc7/HhxjIEiDjC8czP4PV+44LjXvLYcSV0fwi6nE4LH2c5PBPEnPfqp0g8TZeX+bYGvd70cXee9d8wHgBqi4k0J0X33c0ZnW7JruftPyvJo9OelYSofBXQTcwI+3uIl/YvrgQRv6A5mW01QIDAQAB-----END PUBLIC KEY-----", diff --git a/CloudBuilderLauncher.sln b/CloudBuilderLauncher.sln index 5b7099d..29cea74 100644 --- a/CloudBuilderLauncher.sln +++ b/CloudBuilderLauncher.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.AI.Service", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.Security", "CloudBuilder.Security\CloudBuilder.Security.csproj", "{269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudBuilder.Core", "CloudBuilder.Core\CloudBuilder.Core.csproj", "{AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {269A5036-BAE9-FD61-1DD5-0EA93B79F0D7}.Release|Any CPU.Build.0 = Release|Any CPU + {AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFA9D41C-E8A5-32D3-E81B-2ABA7C37D46C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CloudBuilderLauncher/CloudBuilderLauncher.csproj b/CloudBuilderLauncher/CloudBuilderLauncher.csproj index e6ee6d8..bfc34b6 100644 --- a/CloudBuilderLauncher/CloudBuilderLauncher.csproj +++ b/CloudBuilderLauncher/CloudBuilderLauncher.csproj @@ -61,6 +61,8 @@ + + \ No newline at end of file