parent
c9ef1d230c
commit
489aff9087
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@page "/"
|
||||
@page "/Project"
|
||||
@using AntDesign
|
||||
@using AntDesign.TableModels
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
@page "/"
|
||||
@using AntDesign
|
||||
@using AntDesign.TableModels
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using SqlSugar
|
||||
@using VideoAnalysisCore.Model
|
||||
@using VideoAnalysisCore.Model.Dto
|
||||
|
||||
<Table @ref="_table" Loading="tableLoading" TItem="VideoTaskDto" PageSize="15" Total="_total" DataSource="_dataSource" @bind-SelectedRows="_selectedRows" OnChange="OnChange">
|
||||
<TitleTemplate>
|
||||
<Flex Justify="end" Gap="10"> </Flex>
|
||||
</TitleTemplate>
|
||||
<ColumnDefinitions Context="row">
|
||||
<Selection />
|
||||
<ActionColumn Title="操作列">
|
||||
<Button Type="@ButtonType.Link" Danger @onclick="() => ReStart(row)">
|
||||
重试
|
||||
</Button>
|
||||
</ActionColumn>
|
||||
<GenerateColumns Definitions="@((n,c) => { c.Filterable = true; c.Sortable = true; })" />
|
||||
</ColumnDefinitions>
|
||||
</Table>
|
||||
|
|
@ -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<VideoTask> taskDB { get; set; } = default!;
|
||||
|
||||
|
||||
IEnumerable<VideoTaskDto> _selectedRows = [];
|
||||
ITable _table;
|
||||
|
||||
List<VideoTaskDto> _dataSource = null;
|
||||
RefAsync<int> _total = 0;
|
||||
|
||||
bool tableLoading = false;
|
||||
|
||||
/// <summary>
|
||||
/// 重试
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
async void ReStart(VideoTaskDto query)
|
||||
{
|
||||
var lastEnum = (await RedisExpand.Redis.HMGetAsync<RedisChannelEnum>(RedisExpandKey.Task(query.Id), "LastEnum")).FirstOrDefault() ;
|
||||
RedisExpand.InsertChannel(lastEnum, query.Id);
|
||||
}
|
||||
/// <summary>
|
||||
/// 分页 查询 筛选 时
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
async void OnChange(QueryModel<VideoTaskDto> query)
|
||||
{
|
||||
tableLoading = true;
|
||||
List<IConditionalModel> where = default!;
|
||||
if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
|
||||
{
|
||||
where = query.ToSqlSugerWhere();
|
||||
}
|
||||
_dataSource = await taskDB.AsQueryable()
|
||||
.Where(where)
|
||||
.Select<VideoTaskDto>()
|
||||
.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<double>(RedisExpandKey.Task(item.Id), "Progress").FirstOrDefault();
|
||||
}
|
||||
|
||||
tableLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
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<double>(RedisExpandKey.Task(item.Id), "Progress").FirstOrDefault();
|
||||
}
|
||||
} );
|
||||
InvokeAsync(StateHasChanged);
|
||||
}, null, dueTime: 500, period: 3000);
|
||||
}
|
||||
|
||||
private async Task<bool> Comfirm(string message)
|
||||
{
|
||||
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
input[aria-hidden="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VideoAnalysisCore\VideoAnalysisCore.csproj" />
|
||||
<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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ProSettings>(builder.Configuration.GetSection("ProSettings"));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取任务进度
|
||||
/// </summary>
|
||||
/// <param name="taskId"></param>
|
||||
public static double SetTaskProgress(object taskId)
|
||||
{
|
||||
return Redis.HMGet<double>(RedisExpandKey.Task(taskId), "Progress")[0];
|
||||
}
|
||||
/// <summary>
|
||||
/// 设置任务进度
|
||||
/// </summary>
|
||||
/// <param name="p">进度百分比</param>
|
||||
/// <param name="taskId"></param>
|
||||
public static void SetTaskProgress(object taskId,double p)
|
||||
{
|
||||
Redis.HMSet(RedisExpandKey.Task(taskId), "Progress", p);
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 将任务 插入 队列
|
||||
/// </summary>
|
||||
|
|
@ -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
|
|||
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// 写入任务异常
|
||||
/// </summary>
|
||||
/// <param name="taskID"></param>
|
||||
/// <param name="errorMessage"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> SetTaskErrorMessage(long taskID, string errorMessage)
|
||||
{
|
||||
return await DbScoped.SugarScope.Updateable<VideoTask>()
|
||||
.SetColumns(it => it.ErrorMessage == errorMessage)//SetColumns是可以叠加的 写2个就2个字段赋值
|
||||
.Where(it => it.Id == taskID)
|
||||
.ExecuteCommandAsync() == 1;
|
||||
}
|
||||
/// <summary>
|
||||
/// 触发
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="taskId"></param>
|
||||
/// <param name="action"></param>
|
||||
public static async void TouchChannel(RedisChannelEnum key, string msg, Func<string, Task> action = null)
|
||||
public static async void TouchChannel(RedisChannelEnum key, string taskId, Func<string, Task> 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<VideoTask>()
|
||||
.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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VideoAnalysisCore.Model;
|
||||
|
||||
namespace VideoAnalysisCore.Common
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@
|
|||
/// <summary>
|
||||
/// 回调三方系统
|
||||
/// </summary>
|
||||
CallBackSystem = 6
|
||||
CallBackSystem = 6,
|
||||
/// <summary>
|
||||
/// 结束任务
|
||||
/// </summary>
|
||||
EndTask = 6,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务id
|
||||
/// <para>视频音频文件地址都使用taskID能获取</para>
|
||||
/// </summary>
|
||||
[DisplayName("任务id")]
|
||||
public long Id { get; set; }
|
||||
/// <summary>
|
||||
/// ApiKey
|
||||
/// </summary>
|
||||
[DisplayName("ApiKey")]
|
||||
public string ApiToken { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 请求来自哪个ip地址
|
||||
/// </summary>
|
||||
[DisplayName("请求IP")]
|
||||
public string ComeFrom { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 上一次执行的枚举
|
||||
/// </summary>
|
||||
[DisplayName("最后执行")]
|
||||
public RedisChannelEnum LastEnum { get; set; }
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
[DisplayName("错误信息")]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 执行进度
|
||||
/// </summary>
|
||||
[DisplayName("进度")]
|
||||
public double Progress { get; set; }
|
||||
/// <summary>
|
||||
/// 媒体路径
|
||||
/// </summary>
|
||||
[DisplayName("媒体路径")]
|
||||
public string MediaUrl { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 自定义值 任务完成后附带通知
|
||||
/// </summary>
|
||||
[DisplayName("自定义值")]
|
||||
public string? Tag { get; set; }
|
||||
/// <summary>
|
||||
/// 消耗token
|
||||
/// </summary>
|
||||
[DisplayName("消耗Token")]
|
||||
public int TotalTokens { get; set; }
|
||||
/// <summary>
|
||||
///回调Api地址
|
||||
/// </summary>
|
||||
[DisplayName("回调地址")]
|
||||
public string? CallBackUrl { get; set; }
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
[DisplayName("创建时间")]
|
||||
public DateTime CreateTime { get; set; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
/// <summary>
|
||||
/// 字幕缓存
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
|
||||
public SegmentData[]? Captions { get; set; }
|
||||
[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);
|
||||
}
|
||||
/// <summary>
|
||||
/// 说话人日志解析缓存
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
|
||||
public OfflineSpeakerRes[]? Speaker { get; set; }
|
||||
[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);
|
||||
}
|
||||
/// <summary>
|
||||
/// Chat模型分析缓存
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
|
||||
public object[]? ChatAnalysis { get; set; }
|
||||
[SugarColumn(ColumnName = "ChatAnalysis", ColumnDataType = "longtext", IsNullable = true)]
|
||||
public string _ChatAnalysis { get; set; } = "[]";
|
||||
/// <summary>
|
||||
/// Chat模型分析缓存
|
||||
/// </summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public object[]? ChatAnalysis
|
||||
{
|
||||
get => JsonSerializer.Deserialize<object[]>(_ChatAnalysis ?? "[]");
|
||||
set => _ChatAnalysis = JsonSerializer.Serialize(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 消耗token
|
||||
/// </summary>
|
||||
public int TotalTokens { get; set; }
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
|
||||
public string? ErrorMessage { get; set; }
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; } = DateTime.Now;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FreeRedis" Version="1.3.2" />
|
||||
<PackageReference Include="Mapster" Version="7.4.1-pre01" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue