优化 复习课切题流程/预览UI

This commit is contained in:
小肥羊 2025-05-29 17:42:09 +08:00
parent ad0f415ef6
commit 2bdd77380c
10 changed files with 198 additions and 98 deletions

View File

@ -11,9 +11,31 @@
@for (int i = 0; i < videoKnows.Length; i++)
{
var item = videoKnows[i];
<button class="kBtn" onclick="spClick(@i,this)" title="@item.Content">
<span>@getF(item) @item.Theme</span>
<br /><span class="kSpan">#@item.KnowPointId @item.KnowPoint</span></button>
<div class="knowDiv" onclick="spClick(@i,this)">
<div class="knowTtile">
<div class="knowTtileTheme">@getF(item) @item.Theme</div>
<span class="kSpan">#@item.KnowPointId @item.KnowPoint</span>
<div>概览: @item.Content</div>
<br />
@foreach (var q in item.QuestionArr)
{
<h3>问题: </h3>
<div class="knowQuestion">@q.Question</div>
<img style="text-align:center" src="@q.ImageUrl" width="320" height="180" />
<br />
<br />
}
<br />
<br />
</div>
<button class="kBtn" >
<span>@getF(item) @item.Theme</span>
<br /><span class="kSpan textEllipsis">#@item.KnowPointId @item.KnowPoint</span>
</button>
</div>
}
</div>
}
@ -28,9 +50,15 @@
"></div>
</div>
<script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" crossorigin="anonymous"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']],
displayMath: [['$$', '$$'], ['\[', '\]']]
}
};
window.b = []
window.subtitles = []
window.subtitles1 = []

View File

@ -29,6 +29,8 @@ namespace Learn.VideoAnalysis.Components.Pages
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private IHttpContextAccessor HttpContext { get; set; } = default!;
[Inject] private Repository<VideoTask> taskDB { get; set; } = default!;
[Inject] private Repository<VideoQuestion> videoQuestionDB { get; set; } = default!;
[Inject] private Repository<VideoQuestionKonw> videoQuestionKonwDB { get; set; } = default!;
[Inject] private Repository<VideoKonwPoint> videoKonwPointDB { get; set; } = default!;
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
@ -88,10 +90,31 @@ namespace Learn.VideoAnalysis.Components.Pages
StartTime = s.First().StartTime,
EndTime = s.First().EndTime,
Theme = s.First().Theme,
StageId = s.First().StageId,
KnowPoint = string.Join(',', s.Select(x => x.KnowPoint))
}).ToArray();
videoPath = AppCommon.GetVideoPath(nowTask.Id.ToString());
await JSRuntime.InvokeVoidAsync("setDB", captionsArr, captionsArr1, videoKnows, videoPath);
if (nowTask.VideoType == AttachmentsInfoType.Review)
{
var questionArr = await videoQuestionDB
.AsQueryable().Where(s => s.VideoTaskId == nowTask.Id)
.Select<VideoQuestionShowDto>()
.ToArrayAsync();
var konwDic = (await videoQuestionKonwDB
.AsQueryable().Where(s => s.VideoTaskId == nowTask.Id)
.ToArrayAsync()).GroupBy(s=>s.VideoQuestionId)
.ToDictionary(s=>s.Key);
foreach (var item in questionArr.Where(s=> konwDic.ContainsKey(s.Id)))
item.KonwArr = konwDic[item.Id].ToArray();
foreach (var item in videoKnows)
item.QuestionArr = questionArr
.Where(s => s.StageId == item.StageId).ToArray();
}
await JSRuntime
.InvokeVoidAsync("setDB", captionsArr, captionsArr1, videoKnows, videoPath);
StateHasChanged();
}

View File

@ -12,8 +12,14 @@
overflow-x: hidden;
}
.kSpan {
color:rgba(120, 120, 120,0.66);
font-size:0.8rem;
color: rgba(120, 120, 120,0.66);
font-size: 0.8rem;
width: 330px;
}
.textEllipsis {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
video {
width: 94%;
@ -36,7 +42,6 @@ video {
font-size: 18px;
}
#segmentsContainer:is(:hover) {
width: 400px;
right: 0px!important;
}
#segmentsContainer {
@ -51,18 +56,17 @@ video {
width: 400px;
height: 750px;
gap: 10px;
overflow: hidden;
overflow-y: scroll;
float: left;
flex-wrap: nowrap;
padding: 10px;
align-content: flex-start;
justify-content: flex-start;
align-items: center;
align-items: flex-end
}
.kBtn {
width: 100%;
width: 340px;
height: 60px;
font-size: 1.3rem;
text-align: left;
@ -71,8 +75,30 @@ video {
background-color: rgb(240, 249, 235);
border: 1px solid rgb(179, 225, 157);
}
.knowTtileTheme {
font-size: 1.3rem;
cursor: pointer;
color: rgb(103, 194, 58);
}
.kBtn:hover {
background-color: rgb(248, 230, 191) !important;
border: 1px solid rgb(206, 187, 81);
}
.knowDiv {
cursor: pointer;
}
.knowQuestion {
cursor: pointer;
}
.knowDiv:hover .knowTtile {
width: 340px;
display:block;
background-color: rgb(240, 249, 235);
border: 1px solid rgb(179, 225, 157);
}
.knowTtile {
position: absolute;
text-align: left;
display: none;
}

View File

@ -58,7 +58,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": true
"UpdateTable": false
},
"AlibabaCloudVod": {
"AccessKeyId": "LTAI5tDC6p9h747B7FHbgwkH",

View File

@ -302,82 +302,6 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
}
}
/// <summary>
/// 获取知识点
/// </summary>
/// <param name="task">任务id</param>
/// <returns></returns>
public async Task<TaskRes> GetKnow(string task)
{
var taskId = long.Parse(task);
var taskInfo = await videoTaskDB.AsQueryable()
.Where(s => s.Id == taskId)
.FirstAsync();
var subject = taskInfo.Subject.ToString();
var Course_Id = 27;
switch (taskInfo.Type)//处理不同任务类型的知识点树
{
case TaskTypeEnum._中职视频分段:
Course_Id = 51;
break;
case TaskTypeEnum._视频分段:
default:
Course_Id = 27;
break;
}
if(taskInfo.VideoType == AttachmentsInfoType.Review)
await AnalysisVideoQuestions(taskInfo);
var captionsArr = JsonSerializer.Deserialize<SenseVoiceRes[]>(taskInfo.Captions);
//处理视频授课章节
var sections = await GetSections(taskInfo, Course_Id);
//AI优化字幕
captionsArr = await OptimizeSubtitles(taskInfo, captionsArr, sections);
//合并字幕
var captions = ExpandFunction.GetSpeakerCaptions(captionsArr);
var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0;
var questionRes = new List<VideoKnowRes>();
while (true)
{
questionRes = new List<VideoKnowRes>();
//视频字幕分析
await Analytics(questionRes, taskInfo, captionsArr, sections);
if (questionRes.Count == 0) continue;
//处理分段 知识点
var insertData = await GetVideoKnow(questionRes, taskInfo, sections, Course_Id);
//校验结果质量
var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id);
if (checkRes != null && checkRes.Score >= 80)
{
//写入知识点
await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id);
await videoKonwPointDB.InsertRangeAsync(insertData);
break;
}
else
{
Console.WriteLine(DateTime.Now + $"=>{task} 得分过低/分段长度不匹配 得分{checkRes?.Score} ");
Console.WriteLine(checkRes.Evaluation);
Console.WriteLine();
}
if (questionRes.Any(s => s.KeepTime < 30))
{
Console.WriteLine(DateTime.Now + "=>视频分段过短!! 重新进行AI分析");
continue;
}
}
await RedisExpand.Redis
.HMSetAsync(RedisExpandKey.Task(task), "VideoKnows", questionRes);
RedisExpand.InsertChannel(RedisChannelEnum.EndTask, task);
return null;
}
public async Task<T> ChatAsync<T>(string task, string postMessages, string title, string model = "deepseek-reasoner")
{
@ -452,7 +376,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
/// <returns></returns>
private async Task<SenseVoiceRes[]> AnalysisVideoQuestions(VideoTask taskInfo)
{
Console.WriteLine(DateTime.Now + $"=>{taskInfo.Id} 提取试题");
if (taskInfo is null || string.IsNullOrEmpty(taskInfo.PPTKeyFrame))
return null;
var farmeArr = JsonSerializer.Deserialize<int[]>(taskInfo.PPTKeyFrame);
@ -462,7 +386,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
foreach (var item in farmeArr)
{
var knowInfoArr = videoKnowArr
.Where(s => item >= s.StartTime && item <= s.EndTime)
.Where(s => item+5 >= s.StartTime && item+5 <= s.EndTime)
.ToArray();
if (knowInfoArr is null || knowInfoArr.Count() ==0)
return null;
@ -477,12 +401,12 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
.ProcessImageAsync(new SimpleTexOcrRequest(filePath));
if (!sRes.Success)
continue;
var knowArr=string.Join(',', knowInfoArr.Select(s => s.KnowPoint + "|"+s.KnowPointId));
var knowArr=string.Join(',', knowInfoArr.Select(s => s.KnowPointId + "|" + s.KnowPoint ));
var resFormat = """[{"TopicStem":string(试题题干),"Question:string()","KnowPointId":(string)知识点ID}]""";
var postMessages =
$"提供一段内容是md格式的试题内容字符串。" +
$"请提取出其中的试题内容。并且为每个试题关联上在我限定范围内的知识点(多个则用逗号分割)。" +
$"知识点范围[{knowArr}]。" +
$"知识点格式(知识点ID|知识点名称)范围[{knowArr}]。" +
$"排除不是试题内容的文字,优化公式排版并且去除题号。" +
$"如果存在多道大题,请帮忙拆分开!" +
$"输出内容只返回json格式为({resFormat})" +
@ -491,6 +415,7 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
var resData = await ChatAsync<VideoQuestionOSSDto[]>(taskInfo.Id.ToString(), postMessages, "提取试题", "deepseek-chat");
foreach (var q in resData)
{
q.Id = YitIdHelper.NextId();
q.FilePath = filePath;
q.VideoTaskId = taskInfo.Id;
q.StageId = knowInfoArr.First().StageId;
@ -502,15 +427,16 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
KnowPointId= kid,
StageId =q.StageId,
VideoTaskId = q.VideoTaskId,
VideoQuestionId = q.Id,
});
}
}
insertData.AddRange(resData);
break;
}
catch (Exception)
catch (Exception ex)
{
Console.WriteLine(DateTime.Now + $"=>{taskInfo.Id} 提取{knowInfoArr.First().StartTime}秒试题出现错误 {ex.Message}");
}
}
}
@ -521,12 +447,96 @@ namespace VideoAnalysisCore.AICore.GPT.DeepSeek
foreach (var s in item)
s.TopicId = keyId;
}
//上传oss
//上传oss 并更新imageUrl
ossClient.AddVideoQuestionUrl(insertData);
await videoQuestionDB.AsDeleteable()
.Where(s=>s.VideoTaskId== taskInfo.Id)
.ExecuteCommandAsync();
await videoQuestionKonwDB.AsDeleteable()
.Where(s => s.VideoTaskId == taskInfo.Id)
.ExecuteCommandAsync();
await videoQuestionDB.InsertRangeAsync(insertData.Adapt<VideoQuestion[]>());
await videoQuestionKonwDB.InsertRangeAsync(insertQuestionKonw);
return null;
}
/// <summary>
/// 获取知识点
/// </summary>
/// <param name="task">任务id</param>
/// <returns></returns>
public async Task<TaskRes> GetKnow(string task)
{
var taskId = long.Parse(task);
var taskInfo = await videoTaskDB.AsQueryable()
.Where(s => s.Id == taskId)
.FirstAsync();
var subject = taskInfo.Subject.ToString();
var Course_Id = 27;
switch (taskInfo.Type)//处理不同任务类型的知识点树
{
case TaskTypeEnum._中职视频分段:
Course_Id = 51;
break;
case TaskTypeEnum._视频分段:
default:
Course_Id = 27;
break;
}
var captionsArr = JsonSerializer.Deserialize<SenseVoiceRes[]>(taskInfo.Captions);
//处理视频授课章节
var sections = await GetSections(taskInfo, Course_Id);
//AI优化字幕
captionsArr = await OptimizeSubtitles(taskInfo, captionsArr, sections);
//合并字幕
var captions = ExpandFunction.GetSpeakerCaptions(captionsArr);
var maxVideoTime = captions?.TimeBase?.LastOrDefault()?.End ?? 0;
var questionRes = new List<VideoKnowRes>();
while (true)
{
questionRes = new List<VideoKnowRes>();
//视频字幕分析
await Analytics(questionRes, taskInfo, captionsArr, sections);
if (questionRes.Count == 0) continue;
//处理分段 知识点
var insertData = await GetVideoKnow(questionRes, taskInfo, sections, Course_Id);
//校验结果质量
var checkRes = await VerifySpanQuality(questionRes, taskInfo, captions, sections, Course_Id);
if (checkRes != null && checkRes.Score >= 80)
{
//写入知识点
await videoKonwPointDB.DeleteAsync(s => s.VideoTaskId == taskInfo.Id);
await videoKonwPointDB.InsertRangeAsync(insertData);
break;
}
else
{
Console.WriteLine(DateTime.Now + $"=>{task} 得分过低/分段长度不匹配 得分{checkRes?.Score} ");
Console.WriteLine(checkRes.Evaluation);
Console.WriteLine();
}
if (questionRes.Any(s => s.KeepTime < 30))
{
Console.WriteLine(DateTime.Now + "=>视频分段过短!! 重新进行AI分析");
continue;
}
}
await RedisExpand.Redis
.HMSetAsync(RedisExpandKey.Task(task), "VideoKnows", questionRes);
if (taskInfo.VideoType == AttachmentsInfoType.Review)
await AnalysisVideoQuestions(taskInfo);
RedisExpand.InsertChannel(RedisChannelEnum.EndTask, task);
return null;
}
}
}

View File

@ -7,6 +7,7 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using VideoAnalysisCore.Model.Dto;
using VideoAnalysisCore.Model.Enum;
namespace VideoAnalysisCore.AICore.GPT.Dto
@ -46,6 +47,8 @@ namespace VideoAnalysisCore.AICore.GPT.Dto
/// 主题
/// </summary>
public virtual string? Theme { get; set; }
public virtual long? StageId { get; set; }
public virtual VideoQuestionShowDto[]? QuestionArr { get; set; }
/// <summary>
/// 知识点
/// </summary>

View File

@ -64,7 +64,8 @@ namespace VideoAnalysisCore.Common
AccessKeyId = AppCommon.Config.AliyunOSS.AccessKeyId,
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
AccessKeySecret = AppCommon.Config.AliyunOSS.AccessKeySecret,
Endpoint = AppCommon.Config.AliyunOSS.Endpoint
Endpoint = AppCommon.Config.AliyunOSS.Endpoint,
Region= AppCommon.Config.AliyunOSS.Region,
};// 创建ClientConfiguration实例按照您的需要修改默认参数。
var conf = new ClientConfiguration();
// 设置v4签名。
@ -79,7 +80,7 @@ namespace VideoAnalysisCore.Common
/// 上传文件
/// </summary>
/// <param name="oss"></param>
/// <param name="fileArr">视频体片段</param>
/// <param name="fileArr">视频体片段</param>
public static void AddVideoQuestionUrl(this OssClient oss, List<VideoQuestionOSSDto> fileArr )
{
var cached = new HashSet<string>();

View File

@ -11,4 +11,8 @@ namespace VideoAnalysisCore.Model.Dto
public string FilePath { get; set; }
public string KnowPointId { get; set; }
}
public class VideoQuestionShowDto : VideoQuestion
{
public VideoQuestionKonw[] KonwArr { get; set; }
}
}

View File

@ -20,7 +20,7 @@ namespace VideoAnalysisCore.Model
/// <summary>
/// id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
/// <summary>
/// 视频任务id

View File

@ -35,6 +35,11 @@ namespace VideoAnalysisCore.Model
/// </summary>
public string KnowPointId { get; set; }
/// <summary>
/// 视频试题ID
/// <para>隶属于<see cref="VideoQuestion.Id"/></para>
/// </summary>
public long VideoQuestionId { get; set; }
/// <summary>
/// 视频阶段id
/// <para>隶属于<see cref="VideoKonwPoint.StageId"/></para>
/// </summary>