diff --git a/VideoAnalysisCore/AICore/GPT/BserGPT.cs b/VideoAnalysisCore/AICore/GPT/BserGPT.cs index 4fa3ca3..eccc918 100644 --- a/VideoAnalysisCore/AICore/GPT/BserGPT.cs +++ b/VideoAnalysisCore/AICore/GPT/BserGPT.cs @@ -191,6 +191,7 @@ namespace VideoAnalysisCore.AICore.GPT { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs index 6a47fda..dd646eb 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs @@ -55,7 +55,8 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT { taskId = task, model = model, - max_tokens =8000, + title = title, + max_tokens =8000, stream = true, temperature = 0.2f, messages = messageArr diff --git a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs index 3544f07..5be5b2d 100644 --- a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekGPTClient.cs @@ -51,6 +51,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT var chatReq = new ChatRequest { taskId = task, + title = title, model = model ?? ChatGPTType.Deepseek_Reasoner, max_tokens = model == ChatGPTType.Deepseek_Reasoner ? 16000 : max_tokens, stream = true, diff --git a/VideoAnalysisCore/AICore/GPT/GPTClient.cs b/VideoAnalysisCore/AICore/GPT/GPTClient.cs index 441e737..2b1946d 100644 --- a/VideoAnalysisCore/AICore/GPT/GPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/GPTClient.cs @@ -66,10 +66,9 @@ namespace VideoAnalysisCore.AICore.GPT /// Return HttpResponseMessage for SSE public async Task<(Usage u, string res, string reasoning)?> ChatSSE(ChatRequest chatReq) { - var requestBody = chatReq.ToJson(); var i = 5; PostJsonStream: - var chatResp = await PostJsonStreamAsync(Config.Host + Config.Path, requestBody, Config.ApiKey); + var chatResp = await PostJsonStreamAsync(Config.Host + Config.Path, chatReq, Config.ApiKey); if (!chatResp.IsSuccessStatusCode) { await redisManager.AddTaskLog(chatReq.taskId, "=>请求GPT服务器异常 " + chatResp?.StatusCode + " " + await chatResp.Content.ReadAsStringAsync()); @@ -182,7 +181,7 @@ namespace VideoAnalysisCore.AICore.GPT chatResContent = chatResContent?.Trim(); if (string.IsNullOrEmpty(chatResContent)) - throw new Exception("GPT返回结果无有效JSON"); + throw new Exception($"GPT返回结果无有效JSON =>{chatResp?.res}"); var startsStr = typeof(T).IsArray ? "[" : "{"; var endStr = typeof(T).IsArray ? "]" : "}"; if (!chatResContent.StartsWith(startsStr)) @@ -211,8 +210,10 @@ namespace VideoAnalysisCore.AICore.GPT } public async Task PostJsonStreamAsync( - string path, string json, string apiKey, bool readAll = false) + string path, ChatRequest data, string apiKey, bool readAll = false) { + + var json = data.ToJson(); var uriBuilder = new UriBuilder(path); var maxRestart = 4; var errorMSG = new Exception[maxRestart]; @@ -237,11 +238,14 @@ namespace VideoAnalysisCore.AICore.GPT catch (Exception e) { errorMSG[i] = e; - Console.WriteLine("====================[请求异常,重试]===================="); - Console.WriteLine(uriBuilder.Uri); - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); - Console.WriteLine("=============================================="); + var msg = $""" + ====================[{DateTime.Now.ToString("MM-dd HH:mm:ss")} 请求异常,重试]==================== + {uriBuilder.Uri} + {e.Message} + {e.StackTrace} + ============================================== + """; + await redisManager.AddTaskLog(data.taskId, $"=>GPT Http请求失败 {msg} 1秒后重试"); Thread.Sleep(1000); } } diff --git a/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs b/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs index d03f6f2..b22d303 100644 --- a/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs +++ b/VideoAnalysisCore/AICore/GPT/GTP_Analysis_1.cs @@ -194,12 +194,12 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var checkMessage = $""" 请你担任一位专业的视频内容分析教研老师,擅长评估视频内容的结构和逻辑流暢度。 - 核心任务: 请根据我提供的【视频分段方案】【完整字幕文本】【完整字幕文本】,对该分段方案进行严谨评估。 + 核心任务: 请根据我提供的【视频分段方案】【完整字幕文本】,对该分段方案进行严谨评估。 评估维度与具体标准: 时间间隔检查(硬性指标) 严格检查每个分段与下一个分段的开始时间之间的间隔是否大于40秒。 - 输出要求:请列出所有不满足此条件的分段对,并说明具体的时间差。 内容结构与主题合理性: + 分段方案的准确度:评估单个分段内的Content是否与对应时间段内的字幕文本内容匹配(硬性指标,不能存在捏造的内容) 主题凝聚力:评估单个分段内的内容是否围绕一个清晰、统一的主题展开,是否存在主题混杂或跳跃的情况。 逻辑过渡:评估分段之间的过渡是否自然流畅,后一段是否是前一段内容的合理延伸或转折。 知识点分配:检查分段内的知识点是否与分段内容有关联,知识点分配给这个分段是否合理(这项很重要)。 @@ -213,17 +213,6 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek 输出格式为json: {checkResFormat} """; return await chatGPTClient.ChatAsync(taskInfo.Id.ToString(), checkMessage, "结果检查"); - //var res = await chatClient.ChatAsync(taskInfo.Id.ToString(), checkMessage, "结果检查"); - - //var checkMessage1 = "我为视频的讲解内容做了一些分段,希望你能通读字幕内容后检查下的分段是否符合我的要求?" + - // $"检查这些分段的时间是否合理 与相邻的时间段间隔是否大于30秒?" + - // $"分段的主题内容,知识点分配是否合理符合实际吗?" + - // $"{pptFormat}" + - // $"请给出你的打分(0-100,70分及格)以及打分原因。" + - // $"这是我的分段 {thems}。" + - // $"后续的内容是包含时间戳的视频字幕的固定格式文本。" + - // $"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" + - // $"最后输出格式为json({checkResFormat})"; } /// @@ -300,27 +289,19 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek { var keyFrameArr = string.IsNullOrEmpty(taskInfo?.PPTVideoCode) || string.IsNullOrEmpty(taskInfo?.PPTKeyFrame) ? $"" - : $"授课PPT发生了变化的时间是{taskInfo.PPTKeyFrame},基于PPT变化时间点,将字幕内容分割成时间段。每个时间段的起始和结束应接近这些时间点(例如,以时间点为中心,扩展至内容自然过渡处)。"; + : $"初步划分阶段:授课PPT发生了变化的时间是{taskInfo.PPTKeyFrame},基于PPT变化时间点,将字幕内容分割成时间段。每个时间段的起始和结束应接近这些时间点(例如,以时间点为中心,扩展至内容自然过渡处)。"; var resFormat = """[{"StartTime":开始秒(number),"EndTime":结束秒(number),"Stage":阶段(string),"Theme":主题(string),"Content":内容总结(string)}]"""; var postMessages = string.Empty; - switch (taskInfo?.VideoType) - { - case AttachmentsInfoType.其他资料: - case AttachmentsInfoType.新课: - case AttachmentsInfoType.复习: - case AttachmentsInfoType.活动: - case AttachmentsInfoType.班会: - break; - default: - throw new Exception("无效的课程类型"); - } + postMessages = $"请通过视频字幕内容分析出视频中课堂的授课阶段。" + $"课堂内容与{taskInfo.Subject}学科下的{sections}章节相关。" + $"完整的课堂标准流程包含以下5个阶段:课程引入/新知讲解/例题精讲/课堂练习/知识总结。" + - (taskInfo?.VideoType == AttachmentsInfoType.复习 ? $"但本堂课是习题课,所以大部分阶段是不同的例题讲解内容。" : string.Empty) + - $"初步划分阶段:{keyFrameArr}" + + (taskInfo?.VideoType == AttachmentsInfoType.复习 + ? $"但本堂课是习题课,所以大部分阶段是不同的例题讲解内容。" + : string.Empty) + + $"{keyFrameArr}" + $"内容分析:对每个时间段,提取主要讲解内容:识别关键词(如“例题”“证明”“练习”“总结”)和内容结构。" + $"判断阶段类型:如果内容以解题为主,归类为“例题精讲”;如果涉及新知识讲解,归类为“新知讲解”;以此类推。" + $"内容总结:简述该阶段的核心讲解内容70~200字,确保内容与阶段时间内授课内容符合。" + @@ -329,10 +310,12 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek $"输出格式要求:内容只返回json格式({resFormat})" + $"字幕格式(开始秒:内容|下一段字幕).以下是包含时间的视频字幕文本。" + $"字幕列表 {captions.Captions} 字幕结束!"; - + await redisManager.AddTaskLog(taskInfo.Id, $"开始分析视频内容 {tryCount}"); //return await chatGPTClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); - return await chatClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); + var res = await geminiClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); + //var r2 = await chatClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); + return res; } catch (Exception ex) { diff --git a/VideoAnalysisCore/AICore/GPT/Gemini/GeminiGPTClient.cs b/VideoAnalysisCore/AICore/GPT/Gemini/GeminiGPTClient.cs index 2d18ef4..2dd8c7f 100644 --- a/VideoAnalysisCore/AICore/GPT/Gemini/GeminiGPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/Gemini/GeminiGPTClient.cs @@ -51,6 +51,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT var chatReq = new ChatRequest { taskId = task, + title=title, model = model, max_tokens =12000, stream = true, diff --git a/VideoAnalysisCore/Common/AppCommon.cs b/VideoAnalysisCore/Common/AppCommon.cs index 485ce4a..ab3a9c4 100644 --- a/VideoAnalysisCore/Common/AppCommon.cs +++ b/VideoAnalysisCore/Common/AppCommon.cs @@ -12,6 +12,7 @@ using System.Reflection.PortableExecutable; using System.Runtime.Loader; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; using UserCenter.Model; @@ -154,6 +155,8 @@ namespace VideoAnalysisCore.Common var jsonOptions = new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + // 忽略所有 null 值属性 + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = WriteIndented // 如果需要美化输出 }; return JsonSerializer.Serialize(o, jsonOptions); diff --git a/VideoAnalysisCore/Common/DownloadFile.cs b/VideoAnalysisCore/Common/DownloadFile.cs index 7d95748..7a84781 100644 --- a/VideoAnalysisCore/Common/DownloadFile.cs +++ b/VideoAnalysisCore/Common/DownloadFile.cs @@ -138,6 +138,15 @@ namespace VideoAnalysisCore.Common if (taskInfo is null ) throw new Exception($"任务为null/是教研视频/没有视频课程名称"); var fileUrl = taskInfo.MediaUrl; + switch (taskInfo?.VideoType) + { + case AttachmentsInfoType.无: + case AttachmentsInfoType.新课: + case AttachmentsInfoType.复习: + break; + default: + throw new Exception("无效的课程类型"); + } if (string.IsNullOrEmpty(fileUrl)) { var videoInfo = await vodClient.GetPlayInfoAsync(new AlibabaCloud.SDK.Vod20170321.Models.GetPlayInfoRequest() diff --git a/VideoAnalysisCore/Common/JsonExtractor.cs b/VideoAnalysisCore/Common/JsonExtractor.cs index 4030e0f..f0bd2aa 100644 --- a/VideoAnalysisCore/Common/JsonExtractor.cs +++ b/VideoAnalysisCore/Common/JsonExtractor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace VideoAnalysisCore.Common { @@ -11,81 +12,73 @@ namespace VideoAnalysisCore.Common /// /// /// - public static List ExtractJsonStrings(this string input) + public static List ExtractJsonStrings(this string input) + { + var results = new List(); + if (string.IsNullOrWhiteSpace(input)) return results; + + int braceCount = 0; + int bracketCount = 0; + int startIndex = -1; + bool inString = false; + bool isEscaped = false; + + for (int i = 0; i jsonList = new List(); - int index = 0; - while (index < input.Length) - { - if (input[index] == '{' || input[index] == '[') - { - int? endIndex = FindMatchingBracket(input, index); - if (endIndex.HasValue) - { - string candidate = input.Substring(index, endIndex.Value - index + 1); - if (IsValidJson(candidate)) - { - jsonList.Add(candidate); - index = endIndex.Value + 1; - continue; - } - } - } - index++; - } - return jsonList; - } + char c = input[i]; - private static int? FindMatchingBracket(string str, int start) + // 1. 处理转义字符 (例如 \") + if (isEscaped) + { + isEscaped = false; + continue; + } + + if (c == '\\') + { + isEscaped = true; + continue; + } + +// 2. 处理字符串边界 +if (c == '"') +{ + inString = !inString; + continue; +} + +// 3. 如果在字符串内,忽略括号逻辑 +if (inString) continue; + +// 4. 处理 JSON 对象和数组的开始 +if (c == '{' || c == '[') +{ + if (braceCount == 0 && bracketCount == 0) + { + startIndex = i; + } + if (c == '{') braceCount++; + else bracketCount++; +} +// 5. 处理 JSON 对象和数组的结束 +else if (c == '}' || c == ']') +{ + if (c == '}') braceCount--; + else bracketCount--; + + if (braceCount == 0 && bracketCount == 0 && startIndex != -1) + { + string potentialJson = input.Substring(startIndex, i - startIndex + 1); + if (IsValidJson(potentialJson)) { - Stack stack = new Stack(); - bool inString = false; - bool inEscape = false; - - for (int i = start; i < str.Length; i++) - { - char c = str[i]; - - if (inEscape) - { - inEscape = false; - } - else if (inString) - { - if (c == '\\') - inEscape = true; - else if (c == '"') - inString = false; - } - else - { - switch (c) - { - case '{': - case '[': - stack.Push(c); - break; - case '}': - if (stack.Count == 0 || stack.Peek() != '{') - return null; - stack.Pop(); - break; - case ']': - if (stack.Count == 0 || stack.Peek() != '[') - return null; - stack.Pop(); - break; - case '"': - inString = true; - break; - } - } - - if (stack.Count == 0) - return i; - } - return null; // 括号未完全匹配 + results.Add(potentialJson); } + startIndex = -1; + } +} + } + return results; + } public static bool IsValidJson(string candidate) { @@ -96,7 +89,7 @@ namespace VideoAnalysisCore.Common JsonDocument.Parse(candidate); return true; } - catch (Exception) + catch { return false; } diff --git a/VideoAnalysisCore/Common/RedisExpand.cs b/VideoAnalysisCore/Common/RedisExpand.cs index 3692422..59bebe8 100644 --- a/VideoAnalysisCore/Common/RedisExpand.cs +++ b/VideoAnalysisCore/Common/RedisExpand.cs @@ -281,7 +281,7 @@ namespace VideoAnalysisCore.Common if (!SubscribeList.ContainsKey(@enum)) throw new Exception(@enum + " 未实现"); var tId = taskId.ToString(); - + await AddTaskLog(tId, "-------------> 开始执行任务 "); try { @@ -457,7 +457,10 @@ namespace VideoAnalysisCore.Common Subscribe = Redis.SubscribeList(RedisExpandKey.ChannelKey, async (taskId) => { if (taskId is null) return; - Subscribe?.Dispose();//取消接收任务监听 + lock (Redis) + { + Subscribe?.Dispose();//取消接收任务监听 + } Redis.LPush(RedisExpandKey.IDTask, taskId); await AddTaskLog(taskId, "-------------> 接收到任务 "); await InsertChannel(RedisChannelEnum.下载文件, taskId);