diff --git a/Lib/CloudBuilder.AI.dll b/Lib/CloudBuilder.AI.dll index 1cefadc..c9f7991 100644 Binary files a/Lib/CloudBuilder.AI.dll and b/Lib/CloudBuilder.AI.dll differ diff --git a/Program.cs b/Program.cs index 1b1bfd5..49b2dc9 100644 --- a/Program.cs +++ b/Program.cs @@ -33,6 +33,7 @@ public partial class Program string taskName = null; Dictionary bodyDict = new Dictionary(); + File.AppendAllText("log.txt", "Main:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); if (args != null && args.Length > 0) { diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index ea2989a..44ac99d 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CloudBuilder.Topshelf": { "commandName": "Project", - "commandLineArgs": "task:ChineseNameExtractorTask book_id:B000008" + "commandLineArgs": "task:TtsTask book_id:B000008 chapter_id:4 speed:1 voice:Sherpa2 actor:韩立 actor_voice:Sherpa1 save_path:D:\\\\Net8\\\\FileServer\\\\Backup\\\\DmsFile\\\\voice" } } } \ No newline at end of file diff --git a/Python/Hanlp2.cs b/Python/Hanlp2.cs index a6af3e6..e474525 100644 --- a/Python/Hanlp2.cs +++ b/Python/Hanlp2.cs @@ -156,6 +156,27 @@ namespace CloudBuilder.Topshelf.Python return null; } + public List GetSurnames() + { + try + { + //必须定义方法的方式使用 + using (Py.GIL()) + { + string json = python.GetValueExec($"processor.get_surnames_json()"); + // 反序列化 + if (json == "[]") return null; + return JsonSerializer.Deserialize>(json); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + return null; + } + + public string Get(string arg) { return python.GetValueExec(arg); diff --git a/Task/AI/ChineseNameExtractorTask.cs b/Task/AI/ChineseNameExtractorTask.cs index a8501bc..8e7b361 100644 --- a/Task/AI/ChineseNameExtractorTask.cs +++ b/Task/AI/ChineseNameExtractorTask.cs @@ -3,15 +3,16 @@ using CloudBuilder.Core.DatabaseAccessor.Entity; using CloudBuilder.Core.DependencyInjection.Task; using CloudBuilder.Topshelf.Python; using CloudBuilder.Topshelf.Utility; +using System.Text.RegularExpressions; using static System.Net.Mime.MediaTypeNames; namespace CloudBuilder.Topshelf.Task { - //task:ChineseNameExtractorTask book_id:B000008 + //task:ChineseNameExtractorTask book_id:B000008 chapter_id:3 public class ChineseNameExtractorTask : IScheduleTask { private readonly IApplicationService service; - + private List commonSurnames; public ChineseNameExtractorTask(IApplicationService service) { this.service = service; @@ -19,14 +20,20 @@ namespace CloudBuilder.Topshelf.Task public void Run(Dictionary bodyDict) { + File.AppendAllText("log.txt", "ChineseNameExtractorTask:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + if (!bodyDict.ContainsKey("book_id")) return; - string guid = bodyDict["book_id"]; + string bookId = bodyDict["book_id"]; + string chapterId = string.Empty; + if (bodyDict.ContainsKey("chapter_id")) + chapterId = bodyDict["chapter_id"]; PythonCommand python = new PythonCommand(service); try { Hanlp2 hanlp = new Hanlp2(python); + commonSurnames = hanlp.GetSurnames(); IRepository repository = service.GetRepository>(); @@ -34,6 +41,9 @@ namespace CloudBuilder.Topshelf.Task IRepository repositoryAiChapterPersonEntity = service.GetRepository>(); + AiChapterPersonEntity[] dels = repositoryAiChapterPersonEntity.DetachedEntities.Where(x => x.BookId == bookId).ToArray(); + if (dels != null && dels.Length > 0) repositoryAiChapterPersonEntity.DeleteNow(dels); + string depRoot; List ners; @@ -44,7 +54,7 @@ namespace CloudBuilder.Topshelf.Task SpeakerAnalysisHelper speakerAnalysisHelper = new SpeakerAnalysisHelper(); - AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == guid).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray(); + AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId && (string.IsNullOrEmpty(chapterId) || x.ChapterId == Convert.ToInt32(chapterId))).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray(); if (ents == null || ents.Length == 0) return; AiSentenceEntity aiSentenceEntity; @@ -70,22 +80,23 @@ namespace CloudBuilder.Topshelf.Task { if (p.Length == 1) continue; aiChapterPerson = new AiChapterPersonEntity(); - aiChapterPerson.PersonName = p; + aiChapterPerson.PersonName = KeepChineseCharactersOnly(p); aiChapterPerson.BookId = ent.BookId; aiChapterPerson.ChapterId = ent.ChapterId; aiChapterPerson.ParagraphIndex = ent.ParagraphId; - aiChapterPerson.SentenceIndex= ent.SentenceIndex; + aiChapterPerson.SentenceIndex = ent.SentenceIndex; + aiChapterPerson.DialogueIndc = ent.DialogueIndc; aiChapterPerson.OrderIndex = orderIndex++; cps.Add(aiChapterPerson); repositoryAiChapterPersonEntity.InsertNow(aiChapterPerson); } } - UpdateProgress(processedLines, totalLines); + ConsoleOutput.UpdateProgress(processedLines, totalLines); } speakerAnalysisHelper.AiChapterPersons = cps.ToArray(); - + speakerAnalysisHelper.CommonSurnames = commonSurnames; speakerAnalysisHelper.MatchPerson(ents, repositoryAiSentenceEntity); } catch (Exception ex) @@ -99,18 +110,25 @@ namespace CloudBuilder.Topshelf.Task } } - private static void UpdateProgress(int processed, int total) + + private string KeepChineseCharactersOnly(string input) { - double progressPercent = (double)processed / total * 100; - int progressBarLength = 50; // 进度条总长度 - int filledLength = (int)(progressPercent / 100 * progressBarLength); + if (string.IsNullOrEmpty(input)) + return input; - // 构建进度条(如:[██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]) - string progressBar = "[" + new string('█', filledLength) + new string('░', progressBarLength - filledLength) + "]"; - - string progressInfo = $"{progressBar} {progressPercent:F2}% | 已处理:{processed}/{total} "; - Console.Write($"\r{progressInfo}"); - Console.Out.Flush(); + // 方法1:使用正则表达式 + //return Regex.Replace(input, @"[^\u4e00-\u9fff]", ""); + return Regex.Replace(input, @"[^\u4e00-\u9fff\【\】]", ""); + // 方法2:手动遍历(性能更好) + // var sb = new StringBuilder(); + // foreach (char c in input) + // { + // if (c >= 0x4e00 && c <= 0x9fff) + // sb.Append(c); + // } + // return sb.ToString(); } + + } } diff --git a/Task/TTS/TtsTask.cs b/Task/TTS/TtsTask.cs index d8a54a7..f12929f 100644 --- a/Task/TTS/TtsTask.cs +++ b/Task/TTS/TtsTask.cs @@ -17,7 +17,7 @@ using static System.Runtime.InteropServices.JavaScript.JSType; namespace CloudBuilder.Topshelf.Task.TTS { - //task:TtsTask book_id:B000008 voice:Sherpa2 actor:韩立 actor_voice:Sherpa1 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice + //task:TtsTask book_id:B000008 chapter_id:3 speed:1 voice:Sherpa2 actor:韩立 actor_voice:Sherpa1 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice //task:TtsTask voice:Sherpa2 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice\\20260528112739.mp3 text:二愣子睁大着双眼,直直望着茅草和烂泥糊成的黑屋顶,身上盖着的旧棉被,已呈深黄色,看不出原来的本来面目,还若有若无的散发着淡淡的霉味。 public class TtsTask : IScheduleTask { @@ -38,7 +38,11 @@ namespace CloudBuilder.Topshelf.Task.TTS if (bodyDict.ContainsKey("book_id")) bookId = bodyDict["book_id"]; - AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray(); + string chapterId = string.Empty; + if (bodyDict.ContainsKey("chapter_id")) + chapterId = bodyDict["chapter_id"]; + + AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId && (string.IsNullOrEmpty(chapterId) || x.ChapterId == Convert.ToInt32(chapterId))).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray(); if (ents == null || ents.Length == 0) return; @@ -55,10 +59,15 @@ namespace CloudBuilder.Topshelf.Task.TTS if (bodyDict.ContainsKey("save_path")) save_path = bodyDict["save_path"]; - GenerateAndMergeChapterAudio(ents, actor, actor_voice, voice, save_path); + float speed = 1.0f; + + if (bodyDict.ContainsKey("speed")) + speed = Convert.ToSingle(bodyDict["speed"]); + + GenerateAndMergeChapterAudio(ents, speed, actor, actor_voice, voice, save_path); } - public void GenerateAndMergeChapterAudio(AiSentenceViewEntity[] ents, string actor, string actor_voice, string mainVoice, string save_path) + public void GenerateAndMergeChapterAudio(AiSentenceViewEntity[] ents, float speed, string actor, string actor_voice, string mainVoice, string save_path) { // 1. 按 BookId + ChapterId 分组(关键!) var chapterGroups = ents @@ -69,7 +78,7 @@ namespace CloudBuilder.Topshelf.Task.TTS int totalLines = ents.Count(); int processedLines = 0; // 已处理行数计数器 UpdateProgress(processedLines, totalLines); - + Dictionary personMapping = new Dictionary(); foreach (var chapter in chapterGroups) { string bookId = chapter.Key.BookId; @@ -85,9 +94,27 @@ namespace CloudBuilder.Topshelf.Task.TTS if (ent.DialogueIndc == YesNoPolicy.YES) { if (ent.PersonName == actor) - voice = actor_voice; + { + if (personMapping.ContainsKey(ent.PersonName)) + voice = personMapping[ent.PersonName]; + else + { + voice = actor_voice; + personMapping.Add(ent.PersonName, voice); + } + + } else - voice = AI.Policy.ChineseTtsVoiceRandom.GetRandom().ToString(); + { + if (!string.IsNullOrEmpty(ent.PersonName) && personMapping.ContainsKey(ent.PersonName)) + voice = personMapping[ent.PersonName]; + else + { + voice = AI.Policy.ChineseTtsVoiceRandom.GetRandom(80).ToString(); + personMapping.Add(ent.PersonName, voice); + } + + } } else { @@ -102,7 +129,7 @@ namespace CloudBuilder.Topshelf.Task.TTS string filePath = Path.Combine(save_path, fileName); // 生成音频 - SaveAudioToFileHelper.AudioToFile(EnumHelper.ToEnum(voice), ent.Content, filePath); + SaveAudioToFileHelper.AudioToFile(EnumHelper.ToEnum(voice), speed, ent.Content, filePath); tempMp3List.Add(filePath); // 加入合并列表 diff --git a/Utility/ConsoleHelper.cs b/Utility/ConsoleHelper.cs index 2b01adf..355cfd3 100644 --- a/Utility/ConsoleHelper.cs +++ b/Utility/ConsoleHelper.cs @@ -62,6 +62,20 @@ namespace CloudBuilder.Topshelf.Utility _originalErrorHandle = GetStdHandle(STD_ERROR_HANDLE); SetStdHandle(STD_ERROR_HANDLE, IntPtr.Zero); } + + public static void UpdateProgress(int processed, int total) + { + double progressPercent = (double)processed / total * 100; + int progressBarLength = 50; // 进度条总长度 + int filledLength = (int)(progressPercent / 100 * progressBarLength); + + // 构建进度条(如:[██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]) + string progressBar = "[" + new string('█', filledLength) + new string('░', progressBarLength - filledLength) + "]"; + + string progressInfo = $"{progressBar} {progressPercent:F2}% | 已处理:{processed}/{total} "; + Console.Write($"\r{progressInfo}"); + Console.Out.Flush(); + } } } diff --git a/Utility/SaveAudioToFileHelper.cs b/Utility/SaveAudioToFileHelper.cs index 756ff22..71af4ec 100644 --- a/Utility/SaveAudioToFileHelper.cs +++ b/Utility/SaveAudioToFileHelper.cs @@ -97,7 +97,7 @@ namespace CloudBuilder.Topshelf.Utility - public static void AudioToFile(ChineseTtsVoice actorName, string text, string saveFilePath) + public static void AudioToFile(ChineseTtsVoice actorName, float speed, string text, string saveFilePath) { InitConfig(); int sid = 0; @@ -113,7 +113,7 @@ namespace CloudBuilder.Topshelf.Utility case ChineseTtsVoice.Fanchen14: case ChineseTtsVoice.Fanchen36: sid = actorName.GetSidId(); - audio = fanchenTTs.Generate(text, 1.0f, sid); + audio = fanchenTTs.Generate(text, speed, sid); break; case ChineseTtsVoice.Sherpa0: case ChineseTtsVoice.Sherpa1: @@ -121,19 +121,19 @@ namespace CloudBuilder.Topshelf.Utility case ChineseTtsVoice.Sherpa3: case ChineseTtsVoice.Sherpa4: sid = actorName.GetSidId(); - audio = sherpaTTs.Generate(text, 1.0f, sid); + audio = sherpaTTs.Generate(text, speed, sid); break; case ChineseTtsVoice.Melo: sid = actorName.GetSidId(); - audio = meloTTs.Generate(text, 1.0f, sid); + audio = meloTTs.Generate(text, speed, sid); break; case ChineseTtsVoice.Eula: sid = actorName.GetSidId(); - audio = eulaTTs.Generate(text, 1.0f, sid); + audio = eulaTTs.Generate(text, speed, sid); break; case ChineseTtsVoice.Theresa: sid = actorName.GetSidId(); - audio = theresaTTs.Generate(text, 1.0f, sid); + audio = theresaTTs.Generate(text, speed, sid); break; case ChineseTtsVoice.Aaishe1: case ChineseTtsVoice.Aaishe6: @@ -142,11 +142,11 @@ namespace CloudBuilder.Topshelf.Utility case ChineseTtsVoice.Aaishe18: case ChineseTtsVoice.Aaishe10: sid = actorName.GetSidId(); - audio = icefallTTs.Generate(text, 1.0f, sid); + audio = icefallTTs.Generate(text, speed, sid); break; default: sid = actorName.GetSidId(); - audio = matchaTTs.Generate(text, 1.0f, sid); + audio = matchaTTs.Generate(text, speed, sid); break; } diff --git a/Utility/SpeakerAnalysisHelper.cs b/Utility/SpeakerAnalysisHelper.cs index 18b3b1e..21608a7 100644 --- a/Utility/SpeakerAnalysisHelper.cs +++ b/Utility/SpeakerAnalysisHelper.cs @@ -1,15 +1,6 @@ using CloudBuilder.AI.Entity; using CloudBuilder.Core.DatabaseAccessor.Entity; using CloudBuilder.Topshelf.Python; -using System; -using System.Collections.Generic; -using System.Diagnostics.Eventing.Reader; -using System.Linq; -using System.Reflection.Metadata; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using static System.Net.Mime.MediaTypeNames; namespace CloudBuilder.Topshelf.Utility { @@ -104,7 +95,7 @@ namespace CloudBuilder.Topshelf.Utility } //如果主语==动作发出者 - //if (!string.IsNullOrEmpty(person) && !ps.Contains(person) && !ps.Contains(string.Format("[{0}]", person))) ps.Add(string.Format("[{0}]", person)); + if (!string.IsNullOrEmpty(person) && !ps.Contains(person) && !ps.Contains(string.Format("[{0}]", person))) ps.Add(string.Format("【{0}】", person)); //动作接受者 //if (!string.IsNullOrEmpty(arg1) && !ps.Contains(arg1)) ps.Add(arg1); @@ -191,22 +182,83 @@ namespace CloudBuilder.Topshelf.Utility AiSentenceEntity aiSentence; string dialogueName = string.Empty; + int totalLines = aiSentences.Length; + int processedLines = 0; // 已处理行数计数器 + Console.WriteLine($"\r"); foreach (AiSentenceViewEntity aiSentenceView in aiSentences) { - dialogueName= string.Empty; + aiSentenceView.PersonName = null; + processedLines++; + ConsoleOutput.UpdateProgress(processedLines, totalLines); if (aiSentenceView.DialogueIndc != YesNoPolicy.YES) continue; - + //A说:“B,你好!”/“A,你好!”B对A说。 if (aiSentences.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphId == aiSentenceView.ParagraphId).Count() > 1) + { + dialogueName = string.Empty; + + //提取对话中的人物 + if (AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex == x.SentenceIndex).Any()) + { + dialogueName = AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex == x.SentenceIndex).FirstOrDefault().PersonName; + } + //提取对话段落中,非对话内容中的人物 + if (AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex != x.SentenceIndex && x.PersonName != dialogueName).Any()) + { + aiSentenceView.PersonName = AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex != x.SentenceIndex && x.PersonName != dialogueName).OrderBy(x => x.OrderIndex).FirstOrDefault().PersonName; + + if (aiSentenceView.PersonName.Contains("【")) + { + aiSentenceView.PersonName = AiChapterPersons.Where(x => !x.PersonName.Contains("【") && x.PersonName.Contains(aiSentenceView.PersonName.Replace("【", "").Replace("】", ""))).FirstOrDefault()!.PersonName; + } + } + //说话者在前面 + if (aiSentenceView.SentenceIndex == 2) + { + //如果没有提取到人物,优先向前,再向后 + if (string.IsNullOrEmpty(aiSentenceView.PersonName)) + { + aiSentenceView.PersonName = GetForwardName(dialogueName, aiSentenceView, aiSentences); + } + //如果没有提取到人物,优先向前,再向后 + if (string.IsNullOrEmpty(aiSentenceView.PersonName)) + { + aiSentenceView.PersonName = GetBackwardName(dialogueName, aiSentenceView, aiSentences); + } + } + else + { + if (string.IsNullOrEmpty(aiSentenceView.PersonName)) + { + aiSentenceView.PersonName = GetBackwardName(dialogueName, aiSentenceView, aiSentences); + } + if (string.IsNullOrEmpty(aiSentenceView.PersonName)) + { + aiSentenceView.PersonName = GetForwardName(dialogueName, aiSentenceView, aiSentences); + } + } + //如果“你好”,前后面有說話者的,单独内容,下下文不能是同一個人 + dialogueName = aiSentenceView.PersonName; + //dialogueName = string.Empty; + } + else { if (AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex == x.SentenceIndex).Any()) { dialogueName = AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex == x.SentenceIndex).FirstOrDefault().PersonName; } + //"你好,"張三說道 + //"你好" + if (aiSentences.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphId == aiSentenceView.ParagraphId - 1 && x.DialogueIndc == YesNoPolicy.NO).Count() > 0 && aiSentences.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphId == aiSentenceView.ParagraphId + 1 && x.DialogueIndc == YesNoPolicy.YES).Count() == 1) dialogueName = string.Empty; - if (AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex != x.SentenceIndex && x.PersonName != dialogueName).Any()) + //如果没有提取到人物,优先向前,再向后 + aiSentenceView.PersonName = GetForwardName(dialogueName, aiSentenceView, aiSentences, 3); + //如果没有提取到人物,优先向前,再向后 + if (string.IsNullOrEmpty(aiSentenceView.PersonName)) { - aiSentenceView.PersonName = AiChapterPersons.Where(x =>x.ChapterId==aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex != x.SentenceIndex && x.PersonName != dialogueName).OrderBy(x => x.OrderIndex).FirstOrDefault().PersonName; + aiSentenceView.PersonName = GetBackwardName(dialogueName, aiSentenceView, aiSentences, 5); } + //如果“你好”,前后面有說話者的,单独内容,下下文不能是同一個人 + dialogueName = aiSentenceView.PersonName; } aiSentence = new AiSentenceEntity(); @@ -215,9 +267,86 @@ namespace CloudBuilder.Topshelf.Utility } } + public string GetForwardName(string excludeName, AiSentenceViewEntity aiSentenceView, AiSentenceViewEntity[] aiSentences, int maxLength = 10) + { + foreach (var cp in AiChapterPersons.Where(x => x.DialogueIndc != YesNoPolicy.YES && x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex < aiSentenceView.ParagraphId && x.OrderIndex == 1).OrderByDescending(x => x.ParagraphIndex).ToArray()) + { + if (aiSentences.Where(x => x.ChapterId == cp.ChapterId && x.ParagraphId == cp.ParagraphIndex + && x.DialogueIndc == YesNoPolicy.YES).Any()) + { + if (aiSentences.Where(x => x.ChapterId == cp.ChapterId && x.ParagraphId == cp.ParagraphIndex + && x.DialogueIndc == YesNoPolicy.NO).Any()) + { + if (string.IsNullOrEmpty(cp.PersonName) || cp.PersonName.Contains("【")) continue; + if (!string.IsNullOrEmpty(excludeName) && excludeName == cp.PersonName) continue; + if (!string.IsNullOrEmpty(excludeName) && AreSamePerson(excludeName, cp.PersonName)) continue; + + if (aiSentenceView.ParagraphId - cp.ParagraphIndex > maxLength) break; + return cp.PersonName; + } + + break; + } + + if (string.IsNullOrEmpty(cp.PersonName) || cp.PersonName.Contains("【")) continue; + if (!string.IsNullOrEmpty(excludeName) && excludeName == cp.PersonName) continue; + if (!string.IsNullOrEmpty(excludeName) && AreSamePerson(excludeName, cp.PersonName)) continue; + + if (aiSentenceView.ParagraphId - cp.ParagraphIndex > maxLength) break; + return cp.PersonName; + } + return null; + } + public string GetBackwardName(string excludeName, AiSentenceViewEntity aiSentenceView, AiSentenceViewEntity[] aiSentences, int maxLength = 10) + { + foreach (var cp in AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex > aiSentenceView.ParagraphId).OrderBy(x => x.ParagraphIndex).ThenBy(x => x.OrderIndex).ToArray()) + { + if (aiSentences.Where(x => x.ChapterId == cp.ChapterId && x.ParagraphId == cp.ParagraphIndex && x.DialogueIndc == YesNoPolicy.YES).Any()) break; + + if (string.IsNullOrEmpty(cp.PersonName) || cp.PersonName.Contains("【")) continue; + if (!string.IsNullOrEmpty(excludeName) && aiSentenceView.PersonName == cp.PersonName) continue; + if (!string.IsNullOrEmpty(excludeName) && AreSamePerson(excludeName, cp.PersonName)) continue; + + if (cp.ParagraphIndex - aiSentenceView.ParagraphId > maxLength) break; + return cp.PersonName; + } + return null; + } + public bool AreSamePerson(string nameA, string nameB) + { + if (string.IsNullOrWhiteSpace(nameA) || string.IsNullOrWhiteSpace(nameB)) + return false; + + if (nameA.Substring(0, 1) == nameB.Substring(0, 1) && nameA != nameB) + return false; + + string surnameA = ExtractSurname(nameA); + string surnameB = ExtractSurname(nameB); + + return !string.IsNullOrEmpty(surnameA) && surnameA == surnameB; + } + + private string ExtractSurname(string name) + { + // 优先提取第一个字符(常见“姓+后缀”模式) + if (name.Length > 0 && CommonSurnames.Contains(name[0].ToString())) + return name[0].ToString(); + + // 对于“韩胖子”这类姓不在首位的情况,扫描整个字符串 + foreach (char c in name) + { + if (CommonSurnames.Contains(c.ToString())) + return c.ToString(); + } + + return null; + } + public AiSentenceEntity[] AiSentences { get; set; } public AiChapterPersonEntity[] AiChapterPersons { get; set; } + + public List CommonSurnames { get; set; } } } /*