/** * @fileoverview useDigitalHumanGeneration Hook - 数字人生成逻辑 * * 重构后:不管理识别状态,只提供数据和操作方法 * 状态由 Pipeline 统一管理 */ import { ref, computed } from 'vue' import { message } from 'ant-design-vue' import type { VideoState, IdentifyResult, Video, } from '../types/identify-face' import { identifyUploadedVideo, uploadAndIdentifyVideo } from '@/api/kling' import { useUpload } from '@/composables/useUpload' export function useDigitalHumanGeneration() { // ========== 状态 ========== const videoState = ref({ uploadedVideo: '', videoFile: null, previewVideoUrl: '', selectedVideo: null, fileId: null, videoSource: null, selectorVisible: false, }) // 识别结果数据(不含状态标志) const identifyResult = ref({ sessionId: '', faceId: '', faceStartTime: 0, faceEndTime: 0, videoFileId: null, }) const { upload } = useUpload() // ========== 计算属性 ========== const faceDuration = computed(function() { return identifyResult.value.faceEndTime - identifyResult.value.faceStartTime }) const hasVideo = computed(function() { return !!videoState.value.uploadedVideo || !!videoState.value.selectedVideo }) const isIdentified = computed(function() { return !!identifyResult.value.sessionId }) // ========== 方法 ========== async function handleFileUpload(file: File): Promise { if (!file.name.match(/\.(mp4|mov)$/i)) { message.error('仅支持 MP4 和 MOV') return } // 释放旧的 blob URL if (videoState.value.uploadedVideo && videoState.value.uploadedVideo.startsWith('blob:')) { URL.revokeObjectURL(videoState.value.uploadedVideo) } videoState.value.videoFile = file videoState.value.uploadedVideo = URL.createObjectURL(file) videoState.value.selectedVideo = null videoState.value.previewVideoUrl = '' videoState.value.videoSource = 'upload' resetIdentifyResult() } async function handleVideoSelect(video: Video): Promise { videoState.value.selectedVideo = video videoState.value.uploadedVideo = video.fileUrl videoState.value.videoFile = null videoState.value.videoSource = 'select' videoState.value.selectorVisible = false resetIdentifyResult() identifyResult.value.videoFileId = video.fileId } /** * 执行人脸识别 * 返回识别结果供 Pipeline 使用 */ async function performFaceRecognition(): Promise { const hasUploadFile = videoState.value.videoFile const hasSelectedVideo = videoState.value.selectedVideo if (!hasUploadFile && !hasSelectedVideo) { throw new Error('请先选择视频') } if (hasSelectedVideo) { // 从素材库选择:调用识别接口 const res = await identifyUploadedVideo(hasSelectedVideo) as { success: boolean; data: { sessionId: string; faceId: string | null; startTime: number; endTime: number } } identifyResult.value.videoFileId = hasSelectedVideo.fileId identifyResult.value.sessionId = res.data.sessionId identifyResult.value.faceId = res.data.faceId || '' identifyResult.value.faceStartTime = res.data.startTime || 0 identifyResult.value.faceEndTime = res.data.endTime || 0 } else { // 上传新视频:使用 uploadAndIdentifyVideo 完成上传+识别 const file = hasUploadFile! const res = await uploadAndIdentifyVideo(file) as { success: boolean; data: { fileId: string; sessionId: string; faceId: string | null; startTime: number; endTime: number } } identifyResult.value.videoFileId = res.data.fileId identifyResult.value.sessionId = res.data.sessionId identifyResult.value.faceId = res.data.faceId || '' identifyResult.value.faceStartTime = res.data.startTime || 0 identifyResult.value.faceEndTime = res.data.endTime || 0 } return { ...identifyResult.value } } function resetVideoState(): void { // 释放 blob URL 避免内存泄漏 if (videoState.value.uploadedVideo && videoState.value.uploadedVideo.startsWith('blob:')) { URL.revokeObjectURL(videoState.value.uploadedVideo) } videoState.value.uploadedVideo = '' videoState.value.videoFile = null videoState.value.selectedVideo = null videoState.value.fileId = null videoState.value.videoSource = null videoState.value.previewVideoUrl = '' videoState.value.selectorVisible = false resetIdentifyResult() } function getVideoPreviewUrl(video: Video): string { if (video.coverBase64) { return video.coverBase64.startsWith('data:') ? video.coverBase64 : `data:image/jpeg;base64,${video.coverBase64}` } if (video.previewUrl) return video.previewUrl if (video.coverUrl) return video.coverUrl return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjExMCIgdmlld0JveD0iMCAwIDIwMCAxMTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTEwIiBmaWxsPSIjMzc0MTUxIi8+CjxwYXRoIGQ9Ik04NSA0NUwxMTUgNjVMMTA1IDg1TDc1IDc1TDg1IDQ1WiIgZmlsbD0iIzU3MjY1MSIvPgo8L3N2Zz4K' } function resetIdentifyResult(): void { identifyResult.value.sessionId = '' identifyResult.value.faceId = '' identifyResult.value.videoFileId = null } return { videoState, identifyResult, hasVideo, isIdentified, faceDuration, handleFileUpload, handleVideoSelect, performFaceRecognition, resetVideoState, resetIdentifyResult, getVideoPreviewUrl, } }