using CloudBuilder.AI.Entity; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace CloudBuilder.AI.Utility { /// /// 智能句子分割器 /// public class SentenceSplitter { // 成对引号配置数组(可自由扩展,格式:[左引号, 右引号]) private readonly List<(string LeftQuote, string RightQuote)> _quotePairs; // 动态生成的正则表达式(匹配所有配置的引号类型) private Regex _dynamicQuotePattern; // 引号内必须包含的结尾标点集合 private readonly string _endPunctuations = "?!.,;:?!。,;:、……~·"; /// /// 默认构造函数:初始化常用成对引号(中文、英文、直角引号) /// public SentenceSplitter() { _quotePairs = new List<(string, string)> { ("“", "”"), // 中文弯引号 ("\"", "\""), // 英文双引号(需转义) ("〝", "〞"), // 中文直角引号 ("「", "」") // 中文方头引号(可选扩展) }; InitDynamicRegex(); } /// /// 自定义构造函数:允许外部传入任意成对引号配置 /// /// 自定义成对引号列表 public SentenceSplitter(List<(string LeftQuote, string RightQuote)> customQuotePairs) { _quotePairs = customQuotePairs ?? new List<(string, string)>(); InitDynamicRegex(); } /// /// 初始化动态正则:根据配置的成对引号生成匹配规则 /// private void InitDynamicRegex() { if (_quotePairs.Count == 0) { // 空配置时生成空正则,避免匹配任何内容 _dynamicQuotePattern = new Regex(@"^$", RegexOptions.Compiled); return; } // 步骤1:转义所有引号(避免正则特殊字符冲突),拼接左/右引号匹配组 string escapedLeftQuotes = string.Join("|", _quotePairs.Select(p => Regex.Escape(p.LeftQuote))); string escapedRightQuotes = string.Join("|", _quotePairs.Select(p => Regex.Escape(p.RightQuote))); // 步骤2:转义标点集合,避免正则解析错误 string escapedPunctuations = Regex.Escape(_endPunctuations); // 步骤3:构建动态正则表达式 // 分组说明: // ?:匹配配置的任意左引号 // ?:匹配引号内的内容(必须以指定标点结尾) // ?:匹配配置的任意右引号 string regexPattern = $@"(?{escapedLeftQuotes})(?.+?[{escapedPunctuations}])(?{escapedRightQuotes})"; _dynamicQuotePattern = new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.Singleline); } /// /// 核心分割方法:按配置的成对引号分割文本,结果无引号字符 /// /// 待分割的文本(如:“其他标点符号,如?”紫茜的秀目 或 "其他标点符号,如?"紫茜的秀目) /// 分割后的纯文本列表 public List SplitByConfigurableQuotes(AiSentenceEntity aiParagraphEntity) { AiSentenceEntity aiSentence; int sentenceIndex = 1; // 空值/空白校验 if (string.IsNullOrWhiteSpace(aiParagraphEntity.Content)) { return new List(); } List result = new List(); string remainingText = aiParagraphEntity.Content.Trim(); int lastMatchEnd = 0; // 遍历所有匹配的「引号+内容+标点+引号」文本块 foreach (Match match in _dynamicQuotePattern.Matches(remainingText)) { // 1. 提取匹配的左/右引号,校验是否为成对配置 string matchedLeft = match.Groups["left"].Value; string matchedRight = match.Groups["right"].Value; bool isPairMatched = _quotePairs.Any(p => p.LeftQuote == matchedLeft && p.RightQuote == matchedRight); if (!isPairMatched) { continue; // 非成对引号,跳过该匹配(避免“xxx"这类混合匹配) } // 2. 提取引号前的文本(过滤空值) string prefixText = remainingText.Substring(lastMatchEnd, match.Index - lastMatchEnd).Trim(); if (!string.IsNullOrEmpty(prefixText)) { aiSentence = new AiSentenceEntity(); aiSentence.BookId = aiParagraphEntity.BookId; aiSentence.ChapterId = aiParagraphEntity.ChapterId; aiSentence.Content = prefixText; aiSentence.DialogueIndc = YesNoPolicy.NO; aiSentence.SentenceIndex = sentenceIndex++; aiSentence.ParagraphId = aiParagraphEntity.ParagraphId; aiSentence.Status = "G"; //D-【待确认对话】p-【待确认人物】A-【等待分配角色】G-【生成中】C-【完成】 result.Add(aiSentence); } // 3. 提取引号内的纯内容(无引号字符) string quotedContent = match.Groups["content"].Value.Trim(); if (!string.IsNullOrEmpty(quotedContent)) { aiSentence = new AiSentenceEntity(); aiSentence.BookId = aiParagraphEntity.BookId; aiSentence.ChapterId = aiParagraphEntity.ChapterId; aiSentence.Content = quotedContent; aiSentence.DialogueIndc = YesNoPolicy.YES; aiSentence.SentenceIndex = sentenceIndex++; aiSentence.ParagraphId = aiParagraphEntity.ParagraphId; aiSentence.Status = "P"; //D-【待确认对话】p-【待确认人物】A-【等待分配角色】G-【生成中】C-【完成】 result.Add(aiSentence); } // 更新最后匹配位置 lastMatchEnd = match.Index + match.Length; } // 4. 提取最后一个匹配项后的剩余文本 string suffixText = remainingText.Substring(lastMatchEnd).Trim(); if (!string.IsNullOrEmpty(suffixText)) { aiSentence = new AiSentenceEntity(); aiSentence.BookId = aiParagraphEntity.BookId; aiSentence.ChapterId = aiParagraphEntity.ChapterId; aiSentence.Content = suffixText; aiSentence.DialogueIndc = YesNoPolicy.NO; aiSentence.SentenceIndex = sentenceIndex++; aiSentence.ParagraphId = aiParagraphEntity.ParagraphId; aiSentence.Status = "G"; //D-【待确认对话】p-【待确认人物】A-【等待分配角色】G-【生成中】C-【完成】 result.Add(aiSentence); } return result; } /// /// 新增:判断文本是否包含对话内容 /// /// 待判断的文本(句子/段落) /// true=包含对话,false=纯旁白/空文本 public bool IsDialogue(string text) { if (string.IsNullOrWhiteSpace(text)) return false; string cleanedText = text.Trim(); cleanedText = Regex.Replace(cleanedText, @" ", " "); // 统一引号 cleanedText = cleanedText.Replace("“", "\"") .Replace("”", "\"") .Replace("〝", "\"") .Replace("〞", "\"") .Replace("「", "\"") .Replace("」", "\"") .Replace("‘", "'") .Replace("’", "'"); // 1. 存在说话引导标记(冒号/逗号 + 引号) bool hasDialogueTag = Regex.IsMatch(cleanedText, @"[::,,]\s*"""); // 2. 提取所有双引号内容,逐个判断是否为有效对话 var quotedMatches = Regex.Matches(cleanedText, @"""(?.*?)"""); bool hasValidDialogue = false; foreach (Match m in quotedMatches) { string content = m.Groups["content"].Value.Trim(); if (string.IsNullOrEmpty(content)) continue; // 规则A:包含典型对话结尾标点 bool endsWithDialogPunct = Regex.IsMatch(content, @"[。!??!…\.~~、,,]$"); // 规则B:长度至少4个字符(或按中文字数算) bool isLongEnough = content.Length >= 4; if (endsWithDialogPunct || isLongEnough) { hasValidDialogue = true; break; } } // 3. 单引号内容同理(可省略或同逻辑) // 最终:有说话标记 或 存在有效对话内容,且不是“仅由引号符号组成” bool onlyQuote = Regex.IsMatch(cleanedText, @"^\s*[""']{1,2}\s*$"); return (hasDialogueTag || hasValidDialogue) && !onlyQuote; } } }