Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
72492e6c
提交
72492e6c
authored
8月 14, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
refactor: 重构数字人模块
上级
5452e724
全部展开
隐藏空白字符变更
内嵌
并排
正在显示
14 个修改的文件
包含
1045 行增加
和
14 行删除
+1045
-14
package.json
apps/admin/package.json
+9
-9
xiling.js
apps/admin/src/components/editor/api/xiling.js
+42
-1
digitalHumanRequest.js
.../admin/src/components/editor/hooks/digitalHumanRequest.js
+228
-0
digitalHumanTaskStore.js
...dmin/src/components/editor/hooks/digitalHumanTaskStore.js
+39
-0
useDigitalHuman.js
apps/admin/src/components/editor/hooks/useDigitalHuman.js
+0
-0
digital-human-request.test.ts
...ngeditor-customer/__tests__/digital-human-request.test.ts
+212
-0
digital-human-task-store.test.ts
...ditor-customer/__tests__/digital-human-task-store.test.ts
+60
-0
DigitalHumanBaiduUploadField.jsx
...stomer/features/ai-media/DigitalHumanBaiduUploadField.jsx
+129
-0
DigitalHumanCustomizePanel.jsx
...customer/features/ai-media/DigitalHumanCustomizePanel.jsx
+157
-0
DigitalHumanDialog.jsx
...geditor-customer/features/ai-media/DigitalHumanDialog.jsx
+0
-0
DigitalHumanUploadField.jsx
...or-customer/features/ai-media/DigitalHumanUploadField.jsx
+165
-0
digital-human.less
.../wangeditor-customer/features/ai-media/digital-human.less
+0
-0
package.json
apps/learning/package.json
+4
-4
pnpm-lock.yaml
pnpm-lock.yaml
+0
-0
没有找到文件。
apps/admin/package.json
浏览文件 @
72492e6c
...
@@ -28,20 +28,20 @@
...
@@ -28,20 +28,20 @@
"axios"
:
"^1.18.1"
,
"axios"
:
"^1.18.1"
,
"dayjs"
:
"^1.11.11"
,
"dayjs"
:
"^1.11.11"
,
"easy-formula-editor"
:
"^0.0.2-alpha.1"
,
"easy-formula-editor"
:
"^0.0.2-alpha.1"
,
"echarts"
:
"^
5.4.3
"
,
"echarts"
:
"^
6.1.0
"
,
"file-saver"
:
"^2.0.5"
,
"file-saver"
:
"^2.0.5"
,
"highlight.js"
:
"^11.11.1"
,
"highlight.js"
:
"^11.11.1"
,
"jquery"
:
"^3.7.1"
,
"jquery"
:
"^3.7.1"
,
"js-cookie"
:
"^3.0.
5
"
,
"js-cookie"
:
"^3.0.
8
"
,
"js-md5"
:
"^0.8.3"
,
"js-md5"
:
"^0.8.3"
,
"lodash-es"
:
"^4.1
7.2
1"
,
"lodash-es"
:
"^4.1
8.
1"
,
"markdown-it"
:
"^14.
1
.0"
,
"markdown-it"
:
"^14.
3
.0"
,
"mathjax"
:
"3.2.2"
,
"mathjax"
:
"3.2.2"
,
"nanoid"
:
"^3.3.8"
,
"nanoid"
:
"^3.3.
1
8"
,
"react"
:
"^18.3.1"
,
"react"
:
"^18.3.1"
,
"react-dom"
:
"^18.3.1"
,
"react-dom"
:
"^18.3.1"
,
"react-error-boundary"
:
"^4.0.13"
,
"react-error-boundary"
:
"^4.0.13"
,
"react-router"
:
"^7.18.
1
"
,
"react-router"
:
"^7.18.
2
"
,
"react-use"
:
"^17.6.0"
,
"react-use"
:
"^17.6.0"
,
"snabbdom"
:
"^3.6.2"
,
"snabbdom"
:
"^3.6.2"
,
"zustand"
:
"^4.5.5"
"zustand"
:
"^4.5.5"
...
@@ -52,14 +52,14 @@
...
@@ -52,14 +52,14 @@
"@testing-library/user-event"
:
"^14.6.1"
,
"@testing-library/user-event"
:
"^14.6.1"
,
"@types/react"
:
"^18.3.7"
,
"@types/react"
:
"^18.3.7"
,
"@types/react-dom"
:
"^18.3.0"
,
"@types/react-dom"
:
"^18.3.0"
,
"@vitejs/plugin-react"
:
"^6.0.
3
"
,
"@vitejs/plugin-react"
:
"^6.0.
5
"
,
"jsdom"
:
"^29.1.1"
,
"jsdom"
:
"^29.1.1"
,
"less"
:
"^4.2.0"
,
"less"
:
"^4.2.0"
,
"oxfmt"
:
"^0.59.0"
,
"oxfmt"
:
"^0.59.0"
,
"oxlint"
:
"^1.74.0"
,
"oxlint"
:
"^1.74.0"
,
"typescript"
:
"^7.0.2"
,
"typescript"
:
"^7.0.2"
,
"vite"
:
"^8.
1.5
"
,
"vite"
:
"^8.
2.1
"
,
"vite-plugin-mkcert"
:
"^
1.17.1
0"
,
"vite-plugin-mkcert"
:
"^
2.1.
0"
,
"vitest"
:
"^4.1.10"
"vitest"
:
"^4.1.10"
},
},
"browserslist"
:
[
"browserslist"
:
[
...
...
apps/admin/src/components/editor/api/xiling.js
浏览文件 @
72492e6c
...
@@ -72,9 +72,20 @@ export const upload = async (data, options = {}) => {
...
@@ -72,9 +72,20 @@ export const upload = async (data, options = {}) => {
return
response
.
result
return
response
.
result
}
}
export
const
uploadDigitalHumanFile
=
async
(
file
,
providerType
,
options
=
{})
=>
{
const
data
=
new
FormData
()
data
.
append
(
'file'
,
file
)
const
response
=
await
request
.
post
(
'/api/digitalhuman/open/v1/file/upload'
,
data
,
{
params
:
{
providerType
,
sourceFileName
:
file
.
name
},
headers
:
{
'Content-Type'
:
'multipart/form-data'
},
...
options
,
})
return
response
.
result
}
// 获取数字人列表
// 获取数字人列表
export
const
getDigitalHumanList
=
async
(
params
,
options
)
=>
{
export
const
getDigitalHumanList
=
async
(
params
,
options
)
=>
{
const
{
systemFigure
,
item
=
'VIDEO-DH_2D'
,
pageSize
=
1000
}
=
params
const
{
systemFigure
,
item
=
'VIDEO-DH_2D
_LITE
'
,
pageSize
=
1000
}
=
params
const
response
=
await
request
.
get
(
'/api/digitalhuman/open/v1/figure/query'
,
{
const
response
=
await
request
.
get
(
'/api/digitalhuman/open/v1/figure/query'
,
{
params
:
{
systemFigure
,
item
,
pageSize
},
params
:
{
systemFigure
,
item
,
pageSize
},
...
options
,
...
options
,
...
@@ -82,6 +93,15 @@ export const getDigitalHumanList = async (params, options) => {
...
@@ -82,6 +93,15 @@ export const getDigitalHumanList = async (params, options) => {
return
response
.
result
?.
result
||
[]
return
response
.
result
?.
result
||
[]
}
}
// 获取系统音色列表
export
const
getDigitalHumanVoiceList
=
async
(
params
=
{},
options
)
=>
{
const
response
=
await
request
.
get
(
'/api/digitalhuman/open/v1/tts/persons'
,
{
params
:
{
isSystem
:
true
,
...
params
},
...
options
,
})
return
response
.
result
?.
result
||
response
.
result
||
[]
}
/**
/**
* 提交生成式数字人形象定制任务
* 提交生成式数字人形象定制任务
*/
*/
...
@@ -130,3 +150,24 @@ export const submitVideoAdvanced = async (params, options = {}) => {
...
@@ -130,3 +150,24 @@ export const submitVideoAdvanced = async (params, options = {}) => {
export
const
getVideoAdvancedTask
=
async
(
params
,
options
=
{})
=>
{
export
const
getVideoAdvancedTask
=
async
(
params
,
options
=
{})
=>
{
return
request
.
get
(
'/api/digitalhuman/open/v1/video/advanced/task'
,
{
params
,
...
options
})
return
request
.
get
(
'/api/digitalhuman/open/v1/video/advanced/task'
,
{
params
,
...
options
})
}
}
/**
* 校验照片数字人人像图片
*/
export
const
verifyImageVideo
=
async
(
params
,
options
=
{})
=>
{
return
request
.
post
(
'/api/digitalhuman/open/v1/video/image/verify'
,
params
,
options
)
}
/**
* 提交照片数字人视频任务
*/
export
const
submitImageVideo
=
async
(
params
,
options
=
{})
=>
{
return
request
.
post
(
'/api/digitalhuman/open/v1/video/image/submit'
,
params
,
options
)
}
/**
* 查询照片数字人视频任务
*/
export
const
getImageVideoTask
=
async
(
params
,
options
=
{})
=>
{
return
request
.
get
(
'/api/digitalhuman/open/v1/video/image/task'
,
{
params
,
...
options
})
}
apps/admin/src/components/editor/hooks/digitalHumanRequest.js
0 → 100644
浏览文件 @
72492e6c
export
const
DIGITAL_HUMAN_TYPES
=
{
BASIC
:
'basic'
,
ADVANCED
:
'advanced'
,
FAST
:
'fast'
,
PHOTO
:
'photo'
,
}
export
const
DIGITAL_HUMAN_TYPE_LABELS
=
{
[
DIGITAL_HUMAN_TYPES
.
BASIC
]:
'基础接口'
,
[
DIGITAL_HUMAN_TYPES
.
ADVANCED
]:
'高级接口'
,
[
DIGITAL_HUMAN_TYPES
.
FAST
]:
'已有真人视频'
,
[
DIGITAL_HUMAN_TYPES
.
PHOTO
]:
'照片数字人'
,
}
// 视频组件只在这里切换接口,页面不再把接口能力暴露成四个并列选项。
export
const
DIGITAL_HUMAN_VIDEO_CONFIG
=
{
interfaceType
:
DIGITAL_HUMAN_TYPES
.
BASIC
,
figureItem
:
'VIDEO-DH_2D_LITE'
,
}
export
function
getConfiguredDigitalHumanType
()
{
return
DIGITAL_HUMAN_VIDEO_CONFIG
.
interfaceType
===
DIGITAL_HUMAN_TYPES
.
ADVANCED
?
DIGITAL_HUMAN_TYPES
.
ADVANCED
:
DIGITAL_HUMAN_TYPES
.
BASIC
}
export
const
ADVANCED_TEMPLATES
=
[
{
value
:
't-pf4kqasspwzwyexyte121'
,
label
:
'视频样式 1'
,
ratio
:
'portrait'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/1_8c5a2e7.png'
,
},
{
value
:
't-af4keqsspfzwyexyte123'
,
label
:
'视频样式 2'
,
ratio
:
'portrait'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/2_755cd08.png'
,
},
{
value
:
't-ad4eeqsspfzwyqxyte125'
,
label
:
'视频样式 3'
,
ratio
:
'portrait'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/3_c730e57.png'
,
},
{
value
:
't-cd4eeqsspfzwyqxyte127'
,
label
:
'视频样式 4'
,
ratio
:
'portrait'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/4_d542bdf.png'
,
},
{
value
:
't-hb4eeqsspfzwyqxyte129'
,
label
:
'视频样式 5'
,
ratio
:
'landscape'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/5_5708421.png'
,
},
{
value
:
't-cd4eeqsspfzwyqxyteditu3'
,
label
:
'视频样式 6'
,
ratio
:
'portrait'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%20%284%29_a9ef435.png'
,
},
{
value
:
't-cd4eeqsspfzwyqxyteditu4'
,
label
:
'视频样式 7'
,
ratio
:
'portrait'
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%20%285%29_4f18cab.png'
,
},
{
value
:
't-cd4eeqsspfzwyqxyteditu5'
,
label
:
'视频样式 8'
,
ratio
:
'portrait'
,
withoutFigure
:
true
,
preview
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/%E6%97%A0%E6%95%B0%E5%AD%97%E4%BA%BA%E6%A8%A1%E6%9D%BF_97739d1.png'
,
},
]
function
compact
(
value
)
{
if
(
Array
.
isArray
(
value
))
return
value
.
filter
(
Boolean
)
if
(
!
value
||
typeof
value
!==
'object'
)
return
value
return
Object
.
fromEntries
(
Object
.
entries
(
value
)
.
filter
(([,
item
])
=>
item
!==
''
&&
item
!==
undefined
&&
item
!==
null
)
.
map
(([
key
,
item
])
=>
[
key
,
compact
(
item
)]),
)
}
function
buildDriveParams
(
options
,
includeLanguage
=
true
)
{
if
(
options
.
driveType
===
'VOICE'
)
{
return
{
driveType
:
'VOICE'
,
inputAudioUrl
:
options
.
inputAudioUrl
,
}
}
return
{
driveType
:
'TEXT'
,
text
:
options
.
text
,
ttsParams
:
{
person
:
options
.
voiceId
,
lan
:
includeLanguage
?
options
.
language
||
undefined
:
undefined
,
speed
:
String
(
options
.
speed
??
5
),
volume
:
String
(
options
.
volume
??
5
),
pitch
:
String
(
options
.
pitch
??
5
),
},
}
}
export
function
buildDigitalHumanRequest
(
type
,
options
)
{
const
driveParams
=
buildDriveParams
(
options
,
type
!==
DIGITAL_HUMAN_TYPES
.
ADVANCED
)
if
(
type
===
DIGITAL_HUMAN_TYPES
.
FAST
)
{
return
compact
({
templateVideoId
:
options
.
templateVideoId
,
...
driveParams
,
})
}
if
(
type
===
DIGITAL_HUMAN_TYPES
.
PHOTO
)
{
return
compact
({
inputImageUrl
:
options
.
inputImageUrl
,
model
:
options
.
model
,
...
driveParams
,
subtitleParams
:
{
enabled
:
options
.
driveType
===
'TEXT'
&&
!!
options
.
enableSubtitle
,
subtitlePolicy
:
'SRT'
,
},
})
}
if
(
type
===
DIGITAL_HUMAN_TYPES
.
ADVANCED
)
{
return
compact
({
figureId
:
options
.
figureId
,
templateId
:
options
.
templateId
,
model
:
options
.
model
,
...
driveParams
,
title
:
options
.
title
,
logoParams
:
options
.
logoUrl
?
{
logoUrl
:
options
.
logoUrl
}
:
undefined
,
bgmParams
:
options
.
bgmUrl
?
{
bgmUrl
:
options
.
bgmUrl
}
:
undefined
,
materialUrl
:
options
.
materialUrl
,
extendBackgroundUrl
:
options
.
extendBackgroundUrl
,
riskTip
:
options
.
riskTip
,
openingMaterial
:
options
.
openingFileId
?
{
fileId
:
options
.
openingFileId
,
mediaType
:
'VIDEO'
}
:
options
.
openingUrl
?
{
fileUrl
:
options
.
openingUrl
,
mediaType
:
'VIDEO'
}
:
undefined
,
endingMaterial
:
options
.
endingFileId
?
{
fileId
:
options
.
endingFileId
,
mediaType
:
'VIDEO'
}
:
options
.
endingUrl
?
{
fileUrl
:
options
.
endingUrl
,
mediaType
:
'VIDEO'
}
:
undefined
,
mashupMaterials
:
options
.
mashupMaterials
?.
map
(({
fileId
,
fileUrl
,
mediaType
})
=>
({
fileId
,
fileUrl
,
mediaType
,
})),
videoParams
:
{
width
:
Number
(
options
.
width
),
height
:
Number
(
options
.
height
),
},
dhParams
:
options
.
dhParams
,
fissionParams
:
options
.
driveType
===
'TEXT'
&&
options
.
fissionFigureIds
?.
length
?
{
figureIds
:
options
.
fissionFigureIds
,
ttsPersons
:
options
.
fissionVoiceIds
,
}
:
undefined
,
})
}
return
compact
({
figureId
:
options
.
figureId
,
...
driveParams
,
backgroundImageUrl
:
options
.
transparent
?
undefined
:
options
.
backgroundImageUrl
,
playControlParams
:
options
.
enableSegmentLoop
?
{
controlType
:
'SEGMENT_LOOP'
,
startTimestamp
:
Number
(
options
.
startTimestamp
||
0
),
endTimestamp
:
Number
(
options
.
endTimestamp
),
}
:
undefined
,
videoParams
:
{
width
:
Number
(
options
.
width
),
height
:
Number
(
options
.
height
),
transparent
:
!!
options
.
transparent
,
},
dhParams
:
options
.
dhParams
||
(
options
.
positionMode
===
'camera'
?
{
cameraId
:
Number
(
options
.
cameraId
)
}
:
options
.
positionMode
===
'pixel'
?
{
positionV2
:
{
crop
:
{
top
:
Number
(
options
.
cropTop
),
left
:
Number
(
options
.
cropLeft
),
width
:
Number
(
options
.
cropWidth
),
height
:
Number
(
options
.
cropHeight
),
},
location
:
{
top
:
Number
(
options
.
locationTop
),
left
:
Number
(
options
.
locationLeft
),
width
:
Number
(
options
.
locationWidth
),
height
:
Number
(
options
.
locationHeight
),
},
},
}
:
options
.
positionX
!==
undefined
?
{
position
:
{
x
:
Number
(
options
.
positionX
),
y
:
Number
(
options
.
positionY
),
z
:
Number
(
options
.
positionZ
),
},
}
:
undefined
),
autoAnimoji
:
options
.
driveType
===
'TEXT'
&&
!!
options
.
autoAnimoji
,
subtitleParams
:
{
enabled
:
options
.
driveType
===
'TEXT'
&&
!!
options
.
enableSubtitle
,
subtitlePolicy
:
'SRT'
,
},
})
}
apps/admin/src/components/editor/hooks/digitalHumanTaskStore.js
0 → 100644
浏览文件 @
72492e6c
const
STORAGE_KEY
=
'center-book:digital-human-tasks'
const
MAX_TASKS
=
10
function
getStorage
(
storage
)
{
if
(
storage
)
return
storage
if
(
typeof
window
===
'undefined'
)
return
null
return
window
.
localStorage
}
export
function
readDigitalHumanTasks
(
storage
)
{
try
{
const
value
=
getStorage
(
storage
)?.
getItem
(
STORAGE_KEY
)
const
tasks
=
value
?
JSON
.
parse
(
value
)
:
[]
return
Array
.
isArray
(
tasks
)
?
tasks
:
[]
}
catch
{
return
[]
}
}
export
function
saveDigitalHumanTask
(
task
,
storage
)
{
const
target
=
getStorage
(
storage
)
if
(
!
target
||
!
task
?.
taskId
)
return
[]
const
tasks
=
readDigitalHumanTasks
(
target
)
const
nextTasks
=
[
task
,
...
tasks
.
filter
((
item
)
=>
item
.
taskId
!==
task
.
taskId
)].
slice
(
0
,
MAX_TASKS
,
)
try
{
target
.
setItem
(
STORAGE_KEY
,
JSON
.
stringify
(
nextTasks
))
}
catch
{
return
tasks
}
return
nextTasks
}
export
function
clearDigitalHumanTasks
(
storage
)
{
getStorage
(
storage
)?.
removeItem
(
STORAGE_KEY
)
}
apps/admin/src/components/editor/hooks/useDigitalHuman.js
浏览文件 @
72492e6c
差异被折叠。
点击展开。
apps/admin/src/components/editor/wangeditor-customer/__tests__/digital-human-request.test.ts
0 → 100644
浏览文件 @
72492e6c
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
buildDigitalHumanRequest
,
DIGITAL_HUMAN_TYPES
}
from
'../../hooks/digitalHumanRequest'
const
common
=
{
driveType
:
'TEXT'
,
text
:
'数字人测试。'
,
voiceId
:
'5116'
,
language
:
'Chinese'
,
speed
:
5
,
volume
:
6
,
pitch
:
4
,
}
describe
(
'digital human request builder'
,
()
=>
{
it
(
'builds the basic video contract'
,
()
=>
{
expect
(
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
BASIC
,
{
...
common
,
figureId
:
'1081'
,
width
:
720
,
height
:
1280
,
transparent
:
false
,
enableSubtitle
:
true
,
positionX
:
0
,
positionY
:
0.2
,
positionZ
:
1
,
}),
).
toMatchObject
({
figureId
:
'1081'
,
driveType
:
'TEXT'
,
ttsParams
:
{
person
:
'5116'
,
speed
:
'5'
,
volume
:
'6'
,
pitch
:
'4'
},
videoParams
:
{
width
:
720
,
height
:
1280
,
transparent
:
false
},
dhParams
:
{
position
:
{
x
:
0
,
y
:
0.2
,
z
:
1
}
},
subtitleParams
:
{
enabled
:
true
,
subtitlePolicy
:
'SRT'
},
})
})
it
(
'builds the advanced video media and fission contract'
,
()
=>
{
const
result
=
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
ADVANCED
,
{
...
common
,
figureId
:
'1081'
,
templateId
:
'template-1'
,
width
:
1080
,
height
:
1920
,
model
:
'turbo_v2'
,
title
:
'标题'
,
logoUrl
:
'https://example.com/logo.png'
,
bgmUrl
:
'https://example.com/bgm.mp3'
,
materialUrl
:
'https://example.com/material.mp4'
,
extendBackgroundUrl
:
'https://example.com/extend.png'
,
riskTip
:
'示例水印'
,
openingFileId
:
'opening-file-id'
,
endingFileId
:
'ending-file-id'
,
mashupMaterials
:
[
{
fileId
:
'image-file-id'
,
fileName
:
'背景.jpg'
,
mediaType
:
'IMAGE'
},
{
fileId
:
'video-file-id'
,
fileName
:
'背景.mp4'
,
mediaType
:
'VIDEO'
},
],
fissionFigureIds
:
[
'1081'
,
'1112'
],
fissionVoiceIds
:
[
'5116'
],
})
expect
(
result
).
toMatchObject
({
figureId
:
'1081'
,
templateId
:
'template-1'
,
model
:
'turbo_v2'
,
title
:
'标题'
,
logoParams
:
{
logoUrl
:
'https://example.com/logo.png'
},
bgmParams
:
{
bgmUrl
:
'https://example.com/bgm.mp3'
},
materialUrl
:
'https://example.com/material.mp4'
,
extendBackgroundUrl
:
'https://example.com/extend.png'
,
riskTip
:
'示例水印'
,
openingMaterial
:
{
fileId
:
'opening-file-id'
,
mediaType
:
'VIDEO'
},
endingMaterial
:
{
fileId
:
'ending-file-id'
,
mediaType
:
'VIDEO'
},
mashupMaterials
:
[
{
fileId
:
'image-file-id'
,
mediaType
:
'IMAGE'
},
{
fileId
:
'video-file-id'
,
mediaType
:
'VIDEO'
},
],
fissionParams
:
{
figureIds
:
[
'1081'
,
'1112'
],
ttsPersons
:
[
'5116'
]
},
})
expect
(
result
).
not
.
toHaveProperty
(
'ttsParams.lan'
)
})
it
(
'does not enable fission without figure IDs'
,
()
=>
{
const
result
=
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
ADVANCED
,
{
...
common
,
figureId
:
'1081'
,
templateId
:
'template-1'
,
width
:
1080
,
height
:
1920
,
fissionVoiceIds
:
[
'5116'
],
})
expect
(
result
).
not
.
toHaveProperty
(
'fissionParams'
)
})
it
(
'supports the basic camera and pixel positioning contracts'
,
()
=>
{
expect
(
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
BASIC
,
{
...
common
,
figureId
:
'1081'
,
width
:
1080
,
height
:
1920
,
positionMode
:
'camera'
,
cameraId
:
3
,
}),
).
toMatchObject
({
dhParams
:
{
cameraId
:
3
}
})
expect
(
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
BASIC
,
{
...
common
,
figureId
:
'1081'
,
width
:
1080
,
height
:
1920
,
positionMode
:
'pixel'
,
cropTop
:
10
,
cropLeft
:
20
,
cropWidth
:
900
,
cropHeight
:
1600
,
locationTop
:
30
,
locationLeft
:
40
,
locationWidth
:
720
,
locationHeight
:
1280
,
}),
).
toMatchObject
({
dhParams
:
{
positionV2
:
{
crop
:
{
top
:
10
,
left
:
20
,
width
:
900
,
height
:
1600
},
location
:
{
top
:
30
,
left
:
40
,
width
:
720
,
height
:
1280
},
},
},
})
})
it
(
'uses the visual preview location for 2D small-sample figures'
,
()
=>
{
expect
(
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
BASIC
,
{
...
common
,
figureId
:
'1081'
,
width
:
1920
,
height
:
1080
,
dhParams
:
{
positionV2
:
{
location
:
{
top
:
-
39
,
left
:
525
,
width
:
1920
,
height
:
1080
},
},
},
}),
).
toMatchObject
({
dhParams
:
{
positionV2
:
{
location
:
{
top
:
-
39
,
left
:
525
,
width
:
1920
,
height
:
1080
},
},
},
})
})
it
(
'builds 123 and photo video contracts with their own inputs'
,
()
=>
{
expect
(
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
FAST
,
{
...
common
,
templateVideoId
:
'https://example.com/123.mp4'
,
}),
).
toMatchObject
({
templateVideoId
:
'https://example.com/123.mp4'
,
driveType
:
'TEXT'
})
expect
(
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
PHOTO
,
{
...
common
,
inputImageUrl
:
'https://example.com/photo.jpg'
,
model
:
'turbo_v2'
,
enableSubtitle
:
true
,
}),
).
toMatchObject
({
inputImageUrl
:
'https://example.com/photo.jpg'
,
model
:
'turbo_v2'
,
subtitleParams
:
{
enabled
:
true
},
})
})
it
(
'uses input audio instead of TTS parameters for voice drive'
,
()
=>
{
const
result
=
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
PHOTO
,
{
driveType
:
'VOICE'
,
inputAudioUrl
:
'https://example.com/voice.mp3'
,
inputImageUrl
:
'https://example.com/photo.jpg'
,
})
expect
(
result
).
toMatchObject
({
driveType
:
'VOICE'
,
inputAudioUrl
:
expect
.
any
(
String
),
subtitleParams
:
{
enabled
:
false
},
})
expect
(
result
).
not
.
toHaveProperty
(
'ttsParams'
)
})
it
(
'omits options that do not take effect in the selected mode'
,
()
=>
{
const
result
=
buildDigitalHumanRequest
(
DIGITAL_HUMAN_TYPES
.
BASIC
,
{
driveType
:
'VOICE'
,
figureId
:
'1081'
,
inputAudioUrl
:
'https://example.com/voice.mp3'
,
width
:
1080
,
height
:
1920
,
transparent
:
true
,
backgroundImageUrl
:
'https://example.com/background.jpg'
,
autoAnimoji
:
true
,
enableSubtitle
:
true
,
})
expect
(
result
).
not
.
toHaveProperty
(
'backgroundImageUrl'
)
expect
(
result
).
toMatchObject
({
autoAnimoji
:
false
,
subtitleParams
:
{
enabled
:
false
},
})
})
})
apps/admin/src/components/editor/wangeditor-customer/__tests__/digital-human-task-store.test.ts
0 → 100644
浏览文件 @
72492e6c
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
clearDigitalHumanTasks
,
readDigitalHumanTasks
,
saveDigitalHumanTask
,
}
from
'../../hooks/digitalHumanTaskStore'
function
createStorage
()
{
const
values
=
new
Map
<
string
,
string
>
()
return
{
getItem
:
(
key
:
string
)
=>
values
.
get
(
key
)
??
null
,
setItem
:
(
key
:
string
,
value
:
string
)
=>
values
.
set
(
key
,
value
),
removeItem
:
(
key
:
string
)
=>
values
.
delete
(
key
),
}
}
describe
(
'digital human task storage'
,
()
=>
{
it
(
'keeps the newest version of a task first'
,
()
=>
{
const
storage
=
createStorage
()
saveDigitalHumanTask
({
taskId
:
'task-1'
,
status
:
'PROCESSING'
},
storage
)
saveDigitalHumanTask
({
taskId
:
'task-2'
,
status
:
'SUCCESS'
},
storage
)
saveDigitalHumanTask
({
taskId
:
'task-1'
,
status
:
'FAILED'
},
storage
)
expect
(
readDigitalHumanTasks
(
storage
)).
toEqual
([
{
taskId
:
'task-1'
,
status
:
'FAILED'
},
{
taskId
:
'task-2'
,
status
:
'SUCCESS'
},
])
})
it
(
'keeps at most ten recent tasks and can clear them'
,
()
=>
{
const
storage
=
createStorage
()
for
(
let
index
=
0
;
index
<
12
;
index
+=
1
)
{
saveDigitalHumanTask
({
taskId
:
`task-
${
index
}
`
,
status
:
'SUCCESS'
},
storage
)
}
expect
(
readDigitalHumanTasks
(
storage
)).
toHaveLength
(
10
)
expect
(
readDigitalHumanTasks
(
storage
)[
0
].
taskId
).
toBe
(
'task-11'
)
clearDigitalHumanTasks
(
storage
)
expect
(
readDigitalHumanTasks
(
storage
)).
toEqual
([])
})
it
(
'ignores invalid stored data'
,
()
=>
{
const
storage
=
createStorage
()
storage
.
setItem
(
'center-book:digital-human-tasks'
,
'{invalid'
)
expect
(
readDigitalHumanTasks
(
storage
)).
toEqual
([])
})
it
(
'does not break generation when browser storage is unavailable'
,
()
=>
{
const
storage
=
createStorage
()
storage
.
setItem
=
()
=>
{
throw
new
Error
(
'storage unavailable'
)
}
expect
(
saveDigitalHumanTask
({
taskId
:
'task-1'
,
status
:
'PROCESSING'
},
storage
)).
toEqual
([])
})
})
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/DigitalHumanBaiduUploadField.jsx
0 → 100644
浏览文件 @
72492e6c
import
{
useState
}
from
'react'
import
{
App
,
Button
,
Upload
}
from
'antd'
import
{
DeleteOutlined
,
UploadOutlined
}
from
'@ant-design/icons'
import
{
uploadDigitalHumanFile
}
from
'@/components/editor/api/xiling'
const
PROVIDER_TYPE
=
'OPEN_VIDEO_DURATION_VIDEO'
const
IMAGE_EXTENSIONS
=
[
'png'
,
'jpg'
,
'jpeg'
,
'bmp'
]
function
getExtension
(
file
)
{
return
file
.
name
.
split
(
'.'
).
pop
()?.
toLowerCase
()
||
''
}
function
getMediaType
(
file
)
{
return
file
.
type
.
startsWith
(
'image/'
)
||
IMAGE_EXTENSIONS
.
includes
(
getExtension
(
file
))
?
'IMAGE'
:
'VIDEO'
}
function
matchesAccept
(
file
,
accept
)
{
const
extension
=
getExtension
(
file
)
return
accept
.
split
(
','
).
some
((
item
)
=>
{
const
pattern
=
item
.
trim
().
toLowerCase
()
if
(
pattern
.
startsWith
(
'.'
))
return
pattern
===
`.
${
extension
}
`
if
(
pattern
.
endsWith
(
'/*'
))
return
file
.
type
.
startsWith
(
pattern
.
slice
(
0
,
-
1
))
return
file
.
type
.
toLowerCase
()
===
pattern
})
}
function
getFileLimit
(
file
)
{
return
getMediaType
(
file
)
===
'IMAGE'
?
3
:
500
}
export
default
function
DigitalHumanBaiduUploadField
({
label
,
value
,
onChange
,
accept
,
multiple
=
false
,
maxCount
=
1
,
hint
,
})
{
const
{
message
}
=
App
.
useApp
()
const
[
pendingUploads
,
setPendingUploads
]
=
useState
(
0
)
const
uploading
=
pendingUploads
>
0
const
files
=
multiple
?
value
||
[]
:
value
?
[
value
]
:
[]
const
uploadDisabled
=
uploading
||
(
multiple
&&
files
.
length
>=
maxCount
)
const
upload
=
async
({
file
,
onSuccess
,
onError
})
=>
{
if
(
!
matchesAccept
(
file
,
accept
))
{
const
error
=
new
Error
(
'文件格式不符合要求'
)
message
.
error
(
error
.
message
)
onError
(
error
)
return
}
const
limit
=
getFileLimit
(
file
)
if
(
file
.
size
>
limit
*
1024
*
1024
)
{
const
error
=
new
Error
(
`
${
getMediaType
(
file
)
===
'IMAGE'
?
'图片'
:
'视频'
}
不能超过
${
limit
}
MB`
,
)
message
.
error
(
error
.
message
)
onError
(
error
)
return
}
setPendingUploads
((
current
)
=>
current
+
1
)
try
{
const
result
=
await
uploadDigitalHumanFile
(
file
,
PROVIDER_TYPE
)
if
(
!
result
?.
fileId
)
throw
new
Error
(
'上传结果缺少文件编号'
)
const
nextFile
=
{
fileId
:
result
.
fileId
,
fileName
:
result
.
fileName
||
file
.
name
,
mediaType
:
getMediaType
(
file
),
}
onChange
(
multiple
?
(
current
)
=>
[...(
current
||
[]),
nextFile
].
slice
(
0
,
maxCount
)
:
nextFile
)
onSuccess
(
result
)
message
.
success
(
`
${
label
}
上传成功`
)
}
catch
(
error
)
{
message
.
error
(
`
${
label
}
上传失败`
)
onError
(
error
)
}
finally
{
setPendingUploads
((
current
)
=>
Math
.
max
(
0
,
current
-
1
))
}
}
const
remove
=
(
index
)
=>
{
if
(
multiple
)
onChange
(
files
.
filter
((
_
,
itemIndex
)
=>
itemIndex
!==
index
))
else
onChange
(
null
)
}
return
(
<
div
className=
'media-field baidu-upload-field'
>
<
div
className=
'field-label'
>
{
label
}
</
div
>
<
Upload
accept=
{
accept
}
multiple=
{
multiple
}
maxCount=
{
maxCount
}
showUploadList=
{
false
}
customRequest=
{
upload
}
disabled=
{
uploadDisabled
}
>
<
Button
icon=
{
<
UploadOutlined
/>
}
loading=
{
uploading
}
disabled=
{
uploadDisabled
}
>
{
multiple
?
`添加${label}(${files.length}/${maxCount})`
:
files
.
length
?
`更换${label}`
:
`上传${label}`
}
</
Button
>
</
Upload
>
{
files
.
length
?
(
<
div
className=
'baidu-upload-list'
>
{
files
.
map
((
file
,
index
)
=>
(
<
div
className=
'baidu-upload-item'
key=
{
file
.
fileId
}
>
<
span
>
{
file
.
fileName
}
</
span
>
<
Button
type=
'text'
size=
'small'
icon=
{
<
DeleteOutlined
/>
}
aria
-
label=
{
`删除${file.fileName}`
}
onClick=
{
()
=>
remove
(
index
)
}
/>
</
div
>
))
}
</
div
>
)
:
null
}
{
hint
?
<
div
className=
'field-hint'
>
{
hint
}
</
div
>
:
null
}
</
div
>
)
}
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/DigitalHumanCustomizePanel.jsx
0 → 100644
浏览文件 @
72492e6c
import
{
useState
}
from
'react'
import
{
Alert
,
App
,
Button
,
Form
,
Input
,
Radio
,
Tag
}
from
'antd'
import
{
ReloadOutlined
}
from
'@ant-design/icons'
import
{
queryGenerativeFigure
,
submitGenerativeFigure
}
from
'@/components/editor/api/xiling'
import
DigitalHumanUploadField
from
'./DigitalHumanUploadField'
const
customizeStatus
=
{
LINE_UP
:
{
color
:
'default'
,
text
:
'排队中'
},
ACTIONING
:
{
color
:
'processing'
,
text
:
'动作生成中'
},
GENERATING
:
{
color
:
'processing'
,
text
:
'训练中'
},
SUCCESS
:
{
color
:
'success'
,
text
:
'已完成'
},
FAILED
:
{
color
:
'error'
,
text
:
'定制失败'
},
}
function
findFigure
(
response
,
figureId
)
{
const
payload
=
response
?.
result
const
list
=
Array
.
isArray
(
payload
?.
result
)
?
payload
.
result
:
Array
.
isArray
(
payload
)
?
payload
:
payload
?
[
payload
]
:
[]
return
list
.
find
((
item
)
=>
String
(
item
.
figureId
)
===
String
(
figureId
))
||
list
[
0
]
}
export
default
function
DigitalHumanCustomizePanel
({
onSuccess
})
{
const
{
message
}
=
App
.
useApp
()
const
[
imageUrl
,
setImageUrl
]
=
useState
(
''
)
const
[
task
,
setTask
]
=
useState
(
null
)
const
[
submitting
,
setSubmitting
]
=
useState
(
false
)
const
[
querying
,
setQuerying
]
=
useState
(
false
)
const
[
error
,
setError
]
=
useState
(
''
)
const
handleSubmit
=
async
(
values
)
=>
{
if
(
!
imageUrl
)
return
message
.
warning
(
'请先上传参考图片'
)
setSubmitting
(
true
)
setError
(
''
)
try
{
const
response
=
await
submitGenerativeFigure
({
name
:
values
.
name
.
trim
(),
customizeType
:
'GENERATE_NO_ACTION'
,
gender
:
values
.
gender
,
figureImageUrl
:
imageUrl
,
})
const
figureId
=
response
.
result
?.
figureId
if
(
!
figureId
)
throw
new
Error
(
'未获取到形象 ID'
)
setTask
({
figureId
,
name
:
values
.
name
.
trim
(),
status
:
'LINE_UP'
})
message
.
success
(
'形象定制任务已提交'
)
}
catch
(
submitError
)
{
setError
(
submitError
.
response
?.
data
?.
message
?.
global
||
submitError
.
message
||
'提交失败'
)
}
finally
{
setSubmitting
(
false
)
}
}
const
handleQuery
=
async
()
=>
{
if
(
!
task
?.
figureId
)
return
setQuerying
(
true
)
setError
(
''
)
try
{
const
response
=
await
queryGenerativeFigure
({
figureId
:
task
.
figureId
})
const
figure
=
findFigure
(
response
,
task
.
figureId
)
if
(
!
figure
)
throw
new
Error
(
'暂未查询到形象任务'
)
const
nextTask
=
{
...
task
,
...
figure
}
setTask
(
nextTask
)
if
(
nextTask
.
status
===
'SUCCESS'
)
onSuccess
?.()
}
catch
(
queryError
)
{
setError
(
queryError
.
response
?.
data
?.
message
?.
global
||
queryError
.
message
||
'查询失败'
)
}
finally
{
setQuerying
(
false
)
}
}
return
(
<
div
className=
'customize-layout'
>
<
section
className=
'customize-form-card'
>
<
h2
>
生成式数字人形象定制
</
h2
>
<
p
>
上传一张人物图片,生成可用于视频合成的数字人形象。
</
p
>
<
Form
layout=
'vertical'
initialValues=
{
{
gender
:
'MALE'
}
}
requiredMark=
{
false
}
onFinish=
{
handleSubmit
}
>
<
Form
.
Item
label=
'形象名称'
name=
'name'
rules=
{
[
{
required
:
true
,
whitespace
:
true
,
message
:
'请输入形象名称'
},
{
max
:
20
,
message
:
'形象名称不能超过20个字符'
},
]
}
>
<
Input
maxLength=
{
20
}
placeholder=
'请输入形象名称'
/>
</
Form
.
Item
>
<
Form
.
Item
label=
'性别'
name=
'gender'
>
<
Radio
.
Group
optionType=
'button'
buttonStyle=
'solid'
options=
{
[
{
label
:
'男'
,
value
:
'MALE'
},
{
label
:
'女'
,
value
:
'FEMALE'
},
]
}
/>
</
Form
.
Item
>
<
DigitalHumanUploadField
label=
'参考图片'
value=
{
imageUrl
}
onChange=
{
setImageUrl
}
accept=
'image/jpeg,image/png'
maxSizeMB=
{
10
}
filenamePrefix=
'digital-human-figure'
previewType=
'image'
hint=
'支持 JPG、JPEG、PNG,建议使用 1080×1920 或 1920×1080 图片。'
/>
<
Button
type=
'primary'
htmlType=
'submit'
loading=
{
submitting
}
block
>
开始定制
</
Button
>
</
Form
>
</
section
>
<
section
className=
'customize-result-card'
>
<
div
className=
'customize-result-heading'
>
<
h2
>
定制进度
</
h2
>
{
task
?
(
<
Button
icon=
{
<
ReloadOutlined
/>
}
onClick=
{
handleQuery
}
loading=
{
querying
}
>
查询进度
</
Button
>
)
:
null
}
</
div
>
{
error
?
<
Alert
type=
'error'
showIcon
title=
{
error
}
/>
:
null
}
{
task
?
(
<
div
className=
'customize-task'
>
{
task
.
templateImg
||
task
.
pictureUrl
||
imageUrl
?
(
<
img
src=
{
task
.
templateImg
||
task
.
pictureUrl
||
imageUrl
}
alt=
{
`${task.name || '数字人'}形象预览`
}
/>
)
:
null
}
<
div
>
<
strong
>
{
task
.
name
||
'数字人形象'
}
</
strong
>
<
span
>
ID:
{
task
.
figureId
}
</
span
>
<
Tag
color=
{
customizeStatus
[
task
.
status
]?.
color
}
>
{
customizeStatus
[
task
.
status
]?.
text
||
task
.
status
}
</
Tag
>
{
task
.
failedMessage
?
<
small
>
{
task
.
failedMessage
}
</
small
>
:
null
}
</
div
>
</
div
>
)
:
(
<
div
className=
'customize-empty'
>
提交任务后,可在这里查询训练进度。
</
div
>
)
}
</
section
>
</
div
>
)
}
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/DigitalHumanDialog.jsx
浏览文件 @
72492e6c
差异被折叠。
点击展开。
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/DigitalHumanUploadField.jsx
0 → 100644
浏览文件 @
72492e6c
import
{
useState
}
from
'react'
import
{
Button
,
message
,
Upload
}
from
'antd'
import
{
CheckCircleFilled
,
DeleteOutlined
,
UploadOutlined
}
from
'@ant-design/icons'
import
{
uploadFile
}
from
'@/utils/oss'
const
IMAGE_EXTENSIONS
=
[
'png'
,
'jpg'
,
'jpeg'
,
'bmp'
]
const
VIDEO_EXTENSIONS
=
[
'mp4'
]
const
AUDIO_EXTENSIONS
=
[
'wav'
,
'mp3'
,
'wma'
,
'm4a'
]
function
getExtension
(
file
)
{
return
file
.
name
.
split
(
'.'
).
pop
()?.
toLowerCase
()
||
''
}
function
getFileCategory
(
file
)
{
const
extension
=
getExtension
(
file
)
if
(
file
.
type
.
startsWith
(
'image/'
)
||
IMAGE_EXTENSIONS
.
includes
(
extension
))
return
'image'
if
(
file
.
type
.
startsWith
(
'video/'
)
||
VIDEO_EXTENSIONS
.
includes
(
extension
))
return
'video'
if
(
file
.
type
.
startsWith
(
'audio/'
)
||
AUDIO_EXTENSIONS
.
includes
(
extension
))
return
'audio'
return
''
}
function
matchesAccept
(
file
,
accept
)
{
const
extension
=
getExtension
(
file
)
const
category
=
getFileCategory
(
file
)
return
accept
.
split
(
','
).
some
((
item
)
=>
{
const
pattern
=
item
.
trim
().
toLowerCase
()
if
(
pattern
.
startsWith
(
'.'
))
return
pattern
===
`.
${
extension
}
`
if
(
pattern
.
endsWith
(
'/*'
))
return
category
===
pattern
.
slice
(
0
,
-
2
)
return
file
.
type
.
toLowerCase
()
===
pattern
})
}
export
default
function
DigitalHumanUploadField
({
label
,
value
,
onChange
,
accept
,
maxSizeMB
,
maxImageSizeMB
,
maxVideoSizeMB
,
hint
,
uploadHint
,
previewType
,
filenamePrefix
=
'digital-human'
,
})
{
const
[
uploading
,
setUploading
]
=
useState
(
false
)
const
displayLabel
=
label
.
replace
(
'(可选)'
,
''
)
const
upload
=
async
({
file
,
onSuccess
,
onError
})
=>
{
if
(
!
matchesAccept
(
file
,
accept
))
{
const
error
=
new
Error
(
'文件格式不符合要求'
)
message
.
error
(
error
.
message
)
onError
(
error
)
return
}
const
fileLimit
=
getFileCategory
(
file
)
===
'image'
?
(
maxImageSizeMB
??
maxSizeMB
)
:
(
maxVideoSizeMB
??
maxSizeMB
)
if
(
file
.
size
>
fileLimit
*
1024
*
1024
)
{
const
error
=
new
Error
(
`文件不能超过
${
fileLimit
}
MB`
)
message
.
error
(
error
.
message
)
onError
(
error
)
return
}
setUploading
(
true
)
try
{
const
url
=
await
uploadFile
(
file
,
{
filenamePrefix
})
onChange
(
url
)
onSuccess
({
url
})
message
.
success
(
`
${
label
}
上传成功`
)
}
catch
(
error
)
{
message
.
error
(
`
${
label
}
上传失败`
)
onError
(
error
)
}
finally
{
setUploading
(
false
)
}
}
if
(
previewType
===
'image'
)
{
return
(
<
div
className=
'media-field image-media-field'
>
<
div
className=
'field-label'
>
{
label
}
</
div
>
<
div
className=
{
`image-upload-card ${value ? 'has-value' : ''}`
}
>
<
Upload
className=
'image-upload-trigger'
accept=
{
accept
}
showUploadList=
{
false
}
customRequest=
{
upload
}
>
<
button
type=
'button'
className=
'image-upload-button'
disabled=
{
uploading
}
>
{
value
?
(
<
img
src=
{
value
}
alt=
{
`${label}预览`
}
/>
)
:
(
<
span
className=
'image-upload-icon'
>
<
UploadOutlined
/>
</
span
>
)
}
<
span
className=
'image-upload-copy'
>
<
strong
>
{
uploading
?
'正在上传…'
:
value
?
`已选择${displayLabel}`
:
`上传${displayLabel}`
}
</
strong
>
<
small
>
{
value
?
'点击更换图片'
:
uploadHint
||
`文件不能超过 ${maxSizeMB}MB`
}
</
small
>
</
span
>
</
button
>
</
Upload
>
{
value
?
(
<
Button
type=
'text'
icon=
{
<
DeleteOutlined
/>
}
className=
'image-upload-remove'
onClick=
{
()
=>
onChange
(
''
)
}
aria
-
label=
{
`清除${label}`
}
/>
)
:
null
}
</
div
>
{
hint
?
<
div
className=
'field-hint'
>
{
hint
}
</
div
>
:
null
}
</
div
>
)
}
return
(
<
div
className=
'media-field'
>
<
div
className=
'field-label'
>
{
label
}
</
div
>
<
div
className=
{
`teacher-upload ${value ? 'has-value' : ''}`
}
>
<
Upload
accept=
{
accept
}
showUploadList=
{
false
}
customRequest=
{
upload
}
>
<
Button
icon=
{
<
UploadOutlined
/>
}
loading=
{
uploading
}
>
{
value
?
`重新上传${label}`
:
`上传${label}`
}
</
Button
>
</
Upload
>
{
value
?
(
<
span
className=
'upload-ready'
>
<
CheckCircleFilled
/>
已准备好
</
span
>
)
:
null
}
{
value
?
(
<
Button
type=
'text'
size=
'small'
icon=
{
<
DeleteOutlined
/>
}
onClick=
{
()
=>
onChange
(
''
)
}
aria
-
label=
{
`清除${label}`
}
>
清除
</
Button
>
)
:
null
}
</
div
>
{
value
&&
previewType
===
'video'
?
(
<
div
className=
'source-preview video-preview'
>
<
video
src=
{
value
}
controls
preload=
'metadata'
aria
-
label=
{
`${label}预览`
}
/>
<
span
>
请播放检查画面、时长和声音是否正确
</
span
>
</
div
>
)
:
null
}
{
hint
?
<
div
className=
'field-hint'
>
{
hint
}
</
div
>
:
null
}
</
div
>
)
}
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/digital-human.less
浏览文件 @
72492e6c
差异被折叠。
点击展开。
apps/learning/package.json
浏览文件 @
72492e6c
...
@@ -21,7 +21,7 @@
...
@@ -21,7 +21,7 @@
"nuqs"
:
"^2.9.0"
,
"nuqs"
:
"^2.9.0"
,
"react"
:
"^19.2.7"
,
"react"
:
"^19.2.7"
,
"react-dom"
:
"^19.2.7"
,
"react-dom"
:
"^19.2.7"
,
"react-router"
:
"^7.
4.0
"
,
"react-router"
:
"^7.
18.2
"
,
"zustand"
:
"^5.0.3"
"zustand"
:
"^5.0.3"
},
},
"devDependencies"
:
{
"devDependencies"
:
{
...
@@ -31,13 +31,13 @@
...
@@ -31,13 +31,13 @@
"@types/node"
:
"^22.13.9"
,
"@types/node"
:
"^22.13.9"
,
"@types/react"
:
"^19.2.17"
,
"@types/react"
:
"^19.2.17"
,
"@types/react-dom"
:
"^19.2.3"
,
"@types/react-dom"
:
"^19.2.3"
,
"@vitejs/plugin-react"
:
"^6.0.
3
"
,
"@vitejs/plugin-react"
:
"^6.0.
5
"
,
"jsdom"
:
"^29.1.1"
,
"jsdom"
:
"^29.1.1"
,
"oxfmt"
:
"^0.59.0"
,
"oxfmt"
:
"^0.59.0"
,
"oxlint"
:
"^1.74.0"
,
"oxlint"
:
"^1.74.0"
,
"typescript"
:
"^7.0.2"
,
"typescript"
:
"^7.0.2"
,
"vite"
:
"^8.
1.5
"
,
"vite"
:
"^8.
2.1
"
,
"vite-plugin-mkcert"
:
"^
1.17.1
0"
,
"vite-plugin-mkcert"
:
"^
2.1.
0"
,
"vitest"
:
"^4.1.10"
"vitest"
:
"^4.1.10"
}
}
}
}
pnpm-lock.yaml
浏览文件 @
72492e6c
差异被折叠。
点击展开。
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论