This commit is contained in:
owenchen committed 2026-05-28 15:49:25 +08:00
commit 597b88d075
37 files changed
+3943

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
################################################################################
# 這個 .gitignore 檔案由 Microsoft(R) Visual Studio 自動建立。
################################################################################
/.vs
/bin/Debug/net8.0
/obj
+93
View File
@@ -0,0 +1,93 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Lib\**" />
<Compile Remove="mp3\**" />
<Compile Remove="mp3_new1\**" />
<Compile Remove="mp3_new\**" />
<EmbeddedResource Remove="Lib\**" />
<EmbeddedResource Remove="mp3\**" />
<EmbeddedResource Remove="mp3_new1\**" />
<EmbeddedResource Remove="mp3_new\**" />
<None Remove="Lib\**" />
<None Remove="mp3\**" />
<None Remove="mp3_new1\**" />
<None Remove="mp3_new\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Task\AI\CharacterInsertTask.cs" />
<Compile Remove="Task\AI\CharacterTtsTask.cs" />
<Compile Remove="Task\AI\NovelCutWordTask.cs" />
<Compile Remove="Task\AI\WordInsertTask.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CognitiveServices.Speech" Version="1.50.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="pythonnet" Version="3.0.5" />
<PackageReference Include="org.k2fsa.sherpa.onnx" Version="1.13.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Policy\" />
</ItemGroup>
<ItemGroup>
<Reference Include="CloudBuilder.AI">
<HintPath>Lib\CloudBuilder.AI.dll</HintPath>
</Reference>
<Reference Include="CloudBuilder.AI.Service">
<HintPath>Lib\CloudBuilder.AI.Service.dll</HintPath>
</Reference>
<Reference Include="CloudBuilder.Core">
<HintPath>Lib\CloudBuilder.Core.dll</HintPath>
</Reference>
<Reference Include="CloudBuilder.Request">
<HintPath>Lib\CloudBuilder.Request.dll</HintPath>
</Reference>
<Reference Include="CloudBuilder.Security">
<HintPath>Lib\CloudBuilder.Security.dll</HintPath>
</Reference>
<Reference Include="CloudBuilder.Security.Service">
<HintPath>Lib\CloudBuilder.Security.Service.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34316.72
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudBuilder.Topshelf", "CloudBuilder.Topshelf.csproj", "{63EB47F6-A616-4C64-B850-85ED294E2524}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{63EB47F6-A616-4C64-B850-85ED294E2524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63EB47F6-A616-4C64-B850-85ED294E2524}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63EB47F6-A616-4C64-B850-85ED294E2524}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63EB47F6-A616-4C64-B850-85ED294E2524}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {24F7770A-0647-4A47-B147-80AFD34ACB87}
EndGlobalSection
EndGlobal
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+156
View File
@@ -0,0 +1,156 @@
using CloudBuilder;
using CloudBuilder.Core.Authorization;
using CloudBuilder.Core.DatabaseAccessor;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Form;
using CloudBuilder.Core.DependencyInjection.Request;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Core.Policy;
using CloudBuilder.Core.Service;
using CloudBuilder.Request.Security;
using CloudBuilder.Security.Data;
using CloudBuilder.Security.Policy;
using CloudBuilder.Topshelf.Task;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Text;
public partial class Program
{
/// <summary>
/// request:Y 要求调用webApi参数
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
try
{
// 设置控制台编码为UTF-8,解决中文乱码问题
Console.OutputEncoding = Encoding.UTF8;
Console.InputEncoding = Encoding.UTF8;
string taskName = null;
Dictionary<string, string> bodyDict = new Dictionary<string, string>();
if (args != null && args.Length > 0)
{
var services = new ServiceCollection();
services.Initialize();
var serviceProvider = services.BuildServiceProvider();
ApplicationServiceContext applicationServiceContext = serviceProvider.GetService<ApplicationServiceContext>()!;
string key = null;
string[] temp = null;
foreach (string s in args)
{
temp = s.Split(new char[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries);
if (temp.Length <= 1) continue;
if (temp[1] == null || temp[1].Trim() == string.Empty) continue;
key = temp[0].Trim();
if (key == "task")
{
taskName = temp[1];// config.TaskMappings.Where(x => x.Task == temp[1]).FirstOrDefault();
if (taskName == null) continue;
continue;
}
if (bodyDict.ContainsKey(key)) continue;
bodyDict.Add(key, temp[1]);
}
if (string.IsNullOrEmpty(taskName)) return;
try
{
IApplicationService applicationService = serviceProvider.GetService<IApplicationService>();
applicationService.UserId = applicationService.Configuration["IApplicationService:UserId"];
//如果有传入用户ID,则使用传入的用户ID
if (bodyDict.ContainsKey("user_id"))
{
applicationService.UserId = bodyDict["user_id"];
}
IEnumerable<IScheduleTask> tasks = serviceProvider.GetServices<IScheduleTask>();
foreach (var task in tasks)
{
if (task.GetType().Name.ToLower() != taskName.ToLower()) continue;
if (bodyDict.ContainsKey("request") && bodyDict["request"].ToUpper().Trim() == "Y") Login(applicationService);
task.Run(bodyDict);
}
}
catch (Exception ex)
{
Console.WriteLine($"[{ex.Message}]");
}
finally
{
Environment.Exit(0);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
static void Login(IApplicationService applicationService)
{
#region 系统用户登录
try
{
LoginData loginData = new LoginData();
//从容器获取请求对象
AuthRequest authRequest = ServiceHelper.GetTypeService<AuthRequest>();
//获取RSA公钥
string pubKey = authRequest.GetRsaPublicKey();
//加密用户密码
string psw = applicationService.Configuration["CloudBuilder:SecurityUser:UserPassword"]!;
loginData.Guid = psw.Substring(0, 36);
var rsa = new RSA(pubKey, true);
loginData.UserMobile = applicationService.Configuration["CloudBuilder:SecurityUser:UserMobile"]!;
loginData.RsaUserPassword = rsa.Encode(psw);
//设置当前文化
loginData.Culture = CulturePolicy.ZH_CN;
LoginProfile loginProfile;
loginProfile = authRequest.Login(loginData);
IContext loginContext = ServiceHelper.GetInterfaceService<IContext>()!;
//登录成功后设置登录上下文
loginContext.Requestor = loginProfile.Username;
loginContext.SetPermissionInfo(loginProfile.PermissionInfo);
loginContext.SetToken(loginProfile.Token.AccessToken);
loginContext.LoginDate = DateTime.Now;
}
catch (Exception ex)
{
throw ex;
}
//ISecurityMasterService securityMasterService = service.ServiceProvider.GetService<ISecurityMasterService>();
//SecurityMasterEntityKey key = new SecurityMasterEntityKey();
//key.MasterCodeItem = "default_work_shift";
//key.MasterCategory = "CloudBuilder.Hr.Policy.Master";
//key.MasterSubcategory = "StaffWorkShiftPolicy";
//SecurityMasterEntity securityMaster = securityMasterService.FindByKey(key);
#endregion
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"profiles": {
"CloudBuilder.Topshelf": {
"commandName": "Project",
"commandLineArgs": "task:TtsTask voice:Sherpa2 save_path:D:\\\\Net8\\\\FileServer\\\\Backup\\\\DmsFile\\\\voice\\\\20260528112739.mp3 text:二愣子睁大着双眼,直直望着茅草和烂泥糊成的黑屋顶,身上盖着的旧棉被,已呈深黄色,看不出原来的本来面目,还若有若无的散发着淡淡的霉味。"
}
}
}
+167
View File
@@ -0,0 +1,167 @@
using Python.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Python
{
public class Hanlp2
{
public readonly PythonCommand python;
public Hanlp2(PythonCommand python)
{
this.python = python;
string file = python.ApplicationService.Configuration["Python:PythonFile"]!;
if (!File.Exists(file)) return;
string init = File.ReadAllText(file, Encoding.UTF8);
using (Py.GIL())
{
python.Exec(init);
}
}
public List<NamedEntity> GetHanlpNer(string text)
{
try
{
//必须定义方法的方式使用
using (Py.GIL())
{
string json = python.GetValueExec($"processor.get_ner_json('{text}')");
// 反序列化
if (json == "[]") return null;
return JsonSerializer.Deserialize<List<NamedEntity>>(json);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
public List<List<MeaningRepresentationParsingEntity>> GetHanlpSrl(string text)
{
try
{
//必须定义方法的方式使用
using (Py.GIL())
{
string json = python.GetValueExec($"processor.get_srl_json('{text}')");
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true // 忽略属性名大小写
};
// 反序列化
return JsonSerializer.Deserialize<List<List<MeaningRepresentationParsingEntity>>>(json, options);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
public string GetDepRoot(string text)
{
try
{
//必须定义方法的方式使用
using (Py.GIL())
{
string json = python.GetValueExec($"processor.get_root('{text}')");
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true // 忽略属性名大小写
};
// 反序列化
return json;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
public List<HanlpConstituencyNode> GetHanlpCon(string text)
{
if (string.IsNullOrEmpty(text) || text.Length < 3) return null;
try
{
//必须定义方法的方式使用
using (Py.GIL())
{
string json = python.GetValueExec($"processor.get_con_json('{text}')");
if (json == "[]") return null;
return HanlpConstituencyNode.FromJsonString(json);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
public List<string> GetTokFine(string text)
{
try
{
//必须定义方法的方式使用
using (Py.GIL())
{
string json = python.GetValueExec($"processor.get_tok_fine('{text}')");
// 反序列化
if (json == "[]") return null;
return JsonSerializer.Deserialize<List<string>>(json);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
public List<string> GetTokCoarse(string text)
{
try
{
//必须定义方法的方式使用
using (Py.GIL())
{
string json = python.GetValueExec($"processor.get_tok_coarse('{text}')");
// 反序列化
if (json == "[]") return null;
return JsonSerializer.Deserialize<List<string>>(json);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
public string Get(string arg)
{
return python.GetValueExec(arg);
}
}
}
+954
View File
@@ -0,0 +1,954 @@
using Microsoft.Extensions.FileSystemGlobbing;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Python
{
public class SentenceInfo
{
public List<string> Subjects { get; set; } = new();
public List<string> Predicates { get; set; } = new();
public List<string> Objects { get; set; } = new();
public List<string> PersonRoles { get; set; } = new();
}
public class NamedEntity
{
public string Text { get; set; } // 實體文本 (如 "王小明")
public string Type { get; set; } // 實體類型 (如 "PERSON")
public int Start { get; set; } // 起始位置 (索引)
public int End { get; set; } // 結束位置 (索引)
public override string ToString() => $"{Text} ({Type}) [{Start}-{End}]";
}
public class MeaningRepresentationParsingEntity
{
public string Text { get; set; }
public string Type { get; set; }
public int Start { get; set; }
public int End { get; set; }
}
public class HanlpConstituencyNode
{
/// <summary>
/// 节点ID
/// </summary>
[JsonProperty("ItemId")]
public int ItemId { get; set; }
/// <summary>
/// 父节点ID
/// </summary>
[JsonProperty("FatherId")]
public int FatherId { get; set; }
/// <summary>
/// 节点标签(如TOP、IP、NP、VP等)
/// </summary>
[JsonProperty("Label")]
public string Label { get; set; }
/// <summary>
/// 节点层级
/// </summary>
[JsonProperty("Level")]
public int Level { get; set; }
/// <summary>
/// 子节点数量
/// </summary>
[JsonProperty("Children")]
public int Children { get; set; }
/// <summary>
/// 从JSON字符串解析节点列表
/// </summary>
/// <param name="jsonString">Python HanLP返回的JSON字符串</param>
/// <returns>节点列表</returns>
public static List<HanlpConstituencyNode> FromJsonString(string jsonString)
{
if (string.IsNullOrEmpty(jsonString))
return new List<HanlpConstituencyNode>();
try
{
return JsonConvert.DeserializeObject<List<HanlpConstituencyNode>>(jsonString);
}
catch (Exception ex)
{
Console.WriteLine($"JSON解析错误: {ex.Message}");
return new List<HanlpConstituencyNode>();
}
}
/// <summary>
/// 将节点列表转换为JSON字符串
/// </summary>
/// <param name="nodes">节点列表</param>
/// <returns>JSON字符串</returns>
public static string ToJsonString(List<HanlpConstituencyNode> nodes)
{
if (nodes == null)
return "[]";
return JsonConvert.SerializeObject(nodes,
Formatting.Indented,
new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.Default });
}
/// <summary>
/// 重写ToString方法,返回节点的详细信息
/// </summary>
/// <returns>节点信息字符串</returns>
public override string ToString()
{
return $"ItemId: {ItemId}, FatherId: {FatherId}, Label: {Label}, Level: {Level}, Children: {Children}";
}
}
/// <summary>
/// HanLP处理结果工具类
/// 用于与Python HanLP服务交互
/// </summary>
public class HanlpResultHelper
{
/// <summary>
/// 解析句法分析结果
/// </summary>
/// <param name="pythonOutput">Python程序输出的JSON字符串</param>
/// <returns>节点列表</returns>
public static List<HanlpConstituencyNode> ParseConstituencyResult(string pythonOutput)
{
return HanlpConstituencyNode.FromJsonString(pythonOutput);
}
/// <summary>
/// 根据父节点ID查找子节点
/// </summary>
/// <param name="nodes">所有节点</param>
/// <param name="fatherId">父节点ID</param>
/// <returns>子节点列表</returns>
public static List<HanlpConstituencyNode> GetChildrenNodes(List<HanlpConstituencyNode> nodes, int fatherId)
{
return nodes?.FindAll(node => node.FatherId == fatherId) ?? new List<HanlpConstituencyNode>();
}
/// <summary>
/// 根据ID查找节点
/// </summary>
/// <param name="nodes">所有节点</param>
/// <param name="itemId">节点ID</param>
/// <returns>节点对象,如果未找到则返回null</returns>
public static HanlpConstituencyNode GetNodeById(List<HanlpConstituencyNode> nodes, int itemId)
{
return nodes?.Find(node => node.ItemId == itemId);
}
/// <summary>
/// 根据Label查找节点
/// </summary>
/// <param name="nodes">所有节点</param>
/// <param name="itemId">节点ID</param>
/// <returns>节点对象,如果未找到则返回null</returns>
public static HanlpConstituencyNode[] GetNodeByLabel(List<HanlpConstituencyNode> nodes, string label)
{
return nodes?.Where(node => node.Label == label).ToArray();
}
/// <summary>
/// 根据ID查找父节点
/// </summary>
/// <param name="nodes">所有节点</param>
/// <param name="itemId">节点ID</param>
/// <returns>节点对象,如果未找到则返回null</returns>
public static HanlpConstituencyNode GetNodeFatherById(List<HanlpConstituencyNode> nodes, int itemId)
{
HanlpConstituencyNode self = GetNodeById(nodes, itemId);
return nodes?.Find(node => node.ItemId == self.FatherId);
}
/// <summary>
/// 根据ID查找等于label父节点
/// </summary>
/// <param name="nodes">所有节点</param>
/// <param name="itemId">节点ID</param>
/// <returns>节点对象,如果未找到则返回null</returns>
public static HanlpConstituencyNode GetNodeTopLabelById(List<HanlpConstituencyNode> nodes, int itemId, string label)
{
HanlpConstituencyNode nd = GetNodeFatherById(nodes, itemId);
if (label == nd.Label && nd.Children > 0) return nd;
while (nd.FatherId > 0)
{
return GetNodeTopLabelById(nodes, nd.ItemId, label);
}
return null;
}
/// <summary>
/// 根据ID查找等于label子节点
/// </summary>
/// <param name="nodes">所有节点</param>
/// <param name="itemId">节点ID</param>
/// <returns>节点对象,如果未找到则返回null</returns>
public static string GetNodeBottomLabelById(List<HanlpConstituencyNode> nodes, int itemId, string label)
{
HanlpConstituencyNode[] nds = nodes?.Where(node => node.FatherId == itemId && node.Label == label && node.Children > 0).ToArray();
if (nds == null || nds.Length == 0) return null;
HanlpConstituencyNode father;
List<string> list = new List<string>();
foreach (HanlpConstituencyNode nd in nds)
{
List<HanlpConstituencyNode> sons = GetChildrenNodes(nodes, nd.ItemId);
if (sons == null || sons.Count() == 0) continue;
return string.Join("", sons.Select(x => x.Label).ToArray());
}
return null;
}
/// <summary>
/// 根据ID查找所有兄弟节点(同父且不同ID的节点)
/// </summary>
/// <param name="nodes">所有节点列表</param>
/// <param name="itemId">目标节点ID</param>
/// <returns>兄弟节点列表,如果无兄弟节点/节点不存在则返回空列表</returns>
public static List<HanlpConstituencyNode> GetNodeBrothersById(List<HanlpConstituencyNode> nodes, int itemId)
{
// 空值保护:如果节点列表为空,直接返回空列表
if (nodes == null || nodes.Count == 0)
{
return new List<HanlpConstituencyNode>();
}
// 第一步:找到目标节点(根据ID)
var targetNode = nodes.Find(node => node.ItemId == itemId);
// 如果目标节点不存在,返回空列表
if (targetNode == null)
{
return new List<HanlpConstituencyNode>();
}
// 第二步:获取目标节点的父ID,筛选所有同父且ID不等于目标节点的节点
int fatherId = targetNode.FatherId;
var brotherNodes = nodes.FindAll(node =>
node.FatherId == fatherId && // 同父节点
node.ItemId != itemId // 排除自身
);
// 返回兄弟节点列表(无兄弟则返回空列表)
return brotherNodes ?? new List<HanlpConstituencyNode>();
}
}
}
/*
import os
import json
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
try:
import hanlp
from hanlp.components.mtl.multi_task_learning import MultiTaskLearning
from hanlp.components.mtl.tasks.tok.tag_tok import TaggingTokenization
from hanlp.components.mtl.tasks.ner.tag_ner import TaggingNamedEntityRecognition
# 尝试导入tqdm,如果不存在则使用替代
try:
from tqdm import tqdm
HAS_TQDM = True
except ImportError:
logger.warning("tqdm未安装,将不显示进度条")
HAS_TQDM = False
# 创建一个简单的tqdm替代
class tqdm:
def __init__(self, iterable, desc=None):
self.iterable = iterable
self.desc = desc
def __iter__(self):
return iter(self.iterable)
except ImportError as e:
logger.error(f"导入HanLP失败: {e}")
class HanLPProcessor:
"""HanLP自然语言处理工具类"""
def __init__(self,
hanlp_home='D:/Python38/hanlp/',
hf_home='D:/Python38/models/',
hf_endpoint='https://hf-mirror.com',
hanlp_url='https://ftp.hankcs.com/hanlp/',
surnames_path='D:/Python38/hanlp/dictionary/surnames.txt',
titles_path='D:/Python38/hanlp/dictionary/titles.txt',
words_path='D:/Python38/hanlp/dictionary/words.txt',
salutations_path='D:/Python38/hanlp/dictionary/salutations.txt'):
# 初始化ID计数器和父ID跟踪
self.item_id_counter = 1
self.current_father_id = 0
"""
初始化HanLP处理器
参数:
hanlp_home: HanLP资源缓存目录
hf_home: HuggingFace模型缓存目录
hf_endpoint: HuggingFace下载端点
hanlp_url: HanLP下载URL
surnames_path: 姓氏词典路径
titles_path: 头衔词典路径
words_path: 专有名词词典路径
salutations_path: 称呼词典路径
"""
# 存储配置信息
self.hanlp_home = hanlp_home
self.hf_home = hf_home
self.hf_endpoint = hf_endpoint
self.hanlp_url = hanlp_url
self.surnames_path = surnames_path
self.titles_path = titles_path
self.words_path = words_path
self.salutations_path = salutations_path
# 初始化组件
self.mtl = None
self.tok = None
self.ner = None
# 设置环境变量
self._set_environment_variables()
# 加载模型和词典
self._load_model()
self._load_dictionaries()
def _set_environment_variables(self):
"""设置必要的环境变量"""
try:
os.environ['HANLP_HOME'] = self.hanlp_home
os.environ['HF_HOME'] = self.hf_home
os.environ['HF_ENDPOINT'] = self.hf_endpoint
os.environ['HANLP_URL'] = self.hanlp_url
logger.info("环境变量设置成功")
except Exception as e:
logger.error(f"设置环境变量失败: {e}")
def _load_dict_from_file(self, file_path, default_tag='S-PERSON'):
"""从文件加载词典"""
dict_tags = {}
try:
with open(file_path, 'r', encoding='utf-8') as f:
for word in f:
word = word.strip()
if word: # 跳过空行
dict_tags[(word,)] = (default_tag,) # 保持元组形式
except Exception as e:
print(f"加载词典文件{file_path}失败: {e}")
return dict_tags
def _load_names(self, surnames_path, titles_path):
"""加载姓氏和头衔,生成姓名组合"""
try:
with open(surnames_path, 'r', encoding='utf-8') as f:
surnames = [line.strip() for line in f if line.strip()]
with open(titles_path, 'r', encoding='utf-8') as f:
titles = [line.strip() for line in f if line.strip()]
# 使用生成器表达式(节省内存)
full_names = (f'{surname}{title}' for surname in surnames for title in titles)
return full_names
except Exception as e:
print(f"加载姓名文件失败: {e}")
return []
def _load_model(self):
"""加载HanLP模型"""
try:
# CLOSE是自然语义标注的闭源语料库,BASE是中号模型,ZH中文
logger.info("开始加载HanLP模型...")
self.mtl = hanlp.load(hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH)
self.tok = self.mtl['tok/coarse']
self.tok.dict_force = self.tok.dict_combine = None
self.ner = self.mtl['ner/msra']
logger.info("模型加载成功")
except Exception as e:
logger.error(f"加载模型失败: {e}")
self.mtl = None
self.tok = None
self.ner = None
def _load_dictionaries(self):
"""加载各种词典"""
try:
# 加载称呼词典
self.ner.dict_tags = self._load_dict_from_file(self.salutations_path)
# 读取专有名词词典
with open(self.words_path, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
# 生成所有姓名组合
full_names = self._load_names(self.surnames_path, self.titles_path)
# 构建强制分词词典(使用进度条)
dict_force = {}
for name in tqdm(full_names, desc='读取人名词典'):
dict_force[name] = [name]
for name in names:
dict_force[name] = [name]
self.tok.dict_force = dict_force
except Exception as e:
print(f"加载词典失败: {e}")
def get_ner_json(self, text):
"""识别文本中的实体并返回JSON格式"""
if not text or not isinstance(text, str):
logger.warning("无效的输入文本")
return json.dumps([])
try:
if not self.mtl:
raise RuntimeError("模型未加载")
result = self.mtl(text, tasks=['tok/coarse', 'ner/msra'], skip_tasks=['tok/fine'])
# 检查结果格式
if 'ner/msra' not in result or not isinstance(result['ner/msra'], (list, tuple)):
logger.warning("模型返回结果格式异常")
return json.dumps([])
json_data = []
for item in result['ner/msra']:
# 确保item是正确的格式
if isinstance(item, (list, tuple)) and len(item) >= 4:
json_data.append({
'Text': str(item[0]),
'Type': str(item[1]),
'Start': int(item[2]),
'End': int(item[3])
})
return json.dumps(json_data, ensure_ascii=False)
except Exception as e:
logger.error(f"实体识别失败: {e}")
return json.dumps([])
def get_srl_json(self, text):
"""识别文本中的人物角色并返回JSON格式"""
if not text or not isinstance(text, str):
logger.warning("无效的输入文本")
return json.dumps([])
try:
if not self.mtl:
raise RuntimeError("模型未加载")
results = self.mtl(text, tasks=['tok/coarse', 'srl'], skip_tasks=['tok/fine'])
result = []
# 检查结果格式
if 'srl' not in results:
logger.warning("模型返回结果中未找到SRL数据")
return json.dumps([])
data = results['srl']
if not data:
return json.dumps(result)
for sentence in data:
if not isinstance(sentence, (list, tuple)):
continue
sentence_data = []
for item in sentence:
if not isinstance(item, (list, tuple)):
continue
# 确保至少有2个元素
if len(item) < 2:
continue
entry = {
'Text': str(item[0]),
'Type': str(item[1]),
'Start': int(item[2]) if len(item) > 2 else -1,
'End': int(item[3]) if len(item) > 3 else -1
}
sentence_data.append(entry)
if sentence_data: # 只添加非空的句子数据
result.append(sentence_data)
return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"人物角色识别失败: {e}")
return json.dumps([])
def get_con(self, text):
"""解析文本的句法结构并返回结果"""
if not text or not isinstance(text, str):
logger.warning("无效的输入文本")
return None
try:
if not self.mtl:
raise RuntimeError("模型未加载")
logger.info("开始句法分析...")
result = self.mtl(text, tasks=['con'])
logger.info("句法分析完成")
return result
except Exception as e:
logger.error(f"句法分析失败: {e}")
return None
def get_all(self,text):
result = self.mtl(text)
return result;
def get_con_json(self, text):
"""解析文本的句法结构并返回JSON格式字符串,便于C#对象处理"""
# 重置ID计数器和父ID,确保每次调用都从1开始
self.item_id_counter = 0
self.current_father_id = 0
# 初始化JSON数组用于存储所有解析结果元素
result_array = []
# 保存结果数组的引用,供packLabel方法使用
self.result_array = result_array
if not text or not isinstance(text, str):
logger.warning("无效的输入文本")
return json.dumps([])
if not self.mtl:
raise RuntimeError("模型未加载")
logger.info("开始句法分析...")
results = self.mtl(text, tasks=['con'])
logger.info("句法分析完成")
# 检查结果格式
if 'con' not in results:
logger.warning("模型返回结果中未找到CON数据")
return json.dumps([])
#从results中截取"con": [截取字符串]}
# 将results转为字符串并提取con字段值
results_str = str(results)
# logger.info(results_str)
# 只取出top的内容
new_results = self.get_con_top('"con": [',results_str)
level=0
self.item_id_counter += 1
# 调用pack_childen处理所有节点,所有生成的元素将通过packLabel方法添加到result_array
try:
self.pack_childen(new_results, level)
except Exception as e:
logger.warning(f"pack_childen处理时出错: {e}")
# 清理引用,避免内存泄漏
delattr(self, 'result_array')
# 将结果数组转换为JSON字符串返回,便于C#交互
return json.dumps(result_array, ensure_ascii=False)
def pack_childen(self, text, level, father_id=None):
# 如果没有提供father_id,则使用当前类的current_father_id
if father_id is None:
father_id = self.current_father_id
childen_json = []
json_temp = self.split(text, ',')
# 去除前后的[]
childen = self.substring_before_after_one(json_temp[1])
# 分隔元素
childens = self.split(childen, ',')
label = json_temp[0]
if json_temp[0] == '_':
# "_", ["二愣子"] - 提取列表中的值
# 当标签为"_"时,提取childen中的值并直接返回值字符串
label = self.substring_before_after_one(childen)
# 保存当前的current_father_id
current_father = self.current_father_id
# 设置当前元素的父ID
self.current_father_id = father_id
# 创建节点
top_json = self.packLabel(label, level, 0)
# 恢复原来的current_father_id
self.current_father_id = current_father
# logger.info(top_json)
return
# 保存当前的current_father_id
current_father = self.current_father_id
# 设置当前元素的父ID
self.current_father_id = father_id
# 创建当前节点
top_json = self.packLabel(label, level, len(childens))
# 获取当前节点的ID作为子节点的父ID
current_node_id = top_json['ItemId']
# 恢复原来的current_father_id
self.current_father_id = current_father
# logger.info(top_json)
# 递归处理子节点,传递当前节点的ID作为父ID
for i, item in enumerate(childens):
childen_temp = self.substring_before_after_one(item)
childen_json.append(self.pack_childen(childen_temp, level + 1, current_node_id))
def get_task(self, text, task):
'''
执行指定的任务并跳过fine分词
参数:
text: 输入文本
task: 要执行的任务名称
返回:
任务执行结果
'''
if not text or not isinstance(text, str):
logger.warning('无效的输入文本')
return None
try:
if not self.mtl:
raise RuntimeError('模型未加载')
# 跳过tok/fine任务
result = self.mtl(text, tasks=[task])
return result
except Exception as e:
logger.error(f'执行任务{task}失败: {e}')
return None
def get_con_top(self,from_text,text):
if '"con": [' in text:
start = text.find(from_text) + len(from_text)
# 寻找对应的结束括号
end = start
bracket_count = 1
while end < len(text) and bracket_count > 0:
if text[end] == '[':
bracket_count += 1
elif text[end] == ']':
bracket_count -= 1
end += 1
new_results = text[start:end-1] # -1 是为了去掉最后的']'
else:
new_results = ''
return new_results
def substring_before_after_one(self, text):
if not text or not isinstance(text, str):
return text
if len(text) < 2:
return text
return text[1:-1]
def split(self, text, char):
if not text:
return []
result = []
current = []
in_quotes = False # 是否在引号内
bracket_level = 0 # 括号嵌套级别
i = 0
while i < len(text):
c = text[i]
# 处理引号
if c == '"':
in_quotes = not in_quotes
current.append(c)
# 处理括号
elif c == '[' and not in_quotes:
bracket_level += 1
current.append(c)
elif c == ']' and not in_quotes:
if bracket_level > 0:
bracket_level -= 1
current.append(c)
# 遇到分隔符且不在括号内和引号内
elif c == char and bracket_level == 0 and not in_quotes:
# 添加当前部分到结果
part = ''.join(current).strip()
# 清理引号(如果有)
if part.startswith('"') and part.endswith('"'):
part = part[1:-1]
result.append(part)
current = []
# 处理转义字符
elif c == '\\' and i + 1 < len(text):
current.append(c)
current.append(text[i + 1])
i += 1
else:
current.append(c)
i += 1
# 处理最后一个元素
if current:
part = ''.join(current).strip()
# 清理引号(如果有)
if part.startswith('"') and part.endswith('"'):
part = part[1:-1]
result.append(part)
return result
def packLabel(self, label, level, children):
# 保存当前ID作为返回值
current_id = self.item_id_counter
# 创建包含ID和父ID的字典
result = {
"ItemId": current_id,
"FatherId": self.current_father_id,
"Label": label,
"Level": level,
"Children": children
}
# 将生成的元素添加到result_array(如果存在)
if hasattr(self, 'result_array'):
self.result_array.append(result)
# 更新计数器
self.item_id_counter += 1
return result
def extract_underscore_value(self, text):
"""
从"_", ["值"]格式的文本中提取值,并直接返回字符串值
Args:
text: 格式为"_", ["值"]的字符串
Returns:
提取的字符串值
"""
# 分割文本以获取值部分
parts = self.split(text, ',')
if len(parts) < 2:
return ""
# 获取第二个部分(包含值的列表)
value_part = parts[1]
# 去除前后的[]和可能的引号
value = self.substring_before_after_one(value_part).strip('"')
# 直接返回提取的值
return value
processor=HanLPProcessor()
# 测试split函数
def test_split_function():
# 测试用例1: 简单的逗号分隔
text1 = ' "TOP",[["IP", [["NP", [["_", ["二愣子"]]]], ["VP", [["VP", [["_", ["姓"]], ["NP", [["_", ["韩"]]]]]], ["VP", [["_", ["名"]], ["NP", [["_", ["立"]]]]]]]], ["_", ["。"]]]]]'
result1 = processor.split(text1, ',')
print("测试用例1结果:")
for i, item in enumerate(result1):
print(f" {i}: {item}")
text1='二愣子姓韩名立。'
print(processor.get_con_json(text1))
# 测试用例2: 括号内包含逗号
# text2 = '"NP",[["NN",["_", "张三"]],["NN",["_", "李四"]]]'
# result2 = processor.split(text2, ',')
# print("\n测试用例2结果:")
def test_extract_underscore_value():
"""
测试新添加的extract_underscore_value函数
"""
print("\n测试extract_underscore_value函数:")
# 测试用例1: 基本格式
test_text1 = '"_", ["二愣子"]'
result1 = processor.extract_underscore_value(test_text1)
print(f"输入: {test_text1}")
print(f"输出: {result1}")
# 测试用例2: 其他值
test_text2 = '"_", ["张三"]'
result2 = processor.extract_underscore_value(test_text2)
print(f"\n输入: {test_text2}")
print(f"输出: {result2}")
# 测试用例3: 空值情况
test_text3 = '"_", []'
result3 = processor.extract_underscore_value(test_text3)
print(f"\n输入: {test_text3}")
print(f"输出: {result3}")
# 测试修复后的pack_childen方法对"_", ["二愣子"]格式的处理
def test_pack_childen_with_underscore():
"""
测试pack_childen方法对"_", ["值"]格式的处理
"""
print("\n测试pack_childen方法对'_', ['二愣子']格式的处理:")
# 测试用例:直接调用pack_childen处理"_", ["二愣子"]格式
test_input = '"_", ["二愣子"]'
print(f"输入: {test_input}")
try:
# 调用pack_childen方法,使用level=0
result = processor.pack_childen(test_input, 0)
# 由于pack_childen在处理"_"标签时会直接返回,我们需要查看logger输出
print("测试完成,请检查日志输出")
except Exception as e:
print(f"测试出错: {e}")
# for i, item in enumerate(result2):
# print(f" {i}: {item}")
# 如果直接运行此文件,则执行测试
def test_id_generation():
"""
测试ItemId和FatherId的生成逻辑
"""
print("开始测试ID生成...")
# 创建处理器实例
processor = HanLPProcessor()
# 测试基本的packLabel方法
print("\n测试基本的packLabel方法:")
result1 = processor.packLabel("TOP", 0, 1)
print(result1)
# 测试生成第二个节点,应该有正确的父子关系
print("\n测试生成第二个节点:")
# 对于第二个节点,我们手动设置它的父ID为第一个节点的ID
processor.current_father_id = result1['ItemId']
result2 = processor.packLabel("TOP", 0, 1)
print(result2)
# 测试pack_childen方法
print("\n测试pack_childen方法:")
# 重置处理器的ID计数器
processor.item_id_counter = 1
processor.current_father_id = 0
# 使用一个简单的测试字符串来模拟解析结果,确保格式正确
test_input = "TOP, [IP, [NP, [NN, 你]], [VP, [VV, 好]]]"
try:
processor.pack_childen(test_input, 0)
print("pack_childen测试成功")
except Exception as e:
print(f"pack_childen测试失败: {e}")
# 为了演示,我们可以直接测试packLabel的组合
print("\n直接测试ID和父子关系:")
processor.item_id_counter = 1
processor.current_father_id = 0
# 模拟用户要求的输出格式
result1 = processor.packLabel("TOP", 0, 1)
print(f"{{'ItemId':{result1['ItemId']},'FatherId':{result1['FatherId']},'Label': '{result1['Label']}', 'Level': {result1['Level']}, 'Children': {result1['Children']}}}")
# 第二个节点的父ID是第一个节点的ID
processor.current_father_id = result1['ItemId']
result2 = processor.packLabel("TOP", 0, 1)
print(f"{{'ItemId':{result2['ItemId']},'FatherId':{result2['FatherId']},'Label': '{result2['Label']}', 'Level': {result2['Level']}, 'Children': {result2['Children']}}}")
print("\nID生成测试完成")
def test_con_json():
"""
测试get_con_json方法返回的JSON格式字符串
"""
print("\n开始测试get_con_json方法...")
processor = HanLPProcessor()
# 测试空输入
empty_result = processor.get_con_json("")
print(f"空输入测试: {empty_result}")
print(f"空输入结果类型: {type(empty_result)}")
# 验证可以解析为空数组
try:
parsed_empty = json.loads(empty_result)
print(f"空输入解析结果: {parsed_empty}, 类型: {type(parsed_empty)}")
except json.JSONDecodeError as e:
print(f"空输入JSON解析失败: {e}")
# 测试None输入
none_result = processor.get_con_json(None)
print(f"None输入测试: {none_result}")
print(f"None输入结果类型: {type(none_result)}")
# 验证可以解析为空数组
try:
parsed_none = json.loads(none_result)
print(f"None输入解析结果: {parsed_none}, 类型: {type(parsed_none)}")
except json.JSONDecodeError as e:
print(f"None输入JSON解析失败: {e}")
# 尝试测试有效输入(注意:由于可能没有加载模型,这里可能会抛出异常)
try:
# 尝试简单的中文文本
text = "二愣子姓韩名立。"
print(f"测试有效输入: {text}")
result = processor.get_con_json(text)
print(f"返回结果类型: {type(result)}")
print(f"返回结果字符串长度: {len(result)}")
# 验证返回的是字符串
assert isinstance(result, str), "返回结果应该是字符串类型"
print("✓ 验证通过:返回结果是字符串类型")
# 尝试解析JSON字符串
try:
parsed_result = json.loads(result)
print(f"✓ JSON解析成功,解析后类型: {type(parsed_result)}")
print(f"解析后数组长度: {len(parsed_result)}")
# 如果解析成功且有元素,显示部分内容
if parsed_result:
print(f"解析后第一个元素: {parsed_result[0]}")
if len(parsed_result) > 1:
print(f"解析后第二个元素: {parsed_result[1]}")
except json.JSONDecodeError as e:
print(f"✗ JSON解析失败: {e}")
print(f"完整返回结果: {result}")
except Exception as e:
print(f"有效输入测试出错(可能是模型未加载): {e}")
print("get_con_json方法测试完成")
if __name__ == "__main__":
# test_split_function()
# test_id_generation()
# test_extract_underscore_value()
# test_pack_childen_with_underscore()
test_con_json()
# processor=HanLPProcessor()
*/
+88
View File
@@ -0,0 +1,88 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using Python.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Python
{
public class PythonCommand
{
private bool _disposed = false; // 释放标记
public dynamic PythonCreateScope { get; set; }
public IApplicationService ApplicationService;
public PythonCommand(IApplicationService applicationService)
{
ApplicationService = applicationService;
Runtime.PythonDLL = applicationService.Configuration["Python:PythonCommand"];
PythonEngine.PythonHome = Path.GetDirectoryName(Runtime.PythonDLL);
PythonEngine.Initialize();
PythonCreateScope = Py.CreateScope();
}
public void Exec(string arg)
{
using (Py.GIL())
{
PythonCreateScope.Exec(arg);//"from pyhanlp import HanLP"
}
}
public string GetValueExec(string arg)
{
using (Py.GIL())
{
PythonCreateScope.Exec(string.Format("s_value={0}", arg));
var value = PythonCreateScope.Eval("s_value");
return value.ToString();
}
}
public string[] GetValuesExec(string arg)
{
using (Py.GIL())
{
PythonCreateScope.Exec(string.Format("s_list={0}", arg));
var value = PythonCreateScope.Eval("s_list");
string[] st = value.As<string[]>();
return st;
}
}
// 核心:释放 Python.NET 资源
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 阻止析构函数重复执行
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
// 1. 释放 Python 作用域
if (PythonCreateScope != null)
{
PythonCreateScope.Dispose();
PythonCreateScope = null;
}
// 2. 关闭 Python 运行时(关键!)
if (PythonEngine.IsInitialized)
{
PythonEngine.Shutdown(); // 销毁 Python 解释器
}
_disposed = true;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
static void Main()
{
// 1. 生成 U+4E00 ~ U+9FA5 完整汉字合集
List<char> cjkBasicChars = new List<char>();
for (int code = 0x4E00; code <= 0x9FA5; code++)
{
cjkBasicChars.Add((char)code);
}
// 验证数量(应输出 20976)
Console.WriteLine($"基础区汉字总数:{cjkBasicChars.Count}");
// 2. 输出前 10 个汉字(验证生成结果)
Console.WriteLine("前 10 个汉字:");
for (int i = 0; i < 10; i++)
{
Console.Write(cjkBasicChars[i] + " ");
}
// 输出:一 丁 丂 七 丄 丅 丆 万 丈 三
// 3. 导出为文本文件(UTF-8 编码,便于查看/使用)
string allChars = new string(cjkBasicChars.ToArray());
File.WriteAllText("CJK_Basic_Chars.txt", allChars, System.Text.Encoding.UTF8);
Console.WriteLine("\n已导出完整汉字集到 CJK_Basic_Chars.txt");
}
+65
View File
@@ -0,0 +1,65 @@
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CloudBuilder.Topshelf.Task.AI
{
//task:CharacterInsertTask encoding:GB2312 file_path:D:\NET8\AI\Novel.txt
public class CharacterInsertTask : IScheduleTask
{
private readonly IApplicationService service;
public CharacterInsertTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
IAiCharacterService aiCharacterService = service.ServiceProvider.GetService<IAiCharacterService>();
string encoding = "utf-8";
if (bodyDict.ContainsKey("encoding"))
encoding = bodyDict["encoding"];
else
Console.WriteLine("文件字符集"+ encoding);
string fileName = null;
if (bodyDict.ContainsKey("encoding"))
fileName = bodyDict["file_path"];
else
Console.WriteLine("文件路径不存在");
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
string content = File.ReadAllText(fileName, Encoding.GetEncoding(encoding));
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(60 * 10)
};
using (TransactionScope _transactionScope = new TransactionScope(
TransactionScopeOption.Required,
transactionOptions,
TransactionScopeAsyncFlowOption.Enabled))
{
aiCharacterService.Insert(content);
_transactionScope.Complete();
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using EdgeTtsSharp;
using EdgeTtsSharp.NAudio;
using EdgeTtsSharp.Structures;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.AI
{
//task:CharacterTtsTask save_path:D:\NET8\CloudBuilder.Topshelf\mp3 short_name:zh-CN-XiaoxiaoNeural
public class CharacterTtsTask : IScheduleTask
{
private readonly IApplicationService service;
public CharacterTtsTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
IAiCharacterService aiService = service.ServiceProvider.GetService<IAiCharacterService>();
AiCharacterEntity[] aiCharacters = aiService.FindAll();
string shortName = "zh-CN-XiaoxiaoNeural";
string savePath = string.Empty;
if (bodyDict.ContainsKey("short_name"))
shortName = bodyDict["short_name"];
if (bodyDict.ContainsKey("save_path"))
savePath = bodyDict["save_path"];
int totalLines = aiCharacters.Length;
int processedLines = 0; // 已处理行数计数器
int totalValidWords = 0; // 可选:统计有效词总数,用于进度详情
foreach (var c in aiCharacters)
{
try
{
processedLines++;
TtsAsync(shortName, savePath, c).GetAwaiter().GetResult();
UpdateProgress(processedLines, totalLines, totalValidWords);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
public async System.Threading.Tasks.Task TtsAsync(string shortName, string savePath, AiCharacterEntity aiCharacter)
{
savePath = Path.Combine(savePath, string.Format("{0}.mp3", aiCharacter.Content));
if (File.Exists(savePath)) return;
var voice = await EdgeTts.GetVoice(shortName);
await voice.SaveAudioToFile(aiCharacter.Content, savePath);
}
private static void UpdateProgress(int processed, int total, int validWords)
{
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();
}
}
}
+102
View File
@@ -0,0 +1,102 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Python;
using CloudBuilder.Topshelf.Task.Novel.Process;
using static System.Net.Mime.MediaTypeNames;
namespace CloudBuilder.Topshelf.Task
{
//task:ChineseNameExtractorTask guid:B000006
public class ChineseNameExtractorTask : IScheduleTask
{
private readonly IApplicationService service;
public ChineseNameExtractorTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
if (!bodyDict.ContainsKey("guid")) return;
string guid = bodyDict["guid"];
PythonCommand python = new PythonCommand(service);
try
{
Hanlp2 hanlp = new Hanlp2(python);
IRepository<AiSentenceDialogueViewEntity> repository = service.GetRepository<IRepository<AiSentenceDialogueViewEntity>>();
IRepository<AiSentenceEntity> repositoryAiSentenceEntity = service.GetRepository<IRepository<AiSentenceEntity>>();
string depRoot;
List<NamedEntity> ners;
List<List<MeaningRepresentationParsingEntity>> srls;
List<HanlpConstituencyNode> cons = null;
SpeakerAnalysisHelper speakerFeatureLearner = new SpeakerAnalysisHelper();
AiSentenceDialogueViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookGuid == guid).OrderBy(x => x.ChapterIndex).ThenBy(x => x.ParagraphIndex).ThenBy(x => x.DialogueIndc).ToArray();
AiSentenceEntity aiSentenceEntity;
string person = null;
int paragraphIndex = 0;
int totalLines = ents.Length;
int processedLines = 0; // 已处理行数计数器
foreach (var ent in ents)
{
processedLines++;
if (!string.IsNullOrEmpty(person) && paragraphIndex == ent.ParagraphIndex)
{
paragraphIndex = 0;
aiSentenceEntity = repositoryAiSentenceEntity.Entities.Where(x => x.Guid == ent.Guid).FirstOrDefault();
aiSentenceEntity.PersonName = person;
repositoryAiSentenceEntity.UpdateNow(aiSentenceEntity);
continue;
}
depRoot = hanlp.GetDepRoot(ent.Content);
ners = hanlp.GetHanlpNer(ent.Content);
srls = hanlp.GetHanlpSrl(ent.Content);
person = speakerFeatureLearner.GetPerson(depRoot, ners, srls, cons);
paragraphIndex = ent.ParagraphIndex;
UpdateProgress(processedLines, totalLines);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
python.Dispose();
}
}
private 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();
}
}
}
+182
View File
@@ -0,0 +1,182 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Python;
using CloudBuilder.Topshelf.Utility;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Transactions;
namespace CloudBuilder.Topshelf.Task.AI
{
//task:NovelCutWordTask encoding:gb2312 file_path:D:\NET8\AI\12Novel.txt
public class NovelCutWordTask : IScheduleTask
{
private static readonly Regex _noNumberLetterRegex = new Regex(@"^(?!.*[0-9a-zA-Z]).+$", RegexOptions.Compiled);
private readonly Regex _hasPunctuationRegex = new Regex(@"[,。!?;:""''()()【】《》、·¥…—\+\-*/<>==~@#¥%&*()_+{}|:<>?`\-=[\];\\,./{}~!@#¥%……&*()——+|{}】‘;:”“’。,、?·。"'`|〃〔〕〈〉「」『』.〖〗[]{}]",
RegexOptions.Compiled);
private readonly IApplicationService service;
public NovelCutWordTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
PythonCommand python = new PythonCommand(service);
Hanlp2 hanlp = new Hanlp2(python);
IAiWordService aiService = service.ServiceProvider.GetService<IAiWordService>();
AiWordEntity[] aiWords = aiService.FindAll();
string encoding = "utf-8";
if (bodyDict.ContainsKey("encoding"))
encoding = bodyDict["encoding"];
else
Console.WriteLine("文件字符集" + encoding);
string fileName = null;
if (bodyDict.ContainsKey("encoding"))
fileName = bodyDict["file_path"];
else
Console.WriteLine("文件路径不存在");
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
string[] lines = File.ReadAllLines(fileName, Encoding.GetEncoding(encoding));
int totalLines = lines.Length;
int processedLines = 0; // 已处理行数计数器
int totalValidWords = 0; // 可选:统计有效词总数,用于进度详情
Console.WriteLine($"开始处理文件:{fileName}");
Console.WriteLine($"文件总行数:{totalLines}\n");
SegmenterHelper segmenterHelper = new SegmenterHelper();
IEnumerable<string> words;
List<string> contents = new List<string>();
List<string> existingContents = new List<string>();
if (aiWords != null && aiWords.Length > 0)
existingContents = aiWords.Select(x => x.Content).ToList();
foreach (string line in lines)
{
processedLines++; // 已处理行数+1
if (string.IsNullOrEmpty(line.Trim()))
{
// 空行也计入进度,更新进度显示
UpdateProgress(processedLines, totalLines, totalValidWords);
continue;
}
if (string.IsNullOrEmpty(line.Trim())) continue;
try
{
words = segmenterHelper.Cut(line, hanlp);
}
catch (Exception ex)
{
Console.Write($"\r{ex.Message}");
continue;
}
if (words == null || words.Count() == 0) continue;
var validDatas = words
.Where(str =>
!string.IsNullOrWhiteSpace(str) // 过滤空/空白字符串
&& str.Length > 1 // 过滤长度≤1的字符串
&& _noNumberLetterRegex.IsMatch(str) // 过滤含数字/字母的字符串
&& !str.StartsWith("第") // 过滤以“第”开头的字符串
&& !_hasPunctuationRegex.IsMatch(str) // 核心新增:过滤含任意标点的字符串
)
.Distinct(StringComparer.OrdinalIgnoreCase) // 数组内去重(不区分大小写)
.ToList();
// 无有效数据则直接返回
if (!validDatas.Any()) continue;
var newDatas = validDatas
.Where(str => !existingContents.Contains(str))
.ToList();
// 6. 批量插入(仅当有新数据时执行)
if (!newDatas.Any())
{
UpdateProgress(processedLines, totalLines, totalValidWords);
continue;
}
contents.AddRange(newDatas);
contents = contents
.Where(str => !string.IsNullOrWhiteSpace(str)) // 过滤无效字符串
.Distinct(StringComparer.OrdinalIgnoreCase) // 数组内去重(不区分大小写)
.ToList();
totalValidWords = contents.Count; // 更新有效词总数
// 3. 实时更新进度(每行处理完后刷新)
UpdateProgress(processedLines, totalLines, totalValidWords);
}
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(60 * 10)
};
try
{
python.Dispose();
}
catch (Exception)
{
}
if (contents.Count() == 0) return;
using (TransactionScope _transactionScope = new TransactionScope(
TransactionScopeOption.Required,
transactionOptions,
TransactionScopeAsyncFlowOption.Enabled))
{
aiService.Insert(contents.ToArray());
_transactionScope.Complete();
}
}
private static void UpdateProgress(int processed, int total, int validWords)
{
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} 行 | 有效词数:{validWords}";
Console.Write($"\r{progressInfo}");
Console.Out.Flush();
}
}
}
+64
View File
@@ -0,0 +1,64 @@
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace CloudBuilder.Topshelf.Task.AI
{
//task:WordInsertTask encoding:utf-8 file_path:D:\NET8\AI\Novel.txt
public class WordInsertTask : IScheduleTask
{
private readonly IApplicationService service;
public WordInsertTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
IAiWordService aiService = service.ServiceProvider.GetService<IAiWordService>();
string encoding = "utf-8";
if (bodyDict.ContainsKey("encoding"))
encoding = bodyDict["encoding"];
else
Console.WriteLine("文件字符集" + encoding);
string fileName = null;
if (bodyDict.ContainsKey("encoding"))
fileName = bodyDict["file_path"];
else
Console.WriteLine("文件路径不存在");
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
string[] contents = File.ReadAllLines(fileName, Encoding.GetEncoding(encoding));
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(60 * 10)
};
using (TransactionScope _transactionScope = new TransactionScope(
TransactionScopeOption.Required,
transactionOptions,
TransactionScopeAsyncFlowOption.Enabled))
{
aiService.Insert(contents);
_transactionScope.Complete();
}
}
}
}
+136
View File
@@ -0,0 +1,136 @@
using CloudBuilder.AI.Data;
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.Policy;
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Task.Novel.Process;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CloudBuilder.Topshelf.Task.Novel
{
//task:ImportBookTask encoding:GB2312 pattern: @"第([^】]+)章" input:D:\NET8\AI\仙侠\凡人修仙传_忘语.txt
//input可以是文件名:D:\NET8\AI\仙侠\书名_作者.txt
//input也可以是目錄名:D:\NET8\AI\仙侠,必须是小说类型为结尾
//output輸出目錄
//pattern可能自定義分割表達式
public class ImportBookTask : IScheduleTask
{
private readonly IApplicationService service;
public ImportBookTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
if (!bodyDict.ContainsKey("input") || string.IsNullOrEmpty(bodyDict["input"])) return;
string input = bodyDict["input"];
string pattern = @"第([^】]+)章";
if (bodyDict.ContainsKey("pattern") && !string.IsNullOrEmpty(bodyDict["pattern"]))
pattern = bodyDict["pattern"];
string encoding = "UTF-8";
if (bodyDict.ContainsKey("encoding") && !string.IsNullOrEmpty(bodyDict["encoding"])) encoding = bodyDict["encoding"];
List<string> files = PackFiles(input);
foreach (string file in files)
{
ProcessBook(file, encoding, pattern);
}
}
public void ProcessBook(string file, string encoding, string pattern)
{
AiBookData data = new AiBookData();
AiBookEntity book = new AiBookEntity();
book.Guid = Guid.NewGuid().ToString();
string[] temps = Path.GetFileNameWithoutExtension(file).Split('_');
if (temps == null || temps.Length != 2)
{
Console.WriteLine("文件格式要求:书名_作者.txt");
return;
}
book.Title = temps[0];
book.Author = temps[1];
temps = file.Split('\\');
book.BookType = temps[temps.Length - 2];
//book.FilePath = file;
book.Status = BookStatusPolicy.SUBMIT;//S-待提交
ChapterSplitter chapterSplitter = new ChapterSplitter();
data.AiBook = book;
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
string context = File.ReadAllText(file, Encoding.GetEncoding(encoding));
List<AiChapterEntity> aiChapterEntities = chapterSplitter.SplitNovelIntoChapters(context, pattern);
if (aiChapterEntities == null || aiChapterEntities.Count() == 0) return;
aiChapterEntities.ToLookup(x => x.BookGuid = book.Guid);
aiChapterEntities.ToLookup(x => x.Status = ChapterStatusPolicy.PROCESS);
data.AiChapters = aiChapterEntities.ToArray();
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(60 * 10)
};
using (TransactionScope _transactionScope = new TransactionScope(
TransactionScopeOption.Required,
transactionOptions,
TransactionScopeAsyncFlowOption.Enabled))
{
if (data.AiBook != null)
service.ServiceProvider.GetService<IAiBookService>().Insert(data);
_transactionScope.Complete();
}
}
private List<string> PackFiles(string input)
{
List<string> files = new List<string>();
if (Path.HasExtension(input) && !string.IsNullOrEmpty(input))
{
if (File.Exists(input))
{
files.Add(input);
}
}
else if (!string.IsNullOrEmpty(input))
{
if (Directory.Exists(input))
{
string[] tempFiles = Directory.GetFiles(input);
if (tempFiles != null && tempFiles.Length > 0)
{
files.AddRange(tempFiles);
}
}
}
return files;
}
}
}
+96
View File
@@ -0,0 +1,96 @@
using CloudBuilder.AI.Data;
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Task.Novel.Process;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CloudBuilder.Topshelf.Task.Novel
{
//task:ParagraphSplitterTask book_title:凡人修仙传
public class ParagraphSplitterTask : IScheduleTask
{
private readonly IApplicationService service;
public ParagraphSplitterTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
if (!bodyDict.ContainsKey("book_title") || string.IsNullOrEmpty(bodyDict["book_title"])) return;
string title = bodyDict["book_title"];
AiChapterEntity[] aiChapters = service.ServiceProvider.GetService<IAiChapterService>().FindByTitle(title);
if (aiChapters == null || aiChapters.Length == 0) return;
ParagraphSplitter paragraphSplitter = new ParagraphSplitter();
List<AiParagraphEntity> aiParagraphs = new List<AiParagraphEntity>();
List<AiSentenceEntity> aiSentences = new List<AiSentenceEntity>();
AiBookData data;
foreach (var cpt in aiChapters)
{
data = paragraphSplitter.SplitChapterIntoParagraphs(cpt);
if (data.AiParagraphs == null || data.AiParagraphs.Count() == 0) continue;
aiParagraphs.AddRange(data.AiParagraphs);
}
if (aiParagraphs == null || aiParagraphs.Count() == 0) return;
SentenceSplitter sentenceSplitter = new SentenceSplitter();
AiSentenceEntity aiSentence;
foreach (var ph in aiParagraphs)
{
if (ph.DialogueIndc != YesNoPolicy.YES)
{
aiSentence = new AiSentenceEntity();
aiSentence.Guid = Guid.NewGuid().ToString();
aiSentence.Content = ph.Content;
aiSentence.DialogueIndc = YesNoPolicy.NO;
aiSentence.SentenceIndex = 1;
aiSentence.ParagraphGuid = ph.Guid;
aiSentence.Status = "G";
//D-【待确认对话】p-【待确认人物】A-【等待分配角色】G-【生成中】C-【完成】
aiSentences.Add(aiSentence);
continue;
}
var s = sentenceSplitter.SplitByConfigurableQuotes(ph);
if (s == null || s.Count() == 0) continue;
aiSentences.AddRange(s);
}
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(60 * 10)
};
using (TransactionScope _transactionScope = new TransactionScope(
TransactionScopeOption.Required,
transactionOptions,
TransactionScopeAsyncFlowOption.Enabled))
{
service.ServiceProvider.GetService<IAiParagraphService>().Inserts(aiParagraphs.ToArray());
service.ServiceProvider.GetService<IAiSentenceService>().Inserts(aiSentences.ToArray());
_transactionScope.Complete();
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Topshelf.Python;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.Novel.Process
{
/// <summary>
/// 章节分割器
/// 不同的小说有自己的分割方法,可以传入参数
/// </summary>
public class ChapterSplitter
{
public ChapterSplitter()
{
}
/// <summary>
/// 按章节分割小说文本
/// </summary>
public List<AiChapterEntity> SplitNovelIntoChapters(string novelText, string chapterPatterns)
{
var chapters = new List<AiChapterEntity>();
var lines = novelText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var currentAiChapterEntity = new StringBuilder();
var currentAiChapterEntityTitle = "未命名章节";
var chapterCount = 1;
foreach (var line in lines)
{
// 检查是否是章节标题
if (IsChapterTitle(line, chapterPatterns))
{
// 如果当前章节有内容,则保存
if (currentAiChapterEntity.Length > 0)
{
chapters.Add(new AiChapterEntity
{
Title = currentAiChapterEntityTitle,
Guid = Guid.NewGuid().ToString(),
Content = currentAiChapterEntity.ToString(),
ChapterIndex = chapterCount
});
currentAiChapterEntity.Clear();
chapterCount++;
}
// 更新章节标题
currentAiChapterEntityTitle = line.Trim();
// 添加章节标题
currentAiChapterEntity.AppendLine(line.Trim());
// 添加一个空行作为分隔,确保章节标题和正文内容分离
currentAiChapterEntity.AppendLine();
}
else
{
// 添加到当前章节,确保每行独立
currentAiChapterEntity.AppendLine(line);
}
}
// 添加最后一个章节
if (currentAiChapterEntity.Length > 0)
{
chapters.Add(new AiChapterEntity
{
Title = currentAiChapterEntityTitle,
Content = currentAiChapterEntity.ToString(),
Guid = Guid.NewGuid().ToString(),
ChapterIndex = chapterCount
});
}
return chapters;
}
/// <summary>
/// 检查是否为章节标题
/// </summary>
private bool IsChapterTitle(string line, string cptPattern)
{
// 去除首尾空白字符
var trimmedLine = line.Trim();
// 常见的章节标题模式
var chapterPatterns = new[]
{
@"^第\s*[\u4e00-\u9fa50-9]+\s*章", // 第X章, 第XX章等
@"^第\s*[0-9]+\s*章", // 第1章, 第2章等
};
if (!string.IsNullOrEmpty(cptPattern))
{
Array.Resize(ref chapterPatterns, chapterPatterns.Length + 1);
chapterPatterns[chapterPatterns.Length - 1] = cptPattern;
}
foreach (var pattern in chapterPatterns)
{
if (Regex.IsMatch(trimmedLine, pattern, RegexOptions.IgnoreCase))
{
//第一章内容说明了什么,请描述一下:
if (trimmedLine.Length > 20) return false;
return true;
}
}
return false;
}
/// <summary>
/// 保存章节到单独的文件
/// </summary>
public void SaveChaptersToFiles(List<AiChapterEntity> chapters, string outputDirectory)
{
// 确保输出目录存在
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}
foreach (var chapter in chapters)
{
// 使用章节标题作为文件名,清理非法字符
var fileName = CleanFileName(chapter.Title) + ".txt";
var filePath = Path.Combine(outputDirectory, fileName);
File.WriteAllText(filePath, chapter.Content, Encoding.UTF8);
}
}
/// <summary>
/// 清理文件名中的非法字符
/// </summary>
private string CleanFileName(string fileName)
{
var invalidChars = Path.GetInvalidFileNameChars();
foreach (var invalidChar in invalidChars)
{
fileName = fileName.Replace(invalidChar, '_');
}
// 限制文件名长度
if (fileName.Length > 100)
{
fileName = fileName.Substring(0, 100);
}
return fileName;
}
}
}
+63
View File
@@ -0,0 +1,63 @@
using CloudBuilder.AI.Data;
using CloudBuilder.AI.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.Novel.Process
{
public class ParagraphSplitter
{
public ParagraphSplitter()
{
}
public AiBookData SplitChapterIntoParagraphs(AiChapterEntity aiChapter)
{
AiBookData data = new AiBookData();
var paragraphs = new List<AiParagraphEntity>();
var sentences = new List<AiSentenceEntity>();
if (string.IsNullOrWhiteSpace(aiChapter.Content)) return data;
// 按换行拆分段落(过滤空段落)
var paragraphTexts = aiChapter.Content.Split("\r\n")
.Select(p => p.Trim())
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToList();
SentenceSplitter sentenceSplitter = new SentenceSplitter();
AiParagraphEntity aiParagraph;
int index = 1;
foreach (var text in paragraphTexts)
{
if (!ValidText(text)) continue;
aiParagraph = new AiParagraphEntity();
aiParagraph.Content = text;
aiParagraph.ChapterGuid = aiChapter.Guid;
aiParagraph.Guid = Guid.NewGuid().ToString();
aiParagraph.ParagraphIndex = index++;
aiParagraph.DialogueIndc = sentenceSplitter.IsDialogue(text) ? YesNoPolicy.YES : YesNoPolicy.NO;
paragraphs.Add(aiParagraph);
}
data.AiParagraphs = paragraphs.ToArray();
data.AiSentences = sentences.ToArray();
return data;
}
private bool ValidText(string text)
{
if (string.IsNullOrWhiteSpace(text.Trim())) return false;
return true;
}
}
}
+199
View File
@@ -0,0 +1,199 @@
using CloudBuilder.AI.Entity;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.FileSystemGlobbing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.Novel.Process
{
/// <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(AiParagraphEntity 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.Guid = Guid.NewGuid().ToString();
aiSentence.Content = prefixText;
aiSentence.DialogueIndc = YesNoPolicy.NO;
aiSentence.SentenceIndex = sentenceIndex++;
aiSentence.ParagraphGuid = aiParagraphEntity.Guid;
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.Guid = Guid.NewGuid().ToString();
aiSentence.Content = quotedContent;
aiSentence.DialogueIndc = YesNoPolicy.YES;
aiSentence.SentenceIndex = sentenceIndex++;
aiSentence.ParagraphGuid = aiParagraphEntity.Guid;
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.Guid = Guid.NewGuid().ToString();
aiSentence.Content = suffixText;
aiSentence.DialogueIndc = YesNoPolicy.NO;
aiSentence.SentenceIndex = sentenceIndex++;
aiSentence.ParagraphGuid = aiParagraphEntity.Guid;
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)
{
// 1. 处理空文本/NULL,直接返回false
if (string.IsNullOrWhiteSpace(text))
return false;
// 2. 文本预处理:去空格、统一全角/半角引号
string cleanedText = text.Trim();
// 替换全角空格为半角,避免干扰判断
cleanedText = Regex.Replace(cleanedText, @" ", " ");
// 统一引号格式(中文弯引号、直引号 → 标准双引号)
cleanedText = cleanedText.Replace("“", "\"")
.Replace("”", "\"")
.Replace("〝", "\"")
.Replace("〞", "\"")
.Replace("「", "\"")
.Replace("」", "\"")
.Replace("‘", "'")
.Replace("’", "'");
// 3. 核心判断规则:
// - 包含成对/单个双引号(排除仅含引号无内容的情况)
// - 包含冒号+引号(如:"XXX说:"XXX"" 场景)
var hasQuote = Regex.IsMatch(cleanedText, @""".+?""") || // 成对双引号(含内容)
Regex.IsMatch(cleanedText, @":\s*"""); // 冒号后接引号(说话人+对话)
var hasSingleQuote = Regex.IsMatch(cleanedText, @"'[^']+'"); // 成对单引号(英文对话)
// 4. 排除仅含引号符号的无效情况(如 ""、''、“”)
var onlyQuote = Regex.IsMatch(cleanedText, @"^\s*[""''“”]{1,2}\s*$");
// 最终判断:包含有效引号且不是仅含引号 → 判定为对话
return (hasQuote || hasSingleQuote) && !onlyQuote;
}
}
}
+102
View File
@@ -0,0 +1,102 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.Topshelf.Python;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.Novel.Process
{
public class SpeakerAnalysisHelper
{
public string GetPerson(string depRoot, List<NamedEntity> ners, List<List<MeaningRepresentationParsingEntity>> srls, List<HanlpConstituencyNode> cons)
{
string arg0 = string.Empty;
if (!string.IsNullOrEmpty(depRoot) && srls != null && srls.Count() > 0)
{
foreach (var srl in srls)
{
if (srl.Where(x => x.Text == depRoot).Any())
{
foreach (var mrp in srl)
{
if (mrp.Type.ToUpper() != "ARG0") continue;
if (ners != null && ners.Where(x => mrp.Text.Contains(x.Text)).Any() &&
ners.Where(x => mrp.Text.Contains(x.Text)).FirstOrDefault().Text.Length > 1)
return ners.Where(x => mrp.Text.Contains(x.Text)).FirstOrDefault().Text;
return mrp.Text.Length > 6 ? mrp.Text.Substring(mrp.Text.Length - 6, 6) : mrp.Text;
}
}
foreach (var mrp in srl)
{
if (mrp.Type.ToUpper() != "ARG0") continue;
arg0 = mrp.Text.Length > 6 ? mrp.Text.Substring(mrp.Text.Length - 6, 6) : mrp.Text;
}
}
}
if (ners != null && ners.Count() > 0)
return ners.FirstOrDefault().Text;
return arg0;
}
public AiParagraphEntity[] AiParagraphs { get; set; }
public AiSentenceEntity[] AiSentences { get; set; }
public AiChapterPersonEntity[] AiChapterPersons { get; set; }
}
}
/*
“文本解析→人物提取→上下文匹配→结果输出”
graph TD
A[对话行] --> B{规则1:文本特征匹配};
B -->|匹配成功| C[直接赋值说话者];
B -->|匹配失败| D{规则2:紧邻旁白匹配};
D -->|匹配成功| C;
D -->|匹配失败| E{规则3:段落内最近人物};
E -->|匹配成功| C;
E -->|匹配失败| F[赋值默认值+标记异常];
规则 1:文本特征精准匹配(最高优先级)
基于对话内容中的 “专属特征” 直接匹配说话者,避免上下文干扰:
示例规则:
对话内容特征 匹配的说话者
包含 “第二元婴” 韩立
包含 “银月所化” 银月
包含 “本座 / 本尊” 反派角色
落地:维护一个 “特征 - 人物” 映射字典,匹配时优先使用:
规则 2:紧邻旁白匹配(次高优先级)只找对话行 “紧邻的前 1-2 行旁白”,而非整个段落的旁白,避免跨越多个人物导致匹配错误:
规则:仅检查对话行的前 1 行、前 2 行旁白(最多前 3 行),超过则停止;
落地:修改向前查找的逻辑,限制查找范围
规则 3:排除无效人物(避免干扰)从旁白中提取人物时,过滤掉 “非核心人物”(如 “店小二”“路人”),只保留主要人物:
落地:维护 “核心人物白名单”,提取人物时只保留白名单内的名称
3. 增加异常处理和人工校验(兜底保障)
即使逻辑再完善,也会有边缘场景匹配错误,需通过 “异常标记 + 人工复核” 兜底:
步骤 1:标记可疑匹配结果对以下情况标记为 “可疑”,存入日志或单独字段:
匹配到的人物在旁白中出现次数≥2(比如同一段落有韩立和银月,无法确定);
对话行前后 3 行都没有找到人物,最终赋值为 “未知人物”;
对话内容无任何特征,且段落内人物数量≥3;
落地:为实体增加扩展字段(如IsSuspected),或生成异常日志
步骤 2:人工复核可疑数据
规则:所有标记为 “可疑” 的匹配结果,必须经过人工复核后再更新到数据库;
落地:生成 “可疑数据清单”(包含Guid、Content、PersonName),供运营 / 编辑人员校验,修正错误后重新填充。
收集错误案例:记录每次匹配错误的场景(如 “银月的对话匹配给韩立”),分析错误原因;
迭代特征字典:将错误案例转化为新的特征规则(如新增 “乌云”→银月,“元婴”→韩立);
A/B 测试:对优化后的规则做小范围测试,对比匹配准确率,确认有效后全量上线。
*/
+148
View File
@@ -0,0 +1,148 @@
using CloudBuilder.Topshelf.Python;
using Microsoft.VisualBasic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.Intrinsics.Arm;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.Novel.Process
{
public class SpeakerFeatureLearner
{
private readonly List<SpeakerRule> _rules;
public SpeakerFeatureLearner()
{
_rules = new List<SpeakerRule>();
InitializeDefaultRules();
}
private void InitializeDefaultRules()
{
//AddRule("银月", "少妇");
}
public void AutoLearnFromText(string text)
{
}
public void AutoLearnFromText(string depRoot, List<NamedEntity> ners, List<List<MeaningRepresentationParsingEntity>> srls, List<HanlpConstituencyNode> cons)
{
if (ners == null || !ners.Where(x => x.Type.Trim().ToUpper().Contains("PERSON")).Any()) return;
HanlpConstituencyNode[] nodes;
HanlpConstituencyNode nodef;
string feature = null;
foreach (NamedEntity namedEntity in ners.Where(x => x.Type.Trim().ToUpper().Contains("PERSON")).ToArray())
{
nodes = HanlpResultHelper.GetNodeByLabel(cons, namedEntity.Text);
if (nodes == null || nodes.Length == 0) continue;
//如果存在,找到父节点上的NP
foreach (HanlpConstituencyNode node in nodes)
{
nodef = HanlpResultHelper.GetNodeTopLabelById(cons, node.FatherId, "NP");
if (nodef == null) continue;
feature = HanlpResultHelper.GetNodeBottomLabelById(cons, nodef.ItemId, "NP");
if (string.IsNullOrEmpty(feature)) continue;
if (namedEntity.Text == feature) continue;
AddRule(namedEntity.Text, feature);
}
}
//自学创建特征
//NR->NP->NP->IP
//NN->NP(称呼、职业)
//完整汉语句法树库核心标签对照表(含 TOP/ NP / IP / CLP)
//按 “核心层级 / 短语类型” 分类整理,覆盖 90 % 以上高频标注:
//1.顶层 / 句子级标签
//标签 英文全称 中文名称 定义 示例
//TOP Top - level Phrase 顶层短语 句法树的根节点,包含整个句子的最高层级结构 所有句子的根节点均标注为 TOP
//IP Inflectional Phrase / Simple Clause 简单从句 句子级核心短语(含主谓结构),是 TOP 的直接子节点 “我吃饭” 整体标注为 IP
//CP Complement Clause 补语从句 带补语标记(的 / 吗 / 呢)的从句 / 复句成分 “我买的书” 中 “我买的” 标注为 CP
//2.名词类短语
//标签 英文全称 中文名称 定义 示例
//NP Noun Phrase 名词短语 以名词为核心的短语(可含定语 / 量词) “苹果”“我的手机”“三本书”
//DNP XP+DEG Phrase “的” 字短语 由 “XP + 的” 构成的名词性短语 “红色的”“我喜欢的”
//NNP Proper Noun Phrase 专有名词短语 人名 / 地名 / 机构名等专有名词构成的短语 “北京大学”“张三”“北京市”
//3.动词类短语
//标签 英文全称 中文名称 定义 示例
//VP Verb Phrase 动词短语 以动词为核心的短语(可含宾语 / 状语) “吃饭”“快速跑步”“看电影”
//VV Verb Phrase(Verb Core) 动词核心短语 纯动词构成的短语(无附加成分) “跑”“思考”“购买”
//VCP Verb Complement Phrase 动补短语 动词 +补语构成的短语 “吃完”“跑快”“看明白”
//4.数量 / 量词类短语
//标签 英文全称 中文名称 定义 示例
//CLP Classifier Phrase 量词短语 以量词为核心的短语(数词 + 量词 / 指示词 + 量词) “三个”“这本”“那些”
//QP Quantifier Phrase 量化短语 以数量为核心的短语(涵盖数词 / 量词 / 数量整体) “三”“若干”“三本”“许多”
//DP Determiner Phrase 指示词短语 以指示词(这 / 那 / 此)为核心的短语 “这个”“那些”“此问题”
//5.介词 / 方位类短语
//标签 英文全称 中文名称 定义 示例
//PP Prepositional Phrase 介词短语 以介词为核心的短语 “在学校”“关于这件事”“把书”
//LCP Locative Complement Phrase 方位补语短语 含方位词的短语 “桌子上”“房间里”“马路边”
//LOC Locative Phrase 方位短语 纯方位名词构成的短语(LCP 的简化版) “上面”“左边”“内部”
//6.形容词 / 副词类短语
//标签 英文全称 中文名称 定义 示例
//AP Adjective Phrase 形容词短语 以形容词为核心的短语 “漂亮”“非常开心”“红色的”
//ADVP Adverb Phrase 副词短语 以副词为核心的短语 “快速地”“很”“非常”
//7.其他功能类短语
//标签 英文全称 中文名称 定义 示例
//INTJ Interjection Phrase 感叹语短语 感叹词 / 语气词构成的短语 “啊”“哦”“哎呀”
//CONJP Conjunction Phrase 连词短语 连词构成的短语 “和”“但是”“因为”
//PRN Parenthetical Phrase 插入语短语 插入句子的附加成分 “换句话说”“总的来说”
//总结
//CLP 核心是量词短语,以量词为核心(如 “三本”“这本”),是数量表达的核心标注标签;
//TOP / IP 是句子层级标签(根节点 / 核心从句),NP / VP 是核心短语标签(名词 / 动词为核心);
//不同工具(CTB / HanLP / 北大树库)对标签的细分略有差异(如 CLP 是否归为 QP),但核心含义一致,优先以宾州中文树库(CTB)规范为准。
}
public void AddRule(string speaker, params string[] keywords)
{
if (string.IsNullOrWhiteSpace(speaker))
throw new ArgumentException("说话者名称不能为空", nameof(speaker));
if (keywords == null || keywords.Length == 0)
throw new ArgumentException("至少需要提供一个关键词", nameof(keywords));
_rules.Add(new SpeakerRule(speaker, keywords));
}
public string MatchSpeaker(string dialogue)
{
if (string.IsNullOrWhiteSpace(dialogue))
return null;
foreach (var rule in _rules)
{
if (rule.Match(dialogue))
{
return rule.Speaker;
}
}
return dialogue;
}
}
public class SpeakerRule
{
public string Speaker { get; }
public string[] Keywords { get; }
public SpeakerRule(string speaker, string[] keywords)
{
Speaker = speaker;
Keywords = keywords;
}
public bool Match(string dialogue)
{
return Keywords.Any(keyword =>
dialogue.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0);
}
}
}
+239
View File
@@ -0,0 +1,239 @@
using CloudBuilder.AI.Data;
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.SearchCriteria;
using CloudBuilder.AI.Service;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Core.Service;
using CloudBuilder.Security.Entity;
using CloudBuilder.Topshelf.Python;
using CloudBuilder.Topshelf.Task.Novel.Process;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace CloudBuilder.Topshelf.Task.Novel
{
//task:SpeakerTask param_name:param_value
public class SpeakerTask : IScheduleTask
{
private readonly IApplicationService service;
public SpeakerTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
AiSentenceSearchCriteria c = new AiSentenceSearchCriteria();
c.limit = 999999;
List<SearchFilter> filters = new List<SearchFilter>();
SearchFilter filter = new SearchFilter();
filter.Column = AiSentenceViewEntity.DB_CHAPTER_GUID;
filter.Type = SearchFilterTypePolicy.TEXT;
filter.Operator = SearchFilterOperatorPolicy.CONTAIN;
filter.Value = "cf34136e-d30a-4901-9109-626e412eaf57";
filters.Add(filter);
c.Filter = filters.ToArray();
AiSentenceViewResultData data = service.ServiceProvider.GetService<IAiSentenceService>().FindBySearchCriteria(c);
if (data == null || data.Datas == null || data.Datas.Count() == 0) return;
AiSentenceViewEntity[] sentenceViews = data.Datas.OrderBy(x => x.ParagraphIndex).ThenBy(x => x.DialogueIndc).ToArray();
if (sentenceViews == null || sentenceViews.Length == 0) return;
PythonCommand python = new PythonCommand(service);
Hanlp2 hanlp = new Hanlp2(python);
List<NamedSummary> all = GetNamedSummary(hanlp, data.Datas, 0, data.Datas.Length);
string text = string.Empty;
//@"乌云忽然凝聚变形,瞬间化为了一个身高寸许的黑绿婴儿出来,并口吐人言的说道。";
//text = "艳美少妇打了韩立";
//text = "银月嫣然一笑后,敛冲元婴一拜的说道。这元婴看起来除了比韩立主元婴小了一些外,模样神态全都一般无二的样子。";
//List<string> sentiments = hanlp.GetSentiment(text);
SpeakerFeatureLearner speakerFeatureLearner = new SpeakerFeatureLearner();
BuildPersonNameRules(all.ToArray(), speakerFeatureLearner);
AiSentenceViewEntity[] sentenceTemps;
int paragraphIndex = 1;
foreach (var sentence in sentenceViews)
{
sentence.CreatedBy = sentence.UpdatedBy = string.Empty;
if (string.IsNullOrEmpty(sentence.Content)) continue;
text = sentence.Content;
List<NamedEntity> ners = hanlp.GetHanlpNer(text);
List<List<MeaningRepresentationParsingEntity>> srls = hanlp.GetHanlpSrl(text);
List<HanlpConstituencyNode> cons = hanlp.GetHanlpCon(text);
speakerFeatureLearner.AutoLearnFromText("", ners, srls, cons);
//如果不存在人名
if (ners == null || !ners.Where(x => x.Type.Contains("PERSON")).Any())
{
if (sentence.DialogueIndc == YesNoPolicy.NO)
{
continue;
}
foreach (var p in sentenceViews.Where(x => x.ParagraphIndex == sentence.ParagraphIndex).ToArray())
{
if (string.IsNullOrEmpty(p.PersonName)) continue;
sentence.PersonName = p.PersonName;
}
}
string[] persons = ners.Where(x => x.Type.Contains("PERSON")).Select(x => x.Text).Distinct().ToArray();
if (persons == null && persons.Count() == 0) continue;
if (persons != null && persons.Count() == 1)
{
sentence.PersonName = speakerFeatureLearner.MatchSpeaker(persons.FirstOrDefault());
continue;
}
//如果是两个,取ARG0
sentence.PersonName = speakerFeatureLearner.MatchSpeaker(persons.FirstOrDefault());
}
List<NamedSummary> indexRange = GetNamedSummaryBefore(hanlp, data.Datas, 8);
//3字词根据次数划分到2字词内
python.Dispose();
}
public List<NamedSummary> GetNamedSummaryBefore(Hanlp2 hanlp, AiSentenceViewEntity[] datas, int paragraphIndex)
{
return GetNamedSummary(hanlp, datas, paragraphIndex - 3 > 0 ? paragraphIndex - 3 : 0, paragraphIndex);
}
public List<NamedSummary> GetNamedSummaryAfter(Hanlp2 hanlp, AiSentenceViewEntity[] datas, int paragraphIndex)
{
return GetNamedSummary(hanlp, datas, paragraphIndex, paragraphIndex + 3);
}
public List<NamedSummary> GetNamedSummary(Hanlp2 hanlp, AiSentenceViewEntity[] datas, int startIndex, int endIndex)
{
List<NamedSummary> nameds = new List<NamedSummary>();
NamedSummary summary;
List<NamedEntity> temps;
AiSentenceViewEntity sentence;
for (int i = 0; i < datas.Length; i++)
{
sentence = datas[i];
if (sentence.ParagraphIndex < startIndex) continue;
if (sentence.ParagraphIndex > endIndex) continue;
temps = hanlp.GetHanlpNer(sentence.Content);
if (temps == null || temps.Count() == 0) continue;
foreach (var tp in temps)
{
if (tp.Text.Length == 1) continue;
if (tp.Type != "PERSON") continue;
if (nameds.Select(x => x.Person).Contains(tp.Text))
{
summary = nameds.Where(x => x.Person == tp.Text).FirstOrDefault();
summary.Count++;
continue;
}
summary = new NamedSummary();
summary.Person = tp.Text;
summary.Count = 1;
nameds.Add(summary);
}
}
return nameds;
}
/// <summary>
/// 处理 NamedSummary 数组:按包含关系分组,以次数多的为主名称调用 AddRule
/// </summary>
/// <param name="namedSummaries">人名统计数组</param>
public void BuildPersonNameRules(NamedSummary[] namedSummaries, SpeakerFeatureLearner speakerFeatureLearner)
{
// 空值校验
if (namedSummaries == null || !namedSummaries.Any())
{
Console.WriteLine("NamedSummary 数组为空,无需处理");
return;
}
// 1. 提取去重的人名-次数字典(确保每个人名只统计一次总次数)
var nameCountDict = namedSummaries
.Where(ns => !string.IsNullOrWhiteSpace(ns.Person))
.GroupBy(ns => ns.Person.Trim())
.ToDictionary(g => g.Key, g => g.Sum(ns => ns.Count));
if (nameCountDict.Count < 2)
{
Console.WriteLine("有效人名数量不足,无需建立映射");
return;
}
// 2. 标记已处理的人名,避免重复配对
var processedNames = new HashSet<string>();
// 3. 遍历查找包含关系的人名对,按次数确定主从
foreach (var (name1, count1) in nameCountDict)
{
if (processedNames.Contains(name1)) continue;
// 查找与当前人名存在包含关系的其他人名
var matchedNames = nameCountDict
.Where(kv => !processedNames.Contains(kv.Key)
&& kv.Key != name1
&& (kv.Key.Contains(name1) || name1.Contains(kv.Key))) // 互相包含都算匹配
.ToList();
if (matchedNames.Count == 0)
{
processedNames.Add(name1);
continue; // 无匹配的包含关系人名,跳过
}
// 4. 合并当前人名和匹配人名,按次数选主名称
foreach (var (name2, count2) in matchedNames)
{
// 确定主/次要名称:次数多的是主,次数相同则选长度长的(可选规则)
string mainName = count1 > count2 ? name1 : (count2 > count1 ? name2 : (name1.Length > name2.Length ? name1 : name2));
string minorName = mainName == name1 ? name2 : name1;
// 5. 调用 AddRule(次要名称, 主名称)
speakerFeatureLearner.AddRule(mainName, minorName);
Console.WriteLine($"已调用 AddRule:AddRule(\"{mainName}\", \"{minorName}\")");
// 标记为已处理,避免重复配对
processedNames.Add(name1);
processedNames.Add(name2);
}
}
}
public class NamedSummary
{
public int Count { get; set; }
public string Person { get; set; }
public decimal MinConfidence { get; set; }
}
}
}
+133
View File
@@ -0,0 +1,133 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Task.Novel
{
//task:SplitFileChapterTask encoding:GB2312 pattern: @"第([^】]+)章" input:D:\NET8\AI\frxxz.txt output:D:\NET8\AI\NovelSplitter
//input可以是文件名,也可以是目錄名
//output輸出目錄
//pattern可能自定義分割表達式
public class SplitFileChapterTask : IScheduleTask
{
private readonly IApplicationService service;
public SplitFileChapterTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
if (!bodyDict.ContainsKey("input") || string.IsNullOrEmpty(bodyDict["input"])) return;
string input = bodyDict["input"];
string output = AppDomain.CurrentDomain.BaseDirectory;
//如果是文件名,輸出到同一目錄下
if (File.Exists(input))
{
output = Path.GetDirectoryName(input) + "\\";
}
else if (Directory.Exists(input))
{
output = input;
}
if (bodyDict.ContainsKey("output") && !string.IsNullOrEmpty(bodyDict["output"]))
{
output = bodyDict["output"];
//如果目錄不存在,創建目錄
if (!Directory.Exists(output)) Directory.CreateDirectory(output);
}
string pattern = @"第([^】]+)章";
if (bodyDict.ContainsKey("pattern") && !string.IsNullOrEmpty(bodyDict["pattern"]))
pattern = bodyDict["pattern"];
string encoding = "UTF-8";
if (bodyDict.ContainsKey("encoding") && !string.IsNullOrEmpty(bodyDict["encoding"])) encoding = bodyDict["encoding"];
List<string> files = PackFiles(input);
foreach (string file in files)
{
SaveFile(encoding, file, output, pattern);
}
}
private void SaveFile(string encoding, string fileName, string output, string pattern)
{
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
string[] lines = File.ReadAllLines(fileName, Encoding.GetEncoding(encoding));
string title = string.Empty;
int chapterIndex = 1;
StringBuilder chapter = new StringBuilder();
foreach (string line in lines)
{
Match match = Regex.Match(line, pattern);
if (match.Success)
{
// 提取第一个分组(即括号内的内容)
//string value = match.Groups[1].Value;
//第一次找到是空的,下次找到新的保存之前的
if (!string.IsNullOrEmpty(title) && chapter.Length > 0)
{
File.WriteAllText(Path.Combine(output, Path.GetFileNameWithoutExtension(fileName) + string.Format("_{0}_{1}", chapterIndex++,title) + Path.GetExtension(fileName)), chapter.ToString(), Encoding.UTF8);
chapter = new StringBuilder();
}
title = line;
}
if (!string.IsNullOrEmpty(title))
{
chapter.AppendLine(line);
}
}
//最后一章
if (!string.IsNullOrEmpty(title) && chapter.Length > 0)
{
File.WriteAllText(Path.Combine(output, Path.GetFileNameWithoutExtension(fileName) + string.Format("_{0}_{1}", chapterIndex++, title) + Path.GetExtension(fileName)), chapter.ToString(), Encoding.UTF8);
}
}
private List<string> PackFiles(string input)
{
List<string> files = new List<string>();
if (Path.HasExtension(input) && !string.IsNullOrEmpty(input))
{
if (File.Exists(input))
{
files.Add(input);
}
}
else if (!string.IsNullOrEmpty(input))
{
if (Directory.Exists(input))
{
string[] tempFiles = Directory.GetFiles(input);
if (tempFiles != null && tempFiles.Length > 0)
{
files.AddRange(tempFiles);
}
}
}
return files;
}
}
}
+19
View File
@@ -0,0 +1,19 @@
1.创建一本书
[title]
[author]
[description]
[book_type]
[file_path]
[status]
创建成功[status]>【待上传】,如果同步上传了文件[file_path]不空,[status]>【待提交】(此时已写入章节)
[guid]
[book_guid]
[chapter_index]
[title]
[content]
[status]
[status]>【处理中】
【待确认人物关系】【等待分配角色】【生成中】【完成】
2.上传文件后【待提交】,用户检查章节分割无误后等待提交
3.按【提交】后,写入段落及细分是否是对话
段落换行来分割,如有对话规则,再划分对话
+59
View File
@@ -0,0 +1,59 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Python;
using CloudBuilder.Topshelf.Task.Novel.Process;
using CloudBuilder.Topshelf.Utility;
namespace CloudBuilder.Topshelf.Task
{
//task:PythonRunTask param_name:param_value
public class PythonRunTask : IScheduleTask
{
private readonly IApplicationService service;
public PythonRunTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
PythonCommand python = new PythonCommand(service);
try
{
Hanlp2 hanlp = new Hanlp2(python);
string text = @"金越大师,听说你前两日派人请这位韩道友聚一下,他却开始闭关了,可有此事的。";
//text = "艳美少妇打了韩立";
//text = "徐先生还具体帮助他确定了把画雄鹰、松鼠和麻雀作为主攻目标。";
text = "银发老者神色一肃的向金越禅师问道。";
//text = "鬼灵门门主一边吩咐着,一边冷冷打量着悬浮在身前的画轴,见其并没有任何异样发生,目光闪动几下。";
text = "韩立闻言,眉头不经意的一皱,想了想后,还是摇摇头说道:";
List<string> sentiments = hanlp.GetTokFine(text);
sentiments = hanlp.GetTokCoarse(text);
string depRoot = hanlp.GetDepRoot(text);
List<NamedEntity> ners = hanlp.GetHanlpNer(text);
List<List<MeaningRepresentationParsingEntity>> srls = hanlp.GetHanlpSrl(text);
List<HanlpConstituencyNode> cons = hanlp.GetHanlpCon(text);
SpeakerAnalysisHelper speakerFeatureLearner = new SpeakerAnalysisHelper();
speakerFeatureLearner.GetPerson(depRoot, ners, srls, cons);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
python.Dispose();
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
namespace CloudBuilder.Topshelf.Task
{
//task:RunTask param_name:param_value
public class RunTask : IScheduleTask
{
private readonly IApplicationService service;
public RunTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
}
}
}
+135
View File
@@ -0,0 +1,135 @@
using CloudBuilder.Core.Authorization;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Core.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Text;
namespace CloudBuilder.Topshelf.Task
{
/// <summary>
/// 分隔文件并保存為UTF-8格式,
/// 【encoding】原文件格式必填,
/// 【split_size】分隔大小单位为M 【split_number】分隔文件數量,這兩個參數必填其中之一,
/// 【input】文件夹或者文件路径必填,
/// 【output】输出文件夹路径
/// task:SplitFileSaveUTF8Task encoding:GB2312 split_size:1 input:D:\NET8\AIML文档\汉语大词典1994+订补+资料Txt output:D:\NET8\AIML文档\汉语大词典1994UTF8
/// task:SplitFileSaveUTF8Task encoding:GB2312 split_number:1 input:D:\NET8\AIML文档\汉语大词典1994+订补+资料Txt output:D:\NET8\AIML文档\汉语大词典1994UTF8
/// </summary>
public class SplitFileSaveUTF8Task : IScheduleTask
{
private readonly IApplicationService service;
public SplitFileSaveUTF8Task(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
if (!bodyDict.ContainsKey("input")) return;
if (!bodyDict.ContainsKey("encoding")) return;
if (!bodyDict.ContainsKey("output")) return;
int splitNumber = 0;
if (bodyDict.ContainsKey("split_number"))
splitNumber = Convert.ToInt32(bodyDict["split_number"]);
string encoding = bodyDict["encoding"];
string output = bodyDict["output"];
string input = bodyDict["input"];
decimal splitSize = 0;
if (bodyDict.ContainsKey("split_size"))
splitSize = Convert.ToDecimal(bodyDict["split_size"]);
List<string> files = PackFiles(input);
foreach (string file in files)
{
SaveFile(encoding, file, output, splitNumber, splitSize);
}
}
private void SaveFile(string encoding, string fileName, string output, int splitNumber, decimal splitSize)
{
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
string context = File.ReadAllText(fileName, Encoding.GetEncoding(encoding));
int maxlength = context.Length;
int maxSplit = maxlength;
//如果傳入的是分隔數,以分隔數為準
if (splitNumber > 0)
{
maxSplit = (int)Math.Ceiling(maxlength * 1.0m / splitNumber);
}
if (splitSize > 0 && splitNumber == 0)
{
FileInfo fileInfo = new FileInfo(fileName);
splitNumber = (int)Math.Ceiling(fileInfo.Length / ((1024m * 1024m) * splitSize));
maxSplit = (int)Math.Ceiling(maxlength * 1.0m / splitNumber);
}
int reqLenth = maxSplit;
string ct = string.Empty;
for (int i = 0; i < splitNumber; i++)
{
if (i > 0 && splitNumber == i + 1) { reqLenth = maxlength % maxSplit; }
if (splitNumber > 1)
{
ct = context.Substring(i * maxSplit, reqLenth);
}
else
{
ct = context;
}
string outputPath = Path.Combine(output,
Path.GetFileNameWithoutExtension(fileName) + string.Format("_{0}", i+1) +
Path.GetExtension(fileName));
File.WriteAllText(outputPath,
ct, Encoding.UTF8);
}
}
private List<string> PackFiles(string input)
{
List<string> files = new List<string>();
if (Path.HasExtension(input) && !string.IsNullOrEmpty(input))
{
if (File.Exists(input))
{
files.Add(input);
}
}
else if (!string.IsNullOrEmpty(input))
{
if (Directory.Exists(input))
{
string[] tempFiles = Directory.GetFiles(input);
if (tempFiles != null && tempFiles.Length > 0)
{
files.AddRange(tempFiles);
}
}
}
return files;
}
}
}
+42
View File
@@ -0,0 +1,42 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CloudBuilder.Topshelf.Task.TTS
{
//task:TtsTask voice:Sherpa2 save_path:D:\MAFAI\cc.wav text:乌云忽然凝聚变形,瞬间化为了一个身高寸许的黑绿婴儿出来,并口吐人言的说道。
public class TtsTask : IScheduleTask
{
private readonly IApplicationService service;
public TtsTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
string save_path = string.Empty, text = string.Empty, voice = ChineseTtsVoice.Sherpa2.ToString();
if (bodyDict.ContainsKey("voice"))
voice = bodyDict["voice"];
if (bodyDict.ContainsKey("save_path"))
save_path = bodyDict["save_path"];
if (bodyDict.ContainsKey("text"))
text = bodyDict["text"];
if (string.IsNullOrEmpty(voice) || string.IsNullOrEmpty(save_path) || string.IsNullOrEmpty(text)) return;
SaveAudioToFileHelper.AudioToFile(EnumHelper.ToEnum<ChineseTtsVoice>(voice), text, save_path);
}
}
}
+317
View File
@@ -0,0 +1,317 @@
using SherpaOnnx;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace CloudBuilder.Topshelf.Utility
{
public static class SaveAudioToFileHelper
{
private static OfflineTts fanchenTTs = null;
private static OfflineTts sherpaTTs = null;
private static OfflineTts theresaTTs = null;
private static OfflineTts meloTTs = null;
private static OfflineTts icefallTTs = null;
private static OfflineTts matchaTTs = null;
private static OfflineTts eulaTTs = null;
public static void InitConfig()
{
OfflineTtsConfig config;
OfflineTtsGeneratedAudio audio;
int numThreads = 4;
if (fanchenTTs == null)
{
config = new OfflineTtsConfig();
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/number.fst,tts-models/vits-zh-hf-fanchen-C/phone.fst";
config.Model.NumThreads = numThreads;
//187
fanchenTTs = new OfflineTts(config);
config = new OfflineTtsConfig();
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/number.fst,tts-models/sherpa-onnx-vits-zh-ll/phone.fst";
config.Model.NumThreads = numThreads;
//多人音质最好(0 - 4)
sherpaTTs = new OfflineTts(config);
config = new OfflineTtsConfig();
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";
config.RuleFars = "tts-models/vits-zh-hf-theresa/number.fst,tts-models/vits-zh-hf-theresa/phone.fst";
config.Model.NumThreads = numThreads;
//804
theresaTTs = new OfflineTts(config);
config = new OfflineTtsConfig();
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";
config.RuleFars = "tts-models/vits-melo-tts-zh_en/number.fst,tts-models/vits-melo-tts-zh_en/phone.fst";
config.Model.NumThreads = numThreads;
//1
meloTTs = new OfflineTts(config);
config = new OfflineTtsConfig();
config.Model.Vits.Model = "tts-models/vits-icefall-zh-aishell3/model.onnx";
config.Model.Vits.Tokens = "tts-models/vits-icefall-zh-aishell3/tokens.txt";
config.Model.Vits.Lexicon = "tts-models/vits-icefall-zh-aishell3/lexicon.txt";
config.RuleFars = "tts-models/vits-icefall-zh-aishell3/number.fst,tts-models/vits-icefall-zh-aishell3/phone.fst";
config.Model.NumThreads = numThreads;
//中文、32 个不同说话人,最快
icefallTTs = new OfflineTts(config);
config = new OfflineTtsConfig();
config.Model.Vits.Model = "tts-models/vits-zh-hf-eula/eula.onnx";
config.Model.Vits.Tokens = "tts-models/vits-zh-hf-eula/tokens.txt";
config.Model.Vits.Lexicon = "tts-models/vits-zh-hf-eula/lexicon.txt";
config.RuleFars = "tts-models/vits-zh-hf-eula/number.fst,tts-models/vits-zh-hf-eula/phone.fst";
config.Model.NumThreads = numThreads;
eulaTTs = new OfflineTts(config);
config = new OfflineTtsConfig();
config.Model.Matcha.AcousticModel = "tts-models/matcha-icefall-zh-baker/model-steps-3.onnx";
config.Model.Matcha.Vocoder = "tts-models/matcha-icefall-zh-baker/vocos-22khz-univ.onnx";
config.Model.Matcha.Tokens = "tts-models/matcha-icefall-zh-baker/tokens.txt";
config.Model.Matcha.Lexicon = "tts-models/matcha-icefall-zh-baker/lexicon.txt";
config.RuleFars = "tts-models/matcha-icefall-zh-baker/number.fst,tts-models/matcha-icefall-zh-baker/phone.fst";
config.Model.NumThreads = numThreads;
//1
matchaTTs = new OfflineTts(config);
}
}
public static void AudioToFile(ChineseTtsVoice actorName, string text, string saveFilePath)
{
InitConfig();
int sid = 0;
OfflineTtsGeneratedAudio audio;
switch (actorName)
{
case ChineseTtsVoice.Fanchen10:
case ChineseTtsVoice.Fanchen110:
case ChineseTtsVoice.Fanchen6:
case ChineseTtsVoice.Fanchen15:
case ChineseTtsVoice.Fanchen2:
case ChineseTtsVoice.Fanchen20:
case ChineseTtsVoice.Fanchen14:
case ChineseTtsVoice.Fanchen36:
sid = actorName.GetSidId();
audio = fanchenTTs.Generate(text, 1.0f, sid);
break;
case ChineseTtsVoice.Sherpa0:
case ChineseTtsVoice.Sherpa1:
case ChineseTtsVoice.Sherpa2:
case ChineseTtsVoice.Sherpa3:
case ChineseTtsVoice.Sherpa4:
sid = actorName.GetSidId();
audio = sherpaTTs.Generate(text, 1.0f, sid);
break;
case ChineseTtsVoice.Melo:
sid = actorName.GetSidId();
audio = meloTTs.Generate(text, 1.0f, sid);
break;
case ChineseTtsVoice.Eula:
sid = actorName.GetSidId();
audio = eulaTTs.Generate(text, 1.0f, sid);
break;
case ChineseTtsVoice.Theresa:
sid = actorName.GetSidId();
audio = theresaTTs.Generate(text, 1.0f, sid);
break;
case ChineseTtsVoice.Aaishe1:
case ChineseTtsVoice.Aaishe6:
case ChineseTtsVoice.Aaishe14:
case ChineseTtsVoice.Aaishe15:
case ChineseTtsVoice.Aaishe18:
case ChineseTtsVoice.Aaishe10:
sid = actorName.GetSidId();
audio = icefallTTs.Generate(text, 1.0f, sid);
break;
default:
sid = actorName.GetSidId();
audio = matchaTTs.Generate(text, 1.0f, sid);
break;
}
// --- 4. 保存音频 ---
audio.SaveToWaveFile(saveFilePath);
}
}
/// <summary>
/// 微软Edge TTS中文语音音色枚举
/// 对应官方Neural语音标识,覆盖港澳台及大陆各地区音色
/// </summary>
public enum ChineseTtsVoice
{
/// <summary>
/// 普通话(中国大陆) 娇柔(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-0")]
Sherpa0,
/// <summary>
/// 普通话(中国大陆)- 柔美(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 柔美(女性)-2")]
Sherpa2,
/// <summary>
/// 普通话(中国大陆)- 知性(男性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 知性(男性)-4")]
Sherpa4,
/// <summary>
/// 普通话(中国大陆)- 知青(男性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 知青(男性)-3")]
Sherpa3,
/// <summary>
/// 普通话(中国大陆)- 中青(男性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 中青(男性)-1")]
Sherpa1,
/// <summary>
/// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-6")]
Aaishe6,
/// <summary>
/// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-14")]
Aaishe14,
/// <summary>
/// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-18")]
Aaishe18,
/// <summary>
/// 普通话(中国大陆)- 云健(男性)- 文件小速度快
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 云健(男性)-10")]
Aaishe10,
/// <summary>
/// 普通话(中国大陆)- 爱莎(女性)- 文件小速度快
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 爱莎(女性)-1")]
Aaishe1,
/// <summary>
/// 普通话(中国大陆)- 云健(男性)- 文件小速度快
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 云健(男性)-15")]
Aaishe15,
/// 普通话(中国大陆) 云阳(男性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 云阳(男性)-10")]
Fanchen10,
/// <summary>
/// 普通话(中国大陆) 云阳(男性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 云阳(男性)-110")]
Fanchen110,
/// 普通话(中国大陆) 喜佳(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 云夏(女性)-6")]
Fanchen6,
/// <summary>
/// 普通话(中国大陆) 云阳(男性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 云阳(男性)-15")]
Fanchen15,
/// <summary>
/// 普通话(中国大陆) 喜佳(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-2")]
Fanchen2,
/// <summary>
/// 普通话(中国大陆) 喜佳(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-14")]
Fanchen14,
/// <summary>
/// 普通话(中国大陆) 喜佳(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-20")]
Fanchen20,
/// <summary>
/// 普通话(中国大陆) 喜佳(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 喜佳(女性)-36")]
Fanchen36,
/// <summary>
/// 粤语(香港)- 女性(男性)- 文件大
/// </summary>
[System.ComponentModel.Description("粤语(香港) 梅隆 (女性)-0")]
Melo,
/// <summary>
/// 普通话(中国大陆)- 神精女(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 神精女(女性)-0")]
Eula,
/// <summary>
/// 普通话(中国大陆)- 小神精女(女性)
/// </summary>
[System.ComponentModel.Description("普通话(中国大陆) 小神精女(女性)-27")]
Theresa,
}
public static class ChineseTtsVoiceExtensions
{
/// <summary>
/// 获取枚举值对应的Description特性值
/// </summary>
/// <param name="voice">语音枚举值</param>
/// <returns>描述字符串(无特性时返回枚举名称)</returns>
public static int GetSidId(this ChineseTtsVoice voice)
{
// 获取枚举成员的特性
var field = voice.GetType().GetField(voice.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
// 有特性返回描述,无则返回枚举名
return Convert.ToInt32(attribute?.Description.Split('-')[1] ?? "0");
}
}
public static class EnumHelper
{
// 枚举转字符串
public static string ToEnumString<T>(T value) where T : Enum
{
return value.ToString();
}
// 字符串转枚举
public static T ToEnum<T>(string str) where T : struct, Enum
{
return Enum.TryParse<T>(str, out var result) ? result : default;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Topshelf.Python;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Topshelf.Utility
{
public class SegmenterHelper
{
public List<string> Cut(string text, Hanlp2 hanlp)
{
return hanlp.GetTokFine(text);
}
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"CloudBuilder": {
"Culture": "zh-cn",
"DbContexts": {
"MasterDbContext": {
"DbContextTypeName": "MasterDbContext",
"ConnectionString": "Server=localhost\\SQLEXPRESS;Database=Furion;User=sa;Password=owen123456;MultipleActiveResultSets=True;TrustServerCertificate=true",
"DatabaseType": "SqlServer",
"LazyLoadingProxiesEnabled": true,
"AuditEntityEnabled": true,
"AutoMigrationEnabled": true
}
},
"ServiceRequest": {
"BaseAddress": "http://localhost:5235", //通过Request是需要用户权限及服务器支持
"DefaultRequestHeaders": {
"Accept": "application/json"
}
},
"SecurityUser": {
"UserPassword": "1b457798-6d49-4b37-93a4-a6e105cce84b202cb962ac59075b964b07152d234b70",
"UserMobile": "13560851330"
}
},
"Python": {
"PythonCommand": "D:\\Python38\\python38.dll",
"PythonFile": "D:\\Python38\\hanlp\\hanlp_processor.py",
"surnames_path": "D:/Python38/hanlp/dictionary/surnames.txt",
"titles_path": "D:/Python38/hanlp/dictionary/titles.txt",
"words_path": "D:/Python38/hanlp/dictionary/words.txt",
"salutations_path": "D:/Python38/hanlp/dictionary/salutations.txt",
"interlocutors_path": "D:/Python38/hanlp/dictionary/interlocutors.txt"
},
"IApplicationService": {
"UserId": "000003" //通过Service是不需要用户权限而直接调用
},
"AllowedHosts": "*"
}