Commit f1292fb7fe3cfc41430ddd5ddd8f2af0a993ae0d
1 parent
085ebfc7
【增加】AI Music支持描述模式、歌词模式生成音乐
Showing
18 changed files
with
291 additions
and
311 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiModelEnum.java
| ... | ... | @@ -49,6 +49,11 @@ public enum AiModelEnum { |
| 49 | 49 | XING_HUO_2_0("星火大模型2.0", "generalv2", "/v2.1/chat"), |
| 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 | + | |
| 53 | + //Suno | |
| 54 | + SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null), | |
| 55 | + SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null), | |
| 56 | + SUNO_3_5( "SUNO-3.5", "chirp-v3.5",null), | |
| 52 | 57 | ; |
| 53 | 58 | |
| 54 | 59 | /** | ... | ... |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.enums; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Getter; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * @Author xiaoxin | |
| 8 | + * @Date 2024/6/5 | |
| 9 | + */ | |
| 10 | +@AllArgsConstructor | |
| 11 | +@Getter | |
| 12 | +public enum AiMusicStatusEnum { | |
| 13 | + | |
| 14 | + SUBMITTED("submitted", "已提交"), | |
| 15 | + QUEUED("queued", "排队中"), | |
| 16 | + STREAMING("streaming", "进行中"), | |
| 17 | + COMPLETE("complete", "完成"); | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * 状态 | |
| 21 | + */ | |
| 22 | + private final String status; | |
| 23 | + /** | |
| 24 | + * 状态名 | |
| 25 | + */ | |
| 26 | + private final String name; | |
| 27 | + | |
| 28 | + public static AiMusicStatusEnum valueOfStatus(String status) { | |
| 29 | + for (AiMusicStatusEnum statusEnum : AiMusicStatusEnum.values()) { | |
| 30 | + if (statusEnum.getStatus().equals(status)) { | |
| 31 | + return statusEnum; | |
| 32 | + } | |
| 33 | + } | |
| 34 | + throw new IllegalArgumentException("未知会话状态: " + status); | |
| 35 | + } | |
| 36 | + | |
| 37 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.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; | |
| 4 | 5 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; |
| 5 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO; | |
| 6 | 6 | import cn.iocoder.yudao.module.ai.service.music.MusicService; |
| 7 | 7 | import io.swagger.v3.oas.annotations.Operation; |
| 8 | 8 | import io.swagger.v3.oas.annotations.tags.Tag; |
| ... | ... | @@ -13,6 +13,8 @@ import org.springframework.web.bind.annotation.RequestBody; |
| 13 | 13 | import org.springframework.web.bind.annotation.RequestMapping; |
| 14 | 14 | import org.springframework.web.bind.annotation.RestController; |
| 15 | 15 | |
| 16 | +import java.util.List; | |
| 17 | + | |
| 16 | 18 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 17 | 19 | |
| 18 | 20 | @Tag(name = "管理后台 - AI 音乐生成") |
| ... | ... | @@ -23,9 +25,16 @@ public class MusicController { |
| 23 | 25 | |
| 24 | 26 | private final MusicService musicService; |
| 25 | 27 | |
| 26 | - @PostMapping("/suno-gen") | |
| 27 | - @Operation(summary = "音乐生成") | |
| 28 | - public CommonResult<SunoRespVO> musicGen(@RequestBody @Valid SunoReqVO sunoReqVO) { | |
| 29 | - return success(musicService.musicGen(sunoReqVO)); | |
| 28 | + @PostMapping("generate/description-mode") | |
| 29 | + @Operation(summary = "音乐生成-描述模式") | |
| 30 | + public CommonResult<List<Long>> descriptionMode(@RequestBody @Valid SunoReqVO sunoReqVO) { | |
| 31 | + return success(musicService.descriptionMode(sunoReqVO)); | |
| 32 | + } | |
| 33 | + | |
| 34 | + @PostMapping("generate/lyric-mode") | |
| 35 | + @Operation(summary = "音乐生成-歌词模式") | |
| 36 | + public CommonResult<List<Long>> lyricMode(@RequestBody @Valid SunoLyricModeVO sunoLyricModeVO) { | |
| 37 | + return success(musicService.lyricMode(sunoLyricModeVO)); | |
| 30 | 38 | } |
| 39 | + | |
| 31 | 40 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java deleted
| 1 | -package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; | |
| 4 | -import com.fasterxml.jackson.annotation.JsonProperty; | |
| 5 | -import lombok.Data; | |
| 6 | - | |
| 7 | -import java.util.List; | |
| 8 | -import java.util.stream.Collectors; | |
| 9 | - | |
| 10 | -/** | |
| 11 | - * 表示单个音乐数据的类 | |
| 12 | - */ | |
| 13 | -@Data | |
| 14 | -public class MusicDataVO { | |
| 15 | - /** | |
| 16 | - * 音乐数据的 ID | |
| 17 | - */ | |
| 18 | - private String id; | |
| 19 | - | |
| 20 | - /** | |
| 21 | - * 音乐音频的标题 | |
| 22 | - */ | |
| 23 | - private String title; | |
| 24 | - | |
| 25 | - /** | |
| 26 | - * 音乐音频的图片 URL | |
| 27 | - */ | |
| 28 | - @JsonProperty("image_url") | |
| 29 | - private String imageUrl; | |
| 30 | - | |
| 31 | - /** | |
| 32 | - * 音乐音频的歌词 | |
| 33 | - */ | |
| 34 | - private String lyric; | |
| 35 | - | |
| 36 | - /** | |
| 37 | - * 音乐音频的 URL | |
| 38 | - */ | |
| 39 | - @JsonProperty("audio_url") | |
| 40 | - private String audioUrl; | |
| 41 | - | |
| 42 | - /** | |
| 43 | - * 音乐视频的 URL | |
| 44 | - */ | |
| 45 | - @JsonProperty("video_url") | |
| 46 | - private String videoUrl; | |
| 47 | - | |
| 48 | - /** | |
| 49 | - * 音乐音频的创建时间 | |
| 50 | - */ | |
| 51 | - @JsonProperty("created_at") | |
| 52 | - private String createdAt; | |
| 53 | - | |
| 54 | - /** | |
| 55 | - * 使用的模型名称 | |
| 56 | - */ | |
| 57 | - private String model; | |
| 58 | - | |
| 59 | - /** | |
| 60 | - * 生成音乐音频的提示 | |
| 61 | - */ | |
| 62 | - private String prompt; | |
| 63 | - | |
| 64 | - /** | |
| 65 | - * 音乐音频的风格 | |
| 66 | - */ | |
| 67 | - private String style; | |
| 68 | - | |
| 69 | - public static List<MusicDataVO> convertFrom(List<AceDataSunoApi.SunoResp.MusicData> musicDataList) { | |
| 70 | - return musicDataList.stream().map(musicData -> { | |
| 71 | - MusicDataVO musicDataVO = new MusicDataVO(); | |
| 72 | - musicDataVO.setId(musicData.id()); | |
| 73 | - musicDataVO.setTitle(musicData.title()); | |
| 74 | - musicDataVO.setImageUrl(musicData.imageUrl()); | |
| 75 | - musicDataVO.setLyric(musicData.lyric()); | |
| 76 | - musicDataVO.setAudioUrl(musicData.audioUrl()); | |
| 77 | - musicDataVO.setVideoUrl(musicData.videoUrl()); | |
| 78 | - musicDataVO.setCreatedAt(musicData.createdAt()); | |
| 79 | - musicDataVO.setModel(musicData.model()); | |
| 80 | - musicDataVO.setPrompt(musicData.prompt()); | |
| 81 | - musicDataVO.setStyle(musicData.style()); | |
| 82 | - return musicDataVO; | |
| 83 | - }).collect(Collectors.toList()); | |
| 84 | - } | |
| 85 | -} | |
| 86 | 0 | \ No newline at end of file |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoLyricModeVO.java
0 → 100644
| 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
| ... | ... | @@ -2,39 +2,21 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo; |
| 2 | 2 | |
| 3 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; |
| 4 | 4 | import lombok.Data; |
| 5 | -import lombok.experimental.Accessors; | |
| 6 | 5 | |
| 7 | 6 | @Data |
| 8 | -@Accessors(chain = true) | |
| 9 | 7 | @JsonInclude(value = JsonInclude.Include.NON_NULL) |
| 10 | 8 | public class SunoReqVO { |
| 11 | 9 | /** |
| 12 | 10 | * 用于生成音乐音频的提示 |
| 13 | 11 | */ |
| 14 | 12 | private String prompt; |
| 15 | - | |
| 16 | - /** | |
| 17 | - * 用于生成音乐音频的歌词 | |
| 18 | - */ | |
| 19 | - private String lyric; | |
| 20 | - | |
| 21 | 13 | /** |
| 22 | - * 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 | |
| 14 | + * 是否纯音乐 | |
| 23 | 15 | */ |
| 24 | - private boolean custom; | |
| 25 | - | |
| 26 | - /** | |
| 27 | - * 音乐音频的标题 | |
| 28 | - */ | |
| 29 | - private String title; | |
| 30 | - | |
| 31 | - /** | |
| 32 | - * 音乐音频的风格 | |
| 33 | - */ | |
| 34 | - private String style; | |
| 35 | - | |
| 16 | + private boolean makeInstrumental; | |
| 36 | 17 | /** |
| 37 | - * 音乐音频生成后回调的 URL | |
| 18 | + * //todo 首次请求返回的模型是对的,后续更新音频返回的模型又变成v3.5了 | |
| 19 | + * 模型版本 {@link cn.iocoder.yudao.module.ai.enums.AiModelEnum} Suno | |
| 38 | 20 | */ |
| 39 | - private String callbackUrl; | |
| 21 | + private String mv; | |
| 40 | 22 | } |
| 41 | 23 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java deleted
| 1 | -package cn.iocoder.yudao.module.ai.controller.admin.music.vo; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; | |
| 4 | -import com.fasterxml.jackson.annotation.JsonProperty; | |
| 5 | -import lombok.Data; | |
| 6 | - | |
| 7 | -import java.util.List; | |
| 8 | - | |
| 9 | -/** | |
| 10 | - * API 响应的数据 | |
| 11 | - */ | |
| 12 | -@Data | |
| 13 | -public class SunoRespVO { | |
| 14 | - /** | |
| 15 | - * 表示请求是否成功 | |
| 16 | - */ | |
| 17 | - private boolean success; | |
| 18 | - | |
| 19 | - /** | |
| 20 | - * 任务 ID | |
| 21 | - */ | |
| 22 | - @JsonProperty("task_id") | |
| 23 | - private String taskId; | |
| 24 | - | |
| 25 | - /** | |
| 26 | - * 音乐数据列表 | |
| 27 | - */ | |
| 28 | - private List<MusicDataVO> data; | |
| 29 | - | |
| 30 | - | |
| 31 | - //把 SunoResp转为本vo类 | |
| 32 | - public static SunoRespVO convertFrom(AceDataSunoApi.SunoResp sunoResp) { | |
| 33 | - SunoRespVO sunoRespVO = new SunoRespVO(); | |
| 34 | - sunoRespVO.setSuccess(sunoResp.success()); | |
| 35 | - sunoRespVO.setTaskId(sunoResp.taskId()); | |
| 36 | - sunoRespVO.setData(MusicDataVO.convertFrom(sunoResp.data())); | |
| 37 | - return sunoRespVO; | |
| 38 | - } | |
| 39 | - | |
| 40 | -} | |
| 41 | 0 | \ No newline at end of file |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.dal.dataobject.music; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | |
| 5 | +import com.baomidou.mybatisplus.annotation.IdType; | |
| 6 | +import com.baomidou.mybatisplus.annotation.TableId; | |
| 7 | +import com.baomidou.mybatisplus.annotation.TableName; | |
| 8 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 9 | +import lombok.Data; | |
| 10 | + | |
| 11 | +import java.util.List; | |
| 12 | +import java.util.stream.Collectors; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * @Author xiaoxin | |
| 16 | + * @Date 2024/6/5 | |
| 17 | + */ | |
| 18 | +@TableName("ai_music") | |
| 19 | +@Data | |
| 20 | +public class AiMusicDO extends BaseDO { | |
| 21 | + @TableId(type = IdType.AUTO) | |
| 22 | + @Schema(description = "编号") | |
| 23 | + private Long id; | |
| 24 | + | |
| 25 | + @Schema(description = "用户编号") | |
| 26 | + private Long userId; | |
| 27 | + | |
| 28 | + @Schema(description = "音乐名称") | |
| 29 | + private String title; | |
| 30 | + | |
| 31 | + @Schema(description = "图片地址") | |
| 32 | + private String imageUrl; | |
| 33 | + | |
| 34 | + @Schema(description = "歌词") | |
| 35 | + private String lyric; | |
| 36 | + | |
| 37 | + @Schema(description = "音频地址") | |
| 38 | + private String audioUrl; | |
| 39 | + | |
| 40 | + @Schema(description = "视频地址") | |
| 41 | + private String videoUrl; | |
| 42 | + | |
| 43 | + @Schema(description = "音乐状态") | |
| 44 | + private String status; | |
| 45 | + | |
| 46 | + @Schema(description = "描述词") | |
| 47 | + private String gptDescriptionPrompt; | |
| 48 | + | |
| 49 | + @Schema(description = "提示词") | |
| 50 | + private String prompt; | |
| 51 | + | |
| 52 | + @Schema(description = "模型") | |
| 53 | + private String model; | |
| 54 | + | |
| 55 | + @Schema(description = "错误信息") | |
| 56 | + private String errorMessage; | |
| 57 | + | |
| 58 | + @Schema(description = "音乐风格标签") | |
| 59 | + private String tags; | |
| 60 | + | |
| 61 | + @Schema(description = "任务id") | |
| 62 | + private String taskId; | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + public static AiMusicDO convertFrom(SunoApi.MusicData musicData) { | |
| 67 | + return new AiMusicDO() | |
| 68 | + .setTaskId(musicData.id()) | |
| 69 | + .setPrompt(musicData.prompt()) | |
| 70 | + .setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) | |
| 71 | + .setAudioUrl(musicData.audioUrl()) | |
| 72 | + .setVideoUrl(musicData.videoUrl()) | |
| 73 | + .setImageUrl(musicData.imageUrl()) | |
| 74 | + .setLyric(musicData.lyric()) | |
| 75 | + .setTitle(musicData.title()) | |
| 76 | + .setStatus(musicData.status()) | |
| 77 | + .setModel(musicData.modelName()) | |
| 78 | + .setTags(musicData.tags()); | |
| 79 | + } | |
| 80 | + | |
| 81 | + public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> musicDataList) { | |
| 82 | + return musicDataList.stream() | |
| 83 | + .map(AiMusicDO::convertFrom) | |
| 84 | + .collect(Collectors.toList()); | |
| 85 | + } | |
| 86 | + | |
| 87 | + | |
| 88 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.dal.mysql.music; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | |
| 4 | +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | |
| 5 | +import org.apache.ibatis.annotations.Mapper; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * @Author xiaoxin | |
| 9 | + * @Date 2024/6/5 | |
| 10 | + */ | |
| 11 | +@Mapper | |
| 12 | +public interface AiMusicMapper extends BaseMapperX<AiMusicDO> { | |
| 13 | + | |
| 14 | +} | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.service.music; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO; | |
| 3 | 4 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; |
| 4 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 5 | 7 | |
| 6 | 8 | /** |
| 7 | 9 | * @Author xiaoxin |
| ... | ... | @@ -10,10 +12,13 @@ import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO; |
| 10 | 12 | public interface MusicService { |
| 11 | 13 | |
| 12 | 14 | /** |
| 13 | - * 音乐生成 | |
| 14 | - * | |
| 15 | - * @param sunoReqVO 请求实体 | |
| 16 | - * @return 响应实体 | |
| 15 | + * 音乐生成-描述模式 | |
| 17 | 16 | */ |
| 18 | - SunoRespVO musicGen(SunoReqVO sunoReqVO); | |
| 17 | + List<Long> descriptionMode(SunoReqVO reqVO); | |
| 18 | + | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * 音乐生成-歌词模式 | |
| 22 | + **/ | |
| 23 | + List<Long> lyricMode(SunoLyricModeVO reqVO); | |
| 19 | 24 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.service.music; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; | |
| 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; | |
| 4 | 7 | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; |
| 5 | -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO; | |
| 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; | |
| 6 | 12 | import lombok.RequiredArgsConstructor; |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | +import org.springframework.scheduling.annotation.Scheduled; | |
| 7 | 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; | |
| 8 | 24 | |
| 9 | 25 | /** |
| 10 | 26 | * @Author xiaoxin |
| ... | ... | @@ -12,20 +28,70 @@ import org.springframework.stereotype.Service; |
| 12 | 28 | */ |
| 13 | 29 | @Service |
| 14 | 30 | @RequiredArgsConstructor |
| 31 | +@Slf4j | |
| 15 | 32 | public class MusicServiceImpl implements MusicService { |
| 16 | 33 | |
| 17 | - private final AceDataSunoApi aceDataSunoApi; | |
| 34 | + private final SunoApi sunoApi; | |
| 35 | + private final AiMusicMapper musicMapper; | |
| 36 | + | |
| 37 | + private final Queue<String> taskQueue = new ConcurrentLinkedQueue<>(); | |
| 38 | + | |
| 18 | 39 | |
| 19 | 40 | @Override |
| 20 | - public SunoRespVO musicGen(SunoReqVO sunoReqVO) { | |
| 21 | - AceDataSunoApi.SunoResp sunoResp = aceDataSunoApi.musicGen(new AceDataSunoApi.SunoReq( | |
| 22 | - sunoReqVO.getPrompt(), | |
| 23 | - sunoReqVO.getLyric(), | |
| 24 | - sunoReqVO.isCustom(), | |
| 25 | - sunoReqVO.getTitle(), | |
| 26 | - sunoReqVO.getStyle(), | |
| 27 | - sunoReqVO.getCallbackUrl() | |
| 28 | - )); | |
| 29 | - return SunoRespVO.convertFrom(sunoResp); | |
| 41 | + public List<Long> descriptionMode(SunoReqVO reqVO) { | |
| 42 | + SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental()); | |
| 43 | + //默认异步 | |
| 44 | + List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq); | |
| 45 | + return insertMusicData(musicDataList); | |
| 46 | + } | |
| 47 | + | |
| 48 | + | |
| 49 | + @Override | |
| 50 | + public List<Long> lyricMode(SunoLyricModeVO reqVO) { | |
| 51 | + SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle()); | |
| 52 | + //默认异步 | |
| 53 | + List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq); | |
| 54 | + return insertMusicData(musicDataList); | |
| 55 | + } | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * 新增音乐数据并提交 suno任务 | |
| 59 | + * | |
| 60 | + * @param musicDataList 音乐数据列表 | |
| 61 | + * @return 音乐id集合 | |
| 62 | + */ | |
| 63 | + private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList) { | |
| 64 | + if (CollUtil.isEmpty(musicDataList)) { | |
| 65 | + return Collections.emptyList(); | |
| 66 | + } | |
| 67 | + return AiMusicDO.convertFrom(musicDataList).stream() | |
| 68 | + .peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId()))) | |
| 69 | + .peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add)) | |
| 70 | + .map(AiMusicDO::getId) | |
| 71 | + .collect(Collectors.toList()); | |
| 72 | + } | |
| 73 | + | |
| 74 | + @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) | |
| 75 | + @Transactional | |
| 76 | + public void flushSunoTask() { | |
| 77 | + if (CollUtil.isEmpty(taskQueue)) { | |
| 78 | + return; | |
| 79 | + } | |
| 80 | + CollUtil.split(taskQueue, 5). | |
| 81 | + stream().map(chunk -> CollUtil.join(chunk, StrPool.COMMA)) | |
| 82 | + .forEach(taskIds -> { | |
| 83 | + List<SunoApi.MusicData> musicData = sunoApi.selectById(taskIds); | |
| 84 | + musicData.stream() | |
| 85 | + .map(AiMusicDO::convertFrom) | |
| 86 | + .forEach(musicDO -> { | |
| 87 | + //更新音乐生成结果 | |
| 88 | + musicMapper.update(musicDO, Wrappers.<AiMusicDO>lambdaUpdate().eq(AiMusicDO::getTaskId, musicDO.getTaskId())); | |
| 89 | + //完成后剔除任务 | |
| 90 | + if (Objects.equals(AiMusicStatusEnum.COMPLETE.getStatus(), musicDO.getStatus())) { | |
| 91 | + taskQueue.remove(musicDO.getTaskId()); | |
| 92 | + } | |
| 93 | + }); | |
| 94 | + }); | |
| 95 | + | |
| 30 | 96 | } |
| 31 | 97 | } | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
| ... | ... | @@ -4,7 +4,7 @@ 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 | 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; |
| 7 | -import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; | |
| 7 | +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | |
| 8 | 8 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; |
| 9 | 9 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; |
| 10 | 10 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions; |
| ... | ... | @@ -150,8 +150,8 @@ public class YudaoAiAutoConfiguration { |
| 150 | 150 | |
| 151 | 151 | @Bean |
| 152 | 152 | @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") |
| 153 | - public AceDataSunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { | |
| 154 | - return new AceDataSunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken())); | |
| 153 | + public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { | |
| 154 | + return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getBaseUrl())); | |
| 155 | 155 | } |
| 156 | 156 | |
| 157 | 157 | private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext, | ... | ... |
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
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/AceDataSunoApi.java deleted
| 1 | -package cn.iocoder.yudao.framework.ai.core.model.suno.api; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; | |
| 4 | -import com.fasterxml.jackson.annotation.JsonInclude; | |
| 5 | -import com.fasterxml.jackson.annotation.JsonProperty; | |
| 6 | -import lombok.extern.slf4j.Slf4j; | |
| 7 | -import org.springframework.ai.openai.api.ApiUtils; | |
| 8 | -import org.springframework.web.reactive.function.client.WebClient; | |
| 9 | -import reactor.core.publisher.Mono; | |
| 10 | - | |
| 11 | -import java.util.List; | |
| 12 | - | |
| 13 | -/** | |
| 14 | - * Suno API | |
| 15 | - * <br> | |
| 16 | - * 文档地址:https://platform.acedata.cloud/documents/d016ee3f-421b-4b6e-989a-8beba8701701 | |
| 17 | - * | |
| 18 | - * @Author xiaoxin | |
| 19 | - * @Date 2024/5/27 | |
| 20 | - */ | |
| 21 | -@Slf4j | |
| 22 | -public class AceDataSunoApi { | |
| 23 | - | |
| 24 | - public static final String DEFAULT_BASE_URL = "https://api.acedata.cloud/suno"; | |
| 25 | - private final WebClient webClient; | |
| 26 | - | |
| 27 | - public AceDataSunoApi(SunoConfig config) { | |
| 28 | - this.webClient = WebClient.builder() | |
| 29 | - .baseUrl(DEFAULT_BASE_URL) | |
| 30 | - .defaultHeaders(ApiUtils.getJsonContentHeaders(config.getToken())) | |
| 31 | - .build(); | |
| 32 | - } | |
| 33 | - | |
| 34 | - // TODO @芋艿:方法名,要考虑下; | |
| 35 | - public SunoResp musicGen(SunoReq sunReq) { | |
| 36 | - return this.webClient.post() | |
| 37 | - .uri("/audios") | |
| 38 | - .body(Mono.just(sunReq), SunoReq.class) | |
| 39 | - .retrieve() | |
| 40 | - .onStatus(status -> !status.is2xxSuccessful(), | |
| 41 | - response -> response.bodyToMono(String.class) | |
| 42 | - .handle((respBody, sink) -> { | |
| 43 | - log.error("【Suno】调用失败!resp: 【{}】", respBody); | |
| 44 | - sink.error(new IllegalStateException("【Suno】调用失败!")); | |
| 45 | - })) | |
| 46 | - .bodyToMono(SunoResp.class) | |
| 47 | - .block(); | |
| 48 | - } | |
| 49 | - | |
| 50 | - /** | |
| 51 | - * 请求数据对象,用于生成音乐音频。 | |
| 52 | - * | |
| 53 | - * @param prompt 用于生成音乐音频的提示 | |
| 54 | - * @param lyric 用于生成音乐音频的歌词 | |
| 55 | - * @param custom 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 | |
| 56 | - * @param title 音乐音频的标题 | |
| 57 | - * @param style 音乐音频的风格 | |
| 58 | - * @param callbackUrl 音乐音频生成后回调的 URL | |
| 59 | - */ | |
| 60 | - @JsonInclude(value = JsonInclude.Include.NON_NULL) | |
| 61 | - public record SunoReq( | |
| 62 | - String prompt, | |
| 63 | - String lyric, | |
| 64 | - boolean custom, | |
| 65 | - String title, | |
| 66 | - String style, | |
| 67 | - String callbackUrl | |
| 68 | - ) { | |
| 69 | - public SunoReq(String prompt) { | |
| 70 | - this(prompt, null, false, null, null, null); | |
| 71 | - } | |
| 72 | - | |
| 73 | - } | |
| 74 | - | |
| 75 | - /** | |
| 76 | - * SunoAPI 响应的数据。 | |
| 77 | - * | |
| 78 | - * @param success 表示请求是否成功 | |
| 79 | - * @param taskId 任务 ID | |
| 80 | - * @param data 音乐数据列表 | |
| 81 | - */ | |
| 82 | - public record SunoResp( | |
| 83 | - boolean success, | |
| 84 | - @JsonProperty("task_id") String taskId, | |
| 85 | - List<MusicData> data | |
| 86 | - ) { | |
| 87 | - /** | |
| 88 | - * 单个音乐数据。 | |
| 89 | - * | |
| 90 | - * @param id 音乐数据的 ID | |
| 91 | - * @param title 音乐音频的标题 | |
| 92 | - * @param imageUrl 音乐音频的图片 URL | |
| 93 | - * @param lyric 音乐音频的歌词 | |
| 94 | - * @param audioUrl 音乐音频的 URL | |
| 95 | - * @param videoUrl 音乐视频的 URL | |
| 96 | - * @param createdAt 音乐音频的创建时间 | |
| 97 | - * @param model 使用的模型名称 | |
| 98 | - * @param prompt 生成音乐音频的提示 | |
| 99 | - * @param style 音乐音频的风格 | |
| 100 | - */ | |
| 101 | - public record MusicData( | |
| 102 | - String id, | |
| 103 | - String title, | |
| 104 | - @JsonProperty("image_url") String imageUrl, | |
| 105 | - String lyric, | |
| 106 | - @JsonProperty("audio_url") String audioUrl, | |
| 107 | - @JsonProperty("video_url") String videoUrl, | |
| 108 | - @JsonProperty("created_at") String createdAt, | |
| 109 | - String model, | |
| 110 | - String prompt, | |
| 111 | - String style | |
| 112 | - ) { | |
| 113 | - } | |
| 114 | - } | |
| 115 | -} |
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 | 4 | import com.fasterxml.jackson.annotation.JsonInclude; |
| 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; |
| 5 | 6 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -25,10 +26,7 @@ import java.util.function.Predicate; |
| 25 | 26 | @Slf4j |
| 26 | 27 | public class SunoApi { |
| 27 | 28 | |
| 28 | - public static final String DEFAULT_BASE_URL = "https://suno-9323szg26-status2xxs-projects.vercel.app"; | |
| 29 | 29 | private final WebClient webClient; |
| 30 | - | |
| 31 | - | |
| 32 | 30 | private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful(); |
| 33 | 31 | private final Function<ClientResponse, Mono<? extends Throwable>> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class) |
| 34 | 32 | .handle((respBody, sink) -> { |
| ... | ... | @@ -37,9 +35,9 @@ public class SunoApi { |
| 37 | 35 | }); |
| 38 | 36 | |
| 39 | 37 | |
| 40 | - public SunoApi() { | |
| 38 | + public SunoApi(SunoConfig config) { | |
| 41 | 39 | this.webClient = WebClient.builder() |
| 42 | - .baseUrl(DEFAULT_BASE_URL) | |
| 40 | + .baseUrl(config.getBaseUrl()) | |
| 43 | 41 | .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) |
| 44 | 42 | .build(); |
| 45 | 43 | } |
| ... | ... | @@ -141,8 +139,8 @@ public class SunoApi { |
| 141 | 139 | } |
| 142 | 140 | |
| 143 | 141 | |
| 144 | - public SunoReq(String prompt, String tags, String title) { | |
| 145 | - this(prompt, tags, title, null, false, false); | |
| 142 | + public SunoReq(String prompt, String mv, String tags, String title) { | |
| 143 | + this(prompt, tags, title, mv, false, false); | |
| 146 | 144 | } |
| 147 | 145 | } |
| 148 | 146 | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java
| ... | ... | @@ -13,24 +13,22 @@ import java.util.List; |
| 13 | 13 | */ |
| 14 | 14 | public class SunoTests { |
| 15 | 15 | |
| 16 | - private SunoConfig sunoConfig; | |
| 16 | + SunoApi sunoApi; | |
| 17 | 17 | |
| 18 | 18 | @Before |
| 19 | 19 | public void setup() { |
| 20 | - String token = "16b4356581984d538652354b60d69ff0"; | |
| 21 | - this.sunoConfig = new SunoConfig(token); | |
| 20 | + String url = "https://suno-ix9nve79x-status2xxs-projects.vercel.app"; | |
| 21 | + this.sunoApi = new SunoApi(new SunoConfig(url)); | |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | 24 | @Test |
| 25 | 25 | public void selectById() { |
| 26 | - SunoApi sunoApi = new SunoApi(); | |
| 27 | 26 | System.out.println(sunoApi.selectById("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551")); |
| 28 | 27 | |
| 29 | 28 | } |
| 30 | 29 | |
| 31 | 30 | @Test |
| 32 | 31 | public void generate() { |
| 33 | - SunoApi sunoApi = new SunoApi(); | |
| 34 | 32 | List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); |
| 35 | 33 | System.out.println(generate); |
| 36 | 34 | } |
| ... | ... | @@ -38,7 +36,6 @@ public class SunoTests { |
| 38 | 36 | |
| 39 | 37 | @Test |
| 40 | 38 | public void doChatCompletion() { |
| 41 | - SunoApi sunoApi = new SunoApi(); | |
| 42 | 39 | List<SunoApi.MusicData> generate = sunoApi.doChatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); |
| 43 | 40 | System.out.println(generate); |
| 44 | 41 | } |
| ... | ... | @@ -46,16 +43,13 @@ public class SunoTests { |
| 46 | 43 | |
| 47 | 44 | @Test |
| 48 | 45 | public void generateLyrics() { |
| 49 | - SunoApi sunoApi = new SunoApi(); | |
| 50 | 46 | SunoApi.LyricsData lyricsData = sunoApi.generateLyrics("A soothing lullaby"); |
| 51 | 47 | System.out.println(lyricsData); |
| 52 | 48 | } |
| 53 | 49 | |
| 54 | 50 | |
| 55 | - | |
| 56 | 51 | @Test |
| 57 | 52 | public void selectLimit() { |
| 58 | - SunoApi sunoApi = new SunoApi(); | |
| 59 | 53 | SunoApi.LimitData limitData = sunoApi.selectLimit(); |
| 60 | 54 | System.out.println(limitData); |
| 61 | 55 | } | ... | ... |
yudao-server/src/main/resources/application.yaml
| ... | ... | @@ -201,7 +201,7 @@ yudao.ai: |
| 201 | 201 | channel-id: 1237948819677904960 |
| 202 | 202 | suno: |
| 203 | 203 | enable: true |
| 204 | - token: 16b4356581984d538652354b60d69ff0 | |
| 204 | + base-url: https://suno-ix9nve79x-status2xxs-projects.vercel.app | |
| 205 | 205 | |
| 206 | 206 | --- #################### 芋道相关配置 #################### |
| 207 | 207 | ... | ... |