Commit 4c89342d5bd700156a4d4a9f00e384bfa3785b5c
Merge remote-tracking branch 'origin/master-jdk21-ai' into master-jdk21-ai
Showing
11 changed files
with
67 additions
and
48 deletions
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; |
| 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)可以加下枚举类的注释 | |
| 6 | 8 | /** |
| 7 | 9 | * @Author xiaoxin |
| 8 | 10 | * @Date 2024/6/5 |
| ... | ... | @@ -11,6 +13,8 @@ import lombok.Getter; |
| 11 | 13 | @Getter |
| 12 | 14 | public enum AiMusicStatusEnum { |
| 13 | 15 | |
| 16 | + // TODO @xin:是不是收敛成,只有 3 个:进行中,成功,失败;类似 AiImageStatusEnum | |
| 17 | + | |
| 14 | 18 | SUBMITTED("submitted", "已提交"), |
| 15 | 19 | QUEUED("queued", "排队中"), |
| 16 | 20 | STREAMING("streaming", "进行中"), | ... | ... |
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 { |
| 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 逻辑 | |
| 53 | 54 | //Suno |
| 54 | 55 | SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null), |
| 55 | 56 | SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null), | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java
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; |
| 4 | 4 | import lombok.Data; |
| 5 | 5 | |
| 6 | 6 | @Data |
| 7 | -@JsonInclude(value = JsonInclude.Include.NON_NULL) | |
| 7 | +@JsonInclude(value = JsonInclude.Include.NON_NULL) // TODO @xin:不用加这个哈 | |
| 8 | 8 | public class SunoReqVO { |
| 9 | 9 | /** |
| 10 | 10 | * 用于生成音乐音频的提示 |
| 11 | 11 | */ |
| 12 | 12 | private String prompt; |
| 13 | + // TODO @xin:Boolean,不使用基本类型。 | |
| 13 | 14 | /** |
| 14 | 15 | * 是否纯音乐 |
| 15 | 16 | */ | ... | ... |
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; |
| 18 | 18 | @TableName("ai_music") |
| 19 | 19 | @Data |
| 20 | 20 | public class AiMusicDO extends BaseDO { |
| 21 | + | |
| 22 | + // TODO @xin:@Schema 只在 VO 里使用,这里还是使用标准的注释哈 | |
| 21 | 23 | @TableId(type = IdType.AUTO) |
| 22 | 24 | @Schema(description = "编号") |
| 23 | 25 | private Long id; |
| ... | ... | @@ -40,6 +42,7 @@ public class AiMusicDO extends BaseDO { |
| 40 | 42 | @Schema(description = "视频地址") |
| 41 | 43 | private String videoUrl; |
| 42 | 44 | |
| 45 | + // TODO @xin:需要关联下对应的枚举 | |
| 43 | 46 | @Schema(description = "音乐状态") |
| 44 | 47 | private String status; |
| 45 | 48 | |
| ... | ... | @@ -49,19 +52,24 @@ public class AiMusicDO extends BaseDO { |
| 49 | 52 | @Schema(description = "提示词") |
| 50 | 53 | private String prompt; |
| 51 | 54 | |
| 55 | + // TODO @xin:生成模式,需要记录下;歌词、描述 | |
| 56 | + | |
| 57 | + // TODO @xin:多存储一个平台,platform;考虑未来可能有别的音乐接口 | |
| 52 | 58 | @Schema(description = "模型") |
| 53 | 59 | private String model; |
| 54 | 60 | |
| 55 | 61 | @Schema(description = "错误信息") |
| 56 | 62 | private String errorMessage; |
| 57 | 63 | |
| 64 | + // TODO @xin:tags 要不要使用 List<String> | |
| 65 | + | |
| 58 | 66 | @Schema(description = "音乐风格标签") |
| 59 | 67 | private String tags; |
| 60 | 68 | |
| 61 | - @Schema(description = "任务id") | |
| 69 | + @Schema(description = "任务编号") | |
| 62 | 70 | private String taskId; |
| 63 | 71 | |
| 64 | - | |
| 72 | + // TODO @xin:转换不放在 DO 里面哈。 | |
| 65 | 73 | |
| 66 | 74 | public static AiMusicDO convertFrom(SunoApi.MusicData musicData) { |
| 67 | 75 | return new AiMusicDO() |
| ... | ... | @@ -84,5 +92,4 @@ public class AiMusicDO extends BaseDO { |
| 84 | 92 | .collect(Collectors.toList()); |
| 85 | 93 | } |
| 86 | 94 | |
| 87 | - | |
| 88 | 95 | } | ... | ... |
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; |
| 5 | 5 | import org.apache.ibatis.annotations.Mapper; |
| 6 | 6 | |
| 7 | 7 | /** |
| 8 | - * @Author xiaoxin | |
| 9 | - * @Date 2024/6/5 | |
| 8 | + * AI 音乐 Mapper | |
| 9 | + * @author xiaoxin | |
| 10 | 10 | */ |
| 11 | 11 | @Mapper |
| 12 | 12 | public interface AiMusicMapper extends BaseMapperX<AiMusicDO> { |
| 13 | - | |
| 14 | 13 | } | ... | ... |
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 |
| 31 | 31 | @Slf4j |
| 32 | 32 | public class MusicServiceImpl implements MusicService { |
| 33 | 33 | |
| 34 | + // TODO @xin:使用 @Resource 注入,整个项目保持统一哈; | |
| 34 | 35 | private final SunoApi sunoApi; |
| 35 | 36 | private final AiMusicMapper musicMapper; |
| 36 | 37 | |
| 37 | 38 | private final Queue<String> taskQueue = new ConcurrentLinkedQueue<>(); |
| 38 | 39 | |
| 40 | + // TODO @xin:要不把 descriptionMode、lyricMode 合并,同一个 generateMusic 方法,然后根据传入的 mode 模式:歌词、描述来区分? | |
| 39 | 41 | |
| 40 | 42 | @Override |
| 41 | 43 | public List<Long> descriptionMode(SunoReqVO reqVO) { |
| 42 | - SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental()); | |
| 43 | - //默认异步 | |
| 44 | + // 1. 异步生成 | |
| 45 | + SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental()); | |
| 44 | 46 | List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq); |
| 47 | + // 2. 插入数据库 | |
| 45 | 48 | return insertMusicData(musicDataList); |
| 46 | 49 | } |
| 47 | 50 | |
| 48 | - | |
| 49 | 51 | @Override |
| 50 | 52 | public List<Long> lyricMode(SunoLyricModeVO reqVO) { |
| 51 | - SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle()); | |
| 52 | - //默认异步 | |
| 53 | + // 1. 异步生成 | |
| 54 | + SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle()); | |
| 53 | 55 | List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq); |
| 56 | + // 2. 插入数据库 | |
| 54 | 57 | return insertMusicData(musicDataList); |
| 55 | 58 | } |
| 56 | 59 | |
| ... | ... | @@ -64,6 +67,7 @@ public class MusicServiceImpl implements MusicService { |
| 64 | 67 | if (CollUtil.isEmpty(musicDataList)) { |
| 65 | 68 | return Collections.emptyList(); |
| 66 | 69 | } |
| 70 | + // TODO @xin:建议使用 insertBatch 方法,批量插入 | |
| 67 | 71 | return AiMusicDO.convertFrom(musicDataList).stream() |
| 68 | 72 | .peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId()))) |
| 69 | 73 | .peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add)) |
| ... | ... | @@ -71,6 +75,7 @@ public class MusicServiceImpl implements MusicService { |
| 71 | 75 | .collect(Collectors.toList()); |
| 72 | 76 | } |
| 73 | 77 | |
| 78 | + // TODO @xin:这个,改成标准的 job 来实现哈。从数据库加载任务,然后执行。 | |
| 74 | 79 | @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) |
| 75 | 80 | @Transactional |
| 76 | 81 | public void flushSunoTask() { | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
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; |
| 4 | 4 | import lombok.Data; |
| 5 | 5 | import lombok.NoArgsConstructor; |
| 6 | 6 | |
| 7 | +// TODO @xin:不需要这个类哈,直接 SunoApi 传入 baseUrl 参数即可 | |
| 7 | 8 | /** |
| 8 | - * @Author xiaoxin | |
| 9 | - * @Date 2024/5/29 | |
| 9 | + * Suno 配置类 | |
| 10 | + * | |
| 11 | + * @author xiaoxin | |
| 10 | 12 | */ |
| 11 | 13 | @Data |
| 12 | 14 | @NoArgsConstructor |
| 13 | 15 | @AllArgsConstructor |
| 14 | 16 | public class SunoConfig { |
| 17 | + | |
| 15 | 18 | /** |
| 16 | 19 | * suno-api服务的基本路径 |
| 17 | 20 | */ |
| 18 | 21 | private String baseUrl; |
| 22 | + | |
| 19 | 23 | } | ... | ... |
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; |
| 27 | 27 | public class SunoApi { |
| 28 | 28 | |
| 29 | 29 | private final WebClient webClient; |
| 30 | + | |
| 30 | 31 | private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful(); |
| 31 | 32 | private final Function<ClientResponse, Mono<? extends Throwable>> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class) |
| 32 | 33 | .handle((respBody, sink) -> { |
| 34 | + // TODO @xin:最好是 request、response 都有哈 | |
| 33 | 35 | log.error("【suno-api】调用失败!resp: 【{}】", respBody); |
| 34 | 36 | sink.error(new IllegalStateException("【suno-api】调用失败!")); |
| 35 | 37 | }); |
| 36 | 38 | |
| 37 | - | |
| 38 | 39 | public SunoApi(SunoConfig config) { |
| 39 | 40 | this.webClient = WebClient.builder() |
| 40 | 41 | .baseUrl(config.getBaseUrl()) |
| ... | ... | @@ -42,50 +43,49 @@ public class SunoApi { |
| 42 | 43 | .build(); |
| 43 | 44 | } |
| 44 | 45 | |
| 45 | - public List<MusicData> generate(SunoApi.SunoReq sunReq) { | |
| 46 | + public List<MusicData> generate(SunoRequest request) { | |
| 46 | 47 | return this.webClient.post() |
| 47 | 48 | .uri("/api/generate") |
| 48 | - .body(Mono.just(sunReq), SunoApi.SunoReq.class) | |
| 49 | + .body(Mono.just(request), SunoRequest.class) | |
| 49 | 50 | .retrieve() |
| 50 | 51 | .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) |
| 51 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 52 | - }) | |
| 52 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 53 | 53 | .block(); |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | - public List<MusicData> customGenerate(SunoApi.SunoReq sunReq) { | |
| 56 | + public List<MusicData> customGenerate(SunoRequest request) { | |
| 57 | 57 | return this.webClient.post() |
| 58 | 58 | .uri("/api/custom_generate") |
| 59 | - .body(Mono.just(sunReq), SunoApi.SunoReq.class) | |
| 59 | + .body(Mono.just(request), SunoRequest.class) | |
| 60 | 60 | .retrieve() |
| 61 | 61 | .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) |
| 62 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 63 | - }) | |
| 62 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 64 | 63 | .block(); |
| 65 | 64 | } |
| 66 | 65 | |
| 66 | + // TODO @xin: 是不是叫 chatCompletion | |
| 67 | 67 | public List<MusicData> doChatCompletion(String prompt) { |
| 68 | 68 | return this.webClient.post() |
| 69 | 69 | .uri("/v1/chat/completions") |
| 70 | - .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class) | |
| 70 | + .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) | |
| 71 | 71 | .retrieve() |
| 72 | 72 | .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) |
| 73 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 74 | - }) | |
| 73 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 75 | 74 | .block(); |
| 76 | 75 | } |
| 77 | 76 | |
| 78 | 77 | public LyricsData generateLyrics(String prompt) { |
| 79 | 78 | return this.webClient.post() |
| 80 | 79 | .uri("/api/generate_lyrics") |
| 81 | - .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class) | |
| 80 | + .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) | |
| 82 | 81 | .retrieve() |
| 83 | 82 | .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) |
| 84 | 83 | .bodyToMono(LyricsData.class) |
| 85 | 84 | .block(); |
| 86 | 85 | } |
| 87 | 86 | |
| 88 | - | |
| 87 | + // TODO @xin:应该传入 List<String> ids | |
| 88 | + // TODO @xin:方法名,建议使用 getMusicList | |
| 89 | 89 | public List<MusicData> selectById(String ids) { |
| 90 | 90 | return this.webClient.get() |
| 91 | 91 | .uri(uriBuilder -> uriBuilder |
| ... | ... | @@ -94,12 +94,11 @@ public class SunoApi { |
| 94 | 94 | .build()) |
| 95 | 95 | .retrieve() |
| 96 | 96 | .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) |
| 97 | - .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { | |
| 98 | - }) | |
| 97 | + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { }) | |
| 99 | 98 | .block(); |
| 100 | 99 | } |
| 101 | 100 | |
| 102 | - | |
| 101 | + // TODO @xin:方法名,建议使用 getLimitUsage | |
| 103 | 102 | public LimitData selectLimit() { |
| 104 | 103 | return this.webClient.get() |
| 105 | 104 | .uri("/api/get_limit") |
| ... | ... | @@ -109,7 +108,7 @@ public class SunoApi { |
| 109 | 108 | .block(); |
| 110 | 109 | } |
| 111 | 110 | |
| 112 | - | |
| 111 | + // TODO @xin:可以改成 MusicGenerateRequest | |
| 113 | 112 | /** |
| 114 | 113 | * 根据提示生成音频 |
| 115 | 114 | * |
| ... | ... | @@ -117,12 +116,12 @@ public class SunoApi { |
| 117 | 116 | * @param tags 音乐风格 |
| 118 | 117 | * @param title 音乐名称 |
| 119 | 118 | * @param mv 模型 |
| 120 | - * @param waitAudio false表示后台模式,仅返回音频任务信息,需要调用get API获取详细的音频信息。 | |
| 121 | - * true表示同步模式,API最多等待100s,音频生成完毕后直接返回音频链接等信息,建议在GPT等agent中使用。 | |
| 119 | + * @param waitAudio false 表示后台模式,仅返回音频任务信息,需要调用 get API 获取详细的音频信息。 | |
| 120 | + * true 表示同步模式,API 最多等待 100s,音频生成完毕后直接返回音频链接等信息,建议在 GPT 等 agent 中使用。 | |
| 122 | 121 | * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 |
| 123 | 122 | */ |
| 124 | 123 | @JsonInclude(value = JsonInclude.Include.NON_NULL) |
| 125 | - public record SunoReq( | |
| 124 | + public record SunoRequest( | |
| 126 | 125 | String prompt, |
| 127 | 126 | String tags, |
| 128 | 127 | String title, |
| ... | ... | @@ -130,23 +129,23 @@ public class SunoApi { |
| 130 | 129 | @JsonProperty("wait_audio") boolean waitAudio, |
| 131 | 130 | @JsonProperty("make_instrumental") boolean makeInstrumental |
| 132 | 131 | ) { |
| 133 | - public SunoReq(String prompt) { | |
| 132 | + | |
| 133 | + public SunoRequest(String prompt) { | |
| 134 | 134 | this(prompt, null, null, null, false, false); |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | - public SunoReq(String prompt, String mv, boolean makeInstrumental) { | |
| 137 | + public SunoRequest(String prompt, String mv, boolean makeInstrumental) { | |
| 138 | 138 | this(prompt, null, null, mv, false, makeInstrumental); |
| 139 | 139 | } |
| 140 | 140 | |
| 141 | - | |
| 142 | - public SunoReq(String prompt, String mv, String tags, String title) { | |
| 141 | + public SunoRequest(String prompt, String mv, String tags, String title) { | |
| 143 | 142 | this(prompt, tags, title, mv, false, false); |
| 144 | 143 | } |
| 145 | - } | |
| 146 | 144 | |
| 145 | + } | |
| 147 | 146 | |
| 148 | 147 | /** |
| 149 | - * SunoAPI 响应的音频数据。 | |
| 148 | + * Suno API 响应的音频数据 | |
| 150 | 149 | * |
| 151 | 150 | * @param id 音乐数据的 ID |
| 152 | 151 | * @param title 音乐音频的标题 |
| ... | ... | @@ -179,9 +178,8 @@ public class SunoApi { |
| 179 | 178 | ) { |
| 180 | 179 | } |
| 181 | 180 | |
| 182 | - | |
| 183 | 181 | /** |
| 184 | - * SunoAPI 响应的歌词数据。 | |
| 182 | + * Suno API 响应的歌词数据。 | |
| 185 | 183 | * |
| 186 | 184 | * @param text 歌词 |
| 187 | 185 | * @param title 标题 |
| ... | ... | @@ -194,9 +192,8 @@ public class SunoApi { |
| 194 | 192 | ) { |
| 195 | 193 | } |
| 196 | 194 | |
| 197 | - | |
| 198 | 195 | /** |
| 199 | - * SunoAPI 响应的限额数据,目前每日免费50 | |
| 196 | + * Suno API 响应的限额数据,目前每日免费50 | |
| 200 | 197 | */ |
| 201 | 198 | public record LimitData( |
| 202 | 199 | @JsonProperty("credits_left") Long creditsLeft, |
| ... | ... | @@ -206,5 +203,4 @@ public class SunoApi { |
| 206 | 203 | ) { |
| 207 | 204 | } |
| 208 | 205 | |
| 209 | - | |
| 210 | 206 | } | ... | ... |
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 { |
| 29 | 29 | |
| 30 | 30 | @Test |
| 31 | 31 | public void generate() { |
| 32 | - List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); | |
| 32 | + List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); | |
| 33 | 33 | System.out.println(generate); |
| 34 | 34 | } |
| 35 | 35 | ... | ... |