Commit c77fc954d20b6badae1d0f1512f3652a054e9f1f

Authored by 芋道源码
Committed by Gitee
2 parents 05e4ccac 44a0b04d

!6 【解决todo】AI:音乐接入

Merge pull request !6 from 小新/master-jdk21-ai
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateEnum.java renamed to yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.enums.music;
3 3 import lombok.AllArgsConstructor;
4 4 import lombok.Getter;
5 5  
6   -// TODO @xiaoxin:这个也叫 AiMusicGenerateModeEnum 吧。虽然长,但是和项目统一一点。
7 6 /**
8 7 * AI 音乐状态的枚举
9 8 *
... ... @@ -11,27 +10,18 @@ import lombok.Getter;
11 10 */
12 11 @AllArgsConstructor
13 12 @Getter
14   -public enum AiMusicGenerateEnum {
  13 +public enum AiMusicGenerateModeEnum {
15 14  
16   - LYRIC("1", "歌词模式"),
17   - DESCRIPTION("2", "描述模式");
  15 + LYRIC(1, "歌词模式"),
  16 + DESCRIPTION(2, "描述模式");
18 17  
19 18 /**
20 19 * 模式
21 20 */
22   - private final String mode;
  21 + private final Integer mode;
23 22 /**
24 23 * 模式名
25 24 */
26 25 private final String name;
27 26  
28   - public static AiMusicGenerateEnum valueOfMode(String mode) {
29   - for (AiMusicGenerateEnum modeEnum : AiMusicGenerateEnum.values()) {
30   - if (modeEnum.getMode().equals(mode)) {
31   - return modeEnum;
32   - }
33   - }
34   - throw new IllegalArgumentException("未知模式: " + mode);
35   - }
36   -
37 27 }
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
... ... @@ -12,14 +12,14 @@ import lombok.Getter;
12 12 @Getter
13 13 public enum AiMusicStatusEnum {
14 14  
15   - // @xin 文档中无失败这个返回值 TODO @xin:用 Integer 哈。另外个枚举类也是
16   - STREAMING("10", "进行中"),
17   - COMPLETE("20", "完成");
  15 + // @xin 文档中无失败这个返回值
  16 + STREAMING(10, "进行中"),
  17 + COMPLETE(20, "完成");
18 18  
19 19 /**
20 20 * 状态
21 21 */
22   - private final String status;
  22 + private final Integer status;
23 23  
24 24 /**
25 25 * 状态名
... ...
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.NotNull;
5 6 import lombok.Data;
6 7  
7 8 import java.util.List;
... ... @@ -10,20 +11,19 @@ import java.util.List;
10 11 @Data
11 12 public class AiSunoGenerateReqVO {
12 13  
13   - // TODO @xin:每个参数,必要的是否必填校验
14   - @Schema(description = "用于生成音乐音频的提示", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
  14 + @Schema(description = "用于生成音乐音频的提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
15 15 private String prompt;
16 16  
17   - @Schema(description = "是否纯音乐", example = "true")
  17 + @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true")
18 18 private Boolean makeInstrumental;
19 19  
20   - @Schema(description = "模型版本, 默认 chirp-v3.5", example = "chirp-v3.5")
  20 + @Schema(description = "模型版本, 默认 chirp-v3.5", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "chirp-v3.5")
21 21 private String modelVersion; // 参见 AiModelEnum 枚举
22 22  
23   - @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
  23 + @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]")
24 24 private List<String> tags;
25 25  
26   - @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星")
  26 + @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星")
27 27 private String title;
28 28  
29 29 @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
... ... @@ -31,7 +31,7 @@ public class AiSunoGenerateReqVO {
31 31 private String platform; // 参见 AiPlatformEnum 枚举
32 32  
33 33 @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
34   - @NotBlank(message = "生成模式不能为空")
35   - private String generateMode; // 参见 AiMusicGenerateEnum 枚举
  34 + @NotNull(message = "生成模式不能为空")
  35 + private Integer generateMode; // 参见 AiMusicGenerateEnum 枚举
36 36  
37 37 }
38 38 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
... ... @@ -63,7 +63,7 @@ public class AiMusicDO extends BaseDO {
63 63 * <p>
64 64 * 枚举 {@link AiMusicStatusEnum}
65 65 */
66   - private String status;
  66 + private Integer status;
67 67  
68 68 /**
69 69 * 描述词
... ... @@ -77,7 +77,7 @@ public class AiMusicDO extends BaseDO {
77 77 /**
78 78 * 生成模式
79 79 */
80   - private String generateMode;
  80 + private Integer generateMode;
81 81  
82 82 /**
83 83 * 平台
... ...
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 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
4 5 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
5 6 import org.apache.ibatis.annotations.Mapper;
6 7  
  8 +import java.util.List;
  9 +
7 10 /**
8 11 * AI 音乐 Mapper
9   - * @author xiaoxin
  12 + *
  13 + * @author xiaoxin
10 14 */
11 15 @Mapper
12 16 public interface AiMusicMapper extends BaseMapperX<AiMusicDO> {
  17 +
  18 + default List<AiMusicDO> selectListByStatus(Integer status) {
  19 + return selectList(new LambdaQueryWrapperX<AiMusicDO>()
  20 + .eq(AiMusicDO::getStatus, status));
  21 + }
  22 +
13 23 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/sun/AiSunoSyncJob.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java
1   -package cn.iocoder.yudao.module.ai.job.sun;
  1 +package cn.iocoder.yudao.module.ai.job.music;
2 2  
3 3 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
4 4 import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
... ...
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 3 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
4   -import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
5 4  
6 5 import java.util.List;
7 6  
... ... @@ -21,25 +20,9 @@ public interface AiMusicService {
21 20 List<Long> generateMusic(AiSunoGenerateReqVO reqVO);
22 21  
23 22 /**
24   - * 获取未完成状态的任务
25   - *
26   - * @return 未完成任务列表
27   - */
28   - List<AiMusicDO> getUnCompletedTask();
29   -
30   - /**
31 23 * 同步音乐任务
32 24 *
33 25 * @return 同步数量
34 26 */
35 27 Integer syncMusic();
36   -
37   - /**
38   - * 批量更新音乐信息
39   - *
40   - * @param musicDOS 音乐信息
41   - * @return 是否成功
42   - */
43   - Boolean updateBatch(List<AiMusicDO> musicDOS);
44   -
45 28 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
... ... @@ -8,15 +8,13 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
8 8 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
9 9 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
10 10 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
11   -import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum;
  11 +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
12 12 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
13   -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
14 13 import jakarta.annotation.Resource;
15 14 import lombok.extern.slf4j.Slf4j;
16 15 import org.springframework.stereotype.Service;
17 16  
18 17 import java.util.*;
19   -import java.util.stream.Collectors;
20 18  
21 19 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
22 20  
... ... @@ -38,87 +36,54 @@ public class AiMusicServiceImpl implements AiMusicService {
38 36 @Override
39 37 public List<Long> generateMusic(AiSunoGenerateReqVO reqVO) {
40 38 List<SunoApi.MusicData> musicDataList;
41   - if (Objects.equals(AiMusicGenerateEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
  39 + if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
42 40 // 1.1 歌词模式
43 41 SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(
44 42 reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
45 43 musicDataList = sunoApi.customGenerate(sunoReq);
46   - } else if (Objects.equals(AiMusicGenerateEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
  44 + } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
47 45 // 1.2 描述模式
48 46 SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(
49 47 reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental());
50 48 musicDataList = sunoApi.generate(sunoReq);
51 49 } else {
52   - // TODO @xin:不用 log error,直接抛异常,吧 reqVO 呆进去,有全局处理的哈
53   - log.error("未知的生成模式:{}", reqVO.getGenerateMode());
54   - throw new IllegalArgumentException("未知的生成模式");
  50 + throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO));
55 51 }
56   -
57 52 // 2. 插入数据库
58   - // TODO @xin:因为 insertMusicData 复用的比较少,所以不用愁单独的方法,直接写在这里就好啦
59   - return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform());
60   - }
  53 + if (CollUtil.isEmpty(musicDataList)) {
61 54  
62   - // TODO @xin:1)service 里面,不要直接查询 db;2)不要用 ne,用 STREAMING 哈
63   - @Override
64   - public List<AiMusicDO> getUnCompletedTask() {
65   - return musicMapper.selectList(new LambdaQueryWrapper<AiMusicDO>().ne(AiMusicDO::getStatus, AiMusicStatusEnum.COMPLETE.getStatus()));
  55 + return Collections.emptyList();
  56 + }
  57 + List<AiMusicDO> aiMusicDOList = CollectionUtils.convertList(buildMusicDOList(musicDataList), musicDO ->
  58 + musicDO.setUserId(getLoginUserId())
  59 + .setGenerateMode(reqVO.getGenerateMode())
  60 + .setPlatform(reqVO.getPlatform()
  61 + ));
  62 + musicMapper.insertBatch(aiMusicDOList);
  63 + return CollectionUtils.convertList(aiMusicDOList, AiMusicDO::getId);
66 64 }
67 65  
68 66 @Override
69 67 public Integer syncMusic() {
70   - List<AiMusicDO> unCompletedTask = this.getUnCompletedTask();
71   - if (CollUtil.isEmpty(unCompletedTask)) {
72   - // TODO @xin:这里不用打,反正 Job 也打了
73   - log.info("Suno 无进行中任务需要更新!");
  68 + List<AiMusicDO> streamingTask = musicMapper.selectListByStatus(AiMusicStatusEnum.STREAMING.getStatus());
  69 + if (CollUtil.isEmpty(streamingTask)) {
74 70 return 0;
75 71 }
76   - log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", unCompletedTask.size());
  72 + log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size());
77 73 // GET 请求,为避免参数过长,分批次处理
78   - // TODO @xin:建议批量更大一些。
79   - CollUtil.split(unCompletedTask, 4).forEach(chunk -> {
80   - // TODO @xin:可以使用 CollectionUtils 里的 map 转换
81   - Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId);
  74 + CollUtil.split(streamingTask, 36).forEach(chunk -> {
  75 + Map<String, Long> taskIdMap = CollectionUtils.convertMap(chunk, AiMusicDO::getTaskId, AiMusicDO::getId);
82 76 List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
83   - // TODO @xin:查询不到,直接 return;这样真正逻辑的 85 - 87 就不用多一层括号
84   - if (CollUtil.isNotEmpty(musicTaskList)) {
85   - List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList);
86   - //回填id
87   - aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId())));
88   - this.updateBatch(aiMusicDOS);
89   - } else {
  77 + if (CollUtil.isEmpty(musicTaskList)) {
90 78 log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet());
  79 + return;
91 80 }
  81 + List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList);
  82 + //回填id
  83 + aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId())));
  84 + musicMapper.updateBatch(aiMusicDOS);
92 85 });
93   - return unCompletedTask.size();
94   - }
95   -
96   - // TODO @xin:这个方法,看着不用啦
97   - @Override
98   - public Boolean updateBatch(List<AiMusicDO> musicDOS) {
99   - return musicMapper.updateBatch(musicDOS);
100   - }
101   -
102   - /**
103   - * 新增音乐数据并提交 suno任务
104   - *
105   - * @param musicDataList 音乐数据列表
106   - * @return 音乐id集合
107   - */
108   - private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList, String generateMode, String platform) {
109   - if (CollUtil.isEmpty(musicDataList)) {
110   - return Collections.emptyList();
111   - }
112   - List<AiMusicDO> aiMusicDOList = buildMusicDOList(musicDataList).stream()
113   - .map(musicDO -> musicDO.setUserId(getLoginUserId())
114   - .setGenerateMode(generateMode)
115   - .setPlatform(platform))
116   - .toList();
117   - musicMapper.insertBatch(aiMusicDOList);
118   - // TODO @xin:用 CollectionUtils 简化操作
119   - return aiMusicDOList.stream()
120   - .map(AiMusicDO::getId)
121   - .collect(Collectors.toList());
  86 + return streamingTask.size();
122 87 }
123 88  
124 89 /**
... ... @@ -128,20 +93,12 @@ public class AiMusicServiceImpl implements AiMusicService {
128 93 * @return AiMusicDO 集合
129 94 */
130 95 private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicTaskList) {
131   - // TODO @xin:想通的变量,放在同一行,避免过长。
132 96 return CollectionUtils.convertList(musicTaskList, musicData -> new AiMusicDO()
133 97 .setTaskId(musicData.id())
134   - .setPrompt(musicData.prompt())
135   - .setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
136   - .setAudioUrl(musicData.audioUrl())
137   - .setVideoUrl(musicData.videoUrl())
138   - .setImageUrl(musicData.imageUrl())
139   - .setLyric(musicData.lyric())
140   - .setTitle(musicData.title())
141   - .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.COMPLETE.getStatus() : AiMusicStatusEnum.STREAMING.getStatus())
142   - .setModel(musicData.modelName())
143   - // TODO @xin:可以用 hutool 的 StrUtil 的 split 之类的
144   - .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null));
145   - }
  98 + .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
  99 + .setAudioUrl(musicData.audioUrl()).setVideoUrl(musicData.videoUrl()).setImageUrl(musicData.imageUrl())
  100 + .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
  101 + .setModel(musicData.modelName()).setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.COMPLETE.getStatus() : AiMusicStatusEnum.STREAMING.getStatus()));
146 102  
  103 + }
147 104 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java
... ... @@ -19,7 +19,7 @@ import java.util.function.Predicate;
19 19  
20 20 /**
21 21 * Suno API
22   - *
  22 + * <p>
23 23 * 对接 Suno Proxy:<a href="https://github.com/gcui-art/suno-api">suno-api</a>
24 24 *
25 25 * @author xiaoxin
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -200,7 +200,7 @@ yudao.ai:
200 200 notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
201 201 suno:
202 202 enable: true
203   - base-url: https://suno-imrqwwui8-status2xxs-projects.vercel.app
  203 + base-url: https://suno-om0w1cy6e-status2xxs-projects.vercel.app
204 204  
205 205 --- #################### 芋道相关配置 ####################
206 206  
... ...