diff --git a/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs b/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs index 49d3bbb..af76e51 100644 --- a/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs +++ b/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs @@ -19,15 +19,22 @@ namespace VideoAnalysisRazor.Layouts } protected override async Task OnInitializedAsync() { - _menuData = new[] { + _menuData = [ + new MenuDataItem + { + Path = "/Project", + Name = "课堂指标", + Key = "EvaluationProject", + Icon = "question-circle", + }, new MenuDataItem { Path = "/", - Name = "课堂指标", - Key = "EvaluationProject", - Icon = "EvaluationProject", + Name = "任务队列", + Key = "VideoTaskPage", + Icon = "unordered-list", } - }; + ]; } void Reload() diff --git a/VideoAnalysis/Components/Pages/EvaluationProject.razor b/VideoAnalysis/Components/Pages/EvaluationProject.razor index 09d1f3b..e9a99cb 100644 --- a/VideoAnalysis/Components/Pages/EvaluationProject.razor +++ b/VideoAnalysis/Components/Pages/EvaluationProject.razor @@ -1,4 +1,4 @@ -@page "/" +@page "/Project" @using AntDesign @using AntDesign.TableModels @using System.ComponentModel.DataAnnotations diff --git a/VideoAnalysis/Components/Pages/VideoTaskPage.razor b/VideoAnalysis/Components/Pages/VideoTaskPage.razor new file mode 100644 index 0000000..d298a67 --- /dev/null +++ b/VideoAnalysis/Components/Pages/VideoTaskPage.razor @@ -0,0 +1,22 @@ +@page "/" +@using AntDesign +@using AntDesign.TableModels +@using System.ComponentModel.DataAnnotations +@using SqlSugar +@using VideoAnalysisCore.Model +@using VideoAnalysisCore.Model.Dto + + + + + + + + + + + + +
diff --git a/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs new file mode 100644 index 0000000..7cdd79c --- /dev/null +++ b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs @@ -0,0 +1,94 @@ +using AntDesign; +using AntDesign.TableModels; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using SqlSugar; +using System.Linq.Expressions; +using System.Threading.Tasks; +using VideoAnalysisCore.Common; +using VideoAnalysisCore.Enum; +using VideoAnalysisCore.Model; +using VideoAnalysisCore.Model.Dto; + +namespace Learn.VideoAnalysis.Components.Pages +{ + public partial class VideoTaskPage : ComponentBase + { + + [Inject] private ConfirmService ComfirmService { get; set; } = default!; + [Inject] private Repository taskDB { get; set; } = default!; + + + IEnumerable _selectedRows = []; + ITable _table; + + List _dataSource = null; + RefAsync _total = 0; + + bool tableLoading = false; + + /// + /// 重试 + /// + /// + async void ReStart(VideoTaskDto query) + { + var lastEnum = (await RedisExpand.Redis.HMGetAsync(RedisExpandKey.Task(query.Id), "LastEnum")).FirstOrDefault() ; + RedisExpand.InsertChannel(lastEnum, query.Id); + } + /// + /// 分页 查询 筛选 时 + /// + /// + async void OnChange(QueryModel query) + { + tableLoading = true; + List where = default!; + if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0)) + { + where = query.ToSqlSugerWhere(); + } + _dataSource = await taskDB.AsQueryable() + .Where(where) + .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; + StateHasChanged(); + + } + /// + /// 初始化 + /// + protected override void OnInitialized() + { + var timer = new Timer(_ => + { + InvokeAsync(() => + { + 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(); + } + } ); + InvokeAsync(StateHasChanged); + }, null, dueTime: 500, period: 3000); + } + + private async Task Comfirm(string message) + { + return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes; + } + + } + +} diff --git a/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css new file mode 100644 index 0000000..aca1ed1 --- /dev/null +++ b/VideoAnalysis/Components/Pages/VideoTaskPage.razor.css @@ -0,0 +1,3 @@ +input[aria-hidden="true"] { + display: none !important; +} diff --git a/VideoAnalysis/Learn.VideoAnalysis.csproj b/VideoAnalysis/Learn.VideoAnalysis.csproj index 384c983..ea4259f 100644 --- a/VideoAnalysis/Learn.VideoAnalysis.csproj +++ b/VideoAnalysis/Learn.VideoAnalysis.csproj @@ -17,6 +17,7 @@ + diff --git a/VideoAnalysis/Program.cs b/VideoAnalysis/Program.cs index cf4e036..a4669c5 100644 --- a/VideoAnalysis/Program.cs +++ b/VideoAnalysis/Program.cs @@ -7,6 +7,7 @@ using VideoAnalysisCore.AICore.ChatGPT; using VideoAnalysisCore.AICore.ChatGPT.KIMI; using VideoAnalysisCore.AICore.SherpaOnnx; using SqlSugar; +using Mapster; @@ -69,6 +70,7 @@ namespace Learn.VideoAnalysis builder.Services.AddAntDesign(); builder.Services.InitSqlSugar(); + builder.Services.AddMapster(); builder.Services.Configure(builder.Configuration.GetSection("ProSettings")); diff --git a/VideoAnalysis/appsettings.json b/VideoAnalysis/appsettings.json index c84b063..5bc6be3 100644 --- a/VideoAnalysis/appsettings.json +++ b/VideoAnalysis/appsettings.json @@ -26,7 +26,7 @@ "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": false, + "UpdateTable": true, } } } diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs b/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs index 1ac7864..22e9a23 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/SenseVoice.cs @@ -77,9 +77,7 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) throw new Exception("task 音频路径未找到"); - string testWaveFilename = filePath; - WaveReader reader = new WaveReader(testWaveFilename); - + WaveReader reader = new WaveReader(filePath); int numSamples = reader.Samples.Length; int windowSize = VADModelConfig.SileroVad.WindowSize; int sampleRate = VADModelConfig.SampleRate; @@ -110,7 +108,11 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx { Text = stream.Result.Text, Start= startTime, - End = startTime + duration }); + End = startTime + duration + }); + var progress = (double)(startTime + duration) / numSamples * 100; + RedisExpand.SetTaskProgress(task, progress); + } VAD.Pop(); } diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs b/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs index 203ce1f..2ddf81b 100644 --- a/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs +++ b/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs @@ -50,11 +50,15 @@ namespace VideoAnalysisCore.AICore.SherpaOnnx if (SD.SampleRate != waveReader.SampleRate) throw new Exception($"预期采样率:{SD.SampleRate}. 传入: {waveReader.SampleRate}"); + var i = 0; var segments = SD.ProcessWithCallback(waveReader.Samples, (numProcessedChunks, numTotalChunks, arg) => { + i++; + if(i%50 !=0) + return 1; var progress = (double)numProcessedChunks / numTotalChunks * 100; - Console.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}=> {task} 说话人日志: {progress:F2}%"); + RedisExpand.SetTaskProgress(task, progress); return 1; }, nint.Zero); var res = segments.Select(s => new OfflineSpeakerRes(s)); diff --git a/VideoAnalysisCore/Common/DownloadFile.cs b/VideoAnalysisCore/Common/DownloadFile.cs index 492b6e0..f31ef05 100644 --- a/VideoAnalysisCore/Common/DownloadFile.cs +++ b/VideoAnalysisCore/Common/DownloadFile.cs @@ -58,6 +58,8 @@ namespace VideoAnalysisCore.Common // 获取文件大小 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); if (!Directory.Exists(localPath)) Directory.CreateDirectory(localPath); @@ -80,10 +82,10 @@ namespace VideoAnalysisCore.Common totalBytesRead += bytesRead; // 计算下载进度 - if (totalBytes.HasValue && count % 3 == 0) + if (count % 50 == 0) { var progress = (double)totalBytesRead / totalBytes.Value * 100; - Console.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}=> {task} 下载进度: {progress:F2}%"); + RedisExpand.SetTaskProgress(task, progress); } } diff --git a/VideoAnalysisCore/Common/RedisExpand.cs b/VideoAnalysisCore/Common/RedisExpand.cs index a6ff907..b00d9d1 100644 --- a/VideoAnalysisCore/Common/RedisExpand.cs +++ b/VideoAnalysisCore/Common/RedisExpand.cs @@ -1,5 +1,6 @@ using FreeRedis; using Microsoft.Extensions.DependencyInjection; +using SqlSugar.IOC; using System; using System.Threading.Channels; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using VideoAnalysisCore.AICore.FFMPGE; using VideoAnalysisCore.AICore.SherpaOnnx; using VideoAnalysisCore.AICore.Whisper; using VideoAnalysisCore.Enum; +using VideoAnalysisCore.Model; namespace VideoAnalysisCore.Common { @@ -89,6 +91,25 @@ namespace VideoAnalysisCore.Common Redis.Deserialize = (json, type) => System.Text.Json.JsonSerializer.Deserialize(json, type); InitChannel(); } + + /// + /// 获取任务进度 + /// + /// + public static double SetTaskProgress(object taskId) + { + return Redis.HMGet(RedisExpandKey.Task(taskId), "Progress")[0]; + } + /// + /// 设置任务进度 + /// + /// 进度百分比 + /// + public static void SetTaskProgress(object taskId,double p) + { + Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", p); + + } /// /// 将任务 插入 队列 /// @@ -106,7 +127,6 @@ namespace VideoAnalysisCore.Common else startTime[@enum] = DateTime.Now; - Redis.HMSet(RedisExpandKey.Task(taskId), "StartTime", startTime); Redis.LPush(RedisExpandKey.EnumKey(@enum), taskId); @@ -148,19 +168,47 @@ namespace VideoAnalysisCore.Common } /// - /// + /// 写入任务异常 + /// + /// + /// + /// + public static async Task SetTaskErrorMessage(long taskID, string errorMessage) + { + return await DbScoped.SugarScope.Updateable() + .SetColumns(it => it.ErrorMessage == errorMessage)//SetColumns是可以叠加的 写2个就2个字段赋值 + .Where(it => it.Id == taskID) + .ExecuteCommandAsync() == 1; + } + /// + /// 触发 /// /// - /// + /// /// - public static async void TouchChannel(RedisChannelEnum key, string msg, Func action = null) + public static async void TouchChannel(RedisChannelEnum key, string taskId, Func action = null) { - if (msg is null) return; - Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-> " + key + " " + msg); + if (taskId is null) return; + Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-> " + key + " " + taskId); if (action is not null) { - Redis.HMSet(RedisExpandKey.Task(msg), "LastEnum", key); - await action(msg); + var tID = long.Parse(taskId); + Redis.HMSet(RedisExpandKey.Task(taskId), "LastEnum", key); + await DbScoped.SugarScope.Updateable() + .SetColumns(it => it.LastEnum == key) + .Where(it => it.Id == tID) + .ExecuteCommandAsync(); + try + { + await action(taskId); + } + catch (Exception ex) + { + //执行任务时出现异常 + var error = ex.Message + ex.StackTrace; + await SetTaskErrorMessage(long.Parse(taskId), error); + Redis.HMSet(RedisExpandKey.Task(taskId), "Error", error); + } } else { diff --git a/VideoAnalysisCore/Common/Repository.cs b/VideoAnalysisCore/Common/Repository.cs index 6b463cf..d1afcce 100644 --- a/VideoAnalysisCore/Common/Repository.cs +++ b/VideoAnalysisCore/Common/Repository.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using VideoAnalysisCore.Model; namespace VideoAnalysisCore.Common { diff --git a/VideoAnalysisCore/Enum/RedisChannelEnum.cs b/VideoAnalysisCore/Enum/RedisChannelEnum.cs index 66a2441..f8e1a68 100644 --- a/VideoAnalysisCore/Enum/RedisChannelEnum.cs +++ b/VideoAnalysisCore/Enum/RedisChannelEnum.cs @@ -28,6 +28,10 @@ /// /// 回调三方系统 /// - CallBackSystem = 6 + CallBackSystem = 6, + /// + /// 结束任务 + /// + EndTask = 6, } } diff --git a/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs b/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs new file mode 100644 index 0000000..20a98fa --- /dev/null +++ b/VideoAnalysisCore/Model/Dto/VideoTaskDto.cs @@ -0,0 +1,73 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VideoAnalysisCore.Enum; + +namespace VideoAnalysisCore.Model.Dto +{ + public class VideoTaskDto + { + /// + /// 任务id + /// 视频音频文件地址都使用taskID能获取 + /// + [DisplayName("任务id")] + public long Id { get; set; } + /// + /// ApiKey + /// + [DisplayName("ApiKey")] + public string ApiToken { get; set; } = string.Empty; + /// + /// 请求来自哪个ip地址 + /// + [DisplayName("请求IP")] + public string ComeFrom { get; set; } = string.Empty; + /// + /// 上一次执行的枚举 + /// + [DisplayName("最后执行")] + public RedisChannelEnum LastEnum { get; set; } + /// + /// 错误信息 + /// + [DisplayName("错误信息")] + public string? ErrorMessage { get; set; } + + /// + /// 执行进度 + /// + [DisplayName("进度")] + public double Progress { get; set; } + /// + /// 媒体路径 + /// + [DisplayName("媒体路径")] + public string MediaUrl { get; set; } = string.Empty; + /// + /// 自定义值 任务完成后附带通知 + /// + [DisplayName("自定义值")] + public string? Tag { get; set; } + /// + /// 消耗token + /// + [DisplayName("消耗Token")] + public int TotalTokens { get; set; } + /// + ///回调Api地址 + /// + [DisplayName("回调地址")] + public string? CallBackUrl { get; set; } + /// + /// 创建时间 + /// + [DisplayName("创建时间")] + public DateTime CreateTime { get; set; } = DateTime.Now; + } +} diff --git a/VideoAnalysisCore/Model/VideoTask.cs b/VideoAnalysisCore/Model/VideoTask.cs index 97bf0a4..f280a4d 100644 --- a/VideoAnalysisCore/Model/VideoTask.cs +++ b/VideoAnalysisCore/Model/VideoTask.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Net; using System.Text.Json; using VideoAnalysisCore.AICore.SherpaOnnx; +using VideoAnalysisCore.AICore.Whisper; using VideoAnalysisCore.Enum; using Whisper.net; @@ -53,23 +54,58 @@ namespace VideoAnalysisCore.Model /// /// 字幕缓存 /// - [SugarColumn(ColumnDataType = "longtext", IsNullable = true)] - public SegmentData[]? Captions { get; set; } + [SugarColumn(ColumnName = "Captions", ColumnDataType = "longtext", IsNullable = true)] + public string _Captions { get; set; } = "[]"; + /// + /// 字幕缓存 + /// + [SugarColumn(IsIgnore = true)] + public SenseVoiceRes[]? Captions + { + get=> JsonSerializer.Deserialize(_Captions ?? "[]"); + set => _Captions = JsonSerializer.Serialize(value); + } /// /// 说话人日志解析缓存 /// - [SugarColumn(ColumnDataType = "longtext", IsNullable = true)] - public OfflineSpeakerRes[]? Speaker { get; set; } + [SugarColumn(ColumnName = "Speaker", ColumnDataType = "longtext", IsNullable = true)] + public string _Speaker { get; set; } = "[]"; + /// + /// 说话人日志解析缓存 + /// + [SugarColumn(IsIgnore = true)] + public OfflineSpeakerRes[]? Speaker + { + get => JsonSerializer.Deserialize(_Speaker ?? "[]"); + set => _Speaker = JsonSerializer.Serialize(value); + } /// /// Chat模型分析缓存 /// - [SugarColumn(ColumnDataType = "longtext", IsNullable = true)] - public object[]? ChatAnalysis { get; set; } + [SugarColumn(ColumnName = "ChatAnalysis", ColumnDataType = "longtext", IsNullable = true)] + public string _ChatAnalysis { get; set; } = "[]"; + /// + /// Chat模型分析缓存 + /// + [SugarColumn(IsIgnore = true)] + public object[]? ChatAnalysis + { + get => JsonSerializer.Deserialize(_ChatAnalysis ?? "[]"); + set => _ChatAnalysis = JsonSerializer.Serialize(value); + } + + + /// /// 消耗token /// public int TotalTokens { get; set; } /// + /// 错误信息 + /// + [SugarColumn(ColumnDataType = "text", IsNullable = true)] + public string? ErrorMessage { get; set; } + /// /// 创建时间 /// public DateTime CreateTime { get; set; } = DateTime.Now; diff --git a/VideoAnalysisCore/VideoAnalysisCore.csproj b/VideoAnalysisCore/VideoAnalysisCore.csproj index 80f3bd3..79861de 100644 --- a/VideoAnalysisCore/VideoAnalysisCore.csproj +++ b/VideoAnalysisCore/VideoAnalysisCore.csproj @@ -46,6 +46,7 @@ +