feat: 功能
This commit is contained in:
@@ -76,12 +76,3 @@ export function deleteTask(taskId) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务输出文件的签名URL
|
||||
*/
|
||||
export function getSignedUrls(taskId) {
|
||||
return request({
|
||||
url: `/webApi/api/tik/digital-human/task/${taskId}/signed-url`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -264,12 +264,12 @@ const initPlayer = (url) => {
|
||||
|
||||
player.on('canplay', () => {
|
||||
isPlayerInitializing.value = false
|
||||
// 发送音频时长和 base64 数据给父组件
|
||||
// 发送音频时长和 URL 给父组件
|
||||
const durationMs = Math.floor(player.audio.duration * 1000)
|
||||
if (durationMs > 0) {
|
||||
emit('audioGenerated', {
|
||||
durationMs,
|
||||
audioBase64: currentAudioBase64.value
|
||||
audioUrl: audioUrl.value // 使用 URL(性能优化)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -326,13 +326,13 @@ function clearVideo() {
|
||||
store.resetProcess()
|
||||
}
|
||||
|
||||
function handleAudioGenerated(data: { durationMs: number; audioBase64: string }) {
|
||||
function handleAudioGenerated(data: { durationMs: number; audioUrl: string }) {
|
||||
if (store.timeline && data.durationMs > 0) {
|
||||
store.timeline.audioDurationMs = data.durationMs
|
||||
}
|
||||
if (data.audioBase64) {
|
||||
if (data.audioUrl) {
|
||||
store.audioData = {
|
||||
audioBase64: data.audioBase64,
|
||||
audioUrl: data.audioUrl,
|
||||
format: 'mp3',
|
||||
durationMs: data.durationMs
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { VoiceMeta, Video, PipelinePhase, VideoStep, AudioStep, CreateStep,
|
||||
|
||||
/** 音频数据 */
|
||||
interface AudioData {
|
||||
audioBase64: string
|
||||
audioUrl: string // 预签名 URL
|
||||
format: string
|
||||
durationMs: number
|
||||
}
|
||||
@@ -373,14 +373,20 @@ export const useDigitalHumanStore = defineStore('digitalHuman', () => {
|
||||
providerType: DEFAULT_VOICE_PROVIDER,
|
||||
} as any)
|
||||
|
||||
if (res.code !== 0 || !res.data?.audioBase64) {
|
||||
if (res.code !== 0 || !res.data?.audioUrl) {
|
||||
throw new Error(res.msg || '音频生成失败')
|
||||
}
|
||||
// 使用 URL(性能优化)
|
||||
const audioUrl = res.data.audioUrl
|
||||
if (!audioUrl) {
|
||||
throw new Error('音频生成失败:未返回音频URL')
|
||||
}
|
||||
|
||||
const durationMs = await parseAudioDuration(res.data.audioBase64)
|
||||
// 获取音频时长
|
||||
const durationMs = await getAudioDurationFromUrl(audioUrl)
|
||||
|
||||
audioData.value = {
|
||||
audioBase64: res.data.audioBase64,
|
||||
audioUrl: audioUrl,
|
||||
format: 'mp3',
|
||||
durationMs,
|
||||
}
|
||||
@@ -447,10 +453,8 @@ export const useDigitalHumanStore = defineStore('digitalHuman', () => {
|
||||
kling_face_end_time: identifyData.value.faceEndTime,
|
||||
ai_provider: 'kling',
|
||||
voiceConfigId: voiceId,
|
||||
pre_generated_audio: {
|
||||
audioBase64: audioData.value.audioBase64,
|
||||
format: audioData.value.format,
|
||||
},
|
||||
// 使用预生成的音频 URL(性能优化)
|
||||
audio_url: audioData.value.audioUrl,
|
||||
sound_end_time: audioData.value.durationMs,
|
||||
})
|
||||
|
||||
@@ -571,32 +575,24 @@ export const useDigitalHumanStore = defineStore('digitalHuman', () => {
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/** 解析音频时长 */
|
||||
async function parseAudioDuration(base64Data: string): Promise<number> {
|
||||
const base64 = base64Data.includes(',') ? base64Data.split(',')[1] : base64Data
|
||||
const binaryString = window.atob(base64)
|
||||
const bytes = new Uint8Array(binaryString.length)
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
|
||||
/** 从 URL 获取音频时长 */
|
||||
async function getAudioDurationFromUrl(url: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([bytes], { type: 'audio/mp3' })
|
||||
const audio = new Audio()
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
let resolved = false
|
||||
let lastDuration = 0
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
cleanup()
|
||||
reject(new Error('音频解析超时'))
|
||||
reject(new Error('音频加载超时'))
|
||||
}
|
||||
}, 15000)
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId)
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
audio.pause()
|
||||
audio.src = ''
|
||||
}
|
||||
|
||||
const tryResolve = (duration: number, source: string) => {
|
||||
@@ -621,11 +617,11 @@ export const useDigitalHumanStore = defineStore('digitalHuman', () => {
|
||||
resolve(Math.floor(lastDuration * 1000) - 200)
|
||||
} else {
|
||||
cleanup()
|
||||
reject(new Error('音频解析失败'))
|
||||
reject(new Error('音频加载失败'))
|
||||
}
|
||||
}
|
||||
|
||||
audio.src = objectUrl
|
||||
audio.src = url
|
||||
audio.load()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,9 +95,9 @@ export interface AudioState {
|
||||
* 音频数据接口
|
||||
*/
|
||||
export interface AudioData {
|
||||
audioBase64: string
|
||||
audioUrl?: string
|
||||
audioUrl: string // 预签名 URL(性能优化)
|
||||
format?: string
|
||||
durationMs?: number // 音频时长(毫秒)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,9 +253,6 @@ export interface LipSyncTaskData {
|
||||
ai_provider: string
|
||||
voiceId?: string // 系统预置音色ID
|
||||
voiceConfigId?: string // 用户配音ID(tik_user_voice.id)
|
||||
pre_generated_audio?: {
|
||||
audioBase64: string
|
||||
format: string
|
||||
}
|
||||
audio_url?: string // 预生成音频 URL(性能优化)
|
||||
sound_end_time?: number
|
||||
}
|
||||
|
||||
@@ -132,13 +132,6 @@
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览弹窗 -->
|
||||
<VideoPreviewModal
|
||||
v-model:open="previewVisible"
|
||||
:video-url="previewUrl"
|
||||
:title="previewTitle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -146,13 +139,12 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { SearchOutlined, DownloadOutlined, DeleteOutlined, PlayCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { getDigitalHumanTaskPage, cancelTask, deleteTask, getSignedUrls } from '@/api/digitalHuman'
|
||||
import { getDigitalHumanTaskPage, cancelTask, deleteTask } from '@/api/digitalHuman'
|
||||
import { formatDate } from '@/utils/file'
|
||||
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
|
||||
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
|
||||
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
|
||||
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
|
||||
import VideoPreviewModal from '@/components/VideoPreviewModal.vue'
|
||||
|
||||
// 进度状态映射
|
||||
const PROGRESS_STATUS = {
|
||||
@@ -175,41 +167,16 @@ const rowSelection = {
|
||||
onChange: (keys) => { selectedRowKeys.value = keys }
|
||||
}
|
||||
|
||||
// 视频预览状态
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('')
|
||||
|
||||
// 状态判断
|
||||
const isStatus = (status, target) => status === target || status === target.toUpperCase()
|
||||
|
||||
// 预览视频
|
||||
const handlePreview = async (record) => {
|
||||
if (!record.id) {
|
||||
message.warning('任务信息不完整')
|
||||
// 预览视频(直接打开链接)
|
||||
const handlePreview = (record) => {
|
||||
if (!record.resultVideoUrl) {
|
||||
message.warning('该任务暂无视频结果,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
const hideLoading = message.loading('正在获取视频地址...', 0)
|
||||
|
||||
try {
|
||||
// 调用后端API获取带签名的视频URL
|
||||
const res = await getSignedUrls(record.id)
|
||||
hideLoading()
|
||||
|
||||
if (res.code === 0 && res.data && res.data.length > 0) {
|
||||
previewUrl.value = res.data[0]
|
||||
previewTitle.value = record.taskName || '视频预览'
|
||||
previewVisible.value = true
|
||||
} else {
|
||||
message.error(res.msg || '获取视频地址失败')
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading()
|
||||
console.error('获取视频播放URL失败:', error)
|
||||
message.error('获取视频地址失败,请重试')
|
||||
}
|
||||
window.open(record.resultVideoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 下载视频
|
||||
|
||||
Reference in New Issue
Block a user