添加项目文件。

This commit is contained in:
小肥羊 2024-10-30 17:38:34 +08:00
commit d8224e68d2
45 changed files with 3554 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -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

367
.gitignore vendored Normal file
View File

@ -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/

31
Learn.VideoAnalysis.sln Normal file
View File

@ -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

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" rel="stylesheet" />
<link rel="stylesheet" href="Learn.VideoAnalysis.bundle.scp.css" />
<link href="./css/site.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script type="text/javascript" src="@("https://unpkg.com/@antv/g2plot@2.4.17/dist/g2plot.min.js")"></script>
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
<script src="_content/AntDesign.Charts/ant-design-charts-blazor.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@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;
}

View File

@ -0,0 +1,32 @@
@namespace VideoAnalysisRazor.Layouts
@inherits LayoutComponentBase
<AntDesign.ProLayout.BasicLayout
Logo="@("https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg")"
MenuData="_menuData"
Context="学习视频分析"
MenuAccordion
Title="学习视频分析"
@bind-Collapsed="collapsed">
<HeaderContentRender>
<Space Size="@("24")">
<SpaceItem>
<Icon Class="action" Type="@(collapsed?"menu-unfold":"menu-fold")" OnClick="Toggle" />
</SpaceItem>
<SpaceItem>
<Icon Class="action" Type="reload" Theme="outline" OnClick="Reload" />
</SpaceItem>
</Space>
</HeaderContentRender>
<RightContentRender>
</RightContentRender>
<ChildContent>
<ReuseTabs></ReuseTabs>
</ChildContent>
<FooterRender>
<FooterView Copyright="2024 重庆远轩教育科技有限公司" Links="new LinkItem[0]"></FooterView>
</FooterRender>
</AntDesign.ProLayout.BasicLayout>
<SettingDrawer />

View File

@ -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()
{
}
}
}

View File

@ -0,0 +1,75 @@
@page "/"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using SqlSugar
@using VideoAnalysisCore.Model
<Table @ref="_table" Loading="tableLoading" TItem="CourseGradingCriteria" PageSize="15" Total="_total" DataSource="_dataSource" @bind-SelectedRows="_selectedRows" OnChange="OnChange">
<TitleTemplate>
<Flex Justify="end" Gap="10">
<Button Type="primary" @onclick="()=> StartEdit(default)">新增</Button>
@* <Button Disabled="!_selectedRows.Any()" Danger @onclick="DeleteAll">Delete</Button> *@
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<Selection />
<ActionColumn Title="操作列">
<a @onclick="() => StartEdit(row)">修改</a>
<Button Type="@ButtonType.Link" Danger @onclick="() => Delete(row)">
删除</Button>
</ActionColumn>
<GenerateColumns Definitions="@((n,c) => { c.Filterable = true; c.Sortable = true; })" />
</ColumnDefinitions>
</Table>
@inject ModalService ModalService
@code
{
/// <summary>
/// 新增或者修改
/// </summary>
/// <param name="row"></param>
void StartEdit(CourseGradingCriteria row)
{
var data = row == null ? new() : row;
IForm? form = default;
ModalRef<bool> modalRef = default;
modalRef = ModalService.CreateModal<bool>(new()
{
Title = data.Id > 0 ? "修改" : "新增",
Content =
@<Form @ref="form" Model="data" OnFinish="()=> modalRef.OkAsync(true)"
LabelColSpan="6" WrapperColSpan="18">
<GenerateFormItem NotGenerate="@(x=> x == "Id")" />
</Form>
,
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();
}
});
}
}

View File

@ -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<CourseGradingCriteria> criteria { get; set; } = default!;
IEnumerable<CourseGradingCriteria> _selectedRows = [];
ITable _table;
List<CourseGradingCriteria> _dataSource = null;
RefAsync<int> _total = 0;
bool tableLoading = false;
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
async void OnChange(QueryModel<CourseGradingCriteria> query)
{
tableLoading = true;
Expression<Func<CourseGradingCriteria, bool>> 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();
}
/// <summary>
/// 删除行
/// </summary>
/// <param name="row"></param>
/// <returns></returns>
async Task Delete(CourseGradingCriteria row)
{
if (!await Comfirm($"确定要删除这条数据吗? [{row.NamePrompt}]?"))
return;
await criteria.DeleteByIdAsync(row.Id);
_table.ReloadData();
}
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -0,0 +1,3 @@
input[aria-hidden="true"] {
display: none !important;
}

View File

@ -0,0 +1,6 @@
namespace VideoAnalysisRazor.Resources;
internal class I18n
{
}

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="menu.account.center" xml:space="preserve">
<value>Account Center</value>
</data>
<data name="menu.account.logout" xml:space="preserve">
<value>Logout</value>
</data>
<data name="menu.account.settings" xml:space="preserve">
<value>Settings</value>
</data>
</root>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="menu.account.center" xml:space="preserve">
<value>个人中心</value>
</data>
<data name="menu.account.logout" xml:space="preserve">
<value>退出登录</value>
</data>
<data name="menu.account.settings" xml:space="preserve">
<value>个人设置</value>
</data>
</root>

View File

@ -0,0 +1,10 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<CascadingValue Value="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(VideoAnalysisRazor.Layouts.BasicLayout)" />
</CascadingValue>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
<AntContainer />

View File

@ -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

View File

@ -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<ApiController> _logger;
public ApiController(ILogger<ApiController> logger)
{
_logger = logger;
}
/// <summary>
/// 音频转换wav_16k
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns></returns>
[HttpGet(Name = "Audio2WAV16K")]
public async Task<IActionResult> 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地址");
}
/// <summary>
/// 测试 插入队列
/// </summary>
/// <param name="enum"></param>
/// <param name="msg"></param>
/// <returns></returns>
[HttpPost(Name = "TestInsertChannel")]
public IActionResult TestInsertChannel(int @enum=1, string msg= "test_0001")
{
RedisExpand.InsertChannel(@enum.ToEnum<RedisChannelEnum>().Value
, msg);
return Ok();
}
/// <summary>
/// 视频处理
/// </summary>
/// <param name="req">请求体</param>
/// <returns></returns>
[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();
}
}
}

View File

@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
namespace Learn.VideoAnalysis.Controllers.Dto
{
/// <summary>
/// 视频处理 请求
/// </summary>
public class VideoAnalysisReq
{
/// <summary>
/// 媒体路径
/// </summary>
[Required(ErrorMessage = "资源URL是必填项")]
[Url(ErrorMessage = "请输入有效的 URL")]
public string MediaUrl { get; set; } = string.Empty;
/// <summary>
/// ApiKey
/// </summary>
[Required(ErrorMessage = "接口Token是必填项")]
public string ApiToken { get; set; } = string.Empty;
/// <summary>
/// 自定义值 任务完成后附带通知
/// </summary>
public string Tag { get; set; } = string.Empty;
/// <summary>
///回调Api地址
/// </summary>
[Required(ErrorMessage = "回调Api地址是必填项")]
[Url(ErrorMessage = "请输入有效的 URL")]
public string CallBackUrl { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,2 @@
global using VideoAnalysisRazor.Resources;
global using AntDesign;

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>false</InvariantGlobalization>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="AICore\**" />
<Content Remove="AICore\**" />
<EmbeddedResource Remove="AICore\**" />
<None Remove="AICore\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VideoAnalysisCore\VideoAnalysisCore.csproj" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.5" />
<PackageReference Include="AntDesign.Charts" Version="0.4.0" />
<PackageReference Include="AntDesign.Extensions.Localization" Version="0.20.2.1" />
<PackageReference Include="AntDesign.ProLayout" Version="0.20.2.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@Learn.VideoAnalysis_HostAddress = http://localhost:5238
GET {{Learn.VideoAnalysis_HostAddress}}/Audio2Text?modelType=LargeV3
Accept: application/json
###

109
VideoAnalysis/Program.cs Normal file
View File

@ -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<IHttpContextAccessor>().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<ProSettings>(builder.Configuration.GetSection("ProSettings"));
builder.Services.AddHttpClient();
builder.Services.AddSingleton<MoonshotClient>();
builder.Services.AddSingleton<IBserGPT, KIMI_GPT>();
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<App>()
.AddInteractiveServerRenderMode();
//.AddInteractiveWebAssemblyRenderMode()
//.AddAdditionalAssemblies(typeof(VideoAnalysisRazor._Imports).Assembly);
app.MapControllers();
SqlSugarExpand.InitDB();
app.Run();
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -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,
}
}
}

View File

@ -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;
}

View File

@ -0,0 +1,19 @@
using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Common;
using Whisper.net;
namespace VideoAnalysisCore.AICore.ChatGPT
{
/// <summary>
/// GPT 接口
/// </summary>
public interface IBserGPT
{
/// <summary>
/// 访问GPT
/// </summary>
/// <param name="task">任务id</param>
/// <returns></returns>
public Task CallGPT(string task);
}
}

View File

@ -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
{
/// <summary>
/// kimi 文本模型
/// </summary>
public class KIMI_GPT : IBserGPT
{
private readonly MoonshotClient moonshotClient;
/// <summary>
/// 初始化
/// </summary>
/// <param name="moonshotClient"></param>
/// <param name="logger"></param>
public KIMI_GPT(MoonshotClient moonshotClient)
{
MoonshotClient.Host = AppCommon.Config.ChatGpt.KIMI.Host;
MoonshotClient.ApiKey = AppCommon.Config.ChatGpt.KIMI.ApiKey;
this.moonshotClient = moonshotClient;
}
/// <summary>
/// 访问GPT
/// </summary>
/// <param name="task">任务id</param>
/// <returns></returns>
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<MessagesItem>(){
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}");
}
}
}

View File

@ -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
{
/// <summary>
/// https://platform.moonshot.cn/docs/api-reference
/// </summary>
public class MoonshotClient
{
private readonly ILogger<MoonshotClient> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public MoonshotClient(ILogger<MoonshotClient> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
/// <summary>
/// list models
/// </summary>
/// <returns></returns>
public async Task<ModelListResp> ListModels()
{
var response = await GetAsync("/v1/models");
return await ParseResp<ModelListResp>(response);
}
/// <summary>
/// Chat
/// </summary>
/// <param name="requestBody"></param>
/// <returns>Return HttpResponseMessage for SSE</returns>
public async Task<HttpResponseMessage> Chat(string requestBody)
{
return await PostJsonStreamAsync("/v1/chat/completions", requestBody);
}
/// <summary>
/// Chat
/// </summary>
/// <param name="chatReq"></param>
/// <returns>Return HttpResponseMessage for SSE</returns>
public async Task<HttpResponseMessage> Chat(ChatReq chatReq)
{
var requestBody = System.Text.Json.JsonSerializer.Serialize(chatReq);
return await PostJsonStreamAsync("/v1/chat/completions", requestBody);
}
/// <summary>
/// Get as timate token count
/// </summary>
public async Task<int?> 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<int>();
}
var error = System.Text.Json.JsonSerializer.Deserialize<ErrorResponse>(responseText);
_logger.LogError($"{error?.error?.type}: {error?.error?.message}");
throw new Exception($"{error?.error.type}: {error?.error.message}");
}
/// <summary>
/// Get as timate token count
/// </summary>
/// <param name="chatReq"></param>
/// <returns></returns>
public async Task<int?> GetAsTiMateTokenCount(ChatReq chatReq)
{
var chatReqText =System.Text.Json.JsonSerializer.Serialize(chatReq);
return await GetAsTiMateTokenCount(chatReqText);
}
/// <summary>
/// List files
/// </summary>
public virtual async Task<FileListResp> ListFiles()
{
var response = await GetAsync("/v1/files");
return await ParseResp<FileListResp>(response);
}
/// <summary>
/// Upload file
/// </summary>
public virtual async Task<FileItem> 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<FileItem>(response);
}
/// <summary>
/// Upload file stream
/// </summary>
public virtual async Task<FileItem> 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<FileItem>(response);
}
/// <summary>
/// Get file content
/// </summary>
public virtual async Task<FileContent> GetFileContent(string fileId)
{
var response = await GetAsync($"/v1/files/{fileId}/content");
return await ParseResp<FileContent>(response);
}
private async Task<HttpResponseMessage> GetAsync(string path)
{
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
return await client.GetAsync(Host + path);
}
private async Task<HttpResponseMessage> 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<HttpResponseMessage> 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;
}
/// <summary>
/// Parse response
/// </summary>
private async Task<T> ParseResp<T>(HttpResponseMessage response)
{
var responseText = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return System.Text.Json.JsonSerializer.Deserialize<T>(responseText) ?? default;
}
var error = System.Text.Json.JsonSerializer.Deserialize<ErrorResponse>(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;
}
}
}
}

View File

@ -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;
}
/// <summary>
///
/// </summary>
public string role { get; set; }
/// <summary>
///
/// </summary>
public string content { get; set; }
}
/// <summary>
/// chat请求参数
/// </summary>
public class ChatReq
{
/// <summary>
/// 使用的模型
/// <para>例如[ moonshot-v1-8k ]</para>
/// </summary>
public string model { get; set; } = "moonshot-v1-8k";
/// <summary>
/// 消息主体
/// </summary>
public List<MessagesItem> messages { get; set; }
/// <summary>
/// 使用什么采样温度,介于 0 和 1 之间。较高的值(如 0.7)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定性
/// <para>默认为 0如果设置值域须为 [0, 1] 我们推荐 0.3,以达到较合适的效果</para>
/// </summary>
public double temperature { get; set; }
/// <summary>
/// 聊天完成时生成的最大 token 数。如果到生成了最大 token 数个结果仍然没有结束finish reason 会是 "length", 否则会是 "stop"
/// <para>这个值建议按需给个合理的值,如果不给的话,我们会给一个不错的整数比如 1024。特别要注意的是这个 max_tokens 是指您期待我们返回的 token 长度,而不是输入 + 输出的总长度。比如对一个 moonshot-v1-8k 模型,它的最大输入 + 输出总长度是 8192当输入 messages 总长度为 4096 的时候,您最多只能设置为 4096否则我们服务会返回不合法的输入参数 invalid_request_error ),并拒绝回答。如果您希望获得“输入的精确 token 数”,可以使用下面的“计算 Token” API 使用我们的计算器获得计数</para>
/// </summary>
public int? max_tokens { get; set; }
/// <summary>
/// 另一种采样方法,即模型考虑概率质量为 top_p 的标记的结果。因此0.1 意味着只考虑概率质量最高的 10% 的标记。一般情况下,我们建议改变这一点或温度,但不建议 同时改变
/// </summary>
public float? top_p { get; set; } = 1.0f;
/// <summary>
/// 为每条输入消息生成多少个结果
/// <para>默认为 1不得大于 5。特别的当 temperature 非常小靠近 0 的时候,我们只能返回 1 个结果,如果这个时候 n 已经设置并且 > 1我们的服务会返回不合法的输入参数(invalid_request_error)</para>
/// </summary>
public int? n { get; set; } = 1;
/// <summary>
/// 存在惩罚,介于-2.0到2.0之间的数字。正值会根据新生成的词汇是否出现在文本中来进行惩罚,增加模型讨论新话题的可能性
/// <para>默认为 0</para>
/// </summary>
public float? presence_penalty { get; set; } = 0;
/// <summary>
/// 频率惩罚,介于-2.0到2.0之间的数字。正值会根据新生成的词汇在文本中现有的频率来进行惩罚,减少模型一字不差重复同样话语的可能性
/// <para>默认为 0</para>
/// </summary>
public float? frequency_penalty { get; set; } = 0;
/// <summary>
/// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。最多不能超过 5 个字符串,每个字符串不得超过 32 字节
/// <para>默认 null</para>
/// </summary>
public List<string>? stop { get; set; }
/// <summary>
/// 是否流式返回
/// <para>false</para>
/// </summary>
public bool stream { get; set; } = false;
}
public class PermissionItem
{
/// <summary>
///
/// </summary>
public int created { get; set; }
/// <summary>
///
/// </summary>
public string id { get; set; }
/// <summary>
///
/// </summary>
public string @object { get; set; }
/// <summary>
///
/// </summary>
public string allow_create_engine { get; set; }
/// <summary>
///
/// </summary>
public string allow_sampling { get; set; }
/// <summary>
///
/// </summary>
public string allow_logprobs { get; set; }
/// <summary>
///
/// </summary>
public string allow_search_indices { get; set; }
/// <summary>
///
/// </summary>
public string allow_view { get; set; }
/// <summary>
///
/// </summary>
public string allow_fine_tuning { get; set; }
/// <summary>
///
/// </summary>
public string organization { get; set; }
/// <summary>
///
/// </summary>
public string @group { get; set; }
/// <summary>
///
/// </summary>
public string is_blocking { get; set; }
}
public class ModelInfo
{
/// <summary>
///
/// </summary>
public int created { get; set; }
/// <summary>
///
/// </summary>
public string id { get; set; }
/// <summary>
///
/// </summary>
public string @object { get; set; }
/// <summary>
///
/// </summary>
public string owned_by { get; set; }
/// <summary>
///
/// </summary>
public List<PermissionItem> permission { get; set; }
/// <summary>
///
/// </summary>
public string root { get; set; }
/// <summary>
///
/// </summary>
public string parent { get; set; }
}
public class ModelListResp
{
/// <summary>
///
/// </summary>
public string @object { get; set; }
/// <summary>
///
/// </summary>
public List<ModelInfo> data { get; set; }
}
public class FileListResp
{
/// <summary>
///
/// </summary>
public string @object { get; set; }
/// <summary>
///
/// </summary>
public List<FileItem> data { get; set; }
}
public class FileContent
{
/// <summary>
///
/// </summary>
public string content { get; set; }
/// <summary>
///
/// </summary>
public string file_type { get; set; }
/// <summary>
///
/// </summary>
public string filename { get; set; }
/// <summary>
///
/// </summary>
public string title { get; set; }
/// <summary>
///
/// </summary>
public string type { get; set; }
}
public class FileItem
{
/// <summary>
///
/// </summary>
public string id { get; set; }
/// <summary>
///
/// </summary>
public string @object { get; set; }
/// <summary>
///
/// </summary>
public int bytes { get; set; }
/// <summary>
///
/// </summary>
public int created_at { get; set; }
/// <summary>
///
/// </summary>
public string filename { get; set; }
/// <summary>
///
/// </summary>
public string purpose { get; set; }
/// <summary>
///
/// </summary>
public string status { get; set; }
/// <summary>
///
/// </summary>
public string status_details { get; set; }
}
public class ErrorMsg
{
/// <summary>
///
/// </summary>
public string message { get; set; }
/// <summary>
///
/// </summary>
public string type { get; set; }
}
public class ErrorResponse
{
/// <summary>
///
/// </summary>
public ErrorMsg error { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Ffmpeg处理程序
/// </summary>
public class FFMPGEHandle
{
/// <summary>
///
/// </summary>
public static string FFmpegPath = Path.Combine(AppCommon.AIModelFile, "ffmpeg.exe");
/// <summary>
/// 音频转码为 wav_16k
/// </summary>
/// <param name="task">任务id</param>
/// <returns></returns>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// 说话人模型
/// <para>pyannote </para>
/// </summary>
public class Speaker
{
private static OfflineSpeakerDiarization? SD;
/// <summary>
/// 初始化 Speaker
/// </summary>
/// <param name="speakerNumber"></param>
/// <param name="threshold"></param>
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);
}
/// <summary>
/// 运行 说话人日志
/// </summary>
/// <param name="task"></param>
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);
}
}
/// <summary>
/// 讲话人日志结果
/// </summary>
public class OfflineSpeakerRes
{
/// <summary>
/// 总持续时间
/// </summary>
[JsonIgnore]
public decimal Total => End - Start;
/// <summary>
/// 开始时间
/// </summary>
public decimal Start { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public decimal End { get; set; }
/// <summary>
/// 讲话人索引
/// </summary>
public int SpeakerIndex { get; set; }
/// <summary>
/// 讲话人日志结果
/// </summary>
/// <param name="sds"></param>
public OfflineSpeakerRes(OfflineSpeakerDiarizationSegment sds)
{
Start = (decimal)sds.Start;
End = (decimal)sds.End;
SpeakerIndex = sds.Speaker;
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,35 @@
using Whisper.net;
namespace VideoAnalysisCore.AICore.Whisper
{
/// <summary>
/// 字幕识别 结果
/// </summary>
public class WhisperResDto
{
/// <summary>
///
/// </summary>
/// <param name="sd"></param>
public WhisperResDto(SegmentData sd)
{
Text = sd.Text;
Start = sd.Start;
End = sd.End;
}
/// <summary>
/// 文本
/// </summary>
public string Text { get; } = string.Empty;
/// <summary>
/// 开始时间
/// </summary>
public TimeSpan Start { get; }
/// <summary>
/// 结束时间
/// </summary>
public TimeSpan End { get; }
}
}

View File

@ -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
{
/// <summary>
/// 获取语音字幕
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
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<WhisperResDto>(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);
}
/// <summary>
/// 检测语言的方法
/// </summary>
/// <param name="opt"></param>
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);
}
}
/// <summary>
/// 音频处理选项
/// </summary>
public class WhisperOption
{
/// <summary>
/// 传入目标文件路径
/// </summary>
/// <param name="file"></param>
public WhisperOption(string file)
{
FilePath = file;
}
/// <summary>
/// 指令类型
/// </summary>
public string Command { get; set; } = "transcribe";
/// <summary>
/// 音频文件默认要存放bin目录下
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// 语言,默认自动选择
/// </summary>
public string Language { get; set; } = "chinese";
/// <summary>
/// 模型文件名称
/// </summary>
public string ModelName { get; set; } = "ggml-base.bin";
/// <summary>
/// 模型文件路径
/// </summary>
public string ModPath => Path.Combine(AppCommon.AIModelFile, ModelName);
/// <summary>
/// 模型类型
/// </summary>
//public GgmlType ModelType { get; set; } = GgmlType.Base;
}
}

View File

@ -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
{
/// <summary>
/// 程序 公共变量
/// </summary>
public static class AppCommon
{
/// <summary>
/// 应用有效程序集
/// </summary>
public static readonly IEnumerable<Assembly> Assemblies;
/// <summary>
/// 主库数据库表类型
/// </summary>
public static readonly IEnumerable<Type> 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)));
}
/// <summary>
/// 程序配置
/// </summary>
public static AppConfig Config = new AppConfig();
/// <summary>
/// ServiceProvider
/// </summary>
public static IServiceProvider? Services;
/// <summary>
/// 文件下载路径
/// </summary>
public static string TaskCachedFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TaskCachedFile");
/// <summary>
/// 模型地址
/// </summary>
public static string AIModelFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AICore", "_Static");
}
/// <summary>
/// 拓展函数
/// </summary>
public static class ExpandFunction
{
/// <summary>
/// 获取应用有效程序集
/// </summary>
/// <returns>IEnumerable</returns>
public static List<Assembly> 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;
}
/// <summary>
/// 获取Task处理后的 说话人字幕
/// </summary>
public static TotalCaptionsDto GetSpeakerCaptions(string task)
{
var captionsArr = RedisExpand.Redis.HMGet<SegmentData[]>(RedisExpandKey.Task(task), "Captions").FirstOrDefault();
var speakerArr = RedisExpand.Redis.HMGet<OfflineSpeakerRes[]>(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<SegmentData, List<int>>();
foreach (var segment in captionsArr)
{
var spList = new List<int>();
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.
})
};
}
/// <summary>
/// 转化枚举
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static T? ToEnum<T>(this object data) where T : struct, System.Enum
{
if (data is null || string.IsNullOrEmpty(data?.ToString()))
return null;
return System.Enum.Parse<T>(data.ToString());
}
/// <summary>
/// 转化本地缓存目录
/// </summary>
/// <param name="taskId">任务id</param>
/// <returns></returns>
public static string LocalPath(this string taskId)
{
return Path.Combine(AppCommon.TaskCachedFile, taskId);
}
}
/// <summary>
/// ffmpeg配置
/// </summary>
public class KIMIConfig
{
/// <summary>
/// kimi请求 公开的服务地址
/// </summary>
public string Host { get; set; } = string.Empty;
/// <summary>
/// api的密钥
/// </summary>
public string ApiKey { get; set; } = string.Empty;
}
/// <summary>
/// 文本模型 配置
/// </summary>
public class ChatGptConfig
{
/// <summary>
/// KIMI
/// <para></para>
/// </summary>
public KIMIConfig KIMI { get; set; } = new KIMIConfig();
}
/// <summary>
/// ffmpeg配置
/// </summary>
public class FFmpegConfig
{
/// <summary>
/// 音频切片时间段
/// <para>0不切片</para>
/// </summary>
public int TimeSlice { get; set; } = 0;
}
/// <summary>
/// Whisper配置
/// </summary>
public class WhisperConfig
{
/// <summary>
/// 模型名称
/// </summary>
public string ModelName { get; set; } = string.Empty;
}
/// <summary>
/// redis配置
/// </summary>
public class RedisConfig
{
/// <summary>
/// redis连接字符串
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
}
public class DBConfig
{
/// <summary>
/// 主库链接
/// </summary>
public string ConnectionString { get; set; }=string.Empty;
/// <summary>
/// 数据库类型
/// </summary>
public IocDbType SqlType { get; set; }
/// <summary>
/// 启动时更新表结构
/// </summary>
public bool UpdateTable { get; set; }
}
/// <summary>
/// 应用程序配置
/// </summary>
public class AppConfig
{
/// <summary>
/// redis
/// </summary>
public RedisConfig Redis { get; set; } = new RedisConfig();
/// <summary>
/// Whisper AI
/// </summary>
public WhisperConfig Whisper { get; set; } = new WhisperConfig();
/// <summary>
/// FFmpeg
/// </summary>
public FFmpegConfig FFmpeg { get; set; } = new FFmpegConfig();
/// <summary>
/// ChatGpt
/// </summary>
public ChatGptConfig ChatGpt { get; set; } = new ChatGptConfig();
/// <summary>
/// 数据库配置
/// </summary>
public DBConfig DB { get; set; } = new DBConfig();
}
}

View File

@ -0,0 +1,94 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace VideoAnalysisCore.Common
{
/// <summary>
///
/// </summary>
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", // 默认二进制文件
};
}
/// <summary>
/// 使用 HttpClient 下载任务的资源文件到本地
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// redis key
/// </summary>
public static class RedisExpandKey
{
/// <summary>
/// 基础key
/// </summary>
public const string BaseKey = "VideoAnalysis:";
/// <summary>
/// 基础Channel key
/// </summary>
public const string ChannelKey = BaseKey + "Channel:";
/// <summary>
/// 下载文件
/// </summary>
public const string DownloadFile = ChannelKey + "DownloadFile";
/// <summary>
/// 分离音频
/// </summary>
public const string SeparateAudio = ChannelKey + "SeparateAudio";
/// <summary>
/// 解析字幕
/// </summary>
public const string ParsingCaptions = ChannelKey + "ParsingCaptions";
/// <summary>
/// 解析说话人
/// </summary>
public const string ParsingSpeaker = ChannelKey + "ParsingSpeaker";
/// <summary>
/// Chat模型分析
/// </summary>
public const string ChatModelAnalysis = ChannelKey + "ChatModelAnalysis";
/// <summary>
/// 任务数组
/// </summary>
public const string TaskArr = BaseKey + "TaskArr";
/// <summary>
/// 获取枚举RedisKey
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
public static string EnumKey(RedisChannelEnum e)
{
return ChannelKey + e.ToString();
}
/// <summary>
/// 任务对象地址
/// </summary>
public static string Task(object taskId) => BaseKey + "Task:" + taskId;
}
/// <summary>
/// redis拓展
/// </summary>
public class RedisExpand
{
/// <summary>
/// redis 连接
/// </summary>
public static RedisClient Redis = new RedisClient(AppCommon.Config.Redis.ConnectionString);
/// <summary>
/// 初始化redis
/// <para>需要在初始化配置文件时候调用</para>
/// </summary>
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();
}
/// <summary>
/// 将任务 插入 队列
/// </summary>
/// <param name="enum">枚举</param>
/// <param name="taskId">任务id</param>
public static void InsertChannel(RedisChannelEnum @enum, object taskId)
{
var startTime = Redis.HMGet<Dictionary<RedisChannelEnum, DateTime>>(RedisExpandKey.Task(taskId), "StartTime").FirstOrDefault();
if (startTime is null)
startTime = new Dictionary<RedisChannelEnum, DateTime>();
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);
}
/// <summary>
/// 初始化 队列 任务
/// </summary>
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<IBserGPT>() is null)
throw new Exception("IBserGPT 未注入");
else
return scope.ServiceProvider.GetService<IBserGPT>()?.CallGPT(task)??Task.CompletedTask;
});
});
Redis.SubscribeList(RedisExpandKey.EnumKey(RedisChannelEnum.CallBackSystem),
(msg) => { TouchChannel(RedisChannelEnum.ParsingSpeaker, msg); });
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="msg"></param>
/// <param name="action"></param>
public static async void TouchChannel(RedisChannelEnum key, string msg, Func<string, Task> 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 + " 任务函数 未实现");
}
}
}
}

View File

@ -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<T> : SimpleClient<T> where T : class, new()
{
public Repository()
{
base.Context = DbScoped.SugarScope;
}
}
}

View File

@ -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<IocConfig>() {
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;
}
}
}

View File

@ -0,0 +1,33 @@
namespace VideoAnalysisCore.Enum
{
/// <summary>
/// redis 频道
/// </summary>
public enum RedisChannelEnum
{
/// <summary>
/// 下载文件
/// </summary>
DownloadFile = 1,
/// <summary>
/// 分离音频
/// </summary>
SeparateAudio = 2,
/// <summary>
/// 解析字幕
/// </summary>
ParsingCaptions = 3,
/// <summary>
/// 解析说话人
/// </summary>
ParsingSpeaker = 4,
/// <summary>
/// Chat模型分析
/// </summary>
ChatModelAnalysis = 5,
/// <summary>
/// 回调三方系统
/// </summary>
CallBackSystem = 6
}
}

View File

@ -0,0 +1,29 @@
namespace VideoAnalysisCore.Enum
{
/// <summary>
/// 时间线类型
/// </summary>
public enum TimeBaseTypeEnum
{
/// <summary>
/// 教师讲授
/// </summary>
= 1,
/// <summary>
/// 互动交流
/// </summary>
= 2,
/// <summary>
/// 独立学习
/// </summary>
= 3,
/// <summary>
/// 小组合作
/// </summary>
= 4,
/// <summary>
/// 随堂练习
/// </summary>
}
}

View File

@ -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
{
/// <summary>
/// 课堂评分标准
/// </summary>
[SugarTable("coursegradingcriteria")]
public class CourseGradingCriteria
{
/// <summary>
/// Id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[DisplayName( "编号"), Required]
public long Id { get; set; }
/// <summary>
/// 标准提问词
/// </summary>
[SugarColumn(Length = 100)]
[DisplayName( "标准提问词"), Required]
public string NamePrompt { get; set; }=string.Empty;
/// <summary>
/// 优点展示词
/// </summary>
[SugarColumn(Length = 100)]
[DisplayName( "优点展示词"), Required]
public string Advantage { get; set; } = string.Empty;
/// <summary>
/// 缺点展示词
/// </summary>
[SugarColumn(Length = 100)]
[DisplayName( "缺点展示词"), Required]
public string Flaw { get; set; } = string.Empty;
/// <summary>
/// 改进意见
/// </summary>
[DisplayName( "改进意见")]
[SugarColumn(Length = 100), Required]
public string ImprovedMethods { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,49 @@
using VideoAnalysisCore.Enum;
namespace VideoAnalysisCore.Model.Dto
{
/// <summary>
/// 时间线
/// </summary>
public class TimeBase
{
public TimeBase()
{
}
/// <summary>
/// 开始时间
/// </summary>
public double Start { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public double End { get; set; }
/// <summary>
/// 时间段 类型
/// </summary>
public TimeBaseTypeEnum Type { get; set; }
}
public class TotalCaptionsDto
{
/// <summary>
/// 拼接说话人后的最终字幕
/// </summary>
public string Captions { get; set; } = string.Empty;
/// <summary>
/// 教师发言时间
/// <para>秒</para>
/// </summary>
public decimal TeacherSpeaking { get; set; } = 0;
/// <summary>
/// 学生发言时间
/// <para>秒</para>
/// </summary>
public decimal StudentSpeaking { get; set; } = 0;
/// <summary>
/// 视频时间轴
/// </summary>
public IEnumerable<TimeBase>? TimeBase { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// 视频任务模型
/// </summary>
[SugarTable("videotask")]
public class VideoTask
{
/// <summary>
/// 任务id
/// <para>视频音频文件地址都使用taskID能获取</para>
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public long Id { get; set; }
/// <summary>
/// 媒体路径
/// </summary>
public string MediaUrl { get; set; } = string.Empty;
/// <summary>
/// 下载后本地媒体目录
/// </summary>
public string LocalMediaPath { get; set; } = string.Empty;
/// <summary>
/// 上一次执行的枚举
/// </summary>
public RedisChannelEnum LastEnum { get; set; }
/// <summary>
/// ApiKey
/// </summary>
public string ApiToken { get; set; } = string.Empty;
/// <summary>
/// 请求来自哪个ip地址
/// </summary>
public string ComeFrom { get; set; } = string.Empty;
/// <summary>
/// 自定义值 任务完成后附带通知
/// </summary>
[SugarColumn(Length = 500)]
public string Tag { get; set; }
/// <summary>
///回调Api地址
/// </summary>
[SugarColumn(Length = 500)]
public string CallBackUrl { get; set; }
/// <summary>
/// 字幕缓存
/// </summary>
[SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
public SegmentData[]? Captions { get; set; }
/// <summary>
/// 说话人日志解析缓存
/// </summary>
[SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
public OfflineSpeakerRes[]? Speaker { get; set; }
/// <summary>
/// Chat模型分析缓存
/// </summary>
[SugarColumn(ColumnDataType = "longtext", IsNullable = true)]
public object[]? ChatAnalysis { get; set; }
/// <summary>
/// 消耗token
/// </summary>
public int TotalTokens { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
/// <summary>
/// 开始时间轴
/// </summary>
[SugarColumn(ColumnDataType = "varchar", Length =255)]
public Dictionary<RedisChannelEnum, DateTime> StartTime { get; set; }
}
}

View File

@ -0,0 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="AICore\_Static\ffmpeg.exe" />
<None Remove="AICore\_Static\ggml-base.bin" />
<None Remove="AICore\_Static\ggml-large-v3.bin" />
<None Remove="AICore\_Static\ggml-medium.bin" />
<None Remove="AICore\_Static\ggml-small.bin" />
<None Remove="AICore\_Static\sherpa-onnx-pyannote-segmentation-3-0\model.int8.onnx" />
<None Remove="AICore\_Static\sherpa-onnx-pyannote-segmentation-3-0\model.onnx" />
<None Remove="AICore\_Static\wespeaker\wespeaker_zh_cnceleb_resnet34_LM.onnx" />
</ItemGroup>
<ItemGroup>
<Content Include="AICore\_Static\ffmpeg.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\ggml-base.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\ggml-large-v3.bin">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\ggml-medium.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\ggml-small.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\sherpa-onnx-pyannote-segmentation-3-0\model.int8.onnx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\sherpa-onnx-pyannote-segmentation-3-0\model.onnx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="AICore\_Static\wespeaker\wespeaker_zh_cnceleb_resnet34_LM.onnx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FreeRedis" Version="1.3.2" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="org.k2fsa.sherpa.onnx" Version="1.10.28" />
<PackageReference Include="SqlSugar.IOC" Version="2.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.170" />
<PackageReference Include="Whisper.net" Version="1.5.0" />
<PackageReference Include="Whisper.net.Runtime" Version="1.5.0" />
<PackageReference Include="xFFmpeg.NET" Version="6.0.0" />
</ItemGroup>
</Project>