Commit a1f738dd81dac8149a0004cc1003c76bb3236297
1 parent
782f1085
【增加】增加 midjourney 提交任务
Showing
6 changed files
with
127 additions
and
69 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/client/vo/MidjourneySubmitCodeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.client.vo; | |
| 2 | + | |
| 3 | +import com.google.common.collect.Lists; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Getter; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * Midjourney 提交任务 code 枚举 | |
| 11 | + * | |
| 12 | + * @author fansili | |
| 13 | + * @time 2024/5/30 14:33 | |
| 14 | + * @since 1.0 | |
| 15 | + */ | |
| 16 | +@Getter | |
| 17 | +@AllArgsConstructor | |
| 18 | +public enum MidjourneySubmitCodeEnum { | |
| 19 | + | |
| 20 | + // 状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误) | |
| 21 | + SUBMIT_SUCCESS("1", "提交成功"), | |
| 22 | + ALREADY_EXISTS("1", "已存在"), | |
| 23 | + QUEUING("22", "排队中"), | |
| 24 | + | |
| 25 | + ; | |
| 26 | + | |
| 27 | + public static final List<String> SUCCESS_CODES = Lists.newArrayList( | |
| 28 | + SUBMIT_SUCCESS.code, | |
| 29 | + ALREADY_EXISTS.code, | |
| 30 | + QUEUING.code | |
| 31 | + ); | |
| 32 | + | |
| 33 | + private String code; | |
| 34 | + private String name; | |
| 35 | + | |
| 36 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/client/vo/MidjourneySubmitRespVO.java
| ... | ... | @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.ai.client.vo; |
| 3 | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | 4 | import lombok.Data; |
| 5 | 5 | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 6 | 8 | /** |
| 7 | 9 | * Midjourney:Imagine 请求 |
| 8 | 10 | * |
| ... | ... | @@ -20,7 +22,7 @@ public class MidjourneySubmitRespVO { |
| 20 | 22 | private String description; |
| 21 | 23 | |
| 22 | 24 | @Schema(description = "扩展字段") |
| 23 | - private String properties; | |
| 25 | + private Map<String, Object> properties; | |
| 24 | 26 | |
| 25 | 27 | @Schema(description = "任务ID") |
| 26 | 28 | private String result; | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
| ... | ... | @@ -3,13 +3,17 @@ package cn.iocoder.yudao.module.ai.controller.admin.image; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 6 | -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*; | |
| 6 | +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDallReqVO; | |
| 7 | +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageListReqVO; | |
| 8 | +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyImagineReqVO; | |
| 9 | +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageMyRespVO; | |
| 7 | 10 | import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; |
| 8 | 11 | import cn.iocoder.yudao.module.ai.service.image.AiImageService; |
| 9 | 12 | import io.swagger.v3.oas.annotations.Operation; |
| 10 | 13 | import io.swagger.v3.oas.annotations.Parameter; |
| 11 | 14 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 12 | 15 | import jakarta.annotation.Resource; |
| 16 | +import jakarta.servlet.http.HttpServletRequest; | |
| 13 | 17 | import lombok.extern.slf4j.Slf4j; |
| 14 | 18 | import org.springframework.validation.annotation.Validated; |
| 15 | 19 | import org.springframework.web.bind.annotation.*; |
| ... | ... | @@ -49,32 +53,17 @@ public class AiImageController { |
| 49 | 53 | } |
| 50 | 54 | |
| 51 | 55 | // TODO @fan:建议把 dallDrawing、midjourney 融合成一个 draw 接口,异步绘制;然后返回一个 id 给前端;前端通过 get 接口轮询,直到获取到生成成功 |
| 56 | + // TODO @芋艿: 参数差异较大 | |
| 52 | 57 | @Operation(summary = "dall2/dall3绘画", description = "openAi dall3是付费的!") |
| 53 | 58 | @PostMapping("/dall") |
| 54 | 59 | public CommonResult<Long> dall(@Validated @RequestBody AiImageDallReqVO req) { |
| 55 | 60 | return success(aiImageService.dall(getLoginUserId(), req)); |
| 56 | 61 | } |
| 57 | 62 | |
| 58 | - @Operation(summary = "midjourney绘画", description = "midjourney图片绘画流程:1、提交任务 2、获取完成的任务 3、选择对应功能 4、获取最终结果") | |
| 59 | - @PostMapping("/midjourney") | |
| 60 | - public CommonResult<Void> midjourney(@Validated @RequestBody AiImageMidjourneyReqVO req) { | |
| 61 | - aiImageService.midjourney(req); | |
| 62 | - return success(null); | |
| 63 | - } | |
| 64 | - | |
| 65 | - @Operation(summary = "midjourney绘画操作", description = "一般有选择图片、放大、换一批...") | |
| 66 | - @PostMapping("/midjourney-operate") | |
| 67 | - public CommonResult<Void> midjourneyOperate(@Validated @RequestBody AiImageMidjourneyOperateReqVO req) { | |
| 68 | - aiImageService.midjourneyOperate(req); | |
| 69 | - return success(null); | |
| 70 | - } | |
| 71 | - | |
| 72 | - // TODO @fan:要不先不要 midjourneyOperate、cancelMidjourney 接口哈 | |
| 73 | - @Operation(summary = "取消 midjourney 绘画", description = "取消 midjourney 绘画") | |
| 74 | - @PostMapping("/cancel-midjourney") | |
| 75 | - public CommonResult<Void> cancelMidjourney(@RequestParam("id") Long id) { | |
| 76 | - // @范 这里实现mj取消逻辑 | |
| 77 | - return success(null); | |
| 63 | + @Operation(summary = "midjourney-imagine 绘画", description = "...") | |
| 64 | + @PostMapping("/midjourney/imagine") | |
| 65 | + public CommonResult<Long> midjourneyImagine(@Validated @RequestBody AiImageMidjourneyImagineReqVO req) { | |
| 66 | + return success(aiImageService.midjourneyImagine(getLoginUserId(), req)); | |
| 78 | 67 | } |
| 79 | 68 | |
| 80 | 69 | @Operation(summary = "删除【我的】绘画记录") |
| ... | ... | @@ -83,4 +72,10 @@ public class AiImageController { |
| 83 | 72 | public CommonResult<Boolean> deleteIdMy(@RequestParam("id") Long id) { |
| 84 | 73 | return success(aiImageService.deleteIdMy(id, getLoginUserId())); |
| 85 | 74 | } |
| 75 | + | |
| 76 | + @Operation(summary = "删除【我的】绘画记录") | |
| 77 | + @RequestMapping("/midjourney-notify") | |
| 78 | + public CommonResult<Boolean> midjourneyNotify(HttpServletRequest request) { | |
| 79 | + return success(true); | |
| 80 | + } | |
| 86 | 81 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageMidjourneyReqVO.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageMidjourneyImagineReqVO.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.controller.admin.image.vo; |
| 2 | 2 | |
| 3 | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | import lombok.experimental.Accessors; |
| 6 | 7 | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 7 | 10 | /** |
| 8 | 11 | * midjourney req |
| 9 | 12 | * |
| ... | ... | @@ -13,17 +16,15 @@ import lombok.experimental.Accessors; |
| 13 | 16 | */ |
| 14 | 17 | @Data |
| 15 | 18 | @Accessors(chain = true) |
| 16 | -public class AiImageMidjourneyReqVO { | |
| 19 | +public class AiImageMidjourneyImagineReqVO { | |
| 17 | 20 | |
| 18 | 21 | @Schema(description = "提示词") |
| 22 | + @NotNull(message = "提示词不能为空!") | |
| 19 | 23 | private String prompt; |
| 20 | 24 | |
| 21 | - @Schema(description = "绘画比例 1:1、3:4、4:3、9:16、16:9") | |
| 22 | - private String size; | |
| 23 | - | |
| 24 | - @Schema(description = "风格") | |
| 25 | - private String style; | |
| 25 | + @Schema(description = "模型(midjourney、niji)") | |
| 26 | + private String model; | |
| 26 | 27 | |
| 27 | - @Schema(description = "参考图") | |
| 28 | - private String referImage; | |
| 28 | + @Schema(description = "垫图(参考图)base64数组") | |
| 29 | + private List<String> base64Array; | |
| 29 | 30 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
| ... | ... | @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.ai.service.image; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDallReqVO; |
| 5 | 5 | import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageListReqVO; |
| 6 | +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyImagineReqVO; | |
| 6 | 7 | import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyOperateReqVO; |
| 7 | -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyReqVO; | |
| 8 | 8 | import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; |
| 9 | 9 | |
| 10 | 10 | /** |
| ... | ... | @@ -44,10 +44,11 @@ public interface AiImageService { |
| 44 | 44 | /** |
| 45 | 45 | * midjourney 图片生成 |
| 46 | 46 | * |
| 47 | + * @param loginUserId | |
| 47 | 48 | * @param req |
| 48 | 49 | * @return |
| 49 | 50 | */ |
| 50 | - void midjourney(AiImageMidjourneyReqVO req); | |
| 51 | + Long midjourneyImagine(Long loginUserId, AiImageMidjourneyImagineReqVO req); | |
| 51 | 52 | |
| 52 | 53 | /** |
| 53 | 54 | * midjourney 操作(u1、u2、放大、换一批...) | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.service.image; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.util.IdUtil; | |
| 4 | 3 | import cn.hutool.http.HttpUtil; |
| 4 | +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | |
| 5 | 5 | import cn.iocoder.yudao.framework.ai.core.enums.OpenAiImageModelEnum; |
| 6 | 6 | import cn.iocoder.yudao.framework.ai.core.enums.OpenAiImageStyleEnum; |
| 7 | 7 | import cn.iocoder.yudao.framework.ai.core.exception.AiException; |
| ... | ... | @@ -11,6 +11,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 11 | 11 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 12 | 12 | import cn.iocoder.yudao.module.ai.AiCommonConstants; |
| 13 | 13 | import cn.iocoder.yudao.module.ai.ErrorCodeConstants; |
| 14 | +import cn.iocoder.yudao.module.ai.client.MidjourneyProxyClient; | |
| 15 | +import cn.iocoder.yudao.module.ai.client.vo.MidjourneyImagineReqVO; | |
| 16 | +import cn.iocoder.yudao.module.ai.client.vo.MidjourneySubmitCodeEnum; | |
| 17 | +import cn.iocoder.yudao.module.ai.client.vo.MidjourneySubmitRespVO; | |
| 14 | 18 | import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*; |
| 15 | 19 | import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; |
| 16 | 20 | import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; |
| ... | ... | @@ -18,21 +22,22 @@ import cn.iocoder.yudao.module.ai.enums.AiImagePublicStatusEnum; |
| 18 | 22 | import cn.iocoder.yudao.module.ai.enums.AiImageStatusEnum; |
| 19 | 23 | import cn.iocoder.yudao.module.infra.api.file.FileApi; |
| 20 | 24 | import com.google.common.collect.ImmutableMap; |
| 21 | -import jakarta.annotation.PostConstruct; | |
| 22 | 25 | import jakarta.annotation.Resource; |
| 23 | 26 | import lombok.extern.slf4j.Slf4j; |
| 24 | 27 | import org.springframework.ai.image.ImageGeneration; |
| 25 | 28 | import org.springframework.ai.image.ImagePrompt; |
| 26 | 29 | import org.springframework.ai.image.ImageResponse; |
| 27 | -import org.springframework.ai.models.midjourney.api.MidjourneyInteractionsApi; | |
| 28 | -import org.springframework.ai.models.midjourney.webSocket.MidjourneyWebSocketStarter; | |
| 29 | 30 | import org.springframework.ai.openai.OpenAiImageClient; |
| 30 | 31 | import org.springframework.ai.openai.OpenAiImageOptions; |
| 32 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 33 | +import org.springframework.beans.factory.annotation.Value; | |
| 31 | 34 | import org.springframework.scheduling.annotation.Async; |
| 32 | 35 | import org.springframework.stereotype.Service; |
| 33 | 36 | import org.springframework.transaction.annotation.Transactional; |
| 34 | 37 | |
| 38 | +import java.util.HashMap; | |
| 35 | 39 | import java.util.List; |
| 40 | +import java.util.Map; | |
| 36 | 41 | |
| 37 | 42 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 38 | 43 | |
| ... | ... | @@ -59,28 +64,11 @@ public class AiImageServiceImpl implements AiImageService { |
| 59 | 64 | private FileApi fileApi; |
| 60 | 65 | @Resource |
| 61 | 66 | private OpenAiImageClient openAiImageClient; |
| 62 | - @Resource | |
| 63 | - private MidjourneyWebSocketStarter midjourneyWebSocketStarter; | |
| 64 | - @Resource | |
| 65 | - private MidjourneyInteractionsApi midjourneyInteractionsApi; | |
| 66 | - | |
| 67 | - // TODO @fan:接 mj proxy | |
| 68 | - @PostConstruct | |
| 69 | - public void startMidjourney() { | |
| 70 | - // todo @fan 暂时注释掉 | |
| 71 | -// log.info("midjourney web socket starter..."); | |
| 72 | -// midjourneyWebSocketStarter.start(new WssNotify() { | |
| 73 | -// @Override | |
| 74 | -// public void notify(int code, String message) { | |
| 75 | -// log.info("code: {}, message: {}", code, message); | |
| 76 | -// if (message.contains("Authentication failed")) { | |
| 77 | -// // TODO 芋艿,这里看怎么处理,token无效的时候会认证失败! | |
| 78 | -// // 认证失败 | |
| 79 | -// log.error("midjourney socket 认证失败,检查token是否失效!"); | |
| 80 | -// } | |
| 81 | -// } | |
| 82 | -// }); | |
| 83 | - } | |
| 67 | + @Autowired | |
| 68 | + private MidjourneyProxyClient midjourneyProxyClient; | |
| 69 | + | |
| 70 | + @Value("${ai.midjourney-proxy.notifyUrl:http://127.0.0.1:48080/admin-api/ai/image/midjourney-notify}") | |
| 71 | + private String midjourneyNotifyUrl; | |
| 84 | 72 | |
| 85 | 73 | @Override |
| 86 | 74 | public PageResult<AiImageDO> getImagePageMy(Long loginUserId, AiImageListReqVO req) { |
| ... | ... | @@ -143,18 +131,53 @@ public class AiImageServiceImpl implements AiImageService { |
| 143 | 131 | |
| 144 | 132 | @Override |
| 145 | 133 | @Transactional(rollbackFor = Exception.class) |
| 146 | - public void midjourney(AiImageMidjourneyReqVO req) { | |
| 147 | - // 保存数据库 | |
| 148 | - String messageId = String.valueOf(IdUtil.getSnowflakeNextId()); | |
| 149 | - // todo | |
| 150 | -// AiImageDO aiImageDO = doSave(req.getPrompt(), null, "midjoureny", | |
| 151 | -// null, null, AiImageStatusEnum.SUBMIT, null, | |
| 152 | -// messageId, null, null); | |
| 153 | - // 提交 midjourney 任务 | |
| 154 | - Boolean imagine = midjourneyInteractionsApi.imagine(messageId, req.getPrompt()); | |
| 155 | - if (!imagine) { | |
| 156 | - throw ServiceExceptionUtil.exception(ErrorCodeConstants.AI_MIDJOURNEY_IMAGINE_FAIL); | |
| 134 | + public Long midjourneyImagine(Long loginUserId, AiImageMidjourneyImagineReqVO req) { | |
| 135 | + | |
| 136 | + // 1、构建 AiImageDO | |
| 137 | + AiImageDO aiImageDO = new AiImageDO(); | |
| 138 | + aiImageDO.setId(null); | |
| 139 | + aiImageDO.setUserId(loginUserId); | |
| 140 | + aiImageDO.setPrompt(req.getPrompt()); | |
| 141 | + aiImageDO.setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()); | |
| 142 | + // todo @范 平台需要转换(mj 模型一般分版本) | |
| 143 | + aiImageDO.setModel(null); | |
| 144 | + aiImageDO.setWidth(null); | |
| 145 | + aiImageDO.setHeight(null); | |
| 146 | + aiImageDO.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); | |
| 147 | + aiImageDO.setPublicStatus(AiImagePublicStatusEnum.PRIVATE.getStatus()); | |
| 148 | + aiImageDO.setPicUrl(null); | |
| 149 | + aiImageDO.setOriginalPicUrl(null); | |
| 150 | + aiImageDO.setDrawRequest(null); | |
| 151 | + aiImageDO.setDrawResponse(null); | |
| 152 | + aiImageDO.setErrorMessage(null); | |
| 153 | + | |
| 154 | + // 2、保存 image | |
| 155 | + imageMapper.insert(aiImageDO); | |
| 156 | + | |
| 157 | + // 3、调用 MidjourneyProxy 提交任务 | |
| 158 | + MidjourneyImagineReqVO imagineReqVO = BeanUtils.toBean(req, MidjourneyImagineReqVO.class); | |
| 159 | + imagineReqVO.setNotifyHook(midjourneyNotifyUrl); | |
| 160 | + imagineReqVO.setState(String.valueOf(aiImageDO.getId())); | |
| 161 | + MidjourneySubmitRespVO submitRespVO = midjourneyProxyClient.imagine(imagineReqVO); | |
| 162 | + | |
| 163 | + // 4、保存任务 id (状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)) | |
| 164 | + String updateStatus = null; | |
| 165 | + String errorMessage = null; | |
| 166 | + Map<String, Object> drawResponse = new HashMap<>(); | |
| 167 | + | |
| 168 | + if (!MidjourneySubmitCodeEnum.SUCCESS_CODES.contains(submitRespVO.getCode())) { | |
| 169 | + updateStatus = AiImageStatusEnum.FAIL.getStatus(); | |
| 170 | + errorMessage = submitRespVO.getDescription(); | |
| 171 | + } else { | |
| 172 | + drawResponse.put("jobId", submitRespVO.getResult()); | |
| 157 | 173 | } |
| 174 | + imageMapper.updateById(new AiImageDO() | |
| 175 | + .setId(aiImageDO.getId()) | |
| 176 | + .setStatus(updateStatus) | |
| 177 | + .setErrorMessage(errorMessage) | |
| 178 | + .setDrawResponse(drawResponse) | |
| 179 | + ); | |
| 180 | + return aiImageDO.getId(); | |
| 158 | 181 | } |
| 159 | 182 | |
| 160 | 183 | @Transactional(rollbackFor = Exception.class) | ... | ... |