Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
D
digitalPerson-fe
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
digitalPerson
digitalPerson-fe
Commits
fab78130
Commit
fab78130
authored
Sep 27, 2024
by
Dazzle Wu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 视频生成联调
parent
a5fc900c
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
232 additions
and
48 deletions
+232
-48
digital-creation.ts
src/apis/digital-creation.ts
+4
-7
user.ts
src/apis/user.ts
+5
-0
creation.ts
src/router/modules/creation.ts
+1
-1
audio-setting.ts
src/store/modules/audio-setting.ts
+35
-0
creation.ts
src/store/types/creation.ts
+18
-0
digital-audio.vue
src/views/creation/components/digital/digital-audio.vue
+50
-10
preview-content.vue
src/views/creation/components/preview-content.vue
+64
-16
subtitle-setting.vue
src/views/creation/components/subtitle/subtitle-setting.vue
+1
-1
header-bar.vue
src/views/creation/layout/header-bar.vue
+52
-11
index.vue
src/views/creation/layout/index.vue
+2
-2
No files found.
src/apis/digital-creation.ts
View file @
fab78130
...
...
@@ -55,7 +55,7 @@ export function fetchTimbreByExample<T>(condition: string) {
return
request
.
post
<
T
>
(
`/bizDigitalHumanTimbreRest/getByExample.json?condition=
${
condition
}
`
)
}
//
保存当前用户的草稿配置
//
根据草稿id获取草稿的配置信息
export
function
fetchDraftConfigById
<
T
>
(
id
:
number
)
{
return
request
.
post
<
T
>
(
`/bizDigitalHumanMemberDraftConfigRest/getById.json?id=
${
id
}
`
)
}
...
...
@@ -65,10 +65,7 @@ export function saveDraftConfig<T>(payload: object) {
return
request
.
post
<
T
>
(
'/bizDigitalHumanMemberDraftConfigRest/saveOrUpdate.json'
,
payload
)
}
// 基础数字人视频
export
function
createBaseVideoDigitalHumanTask
<
T
>
(
callbackUrl
:
string
,
payload
:
object
)
{
return
request
.
post
<
T
>
(
`/aiDigitalHumanServiceRest/createBaseVideoDigitalHumanTask.json?callbackUrl=
${
callbackUrl
}
`
,
payload
,
)
// 导出视频到我的作品
export
function
createDigitalHumanVideoTask
<
T
>
(
payload
:
object
)
{
return
request
.
post
<
T
>
(
'/aiDigitalHumanTaskRest/createDigitalHumanVideoTask.json'
,
payload
)
}
src/apis/user.ts
View file @
fab78130
...
...
@@ -16,3 +16,8 @@ export function fetchSMSCode<T>(phoneNumber: string) {
export
function
fetchEmailCode
<
T
>
(
emailAddress
:
string
)
{
return
request
.
post
<
T
>
(
`/sendEmailRest/sendEmailCode.json?emailAddress=
${
emailAddress
}
`
)
}
// 获取当前会员的余额
export
function
fetchUniversalCurrency
<
T
>
()
{
return
request
.
post
<
T
>
(
'/bizMemberEquityRest/getUniversalCurrency.json'
)
}
src/router/modules/creation.ts
View file @
fab78130
...
...
@@ -3,7 +3,7 @@ import Creation from '@/views/creation/creation.vue'
export
default
[
{
path
:
'/creation'
,
path
:
'/creation
/:templateId/:draftId?
'
,
name
:
'Creation'
,
meta
:
{
rank
:
1001
,
...
...
src/store/modules/audio-setting.ts
0 → 100644
View file @
fab78130
import
{
AudioConfig
,
LanType
,
VoiceType
}
from
'@/store/types/creation'
import
{
defineStore
}
from
'pinia'
function
defaultAudioSetting
():
AudioConfig
{
return
{
lanType
:
LanType
.
CANTONESE
,
voiceType
:
VoiceType
.
CANTONESE_FEMALE
,
}
}
function
getLocalState
():
AudioConfig
{
return
defaultAudioSetting
()
}
export
const
useAudioSettingStore
=
defineStore
(
'audio-setting-store'
,
{
state
:
():
AudioConfig
=>
getLocalState
(),
actions
:
{
setLanType
(
lanType
:
LanType
)
{
this
.
lanType
=
lanType
},
setVoiceType
(
voiceType
:
VoiceType
)
{
this
.
voiceType
=
voiceType
},
updateAUdioSetting
(
audioSetting
:
AudioConfig
)
{
this
.
$state
=
{
...
this
.
$state
,
...
audioSetting
}
},
resetAudioSetting
()
{
this
.
$state
=
defaultAudioSetting
()
},
},
})
src/store/types/creation.ts
View file @
fab78130
...
...
@@ -15,6 +15,22 @@ export enum DriveType {
VOICE
=
'VOICE'
,
}
export
enum
LanType
{
CANTONESE
,
MANDARIN
,
}
export
enum
VoiceType
{
CANTONESE_FEMALE
=
101019
,
MANDARIN_FEMALE
=
1001
,
MANDARIN_MALE
=
1018
,
}
export
interface
AudioConfig
{
lanType
:
LanType
voiceType
:
VoiceType
}
export
interface
DigitalTemplate
{
id
:
number
coverUrl
:
string
|
null
...
...
@@ -138,6 +154,8 @@ export interface DraftConfig {
logoUrl
:
string
|
null
bgmUrl
:
string
|
null
materialUrl
:
string
|
null
memberId
?:
number
modifiedTime
?:
string
}
export
interface
BaseVideoTask
{
...
...
src/views/creation/components/digital/digital-audio.vue
View file @
fab78130
<
script
setup
lang=
"ts"
>
import
{
fetchDigitalHumanTimbreList
,
fetchTimbreByExample
}
from
'@/apis/digital-creation'
import
{
useAudioSettingStore
}
from
'@/store/modules/audio-setting'
import
{
useDigitalCreationStore
}
from
'@/store/modules/creation'
import
{
TimbreItem
}
from
'@/store/types/creation'
import
{
LanType
,
TimbreItem
,
VoiceType
}
from
'@/store/types/creation'
import
{
computed
,
onMounted
,
ref
,
watch
}
from
'vue'
import
DigitalAudioCard
from
'./digital-audio-card.vue'
const
audioSettingStore
=
useAudioSettingStore
()
const
digitalCreationStore
=
useDigitalCreationStore
()
const
lanValue
=
ref
(
0
)
const
lanList
=
ref
([
{
key
:
0
,
label
:
'粵語'
},
{
key
:
1
,
label
:
'普通話'
},
{
key
:
LanType
.
CANTONESE
,
label
:
'粵語'
},
{
key
:
LanType
.
MANDARIN
,
label
:
'普通話'
},
])
const
sexValue
=
ref
(
0
)
const
sexList
=
[
...
...
@@ -23,6 +25,15 @@ const digitalTimbreMaleList = ref<TimbreItem[]>([])
const
showAll
=
ref
(
false
)
const
searchName
=
ref
(
''
)
const
lanType
=
computed
({
get
()
{
return
audioSettingStore
.
lanType
},
set
(
value
)
{
audioSettingStore
.
setLanType
(
value
)
},
})
const
speed
=
computed
({
get
()
{
return
Number
(
digitalCreationStore
.
speed
)
...
...
@@ -42,10 +53,30 @@ const pitch = computed({
})
watch
(
()
=>
[
digitalCreationStore
.
person
,
digitalTimbreList
.
value
.
length
],
([
person
,
len
])
=>
{
if
(
person
&&
len
)
{
digitalTimbreValue
.
value
=
digitalTimbreList
.
value
.
find
((
i
)
=>
i
.
timebreId
===
person
)
()
=>
digitalTimbreList
.
value
.
length
,
(
len
)
=>
{
if
(
len
&&
!
digitalTimbreValue
.
value
)
{
if
(
digitalCreationStore
.
person
)
{
digitalTimbreValue
.
value
=
digitalTimbreList
.
value
.
find
((
i
)
=>
i
.
timebreId
===
digitalCreationStore
.
person
)
lanType
.
value
=
LanType
.
MANDARIN
}
else
{
digitalTimbreValue
.
value
=
digitalTimbreList
.
value
[
0
]
}
}
},
)
watch
(
()
=>
lanType
.
value
,
(
newVal
)
=>
{
if
(
newVal
===
LanType
.
CANTONESE
)
{
audioSettingStore
.
setVoiceType
(
VoiceType
.
CANTONESE_FEMALE
)
}
else
{
if
(
digitalTimbreValue
.
value
?.
sex
===
'男'
)
{
audioSettingStore
.
setVoiceType
(
VoiceType
.
MANDARIN_MALE
)
}
else
{
audioSettingStore
.
setVoiceType
(
VoiceType
.
MANDARIN_FEMALE
)
}
}
},
)
...
...
@@ -73,7 +104,11 @@ async function handleSearch(value: string) {
}
function
handleClickAudioCard
(
timbreItem
:
TimbreItem
)
{
digitalTimbreValue
.
value
=
timbreItem
digitalCreationStore
.
setPerson
(
timbreItem
.
timebreId
)
timbreItem
.
sex
===
'男'
?
audioSettingStore
.
setVoiceType
(
VoiceType
.
MANDARIN_MALE
)
:
audioSettingStore
.
setVoiceType
(
VoiceType
.
MANDARIN_FEMALE
)
}
</
script
>
...
...
@@ -81,10 +116,15 @@ function handleClickAudioCard(timbreItem: TimbreItem) {
<div
class=
"h-full overflow-y-auto px-4 py-2"
>
<div
v-if=
"!showAll"
>
<div
class=
"flex justify-end pb-3"
>
<HorizontalTabs
v-model:value=
"lan
Valu
e"
:list=
"lanList"
/>
<HorizontalTabs
v-model:value=
"lan
Typ
e"
:list=
"lanList"
/>
</div>
<DigitalAudioCard
v-if=
"lanValue"
:value=
"digitalTimbreValue"
show-toggle
@
toggle=
"showAll = true"
/>
<DigitalAudioCard
v-if=
"lanType === LanType.MANDARIN"
:value=
"digitalTimbreValue"
show-toggle
@
toggle=
"showAll = true"
/>
<div
class=
"mt-4 text-lg"
>
聲音
</div>
<div
class=
"mt-4 flex items-center gap-2"
>
...
...
src/views/creation/components/preview-content.vue
View file @
fab78130
<
script
setup
lang=
"ts"
>
import
{
useAudioSettingStore
}
from
'@/store/modules/audio-setting'
import
{
useDigitalCreationStore
}
from
'@/store/modules/creation'
import
{
TextScript
}
from
'@/store/types/creation'
import
{
computed
,
ref
}
from
'vue'
import
{
TextScript
,
VoiceType
}
from
'@/store/types/creation'
import
{
computed
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
let
voiceType
=
VoiceType
.
CANTONESE_FEMALE
let
contentData
=
''
let
websocket
:
WebSocket
const
url
=
'wss://ai-api-sit.gsstcloud.com/websocket/textToSpeechTC.ws'
const
audioSettingStore
=
useAudioSettingStore
()
const
digitalCreationStore
=
useDigitalCreationStore
()
const
isConnected
=
ref
(
false
)
const
audioData
=
ref
(
''
)
const
audioPlaying
=
ref
(
false
)
const
previewContentWidth
=
ref
(
0
)
const
previewContentHeight
=
ref
(
0
)
const
previewContent
=
ref
<
HTMLElement
>
()
const
digitalAudio
=
ref
<
HTMLAudioElement
>
()
const
resizeObserver
=
new
ResizeObserver
((
entries
)
=>
{
const
{
contentRect
}
=
entries
[
0
]
previewContentWidth
.
value
=
contentRect
.
width
previewContentHeight
.
value
=
contentRect
.
height
})
const
previewContentWidth
=
computed
(()
=>
previewContent
.
value
?.
offsetWidth
)
const
previewContentHeight
=
computed
(()
=>
previewContent
.
value
?.
offsetHeight
)
const
digitalHumanWidth
=
computed
(()
=>
(
digitalCreationStore
.
w
*
previewContentWidth
.
value
!
)
/
1080
)
const
digitalHumanHeight
=
computed
(()
=>
(
digitalCreationStore
.
h
*
previewContentHeight
.
value
!
)
/
1920
)
const
digitalHumanLeft
=
computed
(()
=>
(
digitalCreationStore
.
x
*
previewContentWidth
.
value
!
)
/
1080
)
...
...
@@ -22,10 +37,13 @@ const audioUrl = computed({
},
})
const
url
=
'wss://ai-api-sit.gsstcloud.com/websocket/textToSpeechTC.ws'
const
isConnected
=
ref
(
false
)
const
audioData
=
ref
(
''
)
let
websocket
:
WebSocket
onMounted
(()
=>
{
previewContent
.
value
&&
resizeObserver
.
observe
(
previewContent
.
value
)
})
onUnmounted
(()
=>
{
previewContent
.
value
&&
resizeObserver
.
unobserve
(
previewContent
.
value
)
})
function
connectWebSocket
()
{
websocket
=
new
WebSocket
(
url
)
...
...
@@ -55,17 +73,20 @@ function connectWebSocket() {
function
disconnectWebSocket
()
{
if
(
websocket
)
{
websocket
.
close
()
controlAudio
()
}
}
function
sendDataToWebSocket
()
{
voiceType
=
audioSettingStore
.
voiceType
contentData
=
digitalCreationStore
.
text
const
payload
:
TextScript
=
{
codec
:
'mp3'
,
sampleRate
:
16000
,
speed
:
Number
(
digitalCreationStore
.
speed
),
volume
:
Number
(
digitalCreationStore
.
volume
),
voiceType
:
101019
,
content
:
digitalCreationStore
.
text
,
voiceType
:
voiceType
,
content
:
contentData
,
}
websocket
.
send
(
JSON
.
stringify
(
payload
))
}
...
...
@@ -74,7 +95,22 @@ function generatePreview() {
connectWebSocket
()
}
function
playAudio
()
{
function
controlAudio
()
{
if
(
audioPlaying
.
value
)
{
audioPlaying
.
value
=
false
digitalAudio
.
value
?.
pause
()
return
}
if
(
contentData
!==
digitalCreationStore
.
text
)
{
audioData
.
value
=
''
return
}
if
(
voiceType
!==
audioSettingStore
.
voiceType
)
{
audioData
.
value
=
''
return
}
audioPlaying
.
value
=
true
digitalAudio
.
value
?.
play
()
}
</
script
>
...
...
@@ -101,11 +137,23 @@ function playAudio() {
/>
</div>
</div>
<div
class=
"flex
bg-white p
-4"
>
<div
class=
"flex
h-12 bg-white px
-4"
>
<!--
<div
class=
"flex flex-1 items-center text-lg"
>
00:11:22
</div>
-->
<div
class=
"flex flex-1 justify-center"
>
<n-button
v-if=
"!audioData"
type=
"info"
:loading=
"isConnected"
@
click=
"generatePreview"
>
生成预览
</n-button>
<CustomIcon
v-else
class=
"cursor-pointer text-2xl"
icon=
"ph:play"
@
click=
"playAudio"
/>
<div
class=
"flex flex-1 items-center justify-center"
>
<n-button
v-if=
"!audioData"
type=
"info"
:loading=
"isConnected"
:disabled=
"!digitalCreationStore.text"
@
click=
"generatePreview"
>
生成预览
</n-button
>
<CustomIcon
v-else
class=
"cursor-pointer text-2xl"
:icon=
"audioPlaying ? 'ph:pause' : 'ph:play'"
@
click=
"controlAudio"
/>
</div>
<!--
<div
class=
"flex flex-1 items-center justify-end gap-4"
>
<CustomIcon
class=
"cursor-pointer text-lg"
icon=
"mingcute:volume-line"
/>
...
...
@@ -114,5 +162,5 @@ function playAudio() {
</div>
</div>
<audio
ref=
"digitalAudio"
:src=
"audioUrl"
></audio>
<audio
ref=
"digitalAudio"
:src=
"audioUrl"
@
ended=
"audioPlaying = false"
></audio>
</
template
>
src/views/creation/components/subtitle/subtitle-setting.vue
View file @
fab78130
...
...
@@ -19,7 +19,7 @@ const subtitleEnabled = computed({
<n-tab-pane
name=
"subtitle"
tab=
"字幕"
class=
"h-full"
>
<div
class=
"flex h-full items-center justify-between overflow-y-auto px-4 py-2"
>
<span>
是否開啓
</span>
<n-switch
v-model:value=
"subtitleEnabled"
/>
<n-switch
v-model:value=
"subtitleEnabled"
checked-value=
"Y"
unchecked-value=
"N"
/>
</div>
</n-tab-pane>
</n-tabs>
...
...
src/views/creation/layout/header-bar.vue
View file @
fab78130
<
script
setup
lang=
"ts"
>
import
{
createBaseVideoDigitalHumanTask
,
saveDraftConfig
}
from
'@/apis/digital-creation'
import
{
createDigitalHumanVideoTask
,
saveDraftConfig
}
from
'@/apis/digital-creation'
import
{
fetchUniversalCurrency
}
from
'@/apis/user'
import
{
useDigitalCreationStore
}
from
'@/store/modules/creation'
import
{
BaseVideoTask
,
DraftConfig
}
from
'@/store/types/creation'
import
{
ref
}
from
'vue'
import
{
onMounted
,
onUnmounted
,
ref
}
from
'vue'
import
{
useRouter
}
from
'vue-router'
const
router
=
useRouter
()
const
digitalCreationStore
=
useDigitalCreationStore
()
const
editDraftName
=
ref
(
false
)
const
saveSuccess
=
ref
(
false
)
const
showExportModal
=
ref
(
false
)
const
ratioValue
=
ref
(
720
)
const
ratioList
=
[
...
...
@@ -15,15 +20,29 @@ const transparent = [
{
value
:
'N'
,
label
:
'全部'
},
{
value
:
'Y'
,
label
:
'僅數字人(透明背景)'
},
]
let
timer
:
any
onMounted
(()
=>
{
timer
=
setInterval
(()
=>
{
saveDraft
()
},
5000
)
})
onUnmounted
(()
=>
{
clearInterval
(
timer
)
timer
=
null
})
// 保存为草稿
async
function
saveDraft
()
{
const
payload
:
{
draftConfigDto
:
DraftConfig
}
=
{
draftConfigDto
:
digitalCreationStore
.
$state
,
const
payload
:
DraftConfig
=
{
...
digitalCreationStore
.
$state
,
draftName
:
digitalCreationStore
.
draftName
,
}
const
res
=
await
saveDraftConfig
(
payload
)
const
res
=
await
saveDraftConfig
<
DraftConfig
>
(
payload
)
if
(
res
.
code
===
0
)
{
window
.
$message
.
success
(
'保存成功'
)
digitalCreationStore
.
updateDigitalCreation
(
res
.
data
)
saveSuccess
.
value
=
true
}
}
...
...
@@ -37,6 +56,11 @@ function confirmExport() {
}
async
function
createBaseVideoTask
()
{
const
balance
=
await
getUniversalCurrency
()
if
(
!
balance
)
{
window
.
$message
.
error
(
'餘額不足'
)
return
}
if
(
!
digitalCreationStore
.
id
)
{
window
.
$message
.
error
(
'請先保存視頻為草稿'
)
return
...
...
@@ -54,24 +78,41 @@ async function createBaseVideoTask() {
videoType
:
'mp4'
,
audioUrl
:
digitalCreationStore
.
inputAudioUrl
,
}
const
res
=
await
create
BaseVideoDigitalHumanTask
(
'null'
,
payload
)
const
res
=
await
create
DigitalHumanVideoTask
(
payload
)
if
(
res
.
code
===
0
)
{
window
.
$message
.
success
(
'導出成功'
)
showExportModal
.
value
=
false
}
}
async
function
getUniversalCurrency
()
{
const
res
=
await
fetchUniversalCurrency
<
number
>
()
if
(
res
.
code
===
0
)
{
return
res
.
data
}
}
</
script
>
<
template
>
<header
class=
"flex h-14 items-center justify-between bg-white px-4"
>
<div
class=
"flex cursor-pointer items-center"
>
<CustomIcon
class=
"text-lg"
icon=
"mingcute:left-line"
/>
<span>
返回
</span>
<div
class=
"flex items-center gap-4"
>
<CustomIcon
class=
"cursor-pointer text-lg"
icon=
"mingcute:left-line"
@
click=
"router.replace('/')"
/>
<n-input
v-if=
"editDraftName"
v-model:value=
"digitalCreationStore.draftName"
placeholder=
"請輸入草稿名稱"
style=
"width: 400px"
@
blur=
"editDraftName = false"
/>
<div
v-else
class=
"flex items-center gap-2"
>
<span>
{{
digitalCreationStore
.
draftName
}}
</span>
<CustomIcon
class=
"cursor-pointer text-lg"
icon=
"icon-park-outline:edit"
@
click=
"editDraftName = true"
/>
</div>
</div>
<div
class=
"flex items-center"
>
<div
class=
"flex items-center gap-4"
>
<div
class=
"flex items-center gap-2"
>
<div
v-if=
"saveSuccess"
class=
"flex items-center gap-2"
>
<CustomIcon
class=
"text-green"
icon=
"ep:success-filled"
/>
<span>
已自動保存
</span>
</div>
...
...
src/views/creation/layout/index.vue
View file @
fab78130
...
...
@@ -15,7 +15,7 @@ onMounted(() => {
if
(
route
.
params
.
draftId
)
{
getDraft
(
Number
(
route
.
params
.
draftId
))
}
else
{
getDigitalTemplate
(
1
)
getDigitalTemplate
(
Number
(
route
.
params
.
templateId
)
)
}
})
...
...
@@ -26,7 +26,7 @@ async function getDigitalTemplate(id: number) {
const
draftConfig
:
DraftConfig
=
{
id
:
null
,
coverUrl
:
digitalTemplate
.
coverUrl
,
draftName
:
''
,
draftName
:
`自定義草稿名稱
${
new
Date
().
toLocaleString
()}
`
,
videoName
:
''
,
taskType
:
digitalTemplate
.
taskType
,
requestId
:
digitalTemplate
.
requestId
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment