From d8224e68d202ce140da54f52ba275701ad41f6fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com>
Date: Wed, 30 Oct 2024 17:38:34 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?=
=?UTF-8?q?=E4=BB=B6=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitattributes | 63 +++
.gitignore | 367 ++++++++++++++++++
Learn.VideoAnalysis.sln | 31 ++
VideoAnalysis/Components/App.razor | 24 ++
VideoAnalysis/Components/Error.razor | 36 ++
.../Components/Layouts/BasicLayout.razor | 32 ++
.../Components/Layouts/BasicLayout.razor.cs | 44 +++
.../Components/Pages/EvaluationProject.razor | 75 ++++
.../Pages/EvaluationProject.razor.cs | 70 ++++
.../Pages/EvaluationProject.razor.css | 3 +
VideoAnalysis/Components/Resources/I18n.cs | 6 +
VideoAnalysis/Components/Resources/I18n.resx | 129 ++++++
.../Components/Resources/I18n.zh-CN.resx | 129 ++++++
VideoAnalysis/Components/Routes.razor | 10 +
VideoAnalysis/Components/_Imports.razor | 16 +
VideoAnalysis/Controllers/ApiController.cs | 87 +++++
VideoAnalysis/Controllers/Dto/ApiDto.cs | 34 ++
VideoAnalysis/GlobalUsings.cs | 2 +
VideoAnalysis/Learn.VideoAnalysis.csproj | 34 ++
VideoAnalysis/Learn.VideoAnalysis.http | 6 +
VideoAnalysis/Program.cs | 109 ++++++
VideoAnalysis/Properties/launchSettings.json | 31 ++
VideoAnalysis/appsettings.Development.json | 8 +
VideoAnalysis/appsettings.json | 31 ++
VideoAnalysis/wwwroot/app.css | 54 +++
VideoAnalysisCore/AICore/ChatGPT/BserGPT.cs | 19 +
.../AICore/ChatGPT/KIMI/KIMI_GPT.cs | 58 +++
.../AICore/ChatGPT/KIMI/MoonshotClient.cs | 244 ++++++++++++
.../AICore/ChatGPT/KIMI/MoonshotModel.cs | 277 +++++++++++++
.../AICore/FFMPGE/FFMPGEHandle.cs | 78 ++++
.../AICore/SherpaOnnx/Speaker.cs | 100 +++++
.../AICore/SherpaOnnx/WaveHeader.cs | 167 ++++++++
.../AICore/Whisper/WhisperDto.cs | 35 ++
.../AICore/Whisper/WhisperHandle.cs | 122 ++++++
VideoAnalysisCore/Common/AppCommon.cs | 281 ++++++++++++++
VideoAnalysisCore/Common/DownloadFile.cs | 94 +++++
VideoAnalysisCore/Common/RedisExpand.cs | 169 ++++++++
VideoAnalysisCore/Common/Repository.cs | 19 +
VideoAnalysisCore/Common/SqlSugarExpand.cs | 154 ++++++++
VideoAnalysisCore/Enum/RedisChannelEnum.cs | 33 ++
VideoAnalysisCore/Enum/TimeBaseEnum.cs | 29 ++
.../Model/CourseGradingCriteria.cs | 53 +++
.../Model/Dto/SpeakerCaptionsDto.cs | 49 +++
VideoAnalysisCore/Model/VideoTask.cs | 83 ++++
VideoAnalysisCore/VideoAnalysisCore.csproj | 59 +++
45 files changed, 3554 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 Learn.VideoAnalysis.sln
create mode 100644 VideoAnalysis/Components/App.razor
create mode 100644 VideoAnalysis/Components/Error.razor
create mode 100644 VideoAnalysis/Components/Layouts/BasicLayout.razor
create mode 100644 VideoAnalysis/Components/Layouts/BasicLayout.razor.cs
create mode 100644 VideoAnalysis/Components/Pages/EvaluationProject.razor
create mode 100644 VideoAnalysis/Components/Pages/EvaluationProject.razor.cs
create mode 100644 VideoAnalysis/Components/Pages/EvaluationProject.razor.css
create mode 100644 VideoAnalysis/Components/Resources/I18n.cs
create mode 100644 VideoAnalysis/Components/Resources/I18n.resx
create mode 100644 VideoAnalysis/Components/Resources/I18n.zh-CN.resx
create mode 100644 VideoAnalysis/Components/Routes.razor
create mode 100644 VideoAnalysis/Components/_Imports.razor
create mode 100644 VideoAnalysis/Controllers/ApiController.cs
create mode 100644 VideoAnalysis/Controllers/Dto/ApiDto.cs
create mode 100644 VideoAnalysis/GlobalUsings.cs
create mode 100644 VideoAnalysis/Learn.VideoAnalysis.csproj
create mode 100644 VideoAnalysis/Learn.VideoAnalysis.http
create mode 100644 VideoAnalysis/Program.cs
create mode 100644 VideoAnalysis/Properties/launchSettings.json
create mode 100644 VideoAnalysis/appsettings.Development.json
create mode 100644 VideoAnalysis/appsettings.json
create mode 100644 VideoAnalysis/wwwroot/app.css
create mode 100644 VideoAnalysisCore/AICore/ChatGPT/BserGPT.cs
create mode 100644 VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs
create mode 100644 VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs
create mode 100644 VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotModel.cs
create mode 100644 VideoAnalysisCore/AICore/FFMPGE/FFMPGEHandle.cs
create mode 100644 VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs
create mode 100644 VideoAnalysisCore/AICore/SherpaOnnx/WaveHeader.cs
create mode 100644 VideoAnalysisCore/AICore/Whisper/WhisperDto.cs
create mode 100644 VideoAnalysisCore/AICore/Whisper/WhisperHandle.cs
create mode 100644 VideoAnalysisCore/Common/AppCommon.cs
create mode 100644 VideoAnalysisCore/Common/DownloadFile.cs
create mode 100644 VideoAnalysisCore/Common/RedisExpand.cs
create mode 100644 VideoAnalysisCore/Common/Repository.cs
create mode 100644 VideoAnalysisCore/Common/SqlSugarExpand.cs
create mode 100644 VideoAnalysisCore/Enum/RedisChannelEnum.cs
create mode 100644 VideoAnalysisCore/Enum/TimeBaseEnum.cs
create mode 100644 VideoAnalysisCore/Model/CourseGradingCriteria.cs
create mode 100644 VideoAnalysisCore/Model/Dto/SpeakerCaptionsDto.cs
create mode 100644 VideoAnalysisCore/Model/VideoTask.cs
create mode 100644 VideoAnalysisCore/VideoAnalysisCore.csproj
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6c2149d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,367 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+
+VideoAnalysis/AICore/_Static/
+VideoAnalysisCore/AICore/_Static/
\ No newline at end of file
diff --git a/Learn.VideoAnalysis.sln b/Learn.VideoAnalysis.sln
new file mode 100644
index 0000000..713ed78
--- /dev/null
+++ b/Learn.VideoAnalysis.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34330.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Learn.VideoAnalysis", "VideoAnalysis\Learn.VideoAnalysis.csproj", "{0CE488B7-F766-4083-86A6-6848A300E44E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoAnalysisCore", "VideoAnalysisCore\VideoAnalysisCore.csproj", "{69F4243A-B22E-431B-8F0B-ECD8729B8665}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {0CE488B7-F766-4083-86A6-6848A300E44E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0CE488B7-F766-4083-86A6-6848A300E44E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0CE488B7-F766-4083-86A6-6848A300E44E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0CE488B7-F766-4083-86A6-6848A300E44E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69F4243A-B22E-431B-8F0B-ECD8729B8665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69F4243A-B22E-431B-8F0B-ECD8729B8665}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69F4243A-B22E-431B-8F0B-ECD8729B8665}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69F4243A-B22E-431B-8F0B-ECD8729B8665}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {37C1B9DE-28AE-46BC-9975-D1201D7F962A}
+ EndGlobalSection
+EndGlobal
diff --git a/VideoAnalysis/Components/App.razor b/VideoAnalysis/Components/App.razor
new file mode 100644
index 0000000..d07604b
--- /dev/null
+++ b/VideoAnalysis/Components/App.razor
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VideoAnalysis/Components/Error.razor b/VideoAnalysis/Components/Error.razor
new file mode 100644
index 0000000..576cc2d
--- /dev/null
+++ b/VideoAnalysis/Components/Error.razor
@@ -0,0 +1,36 @@
+@page "/Error"
+@using System.Diagnostics
+
+Error
+
+Error.
+An error occurred while processing your request.
+
+@if (ShowRequestId)
+{
+
+ Request ID: @RequestId
+
+}
+
+Development Mode
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+
+
+@code{
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+
+ private string? RequestId { get; set; }
+ private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+ protected override void OnInitialized() =>
+ RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+}
diff --git a/VideoAnalysis/Components/Layouts/BasicLayout.razor b/VideoAnalysis/Components/Layouts/BasicLayout.razor
new file mode 100644
index 0000000..2c28310
--- /dev/null
+++ b/VideoAnalysis/Components/Layouts/BasicLayout.razor
@@ -0,0 +1,32 @@
+@namespace VideoAnalysisRazor.Layouts
+@inherits LayoutComponentBase
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs b/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs
new file mode 100644
index 0000000..6387c3c
--- /dev/null
+++ b/VideoAnalysis/Components/Layouts/BasicLayout.razor.cs
@@ -0,0 +1,44 @@
+using AntDesign.Extensions.Localization;
+using AntDesign.ProLayout;
+using Microsoft.AspNetCore.Components;
+using System.Globalization;
+using System.Net.Http.Json;
+
+namespace VideoAnalysisRazor.Layouts
+{
+ public partial class BasicLayout : LayoutComponentBase, IDisposable
+ {
+ private MenuDataItem[] _menuData;
+
+ [Inject] private ReuseTabsService TabService { get; set; }
+
+ bool collapsed;
+ void Toggle()
+ {
+ collapsed = !collapsed;
+ }
+ protected override async Task OnInitializedAsync()
+ {
+ _menuData = new[] {
+ new MenuDataItem
+ {
+ Path = "/",
+ Name = "首页",
+ Key = "hone",
+ Icon = "home",
+ }
+ };
+ }
+
+ void Reload()
+ {
+ TabService.ReloadPage();
+ }
+
+ public void Dispose()
+ {
+
+ }
+
+ }
+}
diff --git a/VideoAnalysis/Components/Pages/EvaluationProject.razor b/VideoAnalysis/Components/Pages/EvaluationProject.razor
new file mode 100644
index 0000000..09d1f3b
--- /dev/null
+++ b/VideoAnalysis/Components/Pages/EvaluationProject.razor
@@ -0,0 +1,75 @@
+@page "/"
+@using AntDesign
+@using AntDesign.TableModels
+@using System.ComponentModel.DataAnnotations
+@using SqlSugar
+@using VideoAnalysisCore.Model
+
+
+
+
+ StartEdit(default)">新增
+ @* Delete *@
+
+
+
+
+
+ StartEdit(row)">修改
+ Delete(row)">
+ 删除
+
+
+
+
+
+@inject ModalService ModalService
+@code
+{
+ ///
+ /// 新增或者修改
+ ///
+ ///
+ void StartEdit(CourseGradingCriteria row)
+ {
+ var data = row == null ? new() : row;
+ IForm? form = default;
+ ModalRef modalRef = default;
+ modalRef = ModalService.CreateModal(new()
+ {
+ Title = data.Id > 0 ? "修改" : "新增",
+ Content =
+ @
+ ,
+ OkText = "确定",
+ CancelText = "取消",
+ OnOk = async (e) =>
+ {
+ if (!form.Validate())
+ return;
+ // save db and refresh
+ modalRef.SetConfirmLoading(true);
+
+ if (data.Id > 0)
+ await criteria.UpdateAsync(data);
+ else
+ data.Id = await criteria.InsertReturnBigIdentityAsync(data);
+ //弹窗按钮 show
+ modalRef.SetConfirmLoading(false);
+
+ await modalRef.CloseAsync();
+ _table.ReloadData();
+ StateHasChanged();
+ },
+ OnCancel = async (e) =>
+ {
+ if (form.IsModified && (!await Comfirm("表格已经更新,您确定要退出吗?")))
+ return;
+ await modalRef.CloseAsync();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/VideoAnalysis/Components/Pages/EvaluationProject.razor.cs b/VideoAnalysis/Components/Pages/EvaluationProject.razor.cs
new file mode 100644
index 0000000..0290b01
--- /dev/null
+++ b/VideoAnalysis/Components/Pages/EvaluationProject.razor.cs
@@ -0,0 +1,70 @@
+using AntDesign.TableModels;
+using Microsoft.AspNetCore.Components;
+using SqlSugar;
+using System.Linq.Expressions;
+using VideoAnalysisCore.Common;
+using VideoAnalysisCore.Model;
+
+namespace Learn.VideoAnalysis.Components.Pages
+{
+ public partial class EvaluationProject : ComponentBase
+ {
+
+ [Inject] private ConfirmService ComfirmService { get; set; } = default!;
+ [Inject] private Repository criteria { get; set; } = default!;
+
+
+ IEnumerable _selectedRows = [];
+ ITable _table;
+
+ List _dataSource = null;
+ RefAsync _total = 0;
+
+ bool tableLoading = false;
+
+ ///
+ /// 分页 查询 筛选 时
+ ///
+ ///
+ async void OnChange(QueryModel query)
+ {
+ tableLoading = true;
+ Expression> where = null;
+ if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
+ {
+ where = query.GetFilterExpression();
+ }
+ _dataSource = await criteria.AsQueryable()
+ .WhereIF(where != null, where)
+ .ToPageListAsync(query.PageIndex - 1, query.PageSize, _total);
+ tableLoading = false;
+ StateHasChanged();
+
+ }
+ ///
+ /// 删除行
+ ///
+ ///
+ ///
+ async Task Delete(CourseGradingCriteria row)
+ {
+ if (!await Comfirm($"确定要删除这条数据吗? [{row.NamePrompt}]?"))
+ return;
+ await criteria.DeleteByIdAsync(row.Id);
+ _table.ReloadData();
+ }
+ ///
+ /// 初始化
+ ///
+ protected override void OnInitialized()
+ {
+ }
+
+ private async Task Comfirm(string message)
+ {
+ return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
+ }
+
+ }
+
+}
diff --git a/VideoAnalysis/Components/Pages/EvaluationProject.razor.css b/VideoAnalysis/Components/Pages/EvaluationProject.razor.css
new file mode 100644
index 0000000..aca1ed1
--- /dev/null
+++ b/VideoAnalysis/Components/Pages/EvaluationProject.razor.css
@@ -0,0 +1,3 @@
+input[aria-hidden="true"] {
+ display: none !important;
+}
diff --git a/VideoAnalysis/Components/Resources/I18n.cs b/VideoAnalysis/Components/Resources/I18n.cs
new file mode 100644
index 0000000..b0fb331
--- /dev/null
+++ b/VideoAnalysis/Components/Resources/I18n.cs
@@ -0,0 +1,6 @@
+namespace VideoAnalysisRazor.Resources;
+
+
+internal class I18n
+{
+}
\ No newline at end of file
diff --git a/VideoAnalysis/Components/Resources/I18n.resx b/VideoAnalysis/Components/Resources/I18n.resx
new file mode 100644
index 0000000..5a932a1
--- /dev/null
+++ b/VideoAnalysis/Components/Resources/I18n.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Account Center
+
+
+ Logout
+
+
+ Settings
+
+
\ No newline at end of file
diff --git a/VideoAnalysis/Components/Resources/I18n.zh-CN.resx b/VideoAnalysis/Components/Resources/I18n.zh-CN.resx
new file mode 100644
index 0000000..d938141
--- /dev/null
+++ b/VideoAnalysis/Components/Resources/I18n.zh-CN.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 个人中心
+
+
+ 退出登录
+
+
+ 个人设置
+
+
\ No newline at end of file
diff --git a/VideoAnalysis/Components/Routes.razor b/VideoAnalysis/Components/Routes.razor
new file mode 100644
index 0000000..919a0de
--- /dev/null
+++ b/VideoAnalysis/Components/Routes.razor
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VideoAnalysis/Components/_Imports.razor b/VideoAnalysis/Components/_Imports.razor
new file mode 100644
index 0000000..83cab43
--- /dev/null
+++ b/VideoAnalysis/Components/_Imports.razor
@@ -0,0 +1,16 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using AntDesign
+@using AntDesign.Charts
+@using AntDesign.ProLayout
+@using AntDesign.Extensions.Localization
+
+@using Learn.VideoAnalysis
+@using VideoAnalysisRazor
+@using Learn.VideoAnalysis.Components
diff --git a/VideoAnalysis/Controllers/ApiController.cs b/VideoAnalysis/Controllers/ApiController.cs
new file mode 100644
index 0000000..1e30681
--- /dev/null
+++ b/VideoAnalysis/Controllers/ApiController.cs
@@ -0,0 +1,87 @@
+
+using VideoAnalysisCore.Common;
+using Learn.VideoAnalysis.Controllers.Dto;
+using Microsoft.AspNetCore.Mvc;
+using System.Reflection;
+using VideoAnalysisCore.Enum;
+using VideoAnalysisCore.Model;
+using VideoAnalysisCore.AICore.FFMPGE;
+
+namespace Learn.VideoAnalysis.Controllers
+{
+ [ApiController]
+ [Route("[controller]/[action]")]
+ public class ApiController : ControllerBase
+ {
+ private readonly ILogger _logger;
+ public ApiController(ILogger logger)
+ {
+ _logger = logger;
+ }
+ ///
+ /// Ƶתwav_16k
+ ///
+ /// ļ·
+ ///
+ [HttpGet(Name = "Audio2WAV16K")]
+ public async Task Audio2WAV16K(string filePath)
+ {
+ await FFMPGEHandle.Audio2WAV16KAsync(filePath);
+ return Ok();
+ }
+
+ private string GetClientIpAddress()
+ {
+ // X-Forwarded-For ͷ
+ if (HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
+ && !string.IsNullOrEmpty(HttpContext.Request.Headers["X-Forwarded-For"]))
+ return HttpContext.Request.Headers["X-Forwarded-For"].ToString();
+ if (HttpContext.Connection.RemoteIpAddress != null)
+ return HttpContext.Connection.RemoteIpAddress.ToString();
+ throw new Exception("δܻȡͻipַ");
+ }
+
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPost(Name = "TestInsertChannel")]
+ public IActionResult TestInsertChannel(int @enum=1, string msg= "test_0001")
+ {
+ RedisExpand.InsertChannel(@enum.ToEnum().Value
+ , msg);
+ return Ok();
+ }
+ ///
+ /// Ƶ
+ ///
+ ///
+ ///
+ [HttpPost(Name = "VideoAnalysis")]
+ public IActionResult VideoAnalysis(VideoAnalysisReq req)
+ {
+ if (!ModelState.IsValid) return BadRequest(ModelState);
+
+ // ԶӳԵϣ
+ var task = new VideoTask()
+ {
+ ComeFrom = GetClientIpAddress(),
+ MediaUrl = req.MediaUrl,
+ ApiToken = req.ApiToken,
+ Tag = req.Tag,
+ CallBackUrl = req.CallBackUrl,
+ };
+ var hashEntries = task.GetType()
+ .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);
+ return Ok();
+ }
+ }
+}
diff --git a/VideoAnalysis/Controllers/Dto/ApiDto.cs b/VideoAnalysis/Controllers/Dto/ApiDto.cs
new file mode 100644
index 0000000..6938d22
--- /dev/null
+++ b/VideoAnalysis/Controllers/Dto/ApiDto.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Learn.VideoAnalysis.Controllers.Dto
+{
+ ///
+ /// 视频处理 请求
+ ///
+ public class VideoAnalysisReq
+ {
+ ///
+ /// 媒体路径
+ ///
+ [Required(ErrorMessage = "资源URL是必填项")]
+ [Url(ErrorMessage = "请输入有效的 URL")]
+ public string MediaUrl { get; set; } = string.Empty;
+ ///
+ /// ApiKey
+ ///
+ [Required(ErrorMessage = "接口Token是必填项")]
+ public string ApiToken { get; set; } = string.Empty;
+ ///
+ /// 自定义值 任务完成后附带通知
+ ///
+ public string Tag { get; set; } = string.Empty;
+ ///
+ ///回调Api地址
+ ///
+ [Required(ErrorMessage = "回调Api地址是必填项")]
+ [Url(ErrorMessage = "请输入有效的 URL")]
+ public string CallBackUrl { get; set; } = string.Empty;
+
+
+ }
+}
diff --git a/VideoAnalysis/GlobalUsings.cs b/VideoAnalysis/GlobalUsings.cs
new file mode 100644
index 0000000..a967599
--- /dev/null
+++ b/VideoAnalysis/GlobalUsings.cs
@@ -0,0 +1,2 @@
+global using VideoAnalysisRazor.Resources;
+global using AntDesign;
\ No newline at end of file
diff --git a/VideoAnalysis/Learn.VideoAnalysis.csproj b/VideoAnalysis/Learn.VideoAnalysis.csproj
new file mode 100644
index 0000000..11594ac
--- /dev/null
+++ b/VideoAnalysis/Learn.VideoAnalysis.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VideoAnalysis/Learn.VideoAnalysis.http b/VideoAnalysis/Learn.VideoAnalysis.http
new file mode 100644
index 0000000..c1e6a9e
--- /dev/null
+++ b/VideoAnalysis/Learn.VideoAnalysis.http
@@ -0,0 +1,6 @@
+@Learn.VideoAnalysis_HostAddress = http://localhost:5238
+
+GET {{Learn.VideoAnalysis_HostAddress}}/Audio2Text?modelType=LargeV3
+Accept: application/json
+
+###
diff --git a/VideoAnalysis/Program.cs b/VideoAnalysis/Program.cs
new file mode 100644
index 0000000..19d389c
--- /dev/null
+++ b/VideoAnalysis/Program.cs
@@ -0,0 +1,109 @@
+using VideoAnalysisCore.Common;
+using VideoAnalysisRazor;
+using Learn.VideoAnalysis.Components;
+using Microsoft.OpenApi.Models;
+using AntDesign.ProLayout;
+using VideoAnalysisCore.AICore.ChatGPT;
+using VideoAnalysisCore.AICore.ChatGPT.KIMI;
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using SqlSugar;
+
+namespace Learn.VideoAnalysis
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Add services to the container.
+ builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+ //.AddInteractiveWebAssemblyComponents();
+
+ builder.Services.AddHttpContextAccessor();
+
+
+ builder.Services.AddControllers();
+
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen(c =>
+ {
+ c.SwaggerDoc("v1", new OpenApiInfo
+ {
+ Title = "Learn.VideoAnalysis",
+ Version = "v1",
+ Description = "ѧƵƽ̨v1"
+ });
+ var file = Path.Combine(AppContext.BaseDirectory, "Learn.VideoAnalysis.xml"); // xmlĵ·
+ c.IncludeXmlComments(file, true); // true : ʾע
+ c.OrderActionsBy(o => o.RelativePath); // actionƽжͿԿЧˡ
+ });
+
+ // appsetting
+ builder.Configuration.GetSection("AppConfig").Bind(AppCommon.Config);
+
+ //ʼ
+ Speaker.Init();
+ RedisExpand.Init();
+
+
+ builder.Services.AddScoped(sp =>
+ {
+ var httpContext = sp.GetRequiredService().HttpContext;
+ if (httpContext != null)
+ {
+ return new HttpClient
+ {
+ BaseAddress = new Uri(httpContext.Request.Scheme + "://" + httpContext.Request.Host)
+ };
+ }
+ return new HttpClient();
+ });
+
+ //VideoAnalysisRazor.Program.AddClientServices(builder.Services);
+
+ builder.Services.AddAntDesign();
+ builder.Services.InitSqlSugar();
+
+
+ builder.Services.Configure(builder.Configuration.GetSection("ProSettings"));
+
+ builder.Services.AddHttpClient();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+
+
+
+
+ var app = builder.Build();
+
+ AppCommon.Services = app.Services;
+
+ // Configure the HTTP request pipeline.
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseSwagger();
+ app.UseSwaggerUI();
+ app.UseExceptionHandler("/Error");
+ }
+
+ app.UseAuthorization();
+
+ app.UseStaticFiles();
+ app.UseAntiforgery();
+
+ app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
+ //.AddInteractiveWebAssemblyRenderMode()
+ //.AddAdditionalAssemblies(typeof(VideoAnalysisRazor._Imports).Assembly);
+
+ app.MapControllers();
+
+ SqlSugarExpand.InitDB();
+
+ app.Run();
+
+ }
+ }
+}
diff --git a/VideoAnalysis/Properties/launchSettings.json b/VideoAnalysis/Properties/launchSettings.json
new file mode 100644
index 0000000..12a2a37
--- /dev/null
+++ b/VideoAnalysis/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:9624",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "http:5238": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "",
+ "applicationUrl": "http://localhost:5238",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/VideoAnalysis/appsettings.Development.json b/VideoAnalysis/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/VideoAnalysis/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/VideoAnalysis/appsettings.json b/VideoAnalysis/appsettings.json
new file mode 100644
index 0000000..1e3df8d
--- /dev/null
+++ b/VideoAnalysis/appsettings.json
@@ -0,0 +1,31 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "AppConfig": {
+ "Redis": {
+ "ConnectionString": "127.0.0.1:6379,password=Woshiren123,defaultDatabase=10"
+ },
+ "Whisper": {
+ "ModelName": "ggml-small.bin"
+ },
+ "FFmpeg": {
+ " TimeSlice": 600
+ },
+ "ChatGpt": {
+ "KIMI": {
+ "Host": "https://api.moonshot.cn",
+ "ApiKey": "sk-CNYJdRHgJsgtgw1Q8GhQ5ayXuFPVLSk5bduOF4l2FMvI5lUo"
+ }
+ },
+ "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,
+ }
+ }
+}
diff --git a/VideoAnalysis/wwwroot/app.css b/VideoAnalysis/wwwroot/app.css
new file mode 100644
index 0000000..e05030d
--- /dev/null
+++ b/VideoAnalysis/wwwroot/app.css
@@ -0,0 +1,54 @@
+html, body {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+.ant-modal div[aria-hidden="true"] {
+ display: none !important;
+}
+a, .btn-link {
+ color: #006bb7;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
+ box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
+}
+
+.content {
+ padding-top: 1.1rem;
+}
+
+h1:focus {
+ outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid #26b050;
+}
+
+.invalid {
+ outline: 1px solid #e50000;
+}
+
+.validation-message {
+ color: #e50000;
+}
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::after {
+ content: "An error has occurred."
+ }
+
+.darker-border-checkbox.form-check-input {
+ border-color: #929292;
+}
diff --git a/VideoAnalysisCore/AICore/ChatGPT/BserGPT.cs b/VideoAnalysisCore/AICore/ChatGPT/BserGPT.cs
new file mode 100644
index 0000000..7fe6c27
--- /dev/null
+++ b/VideoAnalysisCore/AICore/ChatGPT/BserGPT.cs
@@ -0,0 +1,19 @@
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.Common;
+using Whisper.net;
+
+namespace VideoAnalysisCore.AICore.ChatGPT
+{
+ ///
+ /// GPT 接口
+ ///
+ public interface IBserGPT
+ {
+ ///
+ /// 访问GPT
+ ///
+ /// 任务id
+ ///
+ public Task CallGPT(string task);
+ }
+}
diff --git a/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs b/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs
new file mode 100644
index 0000000..6ec29a2
--- /dev/null
+++ b/VideoAnalysisCore/AICore/ChatGPT/KIMI/KIMI_GPT.cs
@@ -0,0 +1,58 @@
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.Common;
+using System.Threading.Tasks;
+using Whisper.net;
+using VideoAnalysisCore.AICore.ChatGPT;
+using System.Text.Json;
+
+namespace VideoAnalysisCore.AICore.ChatGPT.KIMI
+{
+ ///
+ /// kimi 文本模型
+ ///
+ public class KIMI_GPT : IBserGPT
+ {
+ private readonly MoonshotClient moonshotClient;
+ ///
+ /// 初始化
+ ///
+ ///
+ ///
+ public KIMI_GPT(MoonshotClient moonshotClient)
+ {
+ MoonshotClient.Host = AppCommon.Config.ChatGpt.KIMI.Host;
+ MoonshotClient.ApiKey = AppCommon.Config.ChatGpt.KIMI.ApiKey;
+
+ this.moonshotClient = moonshotClient;
+ }
+ ///
+ /// 访问GPT
+ ///
+ /// 任务id
+ ///
+ public async Task CallGPT(string task)
+ {
+ var captions = ExpandFunction.GetSpeakerCaptions(task);
+
+ var modelsResp = await moonshotClient.ListModels();
+ if (modelsResp is null || modelsResp.data.Count == 0)
+ throw new Exception("未获取KIMI模型类型");
+ var chatRep = new ChatReq
+ {
+ max_tokens = 1048 * 16,
+ temperature = 0.3,
+ frequency_penalty = 0,
+ presence_penalty = 0,
+ model = modelsResp.data.First().id,
+ messages = new List(){
+ new MessagesItem("以下是和一个AI助手的对话。这位助手乐于助人,富有创造力,聪明,而且非常友好。","system"),
+ new MessagesItem("美国的人类预期寿命是多少?")
+ }
+ };
+ Console.WriteLine($"Chat 请求参数: {JsonSerializer.Serialize(chatRep)}");
+ var chatResp = await moonshotClient.Chat(chatRep);
+ var chatRespBody = await chatResp.Content.ReadAsStringAsync();
+ Console.WriteLine($"Chat 成功返回值: {chatRespBody}");
+ }
+ }
+}
diff --git a/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs b/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs
new file mode 100644
index 0000000..d08c622
--- /dev/null
+++ b/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotClient.cs
@@ -0,0 +1,244 @@
+using VideoAnalysisCore.Common;
+using System.Net.Http.Headers;
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+using System.Net.Http;
+
+namespace VideoAnalysisCore.AICore.ChatGPT.KIMI
+{
+ ///
+ /// https://platform.moonshot.cn/docs/api-reference
+ ///
+ public class MoonshotClient
+ {
+ private readonly ILogger _logger;
+
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ public MoonshotClient(ILogger logger, IHttpClientFactory httpClientFactory)
+ {
+ _logger = logger;
+ _httpClientFactory = httpClientFactory;
+ }
+
+ ///
+ /// list models
+ ///
+ ///
+ public async Task ListModels()
+ {
+ var response = await GetAsync("/v1/models");
+ return await ParseResp(response);
+ }
+
+ ///
+ /// Chat
+ ///
+ ///
+ /// Return HttpResponseMessage for SSE
+ public async Task Chat(string requestBody)
+ {
+ return await PostJsonStreamAsync("/v1/chat/completions", requestBody);
+ }
+
+ ///
+ /// Chat
+ ///
+ ///
+ /// Return HttpResponseMessage for SSE
+ public async Task Chat(ChatReq chatReq)
+ {
+ var requestBody = System.Text.Json.JsonSerializer.Serialize(chatReq);
+ return await PostJsonStreamAsync("/v1/chat/completions", requestBody);
+ }
+
+ ///
+ /// Get as timate token count
+ ///
+ public async Task GetAsTiMateTokenCount(string chatReqText)
+ {
+ var response = await PostJsonAsync("/v1/tokenizers/estimate-token-count", chatReqText);
+ var responseText = await response.Content.ReadAsStringAsync();
+ if (response.IsSuccessStatusCode)
+ {
+ var responseObj = JToken.Parse(responseText);
+ return responseObj?["data"]?["total_tokens"]?.ToObject();
+ }
+ var error = System.Text.Json.JsonSerializer.Deserialize(responseText);
+ _logger.LogError($"{error?.error?.type}: {error?.error?.message}");
+ throw new Exception($"{error?.error.type}: {error?.error.message}");
+ }
+
+
+ ///
+ /// Get as timate token count
+ ///
+ ///
+ ///
+ public async Task GetAsTiMateTokenCount(ChatReq chatReq)
+ {
+ var chatReqText =System.Text.Json.JsonSerializer.Serialize(chatReq);
+ return await GetAsTiMateTokenCount(chatReqText);
+ }
+
+
+ ///
+ /// List files
+ ///
+ public virtual async Task ListFiles()
+ {
+ var response = await GetAsync("/v1/files");
+ return await ParseResp(response);
+ }
+
+
+
+
+
+ ///
+ /// Upload file
+ ///
+ public virtual async Task UploadFile(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException($"{filePath} not found");
+ }
+ var client = _httpClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{Host}/v1/files");
+ var content = new MultipartFormDataContent
+ {
+ { new StreamContent(File.OpenRead(filePath)), "file", filePath }
+ };
+ request.Content = content;
+ var response = await client.SendAsync(request);
+ return await ParseResp(response);
+ }
+
+
+
+ ///
+ /// Upload file stream
+ ///
+ public virtual async Task UploadFileStream(Stream stream, string fileName)
+ {
+ var client = _httpClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{Host}/v1/files");
+ var content = new MultipartFormDataContent
+ {
+ { new StreamContent(stream), "file", fileName }
+ };
+ request.Content = content;
+ var response = await client.SendAsync(request);
+ return await ParseResp(response);
+ }
+
+
+ ///
+ /// Get file content
+ ///
+
+ public virtual async Task GetFileContent(string fileId)
+ {
+ var response = await GetAsync($"/v1/files/{fileId}/content");
+ return await ParseResp(response);
+ }
+
+
+ private async Task GetAsync(string path)
+ {
+ var client = _httpClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
+ return await client.GetAsync(Host + path);
+ }
+
+ private async Task PostJsonAsync(string path, string json)
+ {
+ var client = _httpClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
+ return await client.PostAsync(Host + path, new StringContent(json, Encoding.UTF8, "application/json"));
+ }
+
+ private async Task PostJsonStreamAsync(string path, string json)
+ {
+ var client = _httpClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
+ var request = ToHttpRequest(path);
+ request.Content = new StringContent(json, Encoding.UTF8, "application/json");
+ return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
+ }
+
+ private HttpRequestMessage ToHttpRequest(string path)
+ {
+ var request = new HttpRequestMessage();
+ var uriBuilder = new UriBuilder(Host + path);
+ request.RequestUri = uriBuilder.Uri;
+ request.Method = new HttpMethod("POST");
+ request.Headers.Host = new Uri(Host).Host;
+ return request;
+ }
+
+
+
+ ///
+ /// Parse response
+ ///
+ private async Task ParseResp(HttpResponseMessage response)
+ {
+ var responseText = await response.Content.ReadAsStringAsync();
+ if (response.IsSuccessStatusCode)
+ {
+ return System.Text.Json.JsonSerializer.Deserialize(responseText) ?? default;
+ }
+ var error = System.Text.Json.JsonSerializer.Deserialize(responseText);
+ _logger.LogError($"{error?.error.type}: {error?.error.message}");
+ throw new Exception($"{error?.error.type}: {error?.error.message}");
+ }
+
+
+
+ private static string _host = "https://api.moonshot.cn";
+
+ public static string Host
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_host) && !string.IsNullOrEmpty(AppCommon.Config.ChatGpt.KIMI.Host))
+ {
+ _host = AppCommon.Config.ChatGpt.KIMI.Host ?? "";
+ }
+
+ return _host;
+ }
+ set
+ {
+
+ _host = value;
+ }
+ }
+
+
+ private static string _apiKey = "sk_";
+
+ public static string ApiKey
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_apiKey) && !string.IsNullOrEmpty(AppCommon.Config.ChatGpt.KIMI.ApiKey))
+ {
+ _apiKey = AppCommon.Config.ChatGpt.KIMI.ApiKey ?? "";
+ }
+
+ return _apiKey;
+ }
+ set
+ {
+ _apiKey = value;
+ }
+ }
+
+ }
+}
diff --git a/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotModel.cs b/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotModel.cs
new file mode 100644
index 0000000..d045c7d
--- /dev/null
+++ b/VideoAnalysisCore/AICore/ChatGPT/KIMI/MoonshotModel.cs
@@ -0,0 +1,277 @@
+namespace VideoAnalysisCore.AICore.ChatGPT.KIMI
+{
+
+ public class MessagesItem
+ {
+ public MessagesItem()
+ {
+
+ }
+ public MessagesItem(string content, string role = "user")
+ {
+ this.content = content;
+ this.role = role;
+ }
+ ///
+ ///
+ ///
+ public string role { get; set; }
+
+ ///
+ ///
+ ///
+ public string content { get; set; }
+ }
+ ///
+ /// chat请求参数
+ ///
+ public class ChatReq
+ {
+ ///
+ /// 使用的模型
+ /// 例如[ moonshot-v1-8k ]
+ ///
+ public string model { get; set; } = "moonshot-v1-8k";
+ ///
+ /// 消息主体
+ ///
+ public List messages { get; set; }
+ ///
+ /// 使用什么采样温度,介于 0 和 1 之间。较高的值(如 0.7)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定性
+ /// 默认为 0,如果设置,值域须为 [0, 1] 我们推荐 0.3,以达到较合适的效果
+ ///
+ public double temperature { get; set; }
+
+ ///
+ /// 聊天完成时生成的最大 token 数。如果到生成了最大 token 数个结果仍然没有结束,finish reason 会是 "length", 否则会是 "stop"
+ /// 这个值建议按需给个合理的值,如果不给的话,我们会给一个不错的整数比如 1024。特别要注意的是,这个 max_tokens 是指您期待我们返回的 token 长度,而不是输入 + 输出的总长度。比如对一个 moonshot-v1-8k 模型,它的最大输入 + 输出总长度是 8192,当输入 messages 总长度为 4096 的时候,您最多只能设置为 4096,否则我们服务会返回不合法的输入参数( invalid_request_error ),并拒绝回答。如果您希望获得“输入的精确 token 数”,可以使用下面的“计算 Token” API 使用我们的计算器获得计数
+ ///
+ public int? max_tokens { get; set; }
+ ///
+ /// 另一种采样方法,即模型考虑概率质量为 top_p 的标记的结果。因此,0.1 意味着只考虑概率质量最高的 10% 的标记。一般情况下,我们建议改变这一点或温度,但不建议 同时改变
+ ///
+ public float? top_p { get; set; } = 1.0f;
+ ///
+ /// 为每条输入消息生成多少个结果
+ /// 默认为 1,不得大于 5。特别的,当 temperature 非常小靠近 0 的时候,我们只能返回 1 个结果,如果这个时候 n 已经设置并且 > 1,我们的服务会返回不合法的输入参数(invalid_request_error)
+ ///
+ public int? n { get; set; } = 1;
+ ///
+ /// 存在惩罚,介于-2.0到2.0之间的数字。正值会根据新生成的词汇是否出现在文本中来进行惩罚,增加模型讨论新话题的可能性
+ /// 默认为 0
+ ///
+ public float? presence_penalty { get; set; } = 0;
+ ///
+ /// 频率惩罚,介于-2.0到2.0之间的数字。正值会根据新生成的词汇在文本中现有的频率来进行惩罚,减少模型一字不差重复同样话语的可能性
+ /// 默认为 0
+ ///
+ public float? frequency_penalty { get; set; } = 0;
+ ///
+ /// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。最多不能超过 5 个字符串,每个字符串不得超过 32 字节
+ /// 默认 null
+ ///
+ public List? stop { get; set; }
+ ///
+ /// 是否流式返回
+ /// false
+ ///
+ public bool stream { get; set; } = false;
+
+
+ }
+
+
+ public class PermissionItem
+ {
+ ///
+ ///
+ ///
+ public int created { get; set; }
+ ///
+ ///
+ ///
+ public string id { get; set; }
+ ///
+ ///
+ ///
+ public string @object { get; set; }
+ ///
+ ///
+ ///
+ public string allow_create_engine { get; set; }
+ ///
+ ///
+ ///
+ public string allow_sampling { get; set; }
+ ///
+ ///
+ ///
+ public string allow_logprobs { get; set; }
+ ///
+ ///
+ ///
+ public string allow_search_indices { get; set; }
+ ///
+ ///
+ ///
+ public string allow_view { get; set; }
+ ///
+ ///
+ ///
+ public string allow_fine_tuning { get; set; }
+ ///
+ ///
+ ///
+ public string organization { get; set; }
+ ///
+ ///
+ ///
+ public string @group { get; set; }
+ ///
+ ///
+ ///
+ public string is_blocking { get; set; }
+ }
+
+ public class ModelInfo
+ {
+ ///
+ ///
+ ///
+ public int created { get; set; }
+ ///
+ ///
+ ///
+ public string id { get; set; }
+ ///
+ ///
+ ///
+ public string @object { get; set; }
+ ///
+ ///
+ ///
+ public string owned_by { get; set; }
+ ///
+ ///
+ ///
+ public List permission { get; set; }
+ ///
+ ///
+ ///
+ public string root { get; set; }
+ ///
+ ///
+ ///
+ public string parent { get; set; }
+ }
+
+ public class ModelListResp
+ {
+ ///
+ ///
+ ///
+ public string @object { get; set; }
+ ///
+ ///
+ ///
+ public List data { get; set; }
+ }
+
+
+ public class FileListResp
+ {
+ ///
+ ///
+ ///
+ public string @object { get; set; }
+ ///
+ ///
+ ///
+ public List data { get; set; }
+ }
+
+ public class FileContent
+ {
+ ///
+ ///
+ ///
+ public string content { get; set; }
+ ///
+ ///
+ ///
+ public string file_type { get; set; }
+ ///
+ ///
+ ///
+ public string filename { get; set; }
+ ///
+ ///
+ ///
+ public string title { get; set; }
+ ///
+ ///
+ ///
+ public string type { get; set; }
+ }
+
+
+ public class FileItem
+ {
+ ///
+ ///
+ ///
+ public string id { get; set; }
+ ///
+ ///
+ ///
+ public string @object { get; set; }
+ ///
+ ///
+ ///
+ public int bytes { get; set; }
+ ///
+ ///
+ ///
+ public int created_at { get; set; }
+ ///
+ ///
+ ///
+ public string filename { get; set; }
+ ///
+ ///
+ ///
+ public string purpose { get; set; }
+ ///
+ ///
+ ///
+ public string status { get; set; }
+ ///
+ ///
+ ///
+ public string status_details { get; set; }
+ }
+
+
+
+ public class ErrorMsg
+ {
+ ///
+ ///
+ ///
+ public string message { get; set; }
+ ///
+ ///
+ ///
+ public string type { get; set; }
+ }
+
+ public class ErrorResponse
+ {
+ ///
+ ///
+ ///
+ public ErrorMsg error { get; set; }
+ }
+
+
+}
diff --git a/VideoAnalysisCore/AICore/FFMPGE/FFMPGEHandle.cs b/VideoAnalysisCore/AICore/FFMPGE/FFMPGEHandle.cs
new file mode 100644
index 0000000..37bf74f
--- /dev/null
+++ b/VideoAnalysisCore/AICore/FFMPGE/FFMPGEHandle.cs
@@ -0,0 +1,78 @@
+using FFmpeg.NET.Events;
+using FFmpeg.NET;
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.Common;
+using System.Threading.Tasks;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+using System.Xml.Linq;
+
+namespace VideoAnalysisCore.AICore.FFMPGE
+{
+ ///
+ /// Ffmpeg处理程序
+ ///
+ public class FFMPGEHandle
+ {
+ ///
+ ///
+ ///
+ public static string FFmpegPath = Path.Combine(AppCommon.AIModelFile, "ffmpeg.exe");
+ ///
+ /// 音频转码为 wav_16k
+ ///
+ /// 任务id
+ ///
+ public static async Task Audio2WAV16KAsync(string task)
+ {
+ var filePath = RedisExpand.Redis.HGet(RedisExpandKey.Task(task), "LocalMediaPath");
+ if (string.IsNullOrEmpty(filePath))
+ throw new Exception($"任务id[{task}] 无效");
+
+ // 打开输入文件
+ var inputFile = new InputFile(filePath);
+ var outputFile = new OutputFile(Path.Combine(task.LocalPath(), Path.GetFileNameWithoutExtension(filePath) + ".wav"));
+ var ffmpeg = new Engine(FFmpegPath);
+
+ ffmpeg.Progress += OnProgress;
+ ffmpeg.Data += OnData;
+ ffmpeg.Complete += OnComplete;
+ ffmpeg.Error += (sender, e) =>
+ {
+ throw new Exception($"[{e.Input.Name} => {e.Output.Name}]: 错误: {e.Exception.Message}");
+ };
+
+ var conversionOptions = new ConversionOptions
+ {
+ ExtraArguments = "-ar 16000 -ac 1"
+ //+ (AppCommon.AppSetting.FFmpeg.TimeSlice == 0
+ //?string.Empty
+ //: $"-f segment -reset_timestamps 1 -segment_time {AppCommon.AppSetting.FFmpeg.TimeSlice}")
+ };
+
+ var res = await ffmpeg.ConvertAsync(inputFile, outputFile, conversionOptions);
+ //加入下一队列
+ RedisExpand.InsertChannel(Enum.RedisChannelEnum.ParsingCaptions, task);
+
+ }
+ private static void OnProgress(object sender, ConversionProgressEventArgs e)
+ {
+ Console.WriteLine("[{0} => {1}]", e.Input.MetaData.FileInfo.Name, e.Output.Name);
+ Console.WriteLine("比特率: {0}", e.Bitrate);
+ Console.WriteLine("Fps: {0}", e.Fps);
+ Console.WriteLine("基本框架: {0}", e.Frame);
+ Console.WriteLine("处理持续时间: {0}", e.ProcessedDuration);
+ Console.WriteLine("Size: {0} kb", e.SizeKb);
+ Console.WriteLine("总持续时间: {0}\n", e.TotalDuration);
+ }
+
+ private static void OnData(object sender, ConversionDataEventArgs e)
+ {
+ Console.WriteLine(e.Data);
+ }
+
+ private static void OnComplete(object sender, ConversionCompleteEventArgs e)
+ {
+ Console.WriteLine("转换完成=>" + e.Output.Name);
+ }
+ }
+}
diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs b/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs
new file mode 100644
index 0000000..4be39f8
--- /dev/null
+++ b/VideoAnalysisCore/AICore/SherpaOnnx/Speaker.cs
@@ -0,0 +1,100 @@
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
+using VideoAnalysisCore.Common;
+using SherpaOnnx;
+
+namespace VideoAnalysisCore.AICore.SherpaOnnx
+{
+
+ ///
+ /// 说话人模型
+ /// pyannote
+ ///
+ public class Speaker
+ {
+ private static OfflineSpeakerDiarization? SD;
+ ///
+ /// 初始化 Speaker
+ ///
+ ///
+ ///
+ public static void Init(int speakerNumber = 0, double threshold = 0.6)
+ {
+ var config = new OfflineSpeakerDiarizationConfig();
+ //Pyannote模型地址
+ config.Segmentation.Pyannote.Model = Path.Combine(AppCommon.AIModelFile, "sherpa-onnx-pyannote-segmentation-3-0", "model.onnx");
+ //验证模型
+ config.Embedding.Model = Path.Combine(AppCommon.AIModelFile, "wespeaker", "wespeaker_zh_cnceleb_resnet34_LM.onnx");
+ //说话人数量
+ config.Clustering.NumClusters = speakerNumber;
+ //说话人判定阈值
+ config.Clustering.Threshold = (float)threshold;
+ SD = new OfflineSpeakerDiarization(config);
+ }
+ ///
+ /// 运行 说话人日志
+ ///
+ ///
+ public static async Task Run(string task)
+ {
+ var filePath = Path.Combine(task.LocalPath(), task + ".wav");
+ if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
+ throw new Exception("task 音频路径未找到");
+
+ if (SD is null)
+ throw new Exception("Speaker 未进行初始化");
+ var waveReader = new WaveReader(filePath);
+ if (SD.SampleRate != waveReader.SampleRate)
+ throw new Exception($"预期采样率:{SD.SampleRate}. 传入: {waveReader.SampleRate}");
+
+ var segments = SD.ProcessWithCallback(waveReader.Samples,
+ (numProcessedChunks, numTotalChunks, arg) =>
+ {
+ var progress = (double)numProcessedChunks / numTotalChunks * 100;
+ Console.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}=> {task} 说话人日志: {progress:F2}%");
+ return 1;
+ }, nint.Zero);
+ var res = segments.Select(s => new OfflineSpeakerRes(s));
+ await RedisExpand.Redis.HSetAsync(RedisExpandKey.Task(task), "Speaker", res);
+ //加入下一队列
+ RedisExpand.InsertChannel(Enum.RedisChannelEnum.ChatModelAnalysis, task);
+
+ }
+ }
+ ///
+ /// 讲话人日志结果
+ ///
+ public class OfflineSpeakerRes
+ {
+ ///
+ /// 总持续时间
+ ///
+ [JsonIgnore]
+ public decimal Total => End - Start;
+ ///
+ /// 开始时间
+ ///
+ public decimal Start { get; set; }
+ ///
+ /// 结束时间
+ ///
+ public decimal End { get; set; }
+ ///
+ /// 讲话人索引
+ ///
+ public int SpeakerIndex { get; set; }
+ ///
+ /// 讲话人日志结果
+ ///
+ ///
+ public OfflineSpeakerRes(OfflineSpeakerDiarizationSegment sds)
+ {
+ Start = (decimal)sds.Start;
+ End = (decimal)sds.End;
+ SpeakerIndex = sds.Speaker;
+ }
+ }
+}
+
diff --git a/VideoAnalysisCore/AICore/SherpaOnnx/WaveHeader.cs b/VideoAnalysisCore/AICore/SherpaOnnx/WaveHeader.cs
new file mode 100644
index 0000000..24084c0
--- /dev/null
+++ b/VideoAnalysisCore/AICore/SherpaOnnx/WaveHeader.cs
@@ -0,0 +1,167 @@
+// Copyright (c) 2023 Xiaomi Corporation (authors: Fangjun Kuang)
+using System;
+using System.IO;
+
+using System.Runtime.InteropServices;
+
+namespace VideoAnalysisCore.AICore.SherpaOnnx
+{
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct WaveHeader
+ {
+ public int ChunkID;
+ public int ChunkSize;
+ public int Format;
+ public int SubChunk1ID;
+ public int SubChunk1Size;
+ public short AudioFormat;
+ public short NumChannels;
+ public int SampleRate;
+ public int ByteRate;
+ public short BlockAlign;
+ public short BitsPerSample;
+ public int SubChunk2ID;
+ public int SubChunk2Size;
+
+ public bool Validate()
+ {
+ if (ChunkID != 0x46464952)
+ {
+ Console.WriteLine($"Invalid chunk ID: 0x{ChunkID:X}. Expect 0x46464952");
+ return false;
+ }
+
+ // E V A W
+ if (Format != 0x45564157)
+ {
+ Console.WriteLine($"Invalid format: 0x{Format:X}. Expect 0x45564157");
+ return false;
+ }
+
+ // t m f
+ if (SubChunk1ID != 0x20746d66)
+ {
+ Console.WriteLine($"Invalid SubChunk1ID: 0x{SubChunk1ID:X}. Expect 0x20746d66");
+ return false;
+ }
+
+ if (SubChunk1Size != 16)
+ {
+ Console.WriteLine($"Invalid SubChunk1Size: {SubChunk1Size}. Expect 16");
+ return false;
+ }
+
+ if (AudioFormat != 1)
+ {
+ Console.WriteLine($"Invalid AudioFormat: {AudioFormat}. Expect 1");
+ return false;
+ }
+
+ if (NumChannels != 1)
+ {
+ Console.WriteLine($"Invalid NumChannels: {NumChannels}. Expect 1");
+ return false;
+ }
+
+ if (ByteRate != SampleRate * NumChannels * BitsPerSample / 8)
+ {
+ Console.WriteLine($"Invalid byte rate: {ByteRate}.");
+ return false;
+ }
+
+ if (BlockAlign != NumChannels * BitsPerSample / 8)
+ {
+ Console.WriteLine($"Invalid block align: {ByteRate}.");
+ return false;
+ }
+
+ if (BitsPerSample != 16)
+ { // we support only 16 bits per sample
+ Console.WriteLine($"Invalid bits per sample: {BitsPerSample}. Expect 16");
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ // It supports only 16-bit, single channel WAVE format.
+ // The sample rate can be any value.
+ public class WaveReader
+ {
+ public WaveReader(string fileName)
+ {
+ if (!File.Exists(fileName))
+ {
+ throw new ApplicationException($"{fileName} does not exist!");
+ }
+
+ using (var stream = File.Open(fileName, FileMode.Open))
+ {
+ using (var reader = new BinaryReader(stream))
+ {
+ _header = ReadHeader(reader);
+
+ if (!_header.Validate())
+ {
+ throw new ApplicationException($"Invalid wave file ${fileName}");
+ }
+
+ SkipMetaData(reader);
+
+ // now read samples
+ // _header.SubChunk2Size contains number of bytes in total.
+ // we assume each sample is of type int16
+ byte[] buffer = reader.ReadBytes(_header.SubChunk2Size);
+ short[] samples_int16 = new short[_header.SubChunk2Size / 2];
+ Buffer.BlockCopy(buffer, 0, samples_int16, 0, buffer.Length);
+
+ _samples = new float[samples_int16.Length];
+
+ for (var i = 0; i < samples_int16.Length; ++i)
+ {
+ _samples[i] = samples_int16[i] / 32768.0F;
+ }
+ }
+ }
+ }
+
+ private static WaveHeader ReadHeader(BinaryReader reader)
+ {
+ byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(WaveHeader)));
+
+ GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ WaveHeader header = (WaveHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(WaveHeader))!;
+ handle.Free();
+
+ return header;
+ }
+
+ private void SkipMetaData(BinaryReader reader)
+ {
+ var bs = reader.BaseStream;
+
+ int subChunk2ID = _header.SubChunk2ID;
+ int subChunk2Size = _header.SubChunk2Size;
+
+ while (bs.Position != bs.Length && subChunk2ID != 0x61746164)
+ {
+ bs.Seek(subChunk2Size, SeekOrigin.Current);
+ subChunk2ID = reader.ReadInt32();
+ subChunk2Size = reader.ReadInt32();
+ }
+ _header.SubChunk2ID = subChunk2ID;
+ _header.SubChunk2Size = subChunk2Size;
+ }
+
+ private WaveHeader _header;
+
+ // Samples are normalized to the range [-1, 1]
+ private float[] _samples;
+
+ public int SampleRate => _header.SampleRate;
+ public float[] Samples => _samples;
+ }
+
+}
\ No newline at end of file
diff --git a/VideoAnalysisCore/AICore/Whisper/WhisperDto.cs b/VideoAnalysisCore/AICore/Whisper/WhisperDto.cs
new file mode 100644
index 0000000..0524c39
--- /dev/null
+++ b/VideoAnalysisCore/AICore/Whisper/WhisperDto.cs
@@ -0,0 +1,35 @@
+using Whisper.net;
+
+namespace VideoAnalysisCore.AICore.Whisper
+{
+ ///
+ /// 字幕识别 结果
+ ///
+ public class WhisperResDto
+ {
+ ///
+ ///
+ ///
+ ///
+ public WhisperResDto(SegmentData sd)
+ {
+ Text = sd.Text;
+ Start = sd.Start;
+ End = sd.End;
+ }
+ ///
+ /// 文本
+ ///
+ public string Text { get; } = string.Empty;
+ ///
+ /// 开始时间
+ ///
+
+ public TimeSpan Start { get; }
+ ///
+ /// 结束时间
+ ///
+
+ public TimeSpan End { get; }
+ }
+}
diff --git a/VideoAnalysisCore/AICore/Whisper/WhisperHandle.cs b/VideoAnalysisCore/AICore/Whisper/WhisperHandle.cs
new file mode 100644
index 0000000..8ab2f15
--- /dev/null
+++ b/VideoAnalysisCore/AICore/Whisper/WhisperHandle.cs
@@ -0,0 +1,122 @@
+using VideoAnalysisCore.Common;
+using System;
+using System.Linq;
+using Whisper.net;
+using Whisper.net.Ggml;
+using Whisper.net.Wave;
+
+namespace VideoAnalysisCore.AICore.Whisper
+{
+ public static class WhisperHandle
+ {
+
+ ///
+ /// 获取语音字幕
+ ///
+ ///
+ ///
+ public static async Task RunTask(string task)
+ {
+ var filePath = Path.Combine(task.LocalPath(), task + ".wav");
+ if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
+ throw new Exception("task 音频路径未找到");
+ var opt = new WhisperOption(filePath) { ModelName = AppCommon.Config.Whisper.ModelName };
+ var modPath = Path.Combine(AppCommon.AIModelFile, opt.ModelName);
+ // 从给定的模型名称路径创建一个 WhisperFactory 实例
+ using var factory = WhisperFactory.FromPath(modPath);
+ var builder = factory.CreateBuilder()
+ //自定义提示词
+ .WithPrompt("以下是普通话的句子")
+ //设置语言
+ .WithLanguage(opt.Language);
+
+ // 如果值为 "translate",则在构建器中启用翻译功能
+ //if (opt.Command == "translate")
+ //{
+ // builder.WithTranslate();
+ //}
+ using var processor = builder.Build();
+ // 打开一个文件流来读取由 opt.FileName 指定的音频文件
+ using var fileStream = File.OpenRead(filePath);
+ var res = new List(200);
+ // 使用处理器异步处理音频文件。对于处理器返回的每个段(segment),它将段的开始时间、结束时间和文本打印到控制台。
+ await foreach (var segment in processor.ProcessAsync(fileStream, CancellationToken.None))
+ {
+ res.Add(new WhisperResDto(segment));
+ }
+ RedisExpand.Redis.HMSet(RedisExpandKey.Task(task), "Captions", res);
+ RedisExpand.InsertChannel(Enum.RedisChannelEnum.ParsingSpeaker, task);
+ }
+ ///
+ /// 检测语言的方法
+ ///
+ ///
+ static void LanguageIdentification(WhisperOption opt)
+ {
+ var modPath = Path.Combine(AppCommon.AIModelFile, opt.ModelName);
+ // 使用 File.ReadAllBytes 方法将模型文件读取到内存中。
+ var bufferedModel = File.ReadAllBytes(modPath);
+
+ // 多个任务可以使用同一个工厂来创建处理器
+ using var factory = WhisperFactory.FromBuffer(bufferedModel);
+
+ // 使用工厂创建一个新的构建器,并设置其语言
+ var builder = factory.CreateBuilder()
+ .WithLanguage(opt.Language);
+
+ using var processor = builder.Build();
+
+ // 打开一个文件流来读取由 opt.FileName 指定的音频文件
+ using var fileStream = File.OpenRead(opt.FilePath);
+
+ // 使用 WaveParser 类来解析音频文件
+ var wave = new WaveParser(fileStream);
+
+ // 使用 WaveParser 的 GetAvgSamples 方法获取音频文件的平均样本
+ var samples = wave.GetAvgSamples();
+
+ // 使用处理器的 DetectLanguage 方法检测音频样本中的语言
+ var language = processor.DetectLanguage(samples, speedUp: true);
+
+ Console.WriteLine("Language is " + language);
+ }
+ }
+ ///
+ /// 音频处理选项
+ ///
+ public class WhisperOption
+ {
+ ///
+ /// 传入目标文件路径
+ ///
+ ///
+ public WhisperOption(string file)
+ {
+ FilePath = file;
+ }
+ ///
+ /// 指令类型
+ ///
+ public string Command { get; set; } = "transcribe";
+ ///
+ /// 音频文件,默认要存放bin目录下
+ ///
+ public string FilePath { get; set; }
+ ///
+ /// 语言,默认自动选择
+ ///
+ public string Language { get; set; } = "chinese";
+ ///
+ /// 模型文件名称
+ ///
+ public string ModelName { get; set; } = "ggml-base.bin";
+ ///
+ /// 模型文件路径
+ ///
+ public string ModPath => Path.Combine(AppCommon.AIModelFile, ModelName);
+ ///
+ /// 模型类型
+ ///
+ //public GgmlType ModelType { get; set; } = GgmlType.Base;
+ }
+}
diff --git a/VideoAnalysisCore/Common/AppCommon.cs b/VideoAnalysisCore/Common/AppCommon.cs
new file mode 100644
index 0000000..9855ad8
--- /dev/null
+++ b/VideoAnalysisCore/Common/AppCommon.cs
@@ -0,0 +1,281 @@
+using FreeRedis;
+using Microsoft.Extensions.DependencyModel;
+using SqlSugar;
+using SqlSugar.IOC;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Text;
+using System.Threading.Tasks;
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.Enum;
+using VideoAnalysisCore.Model.Dto;
+using Whisper.net;
+
+namespace VideoAnalysisCore.Common
+{
+
+ ///
+ /// 程序 公共变量
+ ///
+ public static class AppCommon
+ {
+ ///
+ /// 应用有效程序集
+ ///
+ public static readonly IEnumerable Assemblies;
+ ///
+ /// 主库数据库表类型
+ ///
+ public static readonly IEnumerable DbMatserType;
+ static AppCommon()
+ {
+ try
+ {
+
+ Assemblies = ExpandFunction.GetAssemblies();
+ var assembliesType = Assemblies.Where(s => s.FullName.Contains("VideoAnalysis")).SelectMany(s => s.ExportedTypes
+ .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)));
+ DbMatserType = assembliesType;
+ }
+ catch
+ {
+ }
+ //.Where(u => !u.IsDefined(typeof(SplitTableAttribute), false))
+ //.Where(u => !typeof(Model.DataCenterYH.IDataCenterYHModel).IsAssignableFrom(u))
+ //.Where(u => !u.IsSubclassOf(typeof(YQ_BaseEntity)));
+ }
+
+ ///
+ /// 程序配置
+ ///
+ public static AppConfig Config = new AppConfig();
+ ///
+ /// ServiceProvider
+ ///
+ public static IServiceProvider? Services;
+ ///
+ /// 文件下载路径
+ ///
+ public static string TaskCachedFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TaskCachedFile");
+
+ ///
+ /// 模型地址
+ ///
+ public static string AIModelFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AICore", "_Static");
+
+
+ }
+
+ ///
+ /// 拓展函数
+ ///
+ public static class ExpandFunction
+ {
+
+ ///
+ /// 获取应用有效程序集
+ ///
+ /// IEnumerable
+ public static List GetAssemblies()
+ {
+ // 获取当前解决方案的所有程序集
+ var assembliesStr = DependencyContext.Default.RuntimeLibraries
+ .Where(u => !u.Name.StartsWith(nameof(Microsoft))
+ && !u.Name.StartsWith(nameof(System))
+ && !u.Name.StartsWith("netstandard")
+ && (u.Type == "project"));
+
+ var assemblies = assembliesStr.Select(a => AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(a.Name))).ToList();
+ var assemblies1 = Assembly.GetEntryAssembly().GetReferencedAssemblies().Where(x => x.Name.StartsWith("App.") || x.Name.StartsWith("UserCenter."))
+ .Select(a => AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(a.Name))).ToList();
+ foreach (var item in assemblies1)
+ {
+ if (!assemblies.Contains(item))
+ assemblies.Add(item);
+ }
+ return assemblies;
+ }
+ ///
+ /// 获取Task处理后的 说话人字幕
+ ///
+ public static TotalCaptionsDto GetSpeakerCaptions(string task)
+ {
+ var captionsArr = RedisExpand.Redis.HMGet(RedisExpandKey.Task(task), "Captions").FirstOrDefault();
+ var speakerArr = RedisExpand.Redis.HMGet(RedisExpandKey.Task(task), "Speaker").FirstOrDefault();
+ if (captionsArr is null || captionsArr.Length == 0
+ || speakerArr is null || speakerArr.Length == 0)
+ throw new Exception("音频解析数据异常");
+ // 教师说话人Id
+ var techerId = speakerArr.GroupBy(s=>s.SpeakerIndex).Select(s => (s.Key,s.Sum(x=>x.Total)))
+ .OrderByDescending(s=>s.Item2).First().Key;
+ var teacherSpeaking = 0m;
+ var studentSpeaking = 0m;
+ var results = new Dictionary>();
+ foreach (var segment in captionsArr)
+ {
+ var spList = new List();
+ foreach (var speakerRes in speakerArr)
+ {
+ if ((double)speakerRes.Start > segment.End.TotalSeconds)
+ break;
+ if (segment.Start.TotalSeconds <= (double)speakerRes.End
+ && segment.End.TotalSeconds >= (double)speakerRes.Start)
+ {
+ if (speakerRes.SpeakerIndex == techerId)
+ teacherSpeaking += speakerRes.Total;
+ else
+ studentSpeaking += speakerRes.Total;
+ spList.Add(speakerRes.SpeakerIndex);
+ break;
+ }
+ }
+ results.Add(segment, spList);
+ }
+ //拼接 提示词字幕源
+ var stringBuilder = new StringBuilder();
+ foreach (var item in results)
+ stringBuilder.Append($"{item.Value.First()}:{item.Key.Start.TotalSeconds}:{item.Key.End.TotalSeconds}:{item.Key.Text},");
+
+ //todo 分析上课时间段情况 分析 独立学习 小组合作 随堂练习等情况
+
+ return new TotalCaptionsDto
+ {
+ StudentSpeaking = studentSpeaking,
+ TeacherSpeaking = teacherSpeaking,
+ Captions = stringBuilder.ToString(),
+ TimeBase = results.Select(s=>new TimeBase()
+ {
+ Start = s.Key.Start.TotalSeconds,
+ End = s.Key.End.TotalSeconds,
+ Type = s.Value.Count == 1 && s.Value.First() == techerId
+ ? TimeBaseTypeEnum.教师讲授
+ : TimeBaseTypeEnum.互动交流
+ })
+ };
+ }
+ ///
+ /// 转化枚举
+ ///
+ ///
+ ///
+ public static T? ToEnum(this object data) where T : struct, System.Enum
+ {
+ if (data is null || string.IsNullOrEmpty(data?.ToString()))
+ return null;
+ return System.Enum.Parse(data.ToString());
+ }
+ ///
+ /// 转化本地缓存目录
+ ///
+ /// 任务id
+ ///
+ public static string LocalPath(this string taskId)
+ {
+ return Path.Combine(AppCommon.TaskCachedFile, taskId);
+ }
+
+ }
+
+ ///
+ /// ffmpeg配置
+ ///
+ public class KIMIConfig
+ {
+ ///
+ /// kimi请求 公开的服务地址
+ ///
+ public string Host { get; set; } = string.Empty;
+ ///
+ /// api的密钥
+ ///
+ public string ApiKey { get; set; } = string.Empty;
+ }
+ ///
+ /// 文本模型 配置
+ ///
+ public class ChatGptConfig
+ {
+ ///
+ /// KIMI
+ ///
+ ///
+ public KIMIConfig KIMI { get; set; } = new KIMIConfig();
+ }
+
+ ///
+ /// ffmpeg配置
+ ///
+ public class FFmpegConfig
+ {
+ ///
+ /// 音频切片时间段
+ /// 0不切片
+ ///
+ public int TimeSlice { get; set; } = 0;
+ }
+ ///
+ /// Whisper配置
+ ///
+ public class WhisperConfig
+ {
+ ///
+ /// 模型名称
+ ///
+ public string ModelName { get; set; } = string.Empty;
+
+ }
+ ///
+ /// redis配置
+ ///
+ public class RedisConfig
+ {
+ ///
+ /// redis连接字符串
+ ///
+ public string ConnectionString { get; set; } = string.Empty;
+ }
+
+ public class DBConfig
+ {
+ ///
+ /// 主库链接
+ ///
+ public string ConnectionString { get; set; }=string.Empty;
+ ///
+ /// 数据库类型
+ ///
+ public IocDbType SqlType { get; set; }
+ ///
+ /// 启动时更新表结构
+ ///
+ public bool UpdateTable { get; set; }
+
+ }
+ ///
+ /// 应用程序配置
+ ///
+ public class AppConfig
+ {
+ ///
+ /// redis
+ ///
+ public RedisConfig Redis { get; set; } = new RedisConfig();
+ ///
+ /// Whisper AI
+ ///
+ public WhisperConfig Whisper { get; set; } = new WhisperConfig();
+ ///
+ /// FFmpeg
+ ///
+ public FFmpegConfig FFmpeg { get; set; } = new FFmpegConfig();
+ ///
+ /// ChatGpt
+ ///
+ public ChatGptConfig ChatGpt { get; set; } = new ChatGptConfig();
+ ///
+ /// 数据库配置
+ ///
+ public DBConfig DB { get; set; } = new DBConfig();
+
+ }
+}
diff --git a/VideoAnalysisCore/Common/DownloadFile.cs b/VideoAnalysisCore/Common/DownloadFile.cs
new file mode 100644
index 0000000..492b6e0
--- /dev/null
+++ b/VideoAnalysisCore/Common/DownloadFile.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace VideoAnalysisCore.Common
+{
+ ///
+ ///
+ ///
+ public class DownloadFile
+ {
+ // 根据 Content-Type 映射文件后缀
+ static string GetExtensionFromContentType(HttpResponseMessage res)
+ {
+ var contentType = res.Content.Headers.ContentType?.MediaType;
+ return contentType switch
+ {
+ "application/pdf" => ".pdf",
+ "image/jpeg" => ".jpg",
+ "image/png" => ".png",
+ "application/zip" => ".zip",
+ "text/html" => ".html",
+ "audio/wav" => ".wav",
+ "audio/mp4" => ".mp4",
+ "audio/mp3" => ".mp3",
+ // 根据需要添加其他 Content-Type 映射
+ _ => ".mp4", // 默认二进制文件
+ };
+ }
+
+ ///
+ /// 使用 HttpClient 下载任务的资源文件到本地
+ ///
+ ///
+ ///
+ public static async Task RunTask(string task)
+ {
+ //获取资源文件 地址
+ 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);
+
+ //创建下载文件缓存路径
+ if (!Directory.Exists(AppCommon.TaskCachedFile)) Directory.CreateDirectory(AppCommon.TaskCachedFile);
+
+ // 获取文件大小
+ var totalBytes = response.Content.Headers.ContentLength;
+ var localPath = task.LocalPath();
+ 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)
+ {
+ count++;
+ await fs.WriteAsync(buffer, 0, bytesRead);
+ totalBytesRead += bytesRead;
+
+ // 计算下载进度
+ if (totalBytes.HasValue && count % 3 == 0)
+ {
+ var progress = (double)totalBytesRead / totalBytes.Value * 100;
+ Console.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}=> {task} 下载进度: {progress:F2}%");
+ }
+ }
+
+ //加入下一队列
+ RedisExpand.InsertChannel(Enum.RedisChannelEnum.SeparateAudio, task);
+ }
+ }
+}
diff --git a/VideoAnalysisCore/Common/RedisExpand.cs b/VideoAnalysisCore/Common/RedisExpand.cs
new file mode 100644
index 0000000..0323e5f
--- /dev/null
+++ b/VideoAnalysisCore/Common/RedisExpand.cs
@@ -0,0 +1,169 @@
+using FreeRedis;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using VideoAnalysisCore.AICore.ChatGPT;
+using VideoAnalysisCore.AICore.FFMPGE;
+
+//using VideoAnalysisCore.AICore.FFMPGE;
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.AICore.Whisper;
+using VideoAnalysisCore.Enum;
+
+namespace VideoAnalysisCore.Common
+{
+ ///
+ /// redis key
+ ///
+ public static class RedisExpandKey
+ {
+ ///
+ /// 基础key
+ ///
+ public const string BaseKey = "VideoAnalysis:";
+ ///
+ /// 基础Channel key
+ ///
+ public const string ChannelKey = BaseKey + "Channel:";
+ ///
+ /// 下载文件
+ ///
+ public const string DownloadFile = ChannelKey + "DownloadFile";
+ ///
+ /// 分离音频
+ ///
+ public const string SeparateAudio = ChannelKey + "SeparateAudio";
+ ///
+ /// 解析字幕
+ ///
+ public const string ParsingCaptions = ChannelKey + "ParsingCaptions";
+ ///
+ /// 解析说话人
+ ///
+ public const string ParsingSpeaker = ChannelKey + "ParsingSpeaker";
+ ///
+ /// Chat模型分析
+ ///
+ public const string ChatModelAnalysis = ChannelKey + "ChatModelAnalysis";
+
+ ///
+ /// 任务数组
+ ///
+ 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;
+
+ }
+ ///
+ /// redis拓展
+ ///
+ public class RedisExpand
+ {
+ ///
+ /// redis 连接
+ ///
+ public static RedisClient Redis = new RedisClient(AppCommon.Config.Redis.ConnectionString);
+ ///
+ /// 初始化redis
+ /// 需要在初始化配置文件时候调用
+ ///
+ public static void Init()
+ {
+ Redis.Serialize = obj => System.Text.Json.JsonSerializer.Serialize(obj);
+ Redis.Deserialize = (json, type) => System.Text.Json.JsonSerializer.Deserialize(json, type);
+ InitChannel();
+ }
+ ///
+ /// 将任务 插入 队列
+ ///
+ /// 枚举
+ /// 任务id
+ public static void InsertChannel(RedisChannelEnum @enum, object taskId)
+ {
+ var startTime = Redis.HMGet>(RedisExpandKey.Task(taskId), "StartTime").FirstOrDefault();
+ if (startTime is null)
+ startTime = new Dictionary();
+ if (!startTime.ContainsKey(@enum))
+ startTime.Add(@enum, DateTime.Now);
+ else
+ startTime[@enum] = DateTime.Now;
+ Redis.HMSet(RedisExpandKey.Task(taskId), "StartTime", startTime);
+
+ if (Redis is null) throw new Exception("redis未初始化");
+ Redis.LPush(RedisExpandKey.EnumKey(@enum), taskId);
+
+ }
+
+ ///
+ /// 初始化 队列 任务
+ ///
+ public static void InitChannel()
+ {
+ if (Redis is null) throw new Exception("redis未初始化");
+
+ Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.DownloadFile),
+ (msg) => { TouchChannel(RedisChannelEnum.DownloadFile, msg, DownloadFile.RunTask); });
+
+ Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.SeparateAudio),
+ (msg) => { TouchChannel(RedisChannelEnum.SeparateAudio, msg, FFMPGEHandle.Audio2WAV16KAsync); });
+
+ Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.ParsingCaptions),
+ (msg) => { TouchChannel(RedisChannelEnum.ParsingCaptions, msg, WhisperHandle.RunTask); });
+ Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.ParsingSpeaker),
+ (msg) => { TouchChannel(RedisChannelEnum.ParsingSpeaker, msg, Speaker.Run); });
+ Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.ChatModelAnalysis),
+ (msg) =>
+ {
+ TouchChannel(RedisChannelEnum.ChatModelAnalysis, msg,
+ (task) =>
+ {
+ using var scope = AppCommon.Services?.CreateScope();
+ if (scope is null || scope.ServiceProvider.GetService() is null)
+ throw new Exception("IBserGPT 未注入");
+ else
+ return scope.ServiceProvider.GetService()?.CallGPT(task)??Task.CompletedTask;
+ });
+ });
+ Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.CallBackSystem),
+ (msg) => { TouchChannel(RedisChannelEnum.ParsingSpeaker, msg); });
+
+
+ }
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async void TouchChannel(RedisChannelEnum key, string msg, Func action = null)
+ {
+ if (msg is null) return;
+ Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "-> " + key + " " + msg);
+ if (action is not null)
+ {
+ Redis.HMSet(RedisExpandKey.Task(msg), "LastEnum", key);
+ await action(msg);
+ }
+ else
+ {
+ Console.WriteLine(key + " 任务函数 未实现");
+ }
+ }
+
+ }
+}
diff --git a/VideoAnalysisCore/Common/Repository.cs b/VideoAnalysisCore/Common/Repository.cs
new file mode 100644
index 0000000..6b463cf
--- /dev/null
+++ b/VideoAnalysisCore/Common/Repository.cs
@@ -0,0 +1,19 @@
+using SqlSugar;
+using SqlSugar.IOC;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace VideoAnalysisCore.Common
+{
+ public class Repository : SimpleClient where T : class, new()
+ {
+ public Repository()
+ {
+ base.Context = DbScoped.SugarScope;
+ }
+
+ }
+}
diff --git a/VideoAnalysisCore/Common/SqlSugarExpand.cs b/VideoAnalysisCore/Common/SqlSugarExpand.cs
new file mode 100644
index 0000000..52d959a
--- /dev/null
+++ b/VideoAnalysisCore/Common/SqlSugarExpand.cs
@@ -0,0 +1,154 @@
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using MySqlConnector;
+using SqlSugar;
+using SqlSugar.IOC;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Xml.Linq;
+using VideoAnalysisCore.Common;
+
+namespace VideoAnalysisCore.Common
+{
+ public static class SqlSugarExpand
+ {
+ public static bool ShowSQL = false;
+ public static void InitSqlSugar( this IServiceCollection services)
+ {
+ #region SqlSugar注入
+ var dbList = new List() {
+ new IocConfig()
+ {
+ ConfigId =1,
+ ConnectionString = AppCommon.Config.DB.ConnectionString,
+ DbType =AppCommon.Config.DB.SqlType,
+ IsAutoCloseConnection = true//自动释放
+ },
+ };
+ services.AddSingleton(typeof(Repository<>));
+
+ //注入SqlSugar 主库
+ services.AddSqlSugar(dbList);
+
+
+ services.ConfigurationSugar(db =>
+ {
+ var config = db.CurrentConnectionConfig;
+ // 设置超时时间
+ db.Ado.CommandTimeOut = 61;
+#if DEBUG
+ // 打印SQL语句
+ db.Aop.OnLogExecuting = (sql, pars) =>
+ {
+ if (!ShowSQL) return;
+ //var originColor = Console.ForegroundColor;
+ //if (sql.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
+ // Console.ForegroundColor = ConsoleColor.Green;
+ //if (sql.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) || sql.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase))
+ // Console.ForegroundColor = ConsoleColor.Yellow;
+ //if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase))
+ // Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"【{DateTime.Now}——执行SQL - [{config.ConfigId}]】\r\n" + UtilMethods.GetSqlString(config.DbType, sql, pars) + "\r\n");
+ //Console.ForegroundColor = originColor;
+ };
+#endif
+ db.Aop.OnError = (ex) =>
+ {
+ if (ex.Parametres == null) return;
+ //var originColor = Console.ForegroundColor;
+ //Console.ForegroundColor = ConsoleColor.DarkRed;
+ Console.WriteLine($"【{DateTime.Now}——错误SQL - [{config.ConfigId}]】\r\n" + UtilMethods.GetSqlString(config.DbType, ex.Sql, (SugarParameter[])ex.Parametres) + "\r\n");
+ //Console.ForegroundColor = originColor;
+ };
+ db.Aop.DataExecuting = (oldValue, entityInfo) =>
+ {
+ if (entityInfo.OperationType == DataFilterType.InsertByObject)
+ {
+ // 主键(long类型)且没有值的---赋值雪花Id
+ if (entityInfo.EntityColumnInfo.IsPrimarykey && !entityInfo.EntityColumnInfo.IsIdentity && entityInfo.EntityColumnInfo.PropertyInfo.PropertyType == typeof(long))
+ {
+ var id = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue);
+ //if (id == null || (long)id == 0)
+ // entityInfo.SetValue(YitIdHelper.NextId());
+ }
+ if (entityInfo.PropertyName == "CreateTime")
+ entityInfo.SetValue(DateTime.Now);
+ }
+ if (entityInfo.OperationType == DataFilterType.UpdateByObject)
+ {
+
+ }
+ };
+
+ });
+
+ #endregion
+ }
+ public static void InitDB()
+ {
+ ShowSQL = false;
+ var builder = new MySqlConnectionStringBuilder(AppCommon.Config.DB.ConnectionString);
+ var dbName = builder.Database;
+ builder.Database = "mysql";
+ using SqlSugarClient dbMysql = new SqlSugarClient(new ConnectionConfig
+ {
+ ConnectionString = builder.ConnectionString,
+ DbType = DbType.MySql,
+ IsAutoCloseConnection = true,
+ });
+
+ var dataBaseList = dbMysql.DbMaintenance.GetDataBaseList();
+ if (dataBaseList.Contains(dbName))
+ {
+ Console.WriteLine($"【0】数据库 {dbName} 已存在 【√】");
+ }
+ else
+ {
+ Console.WriteLine($"【0】创建数据库{dbName} ... ");
+ var res = dbMysql.Ado.ExecuteCommand($"CREATE DATABASE `{dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci");
+ try
+ {
+ dbMysql.Ado.ExecuteCommand($"alter database `{dbName}` character set utf8mb4;" +
+ $"alter database `{dbName}` character set utf8mb4 collate utf8mb4_general_ci;");
+ //res 没有权限
+ dbMysql.Ado.ExecuteCommand($"SET GLOBAL local_infile=1;");
+ Console.WriteLine($"【0】数据库 {dbName} 创建完成 【√】");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("【0】主库初始化配置 出现异常 " + ex.Message);
+ }
+
+
+ }
+ InitDbTable();
+ }
+ public static void InitDbTable()
+ {
+ if (!AppCommon.Config.DB.UpdateTable)
+ {
+ Console.WriteLine($"【1】初始化主库表 跳过....");
+ ShowSQL = true;
+ return;
+ }
+ Console.WriteLine($"【1】初始化主库表 执行中....");
+ var entityTypes = AppCommon.DbMatserType;
+ Console.WriteLine($"【1】数量{entityTypes.Count()} ....");
+ if (!entityTypes.Any()) return;
+ var i = 0;
+ var totalCount = entityTypes.Count().ToString().Length;
+ foreach (var t in entityTypes)
+ {
+ Console.Write($"【1】{entityTypes.Count()}/{(++i).ToString().PadLeft(totalCount, '0')} 执行 {t.FullName}".PadRight(60, ' '));
+ DbScoped.SugarScope.CodeFirst.InitTables(t);
+ Console.WriteLine($"【√】");
+ }
+ Console.WriteLine($"【1】数量{entityTypes.Count()} 执行完毕");
+ ShowSQL = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/VideoAnalysisCore/Enum/RedisChannelEnum.cs b/VideoAnalysisCore/Enum/RedisChannelEnum.cs
new file mode 100644
index 0000000..66a2441
--- /dev/null
+++ b/VideoAnalysisCore/Enum/RedisChannelEnum.cs
@@ -0,0 +1,33 @@
+namespace VideoAnalysisCore.Enum
+{
+ ///
+ /// redis 频道
+ ///
+ public enum RedisChannelEnum
+ {
+ ///
+ /// 下载文件
+ ///
+ DownloadFile = 1,
+ ///
+ /// 分离音频
+ ///
+ SeparateAudio = 2,
+ ///
+ /// 解析字幕
+ ///
+ ParsingCaptions = 3,
+ ///
+ /// 解析说话人
+ ///
+ ParsingSpeaker = 4,
+ ///
+ /// Chat模型分析
+ ///
+ ChatModelAnalysis = 5,
+ ///
+ /// 回调三方系统
+ ///
+ CallBackSystem = 6
+ }
+}
diff --git a/VideoAnalysisCore/Enum/TimeBaseEnum.cs b/VideoAnalysisCore/Enum/TimeBaseEnum.cs
new file mode 100644
index 0000000..1bc39ac
--- /dev/null
+++ b/VideoAnalysisCore/Enum/TimeBaseEnum.cs
@@ -0,0 +1,29 @@
+namespace VideoAnalysisCore.Enum
+{
+ ///
+ /// 时间线类型
+ ///
+ public enum TimeBaseTypeEnum
+ {
+ ///
+ /// 教师讲授
+ ///
+ 教师讲授 = 1,
+ ///
+ /// 互动交流
+ ///
+ 互动交流 = 2,
+ ///
+ /// 独立学习
+ ///
+ 独立学习 = 3,
+ ///
+ /// 小组合作
+ ///
+ 小组合作 = 4,
+ ///
+ /// 随堂练习
+ ///
+ 随堂练习
+ }
+}
diff --git a/VideoAnalysisCore/Model/CourseGradingCriteria.cs b/VideoAnalysisCore/Model/CourseGradingCriteria.cs
new file mode 100644
index 0000000..bb4ed71
--- /dev/null
+++ b/VideoAnalysisCore/Model/CourseGradingCriteria.cs
@@ -0,0 +1,53 @@
+using SqlSugar;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Net;
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.Enum;
+using Whisper.net;
+
+namespace VideoAnalysisCore.Model
+{
+ ///
+ /// 课堂评分标准
+ ///
+ [SugarTable("coursegradingcriteria")]
+ public class CourseGradingCriteria
+ {
+ ///
+ /// Id
+ ///
+ [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+ [DisplayName( "编号"), Required]
+ public long Id { get; set; }
+ ///
+ /// 标准提问词
+ ///
+ [SugarColumn(Length = 100)]
+ [DisplayName( "标准提问词"), Required]
+ public string NamePrompt { get; set; }=string.Empty;
+ ///
+ /// 优点展示词
+ ///
+ [SugarColumn(Length = 100)]
+ [DisplayName( "优点展示词"), Required]
+ public string Advantage { get; set; } = string.Empty;
+
+ ///
+ /// 缺点展示词
+ ///
+ [SugarColumn(Length = 100)]
+ [DisplayName( "缺点展示词"), Required]
+ public string Flaw { get; set; } = string.Empty;
+
+ ///
+ /// 改进意见
+ ///
+ [DisplayName( "改进意见")]
+ [SugarColumn(Length = 100), Required]
+ public string ImprovedMethods { get; set; } = string.Empty;
+
+
+ }
+}
diff --git a/VideoAnalysisCore/Model/Dto/SpeakerCaptionsDto.cs b/VideoAnalysisCore/Model/Dto/SpeakerCaptionsDto.cs
new file mode 100644
index 0000000..5c3e340
--- /dev/null
+++ b/VideoAnalysisCore/Model/Dto/SpeakerCaptionsDto.cs
@@ -0,0 +1,49 @@
+using VideoAnalysisCore.Enum;
+
+namespace VideoAnalysisCore.Model.Dto
+{
+ ///
+ /// 时间线
+ ///
+ public class TimeBase
+ {
+ public TimeBase()
+ {
+
+ }
+ ///
+ /// 开始时间
+ ///
+ public double Start { get; set; }
+ ///
+ /// 结束时间
+ ///
+ public double End { get; set; }
+ ///
+ /// 时间段 类型
+ ///
+ public TimeBaseTypeEnum Type { get; set; }
+
+ }
+ public class TotalCaptionsDto
+ {
+ ///
+ /// 拼接说话人后的最终字幕
+ ///
+ public string Captions { get; set; } = string.Empty;
+ ///
+ /// 教师发言时间
+ /// 秒
+ ///
+ public decimal TeacherSpeaking { get; set; } = 0;
+ ///
+ /// 学生发言时间
+ /// 秒
+ ///
+ public decimal StudentSpeaking { get; set; } = 0;
+ ///
+ /// 视频时间轴
+ ///
+ public IEnumerable? TimeBase { get; set; }
+ }
+}
diff --git a/VideoAnalysisCore/Model/VideoTask.cs b/VideoAnalysisCore/Model/VideoTask.cs
new file mode 100644
index 0000000..f208163
--- /dev/null
+++ b/VideoAnalysisCore/Model/VideoTask.cs
@@ -0,0 +1,83 @@
+using SqlSugar;
+using System.ComponentModel.DataAnnotations;
+using System.Net;
+using VideoAnalysisCore.AICore.SherpaOnnx;
+using VideoAnalysisCore.Enum;
+using Whisper.net;
+
+namespace VideoAnalysisCore.Model
+{
+ ///
+ /// 视频任务模型
+ ///
+ [SugarTable("videotask")]
+ public class VideoTask
+ {
+ ///
+ /// 任务id
+ /// 视频音频文件地址都使用taskID能获取
+ ///
+ [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+ public long Id { get; set; }
+ ///
+ /// 媒体路径
+ ///
+ public string MediaUrl { get; set; } = string.Empty;
+ ///
+ /// 下载后本地媒体目录
+ ///
+ public string LocalMediaPath { get; set; } = string.Empty;
+ ///
+ /// 上一次执行的枚举
+ ///
+ public RedisChannelEnum LastEnum { get; set; }
+ ///
+ /// ApiKey
+ ///
+ public string ApiToken { get; set; } = string.Empty;
+ ///
+ /// 请求来自哪个ip地址
+ ///
+ public string ComeFrom { get; set; } = string.Empty;
+ ///
+ /// 自定义值 任务完成后附带通知
+ ///
+ [SugarColumn(Length = 500)]
+ public string Tag { get; set; }
+ ///
+ ///回调Api地址
+ ///
+ [SugarColumn(Length = 500)]
+ public string CallBackUrl { get; set; }
+ ///
+ /// 字幕缓存
+ ///
+ [SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
+ public SegmentData[]? Captions { get; set; }
+ ///
+ /// 说话人日志解析缓存
+ ///
+ [SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
+ public OfflineSpeakerRes[]? Speaker { get; set; }
+ ///
+ /// Chat模型分析缓存
+ ///
+ [SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
+ public object[]? ChatAnalysis { get; set; }
+ ///
+ /// 消耗token
+ ///
+ public int TotalTokens { get; set; }
+ ///
+ /// 创建时间
+ ///
+ public DateTime CreateTime { get; set; } = DateTime.Now;
+ ///
+ /// 开始时间轴
+ ///
+ [SugarColumn(ColumnDataType = "varchar", Length =255)]
+ public Dictionary StartTime { get; set; }
+
+
+ }
+}
diff --git a/VideoAnalysisCore/VideoAnalysisCore.csproj b/VideoAnalysisCore/VideoAnalysisCore.csproj
new file mode 100644
index 0000000..6be7135
--- /dev/null
+++ b/VideoAnalysisCore/VideoAnalysisCore.csproj
@@ -0,0 +1,59 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ Never
+
+
+ PreserveNewest
+
+
+ Always
+
+
+ PreserveNewest
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+