提交昨晚更新改动
This commit is contained in:
@@ -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/*')
|
||||
|
||||
@@ -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 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user