feat(kling): add validation error display in timeline panel and update UI components
- Remove unused audio base64 reference and error message in VoiceSelector - Rename CSS class from 'result-banner' to 'result-inline' and update button styling - Pass validationError prop from GenerateStep to TimelinePanel - Add validation error display in TimelinePanel with error state styling - Update conditional rendering to show either validation error or duration diff - Add CloseCircleOutlined icon for error status display
This commit is contained in:
@@ -103,7 +103,6 @@ const playerContainer = ref(null)
|
||||
const audioUrl = ref('')
|
||||
const currentVoiceName = ref('')
|
||||
const isPlayerInitializing = ref(false)
|
||||
const currentAudioBase64 = ref('') // 保存当前音频的 base64 数据
|
||||
|
||||
// 默认封面图片(音频波形图标)
|
||||
const defaultCover = `data:image/svg+xml;base64,${btoa(`
|
||||
@@ -208,7 +207,6 @@ const handlePlayVoiceSample = (voice) => {
|
||||
(data) => {
|
||||
const url = data.audioUrl || data.objectUrl
|
||||
if (!url) return
|
||||
currentAudioBase64.value = data.audioBase64 || ''
|
||||
initPlayer(url)
|
||||
},
|
||||
undefined, // 错误静默处理
|
||||
@@ -259,7 +257,6 @@ const initPlayer = (url) => {
|
||||
|
||||
player.on('error', (e) => {
|
||||
console.error('APlayer 播放错误:', e)
|
||||
message.error('音频播放失败,请重试')
|
||||
})
|
||||
|
||||
player.on('canplay', () => {
|
||||
|
||||
@@ -850,7 +850,7 @@ onMounted(async () => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.result-banner {
|
||||
.result-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -870,17 +870,7 @@ onMounted(async () => {
|
||||
color: @accent-red;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
.inline-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid currentColor;
|
||||
@@ -890,9 +880,6 @@ onMounted(async () => {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
:audio-duration-ms="store.timeline.audioDurationMs"
|
||||
:face-start-time="store.timeline.faceStartTime"
|
||||
:face-end-time="store.timeline.faceEndTime"
|
||||
:validation-error="store.validationError"
|
||||
/>
|
||||
|
||||
<!-- 积分预估 -->
|
||||
|
||||
@@ -72,8 +72,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 校验错误提示 -->
|
||||
<div v-if="validationError" class="timeline-diff error">
|
||||
<CloseCircleOutlined class="diff-icon" />
|
||||
<span>{{ validationError }}</span>
|
||||
</div>
|
||||
<!-- 时长差异提示 -->
|
||||
<div v-if="audioDurationMs > 0" class="timeline-diff" :class="diffStatus">
|
||||
<div v-else-if="audioDurationMs > 0" class="timeline-diff" :class="diffStatus">
|
||||
<template v-if="diffStatus === 'match'">
|
||||
<CheckCircleOutlined class="diff-icon" />
|
||||
<span>时长匹配良好,可以生成</span>
|
||||
@@ -92,7 +97,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined, InfoCircleOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { formatDurationMs } from '../utils/format'
|
||||
|
||||
interface Props {
|
||||
@@ -100,11 +105,13 @@ interface Props {
|
||||
audioDurationMs: number
|
||||
faceStartTime?: number
|
||||
faceEndTime?: number
|
||||
validationError?: string | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
faceStartTime: 0,
|
||||
faceEndTime: 0,
|
||||
validationError: null,
|
||||
})
|
||||
|
||||
const maxDuration = computed(() => {
|
||||
@@ -393,5 +400,10 @@ const formatDuration = formatDurationMs
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: @accent-orange;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: @accent-red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
:audio-duration-ms="store.timeline.audioDurationMs"
|
||||
:face-start-time="store.timeline.faceStartTime"
|
||||
:face-end-time="store.timeline.faceEndTime"
|
||||
:validation-error="store.validationError"
|
||||
/>
|
||||
|
||||
<!-- 生成音频按钮 -->
|
||||
|
||||
@@ -126,10 +126,22 @@ export const useDigitalHumanStore = defineStore('digitalHuman', () => {
|
||||
const canGenerate = computed(() => {
|
||||
if (!isVideoReady.value || !isAudioReady.value) return false
|
||||
if (!timeline.value) return false
|
||||
// 音频时长不能超过人脸时长
|
||||
// 1. 人脸区间至少2秒
|
||||
if (timeline.value.videoDurationMs < 2000) return false
|
||||
// 2. 音频时长不能超过人脸时长
|
||||
return timeline.value.audioDurationMs <= timeline.value.videoDurationMs
|
||||
})
|
||||
|
||||
/** 校验失败原因 */
|
||||
const validationError = computed(() => {
|
||||
if (!timeline.value) return '请先完成视频识别'
|
||||
if (!isVideoReady.value) return '请先完成视频识别'
|
||||
if (!isAudioReady.value) return '请先生成音频'
|
||||
if (timeline.value.videoDurationMs < 2000) return '人脸区间不足2秒,无法生成对口型视频'
|
||||
if (timeline.value.audioDurationMs > timeline.value.videoDurationMs) return '音频时长超过人脸时长,请缩短文案'
|
||||
return null
|
||||
})
|
||||
|
||||
/** 时间轴匹配状态 */
|
||||
const timelineMatch = computed(() => {
|
||||
if (!timeline.value || timeline.value.audioDurationMs === 0) return 'none'
|
||||
@@ -658,6 +670,7 @@ export const useDigitalHumanStore = defineStore('digitalHuman', () => {
|
||||
isAudioReady,
|
||||
canGoNext,
|
||||
canGenerate,
|
||||
validationError,
|
||||
timelineMatch,
|
||||
faceDurationMs,
|
||||
audioDurationMs,
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
type="link"
|
||||
size="small"
|
||||
class="action-btn action-btn--primary"
|
||||
@click="handlePreview(record)"
|
||||
@click="openVideoUrl(record)"
|
||||
>
|
||||
<PlayCircleOutlined /> 预览
|
||||
</a-button>
|
||||
@@ -111,7 +111,7 @@
|
||||
type="link"
|
||||
size="small"
|
||||
class="action-btn action-btn--success"
|
||||
@click="handleDownload(record)"
|
||||
@click="openVideoUrl(record)"
|
||||
>
|
||||
<DownloadOutlined /> 下载
|
||||
</a-button>
|
||||
@@ -170,17 +170,8 @@ const rowSelection = {
|
||||
// 状态判断
|
||||
const isStatus = (status, target) => status === target || status === target.toUpperCase()
|
||||
|
||||
// 预览视频(直接打开链接)
|
||||
const handlePreview = (record) => {
|
||||
if (!record.resultVideoUrl) {
|
||||
message.warning('该任务暂无视频结果,请稍后再试')
|
||||
return
|
||||
}
|
||||
window.open(record.resultVideoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 下载视频
|
||||
const handleDownload = (record) => {
|
||||
// 打开视频链接(预览/下载共用)
|
||||
const openVideoUrl = (record) => {
|
||||
if (!record.resultVideoUrl) {
|
||||
message.warning('该任务暂无视频结果,请稍后再试')
|
||||
return
|
||||
|
||||
@@ -580,43 +580,6 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
||||
StrUtil.blankToDefault(requestFormat, getDefaultFormat()));
|
||||
}
|
||||
|
||||
private String buildFileName(String voiceId, String format) {
|
||||
String safeVoice = StrUtil.blankToDefault(voiceId, "voice")
|
||||
.replaceAll("[^a-zA-Z0-9_-]", "");
|
||||
return safeVoice + "-" + System.currentTimeMillis() + "." + format;
|
||||
}
|
||||
|
||||
private String resolveContentType(String format) {
|
||||
if (format == null) {
|
||||
return "audio/mpeg";
|
||||
}
|
||||
return switch (format.toLowerCase()) {
|
||||
case "wav" -> "audio/wav";
|
||||
case "flac" -> "audio/flac";
|
||||
default -> "audio/mpeg";
|
||||
};
|
||||
}
|
||||
|
||||
private String determineSynthesisText(String transcriptionText, String inputText, boolean allowFallback) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (StrUtil.isNotBlank(transcriptionText)) {
|
||||
builder.append(transcriptionText.trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(inputText)) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append("\n");
|
||||
}
|
||||
builder.append(inputText.trim());
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
return builder.toString();
|
||||
}
|
||||
if (allowFallback) {
|
||||
return getPreviewText();
|
||||
}
|
||||
throw exception(VOICE_TTS_FAILED, "请提供需要合成的文本内容");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取原始URL(去除查询参数和锚点)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user