feat: 功能优化
This commit is contained in:
77
frontend/app/web-gold/src/api/digitalHuman.js
Normal file
77
frontend/app/web-gold/src/api/digitalHuman.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 数字人任务 API
|
||||
*/
|
||||
import request from './http'
|
||||
|
||||
/**
|
||||
* 创建数字人任务
|
||||
*/
|
||||
export function createDigitalHumanTask(data) {
|
||||
return request({
|
||||
url: '/api/tik/digital-human/task/create',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务详情
|
||||
*/
|
||||
export function getDigitalHumanTask(taskId) {
|
||||
return request({
|
||||
url: '/api/tik/digital-human/task/get',
|
||||
method: 'get',
|
||||
params: { taskId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询任务列表
|
||||
*/
|
||||
export function getDigitalHumanTaskPage(params) {
|
||||
return request({
|
||||
url: '/api/tik/digital-human/task/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务统计
|
||||
*/
|
||||
export function getTaskStatistics() {
|
||||
return request({
|
||||
url: '/api/tik/digital-human/task/statistics',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*/
|
||||
export function cancelTask(taskId) {
|
||||
return request({
|
||||
url: `/api/tik/digital-human/task/${taskId}/cancel`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务
|
||||
*/
|
||||
export function retryTask(taskId) {
|
||||
return request({
|
||||
url: `/api/tik/digital-human/task/${taskId}/retry`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
*/
|
||||
export function deleteTask(taskId) {
|
||||
return request({
|
||||
url: `/api/tik/digital-human/task/${taskId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -5,19 +5,26 @@ import { message } from 'ant-design-vue'
|
||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||
import { VoiceService } from '@/api/voice'
|
||||
import { MaterialService } from '@/api/material'
|
||||
import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
|
||||
|
||||
const voiceStore = useVoiceCopyStore()
|
||||
|
||||
// 状态管理
|
||||
const uploadedVideo = ref('')
|
||||
const uploadedVideoFile = ref(null) // 存储原始文件对象
|
||||
const previewVideoUrl = ref('')
|
||||
const isGenerating = ref(false)
|
||||
const generationProgress = ref(0)
|
||||
const currentTaskId = ref(null)
|
||||
const currentTaskStatus = ref('')
|
||||
const currentTaskStep = ref('')
|
||||
const isSynthesizing = ref(false)
|
||||
const synthesizedAudio = ref(null)
|
||||
const previewLoadingVoiceId = ref('')
|
||||
const isPlayingPreview = ref(false) // 是否正在播放试听音频
|
||||
const isPlayingSynthesized = ref(false) // 是否正在播放已合成的音频
|
||||
const pollingInterval = ref(null) // 轮询间隔ID
|
||||
|
||||
// TTS 配置
|
||||
const ttsText = ref('')
|
||||
@@ -245,6 +252,7 @@ const handleVideoUpload = async (file) => {
|
||||
|
||||
try {
|
||||
uploadedVideo.value = await toDataURL(file)
|
||||
uploadedVideoFile.value = file // 保存文件对象
|
||||
message.success('视频上传成功')
|
||||
} catch (error) {
|
||||
message.error('视频上传失败')
|
||||
@@ -255,6 +263,7 @@ const handleVideoUpload = async (file) => {
|
||||
const handleVideoDrop = (e) => console.log('Video drop:', e)
|
||||
const clearVideo = () => {
|
||||
uploadedVideo.value = ''
|
||||
uploadedVideoFile.value = null
|
||||
previewVideoUrl.value = ''
|
||||
message.info('已清除视频')
|
||||
}
|
||||
@@ -270,26 +279,176 @@ const downloadPreview = () => {
|
||||
// 视频生成
|
||||
const generateVideo = async () => {
|
||||
if (!canGenerate.value) return message.warning('请先完成配置')
|
||||
if (!ttsText.value.trim()) return message.warning('请输入文本')
|
||||
const voice = selectedVoiceMeta.value
|
||||
if (!voice) return message.warning('请选择音色')
|
||||
|
||||
isGenerating.value = true
|
||||
generationProgress.value = 0
|
||||
currentTaskStatus.value = 'PENDING'
|
||||
currentTaskStep.value = 'prepare_files'
|
||||
|
||||
try {
|
||||
// 进度模拟
|
||||
const progressInterval = setInterval(() => {
|
||||
generationProgress.value += 10
|
||||
generationProgress.value >= 100 && clearInterval(progressInterval)
|
||||
}, 500)
|
||||
// 1. 首先上传音频和视频文件到后端
|
||||
message.loading('正在上传文件...', 0)
|
||||
|
||||
// 上传音频(使用合成后的音频或原始音频)
|
||||
let audioFileId = null
|
||||
let audioUrl = null
|
||||
|
||||
if (synthesizedAudio.value?.fileId) {
|
||||
// 如果有已合成的音频,使用其fileId
|
||||
audioFileId = synthesizedAudio.value.fileId
|
||||
} else {
|
||||
// 否则使用voiceConfigId让后端处理
|
||||
audioFileId = voice.rawId || extractIdFromString(voice.id)
|
||||
}
|
||||
|
||||
// 上传视频文件
|
||||
const videoFileId = await uploadVideoFile(uploadedVideoFile.value)
|
||||
if (!videoFileId) {
|
||||
throw new Error('视频上传失败')
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
previewVideoUrl.value = uploadedVideo.value
|
||||
generationProgress.value = 100
|
||||
message.success('视频生成成功')
|
||||
clearInterval(progressInterval)
|
||||
message.destroy()
|
||||
|
||||
// 2. 创建数字人任务
|
||||
const taskData = {
|
||||
taskName: `数字人任务_${Date.now()}`,
|
||||
audioFileId: audioFileId,
|
||||
videoFileId: videoFileId,
|
||||
speechRate: speechRate.value,
|
||||
emotion: emotion.value,
|
||||
guidanceScale: 1,
|
||||
seed: 8888
|
||||
}
|
||||
|
||||
message.loading('正在创建任务...', 0)
|
||||
const createRes = await createDigitalHumanTask(taskData)
|
||||
message.destroy()
|
||||
|
||||
if (createRes.code === 0) {
|
||||
currentTaskId.value = createRes.data
|
||||
message.success('任务创建成功,开始处理')
|
||||
|
||||
// 3. 开始轮询任务状态
|
||||
startPollingTask()
|
||||
} else {
|
||||
throw new Error(createRes.msg || '任务创建失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('视频生成失败')
|
||||
} finally {
|
||||
console.error('generateVideo error:', error)
|
||||
message.destroy()
|
||||
message.error(error.message || '视频生成失败')
|
||||
isGenerating.value = false
|
||||
currentTaskStatus.value = ''
|
||||
currentTaskStep.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 上传视频文件到后端
|
||||
const uploadVideoFile = async (file) => {
|
||||
try {
|
||||
const res = await MaterialService.uploadFile(file, 'video')
|
||||
if (res.code === 0 && res.data?.id) {
|
||||
return res.data.id
|
||||
} else {
|
||||
throw new Error(res.msg || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('uploadVideoFile error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
const startPollingTask = () => {
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value)
|
||||
}
|
||||
|
||||
pollingInterval.value = setInterval(async () => {
|
||||
if (!currentTaskId.value) {
|
||||
clearInterval(pollingInterval.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getDigitalHumanTask(currentTaskId.value)
|
||||
if (res.code === 0 && res.data) {
|
||||
const task = res.data
|
||||
currentTaskStatus.value = task.status
|
||||
currentTaskStep.value = task.currentStep
|
||||
generationProgress.value = task.progress || 0
|
||||
|
||||
// 如果任务完成或失败,停止轮询
|
||||
if (task.status === 'SUCCESS') {
|
||||
clearInterval(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
previewVideoUrl.value = task.resultVideoUrl
|
||||
isGenerating.value = false
|
||||
currentTaskStatus.value = 'SUCCESS'
|
||||
message.success('视频生成成功!')
|
||||
} else if (task.status === 'FAILED') {
|
||||
clearInterval(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
isGenerating.value = false
|
||||
currentTaskStatus.value = 'FAILED'
|
||||
message.error(`任务失败:${task.errorMessage || '未知错误'}`)
|
||||
} else if (task.status === 'CANCELED') {
|
||||
clearInterval(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
isGenerating.value = false
|
||||
currentTaskStatus.value = 'CANCELED'
|
||||
message.info('任务已取消')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('polling error:', error)
|
||||
}
|
||||
}, 2000) // 每2秒轮询一次
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
const handleCancelTask = async () => {
|
||||
if (!currentTaskId.value) return
|
||||
|
||||
try {
|
||||
const res = await cancelTask(currentTaskId.value)
|
||||
if (res.code === 0) {
|
||||
message.success('任务已取消')
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
}
|
||||
isGenerating.value = false
|
||||
} else {
|
||||
message.error(res.msg || '取消失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('cancelTask error:', error)
|
||||
message.error('取消任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重试任务
|
||||
const handleRetryTask = async () => {
|
||||
if (!currentTaskId.value) return
|
||||
|
||||
try {
|
||||
const res = await retryTask(currentTaskId.value)
|
||||
if (res.code === 0) {
|
||||
message.success('任务已重启')
|
||||
currentTaskStatus.value = 'PENDING'
|
||||
currentTaskStep.value = 'prepare_files'
|
||||
isGenerating.value = true
|
||||
startPollingTask()
|
||||
} else {
|
||||
message.error(res.msg || '重试失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('retryTask error:', error)
|
||||
message.error('重试任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +460,29 @@ const toDataURL = (file) => new Promise((resolve, reject) => {
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
// 状态描述映射
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'PENDING': '等待处理',
|
||||
'PROCESSING': '处理中',
|
||||
'SUCCESS': '已完成',
|
||||
'FAILED': '失败',
|
||||
'CANCELED': '已取消'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getStepText = (step) => {
|
||||
const stepMap = {
|
||||
'prepare_files': '准备文件',
|
||||
'synthesize_voice': '语音合成',
|
||||
'sync_lip': '口型同步',
|
||||
'finishing': '完成处理',
|
||||
'canceled': '已取消'
|
||||
}
|
||||
return stepMap[step] || step
|
||||
}
|
||||
|
||||
const playAudioPreview = (url, options = {}) => {
|
||||
if (!url) return message.warning('暂无可试听的音频')
|
||||
|
||||
@@ -383,6 +565,11 @@ onUnmounted(() => {
|
||||
// 重置播放状态
|
||||
isPlayingPreview.value = false
|
||||
isPlayingSynthesized.value = false
|
||||
// 清理轮询
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 监听器
|
||||
@@ -578,21 +765,55 @@ let previewObjectUrl = ''
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="generate-section">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="!canGenerate"
|
||||
:loading="isGenerating"
|
||||
@click="generateVideo"
|
||||
>
|
||||
{{ isGenerating ? '生成中...' : '生成视频' }}
|
||||
</a-button>
|
||||
<div v-if="!currentTaskId || currentTaskStatus === 'SUCCESS' || currentTaskStatus === 'FAILED' || currentTaskStatus === 'CANCELED'" class="generate-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="!canGenerate"
|
||||
:loading="isGenerating"
|
||||
@click="generateVideo"
|
||||
>
|
||||
{{ isGenerating ? '生成中...' : '生成视频' }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-actions">
|
||||
<div class="task-status">
|
||||
<div class="status-row">
|
||||
<span class="status-label">状态:</span>
|
||||
<span class="status-value">{{ getStatusText(currentTaskStatus) }}</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">步骤:</span>
|
||||
<span class="status-value">{{ getStepText(currentTaskStep) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a-button
|
||||
v-if="currentTaskStatus === 'PROCESSING'"
|
||||
danger
|
||||
block
|
||||
@click="handleCancelTask"
|
||||
>
|
||||
取消任务
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="currentTaskStatus === 'FAILED' || currentTaskStatus === 'CANCELED'"
|
||||
type="primary"
|
||||
block
|
||||
@click="handleRetryTask"
|
||||
>
|
||||
重试任务
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-progress
|
||||
v-if="isGenerating"
|
||||
:percent="generationProgress"
|
||||
:show-info="true"
|
||||
:status="currentTaskStatus === 'FAILED' ? 'exception' : 'active'"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -940,6 +1161,56 @@ let previewObjectUrl = ''
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.generate-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-buttons .ant-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user