feat: 重构 IdentifyFace.vue 为 Hooks 架构

- 新增 hooks/ 目录,包含三个专用 Hook:
  * useVoiceGeneration - 语音生成和校验逻辑
  * useDigitalHumanGeneration - 数字人视频生成逻辑
  * useIdentifyFaceController - 协调两个子 Hook 的控制器

- 新增 types/identify-face.ts 完整类型定义

- 重构 IdentifyFace.vue 使用 hooks 架构:
  * 视图层与业务逻辑分离
  * 状态管理清晰化
  * 模块解耦,逻辑清晰

- 遵循单一职责原则,每个 Hook 只负责一个领域
- 提升代码可测试性和可维护性
- 支持两种视频素材来源:素材库选择和直接上传
- 实现语音生成优先校验的业务规则

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-28 00:19:17 +08:00
parent effbbc694c
commit 36195ea55a
46 changed files with 4258 additions and 3454 deletions

View File

@@ -6,8 +6,15 @@ import { message } from "ant-design-vue"
import { MaterialService } from './material'
/**
* 人脸识别
* 显示加载提示
*/
const showLoading = (text) => message.loading(text, 0)
/**
* 销毁加载提示
*/
const hideLoading = () => message.destroy()
export function identifyFace(data) {
return request({
url: '/webApi/api/tik/kling/identify-face',
@@ -16,9 +23,6 @@ export function identifyFace(data) {
})
}
/**
* 创建口型同步任务
*/
export function createLipSyncTask(data) {
return request({
url: '/webApi/api/tik/kling/task/create',
@@ -27,9 +31,6 @@ export function createLipSyncTask(data) {
})
}
/**
* 查询口型同步任务
*/
export function getLipSyncTask(taskId) {
return request({
url: `/webApi/api/tik/kling/lip-sync/${taskId}`,
@@ -37,13 +38,36 @@ export function getLipSyncTask(taskId) {
})
}
/**
* 创建可灵任务并识别(推荐方式)
*/
export async function createKlingTaskAndIdentify(file) {
export async function identifyUploadedVideo(videoFile) {
try {
// 1. 提取视频封面
message.loading('正在提取视频封面...', 0)
showLoading('正在识别视频中的人脸...')
const identifyRes = await identifyFace({ video_url: videoFile.fileUrl })
hideLoading()
if (identifyRes.code !== 0) {
throw new Error(identifyRes.msg || '识别失败')
}
return {
success: true,
data: {
fileId: videoFile.id,
videoUrl: videoFile.fileUrl,
sessionId: identifyRes.data.sessionId,
faceId: identifyRes.data.data.face_data[0].face_id || null,
startTime: identifyRes.data.data.face_data[0].start_time || 0,
endTime: identifyRes.data.data.face_data[0].end_time || 0
}
}
} catch (error) {
hideLoading()
throw error
}
}
export async function uploadAndIdentifyVideo(file) {
try {
showLoading('正在提取视频封面...')
let coverBase64 = null
try {
const { extractVideoCover } = await import('@/utils/video-cover')
@@ -52,46 +76,39 @@ export async function createKlingTaskAndIdentify(file) {
quality: 0.8
})
coverBase64 = cover.base64
console.log('视频封面提取成功')
} catch (coverError) {
console.warn('视频封面提取失败:', coverError)
// 封面提取失败不影响主流程
}
message.destroy()
hideLoading()
// 2. 上传视频到OSS包含封面
message.loading('正在上传视频...', 0)
showLoading('正在上传视频...')
const uploadRes = await MaterialService.uploadFile(file, 'video', coverBase64)
message.destroy()
hideLoading()
if (uploadRes.code !== 0) {
throw new Error(uploadRes.msg || '上传失败')
}
const fileId = uploadRes.data
console.log('文件上传成功ID:', fileId, '封面长度:', coverBase64?.length || 0)
// 3. 获取公网播放URL
message.loading('正在生成播放链接...', 0)
showLoading('正在生成播放链接...')
const urlRes = await MaterialService.getVideoPlayUrl(fileId)
message.destroy()
hideLoading()
if (urlRes.code !== 0) {
throw new Error(urlRes.msg || '获取播放链接失败')
}
const videoUrl = urlRes.data
console.log('视频URL:', videoUrl)
// 4. 调用识别API
message.loading('正在识别视频中的人脸...', 0)
const videoUrl = urlRes.data
showLoading('正在识别视频中的人脸...')
const identifyRes = await identifyFace({ video_url: videoUrl })
message.destroy()
hideLoading()
if (identifyRes.code !== 0) {
throw new Error(identifyRes.msg || '识别失败')
}
return {
success: true,
data: {
@@ -99,14 +116,12 @@ export async function createKlingTaskAndIdentify(file) {
videoUrl,
sessionId: identifyRes.data.sessionId,
faceId: identifyRes.data.data.face_data[0].face_id || null,
// 人脸时间信息,用于音频插入时间
startTime: identifyRes.data.data.face_data[0].start_time || 0,
endTime: identifyRes.data.data.face_data[0].end_time || 0
}
}
} catch (error) {
message.destroy()
console.error('可灵任务失败:', error)
hideLoading()
throw error
}
}