CloudBuilder/CloudBuilder.AI/Utility/SentenceSplitter.cs
owenchen 996bb243df ow
2026-06-22 17:02:13 +08:00

215 lines
9.8 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 智能句子分割器
/// </summary>
public class SentenceSplitter
{
// 成对引号配置数组(可自由扩展,格式:[左引号, 右引号]
private readonly List<(string LeftQuote, string RightQuote)> _quotePairs;
// 动态生成的正则表达式(匹配所有配置的引号类型)
private Regex _dynamicQuotePattern;
// 引号内必须包含的结尾标点集合
private readonly string _endPunctuations = "?!.,;:?!。,;:、……~·";
/// <summary>
/// 默认构造函数:初始化常用成对引号(中文、英文、直角引号)
/// </summary>
public SentenceSplitter()
{
_quotePairs = new List<(string, string)>
{
("“", "”"), // 中文弯引号
("\"", "\""), // 英文双引号(需转义)
("〝", "〞"), // 中文直角引号
("「", "」") // 中文方头引号(可选扩展)
};
InitDynamicRegex();
}
/// <summary>
/// 自定义构造函数:允许外部传入任意成对引号配置
/// </summary>
/// <param name="customQuotePairs">自定义成对引号列表</param>
public SentenceSplitter(List<(string LeftQuote, string RightQuote)> customQuotePairs)
{
_quotePairs = customQuotePairs ?? new List<(string, string)>();
InitDynamicRegex();
}
/// <summary>
/// 初始化动态正则:根据配置的成对引号生成匹配规则
/// </summary>
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构建动态正则表达式
// 分组说明:
// ?<left>:匹配配置的任意左引号
// ?<content>:匹配引号内的内容(必须以指定标点结尾)
// ?<right>:匹配配置的任意右引号
string regexPattern = $@"(?<left>{escapedLeftQuotes})(?<content>.+?[{escapedPunctuations}])(?<right>{escapedRightQuotes})";
_dynamicQuotePattern = new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.Singleline);
}
/// <summary>
/// 核心分割方法:按配置的成对引号分割文本,结果无引号字符
/// </summary>
/// <param name="input">待分割的文本(如:“其他标点符号,如?”紫茜的秀目 或 "其他标点符号,如?"紫茜的秀目)</param>
/// <returns>分割后的纯文本列表</returns>
public List<AiSentenceEntity> SplitByConfigurableQuotes(AiSentenceEntity aiParagraphEntity)
{
AiSentenceEntity aiSentence;
int sentenceIndex = 1;
// 空值/空白校验
if (string.IsNullOrWhiteSpace(aiParagraphEntity.Content))
{
return new List<AiSentenceEntity>();
}
List<AiSentenceEntity> result = new List<AiSentenceEntity>();
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;
}
/// <summary>
/// 新增:判断文本是否包含对话内容
/// </summary>
/// <param name="text">待判断的文本(句子/段落)</param>
/// <returns>true=包含对话false=纯旁白/空文本</returns>
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, @"""(?<content>.*?)""");
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;
}
}
}