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 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 4 import com.fasterxml.jackson.annotation.JsonProperty;
4 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 13 @Data
10   -public class MusicDataVO {
  14 +public class MusicDataVO {
11 15 /**
12 16 * 音乐数据的 ID
13 17 */
... ... @@ -61,4 +65,21 @@ public class MusicDataVO {
61 65 * 音乐音频的风格
62 66 */
63 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 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 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 4 import com.fasterxml.jackson.annotation.JsonProperty;
4 5 import lombok.Data;
5 6  
... ... @@ -26,4 +27,14 @@ public class SunoRespVO {
26 27 */
27 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 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 1 package cn.iocoder.yudao.module.ai.service.music;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
4   -import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 4 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
6 5 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
7 6 import lombok.RequiredArgsConstructor;
... ... @@ -19,7 +18,14 @@ public class MusicServiceImpl implements MusicService {
19 18  
20 19 @Override
21 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 137 }
138 138  
139 139 @Data
140   - @Accessors(chain = true) // TODO @xiaoxin:可以去掉这个,默认全局已经开启
141 140 public static class SunoProperties {
142 141  
143 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 3 import lombok.AllArgsConstructor;
4 4 import lombok.Data;
5 5 import lombok.NoArgsConstructor;
6   -import lombok.experimental.Accessors;
7 6  
8 7 /**
9 8 * @Author xiaoxin
10 9 * @Date 2024/5/29
11 10 */
12 11 @Data
13   -@Accessors(chain = true)
14 12 @NoArgsConstructor
15 13 @AllArgsConstructor
16 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 1 package cn.iocoder.yudao.framework.ai.core.model.suno.api;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
4   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5 4 import com.fasterxml.jackson.annotation.JsonInclude;
6 5 import com.fasterxml.jackson.annotation.JsonProperty;
7   -import lombok.Data;
8   -import lombok.experimental.Accessors;
9 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 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 18 * @Author xiaoxin
19 19 * @Date 2024/5/27
20 20 */
21 21 @Slf4j
22 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 31 .build();
45 32 }
46 33  
47 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 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 15  
16 16 @Before
17 17 public void setup() {
18   - String token = "13f13540dd3f4ae9885f63ac9f5d0b9f";
  18 + String token = "16b4356581984d538652354b60d69ff0";
19 19 this.sunoConfig = new SunoConfig(token);
20 20 }
21 21  
22 22 @Test
23 23 public void generateMusic() {
24 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 201 channel-id: 1237948819677904960
202 202 suno:
203 203 enable: true
204   - token: 13f13540dd3f4ae9885f63ac9f5d0b9f
  204 + token: 16b4356581984d538652354b60d69ff0
205 205  
206 206 --- #################### 芋道相关配置 ####################
207 207  
... ...