feat:优化
This commit is contained in:
@@ -3,11 +3,12 @@ defineOptions({ name: 'DigitalVideoPage' })
|
|||||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
|
||||||
import { VoiceService } from '@/api/voice'
|
import { VoiceService } from '@/api/voice'
|
||||||
import { MaterialService } from '@/api/material'
|
import { MaterialService } from '@/api/material'
|
||||||
import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
|
import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
|
||||||
|
|
||||||
|
// 导入 voiceStore 用于获取用户音色
|
||||||
|
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||||
const voiceStore = useVoiceCopyStore()
|
const voiceStore = useVoiceCopyStore()
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
@@ -35,7 +36,9 @@ const ttsText = ref('')
|
|||||||
const selectedTtsVoice = ref('')
|
const selectedTtsVoice = ref('')
|
||||||
const speechRate = ref(1.0)
|
const speechRate = ref(1.0)
|
||||||
const instruction = ref('neutral') // 指令参数,用于控制音色风格
|
const instruction = ref('neutral') // 指令参数,用于控制音色风格
|
||||||
const voiceSource = ref('user')
|
const emotion = ref('neutral') // 情感参数
|
||||||
|
const emotionActive = ref(false) // 是否激活情感tab(false为指令,true为情感)
|
||||||
|
const voiceSource = ref('system') // 音色来源:user 或 system
|
||||||
|
|
||||||
// 系统音色库(使用CosyVoice v3-flash模型)
|
// 系统音色库(使用CosyVoice v3-flash模型)
|
||||||
const SYSTEM_VOICES = [
|
const SYSTEM_VOICES = [
|
||||||
@@ -103,6 +106,10 @@ const setVoiceSource = (source) => {
|
|||||||
|
|
||||||
const selectVoiceProfile = (voice) => {
|
const selectVoiceProfile = (voice) => {
|
||||||
selectedTtsVoice.value = `${voice.source}-${voice.id}`
|
selectedTtsVoice.value = `${voice.source}-${voice.id}`
|
||||||
|
// 选系统音色时也更新对应的instruction
|
||||||
|
if (voice.source === 'system' && voice.defaultInstruction) {
|
||||||
|
instruction.value = voice.defaultInstruction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 音频播放
|
// 音频播放
|
||||||
@@ -155,8 +162,7 @@ const triggerVoicePreview = async (voice) => {
|
|||||||
|
|
||||||
const buildPreviewParams = (voice) => {
|
const buildPreviewParams = (voice) => {
|
||||||
if (voice.source === 'user') {
|
if (voice.source === 'user') {
|
||||||
// 使用voiceConfigId,让后端查询数据库获取文件URL和transcriptionText
|
// 用户音色:使用voiceConfigId,不传instruction
|
||||||
// 用户音色不传instruction
|
|
||||||
const configId = voice.rawId || extractIdFromString(voice.id)
|
const configId = voice.rawId || extractIdFromString(voice.id)
|
||||||
if (!configId) {
|
if (!configId) {
|
||||||
message.error('配音配置无效')
|
message.error('配音配置无效')
|
||||||
@@ -164,19 +170,29 @@ const buildPreviewParams = (voice) => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
voiceConfigId: configId,
|
voiceConfigId: configId,
|
||||||
inputText: ttsText.value, // 传递用户输入的文本
|
inputText: ttsText.value,
|
||||||
speechRate: speechRate.value || 1.0,
|
speechRate: speechRate.value || 1.0,
|
||||||
audioFormat: 'mp3'
|
audioFormat: 'mp3'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 系统音色使用用户选择的instruction
|
// 系统音色:根据是否选择instruction或emotion来决定传递哪个参数
|
||||||
return {
|
const params = {
|
||||||
voiceId: voice.voiceId,
|
voiceId: voice.voiceId,
|
||||||
inputText: ttsText.value, // 传递用户输入的文本
|
inputText: ttsText.value,
|
||||||
instruction: instruction.value && instruction.value !== 'neutral' ? instruction.value : (voice.defaultInstruction || '请用自然流畅的语调朗读'),
|
|
||||||
speechRate: speechRate.value || 1.0,
|
speechRate: speechRate.value || 1.0,
|
||||||
audioFormat: 'mp3'
|
audioFormat: 'mp3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// instruction和emotion只能选一个传递
|
||||||
|
if (instruction.value && instruction.value !== 'neutral') {
|
||||||
|
params.instruction = instruction.value
|
||||||
|
} else if (emotion.value && emotion.value !== 'neutral') {
|
||||||
|
params.emotion = emotion.value
|
||||||
|
} else if (voice.defaultInstruction) {
|
||||||
|
params.instruction = voice.defaultInstruction
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +216,8 @@ const handleSynthesizeVoice = async () => {
|
|||||||
audioFormat: 'mp3'
|
audioFormat: 'mp3'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是用户配音,使用voiceConfigId让后端查询,不传instruction
|
|
||||||
if (voice.source === 'user') {
|
if (voice.source === 'user') {
|
||||||
|
// 用户音色:使用voiceConfigId,不传instruction
|
||||||
const configId = voice.rawId || extractIdFromString(voice.id)
|
const configId = voice.rawId || extractIdFromString(voice.id)
|
||||||
if (!configId) {
|
if (!configId) {
|
||||||
message.warning('音色配置无效')
|
message.warning('音色配置无效')
|
||||||
@@ -209,14 +225,22 @@ const handleSynthesizeVoice = async () => {
|
|||||||
}
|
}
|
||||||
params.voiceConfigId = configId
|
params.voiceConfigId = configId
|
||||||
} else {
|
} else {
|
||||||
// 使用系统音色voiceId和用户选择的instruction
|
// 系统音色:使用voiceId,根据是否选择instruction或emotion来决定传递哪个参数
|
||||||
const voiceId = voice.voiceId || voice.rawId
|
const voiceId = voice.voiceId || voice.rawId
|
||||||
if (!voiceId) {
|
if (!voiceId) {
|
||||||
message.warning('音色配置无效')
|
message.warning('音色配置无效')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
params.voiceId = voiceId
|
params.voiceId = voiceId
|
||||||
params.instruction = instruction.value && instruction.value !== 'neutral' ? instruction.value : (voice.defaultInstruction || '请用自然流畅的语调朗读')
|
|
||||||
|
// instruction和emotion只能选一个传递
|
||||||
|
if (instruction.value && instruction.value !== 'neutral') {
|
||||||
|
params.instruction = instruction.value
|
||||||
|
} else if (emotion.value && emotion.value !== 'neutral') {
|
||||||
|
params.emotion = emotion.value
|
||||||
|
} else if (voice.defaultInstruction) {
|
||||||
|
params.instruction = voice.defaultInstruction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await VoiceService.synthesize(params)
|
const res = await VoiceService.synthesize(params)
|
||||||
@@ -317,19 +341,41 @@ const generateVideo = async () => {
|
|||||||
|
|
||||||
message.destroy()
|
message.destroy()
|
||||||
|
|
||||||
// 2. 创建数字人任务(简化:只使用voiceId,后端实时TTS)
|
// 2. 创建数字人任务
|
||||||
const taskData = {
|
const taskData = {
|
||||||
taskName: `数字人任务_${Date.now()}`,
|
taskName: `数字人任务_${Date.now()}`,
|
||||||
videoFileId: videoFileId,
|
videoFileId: videoFileId,
|
||||||
// 音频由后端实时合成,使用voiceId
|
inputText: ttsText.value,
|
||||||
voiceId: voice.voiceId || voice.rawId,
|
|
||||||
inputText: ttsText.value, // 文本内容(用于TTS合成)
|
|
||||||
speechRate: speechRate.value,
|
speechRate: speechRate.value,
|
||||||
instruction: voice.source === 'user' ? undefined : (instruction.value && instruction.value !== 'neutral' ? instruction.value : (voice.defaultInstruction || '请用自然流畅的语调朗读')),
|
volume: 0,
|
||||||
guidanceScale: 1,
|
guidanceScale: 1,
|
||||||
seed: 8888
|
seed: 8888
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (voice.source === 'user') {
|
||||||
|
// 用户音色:使用voiceConfigId,不传instruction
|
||||||
|
const configId = voice.rawId || extractIdFromString(voice.id)
|
||||||
|
if (!configId) {
|
||||||
|
message.warning('音色配置无效')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskData.voiceConfigId = configId
|
||||||
|
} else {
|
||||||
|
// 系统音色:使用voiceId,根据是否选择instruction或emotion来决定传递哪个参数
|
||||||
|
taskData.voiceId = voice.voiceId
|
||||||
|
|
||||||
|
// instruction和emotion只能选一个传递
|
||||||
|
if (instruction.value && instruction.value !== 'neutral') {
|
||||||
|
taskData.instruction = instruction.value
|
||||||
|
} else if (emotion.value && emotion.value !== 'neutral') {
|
||||||
|
taskData.emotion = emotion.value
|
||||||
|
} else if (voice.defaultInstruction) {
|
||||||
|
taskData.instruction = voice.defaultInstruction
|
||||||
|
} else {
|
||||||
|
taskData.emotion = 'neutral'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message.loading('正在创建任务...', 0)
|
message.loading('正在创建任务...', 0)
|
||||||
const createRes = await createDigitalHumanTask(taskData)
|
const createRes = await createDigitalHumanTask(taskData)
|
||||||
message.destroy()
|
message.destroy()
|
||||||
@@ -724,16 +770,55 @@ let previewObjectUrl = ''
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="voiceSource === 'system'" class="control-group">
|
<div v-if="voiceSource === 'system'" class="control-group">
|
||||||
<div class="control-label">指令</div>
|
<div class="control-label">情感</div>
|
||||||
|
<div class="control-tabs">
|
||||||
|
<button
|
||||||
|
:class="['tab-btn', { active: !emotionActive }]"
|
||||||
|
@click="emotionActive = false"
|
||||||
|
>
|
||||||
|
语调
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['tab-btn', { active: emotionActive }]"
|
||||||
|
@click="emotionActive = true"
|
||||||
|
>
|
||||||
|
情感
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="emotion-buttons">
|
<div class="emotion-buttons">
|
||||||
<button
|
<button
|
||||||
v-for="inst in ['neutral', '请用自然流畅的语调朗读', '请用温柔专业的语调朗读', '请用热情洋溢的语调朗读', '请用低沉磁性的语调朗读', '请用活泼生动的语调朗读']"
|
v-if="!emotionActive"
|
||||||
:key="inst"
|
v-for="inst in [
|
||||||
|
{ value: 'neutral', label: '中性' },
|
||||||
|
{ value: '请用自然流畅的语调朗读', label: '自然' },
|
||||||
|
{ value: '请用温柔专业的语调朗读', label: '温柔' },
|
||||||
|
{ value: '请用热情洋溢的语调朗读', label: '热情' },
|
||||||
|
{ value: '请用低沉磁性的语调朗读', label: '磁性' },
|
||||||
|
{ value: '请用活泼生动的语调朗读', label: '活泼' }
|
||||||
|
]"
|
||||||
|
:key="inst.value"
|
||||||
class="emotion-btn"
|
class="emotion-btn"
|
||||||
:class="{ active: instruction === inst }"
|
:class="{ active: instruction === inst.value }"
|
||||||
@click="instruction = inst"
|
@click="instruction = inst.value"
|
||||||
>
|
>
|
||||||
{{ inst === 'neutral' ? '中性' : inst }}
|
{{ inst.label }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
v-for="emo in [
|
||||||
|
{ value: 'neutral', label: '中性' },
|
||||||
|
{ value: 'happy', label: '开心' },
|
||||||
|
{ value: 'sad', label: '悲伤' },
|
||||||
|
{ value: 'angry', label: '愤怒' },
|
||||||
|
{ value: 'excited', label: '兴奋' },
|
||||||
|
{ value: 'calm', label: '平静' }
|
||||||
|
]"
|
||||||
|
:key="emo.value"
|
||||||
|
class="emotion-btn"
|
||||||
|
:class="{ active: emotion === emo.value }"
|
||||||
|
@click="emotion = emo.value"
|
||||||
|
>
|
||||||
|
{{ emo.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1091,6 +1176,28 @@ let previewObjectUrl = ''
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 6px 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
.emotion-btn {
|
.emotion-btn {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||||
|
|||||||
@@ -31,6 +31,19 @@
|
|||||||
<!-- 筛选条件 -->
|
<!-- 筛选条件 -->
|
||||||
<div class="material-list__filters">
|
<div class="material-list__filters">
|
||||||
<a-space>
|
<a-space>
|
||||||
|
<a-select
|
||||||
|
v-model:value="filters.fileCategory"
|
||||||
|
style="width: 120px"
|
||||||
|
placeholder="文件分类"
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<a-select-option value="">全部分类</a-select-option>
|
||||||
|
<a-select-option value="video">视频</a-select-option>
|
||||||
|
<a-select-option value="generate">生成</a-select-option>
|
||||||
|
<a-select-option value="audio">音频</a-select-option>
|
||||||
|
<a-select-option value="mix">混剪</a-select-option>
|
||||||
|
<a-select-option value="voice">配音</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
|
||||||
<a-input
|
<a-input
|
||||||
v-model="filters.fileName"
|
v-model="filters.fileName"
|
||||||
@@ -48,6 +61,7 @@
|
|||||||
style="width: 300px"
|
style="width: 300px"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="['开始日期', '结束日期']"
|
||||||
@change="handleFilterChange"
|
@change="handleFilterChange"
|
||||||
/>
|
/>
|
||||||
<a-button type="primary" @click="handleFilterChange">查询</a-button>
|
<a-button type="primary" @click="handleFilterChange">查询</a-button>
|
||||||
@@ -102,13 +116,9 @@
|
|||||||
<a-tag v-else-if="file.isImage" color="blue">图片</a-tag>
|
<a-tag v-else-if="file.isImage" color="blue">图片</a-tag>
|
||||||
<a-tag v-else color="gray">文件</a-tag>
|
<a-tag v-else color="gray">文件</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- 选中复选框 -->
|
<!-- 删除图标 -->
|
||||||
<div class="material-item__checkbox">
|
<div class="material-item__delete" @click.stop="handleDeleteFile(file)">
|
||||||
<a-checkbox
|
<DeleteOutlined />
|
||||||
:model-value="selectedFileIds.includes(file.id)"
|
|
||||||
@click.stop
|
|
||||||
@change="(checked) => handleSelectFile(file.id, checked)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文件信息 -->
|
<!-- 文件信息 -->
|
||||||
@@ -195,7 +205,8 @@ import { message, Modal } from 'ant-design-vue'
|
|||||||
import {
|
import {
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FileOutlined
|
FileOutlined,
|
||||||
|
DeleteOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { MaterialService } from '@/api/material'
|
import { MaterialService } from '@/api/material'
|
||||||
import { MixService } from '@/api/mix'
|
import { MixService } from '@/api/mix'
|
||||||
@@ -213,6 +224,7 @@ const mixing = ref(false)
|
|||||||
|
|
||||||
// 筛选条件
|
// 筛选条件
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
|
fileCategory: 'video', // 默认分类为视频
|
||||||
fileName: '',
|
fileName: '',
|
||||||
createTime: undefined
|
createTime: undefined
|
||||||
})
|
})
|
||||||
@@ -229,8 +241,10 @@ const buildQueryParams = () => {
|
|||||||
const params = {
|
const params = {
|
||||||
pageNo: pagination.pageNo,
|
pageNo: pagination.pageNo,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
...filters
|
fileCategory: filters.fileCategory || undefined,
|
||||||
|
fileName: filters.fileName || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理日期范围
|
// 处理日期范围
|
||||||
if (filters.createTime && Array.isArray(filters.createTime) && filters.createTime.length === 2) {
|
if (filters.createTime && Array.isArray(filters.createTime) && filters.createTime.length === 2) {
|
||||||
params.createTime = [
|
params.createTime = [
|
||||||
@@ -238,6 +252,7 @@ const buildQueryParams = () => {
|
|||||||
`${filters.createTime[1]} 23:59:59`
|
`${filters.createTime[1]} 23:59:59`
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,20 +347,44 @@ const handleBatchDelete = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择文件(简化逻辑)
|
// 删除单个文件
|
||||||
const handleSelectFile = (fileId, checked) => {
|
const handleDeleteFile = (file) => {
|
||||||
const index = selectedFileIds.value.indexOf(fileId)
|
if (!file?.id) return
|
||||||
if (checked && index === -1) {
|
|
||||||
selectedFileIds.value.push(fileId)
|
// 二次确认弹窗
|
||||||
} else if (!checked && index > -1) {
|
Modal.confirm({
|
||||||
selectedFileIds.value.splice(index, 1)
|
title: '确认删除',
|
||||||
}
|
content: `确定要删除文件 "${file.fileName}" 吗?删除后无法恢复。`,
|
||||||
|
okText: '确定删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await MaterialService.deleteFiles([file.id])
|
||||||
|
message.success('删除成功')
|
||||||
|
// 如果在选中列表中,也移除
|
||||||
|
const index = selectedFileIds.value.indexOf(file.id)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedFileIds.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
loadFileList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
message.error(error.message || '删除失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件点击
|
// 文件点击
|
||||||
const handleFileClick = (file) => {
|
const handleFileClick = (file) => {
|
||||||
// TODO: 打开文件详情或预览
|
const isSelected = selectedFileIds.value.includes(file.id)
|
||||||
console.log('点击文件:', file)
|
// 切换选中状态
|
||||||
|
if (isSelected) {
|
||||||
|
selectedFileIds.value = selectedFileIds.value.filter(id => id !== file.id)
|
||||||
|
} else {
|
||||||
|
selectedFileIds.value.push(file.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 筛选
|
// 筛选
|
||||||
@@ -355,6 +394,7 @@ const handleFilterChange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleResetFilters = () => {
|
const handleResetFilters = () => {
|
||||||
|
filters.fileCategory = 'video'
|
||||||
filters.fileName = ''
|
filters.fileName = ''
|
||||||
filters.createTime = undefined
|
filters.createTime = undefined
|
||||||
pagination.pageNo = 1
|
pagination.pageNo = 1
|
||||||
@@ -590,27 +630,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
.material-item {
|
.material-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-item--selected {
|
|
||||||
border: 2px solid var(--color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-item__content {
|
.material-item__content {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: 2px solid transparent;
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-item:hover .material-item__content {
|
.material-item--selected .material-item__content {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-item__preview {
|
.material-item__preview {
|
||||||
@@ -650,10 +681,31 @@ onMounted(() => {
|
|||||||
left: 8px;
|
left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-item__checkbox {
|
.material-item__delete {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
bottom: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(255, 77, 79, 0.9);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item:hover .material-item__delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__delete:hover {
|
||||||
|
background: rgb(255, 77, 79);
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-item__info {
|
.material-item__info {
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ public class LatentsyncClient {
|
|||||||
.addQueryParameter("request_id", requestId)
|
.addQueryParameter("request_id", requestId)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
log.info("[Latentsync][get result] requestId={}, url={}", requestId, url);
|
||||||
|
|
||||||
Request httpRequest = new Request.Builder()
|
Request httpRequest = new Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.addHeader("Authorization", "Bearer " + properties.getApiKey())
|
.addHeader("Authorization", "Bearer " + properties.getApiKey())
|
||||||
@@ -138,6 +140,7 @@ public class LatentsyncClient {
|
|||||||
try {
|
try {
|
||||||
return executeRequest(httpRequest, "get result", requestId);
|
return executeRequest(httpRequest, "get result", requestId);
|
||||||
} catch (ServiceException ex) {
|
} catch (ServiceException ex) {
|
||||||
|
log.error("[Latentsync][get result failed] requestId={}, message={}", requestId, ex.getMessage());
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("[Latentsync][get result exception]", ex);
|
log.error("[Latentsync][get result exception]", ex);
|
||||||
@@ -196,7 +199,16 @@ public class LatentsyncClient {
|
|||||||
private ServiceException buildException(String body) {
|
private ServiceException buildException(String body) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = objectMapper.readTree(body);
|
JsonNode root = objectMapper.readTree(body);
|
||||||
String message = root.path("message").asText(body);
|
// 尝试读取 message 字段(标准错误格式)
|
||||||
|
String message = root.path("message").asText("");
|
||||||
|
// 如果没有 message,尝试读取 detail 字段(302AI 的错误格式)
|
||||||
|
if (StrUtil.isBlank(message)) {
|
||||||
|
message = root.path("detail").asText("");
|
||||||
|
}
|
||||||
|
// 如果都没有,使用整个响应体
|
||||||
|
if (StrUtil.isBlank(message)) {
|
||||||
|
message = body;
|
||||||
|
}
|
||||||
return exception0(LATENTSYNC_SUBMIT_FAILED.getCode(), message);
|
return exception0(LATENTSYNC_SUBMIT_FAILED.getCode(), message);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
return exception0(LATENTSYNC_SUBMIT_FAILED.getCode(), body);
|
return exception0(LATENTSYNC_SUBMIT_FAILED.getCode(), body);
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
|||||||
log.info("[syncWithLatentsync][任务({})提交成功,requestId={}]", task.getId(), requestId);
|
log.info("[syncWithLatentsync][任务({})提交成功,requestId={}]", task.getId(), requestId);
|
||||||
|
|
||||||
// 轮询等待任务完成
|
// 轮询等待任务完成
|
||||||
int maxAttempts = 60; // 最多轮询60次
|
int maxAttempts = 90; // 最多轮询90次(15分钟)
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
while (attempt < maxAttempts) {
|
while (attempt < maxAttempts) {
|
||||||
attempt++;
|
attempt++;
|
||||||
@@ -485,8 +485,10 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
|||||||
AppTikLatentsyncResultRespVO result = latentsyncService.getTaskResult(requestId);
|
AppTikLatentsyncResultRespVO result = latentsyncService.getTaskResult(requestId);
|
||||||
String status = result.getStatus();
|
String status = result.getStatus();
|
||||||
|
|
||||||
log.info("[syncWithLatentsync][任务({})轮询结果: 第{}次, status={}]", task.getId(), attempt, status);
|
log.info("[syncWithLatentsync][任务({})轮询结果: 第{}次, status={}]",
|
||||||
|
task.getId(), attempt, status);
|
||||||
|
|
||||||
|
// 检查任务是否完成
|
||||||
if ("COMPLETED".equals(status)) {
|
if ("COMPLETED".equals(status)) {
|
||||||
// 任务完成,获取视频URL
|
// 任务完成,获取视频URL
|
||||||
String videoUrl = result.getVideo().getUrl();
|
String videoUrl = result.getVideo().getUrl();
|
||||||
@@ -496,12 +498,12 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
|||||||
} else {
|
} else {
|
||||||
throw new Exception("Latentsync 返回视频URL为空");
|
throw new Exception("Latentsync 返回视频URL为空");
|
||||||
}
|
}
|
||||||
} else if ("FAILED".equals(status)) {
|
} else if ("FAILED".equals(status) || "ERROR".equals(status)) {
|
||||||
throw new Exception("Latentsync 任务处理失败");
|
throw new Exception("Latentsync 任务处理失败: " + status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待5秒后再次轮询
|
// 等待10秒后再次轮询(处理中的任务间隔稍长一些)
|
||||||
Thread.sleep(5000);
|
Thread.sleep(10000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new Exception("等待Latentsync结果时被中断", e);
|
throw new Exception("等待Latentsync结果时被中断", e);
|
||||||
@@ -512,7 +514,7 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
|||||||
throw new Exception("等待Latentsync结果超时: " + e.getMessage(), e);
|
throw new Exception("等待Latentsync结果超时: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
// 否则等待后重试
|
// 否则等待后重试
|
||||||
Thread.sleep(5000);
|
Thread.sleep(10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user