优化 新版AI分析流程
This commit is contained in:
parent
383d0b4c32
commit
2609f13ea5
|
|
@ -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.复习)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ using Whisper.net;
|
|||
|
||||
namespace VideoAnalysisCore.AICore.GPT
|
||||
{
|
||||
/// <summary>
|
||||
/// GPT 接口
|
||||
/// </summary>
|
||||
|
||||
public interface IBserGPT
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -29,4 +27,145 @@ namespace VideoAnalysisCore.AICore.GPT
|
|||
/// <returns></returns>
|
||||
public Task GetVideoType(string task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求数据
|
||||
/// </summary>
|
||||
public class ChatRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话
|
||||
/// </summary>
|
||||
public Message[] messages { get; set; }
|
||||
/// <summary>
|
||||
/// 提问种子值[用来确保 相同参数请求尽可能返回相同参数]
|
||||
/// <para>默认:null</para>
|
||||
/// <para>此功能处于 Beta 阶段。 如果指定,我们的系统将尽最大努力确定性地采样,这样具有相同 and 参数的重复请求应该返回相同的结果。 无法保证确定性,您应该参考 response 参数来监控后端的变化</para>
|
||||
/// </summary>
|
||||
public int? seed { get; set; } = null;
|
||||
/// <summary>
|
||||
/// 推理模型 (deepseek-reasoner)
|
||||
/// </summary>
|
||||
public string model { get; set; } = "deepseek-reasoner";
|
||||
|
||||
public float? max_tokens { get; set; } = 8000;
|
||||
public float? max_completion_tokens { get; set; } = 8000;
|
||||
/// <summary>
|
||||
/// 要使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定。 我们通常建议更改此项或同时更改两者。top_p
|
||||
/// <para> 默认为 1</para>
|
||||
/// <para> <see cref="ChatRequest.top_p"/>联动</para>
|
||||
/// </summary>
|
||||
public float? temperature { get; set; } = 0.2f;
|
||||
/// <summary>
|
||||
/// 一种替代温度采样的方法,称为原子核采样, 其中,模型考虑具有top_p概率的标记的结果 质量。所以 0.1 表示仅包含前 10% 概率质量的代币 被考
|
||||
/// <para>建议与<see cref="ChatRequest.temperature"/>联动</para>
|
||||
/// </summary>
|
||||
public float? top_p { get; set; } = 0.1f;
|
||||
/// <summary>
|
||||
/// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。
|
||||
/// </summary>
|
||||
//public object response_format { get; set; } = new { type = "json_object" };
|
||||
/// <summary>
|
||||
/// 流式返回
|
||||
/// </summary>
|
||||
public bool stream { get; set; } = true;
|
||||
/// <summary>
|
||||
/// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是
|
||||
/// <para>默认设置: ["text"]</para>
|
||||
/// <para>该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用:gpt-4o-audio-preview["text", "audio"]</para>
|
||||
/// </summary>
|
||||
public string modalities { get; set; } = "[\"json\"]";
|
||||
/// <summary>
|
||||
/// 任务id
|
||||
/// </summary>
|
||||
public string taskId { get; set; }
|
||||
public object stream_options { get; set; } = new { include_usage = true };
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// GPT 接口
|
||||
/// </summary>
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// gpt返回值
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 系统指纹
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 模型id
|
||||
/// </summary>
|
||||
public string model { get; set; }
|
||||
/// <summary>
|
||||
/// 对话
|
||||
/// </summary>
|
||||
public ChoiceSSE[] choices { get; set; }
|
||||
/// <summary>
|
||||
/// token使用情况
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 推理内容
|
||||
/// </summary>
|
||||
public string reasoning_content { get; set; }
|
||||
public string refusal { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -31,9 +71,159 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
|
|||
/// <returns>Return HttpResponseMessage for SSE</returns>
|
||||
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<ChatResSSE>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求AI
|
||||
/// </summary>
|
||||
/// <typeparam name="T">返回JSON类型</typeparam>
|
||||
/// <param name="task">任务id</param>
|
||||
/// <param name="postMessages">提示词</param>
|
||||
/// <param name="title">任务类型</param>
|
||||
/// <param name="model">GPT版本</param>
|
||||
/// <param name="max_tokens">最大token <para>不设置默认最大值 16000/8000</para></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task<T> ChatAsync<T>(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<T>(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请求失败次数过多!!!");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Chat
|
||||
/// </summary>
|
||||
|
|
@ -41,6 +231,11 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
|
|||
/// <returns>Return HttpResponseMessage for SSE</returns>
|
||||
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<ChatRes>();
|
||||
|
|
|
|||
|
|
@ -6,104 +6,5 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace VideoAnalysisCore.AICore.GPT.ChatGPT
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求数据
|
||||
/// </summary>
|
||||
public class ChatRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话
|
||||
/// </summary>
|
||||
public Message[] messages { get; set; }
|
||||
/// <summary>
|
||||
/// 提问种子值[用来确保 相同参数请求尽可能返回相同参数]
|
||||
/// <para>默认:null</para>
|
||||
/// <para>此功能处于 Beta 阶段。 如果指定,我们的系统将尽最大努力确定性地采样,这样具有相同 and 参数的重复请求应该返回相同的结果。 无法保证确定性,您应该参考 response 参数来监控后端的变化</para>
|
||||
/// </summary>
|
||||
public int? seed { get; set; } =null;
|
||||
public string model { get; set; } = ChatGPTType.GPT4o;
|
||||
/// <summary>
|
||||
/// 要使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定。 我们通常建议更改此项或同时更改两者。top_p
|
||||
/// <para> 默认为 1</para>
|
||||
/// <para> <see cref="ChatRequest.top_p"/>联动</para>
|
||||
/// </summary>
|
||||
public float temperature { get; set; } = 0.2f;
|
||||
/// <summary>
|
||||
/// 一种替代温度采样的方法,称为原子核采样, 其中,模型考虑具有top_p概率的标记的结果 质量。所以 0.1 表示仅包含前 10% 概率质量的代币 被考
|
||||
/// <para>建议与<see cref="ChatRequest.temperature"/>联动</para>
|
||||
/// </summary>
|
||||
public float top_p { get; set; } = 0.5f;
|
||||
public float max_completion_tokens { get; set; } = 5000;
|
||||
/// <summary>
|
||||
/// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。
|
||||
/// </summary>
|
||||
public object response_format { get; set; } = new { type = "json_object" };
|
||||
/// <summary>
|
||||
/// 流式返回
|
||||
/// </summary>
|
||||
public bool stream { get; set; } = false;
|
||||
/// <summary>
|
||||
/// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是
|
||||
/// <para>默认设置: ["text"]</para>
|
||||
/// <para>该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用:gpt-4o-audio-preview["text", "audio"]</para>
|
||||
/// </summary>
|
||||
public string modalities { get; set; } = "[\"json\"]";
|
||||
/// <summary>
|
||||
/// ai引导新话题
|
||||
/// <para>默认-2 范围[-2~2]</para>
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// gpt返回值
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 系统指纹
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
/// <summary>
|
||||
/// GPT-4O 型 -> gpt-4o-2024-08-06
|
||||
/// </summary>
|
||||
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";
|
||||
/// <summary>
|
||||
/// gpt-4-turbo-preview gpt-4-0125-preview
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 访问GPT
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 请求AI
|
||||
/// </summary>
|
||||
/// <typeparam name="T">返回JSON类型</typeparam>
|
||||
/// <param name="task">任务id</param>
|
||||
/// <param name="postMessages">提示词</param>
|
||||
/// <param name="title">任务类型</param>
|
||||
/// <param name="model">GPT版本</param>
|
||||
/// <param name="max_tokens">最大token <para>不设置默认最大值 16000/8000</para></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task<T> ChatAsync<T>(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<T>(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请求失败次数过多!!!");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -6,140 +6,6 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求数据
|
||||
/// </summary>
|
||||
public class ChatRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话
|
||||
/// </summary>
|
||||
public Message[] messages { get; set; }
|
||||
/// <summary>
|
||||
/// 提问种子值[用来确保 相同参数请求尽可能返回相同参数]
|
||||
/// <para>默认:null</para>
|
||||
/// <para>此功能处于 Beta 阶段。 如果指定,我们的系统将尽最大努力确定性地采样,这样具有相同 and 参数的重复请求应该返回相同的结果。 无法保证确定性,您应该参考 response 参数来监控后端的变化</para>
|
||||
/// </summary>
|
||||
public int? seed { get; set; } =null;
|
||||
/// <summary>
|
||||
/// 推理模型 (deepseek-reasoner)
|
||||
/// </summary>
|
||||
public string model { get; set; } = "deepseek-reasoner";
|
||||
|
||||
public float max_tokens { get; set; } = 8000;
|
||||
/// <summary>
|
||||
/// 要使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定。 我们通常建议更改此项或同时更改两者。top_p
|
||||
/// <para> 默认为 1</para>
|
||||
/// <para> <see cref="ChatRequest.top_p"/>联动</para>
|
||||
/// </summary>
|
||||
public float temperature { get; set; } = 0.2f;
|
||||
/// <summary>
|
||||
/// 一种替代温度采样的方法,称为原子核采样, 其中,模型考虑具有top_p概率的标记的结果 质量。所以 0.1 表示仅包含前 10% 概率质量的代币 被考
|
||||
/// <para>建议与<see cref="ChatRequest.temperature"/>联动</para>
|
||||
/// </summary>
|
||||
public float top_p { get; set; } = 0.1f;
|
||||
/// <summary>
|
||||
/// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。
|
||||
/// </summary>
|
||||
//public object response_format { get; set; } = new { type = "json_object" };
|
||||
/// <summary>
|
||||
/// 流式返回
|
||||
/// </summary>
|
||||
public bool stream { get; set; } = false;
|
||||
/// <summary>
|
||||
/// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是
|
||||
/// <para>默认设置: ["text"]</para>
|
||||
/// <para>该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用:gpt-4o-audio-preview["text", "audio"]</para>
|
||||
/// </summary>
|
||||
public string modalities { get; set; } = "[\"json\"]";
|
||||
/// <summary>
|
||||
/// 任务id
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 推理内容
|
||||
/// </summary>
|
||||
public string reasoning_content { get; set; }
|
||||
public string refusal { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// gpt返回值
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 系统指纹
|
||||
/// </summary>
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 模型id
|
||||
/// </summary>
|
||||
public string model { get; set; }
|
||||
/// <summary>
|
||||
/// 对话
|
||||
/// </summary>
|
||||
public ChoiceSSE[] choices { get; set; }
|
||||
/// <summary>
|
||||
/// token使用情况
|
||||
/// </summary>
|
||||
public Usage usage { get; set; }
|
||||
}
|
||||
public class ChoiceSSE
|
||||
{
|
||||
public int index { get; set; }
|
||||
public Message delta { get; set; }
|
||||
public string finish_reason { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CourseGradingCriteria> criteriaDB;
|
||||
private readonly RedisManager redisManager;
|
||||
private readonly Repository<VideoTask> videoTaskDB;
|
||||
|
|
@ -43,9 +46,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
/// </summary>
|
||||
/// <param name="moonshotClient"></param>
|
||||
/// <param name="logger"></param>
|
||||
public DeepSeek_GPT(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB,
|
||||
Repository<KnowledgeInfo> knowledgeInfoDB, Repository<VideoKonwPoint> videoKonwPointDB, SimpLetexClient simpLetexClient,
|
||||
Repository<VideoQuestion> videoQuestionDB, OssClient ossClient, Repository<VideoQuestionKonw> videoQuestionKonwDB, RedisManager redisManager)
|
||||
public DeepSeek_GPT(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB,
|
||||
Repository<KnowledgeInfo> knowledgeInfoDB, Repository<VideoKonwPoint> videoKonwPointDB, SimpLetexClient simpLetexClient,
|
||||
Repository<VideoQuestion> videoQuestionDB, OssClient ossClient, Repository<VideoQuestionKonw> 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;
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取内容对应的章节
|
||||
|
|
@ -84,7 +88,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
$" 格式 (方法点Id|方法点名称) " +
|
||||
$"提供的知识点名称({knows})。";
|
||||
Console.WriteLine(DateTime.Now + "=>2.开始分析视频内容知识点");
|
||||
var konwRes = await ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), knowMessages, "知识点");
|
||||
var konwRes = await chatGPTClient.ChatAsync<VideoKnowRes[]>(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<FileNameInfo>
|
||||
var fileNameInfoRes = await chatGPTClient.ChatAsync<FileNameInfo>
|
||||
(task, fileNamePostMessages, "授课章节");
|
||||
taskInfo.Sections = fileNameInfoRes.授课章节;
|
||||
await videoTaskDB.AsUpdateable()
|
||||
|
|
@ -169,20 +173,42 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
//校验结果质量
|
||||
var thems = questionRes.Adapt<VideoKnowQueryDto[]>().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<CheckMessageDto>(taskInfo.Id.ToString(), checkMessage, "结果检查", "deepseek-chat");
|
||||
var checkMessage =
|
||||
$"""
|
||||
请你担任一位专业的视频内容分析教研老师,擅长评估视频内容的结构和逻辑流暢度。
|
||||
核心任务: 请根据我提供的【视频分段方案】【完整字幕文本】【完整字幕文本】,对该分段方案进行严谨评估。
|
||||
评估维度与具体标准:
|
||||
时间间隔检查(硬性指标)
|
||||
严格检查每个分段与下一个分段的开始时间之间的间隔是否大于30秒。
|
||||
输出要求:请列出所有不满足此条件的分段对,并说明具体的时间差。
|
||||
内容结构与主题合理性:
|
||||
主题凝聚力:评估单个分段内的内容是否围绕一个清晰、统一的主题展开,是否存在主题混杂或跳跃的情况。
|
||||
逻辑过渡:评估分段之间的过渡是否自然流畅,后一段是否是前一段内容的合理延伸或转折。
|
||||
知识点分配:检查分段内的知识点是否与分段有关联,知识点分布是否合理。
|
||||
综合评分与改进建议:
|
||||
请基于以上分析,提供一个0-100的综合得分(70分及格)。
|
||||
详细说明打分理由,并逐条对应到上述评估维度。
|
||||
输入数据格式说明:
|
||||
分段方案: {thems}
|
||||
字幕文本: 格式为说话人:开始秒:结束秒:内容|下一段字幕。完整内容为:{captions.Captions}
|
||||
输出格式:
|
||||
输出格式为json: {checkResFormat}
|
||||
""";
|
||||
return await chatGPTClient.ChatAsync<CheckMessageDto>(taskInfo.Id.ToString(), checkMessage, "结果检查");
|
||||
//var res = await chatClient.ChatAsync<CheckMessageDto>(taskInfo.Id.ToString(), checkMessage, "结果检查");
|
||||
|
||||
//var checkMessage1 = "我为视频的讲解内容做了一些分段,希望你能通读字幕内容后检查下的分段是否符合我的要求?" +
|
||||
// $"检查这些分段的时间是否合理 与相邻的时间段间隔是否大于30秒?" +
|
||||
// $"分段的主题内容,知识点分配是否合理符合实际吗?" +
|
||||
// $"{pptFormat}" +
|
||||
// $"请给出你的打分(0-100,70分及格)以及打分原因。" +
|
||||
// $"这是我的分段 {thems}。" +
|
||||
// $"后续的内容是包含时间戳的视频字幕的固定格式文本。" +
|
||||
// $"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" +
|
||||
// $"最后输出格式为json({checkResFormat})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -223,7 +249,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
$"字幕结束。" +
|
||||
$"最后请确保输出字幕条数与输入字幕条数一致!!! 如果不一致则重新优化并且确保字幕条数一致!!!!";
|
||||
//Console.WriteLine(DateTime.Now + $"=>{taskInfo.Id}字幕优化 分段{s}开始...");
|
||||
var resData = await ChatAsync<string[]>(taskInfo.Id.ToString(), postMessages, "优化字幕", "deepseek-chat", 3000);
|
||||
var resData = await chatClient.ChatAsync<string[]>(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<VideoKnowRes[]>(taskInfo.Id.ToString(), postMessages, "分析字幕");
|
||||
//return await chatGPTClient.ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), postMessages, "分析字幕");
|
||||
return await chatClient.ChatAsync<VideoKnowRes[]>(taskInfo.Id.ToString(), postMessages, "分析字幕");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -325,89 +351,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求AI
|
||||
/// </summary>
|
||||
/// <typeparam name="T">返回JSON类型</typeparam>
|
||||
/// <param name="task">任务id</param>
|
||||
/// <param name="postMessages">提示词</param>
|
||||
/// <param name="title">任务类型</param>
|
||||
/// <param name="model">GPT版本</param>
|
||||
/// <param name="max_tokens">最大token <para>不设置默认最大值 16000/8000</para></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task<T> ChatAsync<T>(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<T>(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请求失败次数过多!!!");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -428,6 +373,13 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
|
|||
.ToDictionary(s => s.Key, s => s.First());
|
||||
var insertData = new List<VideoQuestionOSSDto>();
|
||||
var insertQuestionKonw = new List<VideoQuestionKonw>();
|
||||
|
||||
|
||||
insertData = await videoQuestionDB.AsQueryable()
|
||||
.Where(s => s.VideoTaskId == taskInfo.Id)
|
||||
.Select<VideoQuestionOSSDto>()
|
||||
.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<VideoQuestionOSSDto[]>(taskInfo.Id.ToString(), postMessages, "提取试题", "deepseek-chat");
|
||||
//var resData = await ChatAsync<VideoQuestionOSSDto[]>(taskInfo.Id.ToString(), postMessages, "提取试题");
|
||||
var resData = await chatGPTClient.ChatAsync<VideoQuestionOSSDto[]>(taskInfo.Id.ToString(), postMessages, "提取试题");
|
||||
//var resData = await chatClient.ChatAsync<VideoQuestionOSSDto[]>(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<KnowledgeInfo>? knowledgeInfos = new List<KnowledgeInfo>();
|
||||
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
|
|||
/// </summary>
|
||||
/// <param name="task"></param>
|
||||
/// <returns></returns>
|
||||
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<AIVideoTypeDto?>(taskInfo.Id.ToString(), postMessages, "视频类型", "deepseek-chat");
|
||||
var resData = await chatClient.ChatAsync<AIVideoTypeDto?>(taskInfo.Id.ToString(), postMessages, "视频类型", "deepseek-chat");
|
||||
var res = resData.VideoType.ToEnum<AttachmentsInfoType>();
|
||||
if (res != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -76,7 +76,13 @@ namespace VideoAnalysisCore.Common
|
|||
/// </summary>
|
||||
/// <param name="tid"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetVideoPath(string tid) => $"./video/{tid}/{tid}.mp4";
|
||||
public static string GetVideoPath(string tid) => $"./video/{tid}/task.mp4";
|
||||
/// <summary>
|
||||
/// 获取视频PPT路径
|
||||
/// </summary>
|
||||
/// <param name="tid"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetVideoPPTPath(string tid) => $"./video/{tid}/ppt.mp4";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -84,7 +90,7 @@ namespace VideoAnalysisCore.Common
|
|||
/// </summary>
|
||||
public static class ExpandFunction
|
||||
{
|
||||
|
||||
|
||||
static Dictionary<string, string> FormulaData;
|
||||
static string FormulaDataKey;
|
||||
/// <summary>
|
||||
|
|
@ -99,21 +105,31 @@ namespace VideoAnalysisCore.Common
|
|||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue