Commit abd80fe390483620bca604cadb0f4f20e6170818
1 parent
4c89342d
【解决todo】AI Music: 结构优化,task状态同步使用统一标准Job
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,8 +50,7 @@ public enum AiModelEnum { | ||
| 50 | XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"), | 50 | XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"), |
| 51 | XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"), | 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 | SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null), | 54 | SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null), |
| 56 | SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null), | 55 | SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null), |
| 57 | SUNO_3_5( "SUNO-3.5", "chirp-v3.5",null), | 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 | import lombok.AllArgsConstructor; | 3 | import lombok.AllArgsConstructor; |
| 4 | import lombok.Getter; | 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 | @AllArgsConstructor | 11 | @AllArgsConstructor |
| 13 | @Getter | 12 | @Getter |
| 14 | public enum AiMusicStatusEnum { | 13 | public enum AiMusicStatusEnum { |
| 15 | 14 | ||
| 16 | - // TODO @xin:是不是收敛成,只有 3 个:进行中,成功,失败;类似 AiImageStatusEnum | ||
| 17 | - | ||
| 18 | - SUBMITTED("submitted", "已提交"), | ||
| 19 | - QUEUED("queued", "排队中"), | 15 | + // @xin 文档中无失败这个返回值 |
| 20 | STREAMING("streaming", "进行中"), | 16 | STREAMING("streaming", "进行中"), |
| 21 | COMPLETE("complete", "完成"); | 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 | 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.SunoLyricModeVO; | ||
| 5 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; | 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 | import io.swagger.v3.oas.annotations.Operation; | 6 | import io.swagger.v3.oas.annotations.Operation; |
| 8 | import io.swagger.v3.oas.annotations.tags.Tag; | 7 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 9 | import jakarta.validation.Valid; | 8 | import jakarta.validation.Valid; |
| @@ -17,25 +16,18 @@ import java.util.List; | @@ -17,25 +16,18 @@ import java.util.List; | ||
| 17 | 16 | ||
| 18 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 17 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 19 | 18 | ||
| 20 | -// TODO @xin:AI 前缀;都要加下哈 | ||
| 21 | @Tag(name = "管理后台 - AI 音乐生成") | 19 | @Tag(name = "管理后台 - AI 音乐生成") |
| 22 | @RestController | 20 | @RestController |
| 23 | @RequestMapping("/ai/music") | 21 | @RequestMapping("/ai/music") |
| 24 | @RequiredArgsConstructor | 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 | package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | 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 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * @author xiaoxin | ||
| 12 | + */ | ||
| 6 | @Data | 13 | @Data |
| 7 | -@JsonInclude(value = JsonInclude.Include.NON_NULL) // TODO @xin:不用加这个哈 | ||
| 8 | public class SunoReqVO { | 14 | public class SunoReqVO { |
| 9 | - /** | ||
| 10 | - * 用于生成音乐音频的提示 | ||
| 11 | - */ | 15 | + |
| 16 | + @Schema(description = "用于生成音乐音频的提示") | ||
| 12 | private String prompt; | 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 | private String mv; | 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 | \ 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/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.ai.core.model.suno.api.SunoApi; | 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | ||
| 5 | import com.baomidou.mybatisplus.annotation.IdType; | 6 | import com.baomidou.mybatisplus.annotation.IdType; |
| 7 | +import com.baomidou.mybatisplus.annotation.TableField; | ||
| 6 | import com.baomidou.mybatisplus.annotation.TableId; | 8 | import com.baomidou.mybatisplus.annotation.TableId; |
| 7 | import com.baomidou.mybatisplus.annotation.TableName; | 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 | import lombok.Data; | 11 | import lombok.Data; |
| 10 | 12 | ||
| 11 | import java.util.List; | 13 | import java.util.List; |
| 12 | -import java.util.stream.Collectors; | ||
| 13 | 14 | ||
| 14 | /** | 15 | /** |
| 15 | * @Author xiaoxin | 16 | * @Author xiaoxin |
| @@ -19,77 +20,103 @@ import java.util.stream.Collectors; | @@ -19,77 +20,103 @@ import java.util.stream.Collectors; | ||
| 19 | @Data | 20 | @Data |
| 20 | public class AiMusicDO extends BaseDO { | 21 | public class AiMusicDO extends BaseDO { |
| 21 | 22 | ||
| 22 | - // TODO @xin:@Schema 只在 VO 里使用,这里还是使用标准的注释哈 | 23 | + /** |
| 24 | + * 编号 | ||
| 25 | + */ | ||
| 23 | @TableId(type = IdType.AUTO) | 26 | @TableId(type = IdType.AUTO) |
| 24 | - @Schema(description = "编号") | ||
| 25 | private Long id; | 27 | private Long id; |
| 26 | 28 | ||
| 27 | - @Schema(description = "用户编号") | 29 | + /** |
| 30 | + * 用户编号 | ||
| 31 | + */ | ||
| 28 | private Long userId; | 32 | private Long userId; |
| 29 | 33 | ||
| 30 | - @Schema(description = "音乐名称") | 34 | + /** |
| 35 | + * 音乐名称 | ||
| 36 | + */ | ||
| 31 | private String title; | 37 | private String title; |
| 32 | 38 | ||
| 33 | - @Schema(description = "图片地址") | 39 | + /** |
| 40 | + * 图片地址 | ||
| 41 | + */ | ||
| 34 | private String imageUrl; | 42 | private String imageUrl; |
| 35 | 43 | ||
| 36 | - @Schema(description = "歌词") | 44 | + /** |
| 45 | + * 歌词 | ||
| 46 | + */ | ||
| 37 | private String lyric; | 47 | private String lyric; |
| 38 | 48 | ||
| 39 | - @Schema(description = "音频地址") | 49 | + /** |
| 50 | + * 音频地址 | ||
| 51 | + */ | ||
| 40 | private String audioUrl; | 52 | private String audioUrl; |
| 41 | 53 | ||
| 42 | - @Schema(description = "视频地址") | 54 | + /** |
| 55 | + * 视频地址 | ||
| 56 | + */ | ||
| 43 | private String videoUrl; | 57 | private String videoUrl; |
| 44 | 58 | ||
| 45 | - // TODO @xin:需要关联下对应的枚举 | ||
| 46 | - @Schema(description = "音乐状态") | 59 | + /** |
| 60 | + * 音乐状态 | ||
| 61 | + * <p> | ||
| 62 | + * 枚举 {@link AiMusicStatusEnum} | ||
| 63 | + */ | ||
| 47 | private String status; | 64 | private String status; |
| 48 | 65 | ||
| 49 | - @Schema(description = "描述词") | 66 | + /** |
| 67 | + * 描述词 | ||
| 68 | + */ | ||
| 50 | private String gptDescriptionPrompt; | 69 | private String gptDescriptionPrompt; |
| 51 | - | ||
| 52 | - @Schema(description = "提示词") | 70 | + /** |
| 71 | + * 提示词 | ||
| 72 | + */ | ||
| 53 | private String prompt; | 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 | private String model; | 90 | private String model; |
| 60 | 91 | ||
| 61 | - @Schema(description = "错误信息") | 92 | + /** |
| 93 | + * 错误信息 | ||
| 94 | + */ | ||
| 62 | private String errorMessage; | 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 | private String taskId; | 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,7 +3,6 @@ package cn.iocoder.yudao.framework.ai.config; | ||
| 3 | import cn.hutool.core.io.IoUtil; | 3 | import cn.hutool.core.io.IoUtil; |
| 4 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; | 4 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; |
| 5 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; | 5 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; |
| 6 | -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; | ||
| 7 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 8 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; | 7 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; |
| 9 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; | 8 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; |
| @@ -151,7 +150,7 @@ public class YudaoAiAutoConfiguration { | @@ -151,7 +150,7 @@ public class YudaoAiAutoConfiguration { | ||
| 151 | @Bean | 150 | @Bean |
| 152 | @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") | 151 | @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") |
| 153 | public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { | 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 | private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext, | 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
| @@ -22,6 +22,7 @@ public enum AiPlatformEnum { | @@ -22,6 +22,7 @@ public enum AiPlatformEnum { | ||
| 22 | 22 | ||
| 23 | STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI | 23 | STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI |
| 24 | MIDJOURNEY("midjourney", "midjourney"), // TODO MJ 提供的绘图,接入中 | 24 | MIDJOURNEY("midjourney", "midjourney"), // TODO MJ 提供的绘图,接入中 |
| 25 | + SUNO("Suno", "Suno"), // Suno AI | ||
| 25 | ; | 26 | ; |
| 26 | 27 | ||
| 27 | /** | 28 | /** |
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 | package cn.iocoder.yudao.framework.ai.core.model.suno.api; | 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 | import com.fasterxml.jackson.annotation.JsonInclude; | 5 | import com.fasterxml.jackson.annotation.JsonInclude; |
| 5 | import com.fasterxml.jackson.annotation.JsonProperty; | 6 | import com.fasterxml.jackson.annotation.JsonProperty; |
| 6 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 7 | import org.springframework.core.ParameterizedTypeReference; | 8 | import org.springframework.core.ParameterizedTypeReference; |
| 9 | +import org.springframework.http.HttpRequest; | ||
| 8 | import org.springframework.http.HttpStatusCode; | 10 | import org.springframework.http.HttpStatusCode; |
| 9 | import org.springframework.http.MediaType; | 11 | import org.springframework.http.MediaType; |
| 10 | import org.springframework.web.reactive.function.client.ClientResponse; | 12 | import org.springframework.web.reactive.function.client.ClientResponse; |
| @@ -17,11 +19,10 @@ import java.util.function.Predicate; | @@ -17,11 +19,10 @@ import java.util.function.Predicate; | ||
| 17 | 19 | ||
| 18 | /** | 20 | /** |
| 19 | * Suno API | 21 | * Suno API |
| 20 | - * <br> | 22 | + * <b> |
| 21 | * 文档地址:https://github.com/status2xx/suno-api/blob/main/README_CN.md | 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 | @Slf4j | 27 | @Slf4j |
| 27 | public class SunoApi { | 28 | public class SunoApi { |
| @@ -29,86 +30,88 @@ public class SunoApi { | @@ -29,86 +30,88 @@ public class SunoApi { | ||
| 29 | private final WebClient webClient; | 30 | private final WebClient webClient; |
| 30 | 31 | ||
| 31 | private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful(); | 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 | .handle((respBody, sink) -> { | 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 | this.webClient = WebClient.builder() | 43 | this.webClient = WebClient.builder() |
| 41 | - .baseUrl(config.getBaseUrl()) | 44 | + .baseUrl(baseUrl) |
| 42 | .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) | 45 | .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) |
| 43 | .build(); | 46 | .build(); |
| 44 | } | 47 | } |
| 45 | 48 | ||
| 46 | - public List<MusicData> generate(SunoRequest request) { | 49 | + public List<MusicData> generate(MusicGenerateRequest request) { |
| 47 | return this.webClient.post() | 50 | return this.webClient.post() |
| 48 | .uri("/api/generate") | 51 | .uri("/api/generate") |
| 49 | - .body(Mono.just(request), SunoRequest.class) | 52 | + .body(Mono.just(request), MusicGenerateRequest.class) |
| 50 | .retrieve() | 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 | .block(); | 57 | .block(); |
| 54 | } | 58 | } |
| 55 | 59 | ||
| 56 | - public List<MusicData> customGenerate(SunoRequest request) { | 60 | + public List<MusicData> customGenerate(MusicGenerateRequest request) { |
| 57 | return this.webClient.post() | 61 | return this.webClient.post() |
| 58 | .uri("/api/custom_generate") | 62 | .uri("/api/custom_generate") |
| 59 | - .body(Mono.just(request), SunoRequest.class) | 63 | + .body(Mono.just(request), MusicGenerateRequest.class) |
| 60 | .retrieve() | 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 | .block(); | 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 | return this.webClient.post() | 72 | return this.webClient.post() |
| 69 | .uri("/v1/chat/completions") | 73 | .uri("/v1/chat/completions") |
| 70 | - .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) | 74 | + .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class) |
| 71 | .retrieve() | 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 | .block(); | 79 | .block(); |
| 75 | } | 80 | } |
| 76 | 81 | ||
| 77 | public LyricsData generateLyrics(String prompt) { | 82 | public LyricsData generateLyrics(String prompt) { |
| 78 | return this.webClient.post() | 83 | return this.webClient.post() |
| 79 | .uri("/api/generate_lyrics") | 84 | .uri("/api/generate_lyrics") |
| 80 | - .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class) | 85 | + .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class) |
| 81 | .retrieve() | 86 | .retrieve() |
| 82 | - .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) | 87 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt)) |
| 83 | .bodyToMono(LyricsData.class) | 88 | .bodyToMono(LyricsData.class) |
| 84 | .block(); | 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 | return this.webClient.get() | 93 | return this.webClient.get() |
| 91 | .uri(uriBuilder -> uriBuilder | 94 | .uri(uriBuilder -> uriBuilder |
| 92 | .path("/api/get") | 95 | .path("/api/get") |
| 93 | - .queryParam("ids", ids) | 96 | + .queryParam("ids", CollUtil.join(ids, StrPool.COMMA)) |
| 94 | .build()) | 97 | .build()) |
| 95 | .retrieve() | 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 | .block(); | 102 | .block(); |
| 99 | } | 103 | } |
| 100 | 104 | ||
| 101 | - // TODO @xin:方法名,建议使用 getLimitUsage | ||
| 102 | - public LimitData selectLimit() { | 105 | + public LimitUsageData getLimitUsage() { |
| 103 | return this.webClient.get() | 106 | return this.webClient.get() |
| 104 | .uri("/api/get_limit") | 107 | .uri("/api/get_limit") |
| 105 | .retrieve() | 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 | .block(); | 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,7 +124,7 @@ public class SunoApi { | ||
| 121 | * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 | 124 | * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 |
| 122 | */ | 125 | */ |
| 123 | @JsonInclude(value = JsonInclude.Include.NON_NULL) | 126 | @JsonInclude(value = JsonInclude.Include.NON_NULL) |
| 124 | - public record SunoRequest( | 127 | + public record MusicGenerateRequest( |
| 125 | String prompt, | 128 | String prompt, |
| 126 | String tags, | 129 | String tags, |
| 127 | String title, | 130 | String title, |
| @@ -130,15 +133,15 @@ public class SunoApi { | @@ -130,15 +133,15 @@ public class SunoApi { | ||
| 130 | @JsonProperty("make_instrumental") boolean makeInstrumental | 133 | @JsonProperty("make_instrumental") boolean makeInstrumental |
| 131 | ) { | 134 | ) { |
| 132 | 135 | ||
| 133 | - public SunoRequest(String prompt) { | 136 | + public MusicGenerateRequest(String prompt) { |
| 134 | this(prompt, null, null, null, false, false); | 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 | this(prompt, null, null, mv, false, makeInstrumental); | 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 | this(prompt, tags, title, mv, false, false); | 145 | this(prompt, tags, title, mv, false, false); |
| 143 | } | 146 | } |
| 144 | 147 | ||
| @@ -154,12 +157,12 @@ public class SunoApi { | @@ -154,12 +157,12 @@ public class SunoApi { | ||
| 154 | * @param audioUrl 音乐音频的 URL | 157 | * @param audioUrl 音乐音频的 URL |
| 155 | * @param videoUrl 音乐视频的 URL | 158 | * @param videoUrl 音乐视频的 URL |
| 156 | * @param createdAt 音乐音频的创建时间 | 159 | * @param createdAt 音乐音频的创建时间 |
| 157 | - * @param modelName | 160 | + * @param modelName 模型名称 |
| 158 | * @param status submitted、queued、streaming、complete | 161 | * @param status submitted、queued、streaming、complete |
| 159 | - * @param gptDescriptionPrompt | 162 | + * @param gptDescriptionPrompt 描述词 |
| 160 | * @param prompt 生成音乐音频的提示 | 163 | * @param prompt 生成音乐音频的提示 |
| 161 | - * @param type | ||
| 162 | - * @param tags | 164 | + * @param type 操作类型 |
| 165 | + * @param tags 音乐类型标签 | ||
| 163 | */ | 166 | */ |
| 164 | public record MusicData( | 167 | public record MusicData( |
| 165 | String id, | 168 | String id, |
| @@ -195,7 +198,7 @@ public class SunoApi { | @@ -195,7 +198,7 @@ public class SunoApi { | ||
| 195 | /** | 198 | /** |
| 196 | * Suno API 响应的限额数据,目前每日免费50 | 199 | * Suno API 响应的限额数据,目前每日免费50 |
| 197 | */ | 200 | */ |
| 198 | - public record LimitData( | 201 | + public record LimitUsageData( |
| 199 | @JsonProperty("credits_left") Long creditsLeft, | 202 | @JsonProperty("credits_left") Long creditsLeft, |
| 200 | String period, | 203 | String period, |
| 201 | @JsonProperty("monthly_limit") Long monthlyLimit, | 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 | package cn.iocoder.yudao.framework.ai.suno; | 1 | package cn.iocoder.yudao.framework.ai.suno; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; | ||
| 4 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | 3 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 5 | import org.junit.Before; | 4 | import org.junit.Before; |
| 6 | import org.junit.Test; | 5 | import org.junit.Test; |
| @@ -17,26 +16,26 @@ public class SunoTests { | @@ -17,26 +16,26 @@ public class SunoTests { | ||
| 17 | 16 | ||
| 18 | @Before | 17 | @Before |
| 19 | public void setup() { | 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 | @Test | 23 | @Test |
| 25 | public void selectById() { | 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 | @Test | 29 | @Test |
| 31 | public void generate() { | 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 | System.out.println(generate); | 32 | System.out.println(generate); |
| 34 | } | 33 | } |
| 35 | 34 | ||
| 36 | 35 | ||
| 37 | @Test | 36 | @Test |
| 38 | public void doChatCompletion() { | 37 | public void doChatCompletion() { |
| 39 | - List<SunoApi.MusicData> generate = sunoApi.doChatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); | 38 | + List<SunoApi.MusicData> generate = sunoApi.chatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); |
| 40 | System.out.println(generate); | 39 | System.out.println(generate); |
| 41 | } | 40 | } |
| 42 | 41 | ||
| @@ -50,8 +49,8 @@ public class SunoTests { | @@ -50,8 +49,8 @@ public class SunoTests { | ||
| 50 | 49 | ||
| 51 | @Test | 50 | @Test |
| 52 | public void selectLimit() { | 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,7 +199,7 @@ yudao.ai: | ||
| 199 | channel-id: 1237948819677904960 | 199 | channel-id: 1237948819677904960 |
| 200 | suno: | 200 | suno: |
| 201 | enable: true | 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 |