|
|
|
|
@@ -22,13 +22,32 @@ import { useDigitalHumanGeneration } from './useDigitalHumanGeneration'
|
|
|
|
|
* 内部直接创建和管理两个子 Hook
|
|
|
|
|
*/
|
|
|
|
|
export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
// ==================== 创建子 Hooks ====================
|
|
|
|
|
// ==================== 创建子 Hooks 并解构 ====================
|
|
|
|
|
|
|
|
|
|
// 1. 创建语音生成 Hook(独立管理状态)
|
|
|
|
|
const voiceGeneration = useVoiceGeneration()
|
|
|
|
|
// 1. 语音生成 Hook - 解构响应式变量
|
|
|
|
|
const {
|
|
|
|
|
ttsText,
|
|
|
|
|
speechRate,
|
|
|
|
|
selectedVoiceMeta,
|
|
|
|
|
audioState,
|
|
|
|
|
canGenerateAudio,
|
|
|
|
|
suggestedMaxChars,
|
|
|
|
|
generateAudio,
|
|
|
|
|
resetAudioState,
|
|
|
|
|
} = useVoiceGeneration()
|
|
|
|
|
|
|
|
|
|
// 2. 创建数字人生成 Hook(独立管理状态)
|
|
|
|
|
const digitalHuman = useDigitalHumanGeneration()
|
|
|
|
|
// 2. 数字人生成 Hook - 解构响应式变量
|
|
|
|
|
const {
|
|
|
|
|
videoState,
|
|
|
|
|
identifyState,
|
|
|
|
|
faceDuration,
|
|
|
|
|
performFaceRecognition,
|
|
|
|
|
handleFileUpload,
|
|
|
|
|
handleVideoSelect: _handleVideoSelect,
|
|
|
|
|
getVideoPreviewUrl,
|
|
|
|
|
resetVideoState,
|
|
|
|
|
resetIdentifyState,
|
|
|
|
|
} = useDigitalHumanGeneration()
|
|
|
|
|
|
|
|
|
|
// 3. Controller 统一管理跨 Hook 的状态
|
|
|
|
|
const materialValidation = ref<MaterialValidation>({
|
|
|
|
|
@@ -40,15 +59,15 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
|
|
|
|
|
// 4. 监听音频状态变化,自动触发素材校验
|
|
|
|
|
watch(
|
|
|
|
|
() => voiceGeneration.audioState.value.generated && voiceGeneration.audioState.value.durationMs > 0,
|
|
|
|
|
() => audioState.value.generated && audioState.value.durationMs > 0,
|
|
|
|
|
(newVal, oldVal) => {
|
|
|
|
|
if (newVal && !oldVal) {
|
|
|
|
|
// 音频生成完成,获取视频时长并校验
|
|
|
|
|
const videoDuration = digitalHuman.faceDuration.value || 0
|
|
|
|
|
const audioDuration = voiceGeneration.audioState.value.durationMs
|
|
|
|
|
const videoDurationMs = faceDuration.value || 0
|
|
|
|
|
const audioDurationMs = audioState.value.durationMs
|
|
|
|
|
|
|
|
|
|
if (videoDuration > 0) {
|
|
|
|
|
validateMaterialDuration(videoDuration, audioDuration)
|
|
|
|
|
if (videoDurationMs > 0) {
|
|
|
|
|
validateMaterialDuration(videoDurationMs, audioDurationMs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
@@ -57,19 +76,19 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
|
|
|
|
|
// 5. 监听人脸识别状态变化,更新素材校验的视频时长
|
|
|
|
|
watch(
|
|
|
|
|
() => digitalHuman.identifyState.value.identified,
|
|
|
|
|
() => identifyState.value.identified,
|
|
|
|
|
(newVal, oldVal) => {
|
|
|
|
|
if (newVal && !oldVal) {
|
|
|
|
|
// 人脸识别成功,获取视频时长
|
|
|
|
|
const videoDuration = digitalHuman.faceDuration.value
|
|
|
|
|
const videoDurationMs = faceDuration.value
|
|
|
|
|
|
|
|
|
|
// 如果已有音频,则重新校验
|
|
|
|
|
if (voiceGeneration.audioState.value.generated && voiceGeneration.audioState.value.durationMs > 0) {
|
|
|
|
|
const audioDuration = voiceGeneration.audioState.value.durationMs
|
|
|
|
|
validateMaterialDuration(videoDuration, audioDuration)
|
|
|
|
|
if (audioState.value.generated && audioState.value.durationMs > 0) {
|
|
|
|
|
const audioDurationMs = audioState.value.durationMs
|
|
|
|
|
validateMaterialDuration(videoDurationMs, audioDurationMs)
|
|
|
|
|
} else {
|
|
|
|
|
// 否则只更新视频时长
|
|
|
|
|
materialValidation.value.videoDuration = videoDuration
|
|
|
|
|
materialValidation.value.videoDuration = videoDurationMs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
@@ -81,11 +100,15 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
* 是否可以生成数字人视频(综合检查)
|
|
|
|
|
*/
|
|
|
|
|
const canGenerate = computed(() => {
|
|
|
|
|
const hasText = voiceGeneration.ttsText.value.trim()
|
|
|
|
|
const hasVoice = voiceGeneration.selectedVoiceMeta.value
|
|
|
|
|
const hasVideo = digitalHuman.videoState.value.uploadedVideo || digitalHuman.videoState.value.selectedVideo
|
|
|
|
|
const audioValidated = voiceGeneration.audioState.value.validationPassed
|
|
|
|
|
const materialValidated = materialValidation.value.isValid
|
|
|
|
|
const hasText = ttsText.value.trim()
|
|
|
|
|
const hasVoice = selectedVoiceMeta.value
|
|
|
|
|
const hasVideo = videoState.value.uploadedVideo || videoState.value.selectedVideo
|
|
|
|
|
|
|
|
|
|
// 音频校验:只有生成过音频后才需要校验通过
|
|
|
|
|
const audioValidated = !audioState.value.generated || audioState.value.validationPassed
|
|
|
|
|
// 素材校验:只有进行过校验后才需要校验通过
|
|
|
|
|
const materialValidated = materialValidation.value.videoDuration === 0 || materialValidation.value.isValid
|
|
|
|
|
|
|
|
|
|
return !!(hasText && hasVoice && hasVideo && audioValidated && materialValidated)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@@ -93,18 +116,18 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
* 最大的文本长度
|
|
|
|
|
*/
|
|
|
|
|
const maxTextLength = computed(() => {
|
|
|
|
|
if (!digitalHuman.identifyState.value.identified || digitalHuman.faceDuration.value <= 0) {
|
|
|
|
|
if (!identifyState.value.identified || faceDuration.value <= 0) {
|
|
|
|
|
return 4000
|
|
|
|
|
}
|
|
|
|
|
return Math.min(4000, Math.floor(voiceGeneration.suggestedMaxChars.value * 1.2))
|
|
|
|
|
return Math.min(4000, Math.floor(suggestedMaxChars.value * 1.2))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 文本框占位符
|
|
|
|
|
*/
|
|
|
|
|
const textareaPlaceholder = computed(() => {
|
|
|
|
|
if (digitalHuman.identifyState.value.identified && digitalHuman.faceDuration.value > 0) {
|
|
|
|
|
return `请输入文案,建议不超过${voiceGeneration.suggestedMaxChars.value}字以确保与视频匹配`
|
|
|
|
|
if (identifyState.value.identified && faceDuration.value > 0) {
|
|
|
|
|
return `请输入文案,建议不超过${suggestedMaxChars.value}字以确保与视频匹配`
|
|
|
|
|
}
|
|
|
|
|
return '请输入你想让角色说话的内容'
|
|
|
|
|
})
|
|
|
|
|
@@ -117,7 +140,7 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
/**
|
|
|
|
|
* 语速显示
|
|
|
|
|
*/
|
|
|
|
|
const speechRateDisplay = computed(() => `${voiceGeneration.speechRate.value.toFixed(1)}x`)
|
|
|
|
|
const speechRateDisplay = computed(() => `${speechRate.value.toFixed(1)}x`)
|
|
|
|
|
|
|
|
|
|
// ==================== 业务流程方法 ====================
|
|
|
|
|
|
|
|
|
|
@@ -130,13 +153,13 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const text = voiceGeneration.ttsText.value.trim()
|
|
|
|
|
const text = ttsText.value.trim()
|
|
|
|
|
if (!text) {
|
|
|
|
|
message.warning('请输入文案内容')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const voice = voiceGeneration.selectedVoiceMeta.value
|
|
|
|
|
const voice = selectedVoiceMeta.value
|
|
|
|
|
if (!voice) {
|
|
|
|
|
message.warning('请选择音色')
|
|
|
|
|
return
|
|
|
|
|
@@ -144,9 +167,9 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 如果未识别,先进行人脸识别
|
|
|
|
|
if (!digitalHuman.identifyState.value.identified) {
|
|
|
|
|
const hasUploadFile = digitalHuman.videoState.value.videoFile
|
|
|
|
|
const hasSelectedVideo = digitalHuman.videoState.value.selectedVideo
|
|
|
|
|
if (!identifyState.value.identified) {
|
|
|
|
|
const hasUploadFile = videoState.value.videoFile
|
|
|
|
|
const hasSelectedVideo = videoState.value.selectedVideo
|
|
|
|
|
|
|
|
|
|
if (!hasUploadFile && !hasSelectedVideo) {
|
|
|
|
|
message.warning('请先选择或上传视频')
|
|
|
|
|
@@ -154,27 +177,27 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await digitalHuman.performFaceRecognition()
|
|
|
|
|
await performFaceRecognition()
|
|
|
|
|
message.success('人脸识别完成')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const videoFileId = digitalHuman.identifyState.value.videoFileId
|
|
|
|
|
const videoFileId = identifyState.value.videoFileId
|
|
|
|
|
|
|
|
|
|
const taskData: LipSyncTaskData = {
|
|
|
|
|
taskName: `数字人任务_${Date.now()}`,
|
|
|
|
|
videoFileId: videoFileId!,
|
|
|
|
|
inputText: voiceGeneration.ttsText.value,
|
|
|
|
|
speechRate: voiceGeneration.speechRate.value,
|
|
|
|
|
inputText: ttsText.value,
|
|
|
|
|
speechRate: speechRate.value,
|
|
|
|
|
volume: 0,
|
|
|
|
|
guidanceScale: 1,
|
|
|
|
|
seed: 8888,
|
|
|
|
|
kling_session_id: digitalHuman.identifyState.value.sessionId,
|
|
|
|
|
kling_face_id: digitalHuman.identifyState.value.faceId,
|
|
|
|
|
kling_face_start_time: digitalHuman.identifyState.value.faceStartTime,
|
|
|
|
|
kling_face_end_time: digitalHuman.identifyState.value.faceEndTime,
|
|
|
|
|
kling_session_id: identifyState.value.sessionId,
|
|
|
|
|
kling_face_id: identifyState.value.faceId,
|
|
|
|
|
kling_face_start_time: identifyState.value.faceStartTime,
|
|
|
|
|
kling_face_end_time: identifyState.value.faceEndTime,
|
|
|
|
|
ai_provider: 'kling',
|
|
|
|
|
voiceConfigId: voice.rawId || extractIdFromString(voice.id),
|
|
|
|
|
}
|
|
|
|
|
@@ -185,13 +208,13 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果有预生成的音频,添加到任务数据中
|
|
|
|
|
if (voiceGeneration.audioState.value.generated && voiceGeneration.audioState.value.durationMs > 0) {
|
|
|
|
|
if (audioState.value.generated && audioState.value.durationMs > 0) {
|
|
|
|
|
taskData.pre_generated_audio = {
|
|
|
|
|
audioBase64: voiceGeneration.audioState.value.generated.audioBase64,
|
|
|
|
|
format: voiceGeneration.audioState.value.generated.format || 'mp3',
|
|
|
|
|
audioBase64: audioState.value.generated.audioBase64,
|
|
|
|
|
format: audioState.value.generated.format || 'mp3',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
taskData.sound_end_time = voiceGeneration.audioState.value.durationMs
|
|
|
|
|
taskData.sound_end_time = audioState.value.durationMs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const res = await createLipSyncTask(taskData)
|
|
|
|
|
@@ -210,25 +233,25 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
* 更换视频
|
|
|
|
|
*/
|
|
|
|
|
const replaceVideo = (): void => {
|
|
|
|
|
if (digitalHuman.videoState.value.videoSource === 'upload') {
|
|
|
|
|
digitalHuman.videoState.value.videoFile = null
|
|
|
|
|
digitalHuman.videoState.value.uploadedVideo = ''
|
|
|
|
|
if (videoState.value.videoSource === 'upload') {
|
|
|
|
|
videoState.value.videoFile = null
|
|
|
|
|
videoState.value.uploadedVideo = ''
|
|
|
|
|
} else {
|
|
|
|
|
digitalHuman.videoState.value.selectedVideo = null
|
|
|
|
|
digitalHuman.videoState.value.videoFile = null
|
|
|
|
|
digitalHuman.videoState.value.uploadedVideo = ''
|
|
|
|
|
videoState.value.selectedVideo = null
|
|
|
|
|
videoState.value.videoFile = null
|
|
|
|
|
videoState.value.uploadedVideo = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置所有状态
|
|
|
|
|
digitalHuman.resetVideoState()
|
|
|
|
|
voiceGeneration.resetAudioState()
|
|
|
|
|
resetVideoState()
|
|
|
|
|
resetAudioState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理音色选择
|
|
|
|
|
*/
|
|
|
|
|
const handleVoiceSelect = (voice: any): void => {
|
|
|
|
|
voiceGeneration.selectedVoiceMeta.value = voice
|
|
|
|
|
selectedVoiceMeta.value = voice
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -238,7 +261,7 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
const input = event.target as HTMLInputElement
|
|
|
|
|
const file = input.files?.[0]
|
|
|
|
|
if (file) {
|
|
|
|
|
digitalHuman.handleFileUpload(file)
|
|
|
|
|
handleFileUpload(file)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -249,7 +272,7 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
const file = event.dataTransfer?.files[0]
|
|
|
|
|
if (file) {
|
|
|
|
|
digitalHuman.handleFileUpload(file)
|
|
|
|
|
handleFileUpload(file)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -265,26 +288,26 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
* 选择上传模式
|
|
|
|
|
*/
|
|
|
|
|
const handleSelectUpload = (): void => {
|
|
|
|
|
digitalHuman.videoState.value.videoSource = 'upload'
|
|
|
|
|
digitalHuman.videoState.value.selectedVideo = null
|
|
|
|
|
digitalHuman.resetIdentifyState()
|
|
|
|
|
videoState.value.videoSource = 'upload'
|
|
|
|
|
videoState.value.selectedVideo = null
|
|
|
|
|
resetIdentifyState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从素材库选择
|
|
|
|
|
*/
|
|
|
|
|
const handleSelectFromLibrary = (): void => {
|
|
|
|
|
digitalHuman.videoState.value.videoSource = 'select'
|
|
|
|
|
digitalHuman.videoState.value.videoFile = null
|
|
|
|
|
digitalHuman.videoState.value.uploadedVideo = ''
|
|
|
|
|
digitalHuman.videoState.value.selectorVisible = true
|
|
|
|
|
videoState.value.videoSource = 'select'
|
|
|
|
|
videoState.value.videoFile = null
|
|
|
|
|
videoState.value.uploadedVideo = ''
|
|
|
|
|
videoState.value.selectorVisible = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理视频选择器选择
|
|
|
|
|
*/
|
|
|
|
|
const handleVideoSelect = (video: any): void => {
|
|
|
|
|
digitalHuman.handleVideoSelect(video)
|
|
|
|
|
_handleVideoSelect(video)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -302,7 +325,7 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
* 处理视频加载
|
|
|
|
|
*/
|
|
|
|
|
const handleVideoLoaded = (videoUrl: string): void => {
|
|
|
|
|
digitalHuman.videoState.value.previewVideoUrl = videoUrl
|
|
|
|
|
videoState.value.previewVideoUrl = videoUrl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== UI 辅助方法 ====================
|
|
|
|
|
@@ -359,32 +382,6 @@ export function useIdentifyFaceController(): UseIdentifyFaceController {
|
|
|
|
|
return materialValidation.value.isValid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== 解构子 Hooks 的响应式变量 ====================
|
|
|
|
|
|
|
|
|
|
// 语音生成相关
|
|
|
|
|
const {
|
|
|
|
|
ttsText,
|
|
|
|
|
speechRate,
|
|
|
|
|
selectedVoiceMeta,
|
|
|
|
|
audioState,
|
|
|
|
|
canGenerateAudio,
|
|
|
|
|
suggestedMaxChars,
|
|
|
|
|
generateAudio,
|
|
|
|
|
resetAudioState,
|
|
|
|
|
} = voiceGeneration
|
|
|
|
|
|
|
|
|
|
// 数字人生成相关
|
|
|
|
|
const {
|
|
|
|
|
videoState,
|
|
|
|
|
identifyState,
|
|
|
|
|
faceDuration,
|
|
|
|
|
performFaceRecognition,
|
|
|
|
|
handleFileUpload,
|
|
|
|
|
getVideoPreviewUrl,
|
|
|
|
|
resetVideoState,
|
|
|
|
|
resetIdentifyState,
|
|
|
|
|
} = digitalHuman
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// ==================== 语音生成相关 ====================
|
|
|
|
|
ttsText,
|
|
|
|
|
|