优化 AI分析流程

This commit is contained in:
小肥羊 2025-03-11 14:22:56 +08:00
parent ae6e5ce836
commit c39fbb7501
17 changed files with 476 additions and 100 deletions

View File

@ -24,14 +24,18 @@ namespace Learn.VideoAnalysis.Controllers
private readonly ILogger<LJZK_Controller> _logger;
private readonly IMapper mp;
private readonly Repository<NodeSubscription> nodesubscriptionDB;
private readonly Repository<VideoTask> videoTaskDB;
private readonly Repository<VideoKonwPoint> videoKonwPointDB;
private readonly IBserGPT chatGPT;
public LJZK_Controller(ILogger<LJZK_Controller> logger,
IMapper mp, IBserGPT chatGPT, Repository<NodeSubscription> nodesubscriptionDB)
IMapper mp, IBserGPT chatGPT, Repository<NodeSubscription> nodesubscriptionDB, Repository<VideoTask> videoTaskDB = null, Repository<VideoKonwPoint> videoKonwPointDB = null)
{
_logger = logger;
this.mp = mp;
this.chatGPT = chatGPT;
this.nodesubscriptionDB = nodesubscriptionDB;
this.videoTaskDB = videoTaskDB;
this.videoKonwPointDB = videoKonwPointDB;
}
@ -43,12 +47,16 @@ namespace Learn.VideoAnalysis.Controllers
[HttpPost(Name = "NodeSubscription")]
public async Task<IActionResult> NodeSubscription(NodeMonitoringReq req)
{
if(req is null || req.NodeId ==0)
return BadRequest("无效的提交数据");
if (nodesubscriptionDB.IsAny(s => s.NodeId == req.NodeId))
return BadRequest("重复添加了节点监控任务" + req.NodeId);
var res = await nodesubscriptionDB.InsertReturnEntityAsync(new NodeSubscription()
{
NodeId = req.NodeId,
TaskType = req.Type ?? default
TaskType = req.Type ?? default,
Subject = req.Subject?? default,
});
return Ok(res);
}
@ -76,7 +84,52 @@ namespace Learn.VideoAnalysis.Controllers
.Select(s => new { Text = s.ToString(), Value = (int)s }));
}
/// <summary>
/// 获取视频知识点片段<para>taskId/tagId二选一</para>
/// </summary>
/// <param name="taskId"></param>
/// <param name="tagId">自定义id</param>
/// <returns></returns>
[HttpGet(Name = "TaskKnowInfo")]
public async Task<IActionResult> TaskKnowInfo(long taskId, string? tagId)
{
if (taskId == 0 && string.IsNullOrEmpty(tagId))
return BadRequest();
var task = await videoTaskDB.AsQueryable()
.WhereIF(taskId != 0, s => s.Id == taskId)
.WhereIF(!string.IsNullOrEmpty(tagId), s => s.TagId == tagId)
.FirstAsync();
if (task is null)
return BadRequest("无效任务");
var konwArr = await videoKonwPointDB.AsQueryable()
.Where(s => s.VideoTaskId == task.Id)
.ToArrayAsync();
if (konwArr is null || konwArr.Length == 0)
return BadRequest("无效任务");
return Ok(new TaskKnowRes()
{
TagId = task.TagId,
Status = task.LastEnum,
VideoTaskId = task.Id,
KnowBlockArr = konwArr
.GroupBy(s => s.StartTime)
.Select(s => new TaskKnowBlock()
{
Id = s.First().Id,
Content = s.First().Content,
StartTime = s.First().StartTime,
EndTime = s.First().EndTime,
Theme = s.First().Theme,
Know = s.Select(x => new TaskKnowInfo()
{
Id = x.Id,
KnowPoint = x.KnowPoint,
KnowPointId = x.KnowPointId
}).ToArray()
}).ToArray()
});
}

View File

@ -13,9 +13,10 @@ namespace Learn.VideoAnalysis.Expand
public static class CoravelExpand
{
public static int MyProperty { get; set; }
public static void Init(IServiceCollection service)
public static void AddCoravel(this IServiceCollection service)
{
Console.WriteLine($"{DateTime.Now}=>初始化 Coravel");
service.AddScheduler();
service.AddTransient<NodeSubscriptionJob>();
}
@ -24,7 +25,9 @@ namespace Learn.VideoAnalysis.Expand
provider.UseScheduler(scheduler =>
{
//每5分钟执行一次 未处理视频扫描
scheduler.Schedule<NodeSubscriptionJob>().EveryFiveMinutes();
//scheduler.Schedule<NodeSubscriptionJob>().EveryFiveMinutes();
//每天两点
scheduler.Schedule<TaskFileClearJob>().DailyAtHour(2);
});
}
}

View File

@ -5,6 +5,7 @@ using SqlSugar;
using SqlSugar.IOC;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
@ -17,8 +18,13 @@ namespace Learn.VideoAnalysis.Expand
public static class SqlSugarExpand
{
public static bool ShowSQL = false;
public static void InitSqlSugar(this IServiceCollection services)
public static void AddSqlSugarExpand(this IServiceCollection services)
{
Console.WriteLine($"{DateTime.Now}=>初始化 YitId雪花ID");
var options = new IdGeneratorOptions(ushort.Parse(AppCommon.Config.ID));
YitIdHelper.SetIdGenerator(options);
#region SqlSugar注入
var dbList = new List<IocConfig>() {
new IocConfig()

View File

@ -31,6 +31,7 @@
<ItemGroup>
<ProjectReference Include="..\VideoAnalysisCore\VideoAnalysisCore.csproj" />
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.6.1" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.5" />

View File

@ -53,14 +53,15 @@ namespace Learn.VideoAnalysis
c.OrderActionsBy(o => o.RelativePath); // 对action的名称进行排序如果有多个就可以看见效果了。
});
//°ó¶¨ appsetting ÅäÖÃ
//°ó¶¨ appsetting ÅäÖÃ
builder.Configuration.GetSection("AppConfig").Bind(AppCommon.Config);
//初始化 插件
builder.Services.InitSqlSugar();
RedisExpand.Init();
Speaker.Init();
CoravelExpand.Init(builder.Services);
builder.Services.AddSqlSugarExpand();
builder.Services.AddAlibabaCloudVod();
builder.Services.AddRedisExpand();
builder.Services.AddSpeakerAI();
builder.Services.AddCoravel();
//SenseVoice.Init();
//异常过滤器
builder.Services.AddControllersWithViews(options =>

View File

@ -7,7 +7,7 @@
},
"AllowedHosts": "*",
"AppConfig": {
"ID": "APP0001", //
"ID": 1, //
"Admin": {
"Account": "admin",
"Password": "q1w2e3!@#"
@ -43,7 +43,12 @@
"DB": {
"ConnectionString": "AllowLoadLocalInfile=true;Server=192.168.2.9;User ID=root;Password=qwe123!@#;Port=3306;Database=learn.videoanalysis;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql",
"UpdateTable": true
"UpdateTable": false
},
"AlibabaCloudVod": {
"AccessKeyId": "LTAI5tDC6p9h747B7FHbgwkH",
"AccessKeySecret": "vRKgmbp1LB05LaGOjh3ZrZxbHSLYLF",
"EndPoint": "vod.cn-shanghai.aliyuncs.com" //
},
"OtherDBArr": [
{

View File

@ -11,6 +11,7 @@ using VideoAnalysisCore.AICore.GPT.ChatGPT;
using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Model.;
using VideoAnalysisCore.Model.Enum;
using Mapster;
namespace VideoAnalysisCore.AICore.GPT.DeepSeek
{
@ -51,18 +52,30 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
.Where(s => s.Id == taskId)
.FirstAsync();
var subject = "数学";
var Course_Id = 27;
switch (taskInfo.Type)//处理不同任务类型的知识点树
{
case TaskTypeEnum._中职视频分段:
Course_Id = 51;
break;
case TaskTypeEnum._视频分段:
default:
Course_Id = 27;
break;
}
var xkwKnows = await knowledgeInfoDB.AsQueryable()
.Where(s => s.Course_Id == 27
.Where(s => s.Course_Id == Course_Id
&& (s.Depth == 3
|| s.Depth == 2 ))
|| s.Depth == 2))
.Select(s => s.Name).ToArrayAsync();
string title = taskInfo.MediaName;
var fileNameResFormat = "{授课章节: string|null}";
var fileNamePostMessages = title +
" 这是一堂课的标题,请你基于标题帮我分析出这堂课所讲授的内容与最恰当的授课章节(关联最贴切的章节,保留一个章节!)." +
$"章节范围限定在[{string.Join(',', xkwKnows)}]范围." +
$"章节范围限定在[{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 speakerArr = JsonSerializer.Deserialize<OfflineSpeakerRes[]>(taskInfo.Speaker);
var captionsArr = JsonSerializer.Deserialize<SenseVoiceRes[]>(taskInfo.Captions);
@ -70,31 +83,42 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
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.Course_Id==27 && s.Name == fileNameInfoRes.);
var know = await knowledgeInfoDB.GetFirstAsync(s => s.Course_Id == Course_Id && 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));
throw new Exception("未能找到对应知识点=>" + fileNameInfoRes.);
//提升到父级
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);
var knows = string.Join(',', knowledgeInfos.Select(s => s.Id + "|" + s.Name));
var knowDic = knowledgeInfos
.OrderBy(s=>s.Id)
.GroupBy(s=>s.Name)
.OrderBy(s => s.Id)
.GroupBy(s => s.Name)
.ToDictionary(s => s.First().Name, s => s.First().Id);
VideoKnowRes[] questionRes;
while (true)
{
// var postMessages =
//$"你的任务是分析视频字幕内容并提取出中国高考考试试题方法点,然后分析出<知识块>,来帮助学生快速了解视频字幕的内容" +
//$"通过阅读并理解字幕内容.然后识别出{subject}学科中属于{fileNameInfoRes.授课章节}章节相关的方法点以及对应的时间段。" +
//$"关联合并知识内容相似的知识点来合并为<知识块>。" +
//$"分配空余未使用的时间段到内容相近的<知识块>时间区间来获取更加详细的上下文,但是请避免<知识块>之间时间重合。" +
//$"从提取出的<知识块>内容称来匹配我提供的知识点,来作为片段的知识点(请确保匹配的知识点是用户提供的,否则片段知识点值为空字符串)。" +
//$"提供的方法点名称({knows})。 格式 (方法点Id|方法点名称) 如果一个<知识块>出现多个知识点那么知识点Id与知识点名称都用逗号','分割。" +
//$"这是输入的视频字幕并且是包含时间戳的视频字幕的固定格式文本。" +
////$"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" +
//$"字幕格式(开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" +
//$"最后请检查某些<知识块>之间的过渡是否自然,如果<知识块>时长超过500秒则考虑拆封为两个更加贴切的<知识块>.或者<知识块>时长小于30秒则考虑合并<知识块>到相邻的<知识块>)。" +
//$"输出内容只返回json格式({resFormat})";
var resFormat = """[{"StartTime":开始秒(number),"Theme":主题(string),"Content":内容总结(string)}]""";
var postMessages =
$"你的任务是分析视频字幕内容并提取出中国高考考试试题方法点,然后分析出<知识块>,来帮助学生快速了解视频字幕的内容" +
$"通过阅读并理解字幕内容.然后识别出{subject}学科中属于{fileNameInfoRes.授课章节}章节相关的方法点以及对应的时间段。" +
$"通过阅读并理解字幕内容.然后识别出{subject}学科中属于{fileNameInfoRes.授课章节}章节相关的时间段。" +
$"关联合并知识内容相似的知识点来合并为<知识块>。" +
$"分配空余未使用的时间段到内容相近的<知识块>时间区间来获取更加详细的上下文,但是请避免<知识块>之间时间重合。" +
$"从提取出的<知识块>内容称来匹配我提供的知识点,来作为片段的知识点(请确保匹配的知识点是用户提供的,否则片段知识点值为空字符串)。" +
$"提供的方法点名称({knows})。 格式 (方法点Id|方法点名称) 如果一个<知识块>出现多个知识点那么知识点Id与知识点名称都用逗号','分割。" +
$"这是输入的视频字幕并且是包含时间戳的视频字幕的固定格式文本。" +
//$"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" +
$"字幕格式(开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" +
$"最后请检查某些<知识块>之间的过渡是否自然,如果<知识块>时长超过500秒则考虑拆封为两个更加贴切的<知识块>.或者<知识块>时长小于30秒则考虑合并<知识块>到相邻的<知识块>)。" +
$"输出格式({resFormat})";
$"输出内容只返回json格式({resFormat})";
Console.WriteLine(DateTime.Now + "=>开始分析视频内容");
@ -103,37 +127,58 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
if (questionRes.Length <= 3)
throw new Exception("视频分段数量过低 =>" + questionRes.Length);
questionRes = questionRes.OrderBy(s => s.StartTime).ToArray();
for (int i = 0; i < questionRes.Length; i++)
{
var item = questionRes[i];
if (i == questionRes.Length - 1)
item.EndTime = maxVideoTime;
else
item.EndTime = (int)(questionRes[i + 1]?.StartTime ?? 0) - 1;
}
var thems = string.Join(',', questionRes.Select(s => s.StartTime + "->" + s.Theme));
var checkResFormat = """{"Score":打分(number),"Evaluation":评价(string),"Data":优化后的分段(array)}""";
var checkResFormat1 = """[{"Start":开始秒(number),"Theme":优化后的分段主题(string)}]""";
var checkMessage = "我为视频的讲解内容做了一些分段,你能帮我检查下这些分段的时间分配是否合理,标题内容符合实际吗?" +
$"请给出你的打分(0-100,70分及格)以及打分原因,并且给出优化后的分段 分段格式(${checkResFormat1})" +
var thems = JsonSerializer.Serialize(questionRes.Adapt<VideoKnowQueryDto[]>());// string.Join(',', questionRes.Select(s => s.StartTime + "->" + s.Theme));
var checkResFormat1 = """[{"StartTime":开始秒(number),"KnowPoint":知识点名称(string),"KnowPointId":知识点Id(string)}]""";
var knowStr = string.Join(',', knowledgeInfos.Select(s => s.Name));
var knowMessages =
$"我针对视频<{title}>分析出了一些视频的知识片段,现在需要你帮我将每个片段分配恰当的知识点(单个片段允许多个知识点用逗号','分割)。" +
$"这是我的分段 {thems}。" +
$"提供的知识点名称({knows})。 格式 (方法点Id|方法点名称) " +
$"最后请确保分配的知识点是用户提供的,否则片段知识点值留空!。" +
$"输出内容只返回json格式({checkResFormat1})";
Console.WriteLine(DateTime.Now + "=>开始分析视频内容知识点");
var konwRes = await ChatAsync<VideoKnowRes[]>(task, knowMessages, null);
var checkResFormat = """{"Score":打分(number),"Evaluation":评价(string)""";//,"Data":优化后的分段(array)}""";
var checkMessage = "我为视频的讲解内容做了一些分段,你能帮我检查下这些分段的时间,主题,知识点分配是否合理符合实际吗?" +
$"请给出你的打分(0-100,70分及格)以及打分原因" +//,并且给出优化后的分段 分段格式(${checkResFormat1})" +
//$"如果不合理的话强帮我修改优化他们(注意优化知识点时只允许使用已存在的知识点不允许杜撰猜测捏造)" +
$"这是我的分段 {thems}." +
$"后续的内容是包含时间戳的视频字幕的固定格式文本。" +
$"字幕格式(说话人:开始秒:结束秒:内容|下一段字幕).以下是包含时间的视频字幕文本。字幕列表 {captions.Captions}。" +
$"输出格式为json({checkResFormat})";
$"最后输出格式为json({checkResFormat})";
Console.WriteLine(DateTime.Now + "=>开始检查视频分段结果");
var checkRes = await ChatAsync <CheckMessageDto>(task, checkMessage, null);
if (checkRes != null && checkRes.Score >= 80)
var checkRes = await ChatAsync<CheckMessageDto>(task, checkMessage, null);
if (checkRes != null && checkRes.Score >= 80 && questionRes.Count() == checkRes.Data.Count())
{
for (int i = 0; i < checkRes.Data.Count(); i++)
{
questionRes[i].Theme = checkRes.Data[i].Theme;
questionRes[i].StartTime = checkRes.Data[i].StartTime;
}
break;
}
else
{
Console.WriteLine(DateTime.Now + $"=>{task} 得分过低 " + checkRes?.Score );
Console.WriteLine( checkRes.Evaluation);
Console.WriteLine(DateTime.Now + $"=>{task} 得分过低/分段长度不匹配 得分{checkRes?.Score} 长度 {questionRes.Count()}/{checkRes.Data.Count()}");
Console.WriteLine(checkRes.Evaluation);
Console.WriteLine();
}
}
for (int i = 0; i < questionRes.Length; i++)
{
var item = questionRes[i];
if (i == questionRes.Length - 1)
item.EndTime = maxVideoTime;
else
item.EndTime = (int)(questionRes[i + 1]?.StartTime ?? 0) - 1;
}
//todo 未包含的知识点片段 如何处理
var insertData = questionRes
.Where(s => !string.IsNullOrEmpty(s.KnowPoint))
@ -169,7 +214,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
RedisExpand.InsertChannel(RedisChannelEnum.EndTask, task);
return gptRes;
}
public async Task<T> ChatAsync<T>(string task, string postMessages, string postMessages1, string model= "deepseek-reasoner")
public async Task<T> ChatAsync<T>(string task, string postMessages, string postMessages1, string model = "deepseek-reasoner")
{
var maxTokens = 4000;
Message[] messageArr = [
@ -180,7 +225,7 @@ 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
@ -200,17 +245,32 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
chatResContent = chatResContent?.Replace("```", "");
chatResContent = chatResContent?.Replace("}{", "},{");
chatResContent = chatResContent?.Replace("}|{", "},{");
chatResContent = chatResContent?.Trim();
chatResContent = chatResContent?.Trim().ExtractJson().FirstOrDefault();
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 questionRes = JsonSerializer.Deserialize<T>(chatResContent);
if (questionRes is null)
throw new Exception("ChatGPT返回无效结果");
return questionRes;
var options = new JsonSerializerOptions
{
// 允许解析不严格符合 JSON 规范的字符串
AllowTrailingCommas = true,
// 处理不匹配的 JSON 字符
ReadCommentHandling = JsonCommentHandling.Skip
};
try
{
var questionRes = JsonSerializer.Deserialize<T>(chatResContent, options);
if (questionRes is null)
throw new Exception("ChatGPT返回无效结果");
return questionRes;
}
catch (Exception ex)
{
throw new Exception("ChatGPT结果解析错误 " + ex.Message + ex.Message);
}
}
}
}

View File

@ -10,33 +10,53 @@ using System.Threading.Tasks;
namespace VideoAnalysisCore.AICore.GPT.Dto
{
public class VideoKnowQueryDto
{
/// <summary>
/// 问题解释
/// </summary>
public virtual float? StartTime { get; set; }
/// <summary>
/// 主题
/// </summary>
public virtual string? Theme { get; set; }
/// <summary>
/// 知识点
/// </summary>
public virtual string? KnowPoint { get; set; }
/// <summary>
/// 内容总结
/// </summary>
public virtual string? Content { get; set; }
}
public class VideoKnowRes
{
/// <summary>
/// 问题解释
/// </summary>
public float? StartTime { get; set; }
public float? EndTime { get; set; }
///// <summary>
///// 持续时间
///// </summary>
public float? KeepTime => (EndTime ?? 0) - StartTime ?? 0;
public virtual float? StartTime { get; set; }
public virtual float? EndTime { get; set; }
/// <summary>
/// 持续时间
/// </summary>
public virtual float? KeepTime => (EndTime ?? 0) - StartTime ?? 0;
/// <summary>
/// 主题
/// </summary>
public string? Theme { get; set; }
public virtual string? Theme { get; set; }
/// <summary>
/// 知识点
/// </summary>
public string? KnowPoint { get; set; }
public virtual string? KnowPoint { get; set; }
/// <summary>
/// 知识点ID
/// </summary>
public string? KnowPointId { get; set; }
public virtual string? KnowPointId { get; set; }
/// <summary>
/// 内容总结
/// </summary>
public string? Content { get; set; }
public virtual string? Content { get; set; }
}
public class FileNameInfo

View File

@ -262,10 +262,11 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
Console.WriteLine(DateTime.Now + "=> SenseVoice 字幕数量"+ res.Count);
var captionsStr = JsonSerializer.Serialize(res);
DbScoped.SugarScope
await DbScoped.SugarScope
.Updateable<VideoTask>()
.SetColumns(it => it.Captions == captionsStr)
.Where(it => it.Id == long.Parse(task));
.Where(it => it.Id == long.Parse(task))
.ExecuteCommandAsync();
await RedisExpand.Redis.HMSetAsync(RedisExpandKey.Task(task), "Captions", res);
//RedisExpand.InsertChannel(Enum.RedisChannelEnum.ParsingSpeaker, task);
RedisExpand.InsertChannel(RedisChannelEnum.ChatModelAnalysis, task);

View File

@ -8,6 +8,7 @@ using SqlSugar.IOC;
using VideoAnalysisCore.Model;
using System.Text.Json;
using VideoAnalysisCore.Model.Enum;
using Microsoft.Extensions.DependencyInjection;
namespace VideoAnalysisCore.AICore.SherpaOnnx
{
@ -16,7 +17,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
/// 说话人模型
/// <para>pyannote </para>
/// </summary>
public class Speaker
public static class Speaker
{
private static OfflineSpeakerDiarization? SD;
/// <summary>
@ -25,9 +26,9 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx
/// <param name="speakerNumber"></param>
/// <param name="threshold"></param>
/// <param name="useGPU"></param>
public static void Init(int speakerNumber = 2, float threshold = 0.6f,bool useGPU = false)
public static void AddSpeakerAI(this IServiceCollection service,int speakerNumber = 2, float threshold = 0.6f,bool useGPU = false)
{
Console.WriteLine("初始化 Speaker");
Console.WriteLine($"{DateTime.Now}=>初始化 Speaker");
var config = new OfflineSpeakerDiarizationConfig();
//Pyannote模型地址
config.Segmentation.Pyannote.Model = Path.Combine(AppCommon.AIModelFile, "sherpa-onnx-pyannote-segmentation-3-0", "model.onnx");

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AlibabaCloud.OpenApiClient.Models;
using AlibabaCloud.SDK.Vod20170321;
using AlibabaCloud.SDK.Vod20170321.Models;
using AlibabaCloud.TeaUtil.Models;
using Microsoft.Extensions.DependencyInjection;
using VideoAnalysisCore.Job;
namespace VideoAnalysisCore.Common
{
public class AlibabaCloudVodConfig
{
/// <summary>
/// id
/// </summary>
public string AccessKeyId { get; set; }
/// <summary>
///密钥
/// </summary>
public string AccessKeySecret { get; set; }
public string Endpoint { get; set; } = "vod.cn-shanghai.aliyuncs.com";
}
/// <summary>
/// 阿里云 视频点播拓展
/// </summary>
public static class AlibabaCloudVodExpand
{
/// <summary>
/// 使用阿里云 vod拓展
/// </summary>
/// <param name="service"></param>
/// <returns></returns>
public static void AddAlibabaCloudVod(this IServiceCollection service)
{
Console.WriteLine($"{DateTime.Now}=>初始化 阿里云VOD");
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
// 建议使用更安全的 STS 方式更多鉴权访问方式请参见https://help.aliyun.com/document_detail/378671.html。
Config config = new()
{
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
AccessKeyId = AppCommon.Config.AlibabaCloudVod.AccessKeyId,
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
AccessKeySecret = AppCommon.Config.AlibabaCloudVod.AccessKeySecret,
Endpoint = AppCommon.Config.AlibabaCloudVod.Endpoint
};
// Endpoint 请参考 https://api.aliyun.com/product/vod
var c = new Client(config);
service.AddSingleton(c);
}
}
}

View File

@ -87,6 +87,50 @@ namespace VideoAnalysisCore.Common
static Dictionary<string, string> FormulaData;
static string FormulaDataKey;
/// <summary>
/// 识别字符串中的json字符串
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static List<string> ExtractJson(this string input)
{
List<string> jsonList = new List<string>();
int index = 0;
while (index < input.Length)
{
if (input[index] == '{' || input[index] == '[')
{
int startIndex = index;
int openCount = 1;
index++;
while (index < input.Length && openCount > 0)
{
if (input[index] == '{' || input[index] == '[')
{
openCount++;
}
else if (input[index] == '}' || input[index] == ']')
{
openCount--;
}
index++;
}
if (openCount == 0)
{
string json = input.Substring(startIndex, index - startIndex);
jsonList.Add(json);
}
}
else
{
index++;
}
}
return jsonList;
}
/// <summary>
/// 处理数学公式
/// </summary>
/// <param name="f"></param>
@ -399,6 +443,10 @@ namespace VideoAnalysisCore.Common
/// ChatGpt
/// </summary>
public ChatGptConfig ChatGpt { get; set; } = new ChatGptConfig();
/// <summary>
/// 阿里云视频点播配置
/// </summary>
public AlibabaCloudVodConfig AlibabaCloudVod { get; set; } = new AlibabaCloudVodConfig();
/// <summary>

View File

@ -1,5 +1,6 @@
using AntDesign;
using Downloader;
using SqlSugar;
using SqlSugar.IOC;
using System;
using System.ComponentModel;
@ -106,9 +107,11 @@ namespace VideoAnalysisCore.Common
{
if (Opt is null)
Init();
var taskId = long.Parse(task);
//获取资源文件 地址
var fileUrl = RedisExpand.Redis.HMGet(RedisExpandKey.Task(task), "MediaUrl")
.FirstOrDefault();
var fileUrl =await DbScoped.SugarScope.Queryable<VideoTask>()
.Where(s => s.Id == taskId)
.Select(s=>s.MediaUrl).FirstAsync();
if (string.IsNullOrEmpty(fileUrl))
throw new Exception($"任务id[{task}] 资源地址无效 {fileUrl}");
@ -125,10 +128,11 @@ namespace VideoAnalysisCore.Common
var outputPath = Path.Combine(localPath, task + fileExtension);
if (!Directory.Exists(localPath)) Directory.CreateDirectory(localPath);
DbScoped.SugarScope
await DbScoped.SugarScope
.Updateable<VideoTask>()
.SetColumns(it => it.LocalMediaPath == outputPath)
.Where(it => it.Id == long.Parse(task));
.Where(it => it.Id == long.Parse(task))
.ExecuteCommandAsync();
IDownload download = DownloadBuilder.New()
.WithUrl(fileUrl)
@ -147,7 +151,7 @@ namespace VideoAnalysisCore.Common
{
if (download.Status == DownloadStatus.Failed && e.Error != null)
{
await RedisExpand.SetTaskErrorMessage(long.Parse(task), e.Error)
await RedisExpand.SetTaskErrorMessage(taskId, e.Error)
.ConfigureAwait(false);//不切回上下文
return;
}

View File

@ -67,14 +67,14 @@ namespace VideoAnalysisCore.Common
/// 任务对象地址
/// </summary>
public static string Task(object taskId) => BaseKey + "Task:" + taskId;
public static string IDTask => BaseKey + AppCommon.Config.ID;
public static string IDTask => BaseKey + "Services:" + AppCommon.Config.ID;
public static string TaskGPT(object taskId) => Task(taskId) + ":GPTCached";
}
/// <summary>
/// redis拓展
/// </summary>
public class RedisExpand
public static class RedisExpand
{
/// <summary>
/// redis 连接
@ -90,9 +90,9 @@ namespace VideoAnalysisCore.Common
/// 初始化 redis
/// <para>需要在初始化配置文件时候调用</para>
/// </summary>
public static void Init()
public static void AddRedisExpand(this IServiceCollection service)
{
Console.WriteLine("初始化 redis");
Console.WriteLine($"{DateTime.Now}=>初始化 Redis");
Redis.Serialize = obj => JsonSerializer.Serialize(obj);
Redis.Deserialize = (json, type) => JsonSerializer.Deserialize(json, type);
Task.Run(() =>
@ -117,10 +117,16 @@ namespace VideoAnalysisCore.Common
/// <summary>
/// 加入到消费队列
/// </summary>
/// <param name="taskId"></param>
public static void JoinQueue(params long[] taskId)
{
Redis.LPush(RedisExpandKey.ChannelKey, taskId);
/// <param name="taskIds"></param>
public static void JoinQueue(params long[] taskIds)
{ //事务
using (var tran = Redis.Multi())
{
foreach (var item in taskIds)
tran.LPush(RedisExpandKey.ChannelKey, item);
object[] ret = tran.Exec();
Console.WriteLine(ret[0] + ", " + ret[2]);
}
}
/// <summary>
/// 获取任务进度
@ -179,7 +185,7 @@ namespace VideoAnalysisCore.Common
if (taskData.Captions == "[]")
taskData.Captions = (await Redis.HMGetAsync(RedisExpandKey.Task(task), "Captions")).First();
if (taskData.Speaker == "[]")
taskData.Speaker = (await Redis.HMGetAsync(RedisExpandKey.Task(task), "Speaker")).First();
taskData.Speaker = (await Redis.HMGetAsync(RedisExpandKey.Task(task), "Speaker"))?.FirstOrDefault()??"[]";

View File

@ -1,14 +1,18 @@
using Coravel.Invocable;
using AlibabaCloud.SDK.Vod20170321;
using Coravel.Invocable;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UserCenter.Model.Enum;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
using VideoAnalysisCore.Model.;
using Yitter.IdGenerator;
namespace VideoAnalysisCore.Job
{
@ -17,26 +21,32 @@ namespace VideoAnalysisCore.Job
/// </summary>
public class NodeSubscriptionJob : IInvocable
{
/// <summary>
/// 每个扫描文件包每次取出{20}个
/// </summary>
private readonly int TopLength = 20;
private readonly Repository<NodeSubscription> nodesubscriptionDB;
private readonly Repository<Attachments> attachmentsDB;
private readonly Repository<VideoTask> videotaskDB;
public NodeSubscriptionJob(Repository<Attachments> videoTaskDB, Repository<NodeSubscription> nodesubscriptionDB, Repository<VideoTask> videotaskDB)
private readonly Client vodClient;
public NodeSubscriptionJob(Repository<Attachments> videoTaskDB, Repository<NodeSubscription> nodesubscriptionDB, Repository<VideoTask> videotaskDB, Client vodClient)
{
this.attachmentsDB = videoTaskDB;
this.nodesubscriptionDB = nodesubscriptionDB;
this.videotaskDB = videotaskDB;
this.vodClient = vodClient;
}
public async Task Invoke()
{
Console.WriteLine($"{DateTime.Now} Invoke=>{this.GetType().FullName}");
var tasks = await nodesubscriptionDB.GetListAsync(s => s.Enable);
var tasks = await nodesubscriptionDB.GetListAsync(s => s.Enable && s.Subject ==SubjectEnum.);
foreach (var item in tasks)
{
var fileNodeId = item.NodeId;
var data = attachmentsDB.Context.Ado
.SqlQuery<Attachments>($"""
SELECT
SELECT top {TopLength}
*
FROM
Attachments
@ -62,22 +72,43 @@ namespace VideoAnalysisCore.Job
AND (
NAME NOT LIKE '%PPT%'
OR NAME NOT LIKE '%ppt%'
)
) and id>{item.LastId}
""");
if (data is null || data.Count == 0)
continue;
Console.WriteLine($"{DateTime.Now} 视频订阅=>Node:{item.NodeId} 数量{data.Count}");
var videos = data.Select(s => new VideoTask()
var videos = new List<VideoTask>(data.Count);
foreach (var s in data)
{
ComeFrom = "127.0.0.1",
ApiToken = "",
Type = item.TaskType,
Subject = item.Subject,
Tag = string.Empty,
TagId = s.VideoCode,
MediaUrl = s.Url,
MediaName = s.Name
}).ToArray();
//入库
var videoInfo = await vodClient.GetPlayInfoAsync(new AlibabaCloud.SDK.Vod20170321.Models.GetPlayInfoRequest()
{
VideoId = s.VideoCode, Formats="mp4",OutputType = "cdn", AuthTimeout = 3600 * 24 * 3,
});
if (videoInfo is null || videoInfo.StatusCode != 200 && !videoInfo.Body.PlayInfoList.PlayInfo.Any())
{
Console.WriteLine($"{DateTime.Now} 视频订阅=>获取阿里云视频信息失败 VideoCode {s.VideoCode} StatusCode {videoInfo?.StatusCode}");
continue;
}
videos.Add(new VideoTask()
{
Id = YitIdHelper.NextId(),
ComeFrom = "127.0.0.1",
ApiToken = "",
Type = item.TaskType,
Subject = item.Subject,
Tag = item.NodeId.ToString(),
TagId = s.VideoCode,
MediaUrl = videoInfo.Body.PlayInfoList.PlayInfo.First().PlayURL,
MediaName = s.Name
});
}
var maxId = data.Max(s => s.Id);
//入库 更新最后扫描记录
await nodesubscriptionDB.AsUpdateable()
.SetColumns(it => it.LastId == maxId)
.Where(it => it.Id == item.Id)
.ExecuteCommandAsync();
await videotaskDB.InsertRangeAsync(videos);
var ids = videos.Select(s => s.Id).ToArray();
RedisExpand.JoinQueue(ids);

View File

@ -0,0 +1,79 @@
using AlibabaCloud.SDK.Vod20170321;
using Coravel.Invocable;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UserCenter.Model.Enum;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
using VideoAnalysisCore.Model.Interface;
using VideoAnalysisCore.Model.;
using Yitter.IdGenerator;
namespace VideoAnalysisCore.Job
{
/// <summary>
/// 下载视频的任务文件清理任务
/// </summary>
public class TaskFileClearJob : IInvocable
{
private readonly Repository<VideoTask> videotaskDB;
public TaskFileClearJob(Repository<VideoTask> videotaskDB)
{
this.videotaskDB = videotaskDB;
}
public void DeleteOldCompletedTaskCaches()
{
try
{
var startTime = -2;
var timeSpan = startTime - 3;
// 计算 2 天前的时间
DateTime twoDaysAgo = DateTime.Now.AddDays(startTime);
DateTime endDaysAgo = DateTime.Now.AddDays(timeSpan);
// 查询 2 天前任务执行完成的记录
var completedTasks = videotaskDB.AsQueryable()
.Where(t =>
t.LastEnum == Model.Enum.RedisChannelEnum.EndTask
&& t.CreateTime < twoDaysAgo
&& t.CreateTime > endDaysAgo)
.ToList();
// 遍历查询结果,删除缓存文件
foreach (var task in completedTasks)
{
var path = task.Id.ToString().LocalPath();
if (!string.IsNullOrEmpty(path) && Directory.Exists(path))
{
try
{
Directory.Delete(path, true);
Console.WriteLine($"已删除缓存文件: {task.LocalMediaPath}");
}
catch (Exception ex)
{
Console.WriteLine($"删除缓存文件 {task.Id} 时出错: {ex.Message}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"清理缓存时出错: {ex.Message}");
}
}
public async Task Invoke()
{
Console.WriteLine($"{DateTime.Now} Invoke=>{this.GetType().FullName}");
DeleteOldCompletedTaskCaches();
}
}
}

View File

@ -57,6 +57,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.6.1" />
<PackageReference Include="Coravel" Version="6.0.2" />
<PackageReference Include="FreeRedis" Version="1.3.2" />
<PackageReference Include="Downloader" Version="3.2.1" />