215 lines
9.8 KiB
C#
215 lines
9.8 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|