feat: 功能优化
This commit is contained in:
@@ -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, "文件分类无效");
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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 客户端
|
||||
|
||||
@@ -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 客户端
|
||||
|
||||
@@ -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<Long> createTask(@Valid @RequestBody AppTikDigitalHumanCreateReqVO reqVO) {
|
||||
Long taskId = digitalHumanTaskService.createTask(reqVO);
|
||||
return CommonResult.success(taskId);
|
||||
}
|
||||
|
||||
@GetMapping("/task/get")
|
||||
@Operation(summary = "查询任务详情")
|
||||
public CommonResult<AppTikDigitalHumanRespVO> 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<PageResult<AppTikDigitalHumanRespVO>> getTaskPage(@ModelAttribute AppTikDigitalHumanPageReqVO pageReqVO) {
|
||||
PageResult<AppTikDigitalHumanRespVO> result = digitalHumanTaskService.getTaskPage(pageReqVO);
|
||||
return CommonResult.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/task/statistics")
|
||||
@Operation(summary = "查询任务统计")
|
||||
public CommonResult<DigitalHumanTaskService.TaskStatisticsVO> getTaskStatistics() {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
DigitalHumanTaskService.TaskStatisticsVO statistics = digitalHumanTaskService.getTaskStatistics(userId);
|
||||
return CommonResult.success(statistics);
|
||||
}
|
||||
|
||||
@PostMapping("/task/{taskId}/cancel")
|
||||
@Operation(summary = "取消任务")
|
||||
public CommonResult<Boolean> 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<Boolean> 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<Boolean> deleteTask(
|
||||
@Parameter(description = "任务ID", required = true, example = "12345")
|
||||
@PathVariable Long taskId) {
|
||||
digitalHumanTaskService.deleteTask(taskId);
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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<TikDigitalHumanTaskDO> {
|
||||
|
||||
/**
|
||||
* 分页查询用户的任务列表(简单版本,仅按userId过滤)
|
||||
*/
|
||||
default PageResult<TikDigitalHumanTaskDO> selectPage(PageParam pageParam, Long userId) {
|
||||
return selectPage(pageParam, new LambdaQueryWrapperX<TikDigitalHumanTaskDO>()
|
||||
.eqIfPresent(TikDigitalHumanTaskDO::getUserId, userId)
|
||||
.orderByDesc(TikDigitalHumanTaskDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询用户的任务列表(完整版本,支持更多过滤条件)
|
||||
*/
|
||||
default PageResult<TikDigitalHumanTaskDO> selectPage(PageParam pageParam, LambdaQueryWrapperX<TikDigitalHumanTaskDO> queryWrapper) {
|
||||
return BaseMapperX.super.selectPage(pageParam, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户进行中的任务数量
|
||||
*/
|
||||
default Long selectProcessingCount(Long userId) {
|
||||
return selectCount(new LambdaQueryWrapperX<TikDigitalHumanTaskDO>()
|
||||
.eq(TikDigitalHumanTaskDO::getUserId, userId)
|
||||
.eq(TikDigitalHumanTaskDO::getStatus, "PROCESSING"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户已完成的任务数量
|
||||
*/
|
||||
default Long selectCompletedCount(Long userId) {
|
||||
return selectCount(new LambdaQueryWrapperX<TikDigitalHumanTaskDO>()
|
||||
.eq(TikDigitalHumanTaskDO::getUserId, userId)
|
||||
.in(TikDigitalHumanTaskDO::getStatus, "SUCCESS", "FAILED", "CANCELED"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户的最新任务列表
|
||||
*/
|
||||
default List<TikDigitalHumanTaskDO> selectRecentTasks(Long userId, Integer limit) {
|
||||
return selectList(new LambdaQueryWrapperX<TikDigitalHumanTaskDO>()
|
||||
.eq(TikDigitalHumanTaskDO::getUserId, userId)
|
||||
.orderByDesc(TikDigitalHumanTaskDO::getCreateTime)
|
||||
.last("LIMIT " + limit));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<AppTikDigitalHumanRespVO> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<AppTikDigitalHumanRespVO> getTaskPage(AppTikDigitalHumanPageReqVO pageReqVO) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
pageReqVO.setUserId(userId);
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapperX<TikDigitalHumanTaskDO> queryWrapper = new LambdaQueryWrapperX<TikDigitalHumanTaskDO>()
|
||||
.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<TikDigitalHumanTaskDO> 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<TikDigitalHumanTaskDO>()
|
||||
.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<TikUserFileDO>()
|
||||
.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 实现类
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user