feat: 功能优化

This commit is contained in:
2026-02-04 01:46:55 +08:00
parent 0e1b6fe643
commit b1bd711cea
6 changed files with 300 additions and 495 deletions

View File

@@ -137,55 +137,30 @@
</div>
</div>
<!-- 配音生成与校验仅在识别后显示 -->
<div v-if="identifyState.identified" class="section audio-generation-section">
<h3>配音生成与校验</h3>
<!-- 生成配音按钮 -->
<div class="generate-audio-row">
<a-button
type="default"
size="large"
:disabled="!canGenerateAudio"
:loading="audioState.generating"
block
@click="generateAudio"
>
{{ audioState.generating ? '生成中...' : '生成配音(用于校验时长)' }}
</a-button>
</div>
<!-- 音频预览 -->
<div v-if="audioState.generated" class="audio-preview">
<div class="audio-info">
<h4>生成的配音</h4>
<div class="duration-info">
<span class="label">音频时长</span>
<span class="value">{{ audioDurationSec }} </span>
</div>
<div class="duration-info">
<span class="label">人脸区间</span>
<span class="value">{{ faceDurationSec }} </span>
</div>
<div class="duration-info" :class="{ 'validation-passed': validationPassed, 'validation-failed': !validationPassed }">
<span class="label">校验结果</span>
<span class="value">
{{ validationPassed ? '✅ 通过' : '❌ 不通过(音频时长不能超过人脸时长)' }}
</span>
</div>
<!-- 配音生成仅在 Pipeline 到达 ready 状态后显示 -->
<div v-if="isPipelineReady" class="section audio-section">
<!-- 已生成音频 -->
<div v-if="audioState.generated" class="audio-generated">
<div class="audio-header">
<span class="audio-title">配音</span>
<span class="audio-duration">{{ audioDurationSec }}</span>
</div>
<!-- 音频播放器 -->
<div v-if="audioUrl" class="audio-player">
<audio :src="audioUrl" controls class="audio-element" />
<div v-if="audioUrl" class="audio-player-wrapper">
<audio :src="audioUrl" controls class="audio-player" />
</div>
<!-- 重新生成按钮 -->
<div class="regenerate-row">
<a-button type="link" size="small" @click="generateAudio" :loading="audioState.generating">
重新生成
</a-button>
<!-- 校验失败提示 -->
<div v-if="!validationPassed" class="validation-warning">
<span class="warning-icon"></span>
<span class="warning-text">音频时长({{ audioDurationSec }})超过视频人脸区间({{ faceDurationSec }})请缩短文案或调整语速</span>
</div>
<!-- 重新生成 -->
<a-button type="link" size="small" :loading="audioState.generating" @click="generateAudio">
重新生成
</a-button>
</div>
</div>
@@ -205,12 +180,26 @@
<!-- 按钮组 -->
<div class="action-buttons">
<!-- 准备阶段先运行到 ready -->
<a-button
v-if="!isPipelineReady"
type="primary"
size="large"
:disabled="!canGenerate"
:loading="isPipelineBusy"
block
@click="generateAudio"
>
{{ isPipelineBusy ? '处理中...' : '生成配音并验证' }}
</a-button>
<!-- Ready 生成数字人视频 -->
<a-button
v-else
type="primary"
size="large"
:loading="isPipelineBusy"
block
@click="generateDigitalHuman"
>
{{ isPipelineBusy ? '处理中...' : '生成数字人视频' }}
@@ -250,19 +239,16 @@ const dragOver = ref(false)
// Controller 内部直接创建和管理两个子 Hook
const controller = useIdentifyFaceController()
// 解构 controller 以简化模板调用
const {
// 语音生成相关
ttsText,
speechRate,
audioState,
canGenerateAudio,
generateAudio,
// 数字人生成相关
videoState,
identifyState,
getVideoPreviewUrl,
// 计算属性
@@ -276,7 +262,7 @@ const {
audioUrl,
validationPassed,
// Pipeline 状态
// Pipeline 状态(单一状态源)
pipelineState,
isPipelineBusy,
isPipelineReady,
@@ -370,20 +356,15 @@ onMounted(async () => {
font-weight: 600;
}
.card-content h4,
.audio-info h4 {
.card-content h4 {
color: var(--text-primary);
font-size: 14px;
margin-bottom: 12px;
}
.card-content p,
.duration-label span:first-child {
.card-content p {
color: var(--text-secondary);
font-size: 13px;
}
.card-content p {
margin: 0;
}
@@ -401,24 +382,6 @@ onMounted(async () => {
}
}
.text-hint {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 12px 16px;
background: rgba(var(--color-primary), 0.1);
border: 1px solid rgba(var(--color-primary), 0.3);
border-radius: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.hint-icon {
font-size: 16px;
}
/* ========== 控制面板 ========== */
.control-group {
margin-bottom: 16px;
}
@@ -635,175 +598,76 @@ onMounted(async () => {
border-radius: 8px;
}
/* ========== 验证结果 ========== */
.validation-result {
padding: 16px;
background: var(--bg-primary);
border-radius: 8px;
border: 1px solid var(--border-light);
/* ========== 音频区域 ========== */
.audio-section {
margin-bottom: 24px;
}
.validation-result.validation-passed {
border-color: var(--color-success);
background: rgba(var(--color-success), 0.05);
.audio-generated {
display: flex;
flex-direction: column;
gap: 12px;
}
.validation-result.validation-failed {
border-color: var(--color-error);
background: rgba(var(--color-error), 0.05);
}
.validation-status {
.audio-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
justify-content: space-between;
}
.status-icon {
font-size: 18px;
}
.status-text {
color: var(--text-primary);
.audio-title {
font-size: 14px;
font-weight: 600;
}
/* ========== 时长对比进度条 ========== */
.duration-comparison {
margin-bottom: 16px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
}
.duration-bar {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.duration-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
}
.duration-value {
color: var(--text-primary);
font-weight: 600;
font-size: 13px;
padding: 4px 8px;
background: var(--bg-primary);
border-radius: 4px;
}
.progress-bar {
height: 8px;
background: var(--bg-primary);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s;
}
.audio-bar .progress-fill {
background: var(--color-primary);
}
.video-bar .progress-fill.success {
background: var(--color-success);
}
.video-bar .progress-fill.error {
background: var(--color-error);
}
/* ========== 错误提示 ========== */
.validation-error {
padding: 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 6px;
}
.error-message {
color: var(--color-error);
font-size: 13px;
margin: 0 0 12px 0;
}
.quick-actions {
display: flex;
gap: 8px;
}
/* ========== 音频生成 ========== */
.audio-generation-section {
margin-bottom: 24px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-light);
}
.generate-audio-row {
margin-bottom: 16px;
}
.audio-preview {
padding: 16px;
background: var(--bg-primary);
border-radius: 8px;
}
.duration-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
}
.duration-info .label {
.audio-duration {
font-size: 12px;
color: var(--text-secondary);
}
.duration-info .value {
color: var(--text-primary);
font-weight: 600;
}
.duration-info.validation-passed .value {
color: var(--color-success);
}
.duration-info.validation-failed .value {
color: var(--color-error);
}
.audio-player {
margin: 16px 0;
}
.audio-element {
.audio-player-wrapper {
width: 100%;
}
.regenerate-row {
.audio-player {
width: 100%;
height: 36px;
}
.validation-warning {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
background: rgba(var(--color-warning), 0.1);
border: 1px solid rgba(var(--color-warning), 0.3);
border-radius: 6px;
font-size: 13px;
}
.warning-icon {
flex-shrink: 0;
font-size: 14px;
}
.warning-text {
color: var(--text-secondary);
line-height: 1.4;
}
.audio-prompt {
text-align: center;
margin-top: 12px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px dashed var(--border-light);
}
.audio-prompt p {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--text-secondary);
}
/* ========== 操作按钮 ========== */
@@ -823,18 +687,6 @@ onMounted(async () => {
border-radius: 8px;
}
.generate-hint {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: rgba(var(--color-warning), 0.1);
border: 1px solid rgba(var(--color-warning), 0.3);
border-radius: 6px;
font-size: 13px;
color: var(--color-warning);
}
/* ========== 响应式 ========== */
@media (max-width: 1024px) {
.kling-content {