From aecfa4ac0d40100bd7446fb71046cc4b792297d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com> Date: Wed, 21 Jan 2026 10:30:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=200122AI=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=9A=84=E8=B0=83?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AICore/GPT/ChatGPT/ChatGPTClient.cs | 2 +- VideoAnalysisCore/AICore/GPT/ChatGPTType.cs | 13 +- .../AICore/GPT/DeepSeek/DeepSeekGPTClient.cs | 6 +- .../AICore/GPT/GTP_Analysis_1.cs | 227 ++++++++++-------- .../AICore/SherpaOnnx/SenseVoice.cs | 39 ++- .../AICore/SherpaOnnx/SherpaOnnxDto.cs | 18 +- .../AICore/SherpaOnnx/SherpaVad.cs | 66 +++-- VideoAnalysisCore/Controllers/Dto/ApiDto.cs | 1 + .../Controllers/LJZK_Controller.cs | 1 + VideoAnalysisCore/Model/VideoTaskStage.cs | 3 +- 10 files changed, 226 insertions(+), 150 deletions(-) diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs index d79afde..23062df 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs @@ -44,7 +44,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT /// 最大token 不设置默认最大值 16000/8000 /// /// - public override async Task ChatAsync(string task, string postMessages, string title, string model = null, int max_tokens = 16000) + public override async Task ChatAsync(string task, string postMessages, string title, string model = null, int max_tokens = 32000) { Message[] messageArr = [ new Message(postMessages,"user"), diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPTType.cs b/VideoAnalysisCore/AICore/GPT/ChatGPTType.cs index f5aaef5..0dfd19d 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPTType.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPTType.cs @@ -8,16 +8,15 @@ namespace VideoAnalysisCore.AICore.GPT { public class ChatGPTType { - public static string GPT5_mini = "gpt-5-mini-2025-08-07"; - public static string GPT5 = "gpt-5-2025-08-07"; - public static string GPT5_nano = "gpt-5-nano-2025-08-07"; + public const string GPT5_mini = "gpt-5-mini"; + public const string GPT5 = "gpt-5-2025-08-07"; - public static string Deepseek_Reasoner = "deepseek-reasoner"; - public static string Deepseek_Chat = "deepseek-chat"; + public const string Deepseek_Reasoner = "deepseek-reasoner"; + public const string Deepseek_Chat = "deepseek-chat"; - public static string Gemini_3_Chat_thinking = "gemini-3-pro-preview-thinking"; - public static string Gemini_3_Chat = "gemini-3-pro-preview"; + public const string Gemini_3_Chat_thinking = "gemini-3-pro-preview-thinking"; + public const string Gemini_3_Chat = "gemini-3-pro-preview"; } diff --git a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs index 4246464..b524df3 100644 --- a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs @@ -42,17 +42,19 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT /// 最大token 不设置默认最大值 16000/8000 /// /// - public override async Task ChatAsync(string task, string postMessages, string title, string model =null, int max_tokens = 32000) + public override async Task ChatAsync(string task, string postMessages, string title, string model = ChatGPTType.Deepseek_Chat, int max_tokens = 8000) { Message[] messageArr = [ new Message(postMessages,"user"), ]; messageArr = messageArr.Where(s => s != null).ToArray(); + if (max_tokens > 8000 &&(model is null || model == ChatGPTType.Deepseek_Chat)) + max_tokens = 8000; var chatReq = new ChatRequest { taskId = task, title = title, - model = model ?? ChatGPTType.Deepseek_Reasoner, + model = model ?? ChatGPTType.Deepseek_Chat, max_tokens = model == ChatGPTType.Deepseek_Reasoner ? 32000 : max_tokens, stream = true, temperature = 0.2f, diff --git a/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs b/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs index 40fb88a..edbd20d 100644 --- a/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs +++ b/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs @@ -23,6 +23,8 @@ using UserCenter.Model.Enum; using Dm.filter; using System.Text.RegularExpressions; using System.Diagnostics; +using Dm.util; +using static System.Net.Mime.MediaTypeNames; namespace VideoAnalysisCore.AICore.GPT.DeepSeek { @@ -88,7 +90,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek 角色:你是一位{taskInfo.Subject}学科教研老师。 任务:为每个【视频分段】分配对应的知识点(可多个),并补充来源信息。 字段说明: - - TextbookSource:该分段讲授内容所属教材来源,仅允许取值:课本/试卷/挹青苑/其他 + - TextbookSource:该分段讲授内容所属教材来源(抓取字幕中的 关键词来识别 默认情况下是PPT),仅允许取值:PPT/课本/试卷/挹青苑 - KnowPoints:数组。每个元素代表一个知识点匹配结果 - KnowPoint:知识点名称(必须来自我提供的列表) - KnowPointId:知识点ID(必须与 KnowPoint 对应) @@ -142,7 +144,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek EndTime = s.EndTime, StageId = StageId, KnowPoint = x.KnowPoint, - KnowPointWeight=x.KnowPointWeight, + KnowPointWeight = x.KnowPointWeight, TextbookSource = s.TextbookSource, KnowSource = x.KnowSource, KnowPointId = knowDic[x.KnowPoint].ToString(), @@ -213,17 +215,21 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var checkMessage = $""" 请你担任一位专业的视频内容分析教研老师,擅长评估视频内容的结构和逻辑流暢度。 - 核心任务: 请根据我提供的【视频分段方案】【完整字幕文本】,对该分段方案进行严谨评估。 + 核心任务: 请根据我提供的【分段方案】【完整字幕文本】,对该分段方案进行严谨评估。 补充上下文:{pptFormat} 本节课所属章节:{sections} 内容结构与主题合理性: 分段准确性:评估单个分段内的课堂字幕内容与分段的Theme/Conten匹配、是否存在错误,捏造的情况(硬性指标)。 知识点分配:检查分段内的知识点是否与分段Conten有关联,知识点分配给这个分段是否合理(硬性指标)。 逻辑过渡:评估分段之间的过渡是否自然流畅,后一段是否是前一段内容的合理延伸或转折。 + 额外补充: + 1. 忽略掉分段没有结束时间的问题我会自己处理 + 2. 评估的分数只针对分段方案的内容是否与实际字幕描述内容符合 + 3. 字幕内容仅供参考不被计入扣分项 综合评分: 请基于以上分析,提供一个0-100的综合得分(70分及格,打分一定要严谨,总分一定要准确)。 MinusScore: 详细说明打分理由,并逐条对应到上述评估维度。 - Suggestion: 基于扣分原因提出针对分段方案的改进意见(请忽略掉分段没有结束时间的问题我会自己处理)。 + Suggestion: 基于扣分原因提出针对分段方案的改进意见 输入数据格式说明: 分段方案: {thems} 字幕文本: 格式为说话人:开始秒:结束秒:内容|下一段字幕。完整内容为:{captions.Captions} @@ -254,7 +260,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var message = $""" 请你担任一位专业的视频内容分析教研老师,擅长根据评估意见修复视频分段方案。 - 目标:在不改变片段数量的前提下,基于【改进意见】对【分段方案】做最小必要修改,使其更符合课堂内容的自然结构。 + 目标:在不改变片段数量的前提下,基于【改进意见和扣分原因】对【分段方案】做最小必要修改,使其更符合课堂内容的自然结构。 补充上下文:{pptFormat} 本节课所属章节:{sections} 强制约束: @@ -265,7 +271,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek 5) 不要新增字段,不要输出解释性文字,只输出 JSON。 输入数据: 分段方案:{thems} - 改进意见:{suggestion} + 改进意见和扣分原因:{suggestion} 字幕文本:格式为说话人:开始秒:结束秒:内容|下一段字幕。完整内容为:{captions.Captions} 输出格式(仅 JSON):{resFormat} """; @@ -284,7 +290,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek private async Task OptimizeSubtitles(VideoTask taskInfo, SenseVoiceRes[] captionsArr, string sections) { - if (!string.IsNullOrEmpty(taskInfo.CaptionsAI) && taskInfo.CaptionsAI!="[]") + if (!string.IsNullOrEmpty(taskInfo.CaptionsAI) && taskInfo.CaptionsAI != "[]") return JsonSerializer.Deserialize(taskInfo.CaptionsAI); var subject = taskInfo.Subject.ToString(); var newCaptionsList = new List(captionsArr.Length); @@ -292,9 +298,21 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var totalCount = captionsArr.Length / spanCount + 1; await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化"); - var chatClentArr = new GPTClient[] { deepSeekClient, chatGPTClient, geminiClient }; + Func>>[] chatClentArr = + [ + async (string m)=>await deepSeekClient + .ChatAsync>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Deepseek_Chat,8_000), + async (string m)=>await chatGPTClient + .ChatAsync>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.GPT5,16_000), + async (string m)=>await geminiClient + .ChatAsync>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Gemini_3_Chat,16_000), ]; await Parallel.ForAsync(0, totalCount, - new ParallelOptions() { MaxDegreeOfParallelism = 1 }, + new ParallelOptions() +#if DEBUG + { MaxDegreeOfParallelism = 1 }, +#else + { MaxDegreeOfParallelism = 9 }, +#endif async (s, c) => { var cArr = captionsArr @@ -302,43 +320,63 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek .Take(spanCount); if (cArr.Count() == 0) return; - var cStrArr = cArr.Select(s => s.Text); + var cStrArr = cArr.Adapt(); var nowCaptionStr = cStrArr.ToJson(); - var resFormat = """[string(修改结果)]"""; + var resFormat = """[{"t":时间(number),"r"字幕(string)}]"""; var postMessages = - $"角色设定:你是一位专业的中国{subject}学科专家,负责校对关于{sections}内容的课堂教学字幕。\n" + - $"任务描述:\n" + - $"请根据上下文逻辑,对输入的语音识别(STT)字幕进行深度优化。具体要求如下:\n" + - $"1. 逻辑纠错:结合{subject}学科背景,利用前后文语义修正所有错误词汇。不仅要修正同音错别词(如:树列改为数列),还要修正因识别模糊导致的语义断裂或学科术语错误。\n" + - $"2. 断句与标点:优化字幕的标点符号,并根据老师说话的语感和学科逻辑重新调整断句位置。确保每一条字幕在学术表达上自然、通顺,修复由于语音停顿造成的断句不当。\n" + - $"3. 公式规范:将字幕中提到的数学或科学公式统一转化为规范的 LaTeX 格式(使用$包裹公式,注意严格遵守Json格式的转义符号)。\n" + - $"强制约束:\n" + - $"- 数量对齐:输出的字幕条数(Array Length)必须与输入的字幕条数完全一致,严禁合并、拆分或删除任何条目。\n" + - $"- 纯净返回:只允许返回 JSON 格式的字符串,严禁包含任何前言、后缀或解释性文字。\n" + - $"- 数据格式:JSON 结构必须严格符合:{resFormat}\n" + - $"待优化字幕内容:\n" + - $"{nowCaptionStr}\n" + - $"最终核对:请确保输出 JSON 中包含的字幕条数与输入的字幕条数完全对应。"; - List? resData = null; - for (int i = 0; i < 3; i++) + $$""" + 角色设定 + 你是一位深耕中国{{subject}}教育领域的学科专家,拥有深厚的学术背景和文字校对经验,负责将{{sections}}内容的原始语音字幕(STT)转化为高质量的教学文稿。 + 核心任务 + 对输入的 JSON 数组进行逐条优化。你必须在保持【原数组长度不变】的前提下,完成以下处理: + 1. 术语纠错:基于{{subject}}学科,{{sections}}的背景,结合上下文的字幕修正同音错别字。 + 如:将“极地”修正为“极点”、“分母”误写为“父母”等。修复因断句导致的语义破碎。 + 即使语义不完整,也只能在当前字幕内做最小修复 + 2. 口语过滤:剔除“那个、然后、嗯、啊、就是”等冗余词汇。 + 若当前字幕在过滤后无有效信息,必须返回空字符串 "",但该数组位置必须保留。 + 3. 学术润色:在不改变老师原意的前提下,微调语序使其符合学术表达逻辑。 + 禁止重写为需要上下文才能成立的完整句 + 4. 公式标准:所有数学/科学公式必须转换为 LaTeX 格式。极其重要:在 JSON 字符串中,LaTeX 的反斜杠需双重转义,例如写成 $\\\\frac{a}{b}$,并且使用$包裹公式"。 + 强制执行约束(不可违背) + 数组保序:输出 JSON 数组的长度必须严格等于 {{cStrArr.Count()}}(即输入条数)。严禁合并、拆分或增减数组成员。 + 纯净 JSON:输出必须为合法的 JSON 字符串,禁止包含 ```json 等 Markdown 标记,禁止任何解释性文字。 + 待处理数据 {{nowCaptionStr}} + 强制执行约束(最高优先级,不可违背) + 1. 数组长度锁定:输出 JSON 数组长度必须严格等于 {{cStrArr.Count()}} + 2. 索引一一对应:第 N 条输出字幕只能由第 N 条输入字幕生成 + 3. 禁止跨条操作:严禁合并、拆分、移动、补充、重排字幕 + 4. 即使某条字幕为 "", 也不得将其语义转移到其他条目 + 5. 纯净 JSON:输出必须为合法 JSON 字符串,禁止 Markdown,禁止解释性文字 + + 输入数据结构 + t:表示开始时间 + r:需要优化的字幕内容 + 输出数据结构: + t:输入传入的t + r:优化后字幕内容 + 请开始处理。 + 在输出前,请自检:输出数组长度是否与输入完全一致。 + """; + List? resData = null; + for (int i = 0; i < 6; i++) { - resData = await chatClentArr[i].ChatAsync>(taskInfo.Id.ToString(), postMessages, "优化字幕", ChatGPTType.Deepseek_Chat, 8000); - if (resData.Count() == cArr.Count()) + resData = await chatClentArr[0](postMessages); + //var cc = resData.Select(s => s.Start); 检查差异化 + //var ccRes = cArr.Where(x => !cc.Contains(x.Start)); + if (cArr.Count() - resData.Count() < 5) break; else - await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 重试{i}"); + await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 重试{i} 剩余{captionsArr.Length - (decimal)newCaptionsList.Count}条字幕"); } - - if (resData.Count() != cArr.Count()) + if (cArr.Count() - resData.Count() > 5) { resData = cStrArr.ToList(); await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 采用原始值"); } - newCaptionsList.AddRange(resData.Select((text, i) => new SenseVoiceRes() + newCaptionsList.AddRange(resData.Select((el, i) => new SenseVoiceRes() { - Start = captionsArr[spanCount * s + i].Start, - End = captionsArr[spanCount * s + i].End, - Text = text, + Start = el.Start, + Text = el.Text, })); return; }); @@ -367,31 +405,33 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var keyFrameStr = string.IsNullOrEmpty(taskInfo?.PPTVideoCode) || string.IsNullOrEmpty(taskInfo?.PPTKeyFrame) ? $"请分析授课中字幕描述的知识内容,然后基于视频整体知识点讲解提炼出不同的阶段以便对老师上课内容切片提取为知识库,所以请确保阶段的内容准确性" : $"授课中老师的PPT在这些时间段内进行了切换{taskInfo.PPTKeyFrame},理应这些时间段内的讲述内容也发生了变化,请你基于PPT变化时间点结合字幕描述的知识内容提炼出不同的切片。" + - $"每个阶段的起始和结束应接近这些时间点(例如,以时间点为中心,扩展至内容自然过渡处)。"; + $"每个阶段的起始和结束应接近这些时间点(部分PPT时间段也可能不准确,请参考字幕内容)。"; var resFormat = """[{"StartTime":开始秒(number),"EndTime":结束秒(number),"Stage":阶段(string),"Theme":阶段主题(string),"Content":内容总结(string)}]"""; var reviewStr = taskInfo?.VideoType == AttachmentsInfoType.复习 - ? $"但本堂课是习题课,所以大部分阶段是不同的例题讲解内容。\n" + ? $"本堂课是习题课,绝大部分阶段是不同的例题讲解内容。\n" : string.Empty; var postMessages = string.Empty; postMessages = - $"请通过视频字幕内容分析出视频中课堂的授课知识点切片\n" + - $"课堂内容与{taskInfo.Subject}学科下的{sections}章节相关。\n" + - $"完整的课堂标准流程包含以下5个阶段:课程引入/新知讲解/例题精讲/课堂练习/知识总结。\n" + - reviewStr + - $"讲解知识内容的阶段的细分程度到某个知识点的讲解/认识/例题/总结\n" + - $"不分析课堂作业相关的内容我已经预处理了\n" + - $"初步划分阶段:{keyFrameStr}\n" + - $"Stage:判断阶段类型如果内容以解题为主,归类为“例题精讲”;如果涉及新知识讲解,归类为“新知讲解”;以此类推。\n" + - $"Content:简述单个阶段的核心讲解内容40~150字(如“例题”“证明”“练习”“总结”...), 必须完全基于字幕文本可推断的信息,禁止捏造不存在的内容(硬性条件)。\n" + - $"Theme:理解Content,提炼一个精确的主题(例如,“柯西不等式的基本应用”)。\n" + - $"输出要求:确保阶段划分合理、无重叠、\n" + - $"作业布置阶段一般出现在末尾如果有" + - $"输出格式要求:内容只返回json格式({resFormat})\n" + - $"字幕格式(开始秒:内容|下一段字幕).以下是包含时间的视频字幕文本。\n" + - $"字幕列表 {captions.Captions} 字幕结束!"; + $""" + 你是一位资深的教研专家,擅长从课堂实录中提取教学逻辑与知识点架构 + 课堂内容与{taskInfo.Subject}学科下的{sections}章节相关。 + 在分析过程中,请先提取出教师每一阶段的显性语言,再推导其背后的隐性教学意图,并对比该知识点在课程大纲中的权重 + 完整的课堂标准流程包含以下5个阶段:课程引入/新知讲解/例题精讲/课堂练习/知识总结。 + {reviewStr} + 讲解知识内容的阶段的细分程度到某个知识点的讲解/认识/例题/总结 + 不分析课堂作业相关的内容我已经预处理了 + 初步划分阶段:{keyFrameStr} + Stage:判断阶段类型如果内容以解题为主,归类为“例题精讲”;如果涉及新知识讲解,归类为“新知讲解”;以此类推。 + Content:简述单个阶段的核心讲解内容40~150字(如“例题”“证明”“练习”“总结”...), 必须完全基于字幕文本可推断的信息,禁止捏造不存在的内容(硬性条件)。 + Theme:理解Content,提炼一个精确的主题(例如,“柯西不等式的基本应用”)。 + 输出要求:确保阶段划分合理、无重叠、 + 输出格式要求:内容只返回json格式({resFormat}) + 字幕格式(开始秒:内容|下一段字幕).以下是包含时间的视频字幕文本。 + 字幕列表 {captions.Captions} 字幕结束! + """; await redisManager.AddTaskLog(taskInfo.Id, $"开始分析视频内容 {tryCount}"); - var res = await geminiClient.ChatAsync>(taskInfo.Id.ToString(), postMessages, "分析字幕"); + var res = await geminiClient.ChatAsync>(taskInfo.Id.ToString(), postMessages, "分析字幕", ChatGPTType.Gemini_3_Chat_thinking); return res; } catch (Exception ex) @@ -716,7 +756,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek if (questionRes is null) continue; //处理分段 知识点 List insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos); - + //校验结果质量 var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id); @@ -724,53 +764,48 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek await redisManager.AddTaskLog(taskInfo.Id, $"==>改进意见 {checkRes.Suggestion}"); await redisManager.AddTaskLog(taskInfo.Id, $"==>扣分原因 {checkRes.MinusScore}"); // 质量复检 - if (checkRes != null) - { - var improved = await ImproveSpanBySuggestion(questionRes, taskInfo, captions, sections, "扣分原因 {checkRes.MinusScore} \n 改进意见 {checkRes.Suggestion}"); - if (improved != null) - { - var improvedCheck = await VerifySpanQuality(improved, taskInfo, captions, sections, Course_Id); - await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后复检得分=>{improvedCheck.Score}"); - await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后扣分原因 {improvedCheck.MinusScore}"); - - if (improvedCheck != null && improvedCheck.Score >= 90 && improvedCheck.Score > checkRes.Score) - { - questionRes = improved; - if (homework != null && (!questionRes.Any(s => s.Stage == StageEnum.作业布置.ToString()))) - questionRes.Add(homework); - - insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos); - await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id); - await videoTaskStageDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id); - var tStage = insertData.GroupBy(s => s.StageId).Select(s => new VideoTaskStage - { - Id = s.Key, - TagId = s.First().TagId, - CloudSchoolId = s.First().CloudSchoolId, - StartTime = s.First().StartTime, - EndTime = s.First().EndTime, - Content = s.First().Content, - TextbookSource = s.First().TextbookSource, - Stage = s.First().Stage, - Theme = s.First().Theme, - VideoTaskId = taskInfo.Id, - }).ToArray(); - await videoTaskStageDB.InsertRangeAsync(tStage); - await videoKonwPointDB.InsertRangeAsync(insertData); - break; - } - else - { - await redisManager.AddTaskLog(taskInfo.Id, $"==>优化之后的得分降低/得分过低"); - continue; - } - } - } + //if (checkRes != null) + //{ + // var improved = await ImproveSpanBySuggestion(questionRes, taskInfo, captions, sections, "扣分原因 {checkRes.MinusScore} \n 改进意见 {checkRes.Suggestion}"); + // var improvedCheck = await VerifySpanQuality(improved, taskInfo, captions, sections, Course_Id); + // await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后复检得分=>{improvedCheck.Score}"); + // await redisManager.AddTaskLog(taskInfo.Id, $"==>优化后扣分原因 {improvedCheck.MinusScore}"); + // if (improved != null) + // { + // if (improvedCheck != null && improvedCheck.Score >= 90 && improvedCheck.Score > checkRes.Score) + // { + // questionRes = improved; + // } + // else + // { + // await redisManager.AddTaskLog(taskInfo.Id, $"==>优化之后的得分降低/得分过低"); + // continue; + // } + // } + //} if (checkRes != null && checkRes.Score >= 90) { //写入知识点 await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id); + await videoTaskStageDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id); + var tStage = insertData.GroupBy(s => s.StageId).Select(s => new VideoTaskStage + { + Id = s.Key, + TagId = s.First().TagId, + CloudSchoolId = s.First().CloudSchoolId, + StartTime = s.First().StartTime, + EndTime = s.First().EndTime, + Content = s.First().Content, + TextbookSource = s.First().TextbookSource, + Stage = s.First().Stage, + Theme = s.First().Theme, + VideoTaskId = taskInfo.Id, + }).ToList(); + //尝试追加 作业布置分段 + if (homework != null && (!questionRes.Any(s => s.Stage == StageEnum.作业布置.ToString()))) + tStage.Add(homework.Adapt()); + await videoTaskStageDB.InsertRangeAsync(tStage); await videoKonwPointDB.InsertRangeAsync(insertData); break; } diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs b/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs index 99cc18c..5f1d025 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs @@ -94,27 +94,22 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx config.ModelConfig.Debug = 1; #endif OR = new OfflineRecognizer(config); - - - - - - - var AIModelVersion_251217 = "sherpa-onnx-sense-voice-funasr-nano-2025-12-17"; - OfflineRecognizerConfig config1 = new OfflineRecognizerConfig(); - config1.FeatConfig.SampleRate = 16000; - config1.FeatConfig.FeatureDim = 80; - config1.ModelConfig.Tokens = Path.Combine(AppCommon.AIModelFile, AIModelVersion_251217, "tokens.txt"); - config1.ModelConfig.SenseVoice.Model = Path.Combine(AppCommon.AIModelFile, AIModelVersion_251217, "model.onnx"); - //1 使用逆文本规范化处理感官语音 [控制标点符号生成]。 - config1.ModelConfig.SenseVoice.UseInverseTextNormalization = 1; - config1.ModelConfig.SenseVoice.Language = "zh"; - config1.ModelConfig.ModelType = string.Empty; - config1.ModelConfig.NumThreads = numThreads; - config1.ModelConfig.Provider = "cpu"; - config1.DecodingMethod = "greedy_search"; - config1.ModelConfig.Debug = 1; - OR1 = new OfflineRecognizer(config: config1); + + //var AIModelVersion_251217 = "sherpa-onnx-sense-voice-funasr-nano-2025-12-17"; + //OfflineRecognizerConfig config1 = new OfflineRecognizerConfig(); + //config1.FeatConfig.SampleRate = 16000; + //config1.FeatConfig.FeatureDim = 80; + //config1.ModelConfig.Tokens = Path.Combine(AppCommon.AIModelFile, AIModelVersion_251217, "tokens.txt"); + //config1.ModelConfig.SenseVoice.Model = Path.Combine(AppCommon.AIModelFile, AIModelVersion_251217, "model.onnx"); + ////1 使用逆文本规范化处理感官语音 [控制标点符号生成]。 + //config1.ModelConfig.SenseVoice.UseInverseTextNormalization = 1; + //config1.ModelConfig.SenseVoice.Language = "zh"; + //config1.ModelConfig.ModelType = string.Empty; + //config1.ModelConfig.NumThreads = numThreads; + //config1.ModelConfig.Provider = "cpu"; + //config1.DecodingMethod = "greedy_search"; + //config1.ModelConfig.Debug = 1; + //OR1 = new OfflineRecognizer(config: config1); //OR1 = FunASRNano.OR; } @@ -143,7 +138,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx throw new Exception("task 音频路径未找到"); if (OR is null) Init(); serviceProvider.GetRequiredService() - .TaskHandle(new WaveReader(filePath), task, SoundHandle, SherpaVadVersion.silero_vad_v5); + .TaskHandle(new WaveReader(filePath), task, SoundHandle, SherpaVadVersion.ten_vad_324); return Task.CompletedTask; } diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/SherpaOnnxDto.cs b/VideoAnalysisCore/AICore/SherpaOnnx/SherpaOnnxDto.cs index 7bb63dd..a9408cc 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/SherpaOnnxDto.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/SherpaOnnxDto.cs @@ -1,7 +1,23 @@ -using Whisper.net; +using System.Text.Json.Serialization; +using Whisper.net; namespace VideoAnalysisCore.AICore.SherpaOnnx { + + public class SenseVoiceInput() + { + + /// + /// 文本 + /// + [JsonPropertyName("r")] + public string Text { get; set; } = string.Empty; + /// + /// 开始时间 + /// + [JsonPropertyName("t")] + public float Start { get; set; } + } /// /// 字幕识别 结果 /// diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/SherpaVad.cs b/VideoAnalysisCore/AICore/SherpaOnnx/SherpaVad.cs index 3e1d983..25aae3b 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/SherpaVad.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/SherpaVad.cs @@ -52,6 +52,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx static VadModelConfig VADModelConfig = default!; private readonly RedisManager redisManager; + private int WindowSize = 512; private readonly IServiceProvider serviceProvider; private readonly VoiceActivityDetector vad; private Func Callback; @@ -63,14 +64,11 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx this.serviceProvider = serviceProvider; VADModelConfig = new VadModelConfig(); - VADModelConfig.SampleRate = 16000; - VADModelConfig.NumThreads = 1; - VADModelConfig.Provider = "cpu"; #if DEBUG VADModelConfig.Debug = 1; #endif - VADModelConfig.SileroVad = new SileroVadModelConfig(); - VADModelConfig.TenVad = new TenVadModelConfig(); + + } /// @@ -84,15 +82,36 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx { VADModelConfig.NumThreads = numThreads; VADModelConfig.Provider = useGPU? "cuda" : "cpu"; - var path = Path.Combine(AppCommon.AIModelFile, "vad", SherpaVadVersion.silero_vad_v5); + var path = Path.Combine(AppCommon.AIModelFile, "vad", vadVersion); switch (vadVersion) { case SherpaVadVersion.silero_vad_v4: case SherpaVadVersion.silero_vad_v5: + VADModelConfig.SileroVad = new SileroVadModelConfig(); VADModelConfig.SileroVad.Model = path; + //(阈值 / 灵敏度) 含义:判定为“语音”的置信度。取值范围通常在 0 到 1 之间。 + VADModelConfig.SileroVad.Threshold = 0.3f; + //(最小静音长度)秒。 含义:“要沉默多久,我才认为这句话说完了?” + VADModelConfig.SileroVad.MinSilenceDuration = 0.2f; + // (最小语音长度)秒 含义:“这段声音至少要多长,我才认为它是有效的说话?” + VADModelConfig.SileroVad.MinSpeechDuration = 0.2f; + //(最大语音长度)秒 + VADModelConfig.SileroVad.MaxSpeechDuration = 3.5f; + WindowSize = VADModelConfig.SileroVad.WindowSize; break; case SherpaVadVersion.ten_vad_324: + VADModelConfig.TenVad = new TenVadModelConfig(); VADModelConfig.TenVad.Model = path; + //(阈值 / 灵敏度) 含义:判定为“语音”的置信度。取值范围通常在 0 到 1 之间。 + VADModelConfig.TenVad.Threshold = 0.3f; + //(最小静音长度)秒。 含义:“要沉默多久,我才认为这句话说完了?” + VADModelConfig.TenVad.MinSilenceDuration = 0.2f; + // (最小语音长度)秒 含义:“这段声音至少要多长,我才认为它是有效的说话?” + VADModelConfig.TenVad.MinSpeechDuration = 0.2f; + //(最大语音长度)秒 + VADModelConfig.TenVad.MaxSpeechDuration = 3.5f; + VADModelConfig.TenVad.WindowSize = 256; + WindowSize = VADModelConfig.TenVad.WindowSize; break; default: break; @@ -118,41 +137,47 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx // 使用 Span 操作原始数据 ReadOnlySpan allSamples = reader.Samples.AsSpan(); int numSamples = allSamples.Length; - int windowSize = VADModelConfig.SileroVad.WindowSize; int sampleRate = VADModelConfig.SampleRate; - int numIter = numSamples / windowSize; + int numIter = numSamples / WindowSize; var totalSecond = numSamples / (float)sampleRate; var res = new List(500); - - using var VAD = new VoiceActivityDetector(VADModelConfig, bufferSizeInSeconds: 30); + VoiceActivityDetector vad; + try + { + vad = new VoiceActivityDetector(VADModelConfig, bufferSizeInSeconds: 20); + } + catch (Exception ex) + { + throw; + } // 优化:复用缓冲区,避免在循环中重复分配内存 - float[] buffer = new float[windowSize]; + float[] buffer = new float[WindowSize]; for (int i = 0; i != numIter; ++i) { - int start = i * windowSize; + int start = i * WindowSize; // 使用 Span 高效复制数据到固定缓冲区 - allSamples.Slice(start, windowSize).CopyTo(buffer); + allSamples.Slice(start, WindowSize).CopyTo(buffer); - VAD.AcceptWaveform(buffer); + vad.AcceptWaveform(buffer); //是否检测到语音 - if (VAD.IsSpeechDetected()) + if (vad.IsSpeechDetected()) { //获取最新的发言片段 - while (!VAD.IsEmpty()) + while (!vad.IsEmpty()) { - var p = ReadNext(VAD,res, totalSecond); + var p = ReadNext(vad,res, totalSecond); if (p != null) redisManager.SetTaskProgress(task, p + "%"); } } } - VAD.Flush(); - while (!VAD.IsEmpty()) + vad.Flush(); + while (!vad.IsEmpty()) { - var p = ReadNext(VAD, res, totalSecond); + var p = ReadNext(vad, res, totalSecond); if(p!= null) redisManager.SetTaskProgress(task, p + "%"); } //如果携带任务ID @@ -169,6 +194,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx //分析完成视频字幕后继续接收任务 //redisManager.NewTask(); } + vad.Dispose(); return res; } /// diff --git a/VideoAnalysisCore/Controllers/Dto/ApiDto.cs b/VideoAnalysisCore/Controllers/Dto/ApiDto.cs index f35f3d9..fb29636 100644 --- a/VideoAnalysisCore/Controllers/Dto/ApiDto.cs +++ b/VideoAnalysisCore/Controllers/Dto/ApiDto.cs @@ -237,6 +237,7 @@ namespace VideoAnalysisCore.Controllers.Dto /// 知识点ID /// public string KnowPointId { get; set; } + public float KnowWeight { get; set; } } public class TaskKnowBlock diff --git a/VideoAnalysisCore/Controllers/LJZK_Controller.cs b/VideoAnalysisCore/Controllers/LJZK_Controller.cs index 156d155..37cca9a 100644 --- a/VideoAnalysisCore/Controllers/LJZK_Controller.cs +++ b/VideoAnalysisCore/Controllers/LJZK_Controller.cs @@ -244,6 +244,7 @@ namespace VideoAnalysisCore.Controllers Id = x.Id, KnowPoint = x.KnowPoint, KnowPointId = x.KnowPointId, + KnowWeight = x.KnowPointWeight??0f, })?.ToArray() : null }).ToArray() diff --git a/VideoAnalysisCore/Model/VideoTaskStage.cs b/VideoAnalysisCore/Model/VideoTaskStage.cs index 8ed5c89..fd75ca5 100644 --- a/VideoAnalysisCore/Model/VideoTaskStage.cs +++ b/VideoAnalysisCore/Model/VideoTaskStage.cs @@ -8,6 +8,7 @@ using VideoAnalysisCore.AICore.SherpaOnnx; using VideoAnalysisCore.Model.Enum; using VideoAnalysisCore.Model.Interface; using Whisper.net; +using Yitter.IdGenerator; namespace VideoAnalysisCore.Model { @@ -21,7 +22,7 @@ namespace VideoAnalysisCore.Model /// id /// [SugarColumn(IsPrimaryKey = true)] - public long Id { get; set; } + public long Id { get; set; } = YitIdHelper.NextId(); /// /// 视频任务id ///