Commit 23baaff84d84c34f1615ffdee3121ffa2de6d13d

Authored by YunaiV
1 parent c77fc954

【代码优化】AI:音乐生成

yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
@@ -12,9 +12,8 @@ import lombok.Getter; @@ -12,9 +12,8 @@ import lombok.Getter;
12 @Getter 12 @Getter
13 public enum AiMusicStatusEnum { 13 public enum AiMusicStatusEnum {
14 14
15 - // @xin 文档中无失败这个返回值  
16 - STREAMING(10, "进行中"),  
17 - COMPLETE(20, "完成"); 15 + IN_PROGRESS(10, "进行中"),
  16 + SUCCESS(20, "已完成");
18 17
19 /** 18 /**
20 * 状态 19 * 状态
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
@@ -7,11 +7,20 @@ import lombok.Data; @@ -7,11 +7,20 @@ import lombok.Data;
7 7
8 import java.util.List; 8 import java.util.List;
9 9
10 -@Schema(description = "管理后台 - 音乐生成 Request VO") 10 +@Schema(description = "管理后台 - AI 音乐生成 Request VO")
11 @Data 11 @Data
12 public class AiSunoGenerateReqVO { 12 public class AiSunoGenerateReqVO {
13 13
14 - @Schema(description = "用于生成音乐音频的提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") 14 + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
  15 + @NotBlank(message = "平台不能为空")
  16 + private String platform; // 参见 AiPlatformEnum 枚举
  17 +
  18 + @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  19 + @NotNull(message = "生成模式不能为空")
  20 + private Integer generateMode; // 参见 AiMusicGenerateEnum 枚举
  21 +
  22 + @Schema(description = "用于生成音乐音频的提示", requiredMode = Schema.RequiredMode.REQUIRED,
  23 + example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
15 private String prompt; 24 private String prompt;
16 25
17 @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true") 26 @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true")
@@ -26,12 +35,4 @@ public class AiSunoGenerateReqVO { @@ -26,12 +35,4 @@ public class AiSunoGenerateReqVO {
26 @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星") 35 @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星")
27 private String title; 36 private String title;
28 37
29 - @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")  
30 - @NotBlank(message = "平台不能为空")  
31 - private String platform; // 参见 AiPlatformEnum 枚举  
32 -  
33 - @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")  
34 - @NotNull(message = "生成模式不能为空")  
35 - private Integer generateMode; // 参见 AiMusicGenerateEnum 枚举  
36 -  
37 } 38 }
38 \ No newline at end of file 39 \ 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
1 package cn.iocoder.yudao.module.ai.dal.dataobject.music; 1 package cn.iocoder.yudao.module.ai.dal.dataobject.music;
2 2
  3 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
4 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; 6 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
5 import com.baomidou.mybatisplus.annotation.IdType; 7 import com.baomidou.mybatisplus.annotation.IdType;
6 import com.baomidou.mybatisplus.annotation.TableField; 8 import com.baomidou.mybatisplus.annotation.TableField;
@@ -39,20 +41,18 @@ public class AiMusicDO extends BaseDO { @@ -39,20 +41,18 @@ public class AiMusicDO extends BaseDO {
39 private String title; 41 private String title;
40 42
41 /** 43 /**
42 - * 图片地址  
43 - */  
44 - private String imageUrl;  
45 -  
46 - /**  
47 * 歌词 44 * 歌词
48 */ 45 */
49 private String lyric; 46 private String lyric;
50 47
51 /** 48 /**
  49 + * 图片地址
  50 + */
  51 + private String imageUrl;
  52 + /**
52 * 音频地址 53 * 音频地址
53 */ 54 */
54 private String audioUrl; 55 private String audioUrl;
55 -  
56 /** 56 /**
57 * 视频地址 57 * 视频地址
58 */ 58 */
@@ -66,6 +66,13 @@ public class AiMusicDO extends BaseDO { @@ -66,6 +66,13 @@ public class AiMusicDO extends BaseDO {
66 private Integer status; 66 private Integer status;
67 67
68 /** 68 /**
  69 + * 生成模式
  70 + *
  71 + * 枚举 {@link AiMusicGenerateModeEnum}
  72 + */
  73 + private Integer generateMode;
  74 +
  75 + /**
69 * 描述词 76 * 描述词
70 */ 77 */
71 private String gptDescriptionPrompt; 78 private String gptDescriptionPrompt;
@@ -75,28 +82,17 @@ public class AiMusicDO extends BaseDO { @@ -75,28 +82,17 @@ public class AiMusicDO extends BaseDO {
75 private String prompt; 82 private String prompt;
76 83
77 /** 84 /**
78 - * 生成模式  
79 - */  
80 - private Integer generateMode;  
81 -  
82 - /**  
83 * 平台 85 * 平台
84 * <p> 86 * <p>
85 - * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum} 87 + * 枚举 {@link AiPlatformEnum}
86 */ 88 */
87 private String platform; 89 private String platform;
88 -  
89 /** 90 /**
90 * 模型 91 * 模型
91 */ 92 */
92 private String model; 93 private String model;
93 94
94 /** 95 /**
95 - * 错误信息  
96 - */  
97 - private String errorMessage;  
98 -  
99 - /**  
100 * 音乐风格标签 96 * 音乐风格标签
101 */ 97 */
102 @TableField(typeHandler = JacksonTypeHandler.class) 98 @TableField(typeHandler = JacksonTypeHandler.class)
@@ -107,4 +103,9 @@ public class AiMusicDO extends BaseDO { @@ -107,4 +103,9 @@ public class AiMusicDO extends BaseDO {
107 */ 103 */
108 private String taskId; 104 private String taskId;
109 105
  106 + /**
  107 + * 错误信息
  108 + */
  109 + private String errorMessage;
  110 +
110 } 111 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java
1 package cn.iocoder.yudao.module.ai.dal.mysql.music; 1 package cn.iocoder.yudao.module.ai.dal.mysql.music;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;  
5 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; 4 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
6 import org.apache.ibatis.annotations.Mapper; 5 import org.apache.ibatis.annotations.Mapper;
7 6
@@ -16,8 +15,7 @@ import java.util.List; @@ -16,8 +15,7 @@ import java.util.List;
16 public interface AiMusicMapper extends BaseMapperX<AiMusicDO> { 15 public interface AiMusicMapper extends BaseMapperX<AiMusicDO> {
17 16
18 default List<AiMusicDO> selectListByStatus(Integer status) { 17 default List<AiMusicDO> selectListByStatus(Integer status) {
19 - return selectList(new LambdaQueryWrapperX<AiMusicDO>()  
20 - .eq(AiMusicDO::getStatus, status)); 18 + return selectList(AiMusicDO::getStatus, status);
21 } 19 }
22 20
23 } 21 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
@@ -14,10 +14,11 @@ public interface AiMusicService { @@ -14,10 +14,11 @@ public interface AiMusicService {
14 /** 14 /**
15 * 音乐生成 15 * 音乐生成
16 * 16 *
  17 + * @param userId 用户编号
17 * @param reqVO 请求参数 18 * @param reqVO 请求参数
18 * @return 生成的音乐ID 19 * @return 生成的音乐ID
19 */ 20 */
20 - List<Long> generateMusic(AiSunoGenerateReqVO reqVO); 21 + List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO);
21 22
22 /** 23 /**
23 * 同步音乐任务 24 * 同步音乐任务
@@ -25,4 +26,5 @@ public interface AiMusicService { @@ -25,4 +26,5 @@ public interface AiMusicService {
25 * @return 同步数量 26 * @return 同步数量
26 */ 27 */
27 Integer syncMusic(); 28 Integer syncMusic();
  29 +
28 } 30 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
@@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil; @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.text.StrPool; 4 import cn.hutool.core.text.StrPool;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
7 -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;  
8 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; 7 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
9 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; 8 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
10 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; 9 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
@@ -16,7 +15,8 @@ import org.springframework.stereotype.Service; @@ -16,7 +15,8 @@ import org.springframework.stereotype.Service;
16 15
17 import java.util.*; 16 import java.util.*;
18 17
19 -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 18 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  19 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
20 20
21 /** 21 /**
22 * AI 音乐 Service 实现类 22 * AI 音乐 Service 实现类
@@ -34,54 +34,53 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -34,54 +34,53 @@ public class AiMusicServiceImpl implements AiMusicService {
34 private AiMusicMapper musicMapper; 34 private AiMusicMapper musicMapper;
35 35
36 @Override 36 @Override
37 - public List<Long> generateMusic(AiSunoGenerateReqVO reqVO) { 37 + public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
  38 + // 1. 调用 Suno 生成音乐
38 List<SunoApi.MusicData> musicDataList; 39 List<SunoApi.MusicData> musicDataList;
39 if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { 40 if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
40 // 1.1 歌词模式 41 // 1.1 歌词模式
41 - SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest( 42 + SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
42 reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); 43 reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
43 - musicDataList = sunoApi.customGenerate(sunoReq); 44 + musicDataList = sunoApi.customGenerate(generateRequest);
44 } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { 45 } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
45 // 1.2 描述模式 46 // 1.2 描述模式
46 - SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest( 47 + SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
47 reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental()); 48 reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental());
48 - musicDataList = sunoApi.generate(sunoReq); 49 + musicDataList = sunoApi.generate(generateRequest);
49 } else { 50 } else {
50 throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO)); 51 throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO));
51 } 52 }
  53 +
52 // 2. 插入数据库 54 // 2. 插入数据库
53 if (CollUtil.isEmpty(musicDataList)) { 55 if (CollUtil.isEmpty(musicDataList)) {
54 -  
55 return Collections.emptyList(); 56 return Collections.emptyList();
56 } 57 }
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); 58 + List<AiMusicDO> musicList = buildMusicDOList(musicDataList);
  59 + musicList.forEach(music -> music.setUserId(userId).setPlatform(music.getPlatform()).setGenerateMode(reqVO.getGenerateMode()));
  60 + musicMapper.insertBatch(musicList);
  61 + return convertList(musicList, AiMusicDO::getId);
64 } 62 }
65 63
66 @Override 64 @Override
67 public Integer syncMusic() { 65 public Integer syncMusic() {
68 - List<AiMusicDO> streamingTask = musicMapper.selectListByStatus(AiMusicStatusEnum.STREAMING.getStatus()); 66 + List<AiMusicDO> streamingTask = musicMapper.selectListByStatus(AiMusicStatusEnum.IN_PROGRESS.getStatus());
69 if (CollUtil.isEmpty(streamingTask)) { 67 if (CollUtil.isEmpty(streamingTask)) {
70 return 0; 68 return 0;
71 } 69 }
72 log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); 70 log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size());
  71 +
73 // GET 请求,为避免参数过长,分批次处理 72 // GET 请求,为避免参数过长,分批次处理
74 - CollUtil.split(streamingTask, 36).forEach(chunk -> {  
75 - Map<String, Long> taskIdMap = CollectionUtils.convertMap(chunk, AiMusicDO::getTaskId, AiMusicDO::getId); 73 + CollUtil.split(streamingTask, 36).forEach(chunkList -> {
  74 + Map<String, Long> taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId);
76 List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); 75 List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
77 if (CollUtil.isEmpty(musicTaskList)) { 76 if (CollUtil.isEmpty(musicTaskList)) {
78 log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet()); 77 log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet());
79 return; 78 return;
80 } 79 }
81 - List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList);  
82 - //回填id  
83 - aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId())));  
84 - musicMapper.updateBatch(aiMusicDOS); 80 + // 更新进度
  81 + List<AiMusicDO> updateMusicList = buildMusicDOList(musicTaskList);
  82 + updateMusicList.forEach(music -> music.setId(taskIdMap.get(music.getTaskId())));
  83 + musicMapper.updateBatch(updateMusicList);
85 }); 84 });
86 return streamingTask.size(); 85 return streamingTask.size();
87 } 86 }
@@ -89,16 +88,16 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -89,16 +88,16 @@ public class AiMusicServiceImpl implements AiMusicService {
89 /** 88 /**
90 * 构建 AiMusicDO 集合 89 * 构建 AiMusicDO 集合
91 * 90 *
92 - * @param musicTaskList suno 音乐任务列表 91 + * @param musicList suno 音乐任务列表
93 * @return AiMusicDO 集合 92 * @return AiMusicDO 集合
94 */ 93 */
95 - private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicTaskList) {  
96 - return CollectionUtils.convertList(musicTaskList, musicData -> new AiMusicDO()  
97 - .setTaskId(musicData.id()) 94 + private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
  95 + return convertList(musicList, musicData -> new AiMusicDO()
  96 + .setTaskId(musicData.id()).setModel(musicData.modelName())
98 .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) 97 .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
99 .setAudioUrl(musicData.audioUrl()).setVideoUrl(musicData.videoUrl()).setImageUrl(musicData.imageUrl()) 98 .setAudioUrl(musicData.audioUrl()).setVideoUrl(musicData.videoUrl()).setImageUrl(musicData.imageUrl())
100 .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA)) 99 .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())); 100 + .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
102 101
103 } 102 }
104 } 103 }