Commit b20129f7e485c28313b7e0cc34e0bd7953f5f0c2

Authored by 芋道源码
Committed by Gitee
2 parents c53633a2 c0de6cc5

!2 解决todo

Merge pull request !2 from 小新/master-jdk21-ai
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.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 cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
3 import com.fasterxml.jackson.annotation.JsonProperty; 4 import com.fasterxml.jackson.annotation.JsonProperty;
4 import lombok.Data; 5 import lombok.Data;
5 6
  7 +import java.util.List;
  8 +import java.util.stream.Collectors;
  9 +
6 /** 10 /**
7 * 表示单个音乐数据的类 11 * 表示单个音乐数据的类
8 */ 12 */
9 @Data 13 @Data
10 -public class MusicDataVO { 14 +public class MusicDataVO {
11 /** 15 /**
12 * 音乐数据的 ID 16 * 音乐数据的 ID
13 */ 17 */
@@ -61,4 +65,21 @@ public class MusicDataVO { @@ -61,4 +65,21 @@ public class MusicDataVO {
61 * 音乐音频的风格 65 * 音乐音频的风格
62 */ 66 */
63 private String style; 67 private String style;
  68 +
  69 + public static List<MusicDataVO> convertFrom(List<SunoApi.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 + }
64 } 85 }
65 \ No newline at end of file 86 \ 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
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 cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
3 import com.fasterxml.jackson.annotation.JsonProperty; 4 import com.fasterxml.jackson.annotation.JsonProperty;
4 import lombok.Data; 5 import lombok.Data;
5 6
@@ -26,4 +27,14 @@ public class SunoRespVO { @@ -26,4 +27,14 @@ public class SunoRespVO {
26 */ 27 */
27 private List<MusicDataVO> data; 28 private List<MusicDataVO> data;
28 29
  30 +
  31 + //把 SunoResp转为本vo类
  32 + public static SunoRespVO convertFrom(SunoApi.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 +
29 } 40 }
30 \ No newline at end of file 41 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java
1 package cn.iocoder.yudao.module.ai.service.music; 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.SunoApi;
4 -import cn.iocoder.yudao.framework.common.util.object.BeanUtils;  
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.controller.admin.music.vo.SunoRespVO; 5 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
7 import lombok.RequiredArgsConstructor; 6 import lombok.RequiredArgsConstructor;
@@ -19,7 +18,14 @@ public class MusicServiceImpl implements MusicService { @@ -19,7 +18,14 @@ public class MusicServiceImpl implements MusicService {
19 18
20 @Override 19 @Override
21 public SunoRespVO musicGen(SunoReqVO sunoReqVO) { 20 public SunoRespVO musicGen(SunoReqVO sunoReqVO) {
22 - SunoApi.SunoRequest req = BeanUtils.toBean(sunoReqVO, SunoApi.SunoRequest.class);  
23 - return BeanUtils.toBean(sunoApi.musicGen(req), SunoRespVO.class); 21 + SunoApi.SunoResp sunoResp = sunoApi.musicGen(new SunoApi.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);
24 } 30 }
25 } 31 }
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
@@ -137,7 +137,6 @@ public class YudaoAiProperties { @@ -137,7 +137,6 @@ public class YudaoAiProperties {
137 } 137 }
138 138
139 @Data 139 @Data
140 - @Accessors(chain = true) // TODO @xiaoxin:可以去掉这个,默认全局已经开启  
141 public static class SunoProperties { 140 public static class SunoProperties {
142 141
143 private boolean enable = false; 142 private boolean enable = false;
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java
@@ -3,14 +3,12 @@ package cn.iocoder.yudao.framework.ai.core.model.suno; @@ -3,14 +3,12 @@ package cn.iocoder.yudao.framework.ai.core.model.suno;
3 import lombok.AllArgsConstructor; 3 import lombok.AllArgsConstructor;
4 import lombok.Data; 4 import lombok.Data;
5 import lombok.NoArgsConstructor; 5 import lombok.NoArgsConstructor;
6 -import lombok.experimental.Accessors;  
7 6
8 /** 7 /**
9 * @Author xiaoxin 8 * @Author xiaoxin
10 * @Date 2024/5/29 9 * @Date 2024/5/29
11 */ 10 */
12 @Data 11 @Data
13 -@Accessors(chain = true)  
14 @NoArgsConstructor 12 @NoArgsConstructor
15 @AllArgsConstructor 13 @AllArgsConstructor
16 public class SunoConfig { 14 public class SunoConfig {
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.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
4 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;  
5 import com.fasterxml.jackson.annotation.JsonInclude; 4 import com.fasterxml.jackson.annotation.JsonInclude;
6 import com.fasterxml.jackson.annotation.JsonProperty; 5 import com.fasterxml.jackson.annotation.JsonProperty;
7 -import lombok.Data;  
8 -import lombok.experimental.Accessors;  
9 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
10 -import okhttp3.*; 7 +import org.springframework.ai.openai.api.ApiUtils;
  8 +import org.springframework.web.reactive.function.client.WebClient;
  9 +import reactor.core.publisher.Mono;
11 10
12 -import java.io.IOException;  
13 import java.util.List; 11 import java.util.List;
14 -import java.util.concurrent.TimeUnit;  
15 12
16 -// TODO @xiaoxin:类注释  
17 /** 13 /**
  14 + * Suno API
  15 + * <br>
  16 + * 文档地址:https://platform.acedata.cloud/documents/d016ee3f-421b-4b6e-989a-8beba8701701
  17 + *
18 * @Author xiaoxin 18 * @Author xiaoxin
19 * @Date 2024/5/27 19 * @Date 2024/5/27
20 */ 20 */
21 @Slf4j 21 @Slf4j
22 public class SunoApi { 22 public class SunoApi {
23 23
24 - // TODO @xiaoxin:APPLICATION_JSON、TOKEN_PREFIX 看看 spring 有没自带的这 2 个枚举哈。变量越少越好  
25 - public static final String APPLICATION_JSON = "application/json";  
26 - public static final String TOKEN_PREFIX = "Bearer ";  
27 - public static final String API_URL = "https://api.acedata.cloud/suno/audios"; 24 + public static final String DEFAULT_BASE_URL = "https://api.acedata.cloud/suno";
  25 + private final WebClient webClient;
28 26
29 - private static final int READ_TIMEOUT = 160; // 连接超时时间(秒),音乐生成时间较长,设置为 160s,后续可做callback  
30 -  
31 - // TODO @xiaoxin:建议使用 webClient 对接。参考 https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java  
32 - private final OkHttpClient client;  
33 -  
34 - // TODO @xiaoxin:sunoConfig => config,简洁一点  
35 - public SunoApi(SunoConfig sunoConfig) {  
36 - this.client = new OkHttpClient().newBuilder().readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)  
37 - .addInterceptor(chain -> {  
38 - Request originalRequest = chain.request();  
39 - Request requestWithUserAgent = originalRequest.newBuilder()  
40 - .header("Authorization", TOKEN_PREFIX + sunoConfig.getToken())  
41 - .build();  
42 - return chain.proceed(requestWithUserAgent);  
43 - }) 27 + public SunoApi(SunoConfig config) {
  28 + this.webClient = WebClient.builder()
  29 + .baseUrl(DEFAULT_BASE_URL)
  30 + .defaultHeaders(ApiUtils.getJsonContentHeaders(config.getToken()))
44 .build(); 31 .build();
45 } 32 }
46 33
47 // TODO @芋艿:方法名,要考虑下; 34 // TODO @芋艿:方法名,要考虑下;
48 - public SunoResponse musicGen(SunoRequest sunoRequest) {  
49 - Request request = new Request.Builder()  
50 - .url(API_URL)  
51 - .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), JsonUtils.toJsonString(sunoRequest)))  
52 - .build();  
53 -  
54 - try (Response response = client.newCall(request).execute()) {  
55 - if (!response.isSuccessful()) {  
56 - log.error("suno调用失败! response: {}", response);  
57 - throw new IllegalStateException("suno调用失败!" + response);  
58 - }  
59 - return JsonUtils.parseObject(response.body().string(), SunoResponse.class);  
60 - } catch (IOException ioException) {  
61 - throw new RuntimeException(ioException);  
62 - } 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();
63 } 48 }
64 49
65 - // TODO @xiaoxin:看看是不是使用 record 特性,简化下;  
66 -  
67 /** 50 /**
68 - * 请求数据对象,用于生成音乐音频 51 + * 请求数据对象,用于生成音乐音频。
  52 + *
  53 + * @param prompt 用于生成音乐音频的提示
  54 + * @param lyric 用于生成音乐音频的歌词
  55 + * @param custom 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
  56 + * @param title 音乐音频的标题
  57 + * @param style 音乐音频的风格
  58 + * @param callbackUrl 音乐音频生成后回调的 URL
69 */ 59 */
70 - @Data  
71 - @Accessors(chain = true)  
72 @JsonInclude(value = JsonInclude.Include.NON_NULL) 60 @JsonInclude(value = JsonInclude.Include.NON_NULL)
73 - public static class SunoRequest {  
74 - /**  
75 - * 用于生成音乐音频的提示  
76 - */  
77 - private String prompt;  
78 -  
79 - /**  
80 - * 用于生成音乐音频的歌词  
81 - */  
82 - private String lyric;  
83 -  
84 - /**  
85 - * 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成  
86 - */  
87 - private boolean custom;  
88 -  
89 - /**  
90 - * 音乐音频的标题  
91 - */  
92 - private String title;  
93 -  
94 - /**  
95 - * 音乐音频的风格  
96 - */  
97 - private String style; 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 + }
98 72
99 - /**  
100 - * 音乐音频生成后回调的 URL  
101 - */  
102 - private String callbackUrl;  
103 } 73 }
104 74
105 - // TODO @xiaoxin:看看是不是使用 record 特性,简化下;  
106 -  
107 /** 75 /**
108 - * SunoAPI 响应的数据 76 + * SunoAPI 响应的数据。
  77 + *
  78 + * @param success 表示请求是否成功
  79 + * @param taskId 任务 ID
  80 + * @param data 音乐数据列表
109 */ 81 */
110 - @Data  
111 - public static class SunoResponse {  
112 - /**  
113 - * 表示请求是否成功  
114 - */  
115 - private boolean success;  
116 -  
117 - /**  
118 - * 任务 ID  
119 - */  
120 - @JsonProperty("task_id")  
121 - private String taskId;  
122 -  
123 - /**  
124 - * 音乐数据列表  
125 - */  
126 - private List<MusicData> data;  
127 - 82 + public record SunoResp(
  83 + boolean success,
  84 + @JsonProperty("task_id") String taskId,
  85 + List<MusicData> data
  86 + ) {
128 /** 87 /**
129 - * 表示单个音乐数据的类 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 音乐音频的风格
130 */ 100 */
131 - @Data  
132 - static class MusicData {  
133 - /**  
134 - * 音乐数据的 ID  
135 - */  
136 - private String id;  
137 -  
138 - /**  
139 - * 音乐音频的标题  
140 - */  
141 - private String title;  
142 -  
143 - /**  
144 - * 音乐音频的图片 URL  
145 - */  
146 - @JsonProperty("image_url")  
147 - private String imageUrl;  
148 -  
149 - /**  
150 - * 音乐音频的歌词  
151 - */  
152 - private String lyric;  
153 -  
154 - /**  
155 - * 音乐音频的 URL  
156 - */  
157 - @JsonProperty("audio_url")  
158 - private String audioUrl;  
159 -  
160 - /**  
161 - * 音乐视频的 URL  
162 - */  
163 - @JsonProperty("video_url")  
164 - private String videoUrl;  
165 -  
166 - /**  
167 - * 音乐音频的创建时间  
168 - */  
169 - @JsonProperty("created_at")  
170 - private String createdAt;  
171 -  
172 - /**  
173 - * 使用的模型名称  
174 - */  
175 - private String model;  
176 -  
177 - /**  
178 - * 生成音乐音频的提示  
179 - */  
180 - private String prompt;  
181 -  
182 - /**  
183 - * 音乐音频的风格  
184 - */  
185 - private String style; 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 + ) {
186 } 113 }
187 } 114 }
188 -  
189 -  
190 } 115 }
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java
@@ -15,18 +15,17 @@ public class SunoTests { @@ -15,18 +15,17 @@ public class SunoTests {
15 15
16 @Before 16 @Before
17 public void setup() { 17 public void setup() {
18 - String token = "13f13540dd3f4ae9885f63ac9f5d0b9f"; 18 + String token = "16b4356581984d538652354b60d69ff0";
19 this.sunoConfig = new SunoConfig(token); 19 this.sunoConfig = new SunoConfig(token);
20 } 20 }
21 21
22 @Test 22 @Test
23 public void generateMusic() { 23 public void generateMusic() {
24 SunoApi sunoApi = new SunoApi(sunoConfig); 24 SunoApi sunoApi = new SunoApi(sunoConfig);
25 - SunoApi.SunoRequest sunoRequest = new SunoApi  
26 - .SunoRequest()  
27 - .setPrompt("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");  
28 - SunoApi.SunoResponse sunoResponse = sunoApi.musicGen(sunoRequest);  
29 - System.out.println(sunoResponse); 25 + SunoApi.SunoReq sunoReq = new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
  26 +
  27 + SunoApi.SunoResp sunoResp = sunoApi.musicGen(sunoReq);
  28 + System.out.println(sunoResp);
30 } 29 }
31 30
32 } 31 }
yudao-server/src/main/resources/application.yaml
@@ -201,7 +201,7 @@ yudao.ai: @@ -201,7 +201,7 @@ yudao.ai:
201 channel-id: 1237948819677904960 201 channel-id: 1237948819677904960
202 suno: 202 suno:
203 enable: true 203 enable: true
204 - token: 13f13540dd3f4ae9885f63ac9f5d0b9f 204 + token: 16b4356581984d538652354b60d69ff0
205 205
206 --- #################### 芋道相关配置 #################### 206 --- #################### 芋道相关配置 ####################
207 207