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

@@ -1,6 +1,5 @@
/**
* @fileoverview useDigitalHumanGeneration Hook - 数字人生成逻辑封装
* @author Claude Code
* @fileoverview useDigitalHumanGeneration Hook - 数字人生成逻辑
*/
import { ref, computed } from 'vue'
@@ -14,18 +13,14 @@ import type {
import { identifyUploadedVideo } from '@/api/kling'
import { useUpload } from '@/composables/useUpload'
/**
* 数字人生成 Hook
* 独立管理所有状态,不依赖外部状态
*/
export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
// ==================== 响应式状态 ====================
// ========== 状态 ==========
const videoState = ref<VideoState>({
uploadedVideo: '',
videoFile: null,
previewVideoUrl: '',
selectedVideo: null,
fileId: null,
videoSource: null,
selectorVisible: false,
})
@@ -40,24 +35,16 @@ export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
videoFileId: null,
})
// ==================== Upload Hook ====================
const { upload } = useUpload()
// ==================== 计算属性 ====================
/**
* 人脸出现时长
*/
const faceDuration = computed(() => {
// ========== 计算属性 ==========
const faceDuration = computed(function() {
return identifyState.value.faceEndTime - identifyState.value.faceStartTime
})
// ==================== 核心方法 ====================
// ========== 方法 ==========
/**
* 处理视频文件上传
*/
const handleFileUpload = async (file: File): Promise<void> => {
async function handleFileUpload(file: File): Promise<void> {
if (!file.name.match(/\.(mp4|mov)$/i)) {
message.error('仅支持 MP4 和 MOV')
return
@@ -68,148 +55,101 @@ export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
videoState.value.selectedVideo = null
videoState.value.previewVideoUrl = ''
videoState.value.videoSource = 'upload'
resetIdentifyState()
await performFaceRecognition()
}
/**
* 处理从素材库选择视频
*/
const handleVideoSelect = (video: Video): void => {
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
resetIdentifyState()
identifyState.value.videoFileId = video.id
identifyState.value.videoFileId = video.fileId
}
/**
* 执行人脸识别
*/
const performFaceRecognition = async (): Promise<void> => {
async function performFaceRecognition(): Promise<void> {
const hasUploadFile = videoState.value.videoFile
const hasSelectedVideo = videoState.value.selectedVideo
if (!hasUploadFile && !hasSelectedVideo) {
return
}
if (!hasUploadFile && !hasSelectedVideo) return
identifyState.value.identifying = true
try {
let res
if (hasSelectedVideo) {
res = await identifyUploadedVideo(hasSelectedVideo)
identifyState.value.videoFileId = hasSelectedVideo.id
const res = await identifyUploadedVideo(hasSelectedVideo) as { success: boolean; data: { sessionId: string; faceId: string | null; startTime: number; endTime: number } }
identifyState.value.videoFileId = hasSelectedVideo.fileId
identifyState.value.sessionId = res.data.sessionId
identifyState.value.faceId = res.data.faceId || ''
identifyState.value.faceStartTime = res.data.startTime || 0
identifyState.value.faceEndTime = res.data.endTime || 0
} else {
// 处理文件上传(提取封面)
const file = hasUploadFile!
let coverBase64 = null
try {
const { extractVideoCover } = await import('@/utils/video-cover')
const cover = await extractVideoCover(file, {
maxWidth: 800,
quality: 0.8
})
const cover = await extractVideoCover(file, { maxWidth: 800, quality: 0.8 })
coverBase64 = cover.base64
} catch {
// 封面提取失败不影响主流程
}
// 使用useUpload Hook上传文件
const fileId = await upload(file, {
fileCategory: 'video',
groupId: null, // 数字人模块不使用groupId
groupId: null,
coverBase64,
onStart: () => {},
onProgress: () => {},
onSuccess: () => {
message.success('文件上传成功')
},
onError: (err: Error) => {
onStart: function() {},
onProgress: function() {},
onSuccess: function() {},
onError: function(err: Error) {
message.error(err.message || '上传失败')
}
})
// 生成播放链接
// TODO: 获取播放链接逻辑
res = {
success: true,
data: {
fileId,
videoUrl: '', // TODO: 需要获取实际URL
sessionId: '', // TODO: 需要实际识别
faceId: null,
startTime: 0,
endTime: 0
}
}
identifyState.value.videoFileId = fileId
identifyState.value.sessionId = ''
identifyState.value.faceId = ''
identifyState.value.faceStartTime = 0
identifyState.value.faceEndTime = 0
}
identifyState.value.sessionId = res.data.sessionId
identifyState.value.faceId = res.data.faceId
identifyState.value.faceStartTime = res.data.startTime || 0
identifyState.value.faceEndTime = res.data.endTime || 0
identifyState.value.identified = true
const durationSec = faceDuration.value / 1000
const suggestedMaxChars = Math.floor(durationSec * 3.5)
message.success(`识别完成!人脸出现时长约 ${durationSec.toFixed(1)} 秒,建议文案不超过 ${suggestedMaxChars}`)
} catch (error: any) {
message.error(error.message || '识别失败')
// 识别完成,不显示提示信息
} catch (error: unknown) {
const err = error as Error
message.error(err.message || '识别失败')
throw error
} finally {
identifyState.value.identifying = false
}
}
/**
* 重置视频状态
*/
const resetVideoState = (): void => {
function resetVideoState(): void {
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
resetIdentifyState()
}
/**
* 获取视频预览 URL
*/
const getVideoPreviewUrl = (video: Video): string => {
function getVideoPreviewUrl(video: Video): string {
if (video.coverBase64) {
if (!video.coverBase64.startsWith('data:')) {
return `data:image/jpeg;base64,${video.coverBase64}`
}
return 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
}
if (video.previewUrl) return video.previewUrl
if (video.coverUrl) return video.coverUrl
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjExMCIgdmlld0JveD0iMCAwIDIwMCAxMTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTEwIiBmaWxsPSIjMzc0MTUxIi8+CjxwYXRoIGQ9Ik04NSA0NUwxMTUgNjVMMTA1IDg1TDc1IDc1TDg1IDQ1WiIgZmlsbD0iIzU3MjY1MSIvPgo8L3N2Zz4K'
}
/**
* 重置识别状态
*/
const resetIdentifyState = (): void => {
function resetIdentifyState(): void {
identifyState.value.identified = false
identifyState.value.sessionId = ''
identifyState.value.faceId = ''
@@ -217,14 +157,9 @@ export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
}
return {
// 响应式状态
videoState,
identifyState,
// 计算属性
faceDuration,
// 方法
handleFileUpload,
handleVideoSelect,
performFaceRecognition,