|
|
|
@ -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.MiniAiTaskMedia; |
|
|
|
import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; |
|
|
|
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.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.mini.service.AiGenerationService; |
|
|
|
import com.youlai.boot.mini.service.MiniPointRecordService; |
|
|
|
@ -58,31 +62,41 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
private final AliyunFileService aliyunFileService; |
|
|
|
private final MiniPointRecordService pointRecordService; |
|
|
|
|
|
|
|
/** |
|
|
|
* 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存储目录配置 |
|
|
|
*/ |
|
|
|
//OSS存储目录配置
|
|
|
|
private static final String OSS_IMAGE_DIR = "ai/image/"; |
|
|
|
private static final String OSS_VIDEO_DIR = "ai/video/"; |
|
|
|
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"; |
|
|
|
//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 |
|
|
|
public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) { |
|
|
|
@ -236,7 +250,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
|
|
|
|
// 组装第三方AI接口需要的参数
|
|
|
|
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("style", form.getStyle()); |
|
|
|
aiRequest.put("ratio", form.getRatio()); |
|
|
|
@ -251,12 +265,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
aiRequest.put("scene_description", form.getSceneDescription()); |
|
|
|
// 传递任务UUID和回调地址
|
|
|
|
aiRequest.put("uuid", taskUuid); |
|
|
|
aiRequest.put("callback_url", aiCallbackUrl); |
|
|
|
aiRequest.put("callback_url", aiSingleImageCallbackUrl); |
|
|
|
|
|
|
|
try { |
|
|
|
log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); |
|
|
|
// 同步调用AI接口,超时1秒
|
|
|
|
HttpResponse response = HttpRequest.post(aiGenerateServerUrl) |
|
|
|
HttpResponse response = HttpRequest.post(aiSingleImageServerUrl) |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.body(JSONUtil.toJsonStr(aiRequest)) |
|
|
|
.timeout(1000) |
|
|
|
@ -291,6 +305,244 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
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 |
|
|
|
public boolean handleTaskCallback(AiTaskCallbackVO vo) { |
|
|
|
log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid()); |
|
|
|
@ -375,6 +627,13 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
* 下载外部URL到OSS,返回OSS访问地址 |
|
|
|
*/ |
|
|
|
private String downloadExternalUrlToOss(String externalUrl) { |
|
|
|
return downloadExternalUrlToOss(externalUrl, "ai/generate/"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 下载外部URL到指定OSS目录,返回OSS访问地址 |
|
|
|
*/ |
|
|
|
private String downloadExternalUrlToOss(String externalUrl, String dir) { |
|
|
|
try ( |
|
|
|
// 直接打开URL输入流,自动关闭
|
|
|
|
InputStream inputStream = new URL(externalUrl).openStream() |
|
|
|
@ -401,7 +660,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
ext = "jpg"; |
|
|
|
} |
|
|
|
|
|
|
|
String objectName = "ai/generate/" |
|
|
|
String objectName = dir |
|
|
|
+ currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) |
|
|
|
+ "." |
|
|
|
+ ext; |
|
|
|
@ -411,4 +670,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { |
|
|
|
throw new MsgException("资源转存失败"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|