172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
/**
|
|
* @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<VideoState>({
|
|
uploadedVideo: '',
|
|
videoFile: null,
|
|
previewVideoUrl: '',
|
|
selectedVideo: null,
|
|
fileId: null,
|
|
videoSource: null,
|
|
selectorVisible: false,
|
|
})
|
|
|
|
// 识别结果数据(不含状态标志)
|
|
const identifyResult = ref<IdentifyResult>({
|
|
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<void> {
|
|
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<void> {
|
|
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<IdentifyResult> {
|
|
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,
|
|
}
|
|
}
|