mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-31 10:18:42 +08:00 
			
		
		
		
	【解决todo】AI:音乐接入
This commit is contained in:
		| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.util.List; | ||||
| @@ -10,20 +11,19 @@ import java.util.List; | ||||
| @Data | ||||
| public class AiSunoGenerateReqVO { | ||||
|  | ||||
|     // TODO @xin:每个参数,必要的是否必填校验 | ||||
|     @Schema(description = "用于生成音乐音频的提示", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") | ||||
|     @Schema(description = "用于生成音乐音频的提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") | ||||
|     private String prompt; | ||||
|  | ||||
|     @Schema(description = "是否纯音乐", example = "true") | ||||
|     @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true") | ||||
|     private Boolean makeInstrumental; | ||||
|  | ||||
|     @Schema(description = "模型版本, 默认 chirp-v3.5", example = "chirp-v3.5") | ||||
|     @Schema(description = "模型版本, 默认 chirp-v3.5", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "chirp-v3.5") | ||||
|     private String modelVersion; // 参见 AiModelEnum 枚举 | ||||
|  | ||||
|     @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]") | ||||
|     @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]") | ||||
|     private List<String> tags; | ||||
|  | ||||
|     @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星") | ||||
|     @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星") | ||||
|     private String title; | ||||
|  | ||||
|     @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno") | ||||
| @@ -31,7 +31,7 @@ public class AiSunoGenerateReqVO { | ||||
|     private String platform; // 参见 AiPlatformEnum 枚举 | ||||
|  | ||||
|     @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | ||||
|     @NotBlank(message = "生成模式不能为空") | ||||
|     private String generateMode; // 参见 AiMusicGenerateEnum 枚举 | ||||
|     @NotNull(message = "生成模式不能为空") | ||||
|     private Integer generateMode; // 参见 AiMusicGenerateEnum 枚举 | ||||
|  | ||||
| } | ||||
| @@ -63,7 +63,7 @@ public class AiMusicDO extends BaseDO { | ||||
|      * <p> | ||||
|      * 枚举 {@link AiMusicStatusEnum} | ||||
|      */ | ||||
|     private String status; | ||||
|     private Integer status; | ||||
|  | ||||
|     /** | ||||
|      * 描述词 | ||||
| @@ -77,7 +77,7 @@ public class AiMusicDO extends BaseDO { | ||||
|     /** | ||||
|      * 生成模式 | ||||
|      */ | ||||
|     private String generateMode; | ||||
|     private Integer generateMode; | ||||
|  | ||||
|     /** | ||||
|      * 平台 | ||||
|   | ||||
| @@ -1,13 +1,23 @@ | ||||
| package cn.iocoder.yudao.module.ai.dal.mysql.music; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||||
| import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||||
| import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | ||||
| import org.apache.ibatis.annotations.Mapper; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * AI 音乐 Mapper | ||||
|  * @author  xiaoxin | ||||
|  * | ||||
|  * @author xiaoxin | ||||
|  */ | ||||
| @Mapper | ||||
| public interface AiMusicMapper extends BaseMapperX<AiMusicDO> { | ||||
|  | ||||
|     default List<AiMusicDO> selectListByStatus(Integer status) { | ||||
|         return selectList(new LambdaQueryWrapperX<AiMusicDO>() | ||||
|                 .eq(AiMusicDO::getStatus, status)); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package cn.iocoder.yudao.module.ai.job.sun; | ||||
| package cn.iocoder.yudao.module.ai.job.music; | ||||
| 
 | ||||
| import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | ||||
| import cn.iocoder.yudao.module.ai.service.music.AiMusicService; | ||||
| @@ -1,7 +1,6 @@ | ||||
| package cn.iocoder.yudao.module.ai.service.music; | ||||
|  | ||||
| import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; | ||||
| import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @@ -20,26 +19,10 @@ public interface AiMusicService { | ||||
|      */ | ||||
|     List<Long> generateMusic(AiSunoGenerateReqVO reqVO); | ||||
|  | ||||
|     /** | ||||
|      * 获取未完成状态的任务 | ||||
|      * | ||||
|      * @return 未完成任务列表 | ||||
|      */ | ||||
|     List<AiMusicDO> getUnCompletedTask(); | ||||
|  | ||||
|     /** | ||||
|      * 同步音乐任务 | ||||
|      * | ||||
|      * @return 同步数量 | ||||
|      */ | ||||
|     Integer syncMusic(); | ||||
|  | ||||
|     /** | ||||
|      * 批量更新音乐信息 | ||||
|      * | ||||
|      * @param musicDOS 音乐信息 | ||||
|      * @return 是否成功 | ||||
|      */ | ||||
|     Boolean updateBatch(List<AiMusicDO> musicDOS); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -8,15 +8,13 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | ||||
| import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; | ||||
| import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | ||||
| import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; | ||||
| import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum; | ||||
| import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; | ||||
| import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import jakarta.annotation.Resource; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.*; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | ||||
|  | ||||
| @@ -38,87 +36,54 @@ public class AiMusicServiceImpl implements AiMusicService { | ||||
|     @Override | ||||
|     public List<Long> generateMusic(AiSunoGenerateReqVO reqVO) { | ||||
|         List<SunoApi.MusicData> musicDataList; | ||||
|         if (Objects.equals(AiMusicGenerateEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { | ||||
|         if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { | ||||
|             // 1.1 歌词模式 | ||||
|             SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest( | ||||
|                     reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); | ||||
|             musicDataList = sunoApi.customGenerate(sunoReq); | ||||
|         } else if (Objects.equals(AiMusicGenerateEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { | ||||
|         } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { | ||||
|             // 1.2 描述模式 | ||||
|             SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest( | ||||
|                     reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental()); | ||||
|             musicDataList = sunoApi.generate(sunoReq); | ||||
|         } else { | ||||
|             // TODO @xin:不用 log error,直接抛异常,吧 reqVO 呆进去,有全局处理的哈 | ||||
|             log.error("未知的生成模式:{}", reqVO.getGenerateMode()); | ||||
|             throw new IllegalArgumentException("未知的生成模式"); | ||||
|             throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO)); | ||||
|         } | ||||
|  | ||||
|         // 2. 插入数据库 | ||||
|         // TODO @xin:因为 insertMusicData 复用的比较少,所以不用愁单独的方法,直接写在这里就好啦 | ||||
|         return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); | ||||
|     } | ||||
|         if (CollUtil.isEmpty(musicDataList)) { | ||||
|  | ||||
|     // TODO @xin:1)service 里面,不要直接查询 db;2)不要用 ne,用 STREAMING 哈 | ||||
|     @Override | ||||
|     public List<AiMusicDO> getUnCompletedTask() { | ||||
|         return musicMapper.selectList(new LambdaQueryWrapper<AiMusicDO>().ne(AiMusicDO::getStatus, AiMusicStatusEnum.COMPLETE.getStatus())); | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|         List<AiMusicDO> aiMusicDOList = CollectionUtils.convertList(buildMusicDOList(musicDataList), musicDO -> | ||||
|                 musicDO.setUserId(getLoginUserId()) | ||||
|                         .setGenerateMode(reqVO.getGenerateMode()) | ||||
|                         .setPlatform(reqVO.getPlatform() | ||||
|                         )); | ||||
|         musicMapper.insertBatch(aiMusicDOList); | ||||
|         return CollectionUtils.convertList(aiMusicDOList, AiMusicDO::getId); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Integer syncMusic() { | ||||
|         List<AiMusicDO> unCompletedTask = this.getUnCompletedTask(); | ||||
|         if (CollUtil.isEmpty(unCompletedTask)) { | ||||
|             // TODO @xin:这里不用打,反正 Job 也打了 | ||||
|             log.info("Suno 无进行中任务需要更新!"); | ||||
|         List<AiMusicDO> streamingTask = musicMapper.selectListByStatus(AiMusicStatusEnum.STREAMING.getStatus()); | ||||
|         if (CollUtil.isEmpty(streamingTask)) { | ||||
|             return 0; | ||||
|         } | ||||
|         log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", unCompletedTask.size()); | ||||
|         log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); | ||||
|         // GET 请求,为避免参数过长,分批次处理 | ||||
|         // TODO @xin:建议批量更大一些。 | ||||
|         CollUtil.split(unCompletedTask, 4).forEach(chunk -> { | ||||
|             // TODO @xin:可以使用 CollectionUtils 里的 map 转换 | ||||
|             Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId); | ||||
|         CollUtil.split(streamingTask, 36).forEach(chunk -> { | ||||
|             Map<String, Long> taskIdMap = CollectionUtils.convertMap(chunk, AiMusicDO::getTaskId, AiMusicDO::getId); | ||||
|             List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); | ||||
|             // TODO @xin:查询不到,直接 return;这样真正逻辑的 85 - 87 就不用多一层括号 | ||||
|             if (CollUtil.isNotEmpty(musicTaskList)) { | ||||
|                 List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList); | ||||
|                 //回填id | ||||
|                 aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId()))); | ||||
|                 this.updateBatch(aiMusicDOS); | ||||
|             } else { | ||||
|             if (CollUtil.isEmpty(musicTaskList)) { | ||||
|                 log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet()); | ||||
|                 return; | ||||
|             } | ||||
|             List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList); | ||||
|             //回填id | ||||
|             aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId()))); | ||||
|             musicMapper.updateBatch(aiMusicDOS); | ||||
|         }); | ||||
|         return unCompletedTask.size(); | ||||
|     } | ||||
|  | ||||
|     // TODO @xin:这个方法,看着不用啦 | ||||
|     @Override | ||||
|     public Boolean updateBatch(List<AiMusicDO> musicDOS) { | ||||
|         return musicMapper.updateBatch(musicDOS); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 新增音乐数据并提交 suno任务 | ||||
|      * | ||||
|      * @param musicDataList 音乐数据列表 | ||||
|      * @return 音乐id集合 | ||||
|      */ | ||||
|     private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList, String generateMode, String platform) { | ||||
|         if (CollUtil.isEmpty(musicDataList)) { | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
|         List<AiMusicDO> aiMusicDOList = buildMusicDOList(musicDataList).stream() | ||||
|                 .map(musicDO -> musicDO.setUserId(getLoginUserId()) | ||||
|                         .setGenerateMode(generateMode) | ||||
|                         .setPlatform(platform)) | ||||
|                 .toList(); | ||||
|         musicMapper.insertBatch(aiMusicDOList); | ||||
|         // TODO @xin:用 CollectionUtils 简化操作 | ||||
|         return aiMusicDOList.stream() | ||||
|                 .map(AiMusicDO::getId) | ||||
|                 .collect(Collectors.toList()); | ||||
|         return streamingTask.size(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -140,8 +105,7 @@ public class AiMusicServiceImpl implements AiMusicService { | ||||
|                 .setTitle(musicData.title()) | ||||
|                 .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.COMPLETE.getStatus() : AiMusicStatusEnum.STREAMING.getStatus()) | ||||
|                 .setModel(musicData.modelName()) | ||||
|                 // TODO @xin:可以用 hutool 的 StrUtil 的 split 之类的 | ||||
|                 .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null)); | ||||
|                 .setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 xiaoxin
					xiaoxin