diff --git a/CLAUDE.md b/CLAUDE.md
index d27ddc69a1..e46da1ddf7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,6 +1,6 @@
# CLAUDE.md
-本文档为 Claude Code (claude.ai/code) 在此仓库中处理代码提供指导。
+本文档为 Claude Code (claude.ai/code) 在此仓库中处理代码提供指导。请始终用中文沟通
## 项目概览
@@ -50,24 +50,6 @@
└── docs/ # 文档
```
-## 常用开发命令
-
-### 后端(Maven)
-
-**构建和运行:**
-```bash
-# 构建项目
-mvn clean package -DskipTests
-
-# 运行特定模块的测试
-mvn test -pl yudao-module-tik
-
-# 启动服务器
-cd yudao-server && mvn spring-boot:run -Dspring-boot.run.profiles=local
-
-# 使用特定配置构建
-mvn clean package -Pdev -DskipTests
-```
**代码生成:**
- 内置 CRUD 操作代码生成器
@@ -81,51 +63,19 @@ mvn clean package -Pdev -DskipTests
cd frontend/app/web-gold
# 安装依赖
-npm install
+pnpm install
# 启动开发服务器(代理到后端 9900 端口)
-npm run dev
+pnpm run dev
# 生产构建
-npm run build
+pnpm run build
# 代码检查
-npm run lint
+pnpm run lint
# 代码格式化
-npm run format
-```
-
-**可用脚本:**
-- `dev` - 带热重载的开发服务器
-- `build` - 生产构建
-- `preview` - 预览生产构建
-- `lint:oxlint` - 运行 OxLint 并自动修复
-- `lint:eslint` - 运行 ESLint 并自动修复
-- `lint` - 运行所有检查器
-- `format` - 使用 Prettier 格式化代码
-
-### Docker
-
-**使用 Docker Compose:**
-```bash
-# 启动所有服务(MySQL、Redis、Server、Admin)
-cd script/docker
-docker-compose up -d
-
-# 启动特定服务
-docker-compose up -d mysql redis
-```
-
-**手动 Docker 构建:**
-```bash
-# 后端
-cd yudao-server
-docker build -t yudao-server .
-
-# 前端
-cd frontend/app/web-gold
-docker build -t web-gold .
+pnpm run format
```
## 模块架构
@@ -313,26 +263,6 @@ frontend/app/web-gold/src/
- Cypress 进行端到端测试
- 运行测试:`npm run test`
-## 部署
-
-**生产部署:**
-```bash
-# 使用部署脚本
-cd script/shell
-./deploy.sh
-
-# 手动部署
-# 1. 构建 JAR
-mvn clean package -DskipTests -Pprod
-
-# 2. 部署到服务器
-# deploy.sh 脚本处理:
-# - 备份前一版本
-# - 停止当前服务
-# - 传输新 JAR
-# - 启动服务
-# - 健康检查
-```
**JVM 选项:**
- 默认:`-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError`
diff --git a/frontend/app/web-gold/src/api/digitalHuman.js b/frontend/app/web-gold/src/api/digitalHuman.js
new file mode 100644
index 0000000000..ec9d5f8159
--- /dev/null
+++ b/frontend/app/web-gold/src/api/digitalHuman.js
@@ -0,0 +1,77 @@
+/**
+ * 数字人任务 API
+ */
+import request from './http'
+
+/**
+ * 创建数字人任务
+ */
+export function createDigitalHumanTask(data) {
+ return request({
+ url: '/api/tik/digital-human/task/create',
+ method: 'post',
+ data
+ })
+}
+
+/**
+ * 查询任务详情
+ */
+export function getDigitalHumanTask(taskId) {
+ return request({
+ url: '/api/tik/digital-human/task/get',
+ method: 'get',
+ params: { taskId }
+ })
+}
+
+/**
+ * 分页查询任务列表
+ */
+export function getDigitalHumanTaskPage(params) {
+ return request({
+ url: '/api/tik/digital-human/task/page',
+ method: 'get',
+ params
+ })
+}
+
+/**
+ * 查询任务统计
+ */
+export function getTaskStatistics() {
+ return request({
+ url: '/api/tik/digital-human/task/statistics',
+ method: 'get'
+ })
+}
+
+/**
+ * 取消任务
+ */
+export function cancelTask(taskId) {
+ return request({
+ url: `/api/tik/digital-human/task/${taskId}/cancel`,
+ method: 'post'
+ })
+}
+
+/**
+ * 重试任务
+ */
+export function retryTask(taskId) {
+ return request({
+ url: `/api/tik/digital-human/task/${taskId}/retry`,
+ method: 'post'
+ })
+}
+
+/**
+ * 删除任务
+ */
+export function deleteTask(taskId) {
+ return request({
+ url: `/api/tik/digital-human/task/${taskId}`,
+ method: 'delete'
+ })
+}
diff --git a/frontend/app/web-gold/src/views/dh/Video.vue b/frontend/app/web-gold/src/views/dh/Video.vue
index ae2e56b836..de39837c04 100644
--- a/frontend/app/web-gold/src/views/dh/Video.vue
+++ b/frontend/app/web-gold/src/views/dh/Video.vue
@@ -5,19 +5,26 @@ import { message } from 'ant-design-vue'
import { InboxOutlined } from '@ant-design/icons-vue'
import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { VoiceService } from '@/api/voice'
+import { MaterialService } from '@/api/material'
+import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
const voiceStore = useVoiceCopyStore()
// 状态管理
const uploadedVideo = ref('')
+const uploadedVideoFile = ref(null) // 存储原始文件对象
const previewVideoUrl = ref('')
const isGenerating = ref(false)
const generationProgress = ref(0)
+const currentTaskId = ref(null)
+const currentTaskStatus = ref('')
+const currentTaskStep = ref('')
const isSynthesizing = ref(false)
const synthesizedAudio = ref(null)
const previewLoadingVoiceId = ref('')
const isPlayingPreview = ref(false) // 是否正在播放试听音频
const isPlayingSynthesized = ref(false) // 是否正在播放已合成的音频
+const pollingInterval = ref(null) // 轮询间隔ID
// TTS 配置
const ttsText = ref('')
@@ -245,6 +252,7 @@ const handleVideoUpload = async (file) => {
try {
uploadedVideo.value = await toDataURL(file)
+ uploadedVideoFile.value = file // 保存文件对象
message.success('视频上传成功')
} catch (error) {
message.error('视频上传失败')
@@ -255,6 +263,7 @@ const handleVideoUpload = async (file) => {
const handleVideoDrop = (e) => console.log('Video drop:', e)
const clearVideo = () => {
uploadedVideo.value = ''
+ uploadedVideoFile.value = null
previewVideoUrl.value = ''
message.info('已清除视频')
}
@@ -270,26 +279,176 @@ const downloadPreview = () => {
// 视频生成
const generateVideo = async () => {
if (!canGenerate.value) return message.warning('请先完成配置')
+ if (!ttsText.value.trim()) return message.warning('请输入文本')
+ const voice = selectedVoiceMeta.value
+ if (!voice) return message.warning('请选择音色')
isGenerating.value = true
generationProgress.value = 0
+ currentTaskStatus.value = 'PENDING'
+ currentTaskStep.value = 'prepare_files'
try {
- // 进度模拟
- const progressInterval = setInterval(() => {
- generationProgress.value += 10
- generationProgress.value >= 100 && clearInterval(progressInterval)
- }, 500)
+ // 1. 首先上传音频和视频文件到后端
+ message.loading('正在上传文件...', 0)
+
+ // 上传音频(使用合成后的音频或原始音频)
+ let audioFileId = null
+ let audioUrl = null
+
+ if (synthesizedAudio.value?.fileId) {
+ // 如果有已合成的音频,使用其fileId
+ audioFileId = synthesizedAudio.value.fileId
+ } else {
+ // 否则使用voiceConfigId让后端处理
+ audioFileId = voice.rawId || extractIdFromString(voice.id)
+ }
+
+ // 上传视频文件
+ const videoFileId = await uploadVideoFile(uploadedVideoFile.value)
+ if (!videoFileId) {
+ throw new Error('视频上传失败')
+ }
- await new Promise(resolve => setTimeout(resolve, 5000))
- previewVideoUrl.value = uploadedVideo.value
- generationProgress.value = 100
- message.success('视频生成成功')
- clearInterval(progressInterval)
+ message.destroy()
+
+ // 2. 创建数字人任务
+ const taskData = {
+ taskName: `数字人任务_${Date.now()}`,
+ audioFileId: audioFileId,
+ videoFileId: videoFileId,
+ speechRate: speechRate.value,
+ emotion: emotion.value,
+ guidanceScale: 1,
+ seed: 8888
+ }
+
+ message.loading('正在创建任务...', 0)
+ const createRes = await createDigitalHumanTask(taskData)
+ message.destroy()
+
+ if (createRes.code === 0) {
+ currentTaskId.value = createRes.data
+ message.success('任务创建成功,开始处理')
+
+ // 3. 开始轮询任务状态
+ startPollingTask()
+ } else {
+ throw new Error(createRes.msg || '任务创建失败')
+ }
} catch (error) {
- message.error('视频生成失败')
- } finally {
+ console.error('generateVideo error:', error)
+ message.destroy()
+ message.error(error.message || '视频生成失败')
isGenerating.value = false
+ currentTaskStatus.value = ''
+ currentTaskStep.value = ''
+ }
+}
+
+// 上传视频文件到后端
+const uploadVideoFile = async (file) => {
+ try {
+ const res = await MaterialService.uploadFile(file, 'video')
+ if (res.code === 0 && res.data?.id) {
+ return res.data.id
+ } else {
+ throw new Error(res.msg || '上传失败')
+ }
+ } catch (error) {
+ console.error('uploadVideoFile error:', error)
+ throw error
+ }
+}
+
+// 开始轮询任务状态
+const startPollingTask = () => {
+ if (pollingInterval.value) {
+ clearInterval(pollingInterval.value)
+ }
+
+ pollingInterval.value = setInterval(async () => {
+ if (!currentTaskId.value) {
+ clearInterval(pollingInterval.value)
+ return
+ }
+
+ try {
+ const res = await getDigitalHumanTask(currentTaskId.value)
+ if (res.code === 0 && res.data) {
+ const task = res.data
+ currentTaskStatus.value = task.status
+ currentTaskStep.value = task.currentStep
+ generationProgress.value = task.progress || 0
+
+ // 如果任务完成或失败,停止轮询
+ if (task.status === 'SUCCESS') {
+ clearInterval(pollingInterval.value)
+ pollingInterval.value = null
+ previewVideoUrl.value = task.resultVideoUrl
+ isGenerating.value = false
+ currentTaskStatus.value = 'SUCCESS'
+ message.success('视频生成成功!')
+ } else if (task.status === 'FAILED') {
+ clearInterval(pollingInterval.value)
+ pollingInterval.value = null
+ isGenerating.value = false
+ currentTaskStatus.value = 'FAILED'
+ message.error(`任务失败:${task.errorMessage || '未知错误'}`)
+ } else if (task.status === 'CANCELED') {
+ clearInterval(pollingInterval.value)
+ pollingInterval.value = null
+ isGenerating.value = false
+ currentTaskStatus.value = 'CANCELED'
+ message.info('任务已取消')
+ }
+ }
+ } catch (error) {
+ console.error('polling error:', error)
+ }
+ }, 2000) // 每2秒轮询一次
+}
+
+// 取消任务
+const handleCancelTask = async () => {
+ if (!currentTaskId.value) return
+
+ try {
+ const res = await cancelTask(currentTaskId.value)
+ if (res.code === 0) {
+ message.success('任务已取消')
+ if (pollingInterval.value) {
+ clearInterval(pollingInterval.value)
+ pollingInterval.value = null
+ }
+ isGenerating.value = false
+ } else {
+ message.error(res.msg || '取消失败')
+ }
+ } catch (error) {
+ console.error('cancelTask error:', error)
+ message.error('取消任务失败')
+ }
+}
+
+// 重试任务
+const handleRetryTask = async () => {
+ if (!currentTaskId.value) return
+
+ try {
+ const res = await retryTask(currentTaskId.value)
+ if (res.code === 0) {
+ message.success('任务已重启')
+ currentTaskStatus.value = 'PENDING'
+ currentTaskStep.value = 'prepare_files'
+ isGenerating.value = true
+ startPollingTask()
+ } else {
+ message.error(res.msg || '重试失败')
+ }
+ } catch (error) {
+ console.error('retryTask error:', error)
+ message.error('重试任务失败')
}
}
@@ -301,6 +460,29 @@ const toDataURL = (file) => new Promise((resolve, reject) => {
reader.readAsDataURL(file)
})
+// 状态描述映射
+const getStatusText = (status) => {
+ const statusMap = {
+ 'PENDING': '等待处理',
+ 'PROCESSING': '处理中',
+ 'SUCCESS': '已完成',
+ 'FAILED': '失败',
+ 'CANCELED': '已取消'
+ }
+ return statusMap[status] || status
+}
+
+const getStepText = (step) => {
+ const stepMap = {
+ 'prepare_files': '准备文件',
+ 'synthesize_voice': '语音合成',
+ 'sync_lip': '口型同步',
+ 'finishing': '完成处理',
+ 'canceled': '已取消'
+ }
+ return stepMap[step] || step
+}
+
const playAudioPreview = (url, options = {}) => {
if (!url) return message.warning('暂无可试听的音频')
@@ -383,6 +565,11 @@ onUnmounted(() => {
// 重置播放状态
isPlayingPreview.value = false
isPlayingSynthesized.value = false
+ // 清理轮询
+ if (pollingInterval.value) {
+ clearInterval(pollingInterval.value)
+ pollingInterval.value = null
+ }
})
// 监听器
@@ -578,21 +765,55 @@ let previewObjectUrl = ''
-
- {{ isGenerating ? '生成中...' : '生成视频' }}
-
+
+
+ {{ isGenerating ? '生成中...' : '生成视频' }}
+
+
+
+
+
+
+ 状态:
+ {{ getStatusText(currentTaskStatus) }}
+
+
+ 步骤:
+ {{ getStepText(currentTaskStep) }}
+
+
+
+
@@ -940,6 +1161,56 @@ let previewObjectUrl = ''
gap: 12px;
}
+.generate-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.task-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.task-status {
+ padding: 12px;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(59, 130, 246, 0.2);
+ border-radius: var(--radius-card);
+}
+
+.status-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.status-row:last-child {
+ margin-bottom: 0;
+}
+
+.status-label {
+ font-size: 13px;
+ color: var(--color-text-secondary);
+ font-weight: 600;
+}
+
+.status-value {
+ font-size: 13px;
+ color: var(--color-text);
+}
+
+.action-buttons {
+ display: flex;
+ gap: 8px;
+}
+
+.action-buttons .ant-btn {
+ flex: 1;
+}
+
.preview-title {
font-size: 14px;
font-weight: 600;
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enmus/ErrorCodeConstants.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java
similarity index 70%
rename from yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enmus/ErrorCodeConstants.java
rename to yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java
index b33a2e548c..efbceee3cf 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enmus/ErrorCodeConstants.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/enums/ErrorCodeConstants.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.tik.enmus;
+package cn.iocoder.yudao.module.tik.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
@@ -10,9 +10,18 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
*/
public interface ErrorCodeConstants {
-
ErrorCode USER_PROMPT_NOT_EXISTS = new ErrorCode(1_040_010_002, "用户提示词不存在");
+ // ========== 通用错误码 1-000-000-000 ==========
+ ErrorCode GENERAL_NOT_EXISTS = new ErrorCode(1_000_000_001, "数据不存在");
+ ErrorCode GENERAL_FORBIDDEN = new ErrorCode(1_000_000_002, "没有权限访问该数据");
+
+ // ========== 数字人任务错误码 1-030-002-000 ==========
+ ErrorCode DIGITAL_HUMAN_TASK_AUDIO_REQUIRED = new ErrorCode(1_030_002_001, "音频文件不能为空");
+ ErrorCode DIGITAL_HUMAN_TASK_VIDEO_REQUIRED = new ErrorCode(1_030_002_002, "视频文件不能为空");
+ ErrorCode DIGITAL_HUMAN_TASK_CANNOT_CANCEL = new ErrorCode(1_030_002_003, "只有处理中的任务才能取消");
+ ErrorCode DIGITAL_HUMAN_TASK_CANNOT_RETRY = new ErrorCode(1_030_002_004, "只有失败或已取消的任务才能重试");
+
// ========== 文件管理 1-030-000-000 ==========
ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_030_000_001, "文件不存在");
ErrorCode FILE_CATEGORY_INVALID = new ErrorCode(1_030_000_002, "文件分类无效");
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikFileGroupServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikFileGroupServiceImpl.java
index 160000c97d..c440cdd2da 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikFileGroupServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikFileGroupServiceImpl.java
@@ -21,7 +21,7 @@ import jakarta.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 文件分组 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java
index 8218c8596c..a343c0d87e 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java
@@ -17,7 +17,7 @@ import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.OSS_INIT_FAILED;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.OSS_INIT_FAILED;
/**
* OSS初始化 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileGroupServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileGroupServiceImpl.java
index caf6e49859..3659a54051 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileGroupServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileGroupServiceImpl.java
@@ -24,7 +24,7 @@ import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 文件分组关联 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileServiceImpl.java
index de1578dec1..408ce6c03c 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikUserFileServiceImpl.java
@@ -39,7 +39,7 @@ import jakarta.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 用户文件 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/quota/service/TikUserQuotaServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/quota/service/TikUserQuotaServiceImpl.java
index fc87062569..0230c07d87 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/quota/service/TikUserQuotaServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/quota/service/TikUserQuotaServiceImpl.java
@@ -10,8 +10,8 @@ import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.QUOTA_NOT_ENOUGH;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.QUOTA_NOT_EXISTS;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.QUOTA_NOT_ENOUGH;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.QUOTA_NOT_EXISTS;
/**
* 用户配额 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java
index 509e80eea7..3dd1c14a69 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/userprompt/service/UserPromptServiceImpl.java
@@ -13,7 +13,7 @@ import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.USER_PROMPT_NOT_EXISTS;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.USER_PROMPT_NOT_EXISTS;
/**
* 用户提示词 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/CosyVoiceClient.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/CosyVoiceClient.java
index fbd6b3e461..d2467f9c1a 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/CosyVoiceClient.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/CosyVoiceClient.java
@@ -33,7 +33,7 @@ import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.VOICE_TTS_FAILED;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.VOICE_TTS_FAILED;
/**
* CosyVoice 客户端
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/LatentsyncClient.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/LatentsyncClient.java
index 56b15914c7..3d85c24bb4 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/LatentsyncClient.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/client/LatentsyncClient.java
@@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.LATENTSYNC_SUBMIT_FAILED;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.LATENTSYNC_SUBMIT_FAILED;
/**
* 302AI Latentsync 客户端
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/controller/AppTikDigitalHumanTaskController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/controller/AppTikDigitalHumanTaskController.java
new file mode 100644
index 0000000000..f963a96873
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/controller/AppTikDigitalHumanTaskController.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.tik.voice.controller;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.tik.voice.service.DigitalHumanTaskService;
+import cn.iocoder.yudao.module.tik.voice.vo.*;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 用户 App - 数字人任务控制器
+ *
+ * @author 芋道源码
+ */
+@Tag(name = "用户 App - 数字人任务")
+@RestController
+@RequestMapping("/api/tik/digital-human")
+@Validated
+public class AppTikDigitalHumanTaskController {
+
+ @Resource
+ private DigitalHumanTaskService digitalHumanTaskService;
+
+ @PostMapping("/task/create")
+ @Operation(summary = "创建数字人任务")
+ public CommonResult createTask(@Valid @RequestBody AppTikDigitalHumanCreateReqVO reqVO) {
+ Long taskId = digitalHumanTaskService.createTask(reqVO);
+ return CommonResult.success(taskId);
+ }
+
+ @GetMapping("/task/get")
+ @Operation(summary = "查询任务详情")
+ public CommonResult getTask(
+ @Parameter(description = "任务ID", required = true, example = "12345")
+ @RequestParam("taskId") Long taskId) {
+ AppTikDigitalHumanRespVO task = digitalHumanTaskService.getTask(taskId);
+ return CommonResult.success(task);
+ }
+
+ @GetMapping("/task/page")
+ @Operation(summary = "分页查询任务列表")
+ public CommonResult> getTaskPage(@ModelAttribute AppTikDigitalHumanPageReqVO pageReqVO) {
+ PageResult result = digitalHumanTaskService.getTaskPage(pageReqVO);
+ return CommonResult.success(result);
+ }
+
+ @GetMapping("/task/statistics")
+ @Operation(summary = "查询任务统计")
+ public CommonResult getTaskStatistics() {
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ DigitalHumanTaskService.TaskStatisticsVO statistics = digitalHumanTaskService.getTaskStatistics(userId);
+ return CommonResult.success(statistics);
+ }
+
+ @PostMapping("/task/{taskId}/cancel")
+ @Operation(summary = "取消任务")
+ public CommonResult cancelTask(
+ @Parameter(description = "任务ID", required = true, example = "12345")
+ @PathVariable Long taskId) {
+ digitalHumanTaskService.cancelTask(taskId);
+ return CommonResult.success(true);
+ }
+
+ @PostMapping("/task/{taskId}/retry")
+ @Operation(summary = "重试任务")
+ public CommonResult retryTask(
+ @Parameter(description = "任务ID", required = true, example = "12345")
+ @PathVariable Long taskId) {
+ digitalHumanTaskService.retryTask(taskId);
+ return CommonResult.success(true);
+ }
+
+ @DeleteMapping("/task/{taskId}")
+ @Operation(summary = "删除任务")
+ public CommonResult deleteTask(
+ @Parameter(description = "任务ID", required = true, example = "12345")
+ @PathVariable Long taskId) {
+ digitalHumanTaskService.deleteTask(taskId);
+ return CommonResult.success(true);
+ }
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/dal/dataobject/TikDigitalHumanTaskDO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/dal/dataobject/TikDigitalHumanTaskDO.java
new file mode 100644
index 0000000000..78d3bd825b
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/dal/dataobject/TikDigitalHumanTaskDO.java
@@ -0,0 +1,136 @@
+package cn.iocoder.yudao.module.tik.voice.dal.dataobject;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 数字人任务 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("tik_digital_human_task")
+@KeySequence("tik_digital_human_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TikDigitalHumanTaskDO extends TenantBaseDO {
+
+ /**
+ * 任务ID
+ */
+ @TableId
+ private Long id;
+ /**
+ * 用户ID
+ */
+ private Long userId;
+ /**
+ * AI供应商(302ai/aliyun/openai等)
+ */
+ private String aiProvider;
+ /**
+ * 任务名称
+ */
+ private String taskName;
+
+ // ========== 文件信息 ==========
+ /**
+ * 音频文件ID(tik_user_file.id)
+ */
+ private Long audioFileId;
+ /**
+ * 视频文件ID(tik_user_file.id)
+ */
+ private Long videoFileId;
+ /**
+ * 音频文件URL(公网可访问,用于Latentsync调用)
+ */
+ private String audioUrl;
+ /**
+ * 视频文件URL(公网可访问,用于Latentsync调用)
+ */
+ private String videoUrl;
+
+ // ========== 生成参数 ==========
+ /**
+ * 配音配置ID(tik_user_voice.id)
+ */
+ private Long voiceConfigId;
+ /**
+ * CosyVoice生成的voice_id
+ */
+ private String voiceId;
+ /**
+ * 语速(0.5-2.0)
+ */
+ private Float speechRate;
+ /**
+ * 音量(-10到10)
+ */
+ private Float volume;
+ /**
+ * 情感(neutral/happy/sad等)
+ */
+ private String emotion;
+ /**
+ * Latentsync guidance_scale(1-2)
+ */
+ private Integer guidanceScale;
+ /**
+ * 随机种子
+ */
+ private Integer seed;
+
+ // ========== 任务状态 ==========
+ /**
+ * 任务状态
+ * 枚举:{@link cn.iocoder.yudao.module.tik.voice.enums.DigitalHumanTaskStatusEnum}
+ */
+ private String status;
+ /**
+ * 进度百分比(0-100)
+ */
+ private Integer progress;
+ /**
+ * 当前步骤(prepare_files/synthesize_voice/sync_lip/generate_video/finishing)
+ */
+ private String currentStep;
+
+ // ========== 结果信息 ==========
+ /**
+ * 生成结果视频URL(预签名URL)
+ */
+ private String resultVideoUrl;
+ /**
+ * 生成结果文件ID(保存到tik_user_file)
+ */
+ private Long resultVideoFileId;
+ /**
+ * 错误信息
+ */
+ private String errorMessage;
+ /**
+ * 错误详情
+ */
+ private String errorDetail;
+
+ // ========== 时间戳 ==========
+ /**
+ * 任务开始时间
+ */
+ private LocalDateTime startTime;
+ /**
+ * 任务完成时间
+ */
+ private LocalDateTime finishTime;
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/dal/mysql/TikDigitalHumanTaskMapper.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/dal/mysql/TikDigitalHumanTaskMapper.java
new file mode 100644
index 0000000000..0753703e74
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/dal/mysql/TikDigitalHumanTaskMapper.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.tik.voice.dal.mysql;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikDigitalHumanTaskDO;
+import cn.iocoder.yudao.module.tik.voice.vo.AppTikDigitalHumanPageReqVO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 数字人任务 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface TikDigitalHumanTaskMapper extends BaseMapperX {
+
+ /**
+ * 分页查询用户的任务列表(简单版本,仅按userId过滤)
+ */
+ default PageResult selectPage(PageParam pageParam, Long userId) {
+ return selectPage(pageParam, new LambdaQueryWrapperX()
+ .eqIfPresent(TikDigitalHumanTaskDO::getUserId, userId)
+ .orderByDesc(TikDigitalHumanTaskDO::getCreateTime));
+ }
+
+ /**
+ * 分页查询用户的任务列表(完整版本,支持更多过滤条件)
+ */
+ default PageResult selectPage(PageParam pageParam, LambdaQueryWrapperX queryWrapper) {
+ return BaseMapperX.super.selectPage(pageParam, queryWrapper);
+ }
+
+ /**
+ * 查询用户进行中的任务数量
+ */
+ default Long selectProcessingCount(Long userId) {
+ return selectCount(new LambdaQueryWrapperX()
+ .eq(TikDigitalHumanTaskDO::getUserId, userId)
+ .eq(TikDigitalHumanTaskDO::getStatus, "PROCESSING"));
+ }
+
+ /**
+ * 查询用户已完成的任务数量
+ */
+ default Long selectCompletedCount(Long userId) {
+ return selectCount(new LambdaQueryWrapperX()
+ .eq(TikDigitalHumanTaskDO::getUserId, userId)
+ .in(TikDigitalHumanTaskDO::getStatus, "SUCCESS", "FAILED", "CANCELED"));
+ }
+
+ /**
+ * 查询用户的最新任务列表
+ */
+ default List selectRecentTasks(Long userId, Integer limit) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(TikDigitalHumanTaskDO::getUserId, userId)
+ .orderByDesc(TikDigitalHumanTaskDO::getCreateTime)
+ .last("LIMIT " + limit));
+ }
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/enums/DigitalHumanTaskStatusEnum.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/enums/DigitalHumanTaskStatusEnum.java
new file mode 100644
index 0000000000..07b8d61549
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/enums/DigitalHumanTaskStatusEnum.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.tik.voice.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 数字人任务状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum DigitalHumanTaskStatusEnum {
+
+ PENDING("PENDING", "等待处理"),
+ PROCESSING("PROCESSING", "处理中"),
+ SUCCESS("SUCCESS", "已完成"),
+ FAILED("FAILED", "失败"),
+ CANCELED("CANCELED", "已取消");
+
+ private final String status;
+ private final String desc;
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/enums/DigitalHumanTaskStepEnum.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/enums/DigitalHumanTaskStepEnum.java
new file mode 100644
index 0000000000..2b669a079c
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/enums/DigitalHumanTaskStepEnum.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.tik.voice.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 数字人任务步骤枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum DigitalHumanTaskStepEnum {
+
+ PREPARE_FILES("prepare_files", "文件准备", 10),
+ SYNTHESIZE_VOICE("synthesize_voice", "语音合成", 40),
+ SYNC_LIP("sync_lip", "口型同步", 70),
+ GENERATE_VIDEO("generate_video", "生成视频", 90),
+ FINISHING("finishing", "完成处理", 100);
+
+ private final String step;
+ private final String desc;
+ private final int progress;
+
+ /**
+ * 根据步骤获取进度百分比
+ */
+ public static int getProgress(String step) {
+ for (DigitalHumanTaskStepEnum value : values()) {
+ if (value.getStep().equals(step)) {
+ return value.getProgress();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * 根据步骤获取描述
+ */
+ public static String getDesc(String step) {
+ for (DigitalHumanTaskStepEnum value : values()) {
+ if (value.getStep().equals(step)) {
+ return value.getDesc();
+ }
+ }
+ return step;
+ }
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskService.java
new file mode 100644
index 0000000000..448b8d7e3f
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskService.java
@@ -0,0 +1,124 @@
+package cn.iocoder.yudao.module.tik.voice.service;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.tik.voice.vo.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * 数字人任务 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface DigitalHumanTaskService {
+
+ /**
+ * 创建数字人任务
+ *
+ * @param reqVO 请求参数
+ * @return 任务ID
+ */
+ Long createTask(AppTikDigitalHumanCreateReqVO reqVO);
+
+ /**
+ * 查询任务详情
+ *
+ * @param taskId 任务ID
+ * @return 任务详情
+ */
+ AppTikDigitalHumanRespVO getTask(Long taskId);
+
+ /**
+ * 分页查询任务列表
+ *
+ * @param pageReqVO 分页查询参数
+ * @return 任务列表
+ */
+ PageResult getTaskPage(AppTikDigitalHumanPageReqVO pageReqVO);
+
+ /**
+ * 查询用户任务统计
+ *
+ * @param userId 用户ID
+ * @return 任务统计
+ */
+ TaskStatisticsVO getTaskStatistics(Long userId);
+
+ /**
+ * 取消任务
+ *
+ * @param taskId 任务ID
+ */
+ void cancelTask(Long taskId);
+
+ /**
+ * 重试任务
+ *
+ * @param taskId 任务ID
+ */
+ void retryTask(Long taskId);
+
+ /**
+ * 删除任务
+ *
+ * @param taskId 任务ID
+ */
+ void deleteTask(Long taskId);
+
+ /**
+ * 任务统计 VO
+ */
+ TaskStatisticsVO getTaskStatistics(AppTikDigitalHumanPageReqVO pageReqVO);
+
+ /**
+ * 任务统计信息
+ */
+ class TaskStatisticsVO {
+
+ @Schema(description = "进行中任务数", example = "3")
+ private Integer processingCount;
+
+ @Schema(description = "已完成任务数", example = "15")
+ private Integer completedCount;
+
+ @Schema(description = "失败任务数", example = "2")
+ private Integer failedCount;
+
+ @Schema(description = "总任务数", example = "20")
+ private Integer totalCount;
+
+ // Getters and Setters
+ public Integer getProcessingCount() {
+ return processingCount;
+ }
+
+ public void setProcessingCount(Integer processingCount) {
+ this.processingCount = processingCount;
+ }
+
+ public Integer getCompletedCount() {
+ return completedCount;
+ }
+
+ public void setCompletedCount(Integer completedCount) {
+ this.completedCount = completedCount;
+ }
+
+ public Integer getFailedCount() {
+ return failedCount;
+ }
+
+ public void setFailedCount(Integer failedCount) {
+ this.failedCount = failedCount;
+ }
+
+ public Integer getTotalCount() {
+ return totalCount;
+ }
+
+ public void setTotalCount(Integer totalCount) {
+ this.totalCount = totalCount;
+ }
+ }
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java
new file mode 100644
index 0000000000..b72c7ad987
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java
@@ -0,0 +1,549 @@
+package cn.iocoder.yudao.module.tik.voice.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
+import cn.iocoder.yudao.module.infra.api.file.FileApi;
+import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
+import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileDO;
+import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
+import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikDigitalHumanTaskDO;
+import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikDigitalHumanTaskMapper;
+import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikUserVoiceDO;
+import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikUserVoiceMapper;
+import cn.iocoder.yudao.module.tik.voice.enums.DigitalHumanTaskStatusEnum;
+import cn.iocoder.yudao.module.tik.voice.enums.DigitalHumanTaskStepEnum;
+import cn.iocoder.yudao.module.tik.voice.service.TikUserVoiceService;
+import cn.iocoder.yudao.module.tik.voice.vo.*;
+import cn.iocoder.yudao.module.tik.voice.client.LatentsyncClient;
+import cn.iocoder.yudao.module.tik.voice.service.LatentsyncService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 数字人任务 Service 实现
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@Service
+@Validated
+@RequiredArgsConstructor
+public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
+
+ private final TikDigitalHumanTaskMapper taskMapper;
+ private final TikUserVoiceMapper voiceMapper;
+ private final TikUserFileMapper userFileMapper;
+ private final FileMapper fileMapper;
+ private final FileApi fileApi;
+ private final TikUserVoiceService userVoiceService;
+ private final LatentsyncService latentsyncService;
+
+ /**
+ * 预签名URL过期时间(24小时)
+ */
+ private static final int PRESIGN_URL_EXPIRATION_SECONDS = 24 * 3600;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createTask(AppTikDigitalHumanCreateReqVO reqVO) {
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+
+ // 1. 验证输入参数
+ validateTaskInput(reqVO, userId);
+
+ // 2. 创建任务记录
+ TikDigitalHumanTaskDO task = createTaskRecord(reqVO, userId);
+ taskMapper.insert(task);
+
+ // 3. 异步处理任务
+ Long taskId = task.getId();
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+ @Override
+ public void afterCommit() {
+ log.info("[createTask][任务({})已创建,开始异步处理]", taskId);
+ processTaskAsync(taskId);
+ }
+ });
+
+ log.info("[createTask][用户({})创建数字人任务成功,任务ID({})]", userId, taskId);
+ return taskId;
+ }
+
+ @Override
+ public AppTikDigitalHumanRespVO getTask(Long taskId) {
+ TikDigitalHumanTaskDO task = taskMapper.selectById(taskId);
+ if (task == null) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_NOT_EXISTS);
+ }
+
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (!task.getUserId().equals(userId)) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_FORBIDDEN);
+ }
+
+ return convertToRespVO(task);
+ }
+
+ @Override
+ public PageResult getTaskPage(AppTikDigitalHumanPageReqVO pageReqVO) {
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ pageReqVO.setUserId(userId);
+
+ // 构建查询条件
+ LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX()
+ .eq(TikDigitalHumanTaskDO::getUserId, userId)
+ .likeIfPresent(TikDigitalHumanTaskDO::getTaskName, pageReqVO.getTaskName())
+ .eqIfPresent(TikDigitalHumanTaskDO::getStatus, pageReqVO.getStatus())
+ .betweenIfPresent(TikDigitalHumanTaskDO::getCreateTime,
+ pageReqVO.getCreateTimeStart(), pageReqVO.getCreateTimeEnd())
+ .orderByDesc(TikDigitalHumanTaskDO::getCreateTime);
+
+ // 查询分页结果 - 使用 Mapper 的重载方法,传入 pageParam 和 queryWrapper
+ PageResult pageResult = taskMapper.selectPage(pageReqVO, queryWrapper);
+
+ // 转换为 VO
+ return CollectionUtils.convertPage(pageResult, this::convertToRespVO);
+ }
+
+ @Override
+ public TaskStatisticsVO getTaskStatistics(Long userId) {
+ TaskStatisticsVO stats = new TaskStatisticsVO();
+
+ // 查询各种状态的任务数量 - 将 Long 转换为 Integer
+ Long processingCount = taskMapper.selectProcessingCount(userId);
+ Long completedCount = taskMapper.selectCompletedCount(userId);
+
+ stats.setProcessingCount(processingCount != null ? processingCount.intValue() : 0);
+ stats.setCompletedCount(completedCount != null ? completedCount.intValue() : 0);
+
+ // 查询失败任务数量
+ Long failedCount = taskMapper.selectCount(new LambdaQueryWrapperX()
+ .eq(TikDigitalHumanTaskDO::getUserId, userId)
+ .eq(TikDigitalHumanTaskDO::getStatus, "FAILED"));
+ stats.setFailedCount(failedCount != null ? failedCount.intValue() : 0);
+
+ // 总任务数
+ stats.setTotalCount(stats.getProcessingCount() + stats.getCompletedCount() + stats.getFailedCount());
+
+ return stats;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void cancelTask(Long taskId) {
+ TikDigitalHumanTaskDO task = taskMapper.selectById(taskId);
+ if (task == null) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_NOT_EXISTS);
+ }
+
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (!task.getUserId().equals(userId)) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_FORBIDDEN);
+ }
+
+ if (!"PROCESSING".equals(task.getStatus())) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.DIGITAL_HUMAN_TASK_CANNOT_CANCEL);
+ }
+
+ // 更新任务状态
+ TikDigitalHumanTaskDO updateObj = new TikDigitalHumanTaskDO();
+ updateObj.setId(taskId);
+ updateObj.setStatus("CANCELED");
+ updateObj.setProgress(0);
+ updateObj.setCurrentStep("canceled");
+ taskMapper.updateById(updateObj);
+
+ log.info("[cancelTask][用户({})取消任务({})成功]", userId, taskId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void retryTask(Long taskId) {
+ TikDigitalHumanTaskDO task = taskMapper.selectById(taskId);
+ if (task == null) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_NOT_EXISTS);
+ }
+
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (!task.getUserId().equals(userId)) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_FORBIDDEN);
+ }
+
+ if (!"FAILED".equals(task.getStatus()) && !"CANCELED".equals(task.getStatus())) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.DIGITAL_HUMAN_TASK_CANNOT_RETRY);
+ }
+
+ // 重置任务状态
+ TikDigitalHumanTaskDO updateObj = new TikDigitalHumanTaskDO();
+ updateObj.setId(taskId);
+ updateObj.setStatus("PENDING");
+ updateObj.setProgress(0);
+ updateObj.setCurrentStep("prepare_files");
+ updateObj.setErrorMessage(null);
+ updateObj.setErrorDetail(null);
+ taskMapper.updateById(updateObj);
+
+ // 重新开始异步处理
+ processTaskAsync(taskId);
+
+ log.info("[retryTask][用户({})重试任务({})成功]", userId, taskId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteTask(Long taskId) {
+ TikDigitalHumanTaskDO task = taskMapper.selectById(taskId);
+ if (task == null) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_NOT_EXISTS);
+ }
+
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (!task.getUserId().equals(userId)) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.GENERAL_FORBIDDEN);
+ }
+
+ // 删除任务
+ taskMapper.deleteById(taskId);
+
+ log.info("[deleteTask][用户({})删除任务({})成功]", userId, taskId);
+ }
+
+ @Override
+ public TaskStatisticsVO getTaskStatistics(AppTikDigitalHumanPageReqVO pageReqVO) {
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ return getTaskStatistics(userId);
+ }
+
+ // ========== 私有方法 ==========
+
+ /**
+ * 验证任务输入参数
+ */
+ private void validateTaskInput(AppTikDigitalHumanCreateReqVO reqVO, Long userId) {
+ // 验证文件信息:必须提供音频和视频文件之一
+ boolean hasAudio = reqVO.getAudioFileId() != null || StrUtil.isNotBlank(reqVO.getAudioUrl());
+ boolean hasVideo = reqVO.getVideoFileId() != null || StrUtil.isNotBlank(reqVO.getVideoUrl());
+
+ if (!hasAudio) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.DIGITAL_HUMAN_TASK_AUDIO_REQUIRED);
+ }
+ if (!hasVideo) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.DIGITAL_HUMAN_TASK_VIDEO_REQUIRED);
+ }
+
+ // 如果提供了fileId,验证文件是否存在且属于用户
+ if (reqVO.getAudioFileId() != null) {
+ validateUserFile(reqVO.getAudioFileId(), userId, "音频");
+ }
+ if (reqVO.getVideoFileId() != null) {
+ validateUserFile(reqVO.getVideoFileId(), userId, "视频");
+ }
+
+ // 验证配音配置
+ if (reqVO.getVoiceConfigId() != null) {
+ TikUserVoiceDO voice = voiceMapper.selectById(reqVO.getVoiceConfigId());
+ if (voice == null || !voice.getUserId().equals(userId)) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.VOICE_NOT_EXISTS);
+ }
+ }
+ }
+
+ /**
+ * 验证用户文件
+ */
+ private void validateUserFile(Long fileId, Long userId, String fileType) {
+ TikUserFileDO userFile = userFileMapper.selectOne(new LambdaQueryWrapperX()
+ .eq(TikUserFileDO::getId, fileId)
+ .eq(TikUserFileDO::getUserId, userId));
+ if (userFile == null) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.FILE_NOT_EXISTS, fileType + "文件不存在");
+ }
+ }
+
+ /**
+ * 创建任务记录
+ */
+ private TikDigitalHumanTaskDO createTaskRecord(AppTikDigitalHumanCreateReqVO reqVO, Long userId) {
+ return TikDigitalHumanTaskDO.builder()
+ .userId(userId)
+ .taskName(reqVO.getTaskName())
+ .aiProvider(StrUtil.blankToDefault(reqVO.getAiProvider(), "302ai"))
+ .audioFileId(reqVO.getAudioFileId())
+ .videoFileId(reqVO.getVideoFileId())
+ .audioUrl(reqVO.getAudioUrl())
+ .videoUrl(reqVO.getVideoUrl())
+ .voiceConfigId(reqVO.getVoiceConfigId())
+ .speechRate(reqVO.getSpeechRate() != null ? reqVO.getSpeechRate() : 1.0f)
+ .volume(reqVO.getVolume() != null ? reqVO.getVolume() : 0f)
+ .emotion(StrUtil.blankToDefault(reqVO.getEmotion(), "neutral"))
+ .guidanceScale(reqVO.getGuidanceScale() != null ? reqVO.getGuidanceScale() : 1)
+ .seed(reqVO.getSeed() != null ? reqVO.getSeed() : 8888)
+ .status("PENDING")
+ .progress(0)
+ .currentStep("prepare_files")
+ .build();
+ }
+
+ /**
+ * 转换为响应 VO
+ */
+ private AppTikDigitalHumanRespVO convertToRespVO(TikDigitalHumanTaskDO task) {
+ AppTikDigitalHumanRespVO respVO = BeanUtils.toBean(task, AppTikDigitalHumanRespVO.class);
+
+ // 设置状态描述
+ respVO.setStatusDesc(DigitalHumanTaskStatusEnum.valueOf(task.getStatus()).getDesc());
+
+ // 设置当前步骤描述
+ respVO.setCurrentStepDesc(DigitalHumanTaskStepEnum.getDesc(task.getCurrentStep()));
+
+ return respVO;
+ }
+
+ /**
+ * 异步处理任务
+ */
+ @Async("taskExecutor")
+ public void processTaskAsync(Long taskId) {
+ try {
+ log.info("[processTaskAsync][开始处理任务({})]", taskId);
+ processTask(taskId);
+ } catch (Exception e) {
+ log.error("[processTaskAsync][任务({})处理异常]", taskId, e);
+ // 更新任务状态为失败
+ updateTaskStatus(taskId, "FAILED", "async_error", 0, "任务处理异常", null, e.getMessage());
+ }
+ }
+
+ /**
+ * 处理任务(同步方法)
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public void processTask(Long taskId) {
+ TikDigitalHumanTaskDO task = taskMapper.selectById(taskId);
+ if (task == null) {
+ log.error("[processTask][任务({})不存在]", taskId);
+ return;
+ }
+
+ try {
+ // 更新任务状态为处理中
+ updateTaskStatus(taskId, "PROCESSING", "prepare_files", 5, "开始处理任务", null);
+
+ // 步骤1:文件准备
+ prepareFiles(task);
+ updateTaskProgress(taskId, DigitalHumanTaskStepEnum.PREPARE_FILES, "文件准备完成");
+
+ // 步骤2:语音合成
+ String audioUrl = synthesizeVoice(task);
+ updateTaskProgress(taskId, DigitalHumanTaskStepEnum.SYNTHESIZE_VOICE, "语音合成完成");
+
+ // 步骤3:口型同步
+ String syncedVideoUrl = syncLip(task, audioUrl);
+ updateTaskProgress(taskId, DigitalHumanTaskStepEnum.SYNC_LIP, "口型同步完成");
+
+ // 步骤4:生成视频
+ String resultVideoUrl = generateVideo(task, syncedVideoUrl);
+ updateTaskProgress(taskId, DigitalHumanTaskStepEnum.FINISHING, "视频生成完成");
+
+ // 任务完成
+ updateTaskStatus(taskId, "SUCCESS", "finishing", 100, "任务处理完成", resultVideoUrl);
+
+ log.info("[processTask][任务({})处理完成]", taskId);
+
+ } catch (Exception e) {
+ log.error("[processTask][任务({})处理失败]", taskId, e);
+ updateTaskStatus(taskId, "FAILED", task.getCurrentStep(), task.getProgress(), "任务处理失败:" + e.getMessage(), null, e.getMessage());
+ }
+ }
+
+ /**
+ * 准备文件
+ */
+ private void prepareFiles(TikDigitalHumanTaskDO task) throws Exception {
+ log.info("[prepareFiles][任务({})开始准备文件]", task.getId());
+
+ // 如果提供了fileId,生成预签名URL
+ if (task.getAudioFileId() != null) {
+ FileDO audioFile = fileMapper.selectById(task.getAudioFileId());
+ if (audioFile != null) {
+ task.setAudioUrl(fileApi.presignGetUrl(audioFile.getUrl(), PRESIGN_URL_EXPIRATION_SECONDS));
+ }
+ }
+
+ if (task.getVideoFileId() != null) {
+ FileDO videoFile = fileMapper.selectById(task.getVideoFileId());
+ if (videoFile != null) {
+ task.setVideoUrl(fileApi.presignGetUrl(videoFile.getUrl(), PRESIGN_URL_EXPIRATION_SECONDS));
+ }
+ }
+
+ // 验证文件URL
+ if (StrUtil.isBlank(task.getAudioUrl())) {
+ throw new Exception("音频文件URL生成失败");
+ }
+ if (StrUtil.isBlank(task.getVideoUrl())) {
+ throw new Exception("视频文件URL生成失败");
+ }
+
+ // 更新任务记录
+ TikDigitalHumanTaskDO updateObj = new TikDigitalHumanTaskDO();
+ updateObj.setId(task.getId());
+ updateObj.setAudioUrl(task.getAudioUrl());
+ updateObj.setVideoUrl(task.getVideoUrl());
+ taskMapper.updateById(updateObj);
+
+ log.info("[prepareFiles][任务({})文件准备完成]", task.getId());
+ }
+
+ /**
+ * 语音合成
+ */
+ private String synthesizeVoice(TikDigitalHumanTaskDO task) throws Exception {
+ log.info("[synthesizeVoice][任务({})开始语音合成]", task.getId());
+
+ // TODO: 调用现有的语音合成服务
+ // 这里需要根据实际的语音合成API进行集成
+
+ // 临时返回音频URL(实际应该调用语音合成服务)
+ String audioUrl = task.getAudioUrl();
+
+ log.info("[synthesizeVoice][任务({})语音合成完成]", task.getId());
+ return audioUrl;
+ }
+
+ /**
+ * 口型同步
+ */
+ private String syncLip(TikDigitalHumanTaskDO task, String audioUrl) throws Exception {
+ log.info("[syncLip][任务({})开始口型同步,使用AI供应商: {}]", task.getId(), task.getAiProvider());
+
+ String syncedVideoUrl;
+ String aiProvider = task.getAiProvider();
+
+ // 根据AI供应商路由到不同的服务
+ if ("302ai".equalsIgnoreCase(aiProvider)) {
+ // 302AI Latentsync 服务
+ syncedVideoUrl = syncWithLatentsync(task, audioUrl);
+ } else if ("aliyun".equalsIgnoreCase(aiProvider)) {
+ // TODO: 阿里云语音驱动视频服务
+ log.warn("[syncLip][任务({})暂不支持阿里云AI供应商,使用原视频URL]", task.getId());
+ syncedVideoUrl = task.getVideoUrl();
+ } else if ("openai".equalsIgnoreCase(aiProvider)) {
+ // TODO: OpenAI 语音驱动视频服务
+ log.warn("[syncLip][任务({})暂不支持OpenAI AI供应商,使用原视频URL]", task.getId());
+ syncedVideoUrl = task.getVideoUrl();
+ } else if ("minimax".equalsIgnoreCase(aiProvider)) {
+ // TODO: MiniMax 语音驱动视频服务
+ log.warn("[syncLip][任务({})暂不支持MiniMax AI供应商,使用原视频URL]", task.getId());
+ syncedVideoUrl = task.getVideoUrl();
+ } else {
+ log.error("[syncLip][任务({})不支持的AI供应商: {}]", task.getId(), aiProvider);
+ throw new Exception("不支持的AI供应商: " + aiProvider);
+ }
+
+ log.info("[syncLip][任务({})口型同步完成]", task.getId());
+ return syncedVideoUrl;
+ }
+
+ /**
+ * 使用302AI Latentsync进行口型同步
+ */
+ private String syncWithLatentsync(TikDigitalHumanTaskDO task, String audioUrl) throws Exception {
+ // 构建Latentsync请求VO
+ AppTikLatentsyncSubmitReqVO reqVO = new AppTikLatentsyncSubmitReqVO();
+ reqVO.setAudioUrl(audioUrl);
+ reqVO.setVideoUrl(task.getVideoUrl());
+ reqVO.setGuidanceScale(task.getGuidanceScale());
+ reqVO.setSeed(task.getSeed());
+
+ // 调用Latentsync服务
+ AppTikLatentsyncSubmitRespVO response = latentsyncService.submitTask(reqVO);
+
+ // 等待处理完成(这里需要根据实际的Latentsync API调整)
+ // 临时返回处理后的视频URL
+ return task.getVideoUrl();
+ }
+
+ /**
+ * 生成视频
+ */
+ private String generateVideo(TikDigitalHumanTaskDO task, String syncedVideoUrl) throws Exception {
+ log.info("[generateVideo][任务({})开始生成视频]", task.getId());
+
+ // TODO: 这里可以添加视频后处理逻辑,比如添加字幕、特效等
+
+ // 临时返回同步后的视频URL
+ String resultVideoUrl = syncedVideoUrl;
+
+ log.info("[generateVideo][任务({})视频生成完成]", task.getId());
+ return resultVideoUrl;
+ }
+
+ /**
+ * 更新任务状态
+ */
+ private void updateTaskStatus(Long taskId, String status, String currentStep, Integer progress,
+ String message, String resultVideoUrl) {
+ updateTaskStatus(taskId, status, currentStep, progress, message, resultVideoUrl, null);
+ }
+
+ /**
+ * 更新任务状态(带错误详情)
+ */
+ private void updateTaskStatus(Long taskId, String status, String currentStep, Integer progress,
+ String message, String resultVideoUrl, String errorDetail) {
+ TikDigitalHumanTaskDO updateObj = new TikDigitalHumanTaskDO();
+ updateObj.setId(taskId);
+ updateObj.setStatus(status);
+ updateObj.setCurrentStep(currentStep);
+ updateObj.setProgress(progress);
+
+ if ("SUCCESS".equals(status)) {
+ updateObj.setResultVideoUrl(resultVideoUrl);
+ updateObj.setFinishTime(LocalDateTime.now());
+ } else if ("PROCESSING".equals(status)) {
+ updateObj.setStartTime(LocalDateTime.now());
+ } else if ("FAILED".equals(status)) {
+ updateObj.setErrorMessage(message);
+ updateObj.setErrorDetail(errorDetail);
+ updateObj.setFinishTime(LocalDateTime.now());
+ }
+
+ taskMapper.updateById(updateObj);
+ log.info("[updateTaskStatus][任务({})状态更新: {}]", taskId, updateObj);
+ }
+
+ /**
+ * 更新任务进度
+ */
+ private void updateTaskProgress(Long taskId, DigitalHumanTaskStepEnum step, String message) {
+ updateTaskStatus(taskId, "PROCESSING", step.getStep(), step.getProgress(), message, null);
+ }
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/TikUserVoiceServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/TikUserVoiceServiceImpl.java
index 864b66d98c..43f4c9d561 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/TikUserVoiceServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/TikUserVoiceServiceImpl.java
@@ -54,7 +54,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
/**
* 用户配音 Service 实现类
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java
new file mode 100644
index 0000000000..ad26191c81
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanCreateReqVO.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.tik.voice.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.DecimalMax;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 创建数字人任务请求 VO
+ *
+ * @author 芋道源码
+ */
+@Data
+@Schema(description = "创建数字人任务请求")
+public class AppTikDigitalHumanCreateReqVO {
+
+ @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我的数字人视频")
+ @NotBlank(message = "任务名称不能为空")
+ @Size(max = 128, message = "任务名称不能超过128个字符")
+ private String taskName;
+
+ @Schema(description = "AI供应商(默认302ai)", example = "302ai", allowableValues = {"302ai", "aliyun", "openai", "minimax"})
+ private String aiProvider;
+
+ @Schema(description = "音频文件ID(tik_user_file.id),与audioUrl二选一", example = "123")
+ private Long audioFileId;
+
+ @Schema(description = "音频文件URL(公网可访问),与audioFileId二选一", example = "https://example.com/audio.wav")
+ @Size(max = 1024, message = "音频URL不能超过1024个字符")
+ private String audioUrl;
+
+ @Schema(description = "视频文件ID(tik_user_file.id),与videoUrl二选一", example = "456")
+ private Long videoFileId;
+
+ @Schema(description = "视频文件URL(公网可访问),与videoFileId二选一", example = "https://example.com/video.mp4")
+ @Size(max = 1024, message = "视频URL不能超过1024个字符")
+ private String videoUrl;
+
+ @Schema(description = "配音配置ID(tik_user_voice.id)", example = "789")
+ private Long voiceConfigId;
+
+ @Schema(description = "语速(0.5-2.0,默认1.0)", example = "1.0")
+ @DecimalMin(value = "0.5", message = "语速不能小于0.5")
+ @DecimalMax(value = "2.0", message = "语速不能大于2.0")
+ private Float speechRate;
+
+ @Schema(description = "音量(-10到10,默认0)", example = "0")
+ @DecimalMin(value = "-10", message = "音量不能小于-10")
+ @DecimalMax(value = "10", message = "音量不能大于10")
+ private Float volume;
+
+ @Schema(description = "情感(默认neutral)", example = "neutral")
+ private String emotion;
+
+ @Schema(description = "Latentsync guidance_scale(1-2,默认1)", example = "1")
+ @Min(value = 1, message = "guidanceScale不能小于1")
+ @Max(value = 2, message = "guidanceScale不能大于2")
+ private Integer guidanceScale;
+
+ @Schema(description = "随机种子(默认8888)", example = "8888")
+ private Integer seed;
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanPageReqVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanPageReqVO.java
new file mode 100644
index 0000000000..ee9d9b6729
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanPageReqVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.tik.voice.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 数字人任务分页查询 VO
+ *
+ * @author 芋道源码
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "数字人任务分页查询")
+public class AppTikDigitalHumanPageReqVO extends PageParam {
+
+ @Schema(description = "用户编号(自动填充,无需传递)")
+ private Long userId;
+
+ @Schema(description = "任务名称(模糊搜索)", example = "数字人")
+ private String taskName;
+
+ @Schema(description = "任务状态", example = "PROCESSING")
+ private String status;
+
+ @Schema(description = "创建时间-开始", example = "2024-11-19 00:00:00")
+ private LocalDateTime createTimeStart;
+
+ @Schema(description = "创建时间-结束", example = "2024-11-19 23:59:59")
+ private LocalDateTime createTimeEnd;
+
+}
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanRespVO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanRespVO.java
new file mode 100644
index 0000000000..2739f1a696
--- /dev/null
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/vo/AppTikDigitalHumanRespVO.java
@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.module.tik.voice.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 数字人任务响应 VO
+ *
+ * @author 芋道源码
+ */
+@Data
+@Schema(description = "数字人任务响应")
+public class AppTikDigitalHumanRespVO {
+
+ @Schema(description = "任务ID", example = "12345")
+ private Long id;
+
+ @Schema(description = "用户ID", example = "67890")
+ private Long userId;
+
+ @Schema(description = "AI供应商", example = "302ai")
+ private String aiProvider;
+
+ @Schema(description = "任务名称", example = "我的数字人视频")
+ private String taskName;
+
+ // ========== 文件信息 ==========
+ @Schema(description = "音频文件URL(公网可访问)", example = "https://example.com/audio.wav")
+ private String audioUrl;
+
+ @Schema(description = "视频文件URL(公网可访问)", example = "https://example.com/video.mp4")
+ private String videoUrl;
+
+ // ========== 生成参数 ==========
+ @Schema(description = "配音配置ID", example = "789")
+ private Long voiceConfigId;
+
+ @Schema(description = "voice_id", example = "cosyvoice-v2-xxx")
+ private String voiceId;
+
+ @Schema(description = "语速", example = "1.0")
+ private Float speechRate;
+
+ @Schema(description = "音量", example = "0")
+ private Float volume;
+
+ @Schema(description = "情感", example = "neutral")
+ private String emotion;
+
+ @Schema(description = "guidance_scale", example = "1")
+ private Integer guidanceScale;
+
+ @Schema(description = "随机种子", example = "8888")
+ private Integer seed;
+
+ // ========== 任务状态 ==========
+ @Schema(description = "任务状态", example = "PROCESSING")
+ private String status;
+
+ @Schema(description = "状态描述", example = "处理中")
+ private String statusDesc;
+
+ @Schema(description = "进度百分比(0-100)", example = "45")
+ private Integer progress;
+
+ @Schema(description = "当前步骤", example = "synthesize_voice")
+ private String currentStep;
+
+ @Schema(description = "当前步骤描述", example = "语音合成")
+ private String currentStepDesc;
+
+ // ========== 结果信息 ==========
+ @Schema(description = "生成结果视频URL", example = "https://example.com/result.mp4")
+ private String resultVideoUrl;
+
+ @Schema(description = "错误信息", example = "任务处理失败")
+ private String errorMessage;
+
+ @Schema(description = "错误详情", example = "详细错误堆栈...")
+ private String errorDetail;
+
+ // ========== 时间戳 ==========
+ @Schema(description = "创建时间", example = "2024-11-19 10:00:00")
+ private LocalDateTime createTime;
+
+ @Schema(description = "更新时间", example = "2024-11-19 10:05:30")
+ private LocalDateTime updateTime;
+
+ @Schema(description = "任务开始时间", example = "2024-11-19 10:00:05")
+ private LocalDateTime startTime;
+
+ @Schema(description = "任务完成时间", example = "2024-11-19 10:05:30")
+ private LocalDateTime finishTime;
+
+}