feat: 功能
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user