提交昨晚更新改动

This commit is contained in:
super
2026-07-14 12:50:17 +08:00
parent d8ca8de537
commit 8a9126e626
21 changed files with 1104 additions and 173 deletions

View File

@@ -63,7 +63,7 @@
</div>
</div>
<div class="delivery-field">
<span class="delivery-field__label">整体风格 / 视频提示词</span>
<span class="delivery-field__label">视频风格/动作引导</span>
<el-input v-model="currentWorkspace.videoPrompt" type="textarea" :rows="4" resize="none"
placeholder="例如:视频画面保持高清,运镜平稳,整体风格干净明亮。" />
</div>
@@ -147,16 +147,18 @@
placeholder="例如防滑耐磨复古网面透气设计" />
</div>
<div class="product-assets">
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index" :title="`产品图 ${index + 1}`"
:description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''" placeholder="上传产品图"
helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName" :preview-url="asset.previewUrl"
:preview-kind="asset.previewKind" :source-url="asset.sourceUrl" :loading="asset.uploading" allow-url compact
preview-clickable action-label="上传产品图" selected-label="产品图" url-label="产品图链接"
url-placeholder="也可以粘贴产品图链接" @select="(file) => setProductAsset(activeTab, index, file)"
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index"
:title="`产品图 ${index + 1}`" :description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''"
placeholder="上传产品图" helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName"
:preview-url="asset.previewUrl" :preview-kind="asset.previewKind" :source-url="asset.sourceUrl"
:loading="asset.uploading" allow-url compact preview-clickable action-label="上传产品图" selected-label="产品图"
url-label="产品图链接" url-placeholder="也可以粘贴产品图链接"
@select="(file) => setProductAsset(activeTab, index, file)"
@url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
@clear="clearProductAsset(activeTab, index)" />
<button v-if="currentWorkspace.productAssets.length < 5" type="button" class="script-action-btn product-assets__add"
@click="addProductAsset(activeTab)">添加产品图({{ currentWorkspace.productAssets.length }}/5</button>
<button v-if="currentWorkspace.productAssets.length < 5" type="button"
class="script-action-btn product-assets__add" @click="addProductAsset(activeTab)">添加产品图({{
currentWorkspace.productAssets.length }}/5</button>
</div>
</AiSectionCard>
@@ -205,16 +207,18 @@
placeholder="例如防滑耐磨复古网面透气设计" />
</div>
<div class="product-assets">
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index" :title="`产品图 ${index + 1}`"
:description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''" placeholder="上传产品图"
helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName" :preview-url="asset.previewUrl"
:preview-kind="asset.previewKind" :source-url="asset.sourceUrl" :loading="asset.uploading" allow-url compact
preview-clickable action-label="上传产品图" selected-label="产品图" url-label="产品图链接"
url-placeholder="也可以粘贴产品图链接" @select="(file) => setProductAsset(activeTab, index, file)"
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index"
:title="`产品图 ${index + 1}`" :description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''"
placeholder="上传产品图" helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName"
:preview-url="asset.previewUrl" :preview-kind="asset.previewKind" :source-url="asset.sourceUrl"
:loading="asset.uploading" allow-url compact preview-clickable action-label="上传产品图" selected-label="产品图"
url-label="产品图链接" url-placeholder="也可以粘贴产品图链接"
@select="(file) => setProductAsset(activeTab, index, file)"
@url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
@clear="clearProductAsset(activeTab, index)" />
<button v-if="currentWorkspace.productAssets.length < 5" type="button" class="script-action-btn product-assets__add"
@click="addProductAsset(activeTab)">添加产品图({{ currentWorkspace.productAssets.length }}/5</button>
<button v-if="currentWorkspace.productAssets.length < 5" type="button"
class="script-action-btn product-assets__add" @click="addProductAsset(activeTab)">添加产品图({{
currentWorkspace.productAssets.length }}/5</button>
</div>
</AiSectionCard>
@@ -260,12 +264,12 @@
</div>
<template v-if="currentWorkspace.voiceMode === 'uploadAudio'">
<AiAssetDropzone title="最终配音文件" description="对应 text_info.file_url只放最终用于视频配音的音频" placeholder="上传音频文件"
<AiAssetDropzone title="背景音乐文件" description="对应 audio_info.bgm_url仅在背景音乐模式传入" placeholder="上传背景音乐"
helper="支持 MP3 / WAV也可粘贴可访问的音频链接" accept="audio/*" :file-name="currentWorkspace.speechAsset.fileName"
:preview-url="currentWorkspace.speechAsset.previewUrl"
:preview-kind="currentWorkspace.speechAsset.previewKind"
:source-url="currentWorkspace.speechAsset.sourceUrl" :loading="currentWorkspace.speechAsset.uploading"
allow-url compact action-label="上传音频" selected-label="最终配音" url-label="最终配音文件链接"
allow-url compact action-label="上传背景音乐" selected-label="背景音乐" url-label="背景音乐链接"
@select="(file) => setAsset(activeTab, 'speechAsset', file)"
@url-change="(url) => setAssetUrl(activeTab, 'speechAsset', url, 'audio/*')"
@clear="clearAsset(activeTab, 'speechAsset')" />
@@ -275,13 +279,16 @@
<div class="delivery-form-grid delivery-form-grid--two">
<div class="delivery-field">
<span class="delivery-field__label">合成音色</span>
<el-input v-model="currentWorkspace.voiceId" clearable placeholder="选择音色或手动输入 voice_id"
@input="currentWorkspace.voiceName = ''" />
<el-input v-model="currentWorkspace.voiceId" clearable placeholder="选择音色或手动输入 voice_id" />
<div v-if="currentWorkspace.voiceName" class="selected-voice-name">
{{ currentWorkspace.voiceName }}
<span>{{ currentWorkspace.voiceId }}</span>
</div>
</div>
<div class="delivery-field">
<span class="delivery-field__label">音色名称</span>
<el-input v-model="currentWorkspace.voiceName" clearable placeholder="对应 audio_info.voice_name" />
</div>
<div class="delivery-field">
<span class="delivery-field__label">音色操作</span>
<div class="voice-inline-actions">
@@ -376,25 +383,29 @@
append-to-body destroy-on-close>
<div class="secret-status" :class="{ 'secret-status--valid': secretStatus?.valid }">
<span>{{ secretStatusText }}</span>
<span v-if="secretStatus?.expiresAt">过期时间:{{ formatSecretTime(secretStatus.expiresAt) }}</span>
<span v-if="secretExpireText">{{ secretExpireText }}</span>
</div>
<el-form label-position="top" class="secret-form">
<el-form-item label="主工作流密钥">
<el-form-item label="爬虫密钥">
<el-input v-model="secretForm.copyApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
<div v-if="secretStatus?.copyApiKeyMasked" class="secret-form__hint">当前:{{ secretStatus.copyApiKeyMasked }}
</div>
</el-form-item>
<el-form-item label="T8 平台密钥">
<el-form-item label="T8密钥海外">
<el-input v-model="secretForm.t8Key" type="password" show-password placeholder="不填写则保留当前有效值" />
<div v-if="secretStatus?.t8KeyMasked" class="secret-form__hint">当前:{{ secretStatus.t8KeyMasked }}</div>
</el-form-item>
<el-form-item label="音色服务密钥">
<el-form-item label="T8密钥国内">
<el-input v-model="secretForm.t8VideoKey" type="password" show-password placeholder="不填写则保留当前有效值" />
<div v-if="secretStatus?.t8VideoKeyMasked" class="secret-form__hint">当前:{{ secretStatus.t8VideoKeyMasked }}</div>
</el-form-item>
<el-form-item label="音频密钥">
<el-input v-model="secretForm.voiceApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
<div v-if="secretStatus?.voiceApiKeyMasked" class="secret-form__hint">当前:{{ secretStatus.voiceApiKeyMasked }}
</div>
</el-form-item>
<el-form-item label="色分组 ID">
<el-form-item label="频团队ID">
<el-input v-model="secretForm.voiceGroupId" type="password" show-password placeholder="不填写则保留当前有效值" />
<div v-if="secretStatus?.voiceGroupIdMasked" class="secret-form__hint">当前:{{ secretStatus.voiceGroupIdMasked
}}</div>
@@ -404,6 +415,7 @@
<el-option label="1 " :value="1" />
<el-option label="7 " :value="7" />
<el-option label="30 " :value="30" />
<el-option label="永久" :value="0" />
</el-select>
</el-form-item>
</el-form>
@@ -423,8 +435,8 @@
<strong>{{ filteredVoiceListItems.length }}</strong>
<span>/ {{ voiceListItems.length }} 个音色</span>
</div>
<el-input v-model="voiceSearchKeyword" class="voice-search-input" clearable
placeholder="搜索音色名称或 voice_id" @keyup.enter="listVoices" @clear="listVoices" />
<el-input v-model="voiceSearchKeyword" class="voice-search-input" clearable placeholder="搜索音色名称或 voice_id"
@keyup.enter="listVoices" @clear="listVoices" />
<button type="button" class="script-primary-action-btn" :disabled="voiceLoading" @click="listVoices">
刷新音色
</button>
@@ -438,8 +450,9 @@
</div>
<span class="voice-list__source">{{ item.sourceLabel }}</span>
<button type="button" class="script-action-btn" @click="useVoice(item)">使用此音色</button>
<button v-if="item.sourceKey !== 'system_voice'" type="button" class="script-action-btn script-action-btn--danger"
:disabled="voiceLoading" @click="deleteVoice(item)">删除音色</button>
<button v-if="item.sourceKey !== 'system_voice'" type="button"
class="script-action-btn script-action-btn--danger" :disabled="voiceLoading"
@click="deleteVoice(item)">删除音色</button>
</div>
</div>
<el-empty v-else description="暂无可展示音色请确认接口已返回音色数据" />
@@ -483,7 +496,7 @@ import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import {
cloneImageVideoVoice,
deleteImageVideoVoice,
getImageVideoWorkflowResult,
getImageVideoAsyncTask,
getImageVideoSecretStatus,
listImageVideoVoices,
runImageVideoDouyinCopy,
@@ -492,6 +505,7 @@ import {
synthesizeImageVideoVoice,
uploadImageVideoMedia,
type ImageVideoSecretStatusVo,
type ImageVideoAsyncTaskVo,
type ImageVideoWorkflowParameters,
} from '@/shared/api/java-modules'
@@ -502,6 +516,9 @@ type AssetKey = 'referenceAsset' | 'sceneAsset' | 'modelAsset' | 'speechAsset'
type VoiceMode = 'textOnly' | 'uploadAudio' | 'synthesize'
type VoiceStatusType = 'info' | 'success' | 'error'
const IMAGE_VIDEO_TASK_POLL_DELAY_MS = 5000
const IMAGE_VIDEO_TASK_MAX_POLLS = 720
type AssetState = {
fileName: string
previewUrl: string
@@ -612,6 +629,7 @@ const assemblyPrimaryActionText = computed(() => {
const secretForm = reactive({
copyApiKey: '',
t8Key: '',
t8VideoKey: '',
voiceApiKey: '',
voiceGroupId: '',
expireDays: 7,
@@ -634,9 +652,9 @@ const audioTypeOptions = [
{ label: '背景旁白', value: '2' },
]
const voiceModeOptions = [
{ label: '只提交文案', value: 'textOnly' },
{ label: '上传/粘贴已有音频', value: 'uploadAudio' },
{ label: '用音色合成配音', value: 'synthesize' },
{ label: '视频原声', value: 'textOnly' },
{ label: '背景音乐', value: 'uploadAudio' },
{ label: '参考音色', value: 'synthesize' },
]
const voiceCloneAsset = reactive<AssetState>(createAssetState())
@@ -716,6 +734,11 @@ const secretStatusText = computed(() => {
if (secretStatus.value.valid) return '密钥已配置且有效'
return '密钥已过期或不可用'
})
const secretExpireText = computed(() => {
if (!secretStatus.value?.expiresAt) return ''
if (secretStatus.value.expireDays === 0) return '有效期:永久'
return `过期时间:${formatSecretTime(secretStatus.value.expiresAt)}`
})
function normalizeDuration(value: number | string) {
const nextValue = Number(value)
@@ -913,9 +936,10 @@ function openSecretDialog() {
function resetSecretForm() {
secretForm.copyApiKey = ''
secretForm.t8Key = ''
secretForm.t8VideoKey = ''
secretForm.voiceApiKey = ''
secretForm.voiceGroupId = ''
secretForm.expireDays = secretStatus.value?.expireDays || 7
secretForm.expireDays = secretStatus.value?.expireDays ?? 7
}
async function saveSecretSettings() {
@@ -924,6 +948,7 @@ async function saveSecretSettings() {
secretStatus.value = await saveImageVideoSecrets({
copyApiKey: secretForm.copyApiKey.trim(),
t8Key: secretForm.t8Key.trim(),
t8VideoKey: secretForm.t8VideoKey.trim(),
voiceApiKey: secretForm.voiceApiKey.trim(),
voiceGroupId: secretForm.voiceGroupId.trim(),
expireDays: secretForm.expireDays,
@@ -943,6 +968,42 @@ function formatSecretTime(value: string) {
return value.replace('T', ' ').slice(0, 19)
}
function sleep(milliseconds: number) {
return new Promise<void>((resolve) => window.setTimeout(resolve, milliseconds))
}
function normalizeTaskStatus(value: string | undefined) {
return (value || '').trim().toUpperCase()
}
function isTerminalCozeStatus(value: string | undefined) {
return ['SUCCESS', 'SUCCEEDED', 'COMPLETED', 'DONE', 'FINISHED', 'FAILED', 'FAIL', 'ERROR', 'CANCELED', 'CANCELLED']
.includes(normalizeTaskStatus(value))
}
function isRecoverableFailedImageVideoTask(task: ImageVideoAsyncTaskVo) {
return task.status === 'FAILED'
&& Boolean(task.cozeExecuteId)
&& !isTerminalCozeStatus(task.cozeStatus)
}
function isTerminalImageVideoTask(task: ImageVideoAsyncTaskVo) {
return task.status === 'SUCCESS' || (task.status === 'FAILED' && !isRecoverableFailedImageVideoTask(task))
}
async function waitForImageVideoTask(ticket: ImageVideoAsyncTaskVo): Promise<unknown> {
let task = ticket
for (let pollCount = 0; pollCount < IMAGE_VIDEO_TASK_MAX_POLLS; pollCount += 1) {
if (task.status === 'SUCCESS') return task.result
if (isTerminalImageVideoTask(task)) {
throw new Error(task.errorMessage || 'Coze task failed')
}
await sleep(IMAGE_VIDEO_TASK_POLL_DELAY_MS)
task = await getImageVideoAsyncTask(task.taskId)
}
throw new Error('Coze task polling timed out')
}
async function rewriteScriptFromSource() {
const sourceUrl = currentWorkspace.value.scriptSourceUrl.trim()
const referenceUrl = currentWorkspace.value.referenceAsset.sourceUrl.trim()
@@ -958,7 +1019,7 @@ async function rewriteScriptFromSource() {
if (activeTab.value === 'remake') currentWorkspace.value.scriptSourceUrl = url
copyLearningLoading.value = true
try {
const result = await runImageVideoDouyinCopy({
const ticket = await runImageVideoDouyinCopy({
url,
duration: Math.max(0, Math.round(Number(currentWorkspace.value.durationSeconds) || 0)),
proc_info: {
@@ -968,7 +1029,8 @@ async function rewriteScriptFromSource() {
properties: currentWorkspace.value.productFocus.trim(),
},
})
const text = cleanCopyText(result.scriptDraft || result.recognizedContent || '')
const result = await waitForImageVideoTask(ticket)
const text = cleanCopyText(findTextByKeys(result, ['scriptDraft', 'script_draft', 'recognizedContent', 'recognized_content']))
currentWorkspace.value.scriptText = text
ElMessage.success('文案已提取')
} catch (error) {
@@ -1000,12 +1062,36 @@ function resolveProductImageUrls(workspace: WorkspaceState) {
return workspace.productAssets.map(resolveAssetUrl).filter(Boolean).slice(0, 5)
}
function resolveSpeechFileUrl(workspace: WorkspaceState) {
if (workspace.voiceMode === 'textOnly') return ''
function resolveVoiceAudioUrl(workspace: WorkspaceState) {
if (workspace.voiceMode !== 'synthesize') return ''
const url = resolveAssetUrl(workspace.speechAsset)
return isCozeWorkflowDebugUrl(url) ? '' : url
}
function resolveBgmUrl(workspace: WorkspaceState) {
if (workspace.voiceMode !== 'uploadAudio') return ''
const url = resolveAssetUrl(workspace.speechAsset)
return isCozeWorkflowDebugUrl(url) ? '' : url
}
function resolveAudioMode(workspace: WorkspaceState) {
if (workspace.voiceMode === 'uploadAudio') return 2
return 1
}
function resolveAudioInfo(workspace: WorkspaceState): ImageVideoWorkflowParameters['audio_info'] {
const mode = resolveAudioMode(workspace)
const audioUrl = mode === 1 ? resolveVoiceAudioUrl(workspace) : ''
const bgmUrl = mode === 2 ? resolveBgmUrl(workspace) : ''
return {
audio_url: audioUrl,
type: Number(workspace.audioType) || 1,
bgm_url: bgmUrl,
mode,
voice_name: mode === 1 ? workspace.voiceName.trim() : '',
}
}
function resolveReferenceVideoInfo(workspace: WorkspaceState) {
if (activeTab.value !== 'remake') {
return { videoUrl: '', shareUrl: '', refVideoMode: '1' }
@@ -1041,6 +1127,7 @@ function buildImageVideoWorkflowParameters(): ImageVideoWorkflowParameters | nul
return {
api_key_info: {
t8star_key: '',
t8_video_key: '',
ai_conductor_key: '',
},
bg_info: {
@@ -1059,12 +1146,9 @@ function buildImageVideoWorkflowParameters(): ImageVideoWorkflowParameters | nul
type: 1,
language: workspace.language,
text: workspace.scriptText.trim(),
file_url: resolveSpeechFileUrl(workspace),
},
audio_info: {
audio_url: resolveSpeechFileUrl(workspace),
type: Number(workspace.audioType) || 1,
file_url: resolveVoiceAudioUrl(workspace),
},
audio_info: resolveAudioInfo(workspace),
video_info: {
video_url: referenceVideoInfo.videoUrl,
share_url: referenceVideoInfo.shareUrl,
@@ -1093,14 +1177,9 @@ async function startAssembly() {
assembly.videoUrl = ''
assembly.debugUrl = ''
try {
const result = await runImageVideoWorkflow(parameters)
assembly.resultText = JSON.stringify(result, null, 2)
assembly.debugUrl = findTextByKeys(result, ['debug_url', 'debugUrl'])
const executeId = findTextByKeys(result, ['execute_id', 'executeId'])
if (executeId) {
assembly.executeId = executeId
startAssemblyPolling(tab, executeId)
}
const ticket = await runImageVideoWorkflow(parameters)
assembly.resultText = JSON.stringify(ticket, null, 2)
startAssemblyPolling(tab, ticket.taskId)
ElMessage.success('工作流已提交')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '工作流提交失败')
@@ -1149,12 +1228,12 @@ async function downloadAssemblyVideo() {
}
}
function startAssemblyPolling(tab: WorkspaceTab, executeId: string) {
function startAssemblyPolling(tab: WorkspaceTab, taskId: number) {
stopAssemblyPolling(tab)
const assembly = workspaces[tab].assembly
assembly.polling = true
assembly.pollCount = 0
void pollAssemblyResult(tab, executeId)
void pollAssemblyResult(tab, taskId)
}
function stopAssemblyPolling(tab: WorkspaceTab) {
@@ -1167,28 +1246,31 @@ function stopAssemblyPolling(tab: WorkspaceTab) {
assembly.pollCount = 0
}
async function pollAssemblyResult(tab: WorkspaceTab, executeId: string) {
if (!executeId) return
async function pollAssemblyResult(tab: WorkspaceTab, taskId: number) {
if (!taskId) return
const assembly = workspaces[tab].assembly
assembly.pollCount += 1
try {
const result = await getImageVideoWorkflowResult(executeId)
const status = resolveWorkflowStatus(result)
const task = await getImageVideoAsyncTask(taskId)
const result = task.result
const status = task.cozeStatus || task.status
const videoUrl = findVideoResultUrl(result)
const debugUrl = findTextByKeys(result, ['debug_url', 'debugUrl'])
if (task.cozeExecuteId) assembly.executeId = task.cozeExecuteId
if (videoUrl) assembly.videoUrl = videoUrl
if (debugUrl) assembly.debugUrl = debugUrl
assembly.resultText = JSON.stringify({
execute_id: executeId,
polling: !isTerminalWorkflowStatus(status),
task_id: taskId,
execute_id: task.cozeExecuteId || assembly.executeId,
polling: !isTerminalImageVideoTask(task),
poll_count: assembly.pollCount,
status,
video_url: videoUrl || assembly.videoUrl,
result,
}, null, 2)
if (isTerminalWorkflowStatus(status)) {
if (isTerminalImageVideoTask(task)) {
assembly.polling = false
if (isFailedWorkflowStatus(status)) {
if (task.status === 'FAILED') {
ElMessage.error('Coze 工作流执行失败')
} else {
ElMessage.success(videoUrl || assembly.videoUrl ? '视频生成完成' : 'Coze 工作流执行完成,未解析到视频地址')
@@ -1197,7 +1279,7 @@ async function pollAssemblyResult(tab: WorkspaceTab, executeId: string) {
}
} catch (error) {
assembly.resultText = JSON.stringify({
execute_id: executeId,
task_id: taskId,
polling: false,
error: error instanceof Error ? error.message : '查询 Coze 执行结果失败',
}, null, 2)
@@ -1212,8 +1294,8 @@ async function pollAssemblyResult(tab: WorkspaceTab, executeId: string) {
return
}
assembly.pollTimer = window.setTimeout(() => {
void pollAssemblyResult(tab, executeId)
}, 5000)
void pollAssemblyResult(tab, taskId)
}, IMAGE_VIDEO_TASK_POLL_DELAY_MS)
}
function setVoiceStatus(message: string, type: VoiceStatusType = 'info') {
@@ -1548,7 +1630,8 @@ function firstString(...values: unknown[]) {
async function listVoices() {
voiceLoading.value = true
try {
const result = await listImageVideoVoices(voiceSearchKeyword.value.trim())
const ticket = await listImageVideoVoices(voiceSearchKeyword.value.trim())
const result = await waitForImageVideoTask(ticket)
voiceListItems.value = extractVoiceItems(result)
setVoiceStatus(
voiceListItems.value.length ? `已获取 ${voiceListItems.value.length} 个音色` : '音色接口已返回,未解析到列表项',
@@ -1580,7 +1663,8 @@ async function deleteVoice(item: VoiceListItem) {
}
voiceLoading.value = true
try {
const result = await deleteImageVideoVoice(voiceId)
const ticket = await deleteImageVideoVoice(voiceId)
await waitForImageVideoTask(ticket)
voiceListItems.value = voiceListItems.value.filter((item) => item.voiceId !== voiceId)
setVoiceStatus(`删除音色请求已提交:${voiceId}`, 'success')
ElMessage.success('删除音色请求已提交')
@@ -1610,15 +1694,16 @@ async function cloneVoice() {
audioUrl: sourceUrl,
videoUrl: voiceCloneAsset.mediaType === 'video' ? sourceUrl : '',
}
const result = await cloneImageVideoVoice({
const ticket = await cloneImageVideoVoice({
name,
audioUrl: clonePayload.audioUrl,
videoUrl: clonePayload.videoUrl,
})
const result = await waitForImageVideoTask(ticket)
const voiceId = findTextByKeys(result, ['voice_id', 'voiceId', 'voiceID'])
if (voiceId) {
currentWorkspace.value.voiceId = voiceId
currentWorkspace.value.voiceName = ''
currentWorkspace.value.voiceName = name
setVoiceStatus(`音色克隆完成,已回填 voice_id${voiceId}`, 'success')
ElMessage.success('音色克隆完成,已回填 voice_id')
} else {
@@ -1648,7 +1733,8 @@ async function synthesizeVoice() {
}
voiceLoading.value = true
try {
const result = await synthesizeImageVideoVoice({ text, voiceId })
const ticket = await synthesizeImageVideoVoice({ text, voiceId })
const result = await waitForImageVideoTask(ticket)
const audioUrl = findMediaFileUrl(result)
if (audioUrl) {
setAssetUrl(activeTab.value, 'speechAsset', audioUrl, 'audio/*')

View File

@@ -2123,7 +2123,9 @@ export interface PriceTrackAsinParsedRow {
shippingFee?: string;
minimumPrice?: string;
firstPlace?: string;
firstShop?: string;
secondPlace?: string;
secondShop?: string;
cartShopName?: string;
priceChangeStatus?: string;
modifyCount?: string;
@@ -2417,6 +2419,10 @@ export function getTaskSkipPriceAsinsPaginated(
taskId: number,
page: number = 1,
pageSize: number = 1000,
options: {
shopName?: string | string[];
countryCode?: string | string[];
} = {},
) {
return unwrapJavaResponse(
get<JavaApiResponse<SkipPriceAsinPageVo>>(
@@ -2425,6 +2431,8 @@ export function getTaskSkipPriceAsinsPaginated(
params: {
page,
page_size: pageSize,
...(options.shopName ? { shop_name: options.shopName } : {}),
...(options.countryCode ? { country_code: options.countryCode } : {}),
},
},
),
@@ -2503,10 +2511,12 @@ export interface ImageVideoSecretStatusVo {
expired?: boolean;
hasCopyApiKey?: boolean;
hasT8Key?: boolean;
hasT8VideoKey?: boolean;
hasVoiceApiKey?: boolean;
hasVoiceGroupId?: boolean;
copyApiKeyMasked?: string;
t8KeyMasked?: string;
t8VideoKeyMasked?: string;
voiceApiKeyMasked?: string;
voiceGroupIdMasked?: string;
expireDays?: number;
@@ -2516,6 +2526,7 @@ export interface ImageVideoSecretStatusVo {
export interface ImageVideoSecretSavePayload {
copyApiKey?: string;
t8Key?: string;
t8VideoKey?: string;
voiceApiKey?: string;
voiceGroupId?: string;
expireDays: number;
@@ -2524,6 +2535,7 @@ export interface ImageVideoSecretSavePayload {
export interface ImageVideoWorkflowParameters {
api_key_info: {
t8star_key: string;
t8_video_key: string;
ai_conductor_key: string;
};
bg_info: {
@@ -2551,6 +2563,9 @@ export interface ImageVideoWorkflowParameters {
audio_info: {
audio_url: string;
type: number;
bgm_url: string;
mode: number;
voice_name: string;
};
video_info: {
video_url: string;
@@ -2575,6 +2590,20 @@ export interface ImageVideoWorkflowResponse {
[key: string]: unknown;
}
export type ImageVideoAsyncTaskStatus = 'PENDING' | 'RUNNING' | 'WAITING' | 'POLLING' | 'SUCCESS' | 'FAILED'
export interface ImageVideoAsyncTaskVo {
taskId: number;
taskType: string;
status: ImageVideoAsyncTaskStatus;
cozeExecuteId?: string;
cozeStatus?: string;
result?: unknown;
errorMessage?: string;
submittedAt?: string;
completedAt?: string;
}
export interface ImageVideoMediaUploadVo {
url: string;
objectKey: string;
@@ -2621,30 +2650,36 @@ export function saveImageVideoSecrets(payload: ImageVideoSecretSavePayload) {
export function runImageVideoDouyinCopy(payload: ImageVideoDouyinCopyPayload) {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoDouyinCopyVo>, ImageVideoDouyinCopyPayload & { userId: number }>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, ImageVideoDouyinCopyPayload & { userId: number }>(
`${JAVA_API_PREFIX}/image-video/douyin-copy`,
{ userId: getCurrentUserId(), ...payload },
{ timeout: 180000 },
),
);
}
export function runImageVideoWorkflow(parameters: ImageVideoWorkflowParameters) {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoWorkflowResponse>, ImageVideoWorkflowRunRequest>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, ImageVideoWorkflowRunRequest>(
`${JAVA_API_PREFIX}/image-video/workflow/run`,
{ userId: getCurrentUserId(), parameters },
{ timeout: 3600000 },
),
);
}
export function getImageVideoWorkflowResult(executeId: string) {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoWorkflowResponse>, ImageVideoWorkflowResultRequest>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, ImageVideoWorkflowResultRequest>(
`${JAVA_API_PREFIX}/image-video/workflow/result`,
{ userId: getCurrentUserId(), executeId },
{ timeout: 3600000 },
),
);
}
export function getImageVideoAsyncTask(taskId: number) {
return unwrapJavaResponse(
get<JavaApiResponse<ImageVideoAsyncTaskVo>>(
`${JAVA_API_PREFIX}/image-video/tasks/${taskId}`,
{ params: { user_id: getCurrentUserId() } },
),
);
}
@@ -2680,40 +2715,36 @@ function resolveImageVideoMediaType(file: File) {
export function listImageVideoVoices(name = "") {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; name: string }>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; name: string }>(
`${JAVA_API_PREFIX}/image-video/voice/list`,
{ userId: getCurrentUserId(), name },
{ timeout: 180000 },
),
);
}
export function deleteImageVideoVoice(voiceId: string) {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; voiceId: string }>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; voiceId: string }>(
`${JAVA_API_PREFIX}/image-video/voice/delete`,
{ userId: getCurrentUserId(), voiceId },
{ timeout: 180000 },
),
);
}
export function cloneImageVideoVoice(payload: { name?: string; audioUrl?: string; videoUrl?: string }) {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; name?: string; audioUrl?: string; videoUrl?: string }>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; name?: string; audioUrl?: string; videoUrl?: string }>(
`${JAVA_API_PREFIX}/image-video/voice/clone`,
{ userId: getCurrentUserId(), ...payload },
{ timeout: 180000 },
),
);
}
export function synthesizeImageVideoVoice(payload: { text: string; voiceId: string }) {
return unwrapJavaResponse(
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; text: string; voiceId: string }>(
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; text: string; voiceId: string }>(
`${JAVA_API_PREFIX}/image-video/voice/synthesis`,
{ userId: getCurrentUserId(), ...payload },
{ timeout: 180000 },
),
);
}