From 3035fbbbc320fd6c4850854f69ba8c56c10fc25d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 17 Jun 2024 20:49:49 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E3=80=91AI=EF=BC=9A=E9=9F=B3=E4=B9=90=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/AiMusicStatusEnum.java | 4 ++ .../module/ai/enums/model/AiModelEnum.java | 1 + .../admin/music/MusicController.java | 1 + .../controller/admin/music/vo/SunoReqVO.java | 3 +- .../ai/dal/dataobject/music/AiMusicDO.java | 13 +++- .../ai/dal/mysql/music/AiMusicMapper.java | 5 +- .../ai/service/music/MusicServiceImpl.java | 15 +++-- .../ai/config/YudaoAiProperties.java | 3 +- .../ai/core/model/suno/SunoConfig.java | 8 ++- .../ai/core/model/suno/api/SunoApi.java | 60 +++++++++---------- .../yudao/framework/ai/suno/SunoTests.java | 2 +- 11 files changed, 67 insertions(+), 48 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java index c29cee9d4f..cb7c8d350b 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.ai.enums; import lombok.AllArgsConstructor; import lombok.Getter; +// TODO @xin:这个类,挪到 enums/music 包下; +// TODO @xin:1)@author 这个是标准的 javadoc;2)@date 可以不要哈;3)可以加下枚举类的注释 /** * @Author xiaoxin * @Date 2024/6/5 @@ -11,6 +13,8 @@ import lombok.Getter; @Getter public enum AiMusicStatusEnum { + // TODO @xin:是不是收敛成,只有 3 个:进行中,成功,失败;类似 AiImageStatusEnum + SUBMITTED("submitted", "已提交"), QUEUED("queued", "排队中"), STREAMING("streaming", "进行中"), diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java index bb1c26abf7..01f0460baf 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java @@ -50,6 +50,7 @@ public enum AiModelEnum { XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"), XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"), + // TODO @xin:// Suno;中间加个空格,会更清晰一点。一般来说,不同类型的单词之间,最好有空格。例如说,// 新增一个;再例如说;// 这是 1 个 create 逻辑 //Suno SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null), SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null), diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java index 125c4a9a66..502dbad649 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java @@ -17,6 +17,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +// TODO @xin:AI 前缀;都要加下哈 @Tag(name = "管理后台 - AI 音乐生成") @RestController @RequestMapping("/ai/music") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java index ad1e9fe295..4685b2142d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java @@ -4,12 +4,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; @Data -@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonInclude(value = JsonInclude.Include.NON_NULL) // TODO @xin:不用加这个哈 public class SunoReqVO { /** * 用于生成音乐音频的提示 */ private String prompt; + // TODO @xin:Boolean,不使用基本类型。 /** * 是否纯音乐 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java index 3ab958c2f0..4938646c24 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java @@ -18,6 +18,8 @@ import java.util.stream.Collectors; @TableName("ai_music") @Data public class AiMusicDO extends BaseDO { + + // TODO @xin:@Schema 只在 VO 里使用,这里还是使用标准的注释哈 @TableId(type = IdType.AUTO) @Schema(description = "编号") private Long id; @@ -40,6 +42,7 @@ public class AiMusicDO extends BaseDO { @Schema(description = "视频地址") private String videoUrl; + // TODO @xin:需要关联下对应的枚举 @Schema(description = "音乐状态") private String status; @@ -49,19 +52,24 @@ public class AiMusicDO extends BaseDO { @Schema(description = "提示词") private String prompt; + // TODO @xin:生成模式,需要记录下;歌词、描述 + + // TODO @xin:多存储一个平台,platform;考虑未来可能有别的音乐接口 @Schema(description = "模型") private String model; @Schema(description = "错误信息") private String errorMessage; + // TODO @xin:tags 要不要使用 List + @Schema(description = "音乐风格标签") private String tags; - @Schema(description = "任务id") + @Schema(description = "任务编号") private String taskId; - + // TODO @xin:转换不放在 DO 里面哈。 public static AiMusicDO convertFrom(SunoApi.MusicData musicData) { return new AiMusicDO() @@ -84,5 +92,4 @@ public class AiMusicDO extends BaseDO { .collect(Collectors.toList()); } - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java index 88d9b18469..22b9cb5ae1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java @@ -5,10 +5,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; import org.apache.ibatis.annotations.Mapper; /** - * @Author xiaoxin - * @Date 2024/6/5 + * AI 音乐 Mapper + * @author xiaoxin */ @Mapper public interface AiMusicMapper extends BaseMapperX { - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java index 0f0d4197fa..96ceddf05c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java @@ -31,26 +31,29 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti @Slf4j public class MusicServiceImpl implements MusicService { + // TODO @xin:使用 @Resource 注入,整个项目保持统一哈; private final SunoApi sunoApi; private final AiMusicMapper musicMapper; private final Queue taskQueue = new ConcurrentLinkedQueue<>(); + // TODO @xin:要不把 descriptionMode、lyricMode 合并,同一个 generateMusic 方法,然后根据传入的 mode 模式:歌词、描述来区分? @Override public List descriptionMode(SunoReqVO reqVO) { - SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental()); - //默认异步 + // 1. 异步生成 + SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental()); List musicDataList = sunoApi.generate(sunoReq); + // 2. 插入数据库 return insertMusicData(musicDataList); } - @Override public List lyricMode(SunoLyricModeVO reqVO) { - SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle()); - //默认异步 + // 1. 异步生成 + SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle()); List musicDataList = sunoApi.customGenerate(sunoReq); + // 2. 插入数据库 return insertMusicData(musicDataList); } @@ -64,6 +67,7 @@ public class MusicServiceImpl implements MusicService { if (CollUtil.isEmpty(musicDataList)) { return Collections.emptyList(); } + // TODO @xin:建议使用 insertBatch 方法,批量插入 return AiMusicDO.convertFrom(musicDataList).stream() .peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId()))) .peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add)) @@ -71,6 +75,7 @@ public class MusicServiceImpl implements MusicService { .collect(Collectors.toList()); } + // TODO @xin:这个,改成标准的 job 来实现哈。从数据库加载任务,然后执行。 @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) @Transactional public void flushSunoTask() { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index fb939790e2..431a186eb2 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -118,8 +118,9 @@ public class YudaoAiProperties { public static class SunoProperties { private boolean enable = false; + /** - * suno-api 服务的基本地址 + * API 服务的基本地址 */ private String baseUrl; diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java index 1d1b62870b..058842eab7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java @@ -4,16 +4,20 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +// TODO @xin:不需要这个类哈,直接 SunoApi 传入 baseUrl 参数即可 /** - * @Author xiaoxin - * @Date 2024/5/29 + * Suno 配置类 + * + * @author xiaoxin */ @Data @NoArgsConstructor @AllArgsConstructor public class SunoConfig { + /** * suno-api服务的基本路径 */ private String baseUrl; + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java index 406f7000e1..80bdd5174d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java @@ -27,14 +27,15 @@ import java.util.function.Predicate; public class SunoApi { private final WebClient webClient; + private final Predicate STATUS_PREDICATE = status -> !status.is2xxSuccessful(); private final Function> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class) .handle((respBody, sink) -> { + // TODO @xin:最好是 request、response 都有哈 log.error("【suno-api】调用失败!resp: 【{}】", respBody); sink.error(new IllegalStateException("【suno-api】调用失败!")); }); - public SunoApi(SunoConfig config) { this.webClient = WebClient.builder() .baseUrl(config.getBaseUrl()) @@ -42,50 +43,49 @@ public class SunoApi { .build(); } - public List generate(SunoApi.SunoReq sunReq) { + public List generate(SunoRequest request) { return this.webClient.post() .uri("/api/generate") - .body(Mono.just(sunReq), SunoApi.SunoReq.class) + .body(Mono.just(request), SunoRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) - .bodyToMono(new ParameterizedTypeReference>() { - }) + .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } - public List customGenerate(SunoApi.SunoReq sunReq) { + public List customGenerate(SunoRequest request) { return this.webClient.post() .uri("/api/custom_generate") - .body(Mono.just(sunReq), SunoApi.SunoReq.class) + .body(Mono.just(request), SunoRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) - .bodyToMono(new ParameterizedTypeReference>() { - }) + .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } + // TODO @xin: 是不是叫 chatCompletion public List doChatCompletion(String prompt) { return this.webClient.post() .uri("/v1/chat/completions") - .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class) + .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) - .bodyToMono(new ParameterizedTypeReference>() { - }) + .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } public LyricsData generateLyrics(String prompt) { return this.webClient.post() .uri("/api/generate_lyrics") - .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class) + .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) .bodyToMono(LyricsData.class) .block(); } - + // TODO @xin:应该传入 List ids + // TODO @xin:方法名,建议使用 getMusicList public List selectById(String ids) { return this.webClient.get() .uri(uriBuilder -> uriBuilder @@ -94,12 +94,11 @@ public class SunoApi { .build()) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) - .bodyToMono(new ParameterizedTypeReference>() { - }) + .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } - + // TODO @xin:方法名,建议使用 getLimitUsage public LimitData selectLimit() { return this.webClient.get() .uri("/api/get_limit") @@ -109,7 +108,7 @@ public class SunoApi { .block(); } - + // TODO @xin:可以改成 MusicGenerateRequest /** * 根据提示生成音频 * @@ -117,12 +116,12 @@ public class SunoApi { * @param tags 音乐风格 * @param title 音乐名称 * @param mv 模型 - * @param waitAudio false表示后台模式,仅返回音频任务信息,需要调用get API获取详细的音频信息。 - * true表示同步模式,API最多等待100s,音频生成完毕后直接返回音频链接等信息,建议在GPT等agent中使用。 + * @param waitAudio false 表示后台模式,仅返回音频任务信息,需要调用 get API 获取详细的音频信息。 + * true 表示同步模式,API 最多等待 100s,音频生成完毕后直接返回音频链接等信息,建议在 GPT 等 agent 中使用。 * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 */ @JsonInclude(value = JsonInclude.Include.NON_NULL) - public record SunoReq( + public record SunoRequest( String prompt, String tags, String title, @@ -130,23 +129,23 @@ public class SunoApi { @JsonProperty("wait_audio") boolean waitAudio, @JsonProperty("make_instrumental") boolean makeInstrumental ) { - public SunoReq(String prompt) { + + public SunoRequest(String prompt) { this(prompt, null, null, null, false, false); } - public SunoReq(String prompt, String mv, boolean makeInstrumental) { + public SunoRequest(String prompt, String mv, boolean makeInstrumental) { this(prompt, null, null, mv, false, makeInstrumental); } - - public SunoReq(String prompt, String mv, String tags, String title) { + public SunoRequest(String prompt, String mv, String tags, String title) { this(prompt, tags, title, mv, false, false); } + } - /** - * SunoAPI 响应的音频数据。 + * Suno API 响应的音频数据 * * @param id 音乐数据的 ID * @param title 音乐音频的标题 @@ -179,9 +178,8 @@ public class SunoApi { ) { } - /** - * SunoAPI 响应的歌词数据。 + * Suno API 响应的歌词数据。 * * @param text 歌词 * @param title 标题 @@ -194,9 +192,8 @@ public class SunoApi { ) { } - /** - * SunoAPI 响应的限额数据,目前每日免费50 + * Suno API 响应的限额数据,目前每日免费50 */ public record LimitData( @JsonProperty("credits_left") Long creditsLeft, @@ -206,5 +203,4 @@ public class SunoApi { ) { } - } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java index e89ab1f000..bd7a9bd86e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java @@ -29,7 +29,7 @@ public class SunoTests { @Test public void generate() { - List generate = sunoApi.generate(new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); + List generate = sunoApi.generate(new SunoApi.SunoRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); System.out.println(generate); }