From 851c290c0d3240822272757cc0ef940fb2ebfb47 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Mon, 3 Jun 2024 18:20:16 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=A2=9E=E5=8A=A0=E3=80=91AI=EF=BC=9A?= =?UTF-8?q?=E4=BD=BF=E7=94=A8suno-api=E6=9C=8D=E5=8A=A1=E6=8E=A5=E5=85=A5S?= =?UTF-8?q?uno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/music/vo/MusicDataVO.java | 4 +- .../controller/admin/music/vo/SunoRespVO.java | 4 +- .../ai/service/music/MusicServiceImpl.java | 6 +- .../ai/config/YudaoAiAutoConfiguration.java | 6 +- .../core/model/suno/api/AceDataSunoApi.java | 115 ++++++++++ .../ai/core/model/suno/api/SunoApi.java | 209 ++++++++++++------ .../yudao/framework/ai/suno/SunoTests.java | 43 +++- 7 files changed, 307 insertions(+), 80 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/AceDataSunoApi.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java index d4c4afa22..f138d605e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @@ -66,7 +66,7 @@ public class MusicDataVO { */ private String style; - public static List convertFrom(List musicDataList) { + public static List convertFrom(List musicDataList) { return musicDataList.stream().map(musicData -> { MusicDataVO musicDataVO = new MusicDataVO(); musicDataVO.setId(musicData.id()); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java index b3d66363f..5eed0b81d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @@ -29,7 +29,7 @@ public class SunoRespVO { //把 SunoResp转为本vo类 - public static SunoRespVO convertFrom(SunoApi.SunoResp sunoResp) { + public static SunoRespVO convertFrom(AceDataSunoApi.SunoResp sunoResp) { SunoRespVO sunoRespVO = new SunoRespVO(); sunoRespVO.setSuccess(sunoResp.success()); sunoRespVO.setTaskId(sunoResp.taskId()); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java index 0673e59ac..f8b2a9d50 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.service.music; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO; import lombok.RequiredArgsConstructor; @@ -14,11 +14,11 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class MusicServiceImpl implements MusicService { - private final SunoApi sunoApi; + private final AceDataSunoApi aceDataSunoApi; @Override public SunoRespVO musicGen(SunoReqVO sunoReqVO) { - SunoApi.SunoResp sunoResp = sunoApi.musicGen(new SunoApi.SunoReq( + AceDataSunoApi.SunoResp sunoResp = aceDataSunoApi.musicGen(new AceDataSunoApi.SunoReq( sunoReqVO.getPrompt(), sunoReqVO.getLyric(), sunoReqVO.isCustom(), diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 7285448a3..cd7512bf3 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/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; import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi; import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions; @@ -150,8 +150,8 @@ public class YudaoAiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") - public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { - return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken())); + public AceDataSunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { + return new AceDataSunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken())); } private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext, diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/AceDataSunoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/AceDataSunoApi.java new file mode 100644 index 000000000..d50379d59 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/AceDataSunoApi.java @@ -0,0 +1,115 @@ +package cn.iocoder.yudao.framework.ai.core.model.suno.api; + +import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Suno API + *
+ * 文档地址:https://platform.acedata.cloud/documents/d016ee3f-421b-4b6e-989a-8beba8701701 + * + * @Author xiaoxin + * @Date 2024/5/27 + */ +@Slf4j +public class AceDataSunoApi { + + public static final String DEFAULT_BASE_URL = "https://api.acedata.cloud/suno"; + private final WebClient webClient; + + public AceDataSunoApi(SunoConfig config) { + this.webClient = WebClient.builder() + .baseUrl(DEFAULT_BASE_URL) + .defaultHeaders(ApiUtils.getJsonContentHeaders(config.getToken())) + .build(); + } + + // TODO @芋艿:方法名,要考虑下; + public SunoResp musicGen(SunoReq sunReq) { + return this.webClient.post() + .uri("/audios") + .body(Mono.just(sunReq), SunoReq.class) + .retrieve() + .onStatus(status -> !status.is2xxSuccessful(), + response -> response.bodyToMono(String.class) + .handle((respBody, sink) -> { + log.error("【Suno】调用失败!resp: 【{}】", respBody); + sink.error(new IllegalStateException("【Suno】调用失败!")); + })) + .bodyToMono(SunoResp.class) + .block(); + } + + /** + * 请求数据对象,用于生成音乐音频。 + * + * @param prompt 用于生成音乐音频的提示 + * @param lyric 用于生成音乐音频的歌词 + * @param custom 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 + * @param title 音乐音频的标题 + * @param style 音乐音频的风格 + * @param callbackUrl 音乐音频生成后回调的 URL + */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public record SunoReq( + String prompt, + String lyric, + boolean custom, + String title, + String style, + String callbackUrl + ) { + public SunoReq(String prompt) { + this(prompt, null, false, null, null, null); + } + + } + + /** + * SunoAPI 响应的数据。 + * + * @param success 表示请求是否成功 + * @param taskId 任务 ID + * @param data 音乐数据列表 + */ + public record SunoResp( + boolean success, + @JsonProperty("task_id") String taskId, + List data + ) { + /** + * 单个音乐数据。 + * + * @param id 音乐数据的 ID + * @param title 音乐音频的标题 + * @param imageUrl 音乐音频的图片 URL + * @param lyric 音乐音频的歌词 + * @param audioUrl 音乐音频的 URL + * @param videoUrl 音乐视频的 URL + * @param createdAt 音乐音频的创建时间 + * @param model 使用的模型名称 + * @param prompt 生成音乐音频的提示 + * @param style 音乐音频的风格 + */ + public record MusicData( + String id, + String title, + @JsonProperty("image_url") String imageUrl, + String lyric, + @JsonProperty("audio_url") String audioUrl, + @JsonProperty("video_url") String videoUrl, + @JsonProperty("created_at") String createdAt, + String model, + String prompt, + String style + ) { + } + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java index 22435affd..25ae04f51 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java @@ -1,115 +1,194 @@ package cn.iocoder.yudao.framework.ai.core.model.suno.api; -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; /** * Suno API *
- * 文档地址:https://platform.acedata.cloud/documents/d016ee3f-421b-4b6e-989a-8beba8701701 + * 文档地址:https://github.com/status2xx/suno-api/blob/main/README_CN.md * * @Author xiaoxin - * @Date 2024/5/27 + * @Date 2024/6/3 */ @Slf4j public class SunoApi { - public static final String DEFAULT_BASE_URL = "https://api.acedata.cloud/suno"; + public static final String DEFAULT_BASE_URL = "https://suno-9323szg26-status2xxs-projects.vercel.app"; private final WebClient webClient; - public SunoApi(SunoConfig config) { + + private final Predicate STATUS_PREDICATE = status -> !status.is2xxSuccessful(); + private final Function> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class) + .handle((respBody, sink) -> { + log.error("【suno-api】调用失败!resp: 【{}】", respBody); + sink.error(new IllegalStateException("【suno-api】调用失败!")); + }); + + + public SunoApi() { this.webClient = WebClient.builder() .baseUrl(DEFAULT_BASE_URL) - .defaultHeaders(ApiUtils.getJsonContentHeaders(config.getToken())) + .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) .build(); } - // TODO @芋艿:方法名,要考虑下; - public SunoResp musicGen(SunoReq sunReq) { + public List generate(SunoApi.SunoReq sunReq) { return this.webClient.post() - .uri("/audios") - .body(Mono.just(sunReq), SunoReq.class) + .uri("/api/generate") + .body(Mono.just(sunReq), SunoApi.SunoReq.class) .retrieve() - .onStatus(status -> !status.is2xxSuccessful(), - response -> response.bodyToMono(String.class) - .handle((respBody, sink) -> { - log.error("【Suno】调用失败!resp: 【{}】", respBody); - sink.error(new IllegalStateException("【Suno】调用失败!")); - })) - .bodyToMono(SunoResp.class) + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) + .bodyToMono(new ParameterizedTypeReference>() { + }) .block(); } + public List doChatCompletion(String prompt) { + return this.webClient.post() + .uri("/v1/chat/completions") + .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class) + .retrieve() + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(); + } + + public LyricsData generateLyrics(String prompt) { + return this.webClient.post() + .uri("/api/generate_lyrics") + .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class) + .retrieve() + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) + .bodyToMono(LyricsData.class) + .block(); + } + + + public List selectById(String ids) { + return this.webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/api/get") + .queryParam("ids", ids) + .build()) + .retrieve() + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(); + } + + + public LimitData selectLimit() { + return this.webClient.get() + .uri("/api/get_limit") + .retrieve() + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION) + .bodyToMono(LimitData.class) + .block(); + } + + /** - * 请求数据对象,用于生成音乐音频。 + * 根据提示生成音频 * - * @param prompt 用于生成音乐音频的提示 - * @param lyric 用于生成音乐音频的歌词 - * @param custom 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 - * @param title 音乐音频的标题 - * @param style 音乐音频的风格 - * @param callbackUrl 音乐音频生成后回调的 URL + * @param prompt 用于生成音乐音频的提示 + * @param tags 自定义模式才需要 + * @param title 自定义模式才需要 + * @param waitAudio false表示后台模式,仅返回音频任务信息,需要调用get API获取详细的音频信息。 + * true表示同步模式,API最多等待100s,音频生成完毕后直接返回音频链接等信息,建议在GPT等agent中使用。 + * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 */ @JsonInclude(value = JsonInclude.Include.NON_NULL) public record SunoReq( String prompt, - String lyric, - boolean custom, + String tags, String title, - String style, - String callbackUrl + @JsonProperty("wait_audio") boolean waitAudio, + @JsonProperty("make_instrumental") boolean makeInstrumental ) { public SunoReq(String prompt) { - this(prompt, null, false, null, null, null); + this(prompt, null, null, false, false); } + public SunoReq(String prompt, String tags, String title) { + this(prompt, tags, title, false, false); + } } + /** - * SunoAPI 响应的数据。 + * SunoAPI 响应的音频数据。 * - * @param success 表示请求是否成功 - * @param taskId 任务 ID - * @param data 音乐数据列表 + * @param id 音乐数据的 ID + * @param title 音乐音频的标题 + * @param imageUrl 音乐音频的图片 URL + * @param lyric 音乐音频的歌词 + * @param audioUrl 音乐音频的 URL + * @param videoUrl 音乐视频的 URL + * @param createdAt 音乐音频的创建时间 + * @param modelName + * @param status + * @param gptDescriptionPrompt + * @param prompt 生成音乐音频的提示 + * @param type + * @param tags */ - public record SunoResp( - boolean success, - @JsonProperty("task_id") String taskId, - List data + public record MusicData( + String id, + String title, + @JsonProperty("image_url") String imageUrl, + String lyric, + @JsonProperty("audio_url") String audioUrl, + @JsonProperty("video_url") String videoUrl, + @JsonProperty("created_at") String createdAt, + @JsonProperty("model_name") String modelName, + String status, + @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt, + String prompt, + String type, + String tags ) { - /** - * 单个音乐数据。 - * - * @param id 音乐数据的 ID - * @param title 音乐音频的标题 - * @param imageUrl 音乐音频的图片 URL - * @param lyric 音乐音频的歌词 - * @param audioUrl 音乐音频的 URL - * @param videoUrl 音乐视频的 URL - * @param createdAt 音乐音频的创建时间 - * @param model 使用的模型名称 - * @param prompt 生成音乐音频的提示 - * @param style 音乐音频的风格 - */ - public record MusicData( - String id, - String title, - @JsonProperty("image_url") String imageUrl, - String lyric, - @JsonProperty("audio_url") String audioUrl, - @JsonProperty("video_url") String videoUrl, - @JsonProperty("created_at") String createdAt, - String model, - String prompt, - String style - ) { - } } + + + /** + * SunoAPI 响应的歌词数据。 + * + * @param text 歌词 + * @param title 标题 + * @param status 状态 + */ + public record LyricsData( + String text, + String title, + String status + ) { + } + + + /** + * SunoAPI 响应的限额数据,目前每日免费50 + */ + public record LimitData( + @JsonProperty("credits_left") Long creditsLeft, + String period, + @JsonProperty("monthly_limit") Long monthlyLimit, + @JsonProperty("monthly_usage") Long monthlyUsage + ) { + } + + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java index 36fc40b17..39ac1ea9b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import org.junit.Before; import org.junit.Test; +import java.util.List; + /** * @Author xiaoxin * @Date 2024/5/27 @@ -20,12 +22,43 @@ public class SunoTests { } @Test - public void generateMusic() { - SunoApi sunoApi = new SunoApi(sunoConfig); - SunoApi.SunoReq sunoReq = new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); + public void selectById() { + SunoApi sunoApi = new SunoApi(); + System.out.println(sunoApi.selectById("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551")); - SunoApi.SunoResp sunoResp = sunoApi.musicGen(sunoReq); - System.out.println(sunoResp); } + @Test + public void generate() { + SunoApi sunoApi = new SunoApi(); + List generate = sunoApi.generate(new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")); + System.out.println(generate); + } + + + @Test + public void doChatCompletion() { + SunoApi sunoApi = new SunoApi(); + List generate = sunoApi.doChatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); + System.out.println(generate); + } + + + @Test + public void generateLyrics() { + SunoApi sunoApi = new SunoApi(); + SunoApi.LyricsData lyricsData = sunoApi.generateLyrics("A soothing lullaby"); + System.out.println(lyricsData); + } + + + + @Test + public void selectLimit() { + SunoApi sunoApi = new SunoApi(); + SunoApi.LimitData limitData = sunoApi.selectLimit(); + System.out.println(limitData); + } + + }