Compare commits
No commits in common. "f8a758ff40afe0aca7f5c0b1bd65eab42e0aaac1" and "a2d14487cb9a0c6b8a3ea57a1581afdd6e83b8a3" have entirely different histories.
f8a758ff40
...
a2d14487cb
|
|
@ -44,7 +44,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
|
|||
/// <param name="max_tokens">最大token <para>不设置默认最大值 16000/8000</para></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public override async Task<T> ChatAsync<T>(string task, string postMessages, string title, string model = null, int max_tokens = 32000)
|
||||
public override async Task<T> ChatAsync<T>(string task, string postMessages, string title, string model = null, int max_tokens = 16000)
|
||||
{
|
||||
Message[] messageArr = [
|
||||
new Message(postMessages,"user"),
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ namespace VideoAnalysisCore.AICore.GPT
|
|||
{
|
||||
public class ChatGPTType
|
||||
{
|
||||
public const string GPT5_mini = "gpt-5-mini";
|
||||
public const string GPT5 = "gpt-5-2025-08-07";
|
||||
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 Deepseek_Reasoner = "deepseek-reasoner";
|
||||
public const string Deepseek_Chat = "deepseek-chat";
|
||||
public static string Deepseek_Reasoner = "deepseek-reasoner";
|
||||
public static string Deepseek_Chat = "deepseek-chat";
|
||||
|
||||
|
||||
public const string Gemini_3_Chat_thinking = "gemini-3-pro-preview-thinking";
|
||||
public const string Gemini_3_Chat = "gemini-3-pro-preview";
|
||||
public static string Gemini_3_Chat_thinking = "gemini-3-pro-preview-thinking";
|
||||
public static string Gemini_3_Chat = "gemini-3-pro-preview";
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,19 +42,17 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
|
|||
/// <param name="max_tokens">最大token <para>不设置默认最大值 16000/8000</para></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public override async Task<T> ChatAsync<T>(string task, string postMessages, string title, string model = ChatGPTType.Deepseek_Chat, int max_tokens = 8000)
|
||||
public override async Task<T> ChatAsync<T>(string task, string postMessages, string title, string model =null, int max_tokens = 32000)
|
||||
{
|
||||
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_Chat,
|
||||
model = model ?? ChatGPTType.Deepseek_Reasoner,
|
||||
max_tokens = model == ChatGPTType.Deepseek_Reasoner ? 32000 : max_tokens,
|
||||
stream = true,
|
||||
temperature = 0.2f,
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ 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
|
||||
{
|
||||
|
|
@ -90,7 +88,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
角色:你是一位{taskInfo.Subject}学科教研老师。
|
||||
任务:为每个【视频分段】分配对应的知识点(可多个),并补充来源信息。
|
||||
字段说明:
|
||||
- TextbookSource:该分段讲授内容所属教材来源(抓取字幕中的 关键词来识别 默认情况下是PPT),仅允许取值:PPT/课本/试卷/挹青苑
|
||||
- TextbookSource:该分段讲授内容所属教材来源,仅允许取值:课本/试卷/挹青苑/其他
|
||||
- KnowPoints:数组。每个元素代表一个知识点匹配结果
|
||||
- KnowPoint:知识点名称(必须来自我提供的列表)
|
||||
- KnowPointId:知识点ID(必须与 KnowPoint 对应)
|
||||
|
|
@ -144,7 +142,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(),
|
||||
|
|
@ -215,21 +213,17 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
var checkMessage =
|
||||
$"""
|
||||
请你担任一位专业的视频内容分析教研老师,擅长评估视频内容的结构和逻辑流暢度。
|
||||
核心任务: 请根据我提供的【分段方案】【完整字幕文本】,对该分段方案进行严谨评估。
|
||||
核心任务: 请根据我提供的【视频分段方案】【完整字幕文本】,对该分段方案进行严谨评估。
|
||||
补充上下文:{pptFormat}
|
||||
本节课所属章节:{sections}
|
||||
内容结构与主题合理性:
|
||||
分段准确性:评估单个分段内的课堂字幕内容与分段的Theme/Conten匹配、是否存在错误,捏造的情况(硬性指标)。
|
||||
知识点分配:检查分段内的知识点是否与分段Conten有关联,知识点分配给这个分段是否合理(硬性指标)。
|
||||
逻辑过渡:评估分段之间的过渡是否自然流畅,后一段是否是前一段内容的合理延伸或转折。
|
||||
额外补充:
|
||||
1. 忽略掉分段没有结束时间的问题我会自己处理
|
||||
2. 评估的分数只针对分段方案的内容是否与实际字幕描述内容符合
|
||||
3. 字幕内容仅供参考不被计入扣分项
|
||||
综合评分:
|
||||
请基于以上分析,提供一个0-100的综合得分(70分及格,打分一定要严谨,总分一定要准确)。
|
||||
MinusScore: 详细说明打分理由,并逐条对应到上述评估维度。
|
||||
Suggestion: 基于扣分原因提出针对分段方案的改进意见
|
||||
Suggestion: 基于扣分原因提出针对分段方案的改进意见(请忽略掉分段没有结束时间的问题我会自己处理)。
|
||||
输入数据格式说明:
|
||||
分段方案: {thems}
|
||||
字幕文本: 格式为说话人:开始秒:结束秒:内容|下一段字幕。完整内容为:{captions.Captions}
|
||||
|
|
@ -260,7 +254,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
var message =
|
||||
$"""
|
||||
请你担任一位专业的视频内容分析教研老师,擅长根据评估意见修复视频分段方案。
|
||||
目标:在不改变片段数量的前提下,基于【改进意见和扣分原因】对【分段方案】做最小必要修改,使其更符合课堂内容的自然结构。
|
||||
目标:在不改变片段数量的前提下,基于【改进意见】对【分段方案】做最小必要修改,使其更符合课堂内容的自然结构。
|
||||
补充上下文:{pptFormat}
|
||||
本节课所属章节:{sections}
|
||||
强制约束:
|
||||
|
|
@ -271,7 +265,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
5) 不要新增字段,不要输出解释性文字,只输出 JSON。
|
||||
输入数据:
|
||||
分段方案:{thems}
|
||||
改进意见和扣分原因:{suggestion}
|
||||
改进意见:{suggestion}
|
||||
字幕文本:格式为说话人:开始秒:结束秒:内容|下一段字幕。完整内容为:{captions.Captions}
|
||||
输出格式(仅 JSON):{resFormat}
|
||||
""";
|
||||
|
|
@ -290,7 +284,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
private async Task<SenseVoiceRes[]> OptimizeSubtitles(VideoTask taskInfo,
|
||||
SenseVoiceRes[] captionsArr, string sections)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(taskInfo.CaptionsAI) && taskInfo.CaptionsAI != "[]")
|
||||
if (!string.IsNullOrEmpty(taskInfo.CaptionsAI) && taskInfo.CaptionsAI!="[]")
|
||||
return JsonSerializer.Deserialize<SenseVoiceRes[]>(taskInfo.CaptionsAI);
|
||||
var subject = taskInfo.Subject.ToString();
|
||||
var newCaptionsList = new List<SenseVoiceRes>(captionsArr.Length);
|
||||
|
|
@ -298,21 +292,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
var totalCount = captionsArr.Length / spanCount + 1;
|
||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化");
|
||||
|
||||
Func<string, Task<List<SenseVoiceInput>>>[] chatClentArr =
|
||||
[
|
||||
async (string m)=>await deepSeekClient
|
||||
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Deepseek_Chat,8_000),
|
||||
async (string m)=>await chatGPTClient
|
||||
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.GPT5,16_000),
|
||||
async (string m)=>await geminiClient
|
||||
.ChatAsync<List<SenseVoiceInput>>(taskInfo.Id.ToString(), m, "优化字幕",ChatGPTType.Gemini_3_Chat,16_000), ];
|
||||
var chatClentArr = new GPTClient[] { deepSeekClient, chatGPTClient, geminiClient };
|
||||
await Parallel.ForAsync(0, totalCount,
|
||||
new ParallelOptions()
|
||||
#if DEBUG
|
||||
{ MaxDegreeOfParallelism = 1 },
|
||||
#else
|
||||
{ MaxDegreeOfParallelism = 9 },
|
||||
#endif
|
||||
new ParallelOptions() { MaxDegreeOfParallelism = 1 },
|
||||
async (s, c) =>
|
||||
{
|
||||
var cArr = captionsArr
|
||||
|
|
@ -320,58 +302,43 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
.Take(spanCount);
|
||||
if (cArr.Count() == 0)
|
||||
return;
|
||||
var cStrArr = cArr.Adapt<SenseVoiceInput[]>();
|
||||
var cStrArr = cArr.Select(s => s.Text);
|
||||
var nowCaptionStr = cStrArr.ToJson();
|
||||
var resFormat = """[{"t":时间(number),"r"字幕(string)}]""";
|
||||
var resFormat = """[string(修改结果)]""";
|
||||
var postMessages =
|
||||
$$"""
|
||||
# Role
|
||||
你是一位{{subject}}学科的教育专家与资深校对。你的任务是将{{sections}}内容的原始语音识别(STT)JSON 数据清洗为高质量教学文本。
|
||||
# Input & Output Protocol
|
||||
输入和输出均为严格的 JSON 数组格式:`[{"t": number, "r": string}]`。
|
||||
* `t` (Time): 绝对锚点,代表时间戳。严禁修改、严禁排序、严禁删除。
|
||||
* `r` (Result): 字幕内容,仅对此字段进行清洗。
|
||||
# Processing Rules (按优先级执行)
|
||||
1. 结构铁律 (最高优先级):
|
||||
长度一致:输入 N 条,输出必须 N 条。
|
||||
时间锁定:输出的第 i 条 `t` 必须等于输入的第 i 条 `t`。
|
||||
原地修正:禁止跨行移动文本。即使一句话被切分在两行,也不要合并它们,只需分别修正各自行的错别字即可。
|
||||
|
||||
2. 上下文关联修正 (核心任务):
|
||||
全局理解,局部修正:请阅读前后多行内容来确定当前行特定词汇的含义,解决同音错别字。
|
||||
案例:如果上一行是“求函数的”,当前行是“集值”,结合数学上下文应修正为“极值”。
|
||||
逻辑:修正依据是“上下文语义”,但修正范围仅限“当前行文本”。
|
||||
|
||||
3. 内容清洗标准:
|
||||
口语过滤:删除“那个、然后、嗯、啊、就是”等无意义填充词。若该行被清空,`r` 设为 `""`,保留对象。
|
||||
LaTeX 格式化:数学/科学公式必须转换为 LaTeX (如 `$\\frac{a}{b}$`),JSON 中反斜杠需双重转义 (`\\\\`)。
|
||||
# Task Data
|
||||
待处理数据 (共 {{cStrArr.Count()}} 条):
|
||||
{{nowCaptionStr}}
|
||||
|
||||
# Action
|
||||
请立即开始处理。输出结果必须是纯净的 JSON 字符串,严格遵守上述规则。
|
||||
""";
|
||||
List<SenseVoiceInput>? resData = null;
|
||||
for (int i = 0; i < 6; i++)
|
||||
$"角色设定:你是一位专业的中国{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<string>? resData = null;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
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)
|
||||
resData = await chatClentArr[i].ChatAsync<List<string>>(taskInfo.Id.ToString(), postMessages, "优化字幕", ChatGPTType.Deepseek_Chat, 8000);
|
||||
if (resData.Count() == cArr.Count())
|
||||
break;
|
||||
else
|
||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 重试{i} 剩余{captionsArr.Length - (decimal)newCaptionsList.Count}条字幕");
|
||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 重试{i}");
|
||||
}
|
||||
if (cArr.Count() - resData.Count() > 5)
|
||||
|
||||
if (resData.Count() != cArr.Count())
|
||||
{
|
||||
resData = cStrArr.ToList();
|
||||
await redisManager.AddTaskLog(taskInfo.Id, $"==>字幕优化 分段{s} AI结果数量不匹配 采用原始值");
|
||||
}
|
||||
newCaptionsList.AddRange(resData.Select((el, i) => new SenseVoiceRes()
|
||||
newCaptionsList.AddRange(resData.Select((text, i) => new SenseVoiceRes()
|
||||
{
|
||||
Start = el.Start,
|
||||
Text = el.Text,
|
||||
Start = captionsArr[spanCount * s + i].Start,
|
||||
End = captionsArr[spanCount * s + i].End,
|
||||
Text = text,
|
||||
}));
|
||||
return;
|
||||
});
|
||||
|
|
@ -400,33 +367,31 @@ 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 =
|
||||
$"""
|
||||
你是一位资深的教研专家,擅长从课堂实录中提取教学逻辑与知识点架构
|
||||
课堂内容与{taskInfo.Subject}学科下的{sections}章节相关。
|
||||
在分析过程中,请先提取出教师每一阶段的显性语言,再推导其背后的隐性教学意图,并对比该知识点在课程大纲中的权重
|
||||
完整的课堂标准流程包含以下5个阶段:课程引入/新知讲解/例题精讲/课堂练习/知识总结。
|
||||
{reviewStr}
|
||||
讲解知识内容的阶段的细分程度到某个知识点的讲解/认识/例题/总结
|
||||
不分析课堂作业相关的内容我已经预处理了
|
||||
初步划分阶段:{keyFrameStr}
|
||||
Stage:判断阶段类型如果内容以解题为主,归类为“例题精讲”;如果涉及新知识讲解,归类为“新知讲解”;以此类推。
|
||||
Content:简述单个阶段的核心讲解内容40~150字(如“例题”“证明”“练习”“总结”...), 必须完全基于字幕文本可推断的信息,禁止捏造不存在的内容(硬性条件)。
|
||||
Theme:理解Content,提炼一个精确的主题(例如,“柯西不等式的基本应用”)。
|
||||
输出要求:确保阶段划分合理、无重叠、
|
||||
输出格式要求:内容只返回json格式({resFormat})
|
||||
字幕格式(开始秒:内容|下一段字幕).以下是包含时间的视频字幕文本。
|
||||
字幕列表 {captions.Captions} 字幕结束!
|
||||
""";
|
||||
$"请通过视频字幕内容分析出视频中课堂的授课知识点切片\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} 字幕结束!";
|
||||
|
||||
await redisManager.AddTaskLog(taskInfo.Id, $"开始分析视频内容 {tryCount}");
|
||||
var res = await geminiClient.ChatAsync<List<VideoKnowRes>>(taskInfo.Id.ToString(), postMessages, "分析字幕", ChatGPTType.Gemini_3_Chat_thinking);
|
||||
var res = await geminiClient.ChatAsync<List<VideoKnowRes>>(taskInfo.Id.ToString(), postMessages, "分析字幕");
|
||||
return res;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -751,7 +716,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
if (questionRes is null) continue;
|
||||
//处理分段 知识点
|
||||
List<VideoKonwPoint> insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos);
|
||||
|
||||
|
||||
//校验结果质量
|
||||
var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id);
|
||||
|
||||
|
|
@ -759,48 +724,53 @@ 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}");
|
||||
// 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)
|
||||
{
|
||||
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 && 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<VideoTaskStage>());
|
||||
await videoTaskStageDB.InsertRangeAsync(tStage);
|
||||
await videoKonwPointDB.InsertRangeAsync(insertData);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,22 +94,27 @@ 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;
|
||||
|
||||
}
|
||||
|
|
@ -138,7 +143,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
|||
throw new Exception("task 音频路径未找到");
|
||||
if (OR is null) Init();
|
||||
serviceProvider.GetRequiredService<SherpaVad>()
|
||||
.TaskHandle(new WaveReader(filePath), task, SoundHandle, SherpaVadVersion.ten_vad_324);
|
||||
.TaskHandle(new WaveReader(filePath), task, SoundHandle, SherpaVadVersion.silero_vad_v5);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,7 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Whisper.net;
|
||||
using Whisper.net;
|
||||
|
||||
namespace VideoAnalysisCore.AICore.SherpaOnnx
|
||||
{
|
||||
|
||||
public class SenseVoiceInput()
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 文本
|
||||
/// </summary>
|
||||
[JsonPropertyName("r")]
|
||||
public string Text { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("t")]
|
||||
public float Start { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 字幕识别 结果
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ 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<int, float[], OfflineStream> Callback;
|
||||
|
|
@ -64,11 +63,14 @@ 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -82,36 +84,15 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
|||
{
|
||||
VADModelConfig.NumThreads = numThreads;
|
||||
VADModelConfig.Provider = useGPU? "cuda" : "cpu";
|
||||
var path = Path.Combine(AppCommon.AIModelFile, "vad", vadVersion);
|
||||
var path = Path.Combine(AppCommon.AIModelFile, "vad", SherpaVadVersion.silero_vad_v5);
|
||||
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;
|
||||
|
|
@ -137,47 +118,41 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
|||
// 使用 Span 操作原始数据
|
||||
ReadOnlySpan<float> 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<SenseVoiceRes>(500);
|
||||
VoiceActivityDetector vad;
|
||||
try
|
||||
{
|
||||
vad = new VoiceActivityDetector(VADModelConfig, bufferSizeInSeconds: 20);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
using var VAD = new VoiceActivityDetector(VADModelConfig, bufferSizeInSeconds: 30);
|
||||
|
||||
// 优化:复用缓冲区,避免在循环中重复分配内存
|
||||
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
|
||||
|
|
@ -194,7 +169,6 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
|
|||
//分析完成视频字幕后继续接收任务
|
||||
//redisManager.NewTask();
|
||||
}
|
||||
vad.Dispose();
|
||||
return res;
|
||||
}
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -237,7 +237,6 @@ namespace VideoAnalysisCore.Controllers.Dto
|
|||
/// 知识点ID
|
||||
/// </summary>
|
||||
public string KnowPointId { get; set; }
|
||||
public float KnowWeight { get; set; }
|
||||
|
||||
}
|
||||
public class TaskKnowBlock
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ namespace VideoAnalysisCore.Controllers
|
|||
private readonly Repository<NodePackageInfo> nodePackageInfoDB;
|
||||
private readonly Repository<VideoQuestion> videoQuestionDB;
|
||||
private readonly Repository<VideoQuestionKonw> videoQuestionKonwDB;
|
||||
private readonly RedisManager redisManager;
|
||||
private readonly RedisManager redisManager;
|
||||
|
||||
public LJZK_Controller(IMapper mp, Repository<NodeSubscription> nodesubscriptionDB,
|
||||
Repository<VideoTask> videoTaskDB = null, Repository<VideoKonwPoint> videoKonwPointDB = null
|
||||
|
|
@ -244,7 +244,6 @@ namespace VideoAnalysisCore.Controllers
|
|||
Id = x.Id,
|
||||
KnowPoint = x.KnowPoint,
|
||||
KnowPointId = x.KnowPointId,
|
||||
KnowWeight = x.KnowPointWeight??0f,
|
||||
})?.ToArray()
|
||||
: null
|
||||
}).ToArray()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using VideoAnalysisCore.AICore.SherpaOnnx;
|
|||
using VideoAnalysisCore.Model.Enum;
|
||||
using VideoAnalysisCore.Model.Interface;
|
||||
using Whisper.net;
|
||||
using Yitter.IdGenerator;
|
||||
|
||||
namespace VideoAnalysisCore.Model
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@ namespace VideoAnalysisCore.Model
|
|||
/// id
|
||||
/// </summary>
|
||||
[SugarColumn(IsPrimaryKey = true)]
|
||||
public long Id { get; set; } = YitIdHelper.NextId();
|
||||
public long Id { get; set; }
|
||||
/// <summary>
|
||||
/// 视频任务id
|
||||
/// <see cref="VideoTask.Id"/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue