Browse Source

增加视频生成任务接口

glx
glx 2 weeks ago
parent
commit
e5489fe290
  1. 29
      src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java
  2. 4
      src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java
  3. 54
      src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java
  4. 28
      src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java
  5. 28
      src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java
  6. 19
      src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java
  7. 61
      src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java
  8. 23
      src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java
  9. 27
      src/main/java/com/youlai/boot/mini/service/AiGenerationService.java
  10. 311
      src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java
  11. 13
      src/main/resources/application-dev.yml

29
src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java

@ -6,8 +6,11 @@ import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.framework.security.util.SecurityUtils; import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm;
import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm;
import com.youlai.boot.mini.model.form.AiVideoGenerateForm;
import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO;
import com.youlai.boot.mini.model.vo.AiVideoCallbackVO;
import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.AiGenerationService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -63,9 +66,31 @@ public class AiGenerationController {
return Result.success(success); return Result.success(success);
} }
@Operation(summary = "提交四宫格漫画生成任务")
@PostMapping("/generate-four-panel")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT)
public Result<String> generateFourPanelImage(@Valid @RequestBody AiFourPanelGenerateForm form) {
Long userId = SecurityUtils.getUserId();
String taskUuid = aiGenerationService.createAndGenerateFourPanel(form, userId);
return Result.success(taskUuid);
}
// TODO: 添加 4宫格图片生成任务 接口 ,外部服务接口为:http://127.0.0.1:8001//api/v1/four-panel-comic @Operation(summary = "提交视频生成任务")
@PostMapping("/generate-video")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT)
public Result<String> generateVideo(@Valid @RequestBody AiVideoGenerateForm form) {
Long userId = SecurityUtils.getUserId();
String taskUuid = aiGenerationService.createAndGenerateVideo(form, userId);
return Result.success(taskUuid);
}
//TODO: 后续增加用户小程序订阅通知
@Operation(summary = "视频任务回调接口")
@PostMapping("/video/task/callback")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE)
public Result<Boolean> videoTaskCallback(@Valid @RequestBody AiVideoCallbackVO vo) {
boolean success = aiGenerationService.handleVideoTaskCallback(vo);
return Result.success(success);
}
} }

4
src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java

@ -40,6 +40,10 @@ public class MiniAiGenerationTask implements Serializable {
@Schema(description = "生成参数JSON,不同类型参数不同") @Schema(description = "生成参数JSON,不同类型参数不同")
private String generateParams; private String generateParams;
@TableField("video_task_uuid")
@Schema(description = "调用视频接口时返回")
private String videoTaskUuid;
@TableField("status") @TableField("status")
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消")
private Integer status; private Integer status;

54
src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java

@ -0,0 +1,54 @@
package com.youlai.boot.mini.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* AI四宫格漫画生成请求表单
*
* @author youlai
*/
@Data
@Schema(description = "AI四宫格漫画生成请求表单")
public class AiFourPanelGenerateForm {
@Schema(description = "参考图片URL列表,可选")
private List<String> referenceImage;
@Schema(description = "使用的模型ID", defaultValue = "doubao-seedream-5-0-260128")
private String model;
@Schema(description = "画风:1=治愈系水彩,2=Q版卡通,3=日式漫画,4=吉卜力风,5=3D毛绒,6=搞笑夸张", defaultValue = "1")
private Integer style = 1;
@Schema(description = "比例:1=1:1,2=2:3,3=3:4,4=4:3,5=5:4", defaultValue = "1")
private Integer ratio = 1;
@Schema(description = "图片尺寸:1=2K,2=4K,3=8K", defaultValue = "1")
private Integer imgSize = 1;
@Schema(description = "图片格式:1=png,2=jpeg", defaultValue = "1")
private Integer imgType = 1;
@Schema(description = "宠物物种:如cat、dog")
private String species;
@Schema(description = "宠物品种:如British Shorthair、Corgi")
private String breed;
@Schema(description = "宠物毛色:如blue-gray、orange")
private String color;
@Schema(description = "眼睛颜色:如amber、blue")
private String eyeColor;
@Schema(description = "体型:如round chubby、slim and elegant")
private String bodyType;
@Schema(description = "特殊特征:如flat face、thick coat", defaultValue = "")
private String distinctiveFeatures = "";
@Schema(description = "故事梗概:AI会据此生成四格漫画脚本")
private String storyOutline;
}

28
src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java

@ -0,0 +1,28 @@
package com.youlai.boot.mini.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* AI视频生成请求表单
*
* @author youlai
*/
@Data
@Schema(description = "AI视频生成请求表单")
public class AiVideoGenerateForm {
@Schema(description = "使用的模型ID", defaultValue = "doubao-seedance-2-0-260128")
private String model;
@Schema(description = "内容数组,包含text、image_url等类型")
private List<Map<String, Object>> content;
@Schema(description = "分辨率: 480p, 720p, 1080p, 2K", defaultValue = "720p")
private String resolution = "720p";
@Schema(description = "视频时长,单位秒,最大15秒", defaultValue = "5")
private Integer duration = 5;
}

28
src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java

@ -0,0 +1,28 @@
package com.youlai.boot.mini.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* AI视频生成任务回调请求
*
* @author youlai
*/
@Data
@Schema(description = "AI视频生成任务回调请求")
public class AiVideoCallbackVO {
@Schema(description = "响应消息")
private String msg;
@Schema(description = "响应码,0表示成功")
private Integer code;
@Schema(description = "回调数据")
private VideoCallbackData data;
@JsonProperty("request_id")
@Schema(description = "请求ID")
private String requestId;
}

19
src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java

@ -0,0 +1,19 @@
package com.youlai.boot.mini.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* AI视频生成回调结果内容
*
* @author youlai
*/
@Data
@Schema(description = "AI视频生成回调结果内容")
public class VideoCallbackContent {
@JsonProperty("video_url")
@Schema(description = "生成的视频地址")
private String videoUrl;
}

61
src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java

@ -0,0 +1,61 @@
package com.youlai.boot.mini.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* AI视频生成回调数据
*
* @author youlai
*/
@Data
@Schema(description = "AI视频生成回调数据")
public class VideoCallbackData {
@Schema(description = "任务ID")
private String id;
@Schema(description = "使用的模型ID")
private String model;
@Schema(description = "任务状态: pending/running/succeeded/failed")
private String status;
@Schema(description = "生成的结果内容")
private VideoCallbackContent content;
@Schema(description = "资源消耗统计")
private VideoCallbackUsage usage;
@JsonProperty("created_at")
@Schema(description = "创建时间戳")
private Long createdAt;
@JsonProperty("updated_at")
@Schema(description = "更新时间戳")
private Long updatedAt;
@Schema(description = "随机种子")
private Long seed;
@Schema(description = "分辨率")
private String resolution;
@Schema(description = "比例")
private String ratio;
@Schema(description = "视频时长,单位秒")
private Integer duration;
@Schema(description = "帧率")
private Integer framespersecond;
@JsonProperty("service_tier")
@Schema(description = "服务等级")
private String serviceTier;
@JsonProperty("execution_expires_after")
@Schema(description = "执行过期时间")
private Long executionExpiresAfter;
}

23
src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java

@ -0,0 +1,23 @@
package com.youlai.boot.mini.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* AI视频生成资源消耗统计
*
* @author youlai
*/
@Data
@Schema(description = "AI视频生成资源消耗统计")
public class VideoCallbackUsage {
@JsonProperty("completion_tokens")
@Schema(description = "完成token数")
private Integer completionTokens;
@JsonProperty("total_tokens")
@Schema(description = "总token数")
private Integer totalTokens;
}

27
src/main/java/com/youlai/boot/mini/service/AiGenerationService.java

@ -1,38 +1,29 @@
package com.youlai.boot.mini.service; package com.youlai.boot.mini.service;
import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm;
import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm;
import com.youlai.boot.mini.model.form.AiVideoGenerateForm;
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask;
import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO;
import com.youlai.boot.mini.model.vo.AiVideoCallbackVO;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
public interface AiGenerationService { public interface AiGenerationService {
/**
* 上传参考文件图片/视频
* @param images 上传的图片列表
* @param videos 上传的视频列表
* @param userId 用户ID
* @return 文件访问URL列表
*/
List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId); List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId);
/**
* 创建AI生成任务并调用生成接口
* @param form 生成请求表单
* @param userId 用户ID
* @return 任务UUID
*/
String createAndGenerateImage(AiSingleImageGenerateForm form, Long userId); String createAndGenerateImage(AiSingleImageGenerateForm form, Long userId);
/** String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId);
* 处理AI生成任务回调
* @param vo 回调请求参数
* @return 是否处理成功
*/
boolean handleTaskCallback(AiTaskCallbackVO vo); boolean handleTaskCallback(AiTaskCallbackVO vo);
String createAndGenerateVideo(AiVideoGenerateForm form, Long userId);
boolean handleVideoTaskCallback(AiVideoCallbackVO vo);
MiniAiGenerationTask getTaskByUuid(String uuid); MiniAiGenerationTask getTaskByUuid(String uuid);
} }

311
src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java

@ -24,9 +24,13 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper;
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask;
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia;
import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm;
import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm;
import com.youlai.boot.mini.model.form.AiVideoGenerateForm;
import com.youlai.boot.mini.model.form.MiniDeductPointForm; import com.youlai.boot.mini.model.form.MiniDeductPointForm;
import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO;
import com.youlai.boot.mini.model.vo.AiVideoCallbackVO;
import com.youlai.boot.mini.model.vo.VideoCallbackData;
import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.AiGenerationService;
import com.youlai.boot.mini.service.MiniPointRecordService; import com.youlai.boot.mini.service.MiniPointRecordService;
@ -58,31 +62,41 @@ public class AiGenerationServiceImpl implements AiGenerationService {
private final AliyunFileService aliyunFileService; private final AliyunFileService aliyunFileService;
private final MiniPointRecordService pointRecordService; private final MiniPointRecordService pointRecordService;
/** //OSS存储目录配置
* AI生成服务地址
*/
@Value("${ai.generate.server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}")
private String aiGenerateServerUrl;
/**
* AI任务回调地址
*/
@Value("${ai.callback.url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}")
private String aiCallbackUrl;
@Value("${ai.default.model:doubao-seedream-5-0-260128}")
private String aiDefaultModel;
/**
* OSS存储目录配置
*/
private static final String OSS_IMAGE_DIR = "ai/image/"; private static final String OSS_IMAGE_DIR = "ai/image/";
private static final String OSS_VIDEO_DIR = "ai/video/"; private static final String OSS_VIDEO_DIR = "ai/video/";
private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/"; private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/";
/**
* AI单图生成积分规则编码 //AI单图生成服务地址
*/ @Value("${ai.generate.single-image-server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}")
private String aiSingleImageServerUrl;
//AI四宫格图片生成服务地址
@Value("${ai.generate.four-panel-server-url:http://127.0.0.1:8001/api/v1/four-panel-comic}")
private String aiFourPanelServerUrl;
//AI单图/四宫格图片生成任务回调地址
@Value("${ai.callback.single-image-callback-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}")
private String aiSingleImageCallbackUrl;
//视频生成服务地址
@Value("${ai.generate.video-server-url:http://127.0.0.1:8001/api/v1/video/submit}")
private String aiVideoServerUrl;
//视频任务回调地址
@Value("${ai.callback.video-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/video/task/callback}")
private String aiVideoCallbackUrl;
//默认图片生成AI模型
@Value("${ai.default.image-model:doubao-seedream-5-0-260128}")
private String aiDefaultImageModel;
//默认视频生成AI模型
@Value("${ai.default.video-model:doubao-seedance-2-0-260128}")
private String aiDefaultVideoModel;
//AI单图生成积分规则编码
private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE";
//AI四宫格生成积分规则编码
private static final String AI_GENERATE_QUAD_GRID_RULE = "AI_GENERATE_QUAD_GRID";
//AI视频生成积分规则编码
private static final String AI_GENERATE_VIDEO_RULE = "AI_GENERATE_VIDEO";
@Override @Override
public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) { public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) {
@ -236,7 +250,7 @@ public class AiGenerationServiceImpl implements AiGenerationService {
// 组装第三方AI接口需要的参数 // 组装第三方AI接口需要的参数
Map<String, Object> aiRequest = new HashMap<>(); Map<String, Object> aiRequest = new HashMap<>();
aiRequest.put("model", form.getModel() == null ? aiDefaultModel : form.getModel()); aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel());
aiRequest.put("reference_image", form.getImageUrl()); aiRequest.put("reference_image", form.getImageUrl());
aiRequest.put("style", form.getStyle()); aiRequest.put("style", form.getStyle());
aiRequest.put("ratio", form.getRatio()); aiRequest.put("ratio", form.getRatio());
@ -251,12 +265,12 @@ public class AiGenerationServiceImpl implements AiGenerationService {
aiRequest.put("scene_description", form.getSceneDescription()); aiRequest.put("scene_description", form.getSceneDescription());
// 传递任务UUID和回调地址 // 传递任务UUID和回调地址
aiRequest.put("uuid", taskUuid); aiRequest.put("uuid", taskUuid);
aiRequest.put("callback_url", aiCallbackUrl); aiRequest.put("callback_url", aiSingleImageCallbackUrl);
try { try {
log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest));
// 同步调用AI接口,超时1秒 // 同步调用AI接口,超时1秒
HttpResponse response = HttpRequest.post(aiGenerateServerUrl) HttpResponse response = HttpRequest.post(aiSingleImageServerUrl)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(aiRequest)) .body(JSONUtil.toJsonStr(aiRequest))
.timeout(1000) .timeout(1000)
@ -291,6 +305,244 @@ public class AiGenerationServiceImpl implements AiGenerationService {
return taskUuid; return taskUuid;
} }
@Override
@Transactional(rollbackFor = Exception.class) // 所有异常都触发事务回滚
public String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId) {
// 生成任务UUID
String taskUuid = UUID.randomUUID().toString().replace("-", "");
Date now = new Date();
long timestamp = System.currentTimeMillis();
// 扣减积分(自动加入当前事务,失败会一起回滚)
MiniDeductPointForm deductForm = new MiniDeductPointForm();
deductForm.setRuleCode(AI_GENERATE_QUAD_GRID_RULE);
deductForm.setBizId(taskUuid);
Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId());
// 创建生成任务(自动加入当前事务,失败会一起回滚)
MiniAiGenerationTask task = new MiniAiGenerationTask();
task.setUuid(taskUuid)
.setMiniUserId(userId)
.setType("img_grid_4") // 四宫格漫画
.setGenerateParams(JSONUtil.toJsonStr(form))
.setPointsConsumed(Math.abs(deductPoint))
.setStatus(0) // 生成中
.setCreateBy(userId)
.setCreateTime(now)
.setCreateTimestamp(timestamp);
aiGenerationTaskMapper.insert(task);
// 组装第三方AI接口需要的参数
Map<String, Object> aiRequest = new HashMap<>();
aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel());
aiRequest.put("reference_image", form.getReferenceImage());
aiRequest.put("style", form.getStyle());
aiRequest.put("ratio", form.getRatio());
aiRequest.put("img_size", form.getImgSize());
aiRequest.put("img_type", form.getImgType());
aiRequest.put("species", form.getSpecies());
aiRequest.put("breed", form.getBreed());
aiRequest.put("color", form.getColor());
aiRequest.put("eye_color", form.getEyeColor());
aiRequest.put("body_type", form.getBodyType());
aiRequest.put("distinctive_features", form.getDistinctiveFeatures());
aiRequest.put("story_outline", form.getStoryOutline());
// 传递任务UUID和回调地址
aiRequest.put("uuid", taskUuid);
aiRequest.put("callback_url", aiSingleImageCallbackUrl);
try {
log.info("提交四宫格漫画生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest));
// 同步调用AI接口,超时1秒
HttpResponse response = HttpRequest.post(aiFourPanelServerUrl)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(aiRequest))
.timeout(1000)
.execute();
// 先判断HTTP状态码
if (!response.isOk()) {
log.error("四宫格漫画生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body());
throw new MsgException("AI生成服务暂时不可用,请稍后重试");
}
// 解析返回结果,校验业务状态码:只有code=0才代表提交成功
String responseBody = response.body();
JSONObject resJson = JSONUtil.parseObj(responseBody);
Integer code = resJson.getInt("code");
if (!Integer.valueOf(0).equals(code)) {
String errMsg = resJson.getStr("msg", "服务调用失败");
log.error("四宫格漫画生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}",
code, errMsg, resJson.getStr("request_id"), responseBody);
throw new MsgException("AI生成失败:" + errMsg);
}
} catch (JSONException e) {
log.error("四宫格漫画生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e);
throw new MsgException("AI生成服务暂时不可用,请稍后重试");
} catch (Exception e) {
log.error("四宫格漫画生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e);
// 抛出异常触发事务回滚
throw new MsgException("AI生成服务暂时不可用,请稍后重试");
}
return taskUuid;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createAndGenerateVideo(AiVideoGenerateForm form, Long userId) {
// 生成任务UUID
String taskUuid = UUID.randomUUID().toString().replace("-", "");
Date now = new Date();
long timestamp = System.currentTimeMillis();
// 扣减积分
MiniDeductPointForm deductForm = new MiniDeductPointForm();
deductForm.setRuleCode(AI_GENERATE_VIDEO_RULE);
deductForm.setBizId(taskUuid);
Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId());
// 创建生成任务
MiniAiGenerationTask task = new MiniAiGenerationTask();
task.setUuid(taskUuid)
.setMiniUserId(userId)
.setType("video")
.setGenerateParams(JSONUtil.toJsonStr(form))
.setPointsConsumed(Math.abs(deductPoint))
.setStatus(0)
.setCreateBy(userId)
.setCreateTime(now)
.setCreateTimestamp(timestamp);
aiGenerationTaskMapper.insert(task);
// 组装第三方接口参数
Map<String, Object> aiRequest = new HashMap<>();
aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel());//TODO写到上面
aiRequest.put("content", form.getContent());
aiRequest.put("resolution", form.getResolution());
aiRequest.put("duration", form.getDuration());
aiRequest.put("uuid", taskUuid);
aiRequest.put("callback_url", aiVideoCallbackUrl); //TODO 后续独立回调地址
try {
log.info("提交视频生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest));
HttpResponse response = HttpRequest.post(aiVideoServerUrl)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(aiRequest))
.timeout(1000)
.execute();
if (!response.isOk()) {
log.error("视频生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body());
throw new MsgException("AI视频生成服务暂时不可用,请稍后重试");
}
String responseBody = response.body();
JSONObject resJson = JSONUtil.parseObj(responseBody);
Integer code = resJson.getInt("code");
if (!Integer.valueOf(0).equals(code)) {
String errMsg = resJson.getStr("msg", "服务调用失败");
log.error("视频生成任务提交失败,业务错误码:{},错误信息:{},完整响应:{}", code, errMsg, responseBody);
throw new MsgException("AI视频生成失败:" + errMsg);
}
// 提取第三方返回的视频任务uuid,保存到任务表中,后续回调通过这个uuid匹配任务
JSONObject data = resJson.getJSONObject("data");
String videoTaskUuid = data.getStr("uuid");
task.setVideoTaskUuid(videoTaskUuid);
aiGenerationTaskMapper.updateById(task);
log.info("视频任务{}第三方返回uuid:{},已保存", task.getUuid(), videoTaskUuid);
} catch (JSONException e) {
log.error("视频生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e);
throw new MsgException("AI视频生成服务暂时不可用,请稍后重试");
} catch (Exception e) {
log.error("视频生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e);
throw new MsgException("AI视频生成服务暂时不可用,请稍后重试");
}
return taskUuid;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean handleVideoTaskCallback(AiVideoCallbackVO vo) {
log.info("处理AI视频生成任务回调,请求参数:{}", JSONUtil.toJsonStr(vo));
boolean success = false;
try {
// 校验回调响应是否成功
if (!Integer.valueOf(0).equals(vo.getCode())) {
log.error("视频生成任务回调失败,错误信息:{}", vo.getMsg());
return false;
}
VideoCallbackData data = vo.getData();
if (data == null) {
log.error("视频回调数据为空");
return false;
}
String videoTaskUuid = data.getId();
// 根据第三方返回的视频任务uuid查询对应的任务
MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper<MiniAiGenerationTask>()
.eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid)
.eq(MiniAiGenerationTask::getDeleted, false));
if (task == null) {
log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid);
return false;
}
// 转换任务状态
Integer status;
if ("succeeded".equals(data.getStatus())) {
status = 1; // 成功
} else if ("failed".equals(data.getStatus())) {
status = 2; // 失败
} else {
log.info("视频任务{}处于中间状态:{},不处理", task.getUuid(), data.getStatus());
return true; // 中间状态直接返回成功,不更新任务
}
// 更新任务状态
task.setStatus(status);
task.setUpdateTime(new Date());
task.setUpdateTimestamp(System.currentTimeMillis());
aiGenerationTaskMapper.updateById(task);
// 如果生成成功,下载外部视频到OSS
if (status == 1 && data.getContent() != null && data.getContent().getVideoUrl() != null) {
String externalVideoUrl = data.getContent().getVideoUrl();
// 调用下载方法,和图片下载逻辑一致,存储到视频目录
String ossUrl = downloadExternalUrlToOss(externalVideoUrl, OSS_VIDEO_DIR);
task.setResultResourceUrl(ossUrl);
aiGenerationTaskMapper.updateById(task);
// 保存视频媒体记录
MiniAiTaskMedia media = new MiniAiTaskMedia();
String mediaUuid = UUID.randomUUID().toString().replace("-", "");
media.setUuid(mediaUuid)
.setTaskId(task.getId())
.setFileSource("ai_generate")
.setMediaType("video")
.setSourceUrl(ossUrl)
.setDuration(data.getDuration())
.setCreateBy(task.getCreateBy())
.setCreateTimestamp(System.currentTimeMillis())
.setCreateTime(new Date());
aiTaskMediaMapper.insert(media);
}
success = true;
log.info("视频任务{}回调处理完成,状态:{}", task.getUuid(), status);
} catch (Exception e) {
log.error("视频任务回调处理异常,异常信息:{}", e.getMessage(), e);
}
return success;
}
@Override @Override
public boolean handleTaskCallback(AiTaskCallbackVO vo) { public boolean handleTaskCallback(AiTaskCallbackVO vo) {
log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid()); log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid());
@ -375,6 +627,13 @@ public class AiGenerationServiceImpl implements AiGenerationService {
* 下载外部URL到OSS返回OSS访问地址 * 下载外部URL到OSS返回OSS访问地址
*/ */
private String downloadExternalUrlToOss(String externalUrl) { private String downloadExternalUrlToOss(String externalUrl) {
return downloadExternalUrlToOss(externalUrl, "ai/generate/");
}
/**
* 下载外部URL到指定OSS目录返回OSS访问地址
*/
private String downloadExternalUrlToOss(String externalUrl, String dir) {
try ( try (
// 直接打开URL输入流,自动关闭 // 直接打开URL输入流,自动关闭
InputStream inputStream = new URL(externalUrl).openStream() InputStream inputStream = new URL(externalUrl).openStream()
@ -401,7 +660,7 @@ public class AiGenerationServiceImpl implements AiGenerationService {
ext = "jpg"; ext = "jpg";
} }
String objectName = "ai/generate/" String objectName = dir
+ currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) + currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8)
+ "." + "."
+ ext; + ext;
@ -411,4 +670,6 @@ public class AiGenerationServiceImpl implements AiGenerationService {
throw new MsgException("资源转存失败"); throw new MsgException("资源转存失败");
} }
} }
} }

13
src/main/resources/application-dev.yml

@ -92,6 +92,7 @@ security:
- /api/v1/mini/homePage/listByBounds - /api/v1/mini/homePage/listByBounds
- /healthcheck - /healthcheck
- /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调
- /api/v1/mini/ai/generation/video/task/callback # AIGeneration 单图任务回调
# 非安全端点路径,完全绕过 Spring Security 的过滤器 # 非安全端点路径,完全绕过 Spring Security 的过滤器
unsecured-urls: unsecured-urls:
- ${springdoc.swagger-ui.path} - ${springdoc.swagger-ui.path}
@ -227,11 +228,15 @@ wx:
appid: Your_AppId appid: Your_AppId
secret: Your_AppSecret secret: Your_AppSecret
# AIGeneration 配置
ai: ai:
generate: generate:
server-url: http://192.168.31.91:8001/api/v1/photo-to-comic single-image-server-url: http://192.168.31.91:8001/api/v1/photo-to-comic
four-panel-server-url: http://192.168.31.91:8001/api/v1/four-panel-comic
video-server-url: http://192.168.31.91:8001/api/v1/video/submit
callback: callback:
url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback
video-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/video/task/callback
default: default:
model: doubao-seedream-5-0-260128 image-model: doubao-seedream-5-0-260128
video-model: doubao-seedance-2-0-260128

Loading…
Cancel
Save