Commit 5ba38dbdaae93cd321bc71b1255796a48c0ef8a5
1 parent
88142ed7
【解决todo】AI:音乐接入
Showing
10 changed files
with
144 additions
and
204 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateEnum.java
| @@ -12,9 +12,8 @@ import lombok.Getter; | @@ -12,9 +12,8 @@ import lombok.Getter; | ||
| 12 | @Getter | 12 | @Getter |
| 13 | public enum AiMusicGenerateEnum { | 13 | public enum AiMusicGenerateEnum { |
| 14 | 14 | ||
| 15 | - // TODO @xin:用数字哈。项目目前枚举都是数字 | ||
| 16 | - LYRIC("lyric", "歌词模式"), | ||
| 17 | - DESCRIPTION("description", "描述模式"); | 15 | + LYRIC("1", "歌词模式"), |
| 16 | + DESCRIPTION("2", "描述模式"); | ||
| 18 | 17 | ||
| 19 | /** | 18 | /** |
| 20 | * 模式 | 19 | * 模式 |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
| @@ -12,27 +12,18 @@ import lombok.Getter; | @@ -12,27 +12,18 @@ import lombok.Getter; | ||
| 12 | @Getter | 12 | @Getter |
| 13 | public enum AiMusicStatusEnum { | 13 | public enum AiMusicStatusEnum { |
| 14 | 14 | ||
| 15 | - // TODO @xin:用数字哈。项目目前枚举都是数字 | ||
| 16 | // @xin 文档中无失败这个返回值 | 15 | // @xin 文档中无失败这个返回值 |
| 17 | - STREAMING("streaming", "进行中"), | ||
| 18 | - COMPLETE("complete", "完成"); | 16 | + STREAMING("10", "进行中"), |
| 17 | + COMPLETE("20", "完成"); | ||
| 19 | 18 | ||
| 20 | /** | 19 | /** |
| 21 | * 状态 | 20 | * 状态 |
| 22 | */ | 21 | */ |
| 23 | private final String status; | 22 | private final String status; |
| 23 | + | ||
| 24 | /** | 24 | /** |
| 25 | * 状态名 | 25 | * 状态名 |
| 26 | */ | 26 | */ |
| 27 | private final String name; | 27 | private final String name; |
| 28 | 28 | ||
| 29 | - public static AiMusicStatusEnum valueOfStatus(String status) { | ||
| 30 | - for (AiMusicStatusEnum statusEnum : AiMusicStatusEnum.values()) { | ||
| 31 | - if (statusEnum.getStatus().equals(status)) { | ||
| 32 | - return statusEnum; | ||
| 33 | - } | ||
| 34 | - } | ||
| 35 | - throw new IllegalArgumentException("未知会话状态: " + status); | ||
| 36 | - } | ||
| 37 | - | ||
| 38 | } | 29 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
| 1 | package cn.iocoder.yudao.module.ai.controller.admin.music; | 1 | package cn.iocoder.yudao.module.ai.controller.admin.music; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | 4 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; |
| 5 | import cn.iocoder.yudao.module.ai.service.music.AiMusicService; | 5 | import cn.iocoder.yudao.module.ai.service.music.AiMusicService; |
| 6 | import io.swagger.v3.oas.annotations.Operation; | 6 | import io.swagger.v3.oas.annotations.Operation; |
| 7 | import io.swagger.v3.oas.annotations.tags.Tag; | 7 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 8 | +import jakarta.annotation.Resource; | ||
| 8 | import jakarta.validation.Valid; | 9 | import jakarta.validation.Valid; |
| 9 | -import lombok.RequiredArgsConstructor; | ||
| 10 | import org.springframework.web.bind.annotation.PostMapping; | 10 | import org.springframework.web.bind.annotation.PostMapping; |
| 11 | import org.springframework.web.bind.annotation.RequestBody; | 11 | import org.springframework.web.bind.annotation.RequestBody; |
| 12 | import org.springframework.web.bind.annotation.RequestMapping; | 12 | import org.springframework.web.bind.annotation.RequestMapping; |
| @@ -19,16 +19,15 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | @@ -19,16 +19,15 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 19 | @Tag(name = "管理后台 - AI 音乐") | 19 | @Tag(name = "管理后台 - AI 音乐") |
| 20 | @RestController | 20 | @RestController |
| 21 | @RequestMapping("/ai/music") | 21 | @RequestMapping("/ai/music") |
| 22 | -@RequiredArgsConstructor // TODO @xin:通过 @Resource 注入哈 | ||
| 23 | public class AiMusicController { | 22 | public class AiMusicController { |
| 24 | 23 | ||
| 25 | - // TODO @xin:变量不用有 ai 前缀 | ||
| 26 | - private final AiMusicService aiMusicService; | 24 | + @Resource |
| 25 | + private AiMusicService musicService; | ||
| 27 | 26 | ||
| 28 | @PostMapping("/generate") | 27 | @PostMapping("/generate") |
| 29 | @Operation(summary = "音乐生成") | 28 | @Operation(summary = "音乐生成") |
| 30 | - public CommonResult<List<Long>> generateMusic(@RequestBody @Valid SunoReqVO sunoReqVO) { | ||
| 31 | - return success(aiMusicService.generateMusic(sunoReqVO)); | 29 | + public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) { |
| 30 | + return success(musicService.generateMusic(reqVO)); | ||
| 32 | } | 31 | } |
| 33 | 32 | ||
| 34 | } | 33 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.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.NotBlank; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * @author xiaoxin | ||
| 11 | + */ | ||
| 12 | +@Schema(description = "管理后台 - 音乐生成 Request VO") | ||
| 13 | +@Data | ||
| 14 | +public class AiSunoGenerateReqVO { | ||
| 15 | + | ||
| 16 | + @Schema(description = "用于生成音乐音频的提示", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") | ||
| 17 | + private String prompt; | ||
| 18 | + | ||
| 19 | + @Schema(description = "是否纯音乐", example = "true") | ||
| 20 | + private Boolean makeInstrumental; | ||
| 21 | + | ||
| 22 | + @Schema(description = "模型版本, 默认 chirp-v3.5", example = "chirp-v3.5") | ||
| 23 | + private String modelVersion;// 参见 AiModelEnum 枚举 | ||
| 24 | + | ||
| 25 | + @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]") | ||
| 26 | + private List<String> tags; | ||
| 27 | + | ||
| 28 | + @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星") | ||
| 29 | + private String title; | ||
| 30 | + | ||
| 31 | + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno") | ||
| 32 | + @NotBlank(message = "平台不能为空") | ||
| 33 | + private String platform;// 参见 AiPlatformEnum 枚举 | ||
| 34 | + | ||
| 35 | + @Schema(description = "生成模式 1(歌词模式), 2(描述模式)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | ||
| 36 | + @NotBlank(message = "生成模式不能为空") | ||
| 37 | + private String generateMode;// 参见 AiMusicGenerateEnum 枚举 | ||
| 38 | + | ||
| 39 | +} | ||
| 0 | \ No newline at end of file | 40 | \ 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/SunoReqVO.java deleted
| 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.NotBlank; | ||
| 5 | -import lombok.Data; | ||
| 6 | - | ||
| 7 | -import java.util.List; | ||
| 8 | - | ||
| 9 | -// TODO @xin:1)ai 前缀;2)AiSunoGenerateReqVO,要有生成哈。3)swaggger 缺少的属性,也最好加下,类似 example,类上的 swagger 等 | ||
| 10 | -/** | ||
| 11 | - * @author xiaoxin | ||
| 12 | - */ | ||
| 13 | -@Data | ||
| 14 | -public class SunoReqVO { | ||
| 15 | - | ||
| 16 | - @Schema(description = "用于生成音乐音频的提示") | ||
| 17 | - private String prompt; | ||
| 18 | - | ||
| 19 | - @Schema(description = "是否纯音乐") | ||
| 20 | - private Boolean makeInstrumental; | ||
| 21 | - | ||
| 22 | - // TODO @xin:我们自己是不是用 modelVersion?还是什么梗精准,减少非必要的缩写 | ||
| 23 | - @Schema(description = "模型版本") | ||
| 24 | - private String mv; | ||
| 25 | - | ||
| 26 | - @Schema(description = "音乐风格") | ||
| 27 | - private List<String> tags; | ||
| 28 | - | ||
| 29 | - @Schema(description = "音乐/歌曲名称") | ||
| 30 | - private String title; | ||
| 31 | - | ||
| 32 | - @Schema(description = "平台") | ||
| 33 | - @NotBlank(message = "平台不能为空") | ||
| 34 | - private String platform; | ||
| 35 | - | ||
| 36 | - @Schema(description = "生成模式 lyric(歌词模式), description(描述模式)") | ||
| 37 | - @NotBlank(message = "生成模式不能为空") | ||
| 38 | - private String generateMode; | ||
| 39 | - | ||
| 40 | -} | ||
| 41 | \ No newline at end of file | 0 | \ 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.common.util.json.JsonUtils; | ||
| 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | 4 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; |
| 6 | import com.baomidou.mybatisplus.annotation.IdType; | 5 | import com.baomidou.mybatisplus.annotation.IdType; |
| 7 | import com.baomidou.mybatisplus.annotation.TableField; | 6 | import com.baomidou.mybatisplus.annotation.TableField; |
| 8 | import com.baomidou.mybatisplus.annotation.TableId; | 7 | import com.baomidou.mybatisplus.annotation.TableId; |
| 9 | import com.baomidou.mybatisplus.annotation.TableName; | 8 | import com.baomidou.mybatisplus.annotation.TableName; |
| 10 | -import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; | 9 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; |
| 11 | import lombok.Data; | 10 | import lombok.Data; |
| 12 | 11 | ||
| 13 | import java.util.List; | 12 | import java.util.List; |
| 14 | 13 | ||
| 15 | -// TODO @xin:注释完善下 | 14 | + |
| 16 | /** | 15 | /** |
| 17 | - * @Author xiaoxin | ||
| 18 | - * @Date 2024/6/5 | 16 | + * AI 音乐 DO |
| 17 | + * | ||
| 18 | + * @author xiaoxin | ||
| 19 | */ | 19 | */ |
| 20 | @TableName("ai_music") | 20 | @TableName("ai_music") |
| 21 | @Data | 21 | @Data |
| @@ -98,25 +98,11 @@ public class AiMusicDO extends BaseDO { | @@ -98,25 +98,11 @@ public class AiMusicDO extends BaseDO { | ||
| 98 | /** | 98 | /** |
| 99 | * 音乐风格标签 | 99 | * 音乐风格标签 |
| 100 | */ | 100 | */ |
| 101 | - @TableField(typeHandler = AiMusicTagsHandler.class) | 101 | + @TableField(typeHandler = JacksonTypeHandler.class) |
| 102 | private List<String> tags; | 102 | private List<String> tags; |
| 103 | 103 | ||
| 104 | /** | 104 | /** |
| 105 | * 任务编号 | 105 | * 任务编号 |
| 106 | */ | 106 | */ |
| 107 | private String taskId; | 107 | private String taskId; |
| 108 | - | ||
| 109 | - // TODO @xin:用 @TableField(typeHandler = JacksonTypeHandler.class) 替代即可 | ||
| 110 | - public static class AiMusicTagsHandler extends AbstractJsonTypeHandler<Object> { | ||
| 111 | - | ||
| 112 | - @Override | ||
| 113 | - protected Object parse(String json) { | ||
| 114 | - return JsonUtils.parseArray(json, String.class); | ||
| 115 | - } | ||
| 116 | - | ||
| 117 | - @Override | ||
| 118 | - protected String toJson(Object obj) { | ||
| 119 | - return JsonUtils.toJsonString(obj); | ||
| 120 | - } | ||
| 121 | - } | ||
| 122 | } | 108 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/SunoJob.java
| 1 | package cn.iocoder.yudao.module.ai.job; | 1 | package cn.iocoder.yudao.module.ai.job; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.collection.CollUtil; | ||
| 4 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||
| 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | 3 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; |
| 6 | -import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | ||
| 7 | -import cn.iocoder.yudao.module.ai.service.music.AiMusicConvert; | ||
| 8 | import cn.iocoder.yudao.module.ai.service.music.AiMusicService; | 4 | import cn.iocoder.yudao.module.ai.service.music.AiMusicService; |
| 9 | import jakarta.annotation.Resource; | 5 | import jakarta.annotation.Resource; |
| 10 | import lombok.extern.slf4j.Slf4j; | 6 | import lombok.extern.slf4j.Slf4j; |
| 11 | import org.springframework.stereotype.Component; | 7 | import org.springframework.stereotype.Component; |
| 12 | 8 | ||
| 13 | -import java.util.ArrayList; | ||
| 14 | -import java.util.HashMap; | ||
| 15 | -import java.util.List; | ||
| 16 | -import java.util.Map; | ||
| 17 | 9 | ||
| 18 | -// TODO @xin:不要直接叫这个名字哈,要有它的目的 | ||
| 19 | /** | 10 | /** |
| 20 | - * 处理 Suno Job | 11 | + * 同步 Suno 任务状态以及回写对应的音乐信息 Job |
| 12 | + * | ||
| 21 | * @author xiaoxin | 13 | * @author xiaoxin |
| 22 | */ | 14 | */ |
| 23 | @Component | 15 | @Component |
| @@ -25,37 +17,12 @@ import java.util.Map; | @@ -25,37 +17,12 @@ import java.util.Map; | ||
| 25 | public class SunoJob implements JobHandler { | 17 | public class SunoJob implements JobHandler { |
| 26 | 18 | ||
| 27 | @Resource | 19 | @Resource |
| 28 | - private SunoApi sunoApi; | ||
| 29 | - @Resource | ||
| 30 | private AiMusicService musicService; | 20 | private AiMusicService musicService; |
| 31 | 21 | ||
| 32 | @Override | 22 | @Override |
| 33 | public String execute(String param) { | 23 | public String execute(String param) { |
| 34 | - // TODO @xin:可以考虑,整个逻辑都下沉到 Service 里,有点类似 AccessLogCleanJob | ||
| 35 | - List<AiMusicDO> unCompletedTask = musicService.getUnCompletedTask(); | ||
| 36 | - | ||
| 37 | - if (CollUtil.isEmpty(unCompletedTask)) { | ||
| 38 | - log.info("Suno 无进行中任务需要更新!"); | ||
| 39 | - return "Suno 无进行中任务需要更新!"; | ||
| 40 | - } | ||
| 41 | - | ||
| 42 | - | ||
| 43 | - log.info("Suno 开始同步, 共 [{}] 个任务!", unCompletedTask.size()); | ||
| 44 | - //GET 请求,为避免参数过长,分批次处理 | ||
| 45 | - CollUtil.split(unCompletedTask, 4) | ||
| 46 | - .forEach(chunk -> { | ||
| 47 | - Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId); | ||
| 48 | - List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); | ||
| 49 | - if (CollUtil.isNotEmpty(musicTaskList)) { | ||
| 50 | - List<AiMusicDO> aiMusicDOS = AiMusicConvert.convertFrom(musicTaskList); | ||
| 51 | - //回填id | ||
| 52 | - aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId()))); | ||
| 53 | - musicService.updateBatch(aiMusicDOS); | ||
| 54 | - } else { | ||
| 55 | - log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet()); | ||
| 56 | - } | ||
| 57 | - }); | ||
| 58 | - | ||
| 59 | - return "Suno 同步 - ".concat(String.valueOf(unCompletedTask.size())).concat(" 个任务!"); | 24 | + Integer count = musicService.syncMusicTask(); |
| 25 | + log.info("[execute][Suno 同步任务数量 [{}] 个]", count); | ||
| 26 | + return String.format("Suno 同步 - [%s] 个任务", count); | ||
| 60 | } | 27 | } |
| 61 | } | 28 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicConvert.java deleted
| 1 | -package cn.iocoder.yudao.module.ai.service.music; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.text.StrPool; | ||
| 4 | -import cn.hutool.core.util.StrUtil; | ||
| 5 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||
| 6 | -import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | ||
| 7 | - | ||
| 8 | -import java.util.List; | ||
| 9 | -import java.util.stream.Collectors; | ||
| 10 | - | ||
| 11 | -/** | ||
| 12 | - * AI 音乐 Convert | ||
| 13 | - * | ||
| 14 | - * @author xiaoxin | ||
| 15 | - */ | ||
| 16 | -public class AiMusicConvert { | ||
| 17 | - | ||
| 18 | - public static AiMusicDO convertFrom(SunoApi.MusicData musicData) { | ||
| 19 | - return new AiMusicDO() | ||
| 20 | - .setTaskId(musicData.id()) | ||
| 21 | - .setPrompt(musicData.prompt()) | ||
| 22 | - .setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) | ||
| 23 | - .setAudioUrl(musicData.audioUrl()) | ||
| 24 | - .setVideoUrl(musicData.videoUrl()) | ||
| 25 | - .setImageUrl(musicData.imageUrl()) | ||
| 26 | - .setLyric(musicData.lyric()) | ||
| 27 | - .setTitle(musicData.title()) | ||
| 28 | - .setStatus(musicData.status()) | ||
| 29 | - .setModel(musicData.modelName()) | ||
| 30 | - .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null); | ||
| 31 | - } | ||
| 32 | - | ||
| 33 | - // TODO @xin:一般情况下,不用 convert,直接逻辑里 convert 就好啦。 | ||
| 34 | - public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> list) { | ||
| 35 | - // TODO @xin:可以使用 CollectionUtils.convertList 简洁一点 | ||
| 36 | - return list.stream() | ||
| 37 | - .map(AiMusicConvert::convertFrom) | ||
| 38 | - .collect(Collectors.toList()); | ||
| 39 | - } | ||
| 40 | - | ||
| 41 | - | ||
| 42 | -} |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
| 1 | package cn.iocoder.yudao.module.ai.service.music; | 1 | package cn.iocoder.yudao.module.ai.service.music; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | 3 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; |
| 4 | import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | 4 | import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; |
| 5 | 5 | ||
| 6 | import java.util.List; | 6 | import java.util.List; |
| @@ -18,7 +18,7 @@ public interface AiMusicService { | @@ -18,7 +18,7 @@ public interface AiMusicService { | ||
| 18 | * @param reqVO 请求参数 | 18 | * @param reqVO 请求参数 |
| 19 | * @return 生成的音乐ID | 19 | * @return 生成的音乐ID |
| 20 | */ | 20 | */ |
| 21 | - List<Long> generateMusic(SunoReqVO reqVO); | 21 | + List<Long> generateMusic(AiSunoGenerateReqVO reqVO); |
| 22 | 22 | ||
| 23 | /** | 23 | /** |
| 24 | * 获取未完成状态的任务 | 24 | * 获取未完成状态的任务 |
| @@ -27,6 +27,20 @@ public interface AiMusicService { | @@ -27,6 +27,20 @@ public interface AiMusicService { | ||
| 27 | */ | 27 | */ |
| 28 | List<AiMusicDO> getUnCompletedTask(); | 28 | List<AiMusicDO> getUnCompletedTask(); |
| 29 | 29 | ||
| 30 | - Boolean updateBatch(List<AiMusicDO> aiMusicDOList); | 30 | + |
| 31 | + /** | ||
| 32 | + * 同步音乐任务 | ||
| 33 | + * | ||
| 34 | + * @return 同步数量 | ||
| 35 | + */ | ||
| 36 | + Integer syncMusicTask(); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 批量更新音乐信息 | ||
| 40 | + * | ||
| 41 | + * @param musicDOS 音乐信息 | ||
| 42 | + * @return 是否成功 | ||
| 43 | + */ | ||
| 44 | + Boolean updateBatch(List<AiMusicDO> musicDOS); | ||
| 31 | 45 | ||
| 32 | } | 46 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
| @@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.ai.service.music; | @@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.ai.service.music; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | 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.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 6 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | 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.dal.dataobject.music.AiMusicDO; | 9 | import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; |
| 8 | import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; | 10 | import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; |
| 9 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum; | 11 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum; |
| @@ -13,8 +15,7 @@ import jakarta.annotation.Resource; | @@ -13,8 +15,7 @@ import jakarta.annotation.Resource; | ||
| 13 | import lombok.extern.slf4j.Slf4j; | 15 | import lombok.extern.slf4j.Slf4j; |
| 14 | import org.springframework.stereotype.Service; | 16 | import org.springframework.stereotype.Service; |
| 15 | 17 | ||
| 16 | -import java.util.Collections; | ||
| 17 | -import java.util.List; | 18 | +import java.util.*; |
| 18 | import java.util.stream.Collectors; | 19 | import java.util.stream.Collectors; |
| 19 | 20 | ||
| 20 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | 21 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| @@ -35,13 +36,22 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -35,13 +36,22 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 35 | private AiMusicMapper musicMapper; | 36 | private AiMusicMapper musicMapper; |
| 36 | 37 | ||
| 37 | @Override | 38 | @Override |
| 38 | - public List<Long> generateMusic(SunoReqVO reqVO) { | ||
| 39 | - // TODO @xin:是不是可以 if else 调用对应的 API,然后 insertMusicData 逻辑 | ||
| 40 | - AiMusicGenerateEnum generateEnum = AiMusicGenerateEnum.valueOfMode(reqVO.getGenerateMode()); | ||
| 41 | - return switch (generateEnum) { | ||
| 42 | - case DESCRIPTION -> descriptionMode(reqVO); | ||
| 43 | - case LYRIC -> lyricMode(reqVO); | ||
| 44 | - }; | 39 | + public List<Long> generateMusic(AiSunoGenerateReqVO reqVO) { |
| 40 | + List<SunoApi.MusicData> musicDataList; | ||
| 41 | + if (Objects.equals(AiMusicGenerateEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { | ||
| 42 | + //歌词模式 | ||
| 43 | + SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); | ||
| 44 | + musicDataList = sunoApi.customGenerate(sunoReq); | ||
| 45 | + } else if (Objects.equals(AiMusicGenerateEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { | ||
| 46 | + //描述模式 | ||
| 47 | + SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental()); | ||
| 48 | + musicDataList = sunoApi.generate(sunoReq); | ||
| 49 | + } else { | ||
| 50 | + log.error("未知的生成模式:{}", reqVO.getGenerateMode()); | ||
| 51 | + throw new IllegalArgumentException("未知的生成模式"); | ||
| 52 | + } | ||
| 53 | + // 2. 插入数据库 | ||
| 54 | + return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); | ||
| 45 | } | 55 | } |
| 46 | 56 | ||
| 47 | @Override | 57 | @Override |
| @@ -50,36 +60,33 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -50,36 +60,33 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 50 | } | 60 | } |
| 51 | 61 | ||
| 52 | @Override | 62 | @Override |
| 53 | - public Boolean updateBatch(List<AiMusicDO> aiMusicDOList) { | ||
| 54 | - return musicMapper.updateBatch(aiMusicDOList); | ||
| 55 | - } | ||
| 56 | - | ||
| 57 | - /** | ||
| 58 | - * 描述模式生成音乐 | ||
| 59 | - * | ||
| 60 | - * @param reqVO 请求参数 | ||
| 61 | - * @return 生成的音乐ID集合 | ||
| 62 | - */ | ||
| 63 | - public List<Long> descriptionMode(SunoReqVO reqVO) { | ||
| 64 | - // 1. 异步生成 | ||
| 65 | - SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getMakeInstrumental()); | ||
| 66 | - List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq); | ||
| 67 | - // 2. 插入数据库 | ||
| 68 | - return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); | 63 | + public Integer syncMusicTask() { |
| 64 | + List<AiMusicDO> unCompletedTask = this.getUnCompletedTask(); | ||
| 65 | + if (CollUtil.isEmpty(unCompletedTask)) { | ||
| 66 | + log.info("Suno 无进行中任务需要更新!"); | ||
| 67 | + return 0; | ||
| 68 | + } | ||
| 69 | + log.info("Suno 开始同步, 共 [{}] 个任务!", unCompletedTask.size()); | ||
| 70 | + //GET 请求,为避免参数过长,分批次处理 | ||
| 71 | + CollUtil.split(unCompletedTask, 4) | ||
| 72 | + .forEach(chunk -> { | ||
| 73 | + Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId); | ||
| 74 | + List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); | ||
| 75 | + if (CollUtil.isNotEmpty(musicTaskList)) { | ||
| 76 | + List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList); | ||
| 77 | + //回填id | ||
| 78 | + aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId()))); | ||
| 79 | + this.updateBatch(aiMusicDOS); | ||
| 80 | + } else { | ||
| 81 | + log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet()); | ||
| 82 | + } | ||
| 83 | + }); | ||
| 84 | + return unCompletedTask.size(); | ||
| 69 | } | 85 | } |
| 70 | 86 | ||
| 71 | - /** | ||
| 72 | - * 歌词模式生成音乐 | ||
| 73 | - * | ||
| 74 | - * @param reqVO 请求参数 | ||
| 75 | - * @return 生成的音乐ID集合 | ||
| 76 | - */ | ||
| 77 | - public List<Long> lyricMode(SunoReqVO reqVO) { | ||
| 78 | - // 1. 异步生成 | ||
| 79 | - SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); | ||
| 80 | - List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq); | ||
| 81 | - // 2. 插入数据库 | ||
| 82 | - return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); | 87 | + @Override |
| 88 | + public Boolean updateBatch(List<AiMusicDO> musicDOS) { | ||
| 89 | + return musicMapper.updateBatch(musicDOS); | ||
| 83 | } | 90 | } |
| 84 | 91 | ||
| 85 | /** | 92 | /** |
| @@ -92,7 +99,7 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -92,7 +99,7 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 92 | if (CollUtil.isEmpty(musicDataList)) { | 99 | if (CollUtil.isEmpty(musicDataList)) { |
| 93 | return Collections.emptyList(); | 100 | return Collections.emptyList(); |
| 94 | } | 101 | } |
| 95 | - List<AiMusicDO> aiMusicDOList = AiMusicConvert.convertFrom(musicDataList).stream() | 102 | + List<AiMusicDO> aiMusicDOList = buildMusicDOList(musicDataList).stream() |
| 96 | .map(musicDO -> musicDO.setUserId(getLoginUserId()) | 103 | .map(musicDO -> musicDO.setUserId(getLoginUserId()) |
| 97 | .setGenerateMode(generateMode) | 104 | .setGenerateMode(generateMode) |
| 98 | .setPlatform(platform)) | 105 | .setPlatform(platform)) |
| @@ -103,4 +110,24 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -103,4 +110,24 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 103 | .collect(Collectors.toList()); | 110 | .collect(Collectors.toList()); |
| 104 | } | 111 | } |
| 105 | 112 | ||
| 113 | + /** | ||
| 114 | + * 构建 AiMusicDO 集合 | ||
| 115 | + * | ||
| 116 | + * @param musicTaskList suno 音乐任务列表 | ||
| 117 | + * @return AiMusicDO 集合 | ||
| 118 | + */ | ||
| 119 | + private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicTaskList) { | ||
| 120 | + return CollectionUtils.convertList(musicTaskList, musicData -> new AiMusicDO() | ||
| 121 | + .setTaskId(musicData.id()) | ||
| 122 | + .setPrompt(musicData.prompt()) | ||
| 123 | + .setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) | ||
| 124 | + .setAudioUrl(musicData.audioUrl()) | ||
| 125 | + .setVideoUrl(musicData.videoUrl()) | ||
| 126 | + .setImageUrl(musicData.imageUrl()) | ||
| 127 | + .setLyric(musicData.lyric()) | ||
| 128 | + .setTitle(musicData.title()) | ||
| 129 | + .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.COMPLETE.getStatus() : AiMusicStatusEnum.STREAMING.getStatus()) | ||
| 130 | + .setModel(musicData.modelName()) | ||
| 131 | + .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null)); | ||
| 132 | + } | ||
| 106 | } | 133 | } |