From 2609f13ea53cdc1d260654f2f39f9b2e37148a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com> Date: Tue, 21 Oct 2025 15:02:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E6=96=B0=E7=89=88AI?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Pages/VideoTaskShow.razor.cs | 2 +- VideoAnalysis/appsettings.json | 4 +- VideoAnalysisCore/AICore/GPT/BserGPT.cs | 145 +++++++++++- .../AICore/GPT/ChatGPT/ChatGPTClient.cs | 199 +++++++++++++++- .../AICore/GPT/ChatGPT/ChatGPTModel.cs | 99 -------- .../AICore/GPT/ChatGPT/ChatGPTType.cs | 19 +- .../AICore/GPT/ChatGPT/Chat_GPT.cs | 4 +- .../AICore/GPT/DeepSeek/DeepSeekClient.cs | 88 ++++++- .../AICore/GPT/DeepSeek/DeepSeekModel.cs | 136 +---------- .../AICore/GPT/DeepSeek/DeepSeek_GPT.cs | 216 +++++++----------- VideoAnalysisCore/Common/AppCommon.cs | 40 +++- VideoAnalysisCore/Job/TaskFileClearJob.cs | 53 ++++- 12 files changed, 598 insertions(+), 407 deletions(-) diff --git a/VideoAnalysis/Components/Pages/VideoTaskShow.razor.cs b/VideoAnalysis/Components/Pages/VideoTaskShow.razor.cs index 9695a7e..9972ac4 100644 --- a/VideoAnalysis/Components/Pages/VideoTaskShow.razor.cs +++ b/VideoAnalysis/Components/Pages/VideoTaskShow.razor.cs @@ -94,7 +94,7 @@ namespace Learn.VideoAnalysis.Components.Pages StageId = s.First().StageId, KnowPoint = string.Join(',', s.Select(x => x.KnowPoint)) }).ToArray(); - videoPath = AppCommon.GetVideoPath(nowTask.Id.ToString()); + videoPath = nowTask.MediaUrl; //AppCommon.GetVideoPath(nowTask.Id.ToString()); if (nowTask.VideoType == AttachmentsInfoType.复习) { diff --git a/VideoAnalysis/appsettings.json b/VideoAnalysis/appsettings.json index dacf58f..ab63f78 100644 --- a/VideoAnalysis/appsettings.json +++ b/VideoAnalysis/appsettings.json @@ -42,8 +42,8 @@ "ApiKey": "sk-8BvvhESZIkgUbiaaJhglPxFa4o2X9H3xEv9lXELrWWwGxHWY" }, "ChatGpt": { - "Host": "https://api.g4f.icu/", - //"Host": "https://api.oaibest.com/", + //"Host": "https://api.g4f.icu/", + "Host": "https://api.oaibest.com/", "ApiKey": "sk-D15tBln31N7dI9Fi7lds7OySFv5tOEK7DMNsG5rY2E6DCr4s" }, "DeepSeek": { diff --git a/VideoAnalysisCore/AICore/GPT/BserGPT.cs b/VideoAnalysisCore/AICore/GPT/BserGPT.cs index 9b4c914..297dff1 100644 --- a/VideoAnalysisCore/AICore/GPT/BserGPT.cs +++ b/VideoAnalysisCore/AICore/GPT/BserGPT.cs @@ -5,9 +5,7 @@ using Whisper.net; namespace VideoAnalysisCore.AICore.GPT { - /// - /// GPT 接口 - /// + public interface IBserGPT { /// @@ -29,4 +27,145 @@ namespace VideoAnalysisCore.AICore.GPT /// public Task GetVideoType(string task); } + + /// + /// 请求数据 + /// + public class ChatRequest + { + /// + /// 对话 + /// + public Message[] messages { get; set; } + /// + /// 提问种子值[用来确保 相同参数请求尽可能返回相同参数] + /// 默认:null + /// 此功能处于 Beta 阶段。 如果指定,我们的系统将尽最大努力确定性地采样,这样具有相同 and 参数的重复请求应该返回相同的结果。 无法保证确定性,您应该参考 response 参数来监控后端的变化 + /// + public int? seed { get; set; } = null; + /// + /// 推理模型 (deepseek-reasoner) + /// + public string model { get; set; } = "deepseek-reasoner"; + + public float? max_tokens { get; set; } = 8000; + public float? max_completion_tokens { get; set; } = 8000; + /// + /// 要使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定。 我们通常建议更改此项或同时更改两者。top_p + /// 默认为 1 + /// 联动 + /// + public float? temperature { get; set; } = 0.2f; + /// + /// 一种替代温度采样的方法,称为原子核采样, 其中,模型考虑具有top_p概率的标记的结果 质量。所以 0.1 表示仅包含前 10% 概率质量的代币 被考 + /// 建议与联动 + /// + public float? top_p { get; set; } = 0.1f; + /// + /// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。 + /// + //public object response_format { get; set; } = new { type = "json_object" }; + /// + /// 流式返回 + /// + public bool stream { get; set; } = true; + /// + /// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是 + /// 默认设置: ["text"] + /// 该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用:gpt-4o-audio-preview["text", "audio"] + /// + public string modalities { get; set; } = "[\"json\"]"; + /// + /// 任务id + /// + public string taskId { get; set; } + public object stream_options { get; set; } = new { include_usage = true }; + + } + /// + /// GPT 接口 + /// + + + /// + /// gpt返回值 + /// + public class ChatRes + { + public string id { get; set; } + public string _object { get; set; } + public int created { get; set; } + public string model { get; set; } + public ChatResError error { get; set; } + public Choice[] choices { get; set; } + public Usage usage { get; set; } + /// + /// 系统指纹 + /// + public string system_fingerprint { get; set; } + } + public class Usage + { + public int prompt_tokens { get; set; } + public int completion_tokens { get; set; } + public int total_tokens { get; set; } + } + + + public class Choice + { + public int index { get; set; } + public Message message { get; set; } + public object logprobs { get; set; } + public string finish_reason { get; set; } + } + public class ChatResError + { + public string message { get; set; } + public string type { get; set; } + } + + public class ChatResSSE + { + public string id { get; set; } + public int created { get; set; } + /// + /// 模型id + /// + public string model { get; set; } + /// + /// 对话 + /// + public ChoiceSSE[] choices { get; set; } + /// + /// token使用情况 + /// + public Usage usage { get; set; } + } + public class ChoiceSSE + { + public int index { get; set; } + public Message delta { get; set; } + public string finish_reason { get; set; } + } + + public class Message + { + public Message() + { + + } + public Message(string content, string role) + { + this.role = role; + this.content = content; + } + public string role { get; set; } + public string content { get; set; } + /// + /// 推理内容 + /// + public string reasoning_content { get; set; } + public string refusal { get; set; } + } } diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs index e6b3662..06f7b6a 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTClient.cs @@ -7,6 +7,8 @@ using System.Net.Http; using Newtonsoft.Json; using System.Net.Http.Json; using System.Net; +using VideoAnalysisCore.AICore.GPT.DeepSeek; +using System.Text.Json; namespace VideoAnalysisCore.AICore.GPT.ChatGPT @@ -18,10 +20,48 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT public static string ApiKey = AppCommon.Config.ChatGpt.ChatGpt.ApiKey; private readonly IHttpClientFactory _httpClientFactory; + private readonly RedisManager redisManager; - public ChatGPTClient(IHttpClientFactory httpClientFactory) + public ChatGPTClient(IHttpClientFactory httpClientFactory, RedisManager redisManager) { _httpClientFactory = httpClientFactory; + this.redisManager = redisManager; + } + + private HttpResponseMessage PostJsonStream(string path, string json) + { + var uriBuilder = new UriBuilder(Host+ path); + var maxRestart = 4; + var errorMSG = new Exception[maxRestart]; + for (int i = 0; i < maxRestart; i++) + { + try + { + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", ApiKey); + client.Timeout = TimeSpan.FromSeconds(60 * 20);//超时时间20分钟 + client.DefaultRequestVersion = HttpVersion.Version20; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; + client.DefaultRequestHeaders.ConnectionClose = true; + + var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + + return client.Send(request, HttpCompletionOption.ResponseHeadersRead); + } + catch (Exception e) + { + errorMSG[i] = e; + Console.WriteLine("====================[请求异常,重试]===================="); + Console.WriteLine(uriBuilder.Uri); + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + Console.WriteLine("=============================================="); + Thread.Sleep(1000); + } + } + throw errorMSG.Last(s => s != null); } /// @@ -31,9 +71,159 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT /// Return HttpResponseMessage for SSE public async Task<(Usage u, string res)?> ChatSSE(ChatRequest chatReq) { - throw new Exception($"未实现"); + var requestBody = chatReq.ToJson(); + PostJsonStream: + var chatResp = PostJsonStream("v1/chat/completions", requestBody); + if (!chatResp.IsSuccessStatusCode) + { + Console.WriteLine(DateTime.Now + "=>请求GPT服务器异常 " + chatResp?.StatusCode); + Console.WriteLine(await chatResp.Content.ReadAsStringAsync()); + goto PostJsonStream; + } + using var stream = chatResp.Content.ReadAsStream(); + using var reader = new StreamReader(stream, Encoding.UTF8); + string line; + var messageBuilder = new StringBuilder(); + var messageBuilder1 = new StringBuilder(); + var lastChat = new ChatResSSE(); + var splitCount = "data:".Length; + var maxLoop = 60 * 100000; + int threshold = 0; + while (maxLoop > 0) + { + line = reader.ReadLine(); + if (line is null || string.IsNullOrEmpty(line) || line.StartsWith(": keep-alive")) + { + Thread.Sleep(10); + maxLoop--; + continue; + } + else if (line.EndsWith("[DONE]")) + { + // 表示一条消息结束 + string message = messageBuilder.ToString(); + string message2 = messageBuilder1.ToString(); + messageBuilder.Clear(); + messageBuilder1.Clear(); + var u = lastChat?.usage; + if (u == null || string.IsNullOrEmpty(message)) + return null; + return (u, message); + //return (u, message, message2); + } + else if (line.StartsWith("data:")) + { + try + { + var data = System.Text.Json.JsonSerializer.Deserialize(line.Substring(splitCount).Trim()); + lastChat = data; + var delta = data?.choices.FirstOrDefault()?.delta; + var str = delta?.content; + var strReasoning = delta?.reasoning_content; + if (!string.IsNullOrEmpty(str)) + messageBuilder.Append(str); + if (!string.IsNullOrEmpty(strReasoning)) + messageBuilder1.Append(strReasoning); + var steamCount = messageBuilder.Length + messageBuilder1.Length; + if (++threshold % 30 == 0) + redisManager.SetTaskProgress(chatReq.taskId, "steam=>" + steamCount); + } + catch (Exception e) + { + Console.WriteLine("异常 ChatSSE=>"); + Console.WriteLine(line); + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + } + } + } + Console.WriteLine(DateTime.Now + "=>AI请求超时 " + chatReq.taskId); + return null; } + /// + /// 请求AI + /// + /// 返回JSON类型 + /// 任务id + /// 提示词 + /// 任务类型 + /// GPT版本 + /// 最大token 不设置默认最大值 16000/8000 + /// + /// + public async Task ChatAsync(string task, string postMessages, string title, string model =null, int max_tokens = -1) + { + Message[] messageArr = [ + new Message(postMessages,"user"), + ]; + messageArr = messageArr.Where(s => s != null).ToArray(); + var chatRep = new ChatRequest + { + taskId = task, + model = model ?? ChatGPTType.GPT5_mini, + max_tokens = 8000, + stream = true, + temperature = 0.2f, + messages = messageArr + }; + if (max_tokens != -1) + chatRep.max_tokens = max_tokens; + var tryCount = 10; + while (tryCount-- > 0) + { + try + { + var time = title + DateTime.Now.ToString("MMddHHmmss"); + var redisCached = new object[2] { chatRep, null }; + redisManager.SetTaskGPTCached(task, time, chatRep); + var chatResp = await Chat(chatRep); + var chatResContent = chatResp?.res; + if (string.IsNullOrEmpty(chatResContent)) + throw new Exception("GPT返回message无效结果"); + if (chatResp != null) + { + redisCached[1] = new object[] { chatResp.Value.res, chatResp.Value.u, chatResp.Value }; + redisManager.SetTaskGPTCached(task, time, redisCached); + } + chatResContent = chatResContent?.ExtractJsonStrings()?.FirstOrDefault(); + chatResContent = chatResContent?.Replace("\n", ""); + chatResContent = chatResContent?.Replace("```json", ""); + chatResContent = chatResContent?.Replace("```", ""); + chatResContent = chatResContent?.Replace("}{", "},{"); + chatResContent = chatResContent?.Replace("}|{", "},{"); + chatResContent = chatResContent?.Trim(); + + if (string.IsNullOrEmpty(chatResContent)) + throw new Exception("ChatGPT返回结果无有效JSON"); + var startsStr = typeof(T).IsArray ? "[" : "{"; + var endStr = typeof(T).IsArray ? "]" : "}"; + if (!chatResContent.StartsWith(startsStr)) + chatResContent = startsStr + chatResContent; + if (!chatResContent.EndsWith(endStr)) + chatResContent = chatResContent + endStr; + var options = new JsonSerializerOptions + { + // 允许解析不严格符合 JSON 规范的字符串 + AllowTrailingCommas = true, + // 处理不匹配的 JSON 字符 + ReadCommentHandling = JsonCommentHandling.Skip + }; + var questionRes = System.Text.Json.JsonSerializer.Deserialize(chatResContent, options); + if (questionRes is null) + throw new Exception("ChatGPT返回无效结果"); + return questionRes; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine(DateTime.Now + $"=>ChatGPT结果解析错误 重试剩余{tryCount}"); + } + } + throw new Exception(DateTime.Now + "=>ChatGPT请求失败次数过多!!!"); + } + + /// /// Chat /// @@ -41,6 +231,11 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT /// Return HttpResponseMessage for SSE public async Task<(Usage u, string res)?> Chat(ChatRequest chatReq) { + chatReq.modalities =null; + chatReq.max_tokens = null; + chatReq.top_p = null; + if (chatReq.stream) return await ChatSSE(chatReq); + var requestBody = chatReq.ToJson(); var chatResp = await PostJsonStreamAsync("v1/chat/completions", requestBody); var res = await chatResp.Content.ReadFromJsonAsync(); diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTModel.cs b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTModel.cs index 569b6d5..352b140 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTModel.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTModel.cs @@ -6,104 +6,5 @@ using System.Threading.Tasks; namespace VideoAnalysisCore.AICore.GPT.ChatGPT { - /// - /// 请求数据 - /// - public class ChatRequest - { - /// - /// 对话 - /// - public Message[] messages { get; set; } - /// - /// 提问种子值[用来确保 相同参数请求尽可能返回相同参数] - /// 默认:null - /// 此功能处于 Beta 阶段。 如果指定,我们的系统将尽最大努力确定性地采样,这样具有相同 and 参数的重复请求应该返回相同的结果。 无法保证确定性,您应该参考 response 参数来监控后端的变化 - /// - public int? seed { get; set; } =null; - public string model { get; set; } = ChatGPTType.GPT4o; - /// - /// 要使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定。 我们通常建议更改此项或同时更改两者。top_p - /// 默认为 1 - /// 联动 - /// - public float temperature { get; set; } = 0.2f; - /// - /// 一种替代温度采样的方法,称为原子核采样, 其中,模型考虑具有top_p概率的标记的结果 质量。所以 0.1 表示仅包含前 10% 概率质量的代币 被考 - /// 建议与联动 - /// - public float top_p { get; set; } = 0.5f; - public float max_completion_tokens { get; set; } = 5000; - /// - /// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。 - /// - public object response_format { get; set; } = new { type = "json_object" }; - /// - /// 流式返回 - /// - public bool stream { get; set; } = false; - /// - /// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是 - /// 默认设置: ["text"] - /// 该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用:gpt-4o-audio-preview["text", "audio"] - /// - public string modalities { get; set; } = "[\"json\"]"; - /// - /// ai引导新话题 - /// 默认-2 范围[-2~2] - /// - public int presence_penalty { get; set; } = -2; -} - public class Message - { - public Message(string content, string role) - { - this.role = role; - this.content = content; - } - public string role { get; set; } - public string content { get; set; } - public string refusal { get; set; } - } - - - /// - /// gpt返回值 - /// - public class ChatRes - { - public string id { get; set; } - public string _object { get; set; } - public int created { get; set; } - public string model { get; set; } - public ChatResError error { get; set; } - public Choice[] choices { get; set; } - public Usage usage { get; set; } - /// - /// 系统指纹 - /// - public string system_fingerprint { get; set; } - } - - public class Usage - { - public int prompt_tokens { get; set; } - public int completion_tokens { get; set; } - public int total_tokens { get; set; } - } - - - public class Choice - { - public int index { get; set; } - public Message message { get; set; } - public object logprobs { get; set; } - public string finish_reason { get; set; } - } - public class ChatResError - { - public string message { get; set; } - public string type { get; set; } - } } diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTType.cs b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTType.cs index 9aba558..3b5b740 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTType.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPT/ChatGPTType.cs @@ -8,22 +8,9 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT { public class ChatGPTType { - public static string GPT4oLatest = "chatgpt-4o-latest"; - public static string GPT4o241120 = "gpt-4o-2024-11-20"; - public static string GPT4o240513 = "gpt-4o-2024-05-13"; - /// - /// GPT-4O 型 -> gpt-4o-2024-08-06 - /// - public static string GPT4o= "gpt-4o"; - - public static string GPT4oMini= "gpt-4o-mini"; - public static string GPT4oMini240718 = "gpt-4o-mini-2024-07-18"; - - public static string GPT4Turbo= "gpt-4-turbo-2024-04-09"; - /// - /// gpt-4-turbo-preview gpt-4-0125-preview - /// - public static string GPT4TurboPreview = "gpt-4-turbo-preview"; + 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"; /// diff --git a/VideoAnalysisCore/AICore/GPT/ChatGPT/Chat_GPT.cs b/VideoAnalysisCore/AICore/GPT/ChatGPT/Chat_GPT.cs index 37ad969..72dad2e 100644 --- a/VideoAnalysisCore/AICore/GPT/ChatGPT/Chat_GPT.cs +++ b/VideoAnalysisCore/AICore/GPT/ChatGPT/Chat_GPT.cs @@ -198,7 +198,7 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT messageArr = messageArr.Where(s => s != null).ToArray(); var chatRep = new ChatRequest { - model = ChatGPTType.GPT4o241120, + model = ChatGPTType.GPT5_mini, max_completion_tokens = maxTokens, temperature = 0.2f, messages = messageArr @@ -240,6 +240,8 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT //} return questionRes; } + + /// /// 访问GPT /// diff --git a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekClient.cs b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekClient.cs index 5b2f239..33114ac 100644 --- a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekClient.cs +++ b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekClient.cs @@ -12,6 +12,7 @@ using System; using System.IO; using VideoAnalysisCore.AICore.GPT.ChatGPT; using System.Threading.Tasks; +using System.Text.Json; namespace VideoAnalysisCore.AICore.GPT.DeepSeek { @@ -155,13 +156,13 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var messageBuilder1 = new StringBuilder(); var lastChat = new ChatResSSE(); var splitCount = "data:".Length; - var maxLoop = 60*10000; + var maxLoop = 60*100000; int threshold = 0; while (maxLoop>0) { line = reader.ReadLine(); if (line is null || string.IsNullOrEmpty(line)|| line.StartsWith(": keep-alive")) { - Thread.Sleep(100); + Thread.Sleep(10); maxLoop--; continue; } @@ -210,5 +211,88 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek return null; } + + /// + /// 请求AI + /// + /// 返回JSON类型 + /// 任务id + /// 提示词 + /// 任务类型 + /// GPT版本 + /// 最大token 不设置默认最大值 16000/8000 + /// + /// + public async Task ChatAsync(string task, string postMessages, string title, string model = "deepseek-reasoner", int max_tokens = -1) + { + Message[] messageArr = [ + new Message(postMessages,"user"), + ]; + messageArr = messageArr.Where(s => s != null).ToArray(); + var chatRep = new ChatRequest + { + taskId = task, + model = model, + max_tokens = model == "deepseek-reasoner" ? 16000 : 8000, + stream = true, + temperature = 0.2f, + messages = messageArr + }; + if (max_tokens != -1) + chatRep.max_tokens = max_tokens; + var tryCount = 10; + while (tryCount-- > 0) + { + try + { + var time = title + DateTime.Now.ToString("MMddHHmmss"); + var redisCached = new object[2] { chatRep, null }; + redisManager.SetTaskGPTCached(task, time, chatRep); + var chatResp = await Chat(chatRep); + var chatResContent = chatResp?.res; + if (string.IsNullOrEmpty(chatResContent)) + throw new Exception("GPT返回message无效结果"); + if (chatResp != null) + { + redisCached[1] = new object[] { chatResp.Value.res, chatResp.Value.u, chatResp.Value.reasoning }; + redisManager.SetTaskGPTCached(task, time, redisCached); + } + chatResContent = chatResContent?.ExtractJsonStrings()?.FirstOrDefault(); + chatResContent = chatResContent?.Replace("\n", ""); + chatResContent = chatResContent?.Replace("```json", ""); + chatResContent = chatResContent?.Replace("```", ""); + chatResContent = chatResContent?.Replace("}{", "},{"); + chatResContent = chatResContent?.Replace("}|{", "},{"); + chatResContent = chatResContent?.Trim(); + + if (string.IsNullOrEmpty(chatResContent)) + throw new Exception("ChatGPT返回结果无有效JSON"); + var startsStr = typeof(T).IsArray ? "[" : "{"; + var endStr = typeof(T).IsArray ? "]" : "}"; + if (!chatResContent.StartsWith(startsStr)) + chatResContent = startsStr + chatResContent; + if (!chatResContent.EndsWith(endStr)) + chatResContent = chatResContent + endStr; + var options = new JsonSerializerOptions + { + // 允许解析不严格符合 JSON 规范的字符串 + AllowTrailingCommas = true, + // 处理不匹配的 JSON 字符 + ReadCommentHandling = JsonCommentHandling.Skip + }; + var questionRes = System.Text.Json.JsonSerializer.Deserialize(chatResContent, options); + if (questionRes is null) + throw new Exception("ChatGPT返回无效结果"); + return questionRes; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine(DateTime.Now + $"=>ChatGPT结果解析错误 重试剩余{tryCount}"); + } + } + throw new Exception(DateTime.Now + "=>ChatGPT请求失败次数过多!!!"); + } + } } \ No newline at end of file diff --git a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekModel.cs b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekModel.cs index 3e6df08..0428e5b 100644 --- a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekModel.cs +++ b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeekModel.cs @@ -6,140 +6,6 @@ using System.Threading.Tasks; namespace VideoAnalysisCore.AICore.GPT.DeepSeek { - /// - /// 请求数据 - /// - public class ChatRequest - { - /// - /// 对话 - /// - public Message[] messages { get; set; } - /// - /// 提问种子值[用来确保 相同参数请求尽可能返回相同参数] - /// 默认:null - /// 此功能处于 Beta 阶段。 如果指定,我们的系统将尽最大努力确定性地采样,这样具有相同 and 参数的重复请求应该返回相同的结果。 无法保证确定性,您应该参考 response 参数来监控后端的变化 - /// - public int? seed { get; set; } =null; - /// - /// 推理模型 (deepseek-reasoner) - /// - public string model { get; set; } = "deepseek-reasoner"; - - public float max_tokens { get; set; } = 8000; - /// - /// 要使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定。 我们通常建议更改此项或同时更改两者。top_p - /// 默认为 1 - /// 联动 - /// - public float temperature { get; set; } = 0.2f; - /// - /// 一种替代温度采样的方法,称为原子核采样, 其中,模型考虑具有top_p概率的标记的结果 质量。所以 0.1 表示仅包含前 10% 概率质量的代币 被考 - /// 建议与联动 - /// - public float top_p { get; set; } = 0.1f; - /// - /// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。 - /// - //public object response_format { get; set; } = new { type = "json_object" }; - /// - /// 流式返回 - /// - public bool stream { get; set; } = false; - /// - /// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是 - /// 默认设置: ["text"] - /// 该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用:gpt-4o-audio-preview["text", "audio"] - /// - public string modalities { get; set; } = "[\"json\"]"; - /// - /// 任务id - /// - public string taskId { get; set; } - public object stream_options { get; set; } = new { include_usage = true }; -} - public class Message - { - public Message() - { - - } - public Message(string content, string role) - { - this.role = role; - this.content = content; - } - public string role { get; set; } - public string content { get; set; } - /// - /// 推理内容 - /// - public string reasoning_content { get; set; } - public string refusal { get; set; } - } - - - /// - /// gpt返回值 - /// - public class ChatRes - { - public string id { get; set; } - public string _object { get; set; } - public int created { get; set; } - public string model { get; set; } - public ChatResError error { get; set; } - public Choice[] choices { get; set; } - public Usage usage { get; set; } - /// - /// 系统指纹 - /// - public string system_fingerprint { get; set; } - } - - public class Usage - { - public int prompt_tokens { get; set; } - public int completion_tokens { get; set; } - public int total_tokens { get; set; } - } - - - public class Choice - { - public int index { get; set; } - public Message message { get; set; } - public object logprobs { get; set; } - public string finish_reason { get; set; } - } - public class ChatResError - { - public string message { get; set; } - public string type { get; set; } - } - - public class ChatResSSE - { - public string id { get; set; } - public int created { get; set; } - /// - /// 模型id - /// - public string model { get; set; } - /// - /// 对话 - /// - public ChoiceSSE[] choices { get; set; } - /// - /// token使用情况 - /// - public Usage usage { get; set; } - } - public class ChoiceSSE - { - public int index { get; set; } - public Message delta { get; set; } - public string finish_reason { get; set; } - } + } diff --git a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeek_GPT.cs b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeek_GPT.cs index a917021..8b32a30 100644 --- a/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeek_GPT.cs +++ b/VideoAnalysisCore/AICore/GPT/DeepSeek/DeepSeek_GPT.cs @@ -20,6 +20,8 @@ using Yitter.IdGenerator; using VideoAnalysisCore.Common.Expand; using System.Collections.Generic; using UserCenter.Model.Enum; +using Dm.filter; +using System.Text.RegularExpressions; namespace VideoAnalysisCore.AICore.GPT.DeepSeek { @@ -29,6 +31,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek public class DeepSeek_GPT : IBserGPT { private readonly DeepSeekGPTClient chatClient; + private readonly ChatGPTClient chatGPTClient; private readonly Repository criteriaDB; private readonly RedisManager redisManager; private readonly Repository videoTaskDB; @@ -43,9 +46,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek /// /// /// - public DeepSeek_GPT(DeepSeekGPTClient moonshotClient, Repository criteria, Repository videoTaskDB, - Repository knowledgeInfoDB, Repository videoKonwPointDB, SimpLetexClient simpLetexClient, - Repository videoQuestionDB, OssClient ossClient, Repository videoQuestionKonwDB, RedisManager redisManager) + public DeepSeek_GPT(DeepSeekGPTClient moonshotClient, Repository criteria, Repository videoTaskDB, + Repository knowledgeInfoDB, Repository videoKonwPointDB, SimpLetexClient simpLetexClient, + Repository videoQuestionDB, OssClient ossClient, Repository videoQuestionKonwDB, RedisManager redisManager, ChatGPTClient chatGPTClient) { chatClient = moonshotClient; criteriaDB = criteria; @@ -57,6 +60,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek this.ossClient = ossClient; this.videoQuestionKonwDB = videoQuestionKonwDB; this.redisManager = redisManager; + this.chatGPTClient = chatGPTClient; } /// /// 获取内容对应的章节 @@ -84,7 +88,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek $" 格式 (方法点Id|方法点名称) " + $"提供的知识点名称({knows})。"; Console.WriteLine(DateTime.Now + "=>2.开始分析视频内容知识点"); - var konwRes = await ChatAsync(taskInfo.Id.ToString(), knowMessages, "知识点"); + var konwRes = await chatGPTClient.ChatAsync(taskInfo.Id.ToString(), knowMessages, "知识点"); for (int i = 0; i < konwRes.Count(); i++) questionRes[i].KnowPoint = konwRes[i].KnowPoint; @@ -147,7 +151,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek $"字幕列表 {rCaptionArr}。" + $"输出格式 json字符串 对象格式{fileNameResFormat}"; var task = taskInfo.Id.ToString(); - var fileNameInfoRes = await ChatAsync + var fileNameInfoRes = await chatGPTClient.ChatAsync (task, fileNamePostMessages, "授课章节"); taskInfo.Sections = fileNameInfoRes.授课章节; await videoTaskDB.AsUpdateable() @@ -169,20 +173,42 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek //校验结果质量 var thems = questionRes.Adapt().ToJson(); var pptFormat = taskInfo.VideoType == AttachmentsInfoType.复习 - ? "这堂课是习题课,所讲解内容都是试题。" + ? "这堂课是习题课,所讲解内容几乎都是试题。" : string.Empty; var checkResFormat = """{"Score":打分(number),"Evaluation":评价(string)""";//,"Data":优化后的分段(array)}"""; - var checkMessage = "我为视频的讲解内容做了一些分段,希望你能通读字幕内容后检查下的分段是否符合我的要求?" + - $"检查这些分段的时间是否合理 与相邻的时间段间隔是否大于30秒?" + - $"分段的主题内容,知识点分配是否合理符合实际吗?" + - $"{pptFormat}" + - $"请给出你的打分(0-100,70分及格)以及打分原因。" + - $"这是我的分段 {thems}。" + - $"后续的内容是包含时间戳的视频字幕的固定格式文本。" + - $"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" + - $"最后输出格式为json({checkResFormat})"; - Console.WriteLine(DateTime.Now + "=>3.开始检查视频分段结果"); - return await ChatAsync(taskInfo.Id.ToString(), checkMessage, "结果检查", "deepseek-chat"); + var checkMessage = + $""" + 请你担任一位专业的视频内容分析教研老师,擅长评估视频内容的结构和逻辑流暢度。 + 核心任务: 请根据我提供的【视频分段方案】【完整字幕文本】【完整字幕文本】,对该分段方案进行严谨评估。 + 评估维度与具体标准: + 时间间隔检查(硬性指标) + 严格检查每个分段与下一个分段的开始时间之间的间隔是否大于30秒。 + 输出要求:请列出所有不满足此条件的分段对,并说明具体的时间差。 + 内容结构与主题合理性: + 主题凝聚力:评估单个分段内的内容是否围绕一个清晰、统一的主题展开,是否存在主题混杂或跳跃的情况。 + 逻辑过渡:评估分段之间的过渡是否自然流畅,后一段是否是前一段内容的合理延伸或转折。 + 知识点分配:检查分段内的知识点是否与分段有关联,知识点分布是否合理。 + 综合评分与改进建议: + 请基于以上分析,提供一个0-100的综合得分(70分及格)。 + 详细说明打分理由,并逐条对应到上述评估维度。 + 输入数据格式说明: + 分段方案: {thems} + 字幕文本: 格式为说话人:开始秒:结束秒:内容|下一段字幕。完整内容为:{captions.Captions} + 输出格式: + 输出格式为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})"; } /// @@ -223,7 +249,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek $"字幕结束。" + $"最后请确保输出字幕条数与输入字幕条数一致!!! 如果不一致则重新优化并且确保字幕条数一致!!!!"; //Console.WriteLine(DateTime.Now + $"=>{taskInfo.Id}字幕优化 分段{s}开始..."); - var resData = await ChatAsync(taskInfo.Id.ToString(), postMessages, "优化字幕", "deepseek-chat", 3000); + var resData = await chatClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "优化字幕", "deepseek-chat", 3000); if (resData.Count() != cArr.Count()) { resData = cStrArr.ToArray(); @@ -276,19 +302,18 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek $"{keyFrameArr}" + $"完整的课堂标准流程包含以下5个阶段:课程引入/新知讲解/例题精讲/课堂练习/知识总结。" + $"每个类型的授课阶段允许有多个 例如 多个新知讲解/例题精讲..." + - $"授课阶段主要由每个知识点或者例题内容的讲解或练习组成。" + - $"通过授课阶段的主要讲解内容分析出对应的授课阶段内容总结。" + - $"通过生成的内容总结分析出对应的授课阶段主题。 " + - $"最后请检查每个授课阶段的时长,不允许出现超出800秒或者低于50秒的授课阶段。" + - $"如果授课阶段的时长不符合标准那么请合并/拆分附近相邻内容相近的阶段。" + + $"1.初步划分阶段:基于PPT变化时间点,将字幕内容分割成时间段。每个时间段的起始和结束应接近这些时间点(例如,以时间点为中心,扩展至内容自然过渡处)。" + + $"2.内容分析:对每个时间段,提取主要讲解内容:识别关键词(如“例题”“证明”“练习”“总结”)和内容结构。" + + $"3.判断阶段类型:如果内容以解题为主,归类为“例题精讲”;如果涉及新知识讲解,归类为“新知讲解”;以此类推。" + + $"4.生成内容总结与主题:内容总结:用1-2句话简述该阶段的核心讲解内容(例如,“通过例题演示柯西不等式在求最值中的应用”)。" + + $"5.阶段主题:基于内容总结,提炼一个具体主题(例如,“柯西不等式的基本应用”)。" + + $"6.时长检查与调整:计算每个阶段的时长(结束时间减开始时间)。如果阶段时长低于50秒,则合并相邻的类似内容阶段(例如,将两个连续的例题讲解合并为一个阶段),或扩展时间段以确保最低50秒。调整时需保持内容连贯性。" + + $"7.输出要求:最终分析结果应列出每个阶段的开始时间、结束时间、阶段类型、主题和内容总结(不包含提示词内容),确保划分合理、无重叠,且时长符合要求。" + $"输出内容只返回json格式({resFormat})" + $"字幕格式(开始秒:内容|下一段字幕).以下是包含时间的视频字幕文本。" + $"字幕列表 {captions.Captions} 字幕结束!"; break; case AttachmentsInfoType.复习: - - - postMessages = $"请通过视频字幕内容分析出视频中课堂的授课阶段。" + $"课堂内容与{taskInfo.Subject}学科下的{sections}章节相关。" + @@ -313,7 +338,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek } Console.WriteLine(DateTime.Now + $"=>{taskInfo.Id.ToString()}.开始分析视频内容 {tryCount}"); - return await ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); + //return await chatGPTClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); + return await chatClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "分析字幕"); } catch (Exception ex) { @@ -325,89 +351,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek return null; } - /// - /// 请求AI - /// - /// 返回JSON类型 - /// 任务id - /// 提示词 - /// 任务类型 - /// GPT版本 - /// 最大token 不设置默认最大值 16000/8000 - /// - /// - public async Task ChatAsync(string task, string postMessages, string title, string model = "deepseek-reasoner", int max_tokens = -1) - { - Message[] messageArr = [ - new Message(postMessages,"user"), - ]; - messageArr = messageArr.Where(s => s != null).ToArray(); - var chatRep = new ChatRequest - { - taskId = task, - model = model, - max_tokens = model == "deepseek-reasoner" ? 16000 : 8000, - stream = true, - temperature = 0.2f, - messages = messageArr - }; - if (max_tokens != -1) - chatRep.max_tokens = max_tokens; - var tryCount = 10; - while (tryCount-- > 0) - { - try - { - var time = title + DateTime.Now.ToString("MMddHHmmss"); - var redisCached = new object[2] { chatRep, null }; - redisManager.SetTaskGPTCached(task, time, chatRep); - var chatResp = await chatClient.Chat(chatRep); - var chatResContent = chatResp?.res; - if (string.IsNullOrEmpty(chatResContent)) - throw new Exception("GPT返回message无效结果"); - if (chatResp != null) - { - redisCached[1] = new object[] { chatResp.Value.res, chatResp.Value.u, chatResp.Value.reasoning }; - redisManager.SetTaskGPTCached(task, time, redisCached); - } - chatResContent = chatResContent?.ExtractJsonStrings()?.FirstOrDefault(); - chatResContent = chatResContent?.Replace("\n", ""); - chatResContent = chatResContent?.Replace("```json", ""); - chatResContent = chatResContent?.Replace("```", ""); - chatResContent = chatResContent?.Replace("}{", "},{"); - chatResContent = chatResContent?.Replace("}|{", "},{"); - chatResContent = chatResContent?.Trim(); + - if (string.IsNullOrEmpty(chatResContent)) - throw new Exception("ChatGPT返回结果无有效JSON"); - var startsStr = typeof(T).IsArray ? "[" : "{"; - var endStr = typeof(T).IsArray ? "]" : "}"; - if (!chatResContent.StartsWith(startsStr)) - chatResContent = startsStr + chatResContent; - if (!chatResContent.EndsWith(endStr)) - chatResContent = chatResContent + endStr; - var options = new JsonSerializerOptions - { - // 允许解析不严格符合 JSON 规范的字符串 - AllowTrailingCommas = true, - // 处理不匹配的 JSON 字符 - ReadCommentHandling = JsonCommentHandling.Skip - }; - var questionRes = JsonSerializer.Deserialize(chatResContent, options); - if (questionRes is null) - throw new Exception("ChatGPT返回无效结果"); - return questionRes; - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - Console.WriteLine(DateTime.Now + $"=>ChatGPT结果解析错误 重试剩余{tryCount}"); - } - } - throw new Exception(DateTime.Now + "=>ChatGPT请求失败次数过多!!!"); - } - - /// @@ -428,6 +373,13 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek .ToDictionary(s => s.Key, s => s.First()); var insertData = new List(); var insertQuestionKonw = new List(); + + + insertData = await videoQuestionDB.AsQueryable() + .Where(s => s.VideoTaskId == taskInfo.Id) + .Select() + .ToListAsync(); + foreach (var item in farmeArr) { var knowInfoArr = videoKnowArr @@ -451,29 +403,29 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek #if DEBUG Console.WriteLine(DateTime.Now + $"=>{taskInfo.Id} 提取{knowInfoArr.StartTime}秒试题的试题内容"); - Console.WriteLine(sRes.Result.res.value); #endif //var knowArr=JsonSerializer.Serialize(knowInfoArr.Select(s => new { s.KnowPointId, s.KnowPoint })); - var resFormat = """[{"Type":string(试题类型),"TopicStem":string(试题题干),"QuestionArr":[{"Question":string(子问题),"KnowPointId":(string)知识点ID}]}]"""; + var resFormat = """[{"Type":string(试题类型),"TopicStem":string(试题题干),"QuestionArr":[{"Question":string(问题小问),"KnowPointId":(string)知识点ID}]}]"""; var postMessages = $"我将提供一段内容是Markdown格式的试题。" + $"请提取出其中有效的试题内容(包括 题干,公式试题内提出的问题 )。" + - $"为每个问题关联上限定范围内的知识点(多个则用逗号分割),知识点格式 (知识点Id|知识点名称)知识点范围[{knowArrStr}]。" + + $"为每个问题关联上限定范围内的知识点 知识点格式 (知识点Id|知识点名称,下一个知识点)知识点范围[{knowArrStr}]。" + $"排除不是试题内容的文字,优化试题排版并且去除题号,尽量保留latex数学公式。" + - $"如果存在多道题,则需要拆分成为多个试题对象!" + + $"如果内容中存在多道题,则需要拆分成为多个试题JSON对象!" + $"试题的类型约束在 填空题/判断题/选择题/解答题/填空题 范围内。" + $"如果是有效试题且题干中存在下划线则试题的题型应该是填空题。" + - $"请检查我提供的字符串内容,如果不能识别知识点则不处理知识点,如不包含问题试题则返回`[]`" + + $"请检查我提供的字符串内容,如果不能识别知识点则不处理知识点!" + + $"传入的内容可能只是知识点,可能不包含试题,如果不包含问题试题则最终内容返回空数组JSON`[]`" + $"输出内容只返回json格式为({resFormat})" + $"以下是试题内容" + $"`{sRes.Result.res.value}`"; - var resData = await ChatAsync(taskInfo.Id.ToString(), postMessages, "提取试题", "deepseek-chat"); - //var resData = await ChatAsync(taskInfo.Id.ToString(), postMessages, "提取试题"); + var resData = await chatGPTClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "提取试题"); + //var resData = await chatClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "提取试题"); if (resData is null || resData.Count() == 0) break; foreach (var q in resData) - { + { var TopicId = YitIdHelper.NextId(); foreach (var qt in q.QuestionArr) { @@ -513,8 +465,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek } } } - - if(insertData==null || insertData.Count==0|| insertQuestionKonw.Count==0) + insertData = insertData.GroupBy(s => string.Join("", Regex.Matches(s.StartTime+s.TopicStem+s.Question, "[\u4e00-\u9fa5a-zA-Z0-9]+"))) + .Select(s => s.First()).ToList(); + if (insertData == null || insertData.Count == 0 || insertQuestionKonw.Count == 0) return null; //上传oss 并更新imageUrl @@ -564,16 +517,24 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek var know = await knowledgeInfoDB.GetFirstAsync(s => s.Course_Id == Course_Id && s.Name == sections); if (know is null) throw new Exception("未能找到对应知识点=>" + sections); + List? knowledgeInfos = new List(); var kInfo = await knowledgeInfoDB.GetByIdAsync(know.Parent_Id); - var knowledgeInfos = await knowledgeInfoDB.AsQueryable() - .ToChildListAsync(s => s.Parent_Id, kInfo.Parent_Id == 0 ? kInfo.Id : kInfo.Parent_Id); + try + { + knowledgeInfos = await knowledgeInfoDB.AsQueryable() + .ToChildListAsync(s => s.Parent_Id, kInfo.Parent_Id == 0 ? kInfo.Id : kInfo.Parent_Id); + } + catch (Exception) + { + throw new Exception("没有对应的子知识点=>" + kInfo.Name); + } //AI优化字幕 captionsArr = await OptimizeSubtitles(taskInfo, captionsArr, sections); //合并字幕 var captions = ExpandFunction.GetSpeakerCaptions(captionsArr); var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0; - VideoKnowRes[]? questionRes =null; + VideoKnowRes[]? questionRes = null; var tryCount = 20; while (tryCount-- > 0) { @@ -582,11 +543,10 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek if (questionRes is null) continue; //处理分段 知识点 - var insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos); + var insertData = await GetVideoKnow(questionRes, taskInfo, sections, knowledgeInfos);//ChatGPT //校验结果质量 var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id); - if (checkRes != null && checkRes.Score >= 80) { //写入知识点 @@ -606,7 +566,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek continue; } } - if (tryCount == 0) + if (tryCount == 0) { throw new Exception("重试次数过多!"); } @@ -654,14 +614,14 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek /// /// /// - public async Task GetVideoType(string task) + public async Task GetVideoType(string task) { var taskId = long.Parse(task); var taskInfo = await videoTaskDB.AsQueryable() .Where(s => s.Id == taskId) .FirstAsync(); - if (taskInfo.VideoType != null) + if (taskInfo.VideoType != null) { return; } @@ -687,7 +647,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek $"输出内容只返回json格式为({resFormat})" + $"以下是字幕内容" + $"`{captions.Captions}`"; - var resData = await ChatAsync(taskInfo.Id.ToString(), postMessages, "视频类型", "deepseek-chat"); + var resData = await chatClient.ChatAsync(taskInfo.Id.ToString(), postMessages, "视频类型", "deepseek-chat"); var res = resData.VideoType.ToEnum(); if (res != null) { diff --git a/VideoAnalysisCore/Common/AppCommon.cs b/VideoAnalysisCore/Common/AppCommon.cs index a48e510..35088fd 100644 --- a/VideoAnalysisCore/Common/AppCommon.cs +++ b/VideoAnalysisCore/Common/AppCommon.cs @@ -76,7 +76,13 @@ namespace VideoAnalysisCore.Common /// /// /// - public static string GetVideoPath(string tid) => $"./video/{tid}/{tid}.mp4"; + public static string GetVideoPath(string tid) => $"./video/{tid}/task.mp4"; + /// + /// 获取视频PPT路径 + /// + /// + /// + public static string GetVideoPPTPath(string tid) => $"./video/{tid}/ppt.mp4"; } /// @@ -84,7 +90,7 @@ namespace VideoAnalysisCore.Common /// public static class ExpandFunction { - + static Dictionary FormulaData; static string FormulaDataKey; /// @@ -99,21 +105,31 @@ namespace VideoAnalysisCore.Common /// public static bool DeleteTaskFile(long? taskId) { - if(taskId is null || taskId == 0) return false; + if (taskId is null || taskId == 0) return false; var path = taskId.ToString().LocalPath(); - if (!string.IsNullOrEmpty(path) && Directory.Exists(path)) + string[] filesToDelete = { "ppt.mp4", "task.mp4" }; + + foreach (string fileName in filesToDelete) { - try + string filePath = Path.Combine(path, fileName); + + if (File.Exists(filePath)) { - Directory.Delete(path, true); - Console.WriteLine($"已删除缓存文件: {taskId}"); - } - catch (Exception ex) - { - Console.WriteLine($"删除缓存文件 {taskId} 时出错: {ex.Message}"); + try + { + File.Delete(filePath); + Console.WriteLine($"已成功删除文件: {filePath}"); + } + catch (Exception ex) + { + Console.WriteLine($"删除文件 {filePath} 时出错: {ex.Message}"); + } } + //else + //{ + // Console.WriteLine($"文件不存在,跳过删除: {filePath}"); + //} } - else return false; return true; } diff --git a/VideoAnalysisCore/Job/TaskFileClearJob.cs b/VideoAnalysisCore/Job/TaskFileClearJob.cs index f871f14..a6b5385 100644 --- a/VideoAnalysisCore/Job/TaskFileClearJob.cs +++ b/VideoAnalysisCore/Job/TaskFileClearJob.cs @@ -3,6 +3,7 @@ using Coravel.Invocable; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text; @@ -28,7 +29,46 @@ namespace VideoAnalysisCore.Job { this.videotaskDB = videotaskDB; } - public void DeleteOldCompletedTaskCaches() + public void DeleteTaskAllCaches() + { + + var startTime = -5; + var timeSpan = startTime - 999; + // 计算 6 天前已完成任务缓存 + DateTime twoDaysAgo = DateTime.Now.AddDays(startTime); + DateTime endDaysAgo = DateTime.Now.AddDays(timeSpan); + + // 查询 2 天前任务执行完成的记录 + var completedTasks = videotaskDB.AsQueryable() + .Where(t => ( + //筛选 结束任务 或者 错误任务 + t.LastEnum == Model.Enum.RedisChannelEnum.结束任务 || + t.ErrorMessage != null + ) + && t.EndTime < twoDaysAgo + && t.EndTime > endDaysAgo) + .Select(s => s.Id) + .ToList(); + + // 遍历查询结果,删除缓存文件 + foreach (var taskId in completedTasks) + { + var path = taskId.ToString().LocalPath(); + if (!string.IsNullOrEmpty(path) && Directory.Exists(path)) + { + try + { + Directory.Delete(path, true); + Console.WriteLine($"已删除缓存文件: {taskId}"); + } + catch (Exception ex) + { + Console.WriteLine($"删除缓存文件 {taskId} 时出错: {ex.Message}"); + } + } + } + } + public void DeleteTaskVideoCaches() { try { @@ -44,14 +84,15 @@ namespace VideoAnalysisCore.Job //筛选 结束任务 或者 错误任务 t.LastEnum == Model.Enum.RedisChannelEnum.结束任务 || t.ErrorMessage != null - ) + ) && t.EndTime < twoDaysAgo && t.EndTime > endDaysAgo) + .Select(s => s.Id) .ToList(); // 遍历查询结果,删除缓存文件 - foreach (var task in completedTasks) - ExpandFunction.DeleteTaskFile(task?.Id); + foreach (var taskId in completedTasks) + ExpandFunction.DeleteTaskFile(taskId); } catch (Exception ex) { @@ -61,8 +102,8 @@ namespace VideoAnalysisCore.Job public async Task Invoke() { Console.WriteLine($"{DateTime.Now} 执行=>{this.GetType().FullName}"); - - DeleteOldCompletedTaskCaches(); + DeleteTaskVideoCaches(); + DeleteTaskAllCaches(); } } }