修复 deepseek 流式传入失败问题

新增 阿里云api
This commit is contained in:
小肥羊 2025-02-13 18:07:52 +08:00
parent 67f3c4fb57
commit 613019da82
11 changed files with 174 additions and 90 deletions

View File

@ -11,7 +11,9 @@
@for (int i = 0; i < videoKnows.Length; i++)
{
var item = videoKnows[i];
<button class="kBtn" onclick="spClick(@i,this)">@getF(item) @item.Theme </button>
<button class="kBtn" onclick="spClick(@i,this)" title="@item.Content">
<span>@getF(item) @item.Theme</span>
<br /><span class="kSpan">#@item.KnowPointId @item.KnowPoint</span></button>
}
</div>
}

View File

@ -11,7 +11,10 @@
float: left;
overflow-x: hidden;
}
.kSpan {
color:rgba(120, 120, 120,0.66);
font-size:0.8rem;
}
video {
width: 94%;
height: 85%;

View File

@ -34,6 +34,10 @@
"DeepSeek": {
"Host": "https://api.deepseek.com/chat/completions",
"ApiKey": "sk-88d3d2bc3dae4d50854b2569b281cf76"
},
"aliyun": {
"Host": "https://dashscope.aliyuncs.com/compatible-mode/",
"ApiKey": "sk-1742c2bf7b9846ae835de598dc6c427b"
}
},
"DB": {

View File

@ -37,22 +37,22 @@ namespace VideoAnalysisCore.AICore.GPT.ChatGPT
/// <summary>
/// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。
/// </summary>
public object response_format = new { type = "json_object" };
public object response_format { get; set; } = new { type = "json_object" };
/// <summary>
/// 流式返回
/// </summary>
public bool stream =false;
public bool stream { get; set; } = false;
/// <summary>
/// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是
/// <para>默认设置: ["text"]</para>
/// <para>该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用gpt-4o-audio-preview["text", "audio"]</para>
/// </summary>
public string modalities = "[\"json\"]";
public string modalities { get; set; } = "[\"json\"]";
/// <summary>
/// ai引导新话题
/// <para>默认-2 范围[-2~2]</para>
/// </summary>
public int presence_penalty = -2;
public int presence_penalty { get; set; } = -2;
}
public class Message

View File

@ -12,14 +12,27 @@ using System.Net;
using VideoAnalysisCore.AICore.GPT.KIMI;
using System.Threading;
using System;
using System.IO;
using VideoAnalysisCore.AICore.GPT.ChatGPT;
namespace VideoAnalysisCore.AICore.GPT.DeepSeek
{
public class DeepSeekGPTClient
{
public static string Host = AppCommon.Config.ChatGpt.DeepSeek.Host;
public static string ApiKey = AppCommon.Config.ChatGpt.DeepSeek.ApiKey;
private readonly string Path = "v1/chat/completions";
public static string Host = AppCommon.Config.ChatGpt.aliyun.Host;
public static string ApiKey = AppCommon.Config.ChatGpt.aliyun.ApiKey;
//private readonly string Path = "";
//public static string Host = AppCommon.Config.ChatGpt.DeepSeek.Host;
//public static string ApiKey = AppCommon.Config.ChatGpt.DeepSeek.ApiKey;
//public static string Host = AppCommon.Config.ChatGpt.ChatGpt.Host;
//public static string ApiKey = AppCommon.Config.ChatGpt.ChatGpt.ApiKey;
//private readonly string Path = "v1/chat/completions";
private readonly IHttpClientFactory _httpClientFactory;
@ -36,19 +49,35 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
/// <returns>Return HttpResponseMessage for SSE</returns>
public async Task<(Usage u, string res,string reasoning)?> Chat(ChatRequest chatReq)
{
chatReq.model = "deepseek-r1";
if (chatReq.stream) return await ChatSSE(chatReq);
postStar:
var requestBody = System.Text.Json.JsonSerializer.Serialize(chatReq);
var chatResp = await PostJsonStreamAsync(string.Empty, requestBody);
HttpResponseMessage chatResp = PostJsonStream(Path, requestBody);
var res1 = await chatResp.Content.ReadAsStringAsync();
if (res1 is null || string.IsNullOrEmpty(res1))
throw new Exception($" GPT模型返回空内容 返回参数: " +
$" {res1}");
if (res1 == null || string.IsNullOrEmpty(res1)|| !chatResp.IsSuccessStatusCode)
{
Console.WriteLine(DateTime.Now + $"=>GPT请求失败重试 Code = {chatResp.StatusCode} Res={res1}");
goto postStar;
}
//throw new Exception($" GPT模型返回异常 返回参数: " +
// $" {System.Text.Json.JsonSerializer.Serialize(res1)}");
var res = await chatResp.Content.ReadFromJsonAsync<ChatRes>();
if (res is null || res.error != null)
throw new Exception($" GPT模型返回异常 返回参数: " +
$" {System.Text.Json.JsonSerializer.Serialize(res)}");
var chatResContent = res?.choices.FirstOrDefault()?.message.content.Trim();
var chatResReasoning = res?.choices.FirstOrDefault()?.message.reasoning_content?.Trim();
var chatResReasoning = string.Empty;
if (chatResContent.StartsWith("<think"))
{
chatResReasoning = chatResContent.Substring(7, chatResContent.IndexOf("</think")-7);
chatResContent = chatResContent
.Substring(chatResContent.IndexOf("</think>")+8);
}
else
chatResReasoning = res?.choices.FirstOrDefault()?.message.reasoning_content?.Trim();
if (string.IsNullOrEmpty(chatResContent))
return null;
@ -56,9 +85,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
}
private async Task<HttpResponseMessage> PostJsonStreamAsync(string path, string json)
private HttpResponseMessage PostJsonStream(string path, string json)
{
var uriBuilder = new UriBuilder(Host + path);
var uriBuilder = new UriBuilder(Host + Path);
var maxRestart = 4;
var errorMSG = new Exception[maxRestart];
for (int i = 0; i < maxRestart; i++)
@ -74,8 +103,9 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
client.DefaultRequestHeaders.ConnectionClose = true;
var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri);
request.Content = new StringContent(json, Encoding.UTF8, "application/json"); ;
return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
return client.Send(request, HttpCompletionOption.ResponseHeadersRead);
}
catch (Exception e)
{
@ -86,8 +116,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
Console.WriteLine(e.StackTrace);
Console.WriteLine("==============================================");
}
Thread.Sleep(1000);
}
}
throw errorMSG.Last(s => s != null);
}
@ -104,8 +134,8 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
{
chatReq.stream = true;
var requestBody = System.Text.Json.JsonSerializer.Serialize(chatReq);
var chatResp = await PostJsonStreamAsync(string.Empty, requestBody);
using var stream = await chatResp.Content.ReadAsStreamAsync();
var chatResp = PostJsonStream(string.Empty, requestBody);
using var stream = chatResp.Content.ReadAsStream();
using var reader = new StreamReader(stream, Encoding.UTF8);
string line;
var messageBuilder = new StringBuilder();
@ -114,14 +144,13 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
var splitCount = "data:".Length;
while (true)
{
line = await reader.ReadLineAsync();
if (line is null || string.IsNullOrEmpty(line))
throw new Exception("AI返回无效内容 =>Null/Empty");
line = reader.ReadLine();
if (line is null || string.IsNullOrEmpty(line)) continue;
else if (line.EndsWith("[DONE]"))
{
// 表示一条消息结束
string message = messageBuilder.ToString();
string message2 = messageBuilder.ToString();
string message2 = messageBuilder1.ToString();
messageBuilder.Clear();
var u = lastChat?.usage;
if (u == null || string.IsNullOrEmpty(message))
@ -151,7 +180,6 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
}
}
}
return null;
}
}

View File

@ -42,17 +42,18 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
/// <summary>
/// 一个对象,用于指定模型必须输出的格式。设置为 enable 结构化输出,确保模型与您提供的 JSON 匹配 图式。
/// </summary>
public object response_format = new { type = "json_object" };
public object response_format { get; set; } = new { type = "json_object" };
/// <summary>
/// 流式返回
/// </summary>
public bool stream =false;
public bool stream { get; set; } = false;
/// <summary>
/// 您希望模型为此请求生成的 Output types。 大多数模型都能够生成文本,这是
/// <para>默认设置: ["text"]</para>
/// <para>该模型还可用于生成音频。自 请求此模型同时生成文本和音频响应,您可以 用gpt-4o-audio-preview["text", "audio"]</para>
/// </summary>
public string modalities = "[\"json\"]";
public string modalities { get; set; } = "[\"json\"]";
public object stream_options { get; set; } = new { include_usage = true };
}
public class Message

View File

@ -20,18 +20,20 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
private readonly DeepSeekGPTClient chatClient;
private readonly Repository<CourseGradingCriteria> criteriaDB;
private readonly Repository<VideoTask> videoTaskDB;
private readonly Repository<VideoKonwPoint> videoKonwPointDB;
private readonly Repository<KnowledgeInfo> knowledgeInfoDB;
/// <summary>
/// 初始化
/// </summary>
/// <param name="moonshotClient"></param>
/// <param name="logger"></param>
public DeepSeek_GPT(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB, Repository<KnowledgeInfo> knowledgeInfoDB)
public DeepSeek_GPT(DeepSeekGPTClient moonshotClient, Repository<CourseGradingCriteria> criteria, Repository<VideoTask> videoTaskDB, Repository<KnowledgeInfo> knowledgeInfoDB, Repository<VideoKonwPoint> videoKonwPointDB)
{
chatClient = moonshotClient;
criteriaDB = criteria;
this.videoTaskDB = videoTaskDB;
this.knowledgeInfoDB = knowledgeInfoDB;
this.videoKonwPointDB = videoKonwPointDB;
}
@ -49,33 +51,35 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
var subject = "数学";
var xkwKnows = await knowledgeInfoDB.AsQueryable()
.Where(s => s.Course_Id == 27
&& s.Depth == 2)
&& (s.Depth == 3
|| s.Depth == 2 ))
.Select(s => s.Name).ToArrayAsync();
string title = taskInfo.MediaName;
var fileNameResFormat = "{授课章节: string|null, 授课内容:string}";
var fileNameResFormat = "{授课章节: string|null}";
var fileNamePostMessages = title +
" 这是一堂课的标题,请你帮我分析一些关于课堂方面的内容." +
$"1.分析出高中{subject}课堂授课的主要章节(例如 章节: 数列),章节范围限定在[{string.Join(',', xkwKnows)}]范围." +
$"2.分析出这堂课的主要授课内容." +
" 这是一堂课的标题,请你基于标题帮我分析出这堂课所讲授的内容与最恰当的授课章节(尽可能的关联最贴切的章节并且就保留一个)." +
$"章节范围限定在[{string.Join(',', xkwKnows)}]范围." +
$"输出格式 json字符串 对象格式{fileNameResFormat}";
var fileNameInfoRes = await ChatAsync<FileNameInfo>(task, fileNamePostMessages, null, "deepseek-chat");
var fileNameInfoRes = await ChatAsync<FileNameInfo>(task, fileNamePostMessages, null);//, "deepseek-chat");
var captions = ExpandFunction.GetSpeakerCaptions(task);
var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0;
var criteriaBuilder = new StringBuilder();
var resFormat = """[{"StartTime":开始秒(number),"Theme":主题(string),"KnowPoint":主题方法点(string),"KnowPointId":主题方法点Id(string)"Content":内容总结(string)}]""";
var know = await knowledgeInfoDB.GetFirstAsync(s => s.Name == fileNameInfoRes.);
var know = await knowledgeInfoDB.GetFirstAsync(s =>s.Course_Id==27 && s.Name == fileNameInfoRes.);
if (know is null)
throw new Exception("未能找到对应知识点=>"+ fileNameInfoRes.);
var knowledgeInfos = await knowledgeInfoDB.AsQueryable().ToChildListAsync(s => s.Parent_Id, know.Id);
var knows = string.Join(',', knowledgeInfos.Select(s => s.Id + "|"+ s.Name));
var postMessages =
$"你的任务是分析视频字幕内容并提取出中国高考考试试题方法点,然后根据步骤分析出知识片段" +
$"按以下步骤完成:" +
$"1.识别方法点:提取字幕内容中与{subject}考试属于{fileNameInfoRes.授课章节}章节相关的方法点。" +
$"2.关联合并知识内容相似的知识点来合并为知识片段。" +
$"2.关联合并知识内容相似的知识点来合并为知识片段。如果知识片段时长超过20分钟则考虑拆封为2个更加细微贴切的知识片段" +
$"3.分配空余未使用的时间段到内容相近的知识片段时间区间来获取更加详细的上下文,但是请避免知识片段之间时间重合。" +
$"4.分析总结:基于提取出的知识片段内容称来匹配我提供的知识点,来作为片段的知识点(请确保匹配的知识点是我提供的,否者片段知识点值为空字符串)" +
$"提供的方法点名称(-1|{fileNameInfoRes.授课章节}的基本概念,-2|{fileNameInfoRes.授课章节}的练习与应用,-3|{fileNameInfoRes.授课章节}的例题讲解,{knows})。 格式 (方法点Id|方法点名称)" +
$"提供的方法点名称({knows})。 格式 (方法点Id|方法点名称) 如果一个知识片段出现多个知识点那么知识点ID与知识点名称都用逗号','分割" +
$"输入:包含时间戳的视频字幕文本。" +
$"以下是包含时间的视频字幕文本。" +
$"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).字幕列表 {captions.Captions}" +
@ -97,6 +101,23 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
item.EndTime = (int)(questionRes[i + 1]?.StartTime ?? 0) - 1;
}
await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskId);
var data = questionRes.Select(s => new VideoKonwPoint()
{
Content = s.Content,
Theme = s.Theme,
StartTime = s.StartTime,
EndTime =s.EndTime,
KnowPoint = s.KnowPoint,
KnowPointId = s.KnowPointId,
TagId = taskInfo.TagId,
VideoTaskId = taskInfo.Id,
}).ToList();
await videoKonwPointDB.InsertRangeAsync(data);
await RedisExpand.Redis
.HMSetAsync(RedisExpandKey.Task(task), "VideoKnows", questionRes);
@ -117,10 +138,11 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
var chatRep = new ChatRequest
{
model = model,
//stream = model== "deepseek-reasoner",
stream = model== "deepseek-reasoner",
max_tokens = maxTokens,
temperature = 0.2f,
messages = messageArr
};
RedisExpand.SetTaskGPTReqCached(task, chatRep);

View File

@ -19,8 +19,6 @@ using VideoAnalysisCore.Enum;
using VideoAnalysisCore.Interface;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
using Whisper.net;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace VideoAnalysisCore.Common
{
@ -310,6 +308,7 @@ namespace VideoAnalysisCore.Common
public GptConfig ChatGpt { get; set; } = new GptConfig();
public GptConfig DeepSeek { get; set; } = new GptConfig();
public GptConfig KIMI { get; set; } = new GptConfig();
public GptConfig aliyun { get; set; } = new GptConfig();
}
/// <summary>

View File

@ -2,8 +2,10 @@
using FreeRedis;
using FreeRedis.Internal;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Schema;
using SqlSugar.IOC;
using System;
using System.Text.Json;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Xml.Linq;
@ -166,7 +168,7 @@ namespace VideoAnalysisCore.Common
var taskData = await DbScoped.SugarScope.Queryable<VideoTask>()
.FirstAsync(s => s.Id == tId);
taskData.ChatAnalysis = gptRes;
taskData.ChatAnalysis =JsonSerializer.Serialize(gptRes);
taskData.ChatAnalysisScore = gptRes?.Assessment?.Merit?.Sum(s => s.Score) ?? 0;
taskData.ErrorMessage = string.Empty;
taskData.LastEnum = RedisChannelEnum.EndTask;

View File

@ -0,0 +1,68 @@
using SqlSugar;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Text.Json;
using UserCenter.Model.Enum;
using VideoAnalysisCore.AICore.GPT.Dto;
using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Enum;
using VideoAnalysisCore.Interface;
using Whisper.net;
namespace VideoAnalysisCore.Model
{
/// <summary>
/// 视频任务知识片段
/// </summary>
[SugarTable("videokonwpoint")]
public class VideoKonwPoint : IDB
{
/// <summary>
/// id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public long Id { get; set; }
/// <summary>
/// 视频任务id
/// </summary>
public long VideoTaskId { get; set; }
/// <summary>
/// 自定义Id [任务视频自定义id]
/// <see cref="VideoTask.TagId"/>
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? TagId { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[SugarColumn( IsNullable = true)]
public float? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
[SugarColumn(IsNullable = true)]
public float? EndTime { get; set; }
/// <summary>
/// 持续时间
/// </summary>
[SugarColumn(IsIgnore = true)]
public float? KeepTime => (EndTime ?? 0) - StartTime ?? 0;
/// <summary>
/// 主题
/// </summary>
public string? Theme { get; set; }
/// <summary>
/// 知识点
/// </summary>
public string? KnowPoint { get; set; }
/// <summary>
/// 知识点ID
/// </summary>
public string? KnowPointId { get; set; }
/// <summary>
/// 内容总结
/// </summary>
public string? Content { get; set; }
}
}

View File

@ -72,44 +72,17 @@ namespace VideoAnalysisCore.Model
/// 字幕缓存
/// </summary>
[SugarColumn(ColumnName = "Captions", ColumnDataType = "longtext", IsNullable = true)]
public string _Captions { get; set; } = "[]";
/// <summary>
/// 字幕缓存
/// </summary>
[SugarColumn(IsIgnore = true)]
public SenseVoiceRes[]? Captions
{
get=> JsonSerializer.Deserialize<SenseVoiceRes[]>(_Captions ?? "[]");
set => _Captions = JsonSerializer.Serialize(value);
}
public string Captions { get; set; } = "[]";
/// <summary>
/// 说话人日志解析缓存
/// </summary>
[SugarColumn(ColumnName = "Speaker", ColumnDataType = "longtext", IsNullable = true)]
public string _Speaker { get; set; } = "[]";
/// <summary>
/// 说话人日志解析缓存
/// </summary>
[SugarColumn(IsIgnore = true)]
public OfflineSpeakerRes[]? Speaker
{
get => JsonSerializer.Deserialize<OfflineSpeakerRes[]>(_Speaker ?? "[]");
set => _Speaker = JsonSerializer.Serialize(value);
}
public string Speaker { get; set; } = "[]";
/// <summary>
/// Chat模型分析缓存
/// </summary>
[SugarColumn(ColumnName = "ChatAnalysis", ColumnDataType = "longtext", IsNullable = true)]
public string _ChatAnalysis { get; set; } = "{}";
/// <summary>
/// Chat模型分析缓存
/// </summary>
[SugarColumn(IsIgnore = true)]
public TaskRes? ChatAnalysis
{
get => JsonSerializer.Deserialize<TaskRes>(_ChatAnalysis ?? "{}");
set => _ChatAnalysis = JsonSerializer.Serialize(value);
}
public string ChatAnalysis { get; set; } = "{}";
/// <summary>
/// AI模型评分
/// </summary>
@ -133,24 +106,6 @@ namespace VideoAnalysisCore.Model
/// </summary>
[SugarColumn(ColumnDataType = "varchar", Length = 255)]
public string StartTime { get; set; } ="{}";
/// <summary>
/// 开始时间轴
/// <para>逻辑字段</para>
/// </summary>
[SugarColumn(IsIgnore = true)]
public Dictionary<RedisChannelEnum, DateTime> StartTimeDic
{
get
{
return JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>(StartTime??"{}")
??new Dictionary<RedisChannelEnum, DateTime>();
}
set
{
StartTime = JsonSerializer.Serialize(value);
}
}
}
}