Commit 9d6b615f10cc644a8de61a68e74abcc69998d07c
1 parent
671c3016
【功能新增】AI:音乐管理 50%
Showing
19 changed files
with
326 additions
and
43 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
| ... | ... | @@ -29,13 +29,16 @@ public interface ErrorCodeConstants { |
| 29 | 29 | |
| 30 | 30 | // ========== API 聊天消息 1-040-004-000 ========== |
| 31 | 31 | |
| 32 | - ErrorCode AI_CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!"); | |
| 33 | - ErrorCode AI_CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!"); | |
| 32 | + ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!"); | |
| 33 | + ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!"); | |
| 34 | 34 | |
| 35 | 35 | // ========== API 绘画 1-040-005-000 ========== |
| 36 | 36 | |
| 37 | - ErrorCode AI_IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); | |
| 38 | - ErrorCode AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); | |
| 39 | - ErrorCode AI_IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); | |
| 37 | + ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); | |
| 38 | + ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); | |
| 39 | + ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); | |
| 40 | + | |
| 41 | + // ========== API 音乐 1-040-006-000 ========== | |
| 42 | + ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); | |
| 40 | 43 | |
| 41 | 44 | } | ... | ... |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.enums.music; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; | |
| 3 | 4 | import lombok.AllArgsConstructor; |
| 4 | 5 | import lombok.Getter; |
| 5 | 6 | |
| 7 | +import java.util.Arrays; | |
| 8 | + | |
| 6 | 9 | /** |
| 7 | 10 | * AI 音乐状态的枚举 |
| 8 | 11 | * |
| ... | ... | @@ -10,7 +13,7 @@ import lombok.Getter; |
| 10 | 13 | */ |
| 11 | 14 | @AllArgsConstructor |
| 12 | 15 | @Getter |
| 13 | -public enum AiMusicGenerateModeEnum { | |
| 16 | +public enum AiMusicGenerateModeEnum implements IntArrayValuable { | |
| 14 | 17 | |
| 15 | 18 | LYRIC(1, "歌词模式"), |
| 16 | 19 | DESCRIPTION(2, "描述模式"); |
| ... | ... | @@ -24,4 +27,11 @@ public enum AiMusicGenerateModeEnum { |
| 24 | 27 | */ |
| 25 | 28 | private final String name; |
| 26 | 29 | |
| 30 | + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicGenerateModeEnum::getMode).toArray(); | |
| 31 | + | |
| 32 | + @Override | |
| 33 | + public int[] array() { | |
| 34 | + return ARRAYS; | |
| 35 | + } | |
| 36 | + | |
| 27 | 37 | } | ... | ... |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.enums.music; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; | |
| 3 | 4 | import lombok.AllArgsConstructor; |
| 4 | 5 | import lombok.Getter; |
| 5 | 6 | |
| 7 | +import java.util.Arrays; | |
| 8 | + | |
| 6 | 9 | /** |
| 7 | 10 | * AI 音乐状态的枚举 |
| 8 | 11 | * |
| ... | ... | @@ -10,7 +13,7 @@ import lombok.Getter; |
| 10 | 13 | */ |
| 11 | 14 | @AllArgsConstructor |
| 12 | 15 | @Getter |
| 13 | -public enum AiMusicStatusEnum { | |
| 16 | +public enum AiMusicStatusEnum implements IntArrayValuable { | |
| 14 | 17 | |
| 15 | 18 | IN_PROGRESS(10, "进行中"), |
| 16 | 19 | SUCCESS(20, "已完成"); |
| ... | ... | @@ -25,4 +28,11 @@ public enum AiMusicStatusEnum { |
| 25 | 28 | */ |
| 26 | 29 | private final String name; |
| 27 | 30 | |
| 31 | + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicStatusEnum::getStatus).toArray(); | |
| 32 | + | |
| 33 | + @Override | |
| 34 | + public int[] array() { | |
| 35 | + return ARRAYS; | |
| 36 | + } | |
| 37 | + | |
| 28 | 38 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.controller.admin.image; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | 3 | import cn.hutool.core.util.ObjUtil; |
| 5 | 4 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 6 | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| ... | ... | @@ -26,9 +25,7 @@ import org.springframework.security.access.prepost.PreAuthorize; |
| 26 | 25 | import org.springframework.validation.annotation.Validated; |
| 27 | 26 | import org.springframework.web.bind.annotation.*; |
| 28 | 27 | |
| 29 | -import java.util.Collections; | |
| 30 | 28 | import java.util.List; |
| 31 | -import java.util.Objects; | |
| 32 | 29 | |
| 33 | 30 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 34 | 31 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| ... | ... | @@ -42,15 +39,16 @@ public class AiImageController { |
| 42 | 39 | @Resource |
| 43 | 40 | private AiImageService imageService; |
| 44 | 41 | |
| 45 | - @Operation(summary = "获取【我的】绘图分页") | |
| 46 | 42 | @GetMapping("/my-page") |
| 43 | + @Operation(summary = "获取【我的】绘图分页") | |
| 47 | 44 | public CommonResult<PageResult<AiImageRespVO>> getImagePageMy(@Validated PageParam pageReqVO) { |
| 48 | 45 | PageResult<AiImageDO> pageResult = imageService.getImagePageMy(getLoginUserId(), pageReqVO); |
| 49 | 46 | return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); |
| 50 | 47 | } |
| 51 | 48 | |
| 52 | - @Operation(summary = "获取【我的】绘图记录") | |
| 53 | 49 | @GetMapping("/get-my") |
| 50 | + @Operation(summary = "获取【我的】绘图记录") | |
| 51 | + @Parameter(name = "id", required = true, description = "绘画编号", example = "1024") | |
| 54 | 52 | public CommonResult<AiImageRespVO> getImageMy(@RequestParam("id") Long id) { |
| 55 | 53 | AiImageDO image = imageService.getImage(id); |
| 56 | 54 | if (image == null || ObjUtil.notEqual(getLoginUserId(), image.getUserId())) { |
| ... | ... | @@ -59,17 +57,13 @@ public class AiImageController { |
| 59 | 57 | return success(BeanUtils.toBean(image, AiImageRespVO.class)); |
| 60 | 58 | } |
| 61 | 59 | |
| 62 | - @Operation(summary = "获取【我的】绘图记录 - ids") | |
| 63 | 60 | @GetMapping("/get-my-ids") |
| 61 | + @Operation(summary = "获取【我的】绘图记录列表") | |
| 62 | + @Parameter(name = "ids", required = true, description = "绘画编号数组", example = "1024,2048") | |
| 64 | 63 | public CommonResult<List<AiImageRespVO>> getImageMyIds(@RequestParam("ids") List<Long> ids) { |
| 65 | 64 | List<AiImageDO> imageList = imageService.getImageByIds(ids); |
| 66 | - if (CollUtil.isEmpty(imageList)) { | |
| 67 | - return success(Collections.emptyList()); | |
| 68 | - } | |
| 69 | - List<AiImageDO> userImageList = imageList.stream() | |
| 70 | - .map(item -> ObjUtil.equal(getLoginUserId(), item.getUserId()) ? item : null) | |
| 71 | - .filter(Objects::nonNull).toList(); | |
| 72 | - return success(BeanUtils.toBean(userImageList, AiImageRespVO.class)); | |
| 65 | + imageList.removeIf(item -> !ObjUtil.equal(getLoginUserId(), item.getUserId())); | |
| 66 | + return success(BeanUtils.toBean(imageList, AiImageRespVO.class)); | |
| 73 | 67 | } |
| 74 | 68 | |
| 75 | 69 | @Operation(summary = "生成图片") | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
| ... | ... | @@ -20,7 +20,7 @@ public class AiImagePageReqVO extends PageParam { |
| 20 | 20 | @Schema(description = "用户编号", example = "28987") |
| 21 | 21 | private Long userId; |
| 22 | 22 | |
| 23 | - @Schema(description = "平台") | |
| 23 | + @Schema(description = "平台", example = "OpenAI") | |
| 24 | 24 | private String platform; |
| 25 | 25 | |
| 26 | 26 | @Schema(description = "绘画状态", example = "1") | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiImageController.http renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
| 1 | -### 生成音乐:Suno + | |
| 1 | +### 生成音乐:Suno + 歌词模式 | |
| 2 | 2 | POST {{baseUrl}}/ai/music/generate |
| 3 | 3 | Content-Type: application/json |
| 4 | 4 | Authorization: {{token}} |
| ... | ... | @@ -10,4 +10,17 @@ Authorization: {{token}} |
| 10 | 10 | "modelVersion": "chirp-v3.5", |
| 11 | 11 | "tags": ["Happy"], |
| 12 | 12 | "title": "Happy Song" |
| 13 | +} | |
| 14 | + | |
| 15 | +### 生成音乐:Suno + 描述模式 | |
| 16 | +POST {{baseUrl}}/ai/music/generate | |
| 17 | +Content-Type: application/json | |
| 18 | +Authorization: {{token}} | |
| 19 | + | |
| 20 | +{ | |
| 21 | + "platform": "Suno", | |
| 22 | + "generateMode": 2, | |
| 23 | + "prompt": "来一首快乐的歌曲", | |
| 24 | + "makeInstrumental": false, | |
| 25 | + "title": "Happy Song" | |
| 13 | 26 | } |
| 14 | 27 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.controller.admin.music; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 5 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 6 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; | |
| 7 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicRespVO; | |
| 8 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdatePublicStatusReqVO; | |
| 4 | 9 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; |
| 10 | +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | |
| 5 | 11 | import cn.iocoder.yudao.module.ai.service.music.AiMusicService; |
| 6 | 12 | import io.swagger.v3.oas.annotations.Operation; |
| 13 | +import io.swagger.v3.oas.annotations.Parameter; | |
| 7 | 14 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 8 | 15 | import jakarta.annotation.Resource; |
| 9 | 16 | import jakarta.validation.Valid; |
| 10 | -import org.springframework.web.bind.annotation.PostMapping; | |
| 11 | -import org.springframework.web.bind.annotation.RequestBody; | |
| 12 | -import org.springframework.web.bind.annotation.RequestMapping; | |
| 13 | -import org.springframework.web.bind.annotation.RestController; | |
| 17 | +import org.springframework.security.access.prepost.PreAuthorize; | |
| 18 | +import org.springframework.web.bind.annotation.*; | |
| 14 | 19 | |
| 15 | 20 | import java.util.List; |
| 16 | 21 | |
| ... | ... | @@ -31,4 +36,31 @@ public class AiMusicController { |
| 31 | 36 | return success(musicService.generateMusic(getLoginUserId(), reqVO)); |
| 32 | 37 | } |
| 33 | 38 | |
| 39 | + // ================ 绘图管理 ================ | |
| 40 | + | |
| 41 | + @GetMapping("/page") | |
| 42 | + @Operation(summary = "获得音乐分页") | |
| 43 | + @PreAuthorize("@ss.hasPermission('ai:music:query')") | |
| 44 | + public CommonResult<PageResult<AiMusicRespVO>> getMusicPage(@Valid AiMusicPageReqVO pageReqVO) { | |
| 45 | + PageResult<AiMusicDO> pageResult = musicService.getMusicPage(pageReqVO); | |
| 46 | + return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); | |
| 47 | + } | |
| 48 | + | |
| 49 | + @DeleteMapping("/delete") | |
| 50 | + @Operation(summary = "删除音乐") | |
| 51 | + @Parameter(name = "id", description = "编号", required = true) | |
| 52 | + @PreAuthorize("@ss.hasPermission('ai:music:delete')") | |
| 53 | + public CommonResult<Boolean> deleteMusic(@RequestParam("id") Long id) { | |
| 54 | + musicService.deleteMusic(id); | |
| 55 | + return success(true); | |
| 56 | + } | |
| 57 | + | |
| 58 | + @PutMapping("/update") | |
| 59 | + @Operation(summary = "更新音乐发布状态") | |
| 60 | + @PreAuthorize("@ss.hasPermission('ai:music:update')") | |
| 61 | + public CommonResult<Boolean> updateMusicPublicStatus(@Valid @RequestBody AiMusicUpdatePublicStatusReqVO updateReqVO) { | |
| 62 | + musicService.updateMusicPublicStatus(updateReqVO); | |
| 63 | + return success(true); | |
| 64 | + } | |
| 65 | + | |
| 34 | 66 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageParam; | |
| 4 | +import cn.iocoder.yudao.framework.common.validation.InEnum; | |
| 5 | +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; | |
| 6 | +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | |
| 7 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 8 | +import lombok.Data; | |
| 9 | +import lombok.EqualsAndHashCode; | |
| 10 | +import lombok.ToString; | |
| 11 | +import org.springframework.format.annotation.DateTimeFormat; | |
| 12 | + | |
| 13 | +import java.time.LocalDateTime; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; | |
| 16 | + | |
| 17 | +@Schema(description = "管理后台 - AI 音乐分页 Request VO") | |
| 18 | +@Data | |
| 19 | +@EqualsAndHashCode(callSuper = true) | |
| 20 | +@ToString(callSuper = true) | |
| 21 | +public class AiMusicPageReqVO extends PageParam { | |
| 22 | + | |
| 23 | + @Schema(description = "用户编号", example = "12212") | |
| 24 | + private Long userId; | |
| 25 | + | |
| 26 | + @Schema(description = "音乐名称", example = "夜空中最亮的星") | |
| 27 | + private String title; | |
| 28 | + | |
| 29 | + @Schema(description = "音乐状态", example = "20") | |
| 30 | + @InEnum(AiMusicStatusEnum.class) | |
| 31 | + private Integer status; | |
| 32 | + | |
| 33 | + @Schema(description = "生成模式", example = "1") | |
| 34 | + @InEnum(AiMusicGenerateModeEnum.class) | |
| 35 | + private Integer generateMode; | |
| 36 | + | |
| 37 | + @Schema(description = "是否发布", example = "true") | |
| 38 | + private Boolean publicStatus; | |
| 39 | + | |
| 40 | + @Schema(description = "创建时间") | |
| 41 | + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | |
| 42 | + private LocalDateTime[] createTime; | |
| 43 | + | |
| 44 | +} | |
| 0 | 45 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +import java.time.LocalDateTime; | |
| 7 | +import java.util.List; | |
| 8 | + | |
| 9 | +@Schema(description = "管理后台 - AI 音乐 Response VO") | |
| 10 | +@Data | |
| 11 | +public class AiMusicRespVO { | |
| 12 | + | |
| 13 | + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") | |
| 14 | + private Long id; | |
| 15 | + | |
| 16 | + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12212") | |
| 17 | + private Long userId; | |
| 18 | + | |
| 19 | + @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星") | |
| 20 | + private String title; | |
| 21 | + | |
| 22 | + @Schema(description = "歌词", example = "oh~卖糕的") | |
| 23 | + private String lyric; | |
| 24 | + | |
| 25 | + @Schema(description = "图片地址", example = "https://www.iocoder.cn") | |
| 26 | + private String imageUrl; | |
| 27 | + | |
| 28 | + @Schema(description = "音频地址", example = "https://www.iocoder.cn") | |
| 29 | + private String audioUrl; | |
| 30 | + | |
| 31 | + @Schema(description = "视频地址", example = "https://www.iocoder.cn") | |
| 32 | + private String videoUrl; | |
| 33 | + | |
| 34 | + @Schema(description = "音乐状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") | |
| 35 | + private Integer status; | |
| 36 | + | |
| 37 | + @Schema(description = "描述词", example = "一首轻快的歌曲") | |
| 38 | + private String gptDescriptionPrompt; | |
| 39 | + | |
| 40 | + @Schema(description = "提示词", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") | |
| 41 | + private String prompt; | |
| 42 | + | |
| 43 | + @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno") | |
| 44 | + private String platform; | |
| 45 | + | |
| 46 | + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5") | |
| 47 | + private String model; | |
| 48 | + | |
| 49 | + @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 50 | + private Integer generateMode; | |
| 51 | + | |
| 52 | + @Schema(description = "音乐风格标签") | |
| 53 | + private List<String> tags; | |
| 54 | + | |
| 55 | + @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | |
| 56 | + private Boolean publicStatus; | |
| 57 | + | |
| 58 | + @Schema(description = "任务编号", example = "11369") | |
| 59 | + private String taskId; | |
| 60 | + | |
| 61 | + @Schema(description = "错误信息") | |
| 62 | + private String errorMessage; | |
| 63 | + | |
| 64 | + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | |
| 65 | + private LocalDateTime createTime; | |
| 66 | + | |
| 67 | +} | |
| 0 | 68 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdatePublicStatusReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +@Schema(description = "管理后台 - AI 音乐修改发布状态 Request VO") | |
| 8 | +@Data | |
| 9 | +public class AiMusicUpdatePublicStatusReqVO { | |
| 10 | + | |
| 11 | + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") | |
| 12 | + private Long id; | |
| 13 | + | |
| 14 | + @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | |
| 15 | + @NotNull(message = "是否发布不能为空") | |
| 16 | + private Boolean publicStatus; | |
| 17 | + | |
| 18 | +} | |
| 0 | 19 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo; |
| 2 | 2 | |
| 3 | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | 4 | import jakarta.validation.constraints.NotBlank; |
| 5 | +import jakarta.validation.constraints.NotEmpty; | |
| 5 | 6 | import jakarta.validation.constraints.NotNull; |
| 6 | 7 | import lombok.Data; |
| 7 | 8 | |
| ... | ... | @@ -15,6 +16,10 @@ public class AiSunoGenerateReqVO { |
| 15 | 16 | @NotBlank(message = "平台不能为空") |
| 16 | 17 | private String platform; // 参见 AiPlatformEnum 枚举 |
| 17 | 18 | |
| 19 | + /** | |
| 20 | + * 1. 描述模式:描述词 + 是否纯音乐 + 模型 TODO @xin:目前貌似描述词没弄对?看着不是 prompt 字段(也可能我弄错了)。可以微信再沟通下哈 | |
| 21 | + * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型 TODO @xin:目前这块少传递了标题; | |
| 22 | + */ | |
| 18 | 23 | @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") |
| 19 | 24 | @NotNull(message = "生成模式不能为空") |
| 20 | 25 | private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举 |
| ... | ... | @@ -26,7 +31,9 @@ public class AiSunoGenerateReqVO { |
| 26 | 31 | @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true") |
| 27 | 32 | private Boolean makeInstrumental; |
| 28 | 33 | |
| 29 | - @Schema(description = "模型版本", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "chirp-v3.5") | |
| 34 | + // TODO @xin:看了下这个字段,发现最终还是 model 合适点;因为它其实是模型 | |
| 35 | + @Schema(description = "模型版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5") | |
| 36 | + @NotEmpty(message = "模型不能为空") | |
| 30 | 37 | private String modelVersion; // 参见 AiModelEnum 枚举 |
| 31 | 38 | |
| 32 | 39 | @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]") | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.dal.mysql.music; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 3 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 6 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; | |
| 4 | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; |
| 5 | 8 | import org.apache.ibatis.annotations.Mapper; |
| 6 | 9 | |
| ... | ... | @@ -18,4 +21,15 @@ public interface AiMusicMapper extends BaseMapperX<AiMusicDO> { |
| 18 | 21 | return selectList(AiMusicDO::getStatus, status); |
| 19 | 22 | } |
| 20 | 23 | |
| 24 | + default PageResult<AiMusicDO> selectPage(AiMusicPageReqVO reqVO) { | |
| 25 | + return selectPage(reqVO, new LambdaQueryWrapperX<AiMusicDO>() | |
| 26 | + .eqIfPresent(AiMusicDO::getUserId, reqVO.getUserId()) | |
| 27 | + .eqIfPresent(AiMusicDO::getTitle, reqVO.getTitle()) | |
| 28 | + .eqIfPresent(AiMusicDO::getStatus, reqVO.getStatus()) | |
| 29 | + .eqIfPresent(AiMusicDO::getGenerateMode, reqVO.getGenerateMode()) | |
| 30 | + .betweenIfPresent(AiMusicDO::getCreateTime, reqVO.getCreateTime()) | |
| 31 | + .eqIfPresent(AiMusicDO::getPublicStatus, reqVO.getPublicStatus()) | |
| 32 | + .orderByDesc(AiMusicDO::getId)); | |
| 33 | + } | |
| 34 | + | |
| 21 | 35 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
| ... | ... | @@ -44,7 +44,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU |
| 44 | 44 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; |
| 45 | 45 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 46 | 46 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 47 | -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.AI_CHAT_MESSAGE_NOT_EXIST; | |
| 47 | +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST; | |
| 48 | 48 | import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS; |
| 49 | 49 | |
| 50 | 50 | /** |
| ... | ... | @@ -150,7 +150,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 150 | 150 | log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); |
| 151 | 151 | chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())); |
| 152 | 152 | }).onErrorResume(error -> { |
| 153 | - return Flux.just(error(ErrorCodeConstants.AI_CHAT_STREAM_ERROR)); | |
| 153 | + return Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)); | |
| 154 | 154 | }); |
| 155 | 155 | } |
| 156 | 156 | |
| ... | ... | @@ -257,7 +257,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 257 | 257 | // 1. 校验消息存在 |
| 258 | 258 | AiChatMessageDO message = chatMessageMapper.selectById(id); |
| 259 | 259 | if (message == null || ObjUtil.notEqual(message.getUserId(), userId)) { |
| 260 | - throw exception(AI_CHAT_MESSAGE_NOT_EXIST); | |
| 260 | + throw exception(CHAT_MESSAGE_NOT_EXIST); | |
| 261 | 261 | } |
| 262 | 262 | // 2. 执行删除 |
| 263 | 263 | chatMessageMapper.deleteById(id); |
| ... | ... | @@ -268,7 +268,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 268 | 268 | // 1. 校验消息存在 |
| 269 | 269 | List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId); |
| 270 | 270 | if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) { |
| 271 | - throw exception(AI_CHAT_MESSAGE_NOT_EXIST); | |
| 271 | + throw exception(CHAT_MESSAGE_NOT_EXIST); | |
| 272 | 272 | } |
| 273 | 273 | // 2. 执行删除 |
| 274 | 274 | chatMessageMapper.deleteBatchIds(convertList(messages, AiChatMessageDO::getId)); |
| ... | ... | @@ -279,7 +279,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 279 | 279 | // 1. 校验消息存在 |
| 280 | 280 | AiChatMessageDO message = chatMessageMapper.selectById(id); |
| 281 | 281 | if (message == null) { |
| 282 | - throw exception(AI_CHAT_MESSAGE_NOT_EXIST); | |
| 282 | + throw exception(CHAT_MESSAGE_NOT_EXIST); | |
| 283 | 283 | } |
| 284 | 284 | // 2. 执行删除 |
| 285 | 285 | chatMessageMapper.deleteById(id); | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
| ... | ... | @@ -35,6 +35,7 @@ import org.springframework.scheduling.annotation.Async; |
| 35 | 35 | import org.springframework.stereotype.Service; |
| 36 | 36 | import org.springframework.transaction.annotation.Transactional; |
| 37 | 37 | |
| 38 | +import java.util.Collections; | |
| 38 | 39 | import java.util.List; |
| 39 | 40 | import java.util.Map; |
| 40 | 41 | |
| ... | ... | @@ -76,6 +77,9 @@ public class AiImageServiceImpl implements AiImageService { |
| 76 | 77 | |
| 77 | 78 | @Override |
| 78 | 79 | public List<AiImageDO> getImageByIds(List<Long> ids) { |
| 80 | + if (CollUtil.isEmpty(ids)) { | |
| 81 | + return Collections.emptyList(); | |
| 82 | + } | |
| 79 | 83 | return imageMapper.selectBatchIds(ids); |
| 80 | 84 | } |
| 81 | 85 | |
| ... | ... | @@ -135,7 +139,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 135 | 139 | // 1. 校验是否存在 |
| 136 | 140 | AiImageDO image = validateImageExists(id); |
| 137 | 141 | if (ObjUtil.notEqual(image.getUserId(), userId)) { |
| 138 | - throw exception(AI_IMAGE_NOT_EXISTS); | |
| 142 | + throw exception(IMAGE_NOT_EXISTS); | |
| 139 | 143 | } |
| 140 | 144 | // 2. 删除记录 |
| 141 | 145 | imageMapper.deleteById(id); |
| ... | ... | @@ -165,7 +169,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 165 | 169 | private AiImageDO validateImageExists(Long id) { |
| 166 | 170 | AiImageDO image = imageMapper.selectById(id); |
| 167 | 171 | if (image == null) { |
| 168 | - throw exception(AI_IMAGE_NOT_EXISTS); | |
| 172 | + throw exception(IMAGE_NOT_EXISTS); | |
| 169 | 173 | } |
| 170 | 174 | return image; |
| 171 | 175 | } |
| ... | ... | @@ -191,7 +195,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 191 | 195 | if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) { |
| 192 | 196 | String description = imagineResponse.description().contains("quota_not_enough") ? |
| 193 | 197 | "账户余额不足" : imagineResponse.description(); |
| 194 | - throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); | |
| 198 | + throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); | |
| 195 | 199 | } |
| 196 | 200 | |
| 197 | 201 | // 4. 情况二【成功】:更新 taskId 和参数 |
| ... | ... | @@ -271,13 +275,13 @@ public class AiImageServiceImpl implements AiImageService { |
| 271 | 275 | // 1.1 检查 image |
| 272 | 276 | AiImageDO image = validateImageExists(reqVO.getId()); |
| 273 | 277 | if (ObjUtil.notEqual(userId, image.getUserId())) { |
| 274 | - throw exception(AI_IMAGE_NOT_EXISTS); | |
| 278 | + throw exception(IMAGE_NOT_EXISTS); | |
| 275 | 279 | } |
| 276 | 280 | // 1.2 检查 customId |
| 277 | 281 | MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(), |
| 278 | 282 | buttonX -> buttonX.customId().equals(reqVO.getCustomId())); |
| 279 | 283 | if (button == null) { |
| 280 | - throw exception(AI_IMAGE_CUSTOM_ID_NOT_EXISTS); | |
| 284 | + throw exception(IMAGE_CUSTOM_ID_NOT_EXISTS); | |
| 281 | 285 | } |
| 282 | 286 | |
| 283 | 287 | // 2. 调用 Midjourney Proxy 提交任务 |
| ... | ... | @@ -286,7 +290,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 286 | 290 | if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(actionResponse.code())) { |
| 287 | 291 | String description = actionResponse.description().contains("quota_not_enough") ? |
| 288 | 292 | "账户余额不足" : actionResponse.description(); |
| 289 | - throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); | |
| 293 | + throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); | |
| 290 | 294 | } |
| 291 | 295 | |
| 292 | 296 | // 3. 新增 image 记录 | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.service.music; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 4 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; | |
| 5 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdatePublicStatusReqVO; | |
| 3 | 6 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; |
| 7 | +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | |
| 8 | +import jakarta.validation.Valid; | |
| 4 | 9 | |
| 5 | 10 | import java.util.List; |
| 6 | 11 | |
| ... | ... | @@ -27,4 +32,26 @@ public interface AiMusicService { |
| 27 | 32 | */ |
| 28 | 33 | Integer syncMusic(); |
| 29 | 34 | |
| 35 | + /** | |
| 36 | + * 更新音乐发布状态 | |
| 37 | + * | |
| 38 | + * @param updateReqVO 更新信息 | |
| 39 | + */ | |
| 40 | + void updateMusicPublicStatus(@Valid AiMusicUpdatePublicStatusReqVO updateReqVO); | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 删除AI 音乐 | |
| 44 | + * | |
| 45 | + * @param id 编号 | |
| 46 | + */ | |
| 47 | + void deleteMusic(Long id); | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 获得音乐分页 | |
| 51 | + * | |
| 52 | + * @param pageReqVO 分页查询 | |
| 53 | + * @return 音乐分页 | |
| 54 | + */ | |
| 55 | + PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO); | |
| 56 | + | |
| 30 | 57 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
| ... | ... | @@ -4,6 +4,9 @@ import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.text.StrPool; |
| 5 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 7 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 8 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; | |
| 9 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdatePublicStatusReqVO; | |
| 7 | 10 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; |
| 8 | 11 | import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; |
| 9 | 12 | import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; |
| ... | ... | @@ -15,8 +18,10 @@ import org.springframework.stereotype.Service; |
| 15 | 18 | |
| 16 | 19 | import java.util.*; |
| 17 | 20 | |
| 21 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 18 | 22 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 19 | 23 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; |
| 24 | +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXISTS; | |
| 20 | 25 | |
| 21 | 26 | /** |
| 22 | 27 | * AI 音乐 Service 实现类 |
| ... | ... | @@ -56,7 +61,7 @@ public class AiMusicServiceImpl implements AiMusicService { |
| 56 | 61 | return Collections.emptyList(); |
| 57 | 62 | } |
| 58 | 63 | List<AiMusicDO> musicList = buildMusicDOList(musicDataList); |
| 59 | - musicList.forEach(music -> music.setUserId(userId).setPlatform(music.getPlatform()).setGenerateMode(reqVO.getGenerateMode())); | |
| 64 | + musicList.forEach(music -> music.setUserId(userId).setPlatform(reqVO.getPlatform()).setGenerateMode(reqVO.getGenerateMode())); | |
| 60 | 65 | musicMapper.insertBatch(musicList); |
| 61 | 66 | return convertList(musicList, AiMusicDO::getId); |
| 62 | 67 | } |
| ... | ... | @@ -92,12 +97,41 @@ public class AiMusicServiceImpl implements AiMusicService { |
| 92 | 97 | * @return AiMusicDO 集合 |
| 93 | 98 | */ |
| 94 | 99 | private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) { |
| 100 | + // TODO @xin:成功的情况下,需要下载到自己的文件服务器。参考图片的处理 | |
| 95 | 101 | return convertList(musicList, musicData -> new AiMusicDO() |
| 96 | 102 | .setTaskId(musicData.id()).setModel(musicData.modelName()) |
| 97 | 103 | .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) |
| 98 | 104 | .setAudioUrl(musicData.audioUrl()).setVideoUrl(musicData.videoUrl()).setImageUrl(musicData.imageUrl()) |
| 99 | 105 | .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA)) |
| 100 | - .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus())); | |
| 106 | + .setStatus(Objects.equals("complete", musicData.status()) ? | |
| 107 | + AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus())); | |
| 108 | + } | |
| 109 | + | |
| 110 | + @Override | |
| 111 | + public void updateMusicPublicStatus(AiMusicUpdatePublicStatusReqVO updateReqVO) { | |
| 112 | + // 校验存在 | |
| 113 | + validateMusicExists(updateReqVO.getId()); | |
| 114 | + // 更新 | |
| 115 | + musicMapper.updateBatch(new AiMusicDO().setPublicStatus(updateReqVO.getPublicStatus())); | |
| 116 | + } | |
| 117 | + | |
| 118 | + @Override | |
| 119 | + public void deleteMusic(Long id) { | |
| 120 | + // 校验存在 | |
| 121 | + validateMusicExists(id); | |
| 122 | + // 删除 | |
| 123 | + musicMapper.deleteById(id); | |
| 124 | + } | |
| 125 | + | |
| 126 | + private void validateMusicExists(Long id) { | |
| 127 | + if (musicMapper.selectById(id) == null) { | |
| 128 | + throw exception(MUSIC_NOT_EXISTS); | |
| 129 | + } | |
| 130 | + } | |
| 101 | 131 | |
| 132 | + @Override | |
| 133 | + public PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO) { | |
| 134 | + return musicMapper.selectPage(pageReqVO); | |
| 102 | 135 | } |
| 136 | + | |
| 103 | 137 | } | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java
| ... | ... | @@ -17,7 +17,8 @@ public class SunoTests { |
| 17 | 17 | |
| 18 | 18 | @Before |
| 19 | 19 | public void setup() { |
| 20 | - String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app"; | |
| 20 | +// String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app"; | |
| 21 | + String url = "http://127.0.0.1:3001"; | |
| 21 | 22 | this.sunoApi = new SunoApi(url); |
| 22 | 23 | } |
| 23 | 24 | |
| ... | ... | @@ -53,5 +54,4 @@ public class SunoTests { |
| 53 | 54 | System.out.println(limitUsageData); |
| 54 | 55 | } |
| 55 | 56 | |
| 56 | - | |
| 57 | 57 | } | ... | ... |
yudao-server/src/main/resources/application.yaml
| ... | ... | @@ -201,7 +201,8 @@ yudao.ai: |
| 201 | 201 | notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify |
| 202 | 202 | suno: |
| 203 | 203 | enable: true |
| 204 | - base-url: https://suno-om0w1cy6e-status2xxs-projects.vercel.app | |
| 204 | +# base-url: https://suno-om0w1cy6e-status2xxs-projects.vercel.app | |
| 205 | + base-url: http://127.0.0.1:3001 | |
| 205 | 206 | |
| 206 | 207 | --- #################### 芋道相关配置 #################### |
| 207 | 208 | ... | ... |