From 79d024f9f63ba54787694e880a1d1a64e060aba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com> Date: Thu, 14 Nov 2024 15:11:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=98=BE=E7=A4=BA=E6=B5=81=E7=A8=8B=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E4=B8=8B=E8=BD=BD=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Pages/VideoTaskPage.razor | 68 +++++++- .../Components/Pages/VideoTaskPage.razor.cs | 85 +++++++--- .../Components/Pages/VideoTaskPage.razor.css | 5 + VideoAnalysis/Controllers/ApiController.cs | 7 +- VideoAnalysis/Program.cs | 4 +- VideoAnalysis/appsettings.json | 1 + .../AICore/ChatGPT/KIMI/KIMI_GPT.cs | 55 ++++--- .../AICore/ChatGPT/KIMI/MoonshotClient.cs | 1 - .../AICore/SherpaOnnx/SenseVoice.cs | 6 +- .../AICore/SherpaOnnx/Speaker.cs | 1 + VideoAnalysisCore/Common/AppCommon.cs | 4 + VideoAnalysisCore/Common/DownloadFile.cs | 132 +++++++++++----- VideoAnalysisCore/Common/RedisExpand.cs | 146 +++++++++++++----- VideoAnalysisCore/Enum/QuestionTypeEnum.cs | 12 +- VideoAnalysisCore/Enum/RedisChannelEnum.cs | 8 +- VideoAnalysisCore/Model/Dto/VideoTaskDto.cs | 6 + VideoAnalysisCore/Model/VideoTask.cs | 1 + VideoAnalysisCore/VideoAnalysisCore.csproj | 1 + 18 files changed, 402 insertions(+), 141 deletions(-) diff --git a/VideoAnalysis/Components/Pages/VideoTaskPage.razor b/VideoAnalysis/Components/Pages/VideoTaskPage.razor index d298a67..b93a5b5 100644 --- a/VideoAnalysis/Components/Pages/VideoTaskPage.razor +++ b/VideoAnalysis/Components/Pages/VideoTaskPage.razor @@ -5,18 +5,72 @@ @using SqlSugar @using VideoAnalysisCore.Model @using VideoAnalysisCore.Model.Dto +@using VideoAnalysisCore.Enum - +
- - - - + + + + + + + + + + + + + @rowData.Data.Progress% + + + + + + + + + + + + + + + + + + + + + + + + @if (!string.IsNullOrEmpty( @rowData.Data.ErrorMessage)) + { + + @rowData.Data.ErrorMessage + + } + + + +
diff --git a/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs index 41c1867..676c712 100644 --- a/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs +++ b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs @@ -1,5 +1,6 @@ using AntDesign; using AntDesign.TableModels; +using FFmpeg.NET.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.DataProtection.KeyManagement; using SqlSugar; @@ -19,6 +20,7 @@ namespace Learn.VideoAnalysis.Components.Pages [Inject] private Repository taskDB { get; set; } = default!; + [Inject] private INotificationService _notice { get; set; } = default!; IEnumerable _selectedRows = []; ITable _table; @@ -26,6 +28,8 @@ namespace Learn.VideoAnalysis.Components.Pages RefAsync _total = 0; bool tableLoading = false; + private VideoTaskDto selectData; + private bool rowRestartLoading=false; /// /// 重试 @@ -34,16 +38,18 @@ namespace Learn.VideoAnalysis.Components.Pages async void ReStart(VideoTaskDto query) { var lastEnum = (await RedisExpand.Redis.HMGetAsync(RedisExpandKey.Task(query.Id), "LastEnum")).FirstOrDefault() ; - await taskDB.UpdateAsync(s => new VideoTask() - { ErrorMessage = string.Empty },s => s.Id == query.Id); + await RedisExpand.SetTaskErrorMessage(query.Id, string.Empty); RedisExpand.InsertChannel(lastEnum, query.Id); } + private QueryModel lastQuery = null; /// /// 分页 查询 筛选 时 /// /// + /// async void OnChange(QueryModel query) { + lastQuery= query; tableLoading = true; List where = default!; if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0)) @@ -55,39 +61,72 @@ namespace Learn.VideoAnalysis.Components.Pages .Select() .ToPageListAsync(query.PageIndex - 1, query.PageSize, _total); - foreach (var item in _dataSource) - { - if (!string.IsNullOrEmpty(item.ErrorMessage) || item.LastEnum == RedisChannelEnum.EndTask) - continue; - item.Progress = RedisExpand.Redis.HMGet(RedisExpandKey.Task(item.Id), "Progress").FirstOrDefault(); - } - - tableLoading = false; + tableLoading = false; StateHasChanged(); } - private static System.Timers.Timer _timer; - public void StartTimer(Object source, System.Timers.ElapsedEventArgs e) + public void RowRestart(RowData rowData) { - if (_dataSource is null) + rowRestartLoading = true; + var item = rowData.Data; + if (item is null) return; - foreach (var item in _dataSource) - { - if (!string.IsNullOrEmpty(item.ErrorMessage) || item.LastEnum == RedisChannelEnum.EndTask) - continue; - item.Progress = RedisExpand.Redis.HMGet(RedisExpandKey.Task(item.Id), "Progress").FirstOrDefault(); - } - + var data = RedisExpand.Redis.HMGet(RedisExpandKey.Task(item.Id), + "Progress", "LastEnum", "StartTime", "ErrorMessage"); + item.Progress = double.Parse(data[0]); + item.LastEnum = data[1].ToEnum() ?? default; + item.StartTimeDic = System.Text.Json.JsonSerializer.Deserialize>(data[2]) ?? null; + item.ErrorMessage = data[3]; + rowRestartLoading = false; StateHasChanged(); + } + /// + /// + /// + /// + private string RowST(RowData rowData, RedisChannelEnum e) + { + var dic = rowData.Data.StartTimeDic; + if (dic is null || !dic.ContainsKey(e)) + return "--"; + return dic[e].ToString(); + } + private string RowSTStatus(RowData rowData) + { + var dic = rowData.Data.StartTimeDic; + if (dic is null) + return "wait"; + if(!string.IsNullOrEmpty(rowData.Data.ErrorMessage)) + return "error"; + if(dic.ContainsKey(RedisChannelEnum.EndTask)) + return "finish"; + return "wait"; + } + private int RowSTIndex(RowData rowData) + { + var dic = rowData.Data.StartTimeDic; + if (dic is null ) + return 0; + return (int)dic.LastOrDefault().Key; + } + private void OnExpand(RowData rowData) + { + RowRestart(rowData); + } + /// + /// 在渲染页面之后 + /// + /// + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + } /// /// 初始化 /// protected override void OnInitialized() { - _timer = new System.Timers.Timer(2000); - _timer.Elapsed += StartTimer; - _timer.Enabled = true; } private async Task Comfirm(string message) diff --git a/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css index aca1ed1..0003041 100644 --- a/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css +++ b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css @@ -1,3 +1,8 @@ input[aria-hidden="true"] { display: none !important; } + +.task_status_tag { + display:flex; + +} diff --git a/VideoAnalysis/Controllers/ApiController.cs b/VideoAnalysis/Controllers/ApiController.cs index 6db61d3..5585689 100644 --- a/VideoAnalysis/Controllers/ApiController.cs +++ b/VideoAnalysis/Controllers/ApiController.cs @@ -8,6 +8,8 @@ using VideoAnalysisCore.Model; using VideoAnalysisCore.AICore.FFMPGE; using VideoAnalysisCore.Model.Dto; using VideoAnalysisCore.AICore.ChatGPT.Dto; +using AntDesign; +using System.Threading.Tasks; namespace Learn.VideoAnalysis.Controllers { @@ -47,6 +49,8 @@ namespace Learn.VideoAnalysis.Controllers .WhereIF(taskId!=0, s => s.Id == taskId) .WhereIF(string.IsNullOrEmpty(tagId), s => s.TagId == tagId) .FirstAsync(); + if(task is null) + return BadRequest(); if (task.LastEnum != RedisChannelEnum.EndTask) return BadRequest(new { Enum = task.LastEnum ,Task = task.ChatAnalysis}); return Ok(new { Enum = task.LastEnum, Task = task.ChatAnalysis }); @@ -90,8 +94,7 @@ namespace Learn.VideoAnalysis.Controllers .GetProperties(BindingFlags.Public | BindingFlags.Instance) .ToDictionary(s => s.Name, s => s.GetValue(task)); RedisExpand.Redis.HMSet(RedisExpandKey.Task(task.Id), hashEntries); - - RedisExpand.InsertChannel(RedisChannelEnum.DownloadFile,task.Id); + RedisExpand.Redis.Set(RedisExpandKey.ChannelKey, task.Id); return Ok(task.Id); } } diff --git a/VideoAnalysis/Program.cs b/VideoAnalysis/Program.cs index cda2602..c3ca055 100644 --- a/VideoAnalysis/Program.cs +++ b/VideoAnalysis/Program.cs @@ -51,8 +51,8 @@ namespace Learn.VideoAnalysis builder.Configuration.GetSection("AppConfig").Bind(AppCommon.Config); //ʼ - //Speaker.Init(); - //RedisExpand.Init(); + RedisExpand.Init(); + Speaker.Init(); //SenseVoice.Init(); diff --git a/VideoAnalysis/appsettings.json b/VideoAnalysis/appsettings.json index 5bc6be3..c38de8d 100644 --- a/VideoAnalysis/appsettings.json +++ b/VideoAnalysis/appsettings.json @@ -7,6 +7,7 @@ }, "AllowedHosts": "*", "AppConfig": { + "ID": "APP0001",//程序唯一值 "Redis": { "ConnectionString": "127.0.0.1:6379,password=Woshiren123,defaultDatabase=10" }, diff --git a/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs b/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs index a3ce0b3..d3dcc87 100644 --- a/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs +++ b/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs @@ -15,6 +15,8 @@ using System.Reflection; using FreeRedis; using VideoAnalysisCore.Model.Dto; using Newtonsoft.Json; +using AntDesign; +using SqlSugar.IOC; namespace VideoAnalysisCore.AICore.ChatGPT.KIMI { @@ -55,7 +57,7 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI criteriaBuilder.Append(item.Id); criteriaBuilder.Append(":"); criteriaBuilder.Append(item.NamePrompt); - criteriaBuilder.Append("请基于解释打分(0-10分 6分为及格线) 结果类型 int |"); + criteriaBuilder.Append("请基于解释打分(0-10分 6分为及格) 结果类型 int |"); } //拼接枚举提问 foreach (var value in System.Enum.GetValues(typeof(QuestionTypeEnum))) @@ -71,7 +73,7 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI criteriaBuilder.Append("|"); } - var resFormat = "[{问题编号:int,结果:array|bool,问题解释:string}]"; + var resFormat = "[{问题编号:int,结果:array|bool|object,问题解释:string}]"; var postMessages = $"以下是一段音频的字幕,分析这段字幕(格式 说话人:开始秒:结束秒:内容|下一段字幕)." + $"来简明的回答提出的问题 问题列表 {criteriaBuilder} " + @@ -95,6 +97,7 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI } }; var chatResp = await moonshotClient.Chat(chatRep); + RedisExpand.SetTaskGPTCached(task, chatResp); if (chatResp is null || chatResp.error != null) throw new Exception($"KIMI模型返回异常 Chat 请求参数: {System.Text.Json.JsonSerializer.Serialize(chatRep)} " + $" chatResp {System.Text.Json.JsonSerializer.Serialize(chatResp)}"); @@ -121,7 +124,7 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI { Id = criteriaDic[s.问题编号].Id, ImprovedMethods = criteriaDic[s.问题编号].ImprovedMethods, - Analyze = s.问题解释??string.Empty, + Analyze = s.问题解释 ?? string.Empty, Score = s.结果.ToObject(), Prompt = criteriaDic[s.问题编号].Flaw, }).ToArray(), @@ -137,7 +140,7 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI }; //高频词汇 - gptRes.Hotwords = arr2[(int)QuestionTypeEnum.高频词].结果.ToObject()??["暂无数据"]; + gptRes.Hotwords = arr2[(int)QuestionTypeEnum.高频词].结果.ToObject() ?? ["暂无数据"]; //时间段概览 gptRes.TimeOverview = arr2[(int)QuestionTypeEnum.时间段概览] @@ -148,27 +151,33 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI //分析上课时间段情况 分析 独立学习 小组合作 随堂练习等情况 var extraTimeBase = arr2[(int)QuestionTypeEnum.额外课堂情况] - .结果.ToObject>()?.Select(s => - new TimeBase(s.Value,s.Key.ToString())); + .结果.ToObject(); + if (extraTimeBase is not null) + foreach (var item in extraTimeBase) + { + if (item is null) + continue; + var arr = gptRes.TimeBase? + .Where(s => s.Start >= item.Start && s.End <= item.End); + if (arr is null) + continue; + foreach (var s in arr) + s.TimeBaseType = item.Content.ToEnum(); + } + var totalTokens = chatResp?.usage.total_tokens ?? 0; + if (totalTokens > 1) + { + var tid = long.Parse(task); + await videoTaskDB.AsUpdateable() + .SetColumns(it => it.TotalTokens == totalTokens)//SetColumns是可以叠加的 写2个就2个字段赋值 + .Where(it => it.Id == tid) + .ExecuteCommandAsync(); + } + await RedisExpand.Redis + .HMSetAsync(RedisExpandKey.Task(task), "ChatAnalysis", gptRes); + RedisExpand.InsertChannel(Enum.RedisChannelEnum.EndTask, task); - - var tId = long.Parse(task); - var taskData = await videoTaskDB.GetFirstAsync(s => s.Id == tId); - taskData.ChatAnalysis = gptRes; - taskData.ChatAnalysisScore = gptRes.Assessment.Merit?.Sum(s=>s.Score)??0; - taskData.ErrorMessage = string.Empty; - taskData.TotalTokens = chatResp.usage.total_tokens; - taskData.LastEnum = RedisChannelEnum.EndTask; - await videoTaskDB.AsUpdateable(taskData) - .UpdateColumns(it => new - { - it.ChatAnalysis, - it.ChatAnalysisScore, - it.ErrorMessage, - it.TotalTokens, - it.LastEnum, - }).ExecuteCommandAsync(); return gptRes; } } diff --git a/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs b/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs index dfbd14f..6cceda1 100644 --- a/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs +++ b/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs @@ -57,7 +57,6 @@ namespace VideoAnalysisCore.AICore.ChatGPT.KIMI { var requestBody = Newtonsoft.Json.JsonConvert.SerializeObject(chatReq); var chatResp = await PostJsonStreamAsync("/v1/chat/completions", requestBody); - var resStr = chatResp.Content.ReadAsStringAsync(); return await chatResp.Content.ReadFromJsonAsync(); } diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs b/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs index 02c20cd..49166de 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs @@ -78,6 +78,8 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx /// public static async Task RunTask(string task) { + if (OR is null) + Init(); var filePath = Path.Combine(task.LocalPath(), task + ".wav"); if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) throw new Exception("task 音频路径未找到"); @@ -87,7 +89,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx int windowSize = VADModelConfig.SileroVad.WindowSize; int sampleRate = VADModelConfig.SampleRate; int numIter = numSamples / windowSize; - + var totalSecond = numSamples / (double)sampleRate; var res = new List(500); for (int i = 0; i != numIter; ++i) { @@ -115,7 +117,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx Start= startTime, End = startTime + duration }); - var progress = (double)(startTime + duration) / numSamples * 100; + var progress = (double)(startTime + duration) / (totalSecond) * 100; RedisExpand.SetTaskProgress(task, progress); } diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs b/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs index d60e450..535b10f 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs @@ -31,6 +31,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx config.Embedding.Model = Path.Combine(AppCommon.AIModelFile, "wespeaker", "wespeaker_zh_cnceleb_resnet34_LM.onnx"); //说话人数量 config.Clustering.NumClusters = speakerNumber; + config.Embedding.NumThreads = 4; //需要使用GPU if (!useGPU) config.Embedding.Provider = "cuda"; diff --git a/VideoAnalysisCore/Common/AppCommon.cs b/VideoAnalysisCore/Common/AppCommon.cs index bfb0b47..f4406bc 100644 --- a/VideoAnalysisCore/Common/AppCommon.cs +++ b/VideoAnalysisCore/Common/AppCommon.cs @@ -303,6 +303,10 @@ namespace VideoAnalysisCore.Common /// public class AppConfig { + /// + /// 程序ID + /// + public string ID { get; set; } = string.Empty; /// /// redis /// diff --git a/VideoAnalysisCore/Common/DownloadFile.cs b/VideoAnalysisCore/Common/DownloadFile.cs index 221b567..f40ed51 100644 --- a/VideoAnalysisCore/Common/DownloadFile.cs +++ b/VideoAnalysisCore/Common/DownloadFile.cs @@ -1,5 +1,10 @@ -using System; +using AntDesign; +using Downloader; +using System; +using System.ComponentModel; +using System.Diagnostics; using System.IO; +using System.Net; using System.Threading.Tasks; namespace VideoAnalysisCore.Common @@ -9,6 +14,67 @@ namespace VideoAnalysisCore.Common /// public class DownloadFile { + static DownloadConfiguration Opt { get; set; } = default!; + /// + /// 初始化下载器 + /// + /// 下载速度mb/s 默认8 + static void Init(int DownloadSpeed = 8) + { + Opt = new DownloadConfiguration() + { + // 通常,主机支持的最大值为8000字节,默认值是8000 + BufferBlockSize = 10240, + // 要下载的文件部分数量,默认值是1 + ChunkCount = 8, + // 下载速度限制为2MB/秒,默认值为零(即无限制) + MaximumBytesPerSecond = 1024 * 1024 * DownloadSpeed, + // 故障转移时的最大重试次数 + MaxTryAgainOnFailover = 5, + // 每50MB后释放内存缓冲区 + MaximumMemoryBufferBytes = 1024 * 1024 * 50, + // 是否并行下载文件的各个部分。默认值为false + ParallelDownload = true, + // 并行下载的数量。默认值与分块数量相同 + ParallelCount = 4, + // 每个流块读取器的超时时间(毫秒),默认值是1000 + Timeout = 1000, + // 如果只想下载大型文件的特定字节范围,则设置为true + RangeDownload = false, + // 大型文件下载范围的起始偏移量 + RangeLow = 0, + // 大型文件下载范围的结束偏移量 + RangeHigh = 0, + // 下载失败完成时清除包块数据,默认值为false + ClearPackageOnCompletionWithFailure = true, + // 将文件分多个部分下载时的最小分块大小,默认值是512 + MinimumSizeOfChunking = 1024, + // 在开始下载之前,按照文件大小预留文件的存储空间,默认值为false + ReserveStorageSpaceBeforeStartingDownload = true, + // 在下载进度改变事件中,通过ReceivedBytes获取按需下载的数据 + EnableLiveStreaming = false, + // 配置和自定义请求头 + RequestConfiguration = + { + Accept = "*/*", + //CookieContainer = cookies, + Headers = new WebHeaderCollection(), // { 你的自定义头部信息 } + KeepAlive = true, // 默认值为false + ProtocolVersion = HttpVersion.Version11, // 默认值是HTTP 1.1 + UseDefaultCredentials = false, + // 你的自定义用户代理或你的应用名称/应用版本。 + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + //Proxy = new WebProxy()//使用代理 + //{ + // Address = new Uri("http://Learn.VideoAnalysis"), + // UseDefaultCredentials = false, + // Credentials = System.Net.CredentialCache.DefaultNetworkCredentials, + // BypassProxyOnLocal = true + //} + } + }; + } + // 根据 Content-Type 映射文件后缀 static string GetExtensionFromContentType(HttpResponseMessage res) { @@ -35,62 +101,56 @@ namespace VideoAnalysisCore.Common /// public static async Task RunTask(string task) { + if (Opt is null) + Init(); //获取资源文件 地址 var fileUrl = RedisExpand.Redis.HMGet(RedisExpandKey.Task(task), "MediaUrl") .FirstOrDefault(); if (string.IsNullOrEmpty(fileUrl)) throw new Exception($"任务id[{task}] 资源地址无效 {fileUrl}"); - using HttpClient client = new HttpClient(); - // 发送 GET 请求以下载文件 - var response = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead); - // 确保响应是成功的 - response.EnsureSuccessStatusCode(); - // 尝试从 URL 中获取文件后缀 string fileExtension = Path.GetExtension(new Uri(fileUrl).AbsolutePath); //否则 获取 从Content-Type 获取文件后缀 if (string.IsNullOrEmpty(fileExtension)) - fileExtension = GetExtensionFromContentType(response); + throw new Exception($"未能从资源路径中获取文件后缀"); //创建下载文件缓存路径 if (!Directory.Exists(AppCommon.TaskCachedFile)) Directory.CreateDirectory(AppCommon.TaskCachedFile); - - // 获取文件大小 - var totalBytes = response.Content.Headers.ContentLength; - if (!totalBytes.HasValue) - throw new Exception(task+" 未能获取到下载文件的 ContentLength "); + var localPath = task.LocalPath(); - var outputPath = Path.Combine(localPath, task + fileExtension); + var outputPath = Path.Combine(localPath, task + fileExtension); if (!Directory.Exists(localPath)) Directory.CreateDirectory(localPath); - - //同步到redis RedisExpand.Redis.HSet(RedisExpandKey.Task(task), "LocalMediaPath", outputPath); - // 打开本地文件流以写入文件 - using var fs = new FileStream(outputPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); - // 读取响应内容流 - using var contentStream = await response.Content.ReadAsStreamAsync(); - var buffer = new byte[512 * 1024]; // 512KB 缓冲区 - long totalBytesRead = 0; - var count = 0; - int bytesRead; - while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) + IDownload download = DownloadBuilder.New() + .WithUrl(fileUrl) + .WithDirectory(localPath) + .WithFileName(task + fileExtension) + .WithConfiguration(Opt) + .Build(); + download.DownloadProgressChanged += (object? sender, Downloader.DownloadProgressChangedEventArgs e) => { - count++; - await fs.WriteAsync(buffer, 0, bytesRead); - totalBytesRead += bytesRead; - - // 计算下载进度 - if (count % 30 == 0) + RedisExpand.SetTaskProgress(task, e.ProgressPercentage); + }; + download.DownloadFileCompleted +=async (object? sender, AsyncCompletedEventArgs e) => + { + if (download.Status == DownloadStatus.Failed && e.Error!=null) { - var progress = (double)totalBytesRead / totalBytes.Value * 100; - RedisExpand.SetTaskProgress(task, progress); + await RedisExpand.SetTaskErrorMessage(long.Parse(task), e.Error) + .ConfigureAwait(false);//不切回上下文 + return; + } + else if (download.Status == DownloadStatus.Completed) + { + //加入下一队列 + RedisExpand.InsertChannel(Enum.RedisChannelEnum.SeparateAudio, task); + return; } - } - //加入下一队列 - RedisExpand.InsertChannel(Enum.RedisChannelEnum.SeparateAudio, task); + }; + await download.StartAsync(); + } } } diff --git a/VideoAnalysisCore/Common/RedisExpand.cs b/VideoAnalysisCore/Common/RedisExpand.cs index 56f97d1..b10f999 100644 --- a/VideoAnalysisCore/Common/RedisExpand.cs +++ b/VideoAnalysisCore/Common/RedisExpand.cs @@ -1,4 +1,6 @@ -using FreeRedis; +using AntDesign; +using FreeRedis; +using FreeRedis.Internal; using Microsoft.Extensions.DependencyInjection; using SqlSugar.IOC; using System; @@ -6,6 +8,7 @@ using System.Threading.Channels; using System.Threading.Tasks; using System.Xml.Linq; using VideoAnalysisCore.AICore.ChatGPT; +using VideoAnalysisCore.AICore.ChatGPT.Dto; using VideoAnalysisCore.AICore.FFMPGE; //using VideoAnalysisCore.AICore.FFMPGE; @@ -13,6 +16,7 @@ using VideoAnalysisCore.AICore.SherpaOnnx; using VideoAnalysisCore.AICore.Whisper; using VideoAnalysisCore.Enum; using VideoAnalysisCore.Model; +using VideoAnalysisCore.Model.Dto; namespace VideoAnalysisCore.Common { @@ -28,7 +32,7 @@ namespace VideoAnalysisCore.Common /// /// 基础Channel key /// - public const string ChannelKey = BaseKey + "Channel:"; + public const string ChannelKey = BaseKey + "TaskChannel"; /// /// 下载文件 /// @@ -55,20 +59,13 @@ namespace VideoAnalysisCore.Common /// public const string TaskArr = BaseKey + "TaskArr"; - /// - /// 获取枚举RedisKey - /// - /// - /// - public static string EnumKey(RedisChannelEnum e) - { - return ChannelKey + e.ToString(); - } /// /// 任务对象地址 /// public static string Task(object taskId) => BaseKey + "Task:" + taskId; + public static string IDTask => BaseKey + AppCommon.Config.ID; + public static string TaskGPT(object taskId) => Task(taskId) + ":GPTCached"; } /// @@ -80,6 +77,12 @@ namespace VideoAnalysisCore.Common /// redis 连接 /// public static RedisClient Redis = new RedisClient(AppCommon.Config.Redis.ConnectionString); + public static Dictionary> SubscribeList = new Dictionary>(); + /// + /// 队列池 + /// + static SubscribeListObject? Subscribe; + /// /// 初始化 redis /// 需要在初始化配置文件时候调用 @@ -92,6 +95,14 @@ namespace VideoAnalysisCore.Common InitChannel(); } + /// + /// 获取任务进度 + /// + /// + public static void SetTaskGPTCached(object taskId, object data) + { + Redis.Set(RedisExpandKey.TaskGPT(taskId), data, 3600); + } /// /// 获取任务进度 /// @@ -105,9 +116,9 @@ namespace VideoAnalysisCore.Common /// /// 进度百分比 /// - public static void SetTaskProgress(object taskId,double p) + public static void SetTaskProgress(object taskId, double p) { - Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", Math.Round(p,2)); + Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", Math.Round(p, 2)); } /// @@ -117,6 +128,7 @@ namespace VideoAnalysisCore.Common /// 任务id public static void InsertChannel(RedisChannelEnum @enum, object taskId) { + if (taskId is null) throw new Exception("taskId为空"); if (Redis is null) throw new Exception("redis未初始化"); var startTime = Redis.HMGet>(RedisExpandKey.Task(taskId), "StartTime").FirstOrDefault(); @@ -128,28 +140,61 @@ namespace VideoAnalysisCore.Common startTime[@enum] = DateTime.Now; Redis.HMSet(RedisExpandKey.Task(taskId), "StartTime", startTime); - Redis.LPush(RedisExpandKey.EnumKey(@enum), taskId); + if(!SubscribeList.ContainsKey(@enum)) + throw new Exception(@enum+" 未实现"); + + SubscribeList[@enum].Invoke(taskId.ToString()); + } + + public static async Task TaskEnd(string task) + { + var tId = long.Parse(task); + var gptRes = (await RedisExpand.Redis + .HMGetAsync(RedisExpandKey.Task(task), "ChatAnalysis")).FirstOrDefault(); + if (gptRes is null) + throw new Exception("未能读取到GPT处理结果"); + + var taskData = await DbScoped.SugarScope.Queryable() + .FirstAsync(s => s.Id == tId); + taskData.ChatAnalysis = gptRes; + taskData.ChatAnalysisScore = gptRes.Assessment.Merit?.Sum(s => s.Score) ?? 0; + taskData.ErrorMessage = string.Empty; + taskData.LastEnum = RedisChannelEnum.EndTask; + await DbScoped.SugarScope.Updateable(taskData) + .UpdateColumns(it => new + { + it.ChatAnalysis, + it.ChatAnalysisScore, + it.ErrorMessage, + it.TotalTokens, + it.LastEnum, + }).ExecuteCommandAsync(); + + + await Redis.DelAsync(RedisExpandKey.IDTask); + await ReceivingTaskAsync(); } /// /// 初始化 队列 任务 /// - public static void InitChannel() + public static async void InitChannel() { if (Redis is null) throw new Exception("redis未初始化"); - Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.DownloadFile), + SubscribeList.Add(RedisChannelEnum.DownloadFile, (msg) => { TouchChannel(RedisChannelEnum.DownloadFile, msg, DownloadFile.RunTask); }); - Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.SeparateAudio), + + SubscribeList.Add(RedisChannelEnum.SeparateAudio, (msg) => { TouchChannel(RedisChannelEnum.SeparateAudio, msg, FFMPGEHandle.Audio2WAV16KAsync); }); - Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.ParsingCaptions), + SubscribeList.Add(RedisChannelEnum.ParsingCaptions, (msg) => { TouchChannel(RedisChannelEnum.ParsingCaptions, msg, SenseVoice.RunTask); }); - Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.ParsingSpeaker), + SubscribeList.Add(RedisChannelEnum.ParsingSpeaker, (msg) => { TouchChannel(RedisChannelEnum.ParsingSpeaker, msg, Speaker.Run); }); - Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.ChatModelAnalysis), + SubscribeList.Add(RedisChannelEnum.ChatModelAnalysis, (msg) => { TouchChannel(RedisChannelEnum.ChatModelAnalysis, msg, @@ -159,12 +204,38 @@ namespace VideoAnalysisCore.Common if (scope is null || scope.ServiceProvider.GetService() is null) throw new Exception("IBserGPT 未注入"); else - return scope.ServiceProvider.GetService()?.CallGPT(task)??Task.CompletedTask; + return scope.ServiceProvider.GetService()?.CallGPT(task) ?? Task.CompletedTask; }); }); - Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.CallBackSystem), - (msg) => { TouchChannel(RedisChannelEnum.ParsingSpeaker, msg); }); + SubscribeList.Add(RedisChannelEnum.EndTask, + (msg) => { TouchChannel(RedisChannelEnum.EndTask, msg, TaskEnd); }); + await ReceivingTaskAsync(); + + } + /// + /// 重新接收新任务 + /// + public static async Task ReceivingTaskAsync() + { + var oldTask = await Redis.GetAsync(RedisExpandKey.IDTask); + if (!string.IsNullOrEmpty(oldTask)) + { + var lastEnum = (await Redis.HMGetAsync(RedisExpandKey.Task(oldTask), "LastEnum")).FirstOrDefault(); + await SetTaskErrorMessage(long.Parse(oldTask), null); + InsertChannel(lastEnum, oldTask); + return; + } + + Subscribe = Redis.SubscribeList(RedisExpandKey.ChannelKey, (taskId) => + { + if (taskId is null) return; + Subscribe?.Dispose(); + //存储当前机器的任务 + Redis.Set(RedisExpandKey.IDTask, taskId); + Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-> 接收到任务 " + taskId); + InsertChannel(RedisChannelEnum.DownloadFile, taskId); + }); } /// @@ -173,10 +244,22 @@ namespace VideoAnalysisCore.Common /// /// /// - public static async Task SetTaskErrorMessage(long taskID, string errorMessage) + public static async Task SetTaskErrorMessage(long taskID, Exception? ex) { + var error = string.Empty; + if (ex != null) + { + //执行任务时出现异常 + error = ex.Message + ex.StackTrace; + Console.WriteLine("====================[出现异常]===================="); + Console.WriteLine(ex.Message); + Console.WriteLine(ex.StackTrace); + Console.WriteLine("=============================================="); + } + + Redis.HMSet(RedisExpandKey.Task(taskID), "ErrorMessage", error); return await DbScoped.SugarScope.Updateable() - .SetColumns(it => it.ErrorMessage == errorMessage)//SetColumns是可以叠加的 写2个就2个字段赋值 + .SetColumns(it => it.ErrorMessage == error)//SetColumns是可以叠加的 写2个就2个字段赋值 .Where(it => it.Id == taskID) .ExecuteCommandAsync() == 1; } @@ -189,12 +272,12 @@ namespace VideoAnalysisCore.Common public static async void TouchChannel(RedisChannelEnum key, string taskId, Func action = null) { if (taskId is null) return; + var tID = long.Parse(taskId); Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-> " + key + " " + taskId); if (action is not null) { try { - var tID = long.Parse(taskId); Redis.HMSet(RedisExpandKey.Task(taskId), "LastEnum", key); Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", 0); await DbScoped.SugarScope.Updateable() @@ -206,15 +289,8 @@ namespace VideoAnalysisCore.Common } catch (Exception ex) { - //执行任务时出现异常 - var error = ex.Message + ex.StackTrace; - await SetTaskErrorMessage(long.Parse(taskId), error); - - Console.WriteLine("====================[出现异常]===================="); - Console.WriteLine(ex.Message); - Console.WriteLine(ex.StackTrace); - Console.WriteLine("=============================================="); - + await SetTaskErrorMessage(tID, ex) + .ConfigureAwait(false);//不切回上下文 } } else diff --git a/VideoAnalysisCore/Enum/QuestionTypeEnum.cs b/VideoAnalysisCore/Enum/QuestionTypeEnum.cs index 8dd19a6..5dd928e 100644 --- a/VideoAnalysisCore/Enum/QuestionTypeEnum.cs +++ b/VideoAnalysisCore/Enum/QuestionTypeEnum.cs @@ -13,14 +13,14 @@ namespace VideoAnalysisCore.Enum [Display(Prompt = "分析授课中使用的高频词" + "10个频率从高到低 结果类型[]")] 高频词 = 100, - [Display(Prompt = "总结字幕内容划分成10个时间段" + - "并且提取它们的内容概览 结果类型[{Start:开始时间,End:结束时间,Content:概览}]")] + [Display(Prompt = "基于字幕内容知识点精准的划分成最多10个片段" + + "提取片段的内容概览,字幕开始秒,结束秒 结果类型[{Start:开始秒,End:结束秒,Content:概览}]")] 时间段概览 = 101, - [Display(Prompt = "统计授课中教师提问类型的次数 提问类型" + - "[重复回答,老师追问,简单性表扬,老师补充答案,表扬并补充答案] 结果类型{提问类型:次数}")] + [Display(Prompt = "统计授课中教师回答类型的次数 回答类型" + + "[重复回答,老师追问,简单性表扬,老师补充答案,表扬并补充答案] 结果类型{回答类型:次数}")] 提问类型 = 102, - [Display(Prompt = " 分析授课中教师提到 类型" + - "[独立学习,小组合作,随堂练习]的时间段 结果类型[{Start:开始时间/null,End:结束时间/null,Content:类型}]")] + [Display(Prompt = " 分析授课中教师提到 以下类型" + + "[独立学习,小组合作,随堂练习]的时间段,提取出其中字幕开始秒,结束秒 结果类型[{Start:开始秒,End:结束秒,Content:类型}/null]")] 额外课堂情况 = 103, } } diff --git a/VideoAnalysisCore/Enum/RedisChannelEnum.cs b/VideoAnalysisCore/Enum/RedisChannelEnum.cs index 187f0cc..281d95d 100644 --- a/VideoAnalysisCore/Enum/RedisChannelEnum.cs +++ b/VideoAnalysisCore/Enum/RedisChannelEnum.cs @@ -25,10 +25,10 @@ /// Chat模型分析 /// ChatModelAnalysis, - /// - /// 回调三方系统 - /// - CallBackSystem, + ///// + ///// 回调三方系统 + ///// + //CallBackSystem, /// /// 结束任务 /// diff --git a/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs b/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs index 4a912fa..3503180 100644 --- a/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs +++ b/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using VideoAnalysisCore.Enum; +using System.Text.Json; namespace VideoAnalysisCore.Model.Dto { @@ -64,5 +65,10 @@ namespace VideoAnalysisCore.Model.Dto /// [DisplayName("创建时间")] public DateTime CreateTime { get; set; } = DateTime.Now; + /// + /// 开始时间轴 + /// 逻辑字段 + /// + public Dictionary? StartTimeDic {get; set;} } } diff --git a/VideoAnalysisCore/Model/VideoTask.cs b/VideoAnalysisCore/Model/VideoTask.cs index 07c3d67..5e8c71f 100644 --- a/VideoAnalysisCore/Model/VideoTask.cs +++ b/VideoAnalysisCore/Model/VideoTask.cs @@ -97,6 +97,7 @@ namespace VideoAnalysisCore.Model /// /// AI模型评分 /// + [SugarColumn(IsNullable =true)] public decimal? ChatAnalysisScore { get; set; } /// /// 消耗token diff --git a/VideoAnalysisCore/VideoAnalysisCore.csproj b/VideoAnalysisCore/VideoAnalysisCore.csproj index 79861de..6aaf56a 100644 --- a/VideoAnalysisCore/VideoAnalysisCore.csproj +++ b/VideoAnalysisCore/VideoAnalysisCore.csproj @@ -46,6 +46,7 @@ +