Commit 69b05a30bdf9ccf84bd13633c20ef207c34c3262
Committed by
Gitee
!4 【解决todo】AI Music: 结构优化,task状态同步使用统一标准Job
Merge pull request !4 from 小新/master-jdk21-ai
Showing
19 changed files
with
447 additions
and
313 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java
| ... | ... | @@ -50,8 +50,7 @@ public enum AiModelEnum { |
| 50 | 50 | XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"), |
| 51 | 51 | XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"), |
| 52 | 52 | |
| 53 | - // TODO @xin:// Suno;中间加个空格,会更清晰一点。一般来说,不同类型的单词之间,最好有空格。例如说,// 新增一个;再例如说;// 这是 1 个 create 逻辑 | |
| 54 | - //Suno | |
| 53 | + // Suno 模型 | |
| 55 | 54 | SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null), |
| 56 | 55 | SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null), |
| 57 | 56 | SUNO_3_5( "SUNO-3.5", "chirp-v3.5",null), | ... | ... |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.enums.music; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Getter; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * AI 音乐状态的枚举 | |
| 8 | + * | |
| 9 | + * @author xiaoxin | |
| 10 | + */ | |
| 11 | +@AllArgsConstructor | |
| 12 | +@Getter | |
| 13 | +public enum AiMusicGenerateEnum { | |
| 14 | + | |
| 15 | + LYRIC("lyric", "歌词模式"), | |
| 16 | + DESCRIPTION("description", "描述模式"); | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * 模式 | |
| 20 | + */ | |
| 21 | + private final String mode; | |
| 22 | + /** | |
| 23 | + * 模式名 | |
| 24 | + */ | |
| 25 | + private final String name; | |
| 26 | + | |
| 27 | + public static AiMusicGenerateEnum valueOfMode(String mode) { | |
| 28 | + for (AiMusicGenerateEnum modeEnum : AiMusicGenerateEnum.values()) { | |
| 29 | + if (modeEnum.getMode().equals(mode)) { | |
| 30 | + return modeEnum; | |
| 31 | + } | |
| 32 | + } | |
| 33 | + throw new IllegalArgumentException("未知模式: " + mode); | |
| 34 | + } | |
| 35 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java renamed to yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
| 1 | -package cn.iocoder.yudao.module.ai.enums; | |
| 1 | +package cn.iocoder.yudao.module.ai.enums.music; | |
| 2 | 2 | |
| 3 | 3 | import lombok.AllArgsConstructor; |
| 4 | 4 | import lombok.Getter; |
| 5 | 5 | |
| 6 | -// TODO @xin:这个类,挪到 enums/music 包下; | |
| 7 | -// TODO @xin:1)@author 这个是标准的 javadoc;2)@date 可以不要哈;3)可以加下枚举类的注释 | |
| 8 | 6 | /** |
| 9 | - * @Author xiaoxin | |
| 10 | - * @Date 2024/6/5 | |
| 7 | + * AI 音乐状态的枚举 | |
| 8 | + * | |
| 9 | + * @author xiaoxin | |
| 11 | 10 | */ |
| 12 | 11 | @AllArgsConstructor |
| 13 | 12 | @Getter |
| 14 | 13 | public enum AiMusicStatusEnum { |
| 15 | 14 | |
| 16 | - // TODO @xin:是不是收敛成,只有 3 个:进行中,成功,失败;类似 AiImageStatusEnum | |
| 17 | - | |
| 18 | - SUBMITTED("submitted", "已提交"), | |
| 19 | - QUEUED("queued", "排队中"), | |
| 15 | + // @xin 文档中无失败这个返回值 | |
| 20 | 16 | STREAMING("streaming", "进行中"), |
| 21 | 17 | COMPLETE("complete", "完成"); |
| 22 | 18 | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java renamed to 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.module.ai.controller.admin.music.vo.SunoLyricModeVO; | |
| 5 | 4 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; |
| 6 | -import cn.iocoder.yudao.module.ai.service.music.MusicService; | |
| 5 | +import cn.iocoder.yudao.module.ai.service.music.AiMusicService; | |
| 7 | 6 | import io.swagger.v3.oas.annotations.Operation; |
| 8 | 7 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 9 | 8 | import jakarta.validation.Valid; |
| ... | ... | @@ -17,25 +16,18 @@ import java.util.List; |
| 17 | 16 | |
| 18 | 17 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 19 | 18 | |
| 20 | -// TODO @xin:AI 前缀;都要加下哈 | |
| 21 | 19 | @Tag(name = "管理后台 - AI 音乐生成") |
| 22 | 20 | @RestController |
| 23 | 21 | @RequestMapping("/ai/music") |
| 24 | 22 | @RequiredArgsConstructor |
| 25 | -public class MusicController { | |
| 23 | +public class AiMusicController { | |
| 26 | 24 | |
| 27 | - private final MusicService musicService; | |
| 25 | + private final AiMusicService aiMusicService; | |
| 28 | 26 | |
| 29 | - @PostMapping("generate/description-mode") | |
| 30 | - @Operation(summary = "音乐生成-描述模式") | |
| 31 | - public CommonResult<List<Long>> descriptionMode(@RequestBody @Valid SunoReqVO sunoReqVO) { | |
| 32 | - return success(musicService.descriptionMode(sunoReqVO)); | |
| 33 | - } | |
| 34 | - | |
| 35 | - @PostMapping("generate/lyric-mode") | |
| 36 | - @Operation(summary = "音乐生成-歌词模式") | |
| 37 | - public CommonResult<List<Long>> lyricMode(@RequestBody @Valid SunoLyricModeVO sunoLyricModeVO) { | |
| 38 | - return success(musicService.lyricMode(sunoLyricModeVO)); | |
| 27 | + @PostMapping("/generate") | |
| 28 | + @Operation(summary = "音乐生成") | |
| 29 | + public CommonResult<List<Long>> generateMusic(@RequestBody @Valid SunoReqVO sunoReqVO) { | |
| 30 | + return success(aiMusicService.generateMusic(sunoReqVO)); | |
| 39 | 31 | } |
| 40 | 32 | |
| 41 | 33 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoLyricModeVO.java deleted
| 1 | -package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | |
| 2 | - | |
| 3 | -import lombok.Data; | |
| 4 | - | |
| 5 | -/** | |
| 6 | - * @Author jxli@quant360.com | |
| 7 | - * @Date 2024/6/7 | |
| 8 | - */ | |
| 9 | -@Data | |
| 10 | -public class SunoLyricModeVO extends SunoReqVO { | |
| 11 | - | |
| 12 | - /** | |
| 13 | - * 标签/音乐风格 | |
| 14 | - */ | |
| 15 | - private String tags; | |
| 16 | - | |
| 17 | - /** | |
| 18 | - * 音乐名称 | |
| 19 | - */ | |
| 20 | - private String title; | |
| 21 | - | |
| 22 | -} |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.controller.admin.music.vo; |
| 2 | 2 | |
| 3 | -import com.fasterxml.jackson.annotation.JsonInclude; | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import jakarta.validation.constraints.NotBlank; | |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | |
| 7 | +import java.util.List; | |
| 8 | + | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * @author xiaoxin | |
| 12 | + */ | |
| 6 | 13 | @Data |
| 7 | -@JsonInclude(value = JsonInclude.Include.NON_NULL) // TODO @xin:不用加这个哈 | |
| 8 | 14 | public class SunoReqVO { |
| 9 | - /** | |
| 10 | - * 用于生成音乐音频的提示 | |
| 11 | - */ | |
| 15 | + | |
| 16 | + @Schema(description = "用于生成音乐音频的提示") | |
| 12 | 17 | private String prompt; |
| 13 | - // TODO @xin:Boolean,不使用基本类型。 | |
| 14 | - /** | |
| 15 | - * 是否纯音乐 | |
| 16 | - */ | |
| 17 | - private boolean makeInstrumental; | |
| 18 | - /** | |
| 19 | - * //todo 首次请求返回的模型是对的,后续更新音频返回的模型又变成v3.5了 | |
| 20 | - * 模型版本 {@link cn.iocoder.yudao.module.ai.enums.AiModelEnum} Suno | |
| 21 | - */ | |
| 18 | + | |
| 19 | + @Schema(description = "是否纯音乐") | |
| 20 | + private Boolean makeInstrumental; | |
| 21 | + | |
| 22 | + @Schema(description = "模型版本 ") | |
| 22 | 23 | private String mv; |
| 24 | + | |
| 25 | + @Schema(description = "音乐风格") | |
| 26 | + private List<String> tags; | |
| 27 | + | |
| 28 | + @Schema(description = "音乐/歌曲名称") | |
| 29 | + private String title; | |
| 30 | + | |
| 31 | + @Schema(description = "平台") | |
| 32 | + @NotBlank(message = "平台不能为空") | |
| 33 | + private String platform; | |
| 34 | + | |
| 35 | + @Schema(description = "生成模式 lyric(歌词模式), description(描述模式)") | |
| 36 | + @NotBlank(message = "生成模式不能为空") | |
| 37 | + private String generateMode; | |
| 38 | + | |
| 23 | 39 | } |
| 24 | 40 | \ 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 | 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.music; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | |
| 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | |
| 5 | 6 | import com.baomidou.mybatisplus.annotation.IdType; |
| 7 | +import com.baomidou.mybatisplus.annotation.TableField; | |
| 6 | 8 | import com.baomidou.mybatisplus.annotation.TableId; |
| 7 | 9 | import com.baomidou.mybatisplus.annotation.TableName; |
| 8 | -import io.swagger.v3.oas.annotations.media.Schema; | |
| 10 | +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; | |
| 9 | 11 | import lombok.Data; |
| 10 | 12 | |
| 11 | 13 | import java.util.List; |
| 12 | -import java.util.stream.Collectors; | |
| 13 | 14 | |
| 14 | 15 | /** |
| 15 | 16 | * @Author xiaoxin |
| ... | ... | @@ -19,77 +20,103 @@ import java.util.stream.Collectors; |
| 19 | 20 | @Data |
| 20 | 21 | public class AiMusicDO extends BaseDO { |
| 21 | 22 | |
| 22 | - // TODO @xin:@Schema 只在 VO 里使用,这里还是使用标准的注释哈 | |
| 23 | + /** | |
| 24 | + * 编号 | |
| 25 | + */ | |
| 23 | 26 | @TableId(type = IdType.AUTO) |
| 24 | - @Schema(description = "编号") | |
| 25 | 27 | private Long id; |
| 26 | 28 | |
| 27 | - @Schema(description = "用户编号") | |
| 29 | + /** | |
| 30 | + * 用户编号 | |
| 31 | + */ | |
| 28 | 32 | private Long userId; |
| 29 | 33 | |
| 30 | - @Schema(description = "音乐名称") | |
| 34 | + /** | |
| 35 | + * 音乐名称 | |
| 36 | + */ | |
| 31 | 37 | private String title; |
| 32 | 38 | |
| 33 | - @Schema(description = "图片地址") | |
| 39 | + /** | |
| 40 | + * 图片地址 | |
| 41 | + */ | |
| 34 | 42 | private String imageUrl; |
| 35 | 43 | |
| 36 | - @Schema(description = "歌词") | |
| 44 | + /** | |
| 45 | + * 歌词 | |
| 46 | + */ | |
| 37 | 47 | private String lyric; |
| 38 | 48 | |
| 39 | - @Schema(description = "音频地址") | |
| 49 | + /** | |
| 50 | + * 音频地址 | |
| 51 | + */ | |
| 40 | 52 | private String audioUrl; |
| 41 | 53 | |
| 42 | - @Schema(description = "视频地址") | |
| 54 | + /** | |
| 55 | + * 视频地址 | |
| 56 | + */ | |
| 43 | 57 | private String videoUrl; |
| 44 | 58 | |
| 45 | - // TODO @xin:需要关联下对应的枚举 | |
| 46 | - @Schema(description = "音乐状态") | |
| 59 | + /** | |
| 60 | + * 音乐状态 | |
| 61 | + * <p> | |
| 62 | + * 枚举 {@link AiMusicStatusEnum} | |
| 63 | + */ | |
| 47 | 64 | private String status; |
| 48 | 65 | |
| 49 | - @Schema(description = "描述词") | |
| 66 | + /** | |
| 67 | + * 描述词 | |
| 68 | + */ | |
| 50 | 69 | private String gptDescriptionPrompt; |
| 51 | - | |
| 52 | - @Schema(description = "提示词") | |
| 70 | + /** | |
| 71 | + * 提示词 | |
| 72 | + */ | |
| 53 | 73 | private String prompt; |
| 54 | 74 | |
| 55 | - // TODO @xin:生成模式,需要记录下;歌词、描述 | |
| 56 | - | |
| 57 | - // TODO @xin:多存储一个平台,platform;考虑未来可能有别的音乐接口 | |
| 58 | - @Schema(description = "模型") | |
| 75 | + /** | |
| 76 | + * 生成模式 | |
| 77 | + */ | |
| 78 | + private String generateMode; | |
| 79 | + | |
| 80 | + /** | |
| 81 | + * 平台 | |
| 82 | + * <p> | |
| 83 | + * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum} | |
| 84 | + */ | |
| 85 | + private String platform; | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 模型 | |
| 89 | + */ | |
| 59 | 90 | private String model; |
| 60 | 91 | |
| 61 | - @Schema(description = "错误信息") | |
| 92 | + /** | |
| 93 | + * 错误信息 | |
| 94 | + */ | |
| 62 | 95 | private String errorMessage; |
| 63 | 96 | |
| 64 | - // TODO @xin:tags 要不要使用 List<String> | |
| 65 | 97 | |
| 66 | - @Schema(description = "音乐风格标签") | |
| 67 | - private String tags; | |
| 98 | + /** | |
| 99 | + * 音乐风格标签 | |
| 100 | + */ | |
| 101 | + @TableField(typeHandler = AiMusicTagsHandler.class) | |
| 102 | + private List<String> tags; | |
| 68 | 103 | |
| 69 | - @Schema(description = "任务编号") | |
| 104 | + /** | |
| 105 | + * 任务编号 | |
| 106 | + */ | |
| 70 | 107 | private String taskId; |
| 71 | 108 | |
| 72 | - // TODO @xin:转换不放在 DO 里面哈。 | |
| 73 | - | |
| 74 | - public static AiMusicDO convertFrom(SunoApi.MusicData musicData) { | |
| 75 | - return new AiMusicDO() | |
| 76 | - .setTaskId(musicData.id()) | |
| 77 | - .setPrompt(musicData.prompt()) | |
| 78 | - .setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) | |
| 79 | - .setAudioUrl(musicData.audioUrl()) | |
| 80 | - .setVideoUrl(musicData.videoUrl()) | |
| 81 | - .setImageUrl(musicData.imageUrl()) | |
| 82 | - .setLyric(musicData.lyric()) | |
| 83 | - .setTitle(musicData.title()) | |
| 84 | - .setStatus(musicData.status()) | |
| 85 | - .setModel(musicData.modelName()) | |
| 86 | - .setTags(musicData.tags()); | |
| 87 | - } | |
| 88 | 109 | |
| 89 | - public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> musicDataList) { | |
| 90 | - return musicDataList.stream() | |
| 91 | - .map(AiMusicDO::convertFrom) | |
| 92 | - .collect(Collectors.toList()); | |
| 93 | - } | |
| 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 | + } | |
| 94 | 116 | |
| 117 | + @Override | |
| 118 | + protected String toJson(Object obj) { | |
| 119 | + return JsonUtils.toJsonString(obj); | |
| 120 | + } | |
| 121 | + } | |
| 95 | 122 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/SunoJob.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.job; | |
| 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; | |
| 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; | |
| 9 | +import jakarta.annotation.Resource; | |
| 10 | +import lombok.extern.slf4j.Slf4j; | |
| 11 | +import org.springframework.stereotype.Component; | |
| 12 | + | |
| 13 | +import java.util.ArrayList; | |
| 14 | +import java.util.HashMap; | |
| 15 | +import java.util.List; | |
| 16 | +import java.util.Map; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * 处理 Suno Job | |
| 20 | + * @author xiaoxin | |
| 21 | + */ | |
| 22 | +@Component | |
| 23 | +@Slf4j | |
| 24 | +public class SunoJob implements JobHandler { | |
| 25 | + | |
| 26 | + @Resource | |
| 27 | + private SunoApi sunoApi; | |
| 28 | + @Resource | |
| 29 | + private AiMusicService musicService; | |
| 30 | + | |
| 31 | + @Override | |
| 32 | + public String execute(String param) { | |
| 33 | + List<AiMusicDO> unCompletedTask = musicService.getUnCompletedTask(); | |
| 34 | + | |
| 35 | + if (CollUtil.isEmpty(unCompletedTask)) { | |
| 36 | + log.info("Suno 无进行中任务需要更新!"); | |
| 37 | + return "Suno 无进行中任务需要更新!"; | |
| 38 | + } | |
| 39 | + | |
| 40 | + | |
| 41 | + log.info("Suno 开始同步, 共 [{}] 个任务!", unCompletedTask.size()); | |
| 42 | + //GET 请求,为避免参数过长,分批次处理 | |
| 43 | + CollUtil.split(unCompletedTask, 4) | |
| 44 | + .forEach(chunk -> { | |
| 45 | + Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId); | |
| 46 | + List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); | |
| 47 | + if (CollUtil.isNotEmpty(musicTaskList)) { | |
| 48 | + List<AiMusicDO> aiMusicDOS = AiMusicConvert.convertFrom(musicTaskList); | |
| 49 | + //回填id | |
| 50 | + aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId()))); | |
| 51 | + musicService.updateBatch(aiMusicDOS); | |
| 52 | + } else { | |
| 53 | + log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet()); | |
| 54 | + } | |
| 55 | + }); | |
| 56 | + | |
| 57 | + return "Suno 同步 - ".concat(String.valueOf(unCompletedTask.size())).concat(" 个任务!"); | |
| 58 | + } | |
| 59 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicConvert.java
0 → 100644
| 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 | + public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> musicDataList) { | |
| 34 | + return musicDataList.stream() | |
| 35 | + .map(AiMusicConvert::convertFrom) | |
| 36 | + .collect(Collectors.toList()); | |
| 37 | + } | |
| 38 | + | |
| 39 | + | |
| 40 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.service.music; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | |
| 4 | +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * AI 音乐 Service 接口 | |
| 10 | + * | |
| 11 | + * @author xiaoxin | |
| 12 | + */ | |
| 13 | +public interface AiMusicService { | |
| 14 | + | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 音乐生成 | |
| 18 | + * | |
| 19 | + * @param reqVO 请求参数 | |
| 20 | + * @return 生成的音乐ID | |
| 21 | + */ | |
| 22 | + List<Long> generateMusic(SunoReqVO reqVO); | |
| 23 | + | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 获取未完成状态的任务 | |
| 27 | + * | |
| 28 | + * @return 未完成任务列表 | |
| 29 | + */ | |
| 30 | + List<AiMusicDO> getUnCompletedTask(); | |
| 31 | + | |
| 32 | + | |
| 33 | + Boolean updateBatch(List<AiMusicDO> aiMusicDOList); | |
| 34 | + | |
| 35 | + | |
| 36 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.service.music; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.text.StrPool; | |
| 5 | +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.module.ai.dal.dataobject.music.AiMusicDO; | |
| 8 | +import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; | |
| 9 | +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum; | |
| 10 | +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | |
| 11 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |
| 12 | +import jakarta.annotation.Resource; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | +import org.springframework.stereotype.Service; | |
| 15 | + | |
| 16 | +import java.util.Collections; | |
| 17 | +import java.util.List; | |
| 18 | +import java.util.stream.Collectors; | |
| 19 | + | |
| 20 | +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * AI 音乐 Service 实现类 | |
| 24 | + * @author xiaoxin | |
| 25 | + */ | |
| 26 | +@Service | |
| 27 | +@Slf4j | |
| 28 | +public class AiMusicServiceImpl implements AiMusicService { | |
| 29 | + | |
| 30 | + @Resource | |
| 31 | + private SunoApi sunoApi; | |
| 32 | + @Resource | |
| 33 | + private AiMusicMapper musicMapper; | |
| 34 | + | |
| 35 | + @Override | |
| 36 | + public List<Long> generateMusic(SunoReqVO reqVO) { | |
| 37 | + AiMusicGenerateEnum generateEnum = AiMusicGenerateEnum.valueOfMode(reqVO.getGenerateMode()); | |
| 38 | + return switch (generateEnum) { | |
| 39 | + case DESCRIPTION -> descriptionMode(reqVO); | |
| 40 | + case LYRIC -> lyricMode(reqVO); | |
| 41 | + }; | |
| 42 | + } | |
| 43 | + | |
| 44 | + @Override | |
| 45 | + public List<AiMusicDO> getUnCompletedTask() { | |
| 46 | + return musicMapper.selectList(new LambdaQueryWrapper<AiMusicDO>().ne(AiMusicDO::getStatus, AiMusicStatusEnum.COMPLETE.getStatus())); | |
| 47 | + } | |
| 48 | + | |
| 49 | + @Override | |
| 50 | + public Boolean updateBatch(List<AiMusicDO> aiMusicDOList) { | |
| 51 | + return musicMapper.updateBatch(aiMusicDOList); | |
| 52 | + } | |
| 53 | + | |
| 54 | + /** | |
| 55 | + * 描述模式生成音乐 | |
| 56 | + * | |
| 57 | + * @param reqVO 请求参数 | |
| 58 | + * @return 生成的音乐ID集合 | |
| 59 | + */ | |
| 60 | + public List<Long> descriptionMode(SunoReqVO reqVO) { | |
| 61 | + // 1. 异步生成 | |
| 62 | + SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getMakeInstrumental()); | |
| 63 | + List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq); | |
| 64 | + // 2. 插入数据库 | |
| 65 | + return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); | |
| 66 | + } | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * 歌词模式生成音乐 | |
| 70 | + * | |
| 71 | + * @param reqVO 请求参数 | |
| 72 | + * @return 生成的音乐ID集合 | |
| 73 | + */ | |
| 74 | + public List<Long> lyricMode(SunoReqVO reqVO) { | |
| 75 | + // 1. 异步生成 | |
| 76 | + SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); | |
| 77 | + List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq); | |
| 78 | + // 2. 插入数据库 | |
| 79 | + return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); | |
| 80 | + } | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * 新增音乐数据并提交 suno任务 | |
| 84 | + * | |
| 85 | + * @param musicDataList 音乐数据列表 | |
| 86 | + * @return 音乐id集合 | |
| 87 | + */ | |
| 88 | + private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList, String generateMode, String platform) { | |
| 89 | + if (CollUtil.isEmpty(musicDataList)) { | |
| 90 | + return Collections.emptyList(); | |
| 91 | + } | |
| 92 | + List<AiMusicDO> aiMusicDOList = AiMusicConvert.convertFrom(musicDataList).stream() | |
| 93 | + .map(musicDO -> musicDO.setUserId(getLoginUserId()) | |
| 94 | + .setGenerateMode(generateMode) | |
| 95 | + .setPlatform(platform)) | |
| 96 | + .toList(); | |
| 97 | + musicMapper.insertBatch(aiMusicDOList); | |
| 98 | + return aiMusicDOList.stream() | |
| 99 | + .map(AiMusicDO::getId) | |
| 100 | + .collect(Collectors.toList()); | |
| 101 | + } | |
| 102 | + | |
| 103 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java deleted
| 1 | -package cn.iocoder.yudao.module.ai.service.music; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO; | |
| 4 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | |
| 5 | - | |
| 6 | -import java.util.List; | |
| 7 | - | |
| 8 | -/** | |
| 9 | - * @Author xiaoxin | |
| 10 | - * @Date 2024/5/29 | |
| 11 | - */ | |
| 12 | -public interface MusicService { | |
| 13 | - | |
| 14 | - /** | |
| 15 | - * 音乐生成-描述模式 | |
| 16 | - */ | |
| 17 | - List<Long> descriptionMode(SunoReqVO reqVO); | |
| 18 | - | |
| 19 | - | |
| 20 | - /** | |
| 21 | - * 音乐生成-歌词模式 | |
| 22 | - **/ | |
| 23 | - List<Long> lyricMode(SunoLyricModeVO reqVO); | |
| 24 | -} |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java deleted
| 1 | -package cn.iocoder.yudao.module.ai.service.music; | |
| 2 | - | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | -import cn.hutool.core.text.StrPool; | |
| 5 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | |
| 6 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO; | |
| 7 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | |
| 8 | -import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | |
| 9 | -import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; | |
| 10 | -import cn.iocoder.yudao.module.ai.enums.AiMusicStatusEnum; | |
| 11 | -import com.baomidou.mybatisplus.core.toolkit.Wrappers; | |
| 12 | -import lombok.RequiredArgsConstructor; | |
| 13 | -import lombok.extern.slf4j.Slf4j; | |
| 14 | -import org.springframework.scheduling.annotation.Scheduled; | |
| 15 | -import org.springframework.stereotype.Service; | |
| 16 | -import org.springframework.transaction.annotation.Transactional; | |
| 17 | - | |
| 18 | -import java.util.*; | |
| 19 | -import java.util.concurrent.ConcurrentLinkedQueue; | |
| 20 | -import java.util.concurrent.TimeUnit; | |
| 21 | -import java.util.stream.Collectors; | |
| 22 | - | |
| 23 | -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | |
| 24 | - | |
| 25 | -/** | |
| 26 | - * @Author xiaoxin | |
| 27 | - * @Date 2024/5/29 | |
| 28 | - */ | |
| 29 | -@Service | |
| 30 | -@RequiredArgsConstructor | |
| 31 | -@Slf4j | |
| 32 | -public class MusicServiceImpl implements MusicService { | |
| 33 | - | |
| 34 | - // TODO @xin:使用 @Resource 注入,整个项目保持统一哈; | |
| 35 | - private final SunoApi sunoApi; | |
| 36 | - private final AiMusicMapper musicMapper; | |
| 37 | - | |
| 38 | - private final Queue<String> taskQueue = new ConcurrentLinkedQueue<>(); | |
| 39 | - | |
| 40 | - // TODO @xin:要不把 descriptionMode、lyricMode 合并,同一个 generateMusic 方法,然后根据传入的 mode 模式:歌词、描述来区分? | |
| 41 | - | |
| 42 | - @Override | |
| 43 | - public List<Long> descriptionMode(SunoReqVO reqVO) { | |
| 44 | - // 1. 异步生成 | |
| 45 | - SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental()); | |
| 46 | - List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq); | |
| 47 | - // 2. 插入数据库 | |
| 48 | - return insertMusicData(musicDataList); | |
| 49 | - } | |
| 50 | - | |
| 51 | - @Override | |
| 52 | - public List<Long> lyricMode(SunoLyricModeVO reqVO) { | |
| 53 | - // 1. 异步生成 | |
| 54 | - SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle()); | |
| 55 | - List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq); | |
| 56 | - // 2. 插入数据库 | |
| 57 | - return insertMusicData(musicDataList); | |
| 58 | - } | |
| 59 | - | |
| 60 | - /** | |
| 61 | - * 新增音乐数据并提交 suno任务 | |
| 62 | - * | |
| 63 | - * @param musicDataList 音乐数据列表 | |
| 64 | - * @return 音乐id集合 | |
| 65 | - */ | |
| 66 | - private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList) { | |
| 67 | - if (CollUtil.isEmpty(musicDataList)) { | |
| 68 | - return Collections.emptyList(); | |
| 69 | - } | |
| 70 | - // TODO @xin:建议使用 insertBatch 方法,批量插入 | |
| 71 | - return AiMusicDO.convertFrom(musicDataList).stream() | |
| 72 | - .peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId()))) | |
| 73 | - .peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add)) | |
| 74 | - .map(AiMusicDO::getId) | |
| 75 | - .collect(Collectors.toList()); | |
| 76 | - } | |
| 77 | - | |
| 78 | - // TODO @xin:这个,改成标准的 job 来实现哈。从数据库加载任务,然后执行。 | |
| 79 | - @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) | |
| 80 | - @Transactional | |
| 81 | - public void flushSunoTask() { | |
| 82 | - if (CollUtil.isEmpty(taskQueue)) { | |
| 83 | - return; | |
| 84 | - } | |
| 85 | - CollUtil.split(taskQueue, 5). | |
| 86 | - stream().map(chunk -> CollUtil.join(chunk, StrPool.COMMA)) | |
| 87 | - .forEach(taskIds -> { | |
| 88 | - List<SunoApi.MusicData> musicData = sunoApi.selectById(taskIds); | |
| 89 | - musicData.stream() | |
| 90 | - .map(AiMusicDO::convertFrom) | |
| 91 | - .forEach(musicDO -> { | |
| 92 | - //更新音乐生成结果 | |
| 93 | - musicMapper.update(musicDO, Wrappers.<AiMusicDO>lambdaUpdate().eq(AiMusicDO::getTaskId, musicDO.getTaskId())); | |
| 94 | - //完成后剔除任务 | |
| 95 | - if (Objects.equals(AiMusicStatusEnum.COMPLETE.getStatus(), musicDO.getStatus())) { | |
| 96 | - taskQueue.remove(musicDO.getTaskId()); | |
| 97 | - } | |
| 98 | - }); | |
| 99 | - }); | |
| 100 | - | |
| 101 | - } | |
| 102 | -} |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.ai.config; |
| 3 | 3 | import cn.hutool.core.io.IoUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; |
| 5 | 5 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; |
| 6 | -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; | |
| 7 | 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 8 | 7 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; |
| 9 | 8 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; |
| ... | ... | @@ -151,7 +150,7 @@ public class YudaoAiAutoConfiguration { |
| 151 | 150 | @Bean |
| 152 | 151 | @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") |
| 153 | 152 | public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { |
| 154 | - return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getBaseUrl())); | |
| 153 | + return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); | |
| 155 | 154 | } |
| 156 | 155 | |
| 157 | 156 | private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext, | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java deleted
| 1 | -package cn.iocoder.yudao.framework.ai.core.model.suno; | |
| 2 | - | |
| 3 | -import lombok.AllArgsConstructor; | |
| 4 | -import lombok.Data; | |
| 5 | -import lombok.NoArgsConstructor; | |
| 6 | - | |
| 7 | -// TODO @xin:不需要这个类哈,直接 SunoApi 传入 baseUrl 参数即可 | |
| 8 | -/** | |
| 9 | - * Suno 配置类 | |
| 10 | - * | |
| 11 | - * @author xiaoxin | |
| 12 | - */ | |
| 13 | -@Data | |
| 14 | -@NoArgsConstructor | |
| 15 | -@AllArgsConstructor | |
| 16 | -public class SunoConfig { | |
| 17 | - | |
| 18 | - /** | |
| 19 | - * suno-api服务的基本路径 | |
| 20 | - */ | |
| 21 | - private String baseUrl; | |
| 22 | - | |
| 23 | -} |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java
| 1 | 1 | package cn.iocoder.yudao.framework.ai.core.model.suno.api; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.text.StrPool; | |
| 4 | 5 | import com.fasterxml.jackson.annotation.JsonInclude; |
| 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; |
| 6 | 7 | import lombok.extern.slf4j.Slf4j; |
| 7 | 8 | import org.springframework.core.ParameterizedTypeReference; |
| 9 | +import org.springframework.http.HttpRequest; | |
| 8 | 10 | import org.springframework.http.HttpStatusCode; |
| 9 | 11 | import org.springframework.http.MediaType; |
| 10 | 12 | import org.springframework.web.reactive.function.client.ClientResponse; |
| ... | ... | @@ -17,11 +19,10 @@ import java.util.function.Predicate; |
| 17 | 19 | |
| 18 | 20 | /** |
| 19 | 21 | * Suno API |
| 20 | - * <br> | |
| 22 | + * <b> | |
| 21 | 23 | * 文档地址:https://github.com/status2xx/suno-api/blob/main/README_CN.md |
| 22 | 24 | * |
| 23 | - * @Author xiaoxin | |
| 24 | - * @Date 2024/6/3 | |
| 25 | + * @author xiaoxin | |
| 25 | 26 | */ |
| 26 | 27 | @Slf4j |
| 27 | 28 | public class SunoApi { |
| ... | ... | @@ -29,86 +30,88 @@ public class SunoApi { |
| 29 | 30 | private final WebClient webClient; |
| 30 | 31 | |
| 31 | 32 | private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful(); |
| 32 | - private final Function<ClientResponse, Mono<? extends Throwable>> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class) | |
| 33 | + | |
| 34 | + private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION = reqParam -> response -> response.bodyToMono(String.class) | |
| 33 | 35 | .handle((respBody, sink) -> { |
| 34 | - // TODO @xin:最好是 request、response 都有哈 | |
| 35 | - log.error("【suno-api】调用失败!resp: 【{}】", respBody); | |
| 36 | - sink.error(new IllegalStateException("【suno-api】调用失败!")); | |
| 36 | + HttpRequest request = response.request(); | |
| 37 | + log.error("[suno-api] 调用失败!请求方式:[{}], 请求地址:[{}], 请求参数:[{}], 响应数据: [{}]", request.getMethod(), request.getURI(), reqParam, respBody); | |
| 38 | + sink.error(new IllegalStateException("[suno-api] 调用失败!")); | |
| 37 | 39 | }); |
| 38 | 40 | |
| 39 | - public SunoApi(SunoConfig config) { | |
| 41 | + | |
| 42 | + public SunoApi(String baseUrl) { | |
| 40 | 43 | this.webClient = WebClient.builder() |
| 41 | - .baseUrl(config.getBaseUrl()) | |
| 44 | + .baseUrl(baseUrl) | |
| 42 | 45 | .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) |
| 43 | 46 | .build(); |
| 44 | 47 | } |
| 45 | 48 | |
| 46 | - public List<MusicData> generate(SunoRequest request) { | |
| 49 | + public List<MusicData> generate(MusicGenerateRequest request) { | |
| 47 | 50 | return this.webClient.post() |
| 48 | 51 | .uri("/api/generate") |
| 49 | - .body(Mono.just(request), SunoRequest.class) | |
| 52 | + .body(Mono.just(request), MusicGenerateRequest.class) | |
| 50 | 53 | .retrieve() |
| 51 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | |
| 52 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 54 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request)) | |
| 55 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 56 | + }) | |
| 53 | 57 | .block(); |
| 54 | 58 | } |
| 55 | 59 | |
| 56 | - public List<MusicData> customGenerate(SunoRequest request) { | |
| 60 | + public List<MusicData> customGenerate(MusicGenerateRequest request) { | |
| 57 | 61 | return this.webClient.post() |
| 58 | 62 | .uri("/api/custom_generate") |
| 59 | - .body(Mono.just(request), SunoRequest.class) | |
| 63 | + .body(Mono.just(request), MusicGenerateRequest.class) | |
| 60 | 64 | .retrieve() |
| 61 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | |
| 62 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 65 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request)) | |
| 66 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 67 | + }) | |
| 63 | 68 | .block(); |
| 64 | 69 | } |
| 65 | 70 | |
| 66 | - // TODO @xin: 是不是叫 chatCompletion | |
| 67 | - public List<MusicData> doChatCompletion(String prompt) { | |
| 71 | + public List<MusicData> chatCompletion(String prompt) { | |
| 68 | 72 | return this.webClient.post() |
| 69 | 73 | .uri("/v1/chat/completions") |
| 70 | - .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) | |
| 74 | + .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class) | |
| 71 | 75 | .retrieve() |
| 72 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | |
| 73 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 76 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt)) | |
| 77 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 78 | + }) | |
| 74 | 79 | .block(); |
| 75 | 80 | } |
| 76 | 81 | |
| 77 | 82 | public LyricsData generateLyrics(String prompt) { |
| 78 | 83 | return this.webClient.post() |
| 79 | 84 | .uri("/api/generate_lyrics") |
| 80 | - .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) | |
| 85 | + .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class) | |
| 81 | 86 | .retrieve() |
| 82 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | |
| 87 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt)) | |
| 83 | 88 | .bodyToMono(LyricsData.class) |
| 84 | 89 | .block(); |
| 85 | 90 | } |
| 86 | 91 | |
| 87 | - // TODO @xin:应该传入 List<String> ids | |
| 88 | - // TODO @xin:方法名,建议使用 getMusicList | |
| 89 | - public List<MusicData> selectById(String ids) { | |
| 92 | + public List<MusicData> getMusicList(List<String> ids) { | |
| 90 | 93 | return this.webClient.get() |
| 91 | 94 | .uri(uriBuilder -> uriBuilder |
| 92 | 95 | .path("/api/get") |
| 93 | - .queryParam("ids", ids) | |
| 96 | + .queryParam("ids", CollUtil.join(ids, StrPool.COMMA)) | |
| 94 | 97 | .build()) |
| 95 | 98 | .retrieve() |
| 96 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | |
| 97 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 99 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(ids)) | |
| 100 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 101 | + }) | |
| 98 | 102 | .block(); |
| 99 | 103 | } |
| 100 | 104 | |
| 101 | - // TODO @xin:方法名,建议使用 getLimitUsage | |
| 102 | - public LimitData selectLimit() { | |
| 105 | + public LimitUsageData getLimitUsage() { | |
| 103 | 106 | return this.webClient.get() |
| 104 | 107 | .uri("/api/get_limit") |
| 105 | 108 | .retrieve() |
| 106 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | |
| 107 | - .bodyToMono(LimitData.class) | |
| 109 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(null)) | |
| 110 | + .bodyToMono(LimitUsageData.class) | |
| 108 | 111 | .block(); |
| 109 | 112 | } |
| 110 | 113 | |
| 111 | - // TODO @xin:可以改成 MusicGenerateRequest | |
| 114 | + | |
| 112 | 115 | /** |
| 113 | 116 | * 根据提示生成音频 |
| 114 | 117 | * |
| ... | ... | @@ -121,7 +124,7 @@ public class SunoApi { |
| 121 | 124 | * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 |
| 122 | 125 | */ |
| 123 | 126 | @JsonInclude(value = JsonInclude.Include.NON_NULL) |
| 124 | - public record SunoRequest( | |
| 127 | + public record MusicGenerateRequest( | |
| 125 | 128 | String prompt, |
| 126 | 129 | String tags, |
| 127 | 130 | String title, |
| ... | ... | @@ -130,15 +133,15 @@ public class SunoApi { |
| 130 | 133 | @JsonProperty("make_instrumental") boolean makeInstrumental |
| 131 | 134 | ) { |
| 132 | 135 | |
| 133 | - public SunoRequest(String prompt) { | |
| 136 | + public MusicGenerateRequest(String prompt) { | |
| 134 | 137 | this(prompt, null, null, null, false, false); |
| 135 | 138 | } |
| 136 | 139 | |
| 137 | - public SunoRequest(String prompt, String mv, boolean makeInstrumental) { | |
| 140 | + public MusicGenerateRequest(String prompt, String mv, boolean makeInstrumental) { | |
| 138 | 141 | this(prompt, null, null, mv, false, makeInstrumental); |
| 139 | 142 | } |
| 140 | 143 | |
| 141 | - public SunoRequest(String prompt, String mv, String tags, String title) { | |
| 144 | + public MusicGenerateRequest(String prompt, String mv, String tags, String title) { | |
| 142 | 145 | this(prompt, tags, title, mv, false, false); |
| 143 | 146 | } |
| 144 | 147 | |
| ... | ... | @@ -154,12 +157,12 @@ public class SunoApi { |
| 154 | 157 | * @param audioUrl 音乐音频的 URL |
| 155 | 158 | * @param videoUrl 音乐视频的 URL |
| 156 | 159 | * @param createdAt 音乐音频的创建时间 |
| 157 | - * @param modelName | |
| 160 | + * @param modelName 模型名称 | |
| 158 | 161 | * @param status submitted、queued、streaming、complete |
| 159 | - * @param gptDescriptionPrompt | |
| 162 | + * @param gptDescriptionPrompt 描述词 | |
| 160 | 163 | * @param prompt 生成音乐音频的提示 |
| 161 | - * @param type | |
| 162 | - * @param tags | |
| 164 | + * @param type 操作类型 | |
| 165 | + * @param tags 音乐类型标签 | |
| 163 | 166 | */ |
| 164 | 167 | public record MusicData( |
| 165 | 168 | String id, |
| ... | ... | @@ -195,7 +198,7 @@ public class SunoApi { |
| 195 | 198 | /** |
| 196 | 199 | * Suno API 响应的限额数据,目前每日免费50 |
| 197 | 200 | */ |
| 198 | - public record LimitData( | |
| 201 | + public record LimitUsageData( | |
| 199 | 202 | @JsonProperty("credits_left") Long creditsLeft, |
| 200 | 203 | String period, |
| 201 | 204 | @JsonProperty("monthly_limit") Long monthlyLimit, | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java
| 1 | 1 | package cn.iocoder.yudao.framework.ai.suno; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; | |
| 4 | 3 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 5 | 4 | import org.junit.Before; |
| 6 | 5 | import org.junit.Test; |
| ... | ... | @@ -17,26 +16,26 @@ public class SunoTests { |
| 17 | 16 | |
| 18 | 17 | @Before |
| 19 | 18 | public void setup() { |
| 20 | - String url = "https://suno-ix9nve79x-status2xxs-projects.vercel.app"; | |
| 21 | - this.sunoApi = new SunoApi(new SunoConfig(url)); | |
| 19 | + String url = "https://suno-imrqwwui8-status2xxs-projects.vercel.app"; | |
| 20 | + this.sunoApi = new SunoApi(url); | |
| 22 | 21 | } |
| 23 | 22 | |
| 24 | 23 | @Test |
| 25 | 24 | public void selectById() { |
| 26 | - System.out.println(sunoApi.selectById("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551")); | |
| 25 | + System.out.println(sunoApi.getMusicList(List.of("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551"))); | |
| 27 | 26 | |
| 28 | 27 | } |
| 29 | 28 | |
| 30 | 29 | @Test |
| 31 | 30 | public void generate() { |
| 32 | - List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); | |
| 31 | + List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.MusicGenerateRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); | |
| 33 | 32 | System.out.println(generate); |
| 34 | 33 | } |
| 35 | 34 | |
| 36 | 35 | |
| 37 | 36 | @Test |
| 38 | 37 | public void doChatCompletion() { |
| 39 | - List<SunoApi.MusicData> generate = sunoApi.doChatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); | |
| 38 | + List<SunoApi.MusicData> generate = sunoApi.chatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); | |
| 40 | 39 | System.out.println(generate); |
| 41 | 40 | } |
| 42 | 41 | |
| ... | ... | @@ -50,8 +49,8 @@ public class SunoTests { |
| 50 | 49 | |
| 51 | 50 | @Test |
| 52 | 51 | public void selectLimit() { |
| 53 | - SunoApi.LimitData limitData = sunoApi.selectLimit(); | |
| 54 | - System.out.println(limitData); | |
| 52 | + SunoApi.LimitUsageData limitUsageData = sunoApi.getLimitUsage(); | |
| 53 | + System.out.println(limitUsageData); | |
| 55 | 54 | } |
| 56 | 55 | |
| 57 | 56 | ... | ... |
yudao-server/src/main/resources/application.yaml
| ... | ... | @@ -199,7 +199,7 @@ yudao.ai: |
| 199 | 199 | channel-id: 1237948819677904960 |
| 200 | 200 | suno: |
| 201 | 201 | enable: true |
| 202 | - base-url: https://suno-ix9nve79x-status2xxs-projects.vercel.app | |
| 202 | + base-url: https://suno-imrqwwui8-status2xxs-projects.vercel.app | |
| 203 | 203 | |
| 204 | 204 | --- #################### 芋道相关配置 #################### |
| 205 | 205 | ... | ... |