Commit fab78130 authored by Dazzle Wu's avatar Dazzle Wu

feat: 视频生成联调

parent a5fc900c
......@@ -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)
}
......@@ -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')
}
......@@ -3,7 +3,7 @@ import Creation from '@/views/creation/creation.vue'
export default [
{
path: '/creation',
path: '/creation/:templateId/:draftId?',
name: 'Creation',
meta: {
rank: 1001,
......
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()
},
},
})
......@@ -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 {
......
<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="lanValue" :list="lanList" />
<HorizontalTabs v-model:value="lanType" :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">
......
<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>
......@@ -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>
......
<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 createBaseVideoDigitalHumanTask('null', payload)
const res = await createDigitalHumanVideoTask(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>
......
......@@ -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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment