ow
This commit is contained in:
parent
bcd8ebab67
commit
5f222a23b3
@ -59,6 +59,8 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||||
|
<PackageReference Include="NAudio" Version="2.3.0" />
|
||||||
|
<PackageReference Include="NAudio.Lame" Version="2.1.0" />
|
||||||
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||||
|
|||||||
Binary file not shown.
@ -2,7 +2,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"CloudBuilder.Topshelf": {
|
"CloudBuilder.Topshelf": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"commandLineArgs": "task:ChineseNameExtractorTask book_id:B000008"
|
"commandLineArgs": "task:TtsTask book_id:B000008 voice:Sherpa2 actor:澈蕾 actor_voice:Sherpa1 save_path:D:\\\\Net8\\\\FileServer\\\\Backup\\\\DmsFile\\\\voice"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ namespace CloudBuilder.Topshelf.Task
|
|||||||
|
|
||||||
List<HanlpConstituencyNode> cons = null;
|
List<HanlpConstituencyNode> cons = null;
|
||||||
|
|
||||||
SpeakerAnalysisHelper speakerFeatureLearner = new SpeakerAnalysisHelper();
|
SpeakerAnalysisHelper speakerAnalysisHelper = new SpeakerAnalysisHelper();
|
||||||
|
|
||||||
AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == guid).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray();
|
AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == guid).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray();
|
||||||
if (ents == null || ents.Length == 0) return;
|
if (ents == null || ents.Length == 0) return;
|
||||||
@ -54,11 +54,12 @@ namespace CloudBuilder.Topshelf.Task
|
|||||||
int totalLines = ents.Length;
|
int totalLines = ents.Length;
|
||||||
int processedLines = 0; // 已处理行数计数器
|
int processedLines = 0; // 已处理行数计数器
|
||||||
AiChapterPersonEntity aiChapterPerson;
|
AiChapterPersonEntity aiChapterPerson;
|
||||||
|
List<AiChapterPersonEntity> cps = new List<AiChapterPersonEntity>();
|
||||||
foreach (var ent in ents)
|
foreach (var ent in ents)
|
||||||
{
|
{
|
||||||
processedLines++;
|
processedLines++;
|
||||||
|
|
||||||
person = speakerFeatureLearner.GetPerson(hanlp, ent.Content);
|
person = speakerAnalysisHelper.GetPerson(hanlp, ent.Content);
|
||||||
paragraphIndex = ent.ParagraphId;
|
paragraphIndex = ent.ParagraphId;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(person))
|
if (!string.IsNullOrEmpty(person))
|
||||||
@ -72,8 +73,9 @@ namespace CloudBuilder.Topshelf.Task
|
|||||||
aiChapterPerson.PersonName = p;
|
aiChapterPerson.PersonName = p;
|
||||||
aiChapterPerson.ChapterId = ent.ChapterId;
|
aiChapterPerson.ChapterId = ent.ChapterId;
|
||||||
aiChapterPerson.ParagraphIndex = ent.ParagraphId;
|
aiChapterPerson.ParagraphIndex = ent.ParagraphId;
|
||||||
|
aiChapterPerson.SentenceIndex= ent.SentenceIndex;
|
||||||
aiChapterPerson.OrderIndex = orderIndex++;
|
aiChapterPerson.OrderIndex = orderIndex++;
|
||||||
|
cps.Add(aiChapterPerson);
|
||||||
repositoryAiChapterPersonEntity.InsertNow(aiChapterPerson);
|
repositoryAiChapterPersonEntity.InsertNow(aiChapterPerson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,6 +83,9 @@ namespace CloudBuilder.Topshelf.Task
|
|||||||
UpdateProgress(processedLines, totalLines);
|
UpdateProgress(processedLines, totalLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
speakerAnalysisHelper.AiChapterPersons = cps.ToArray();
|
||||||
|
|
||||||
|
speakerAnalysisHelper.MatchPerson(ents, repositoryAiSentenceEntity);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
using CloudBuilder.AI.Entity;
|
||||||
|
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||||||
using CloudBuilder.Core.DependencyInjection.Task;
|
using CloudBuilder.Core.DependencyInjection.Task;
|
||||||
using CloudBuilder.Topshelf.Utility;
|
using CloudBuilder.Topshelf.Utility;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using NAudio.Lame;
|
||||||
|
using NAudio.Wave;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -12,6 +17,7 @@ using static System.Runtime.InteropServices.JavaScript.JSType;
|
|||||||
|
|
||||||
namespace CloudBuilder.Topshelf.Task.TTS
|
namespace CloudBuilder.Topshelf.Task.TTS
|
||||||
{
|
{
|
||||||
|
//task:TtsTask book_id:B000008 voice:Sherpa2 actor:韩立 actor_voice:Sherpa1 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice
|
||||||
//task:TtsTask voice:Sherpa2 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice\\20260528112739.mp3 text:二愣子睁大着双眼,直直望着茅草和烂泥糊成的黑屋顶,身上盖着的旧棉被,已呈深黄色,看不出原来的本来面目,还若有若无的散发着淡淡的霉味。
|
//task:TtsTask voice:Sherpa2 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice\\20260528112739.mp3 text:二愣子睁大着双眼,直直望着茅草和烂泥糊成的黑屋顶,身上盖着的旧棉被,已呈深黄色,看不出原来的本来面目,还若有若无的散发着淡淡的霉味。
|
||||||
public class TtsTask : IScheduleTask
|
public class TtsTask : IScheduleTask
|
||||||
{
|
{
|
||||||
@ -24,19 +30,173 @@ namespace CloudBuilder.Topshelf.Task.TTS
|
|||||||
|
|
||||||
public void Run(Dictionary<string, string> bodyDict)
|
public void Run(Dictionary<string, string> bodyDict)
|
||||||
{
|
{
|
||||||
string save_path = string.Empty, text = string.Empty, voice = ChineseTtsVoice.Sherpa2.ToString();
|
IRepository<AiSentenceViewEntity> repository = service.GetRepository<IRepository<AiSentenceViewEntity>>();
|
||||||
|
|
||||||
|
string bookId = string.Empty;
|
||||||
|
if (bodyDict.ContainsKey("book_id"))
|
||||||
|
bookId = bodyDict["book_id"];
|
||||||
|
|
||||||
|
AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray();
|
||||||
|
|
||||||
|
if (ents == null || ents.Length == 0) return;
|
||||||
|
|
||||||
|
string save_path = string.Empty, text = string.Empty, voice = ChineseTtsVoice.Sherpa2.ToString(), actor = string.Empty, actor_voice = ChineseTtsVoice.Sherpa1.ToString();
|
||||||
|
if (bodyDict.ContainsKey("actor"))
|
||||||
|
actor = bodyDict["actor"];
|
||||||
|
|
||||||
if (bodyDict.ContainsKey("voice"))
|
if (bodyDict.ContainsKey("voice"))
|
||||||
voice = bodyDict["voice"];
|
voice = bodyDict["voice"];
|
||||||
|
|
||||||
|
if (bodyDict.ContainsKey("actor_voice"))
|
||||||
|
actor_voice = bodyDict["actor_voice"];
|
||||||
|
|
||||||
if (bodyDict.ContainsKey("save_path"))
|
if (bodyDict.ContainsKey("save_path"))
|
||||||
save_path = bodyDict["save_path"];
|
save_path = bodyDict["save_path"];
|
||||||
|
|
||||||
if (bodyDict.ContainsKey("text"))
|
GenerateAndMergeChapterAudio(ents, actor, actor_voice, voice, save_path);
|
||||||
text = bodyDict["text"];
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(voice) || string.IsNullOrEmpty(save_path) || string.IsNullOrEmpty(text)) return;
|
public void GenerateAndMergeChapterAudio(AiSentenceViewEntity[] ents, string actor, string actor_voice, string mainVoice, string save_path)
|
||||||
|
{
|
||||||
|
// 1. 按 BookId + ChapterId 分组(关键!)
|
||||||
|
var chapterGroups = ents
|
||||||
|
.Where(ent => ent.Content.Any(c => c >= 0x4E00 && c <= 0x9FFF)) // 只保留含中文的句子
|
||||||
|
.GroupBy(ent => new { ent.BookId, ent.ChapterId })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
SaveAudioToFileHelper.AudioToFile(EnumHelper.ToEnum<ChineseTtsVoice>(voice), text, save_path);
|
int totalLines = chapterGroups.Count();
|
||||||
|
int processedLines = 0; // 已处理行数计数器
|
||||||
|
|
||||||
|
foreach (var chapter in chapterGroups)
|
||||||
|
{
|
||||||
|
processedLines++;
|
||||||
|
|
||||||
|
string bookId = chapter.Key.BookId;
|
||||||
|
int chapterId = chapter.Key.ChapterId;
|
||||||
|
List<string> tempMp3List = new List<string>(); // 本章所有碎片音频
|
||||||
|
|
||||||
|
// 2. 生成本章所有句子音频
|
||||||
|
foreach (AiSentenceViewEntity ent in chapter)
|
||||||
|
{
|
||||||
|
string voice = ChineseTtsVoice.Sherpa2.ToString();
|
||||||
|
if (ent.DialogueIndc == YesNoPolicy.YES)
|
||||||
|
{
|
||||||
|
if (ent.PersonName == actor)
|
||||||
|
voice = actor_voice;
|
||||||
|
else
|
||||||
|
voice = AI.Policy.ChineseTtsVoiceRandom.GetRandom().ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
voice = mainVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(voice) || string.IsNullOrEmpty(ent.Content))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// 碎片文件名:BookId_ChapterId_ParagraphId_SentenceId.mp3
|
||||||
|
string fileName = string.Format("{0}_{1}_{2}_{3}.mp3", ent.BookId, ent.ChapterId, ent.ParagraphId, ent.SentenceId);
|
||||||
|
string filePath = Path.Combine(save_path, fileName);
|
||||||
|
|
||||||
|
// 生成音频
|
||||||
|
SaveAudioToFileHelper.AudioToFile(EnumHelper.ToEnum<ChineseTtsVoice>(voice), ent.Content, filePath);
|
||||||
|
|
||||||
|
tempMp3List.Add(filePath); // 加入合并列表
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(100);
|
||||||
|
|
||||||
|
// 3. 合并为:BookId_ChapterId.mp3
|
||||||
|
string outputFileName = string.Format("{0}_{1}.mp3", bookId, chapterId);
|
||||||
|
string outputFilePath = Path.Combine(save_path, outputFileName);
|
||||||
|
|
||||||
|
MergeChapterMp3Files(tempMp3List, outputFilePath);
|
||||||
|
|
||||||
|
// 4. 可选:合并后删除碎片文件(节省空间)
|
||||||
|
foreach (var chunk in tempMp3List)
|
||||||
|
{
|
||||||
|
try { File.Delete(chunk); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"章节合并完成:{outputFileName}");
|
||||||
|
|
||||||
|
UpdateProgress(processedLines, totalLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 最终修复:完全解决 No fmt chunk / 采样率不兼容 / 只有一截声音
|
||||||
|
/// </summary>
|
||||||
|
public void MergeChapterMp3Files(List<string> chunkFiles, string outputFilePath)
|
||||||
|
{
|
||||||
|
var validFiles = chunkFiles.Where(File.Exists).ToList();
|
||||||
|
if (validFiles.Count == 0) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 目标格式:32000Hz 单声道 16bit
|
||||||
|
WaveFormat targetFormat = new WaveFormat(32000, 16, 1);
|
||||||
|
|
||||||
|
// 直接生成 MP3,不经过 WAV 文件,彻底避免 fmt 错误
|
||||||
|
using (var mp3Writer = new LameMP3FileWriter(outputFilePath, targetFormat, 128))
|
||||||
|
{
|
||||||
|
foreach (var file in validFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 释放文件占用
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
|
||||||
|
// 用Windows系统解码器读取任何MP3
|
||||||
|
using (var reader = new MediaFoundationReader(file))
|
||||||
|
using (var resampler = new MediaFoundationResampler(reader, targetFormat))
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int read;
|
||||||
|
while ((read = resampler.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
mp3Writer.Write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"跳过文件:{file},错误:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ 合并成功:{outputFilePath},大小:{new FileInfo(outputFilePath).Length} 字节");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 合并失败:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:WAV 转 MP3
|
||||||
|
private void WaveFileToMp3(byte[] wavData, string mp3Path)
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream(wavData))
|
||||||
|
using (var waveReader = new WaveFileReader(ms))
|
||||||
|
using (var mp3Writer = new LameMP3FileWriter(mp3Path, waveReader.WaveFormat, 128))
|
||||||
|
{
|
||||||
|
waveReader.CopyTo(mp3Writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using CloudBuilder.AI.Entity;
|
using CloudBuilder.AI.Entity;
|
||||||
|
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||||||
using CloudBuilder.Topshelf.Python;
|
using CloudBuilder.Topshelf.Python;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -184,7 +185,35 @@ namespace CloudBuilder.Topshelf.Utility
|
|||||||
return arg0;
|
return arg0;
|
||||||
}
|
}
|
||||||
|
|
||||||
//public AiSentenceEntity[] AiParagraphs { get; set; }
|
public void MatchPerson(AiSentenceViewEntity[] aiSentences, IRepository<AiSentenceEntity> repositoryAiSentenceEntity)
|
||||||
|
{
|
||||||
|
if (AiChapterPersons == null || AiChapterPersons.Length == 0) return;
|
||||||
|
|
||||||
|
AiSentenceEntity aiSentence;
|
||||||
|
string dialogueName = string.Empty;
|
||||||
|
foreach (AiSentenceViewEntity aiSentenceView in aiSentences)
|
||||||
|
{
|
||||||
|
dialogueName= string.Empty;
|
||||||
|
if (aiSentenceView.DialogueIndc != YesNoPolicy.YES) continue;
|
||||||
|
|
||||||
|
if (aiSentences.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphId == aiSentenceView.ParagraphId).Count() > 1)
|
||||||
|
{
|
||||||
|
if (AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex == x.SentenceIndex).Any())
|
||||||
|
{
|
||||||
|
dialogueName = AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex == x.SentenceIndex).FirstOrDefault().PersonName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AiChapterPersons.Where(x => x.ChapterId == aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex != x.SentenceIndex && x.PersonName != dialogueName).Any())
|
||||||
|
{
|
||||||
|
aiSentenceView.PersonName = AiChapterPersons.Where(x =>x.ChapterId==aiSentenceView.ChapterId && x.ParagraphIndex == aiSentenceView.ParagraphId && aiSentenceView.SentenceIndex != x.SentenceIndex && x.PersonName != dialogueName).OrderBy(x => x.OrderIndex).FirstOrDefault().PersonName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aiSentence = new AiSentenceEntity();
|
||||||
|
ObjectCopyHelper.Copy(aiSentenceView, aiSentence);
|
||||||
|
repositoryAiSentenceEntity.UpdateNow(aiSentence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public AiSentenceEntity[] AiSentences { get; set; }
|
public AiSentenceEntity[] AiSentences { get; set; }
|
||||||
|
|
||||||
@ -203,37 +232,4 @@ D -->|匹配失败| E{规则3:段落内最近人物};
|
|||||||
E -->|匹配成功| C;
|
E -->|匹配成功| C;
|
||||||
E -->|匹配失败| F[赋值默认值+标记异常];
|
E -->|匹配失败| F[赋值默认值+标记异常];
|
||||||
|
|
||||||
规则 1:文本特征精准匹配(最高优先级)
|
|
||||||
基于对话内容中的 “专属特征” 直接匹配说话者,避免上下文干扰:
|
|
||||||
示例规则:
|
|
||||||
对话内容特征 匹配的说话者
|
|
||||||
包含 “第二元婴” 韩立
|
|
||||||
包含 “银月所化” 银月
|
|
||||||
包含 “本座 / 本尊” 反派角色
|
|
||||||
|
|
||||||
落地:维护一个 “特征 - 人物” 映射字典,匹配时优先使用:
|
|
||||||
规则 2:紧邻旁白匹配(次高优先级)只找对话行 “紧邻的前 1-2 行旁白”,而非整个段落的旁白,避免跨越多个人物导致匹配错误:
|
|
||||||
规则:仅检查对话行的前 1 行、前 2 行旁白(最多前 3 行),超过则停止;
|
|
||||||
落地:修改向前查找的逻辑,限制查找范围
|
|
||||||
|
|
||||||
规则 3:排除无效人物(避免干扰)从旁白中提取人物时,过滤掉 “非核心人物”(如 “店小二”“路人”),只保留主要人物:
|
|
||||||
落地:维护 “核心人物白名单”,提取人物时只保留白名单内的名称
|
|
||||||
|
|
||||||
3. 增加异常处理和人工校验(兜底保障)
|
|
||||||
即使逻辑再完善,也会有边缘场景匹配错误,需通过 “异常标记 + 人工复核” 兜底:
|
|
||||||
|
|
||||||
步骤 1:标记可疑匹配结果对以下情况标记为 “可疑”,存入日志或单独字段:
|
|
||||||
匹配到的人物在旁白中出现次数≥2(比如同一段落有韩立和银月,无法确定);
|
|
||||||
对话行前后 3 行都没有找到人物,最终赋值为 “未知人物”;
|
|
||||||
对话内容无任何特征,且段落内人物数量≥3;
|
|
||||||
落地:为实体增加扩展字段(如IsSuspected),或生成异常日志
|
|
||||||
|
|
||||||
步骤 2:人工复核可疑数据
|
|
||||||
规则:所有标记为 “可疑” 的匹配结果,必须经过人工复核后再更新到数据库;
|
|
||||||
落地:生成 “可疑数据清单”(包含Guid、Content、PersonName),供运营 / 编辑人员校验,修正错误后重新填充。
|
|
||||||
|
|
||||||
|
|
||||||
收集错误案例:记录每次匹配错误的场景(如 “银月的对话匹配给韩立”),分析错误原因;
|
|
||||||
迭代特征字典:将错误案例转化为新的特征规则(如新增 “乌云”→银月,“元婴”→韩立);
|
|
||||||
A/B 测试:对优化后的规则做小范围测试,对比匹配准确率,确认有效后全量上线。
|
|
||||||
*/
|
*/
|
||||||
Loading…
Reference in New Issue
Block a user