Commit 851c290c0d3240822272757cc0ef940fb2ebfb47

Authored by xiaoxin
1 parent c0de6cc5

【增加】AI:使用suno-api服务接入Suno

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
2 2  
3   -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
  3 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
4 4 import com.fasterxml.jackson.annotation.JsonProperty;
5 5 import lombok.Data;
6 6  
... ... @@ -66,7 +66,7 @@ public class MusicDataVO {
66 66 */
67 67 private String style;
68 68  
69   - public static List<MusicDataVO> convertFrom(List<SunoApi.SunoResp.MusicData> musicDataList) {
  69 + public static List<MusicDataVO> convertFrom(List<AceDataSunoApi.SunoResp.MusicData> musicDataList) {
70 70 return musicDataList.stream().map(musicData -> {
71 71 MusicDataVO musicDataVO = new MusicDataVO();
72 72 musicDataVO.setId(musicData.id());
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
2 2  
3   -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
  3 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
4 4 import com.fasterxml.jackson.annotation.JsonProperty;
5 5 import lombok.Data;
6 6  
... ... @@ -29,7 +29,7 @@ public class SunoRespVO {
29 29  
30 30  
31 31 //把 SunoResp转为本vo类
32   - public static SunoRespVO convertFrom(SunoApi.SunoResp sunoResp) {
  32 + public static SunoRespVO convertFrom(AceDataSunoApi.SunoResp sunoResp) {
33 33 SunoRespVO sunoRespVO = new SunoRespVO();
34 34 sunoRespVO.setSuccess(sunoResp.success());
35 35 sunoRespVO.setTaskId(sunoResp.taskId());
... ...
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.SunoApi;
  3 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
4 4 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
5 5 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
6 6 import lombok.RequiredArgsConstructor;
... ... @@ -14,11 +14,11 @@ import org.springframework.stereotype.Service;
14 14 @RequiredArgsConstructor
15 15 public class MusicServiceImpl implements MusicService {
16 16  
17   - private final SunoApi sunoApi;
  17 + private final AceDataSunoApi aceDataSunoApi;
18 18  
19 19 @Override
20 20 public SunoRespVO musicGen(SunoReqVO sunoReqVO) {
21   - SunoApi.SunoResp sunoResp = sunoApi.musicGen(new SunoApi.SunoReq(
  21 + AceDataSunoApi.SunoResp sunoResp = aceDataSunoApi.musicGen(new AceDataSunoApi.SunoReq(
22 22 sunoReqVO.getPrompt(),
23 23 sunoReqVO.getLyric(),
24 24 sunoReqVO.isCustom(),
... ...
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.SunoApi;
  7 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
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 SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
154   - return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken()));
  153 + public AceDataSunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
  154 + return new AceDataSunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken()));
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/core/model/suno/api/AceDataSunoApi.java 0 → 100644
  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;
4 3 import com.fasterxml.jackson.annotation.JsonInclude;
5 4 import com.fasterxml.jackson.annotation.JsonProperty;
6 5 import lombok.extern.slf4j.Slf4j;
7   -import org.springframework.ai.openai.api.ApiUtils;
  6 +import org.springframework.core.ParameterizedTypeReference;
  7 +import org.springframework.http.HttpStatusCode;
  8 +import org.springframework.http.MediaType;
  9 +import org.springframework.web.reactive.function.client.ClientResponse;
8 10 import org.springframework.web.reactive.function.client.WebClient;
9 11 import reactor.core.publisher.Mono;
10 12  
11 13 import java.util.List;
  14 +import java.util.function.Function;
  15 +import java.util.function.Predicate;
12 16  
13 17 /**
14 18 * Suno API
15 19 * <br>
16   - * 文档地址:https://platform.acedata.cloud/documents/d016ee3f-421b-4b6e-989a-8beba8701701
  20 + * 文档地址:https://github.com/status2xx/suno-api/blob/main/README_CN.md
17 21 *
18 22 * @Author xiaoxin
19   - * @Date 2024/5/27
  23 + * @Date 2024/6/3
20 24 */
21 25 @Slf4j
22 26 public class SunoApi {
23 27  
24   - public static final String DEFAULT_BASE_URL = "https://api.acedata.cloud/suno";
  28 + public static final String DEFAULT_BASE_URL = "https://suno-9323szg26-status2xxs-projects.vercel.app";
25 29 private final WebClient webClient;
26 30  
27   - public SunoApi(SunoConfig config) {
  31 +
  32 + private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
  33 + private final Function<ClientResponse, Mono<? extends Throwable>> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class)
  34 + .handle((respBody, sink) -> {
  35 + log.error("【suno-api】调用失败!resp: 【{}】", respBody);
  36 + sink.error(new IllegalStateException("【suno-api】调用失败!"));
  37 + });
  38 +
  39 +
  40 + public SunoApi() {
28 41 this.webClient = WebClient.builder()
29 42 .baseUrl(DEFAULT_BASE_URL)
30   - .defaultHeaders(ApiUtils.getJsonContentHeaders(config.getToken()))
  43 + .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON))
31 44 .build();
32 45 }
33 46  
34   - // TODO @芋艿:方法名,要考虑下;
35   - public SunoResp musicGen(SunoReq sunReq) {
  47 + public List<MusicData> generate(SunoApi.SunoReq sunReq) {
  48 + return this.webClient.post()
  49 + .uri("/api/generate")
  50 + .body(Mono.just(sunReq), SunoApi.SunoReq.class)
  51 + .retrieve()
  52 + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
  53 + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
  54 + })
  55 + .block();
  56 + }
  57 +
  58 + public List<MusicData> doChatCompletion(String prompt) {
  59 + return this.webClient.post()
  60 + .uri("/v1/chat/completions")
  61 + .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class)
  62 + .retrieve()
  63 + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
  64 + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
  65 + })
  66 + .block();
  67 + }
  68 +
  69 + public LyricsData generateLyrics(String prompt) {
36 70 return this.webClient.post()
37   - .uri("/audios")
38   - .body(Mono.just(sunReq), SunoReq.class)
  71 + .uri("/api/generate_lyrics")
  72 + .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class)
  73 + .retrieve()
  74 + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
  75 + .bodyToMono(LyricsData.class)
  76 + .block();
  77 + }
  78 +
  79 +
  80 + public List<MusicData> selectById(String ids) {
  81 + return this.webClient.get()
  82 + .uri(uriBuilder -> uriBuilder
  83 + .path("/api/get")
  84 + .queryParam("ids", ids)
  85 + .build())
39 86 .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)
  87 + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
  88 + .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
  89 + })
47 90 .block();
48 91 }
49 92  
  93 +
  94 + public LimitData selectLimit() {
  95 + return this.webClient.get()
  96 + .uri("/api/get_limit")
  97 + .retrieve()
  98 + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
  99 + .bodyToMono(LimitData.class)
  100 + .block();
  101 + }
  102 +
  103 +
50 104 /**
51   - * 请求数据对象,用于生成音乐音频。
  105 + * 根据提示生成音频
52 106 *
53   - * @param prompt 用于生成音乐音频的提示
54   - * @param lyric 用于生成音乐音频的歌词
55   - * @param custom 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
56   - * @param title 音乐音频的标题
57   - * @param style 音乐音频的风格
58   - * @param callbackUrl 音乐音频生成后回调的 URL
  107 + * @param prompt 用于生成音乐音频的提示
  108 + * @param tags 自定义模式才需要
  109 + * @param title 自定义模式才需要
  110 + * @param waitAudio false表示后台模式,仅返回音频任务信息,需要调用get API获取详细的音频信息。
  111 + * true表示同步模式,API最多等待100s,音频生成完毕后直接返回音频链接等信息,建议在GPT等agent中使用。
  112 + * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
59 113 */
60 114 @JsonInclude(value = JsonInclude.Include.NON_NULL)
61 115 public record SunoReq(
62 116 String prompt,
63   - String lyric,
64   - boolean custom,
  117 + String tags,
65 118 String title,
66   - String style,
67   - String callbackUrl
  119 + @JsonProperty("wait_audio") boolean waitAudio,
  120 + @JsonProperty("make_instrumental") boolean makeInstrumental
68 121 ) {
69 122 public SunoReq(String prompt) {
70   - this(prompt, null, false, null, null, null);
  123 + this(prompt, null, null, false, false);
71 124 }
72 125  
  126 + public SunoReq(String prompt, String tags, String title) {
  127 + this(prompt, tags, title, false, false);
  128 + }
73 129 }
74 130  
  131 +
75 132 /**
76   - * SunoAPI 响应的数据。
  133 + * SunoAPI 响应的音频数据。
77 134 *
78   - * @param success 表示请求是否成功
79   - * @param taskId 任务 ID
80   - * @param data 音乐数据列表
  135 + * @param id 音乐数据的 ID
  136 + * @param title 音乐音频的标题
  137 + * @param imageUrl 音乐音频的图片 URL
  138 + * @param lyric 音乐音频的歌词
  139 + * @param audioUrl 音乐音频的 URL
  140 + * @param videoUrl 音乐视频的 URL
  141 + * @param createdAt 音乐音频的创建时间
  142 + * @param modelName
  143 + * @param status
  144 + * @param gptDescriptionPrompt
  145 + * @param prompt 生成音乐音频的提示
  146 + * @param type
  147 + * @param tags
81 148 */
82   - public record SunoResp(
83   - boolean success,
84   - @JsonProperty("task_id") String taskId,
85   - List<MusicData> data
  149 + public record MusicData(
  150 + String id,
  151 + String title,
  152 + @JsonProperty("image_url") String imageUrl,
  153 + String lyric,
  154 + @JsonProperty("audio_url") String audioUrl,
  155 + @JsonProperty("video_url") String videoUrl,
  156 + @JsonProperty("created_at") String createdAt,
  157 + @JsonProperty("model_name") String modelName,
  158 + String status,
  159 + @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt,
  160 + String prompt,
  161 + String type,
  162 + String tags
86 163 ) {
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 164 }
  165 +
  166 +
  167 + /**
  168 + * SunoAPI 响应的歌词数据。
  169 + *
  170 + * @param text 歌词
  171 + * @param title 标题
  172 + * @param status 状态
  173 + */
  174 + public record LyricsData(
  175 + String text,
  176 + String title,
  177 + String status
  178 + ) {
  179 + }
  180 +
  181 +
  182 + /**
  183 + * SunoAPI 响应的限额数据,目前每日免费50
  184 + */
  185 + public record LimitData(
  186 + @JsonProperty("credits_left") Long creditsLeft,
  187 + String period,
  188 + @JsonProperty("monthly_limit") Long monthlyLimit,
  189 + @JsonProperty("monthly_usage") Long monthlyUsage
  190 + ) {
  191 + }
  192 +
  193 +
115 194 }
... ...
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;
5 5 import org.junit.Before;
6 6 import org.junit.Test;
7 7  
  8 +import java.util.List;
  9 +
8 10 /**
9 11 * @Author xiaoxin
10 12 * @Date 2024/5/27
... ... @@ -20,12 +22,43 @@ public class SunoTests {
20 22 }
21 23  
22 24 @Test
23   - public void generateMusic() {
24   - SunoApi sunoApi = new SunoApi(sunoConfig);
25   - SunoApi.SunoReq sunoReq = new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
  25 + public void selectById() {
  26 + SunoApi sunoApi = new SunoApi();
  27 + System.out.println(sunoApi.selectById("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551"));
  28 +
  29 + }
  30 +
  31 + @Test
  32 + public void generate() {
  33 + SunoApi sunoApi = new SunoApi();
  34 + List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"));
  35 + System.out.println(generate);
  36 + }
  37 +
  38 +
  39 + @Test
  40 + public void doChatCompletion() {
  41 + SunoApi sunoApi = new SunoApi();
  42 + List<SunoApi.MusicData> generate = sunoApi.doChatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
  43 + System.out.println(generate);
  44 + }
  45 +
  46 +
  47 + @Test
  48 + public void generateLyrics() {
  49 + SunoApi sunoApi = new SunoApi();
  50 + SunoApi.LyricsData lyricsData = sunoApi.generateLyrics("A soothing lullaby");
  51 + System.out.println(lyricsData);
  52 + }
26 53  
27   - SunoApi.SunoResp sunoResp = sunoApi.musicGen(sunoReq);
28   - System.out.println(sunoResp);
  54 +
  55 +
  56 + @Test
  57 + public void selectLimit() {
  58 + SunoApi sunoApi = new SunoApi();
  59 + SunoApi.LimitData limitData = sunoApi.selectLimit();
  60 + System.out.println(limitData);
29 61 }
30 62  
  63 +
31 64 }
... ...