feat: 功能

This commit is contained in:
2026-02-04 01:18:16 +08:00
parent f8e40c039d
commit 0e1b6fe643
19 changed files with 1472 additions and 1008 deletions

View File

@@ -15,10 +15,6 @@
:show-count="true"
class="tts-textarea"
/>
<div v-if="identifyState.identified && faceDuration > 0" class="text-hint">
<span class="hint-icon">💡</span>
<span>视频中人脸出现时长约 {{ (faceDuration / 1000).toFixed(1) }} 建议文案不超过 {{ suggestedMaxChars }} </span>
</div>
</div>
<!-- 音色选择 -->
@@ -141,58 +137,6 @@
</div>
</div>
<!-- 素材校验结果 -->
<div v-if="materialValidation.videoDuration > 0 && materialValidation.audioDuration > 0" class="section">
<h3>素材校验</h3>
<div class="validation-result" :class="{ 'validation-passed': materialValidation.isValid, 'validation-failed': !materialValidation.isValid }">
<div class="validation-status">
<span class="status-icon">{{ materialValidation.isValid ? '✅' : '❌' }}</span>
<span class="status-text">{{ materialValidation.isValid ? '校验通过' : '校验失败' }}</span>
</div>
<!-- 时长对比进度条 -->
<div class="duration-comparison">
<div class="duration-bar">
<div class="duration-label">
<span>音频时长</span>
<span class="duration-value">{{ (materialValidation.audioDuration / 1000).toFixed(1) }}s</span>
</div>
<div class="progress-bar audio-bar">
<div
class="progress-fill"
:style="{ width: `${(materialValidation.audioDuration / Math.max(materialValidation.videoDuration, materialValidation.audioDuration)) * 100}%` }"
></div>
</div>
</div>
<div class="duration-bar">
<div class="duration-label">
<span>视频时长</span>
<span class="duration-value">{{ (materialValidation.videoDuration / 1000).toFixed(1) }}s</span>
</div>
<div class="progress-bar video-bar">
<div
class="progress-fill"
:class="{ 'success': materialValidation.isValid, 'error': !materialValidation.isValid }"
:style="{ width: `${(materialValidation.videoDuration / Math.max(materialValidation.videoDuration, materialValidation.audioDuration)) * 100}%` }"
></div>
</div>
</div>
</div>
<!-- 失败提示和建议 -->
<div v-if="!materialValidation.isValid" class="validation-error">
<p class="error-message">
视频时长必须大于音频时长才能生成数字人视频
</p>
<div class="quick-actions">
<a-button size="small" @click="replaceVideo">更换视频</a-button>
<a-button size="small" @click="handleSimplifyScript">精简文案</a-button>
</div>
</div>
</div>
</div>
<!-- 配音生成与校验仅在识别后显示 -->
<div v-if="identifyState.identified" class="section audio-generation-section">
<h3>配音生成与校验</h3>
@@ -211,73 +155,67 @@
</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">{{ (audioState.durationMs / 1000).toFixed(1) }} </span>
<span class="value">{{ audioDurationSec }} </span>
</div>
<div class="duration-info">
<span class="label">人脸区间</span>
<span class="value">{{ (faceDuration / 1000).toFixed(1) }} </span>
<span class="value">{{ faceDurationSec }} </span>
</div>
<div class="duration-info" :class="{ 'validation-passed': audioState.validationPassed, 'validation-failed': !audioState.validationPassed }">
<div class="duration-info" :class="{ 'validation-passed': validationPassed, 'validation-failed': !validationPassed }">
<span class="label">校验结果</span>
<span class="value">
{{ audioState.validationPassed ? '✅ 通过' : '❌ 不通过(需至少2秒重合' }}
{{ validationPassed ? '✅ 通过' : '❌ 不通过(音频时长不能超过人脸时长' }}
</span>
</div>
</div>
<!-- 音频播放器 -->
<div class="audio-player">
<audio
v-if="audioState.generated.audioBase64"
:src="`data:audio/mp3;base64,${audioState.generated.audioBase64}`"
controls
class="audio-element"
/>
<audio
v-else-if="audioState.generated.audioUrl"
:src="audioState.generated.audioUrl"
controls
class="audio-element"
/>
<div v-if="audioUrl" class="audio-player">
<audio :src="audioUrl" controls class="audio-element" />
</div>
<!-- 重新生成按钮 -->
<div class="regenerate-row">
<a-button
type="link"
size="small"
@click="generateAudio"
:loading="audioState.generating"
>
<a-button type="link" size="small" @click="generateAudio" :loading="audioState.generating">
重新生成
</a-button>
</div>
</div>
</div>
<!-- Pipeline 进度条 -->
<PipelineProgress
v-if="isPipelineBusy || isPipelineReady || isPipelineFailed || isPipelineCompleted"
:state="pipelineState"
:progress="pipelineProgress"
:is-busy="isPipelineBusy"
:is-ready="isPipelineReady"
:is-failed="isPipelineFailed"
:is-completed="isPipelineCompleted"
:error="pipelineError"
@retry="retryPipeline"
@reset="resetPipeline"
/>
<!-- 按钮组 -->
<div class="action-buttons">
<a-button
type="primary"
size="large"
:disabled="!canGenerate"
:loading="isPipelineBusy"
block
@click="generateDigitalHuman"
>
生成数字人视频
{{ isPipelineBusy ? '处理中...' : '生成数字人视频' }}
</a-button>
<!-- 添加提示信息 -->
<div v-if="canGenerate && !audioState.validationPassed" class="generate-hint">
<span class="hint-icon"></span>
<span>请先生成配音并通过时长校验</span>
</div>
</div>
</div>
@@ -299,6 +237,7 @@ import VideoSelector from '@/components/VideoSelector.vue'
import VoiceSelector from '@/components/VoiceSelector.vue'
import ResultPanel from '@/components/ResultPanel.vue'
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
import PipelineProgress from '@/components/PipelineProgress.vue'
// Controller Hook
import { useIdentifyFaceController } from './hooks/useIdentifyFaceController'
@@ -311,6 +250,7 @@ const dragOver = ref(false)
// Controller 内部直接创建和管理两个子 Hook
const controller = useIdentifyFaceController()
// 解构 controller 以简化模板调用
const {
// 语音生成相关
@@ -318,14 +258,11 @@ const {
speechRate,
audioState,
canGenerateAudio,
suggestedMaxChars,
generateAudio,
// 数字人生成相关
videoState,
identifyState,
materialValidation,
faceDuration,
getVideoPreviewUrl,
// 计算属性
@@ -334,6 +271,21 @@ const {
textareaPlaceholder,
speechRateMarks,
speechRateDisplay,
faceDurationSec,
audioDurationSec,
audioUrl,
validationPassed,
// Pipeline 状态
pipelineState,
isPipelineBusy,
isPipelineReady,
isPipelineFailed,
isPipelineCompleted,
pipelineProgress,
pipelineError,
retryPipeline,
resetPipeline,
// 事件处理方法
handleVoiceSelect,
@@ -343,7 +295,6 @@ const {
handleSelectUpload,
handleSelectFromLibrary,
handleVideoSelect,
handleSimplifyScript,
handleVideoLoaded,
replaceVideo,
generateDigitalHuman,