瀏覽代碼

!2 解决todo
Merge pull request !2 from 小新/master-jdk21-ai

芋道源码 11 月之前
父節點
當前提交
b20129f7e4

+ 22 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java

@@ -1,13 +1,17 @@
 package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
 
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 /**
  * 表示单个音乐数据的类
  */
 @Data
-public  class MusicDataVO {
+public class MusicDataVO {
     /**
      * 音乐数据的 ID
      */
@@ -61,4 +65,21 @@ public  class MusicDataVO {
      * 音乐音频的风格
      */
     private String style;
+
+    public static List<MusicDataVO> convertFrom(List<SunoApi.SunoResp.MusicData> musicDataList) {
+        return musicDataList.stream().map(musicData -> {
+            MusicDataVO musicDataVO = new MusicDataVO();
+            musicDataVO.setId(musicData.id());
+            musicDataVO.setTitle(musicData.title());
+            musicDataVO.setImageUrl(musicData.imageUrl());
+            musicDataVO.setLyric(musicData.lyric());
+            musicDataVO.setAudioUrl(musicData.audioUrl());
+            musicDataVO.setVideoUrl(musicData.videoUrl());
+            musicDataVO.setCreatedAt(musicData.createdAt());
+            musicDataVO.setModel(musicData.model());
+            musicDataVO.setPrompt(musicData.prompt());
+            musicDataVO.setStyle(musicData.style());
+            return musicDataVO;
+        }).collect(Collectors.toList());
+    }
 }

+ 11 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
 
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 
@@ -26,4 +27,14 @@ public class SunoRespVO {
      */
     private List<MusicDataVO> data;
 
+
+    //把 SunoResp转为本vo类
+    public static SunoRespVO convertFrom(SunoApi.SunoResp sunoResp) {
+        SunoRespVO sunoRespVO = new SunoRespVO();
+        sunoRespVO.setSuccess(sunoResp.success());
+        sunoRespVO.setTaskId(sunoResp.taskId());
+        sunoRespVO.setData(MusicDataVO.convertFrom(sunoResp.data()));
+        return sunoRespVO;
+    }
+
 }

+ 9 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java

@@ -1,7 +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.common.util.object.BeanUtils;
 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;
@@ -19,7 +18,14 @@ public class MusicServiceImpl implements MusicService {
 
     @Override
     public SunoRespVO musicGen(SunoReqVO sunoReqVO) {
-        SunoApi.SunoRequest req = BeanUtils.toBean(sunoReqVO, SunoApi.SunoRequest.class);
-        return BeanUtils.toBean(sunoApi.musicGen(req), SunoRespVO.class);
+        SunoApi.SunoResp sunoResp = sunoApi.musicGen(new SunoApi.SunoReq(
+                sunoReqVO.getPrompt(),
+                sunoReqVO.getLyric(),
+                sunoReqVO.isCustom(),
+                sunoReqVO.getTitle(),
+                sunoReqVO.getStyle(),
+                sunoReqVO.getCallbackUrl()
+        ));
+        return SunoRespVO.convertFrom(sunoResp);
     }
 }

+ 0 - 1
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 {
     }
 
     @Data
-    @Accessors(chain = true) // TODO @xiaoxin:可以去掉这个,默认全局已经开启
     public static class SunoProperties {
 
         private boolean enable = false;

+ 0 - 2
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;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
 
 /**
  * @Author xiaoxin
  * @Date 2024/5/29
  */
 @Data
-@Accessors(chain = true)
 @NoArgsConstructor
 @AllArgsConstructor
 public class SunoConfig {

+ 79 - 154
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java

@@ -1,190 +1,115 @@
 package cn.iocoder.yudao.framework.ai.core.model.suno.api;
 
 import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import lombok.experimental.Accessors;
 import lombok.extern.slf4j.Slf4j;
-import okhttp3.*;
+import org.springframework.ai.openai.api.ApiUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
 
-import java.io.IOException;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 
-// TODO @xiaoxin:类注释
 /**
+ * Suno API
+ * <br>
+ * 文档地址:https://platform.acedata.cloud/documents/d016ee3f-421b-4b6e-989a-8beba8701701
+ *
  * @Author xiaoxin
  * @Date 2024/5/27
  */
 @Slf4j
 public class SunoApi {
 
-    // TODO @xiaoxin:APPLICATION_JSON、TOKEN_PREFIX 看看 spring 有没自带的这 2 个枚举哈。变量越少越好
-    public static final String APPLICATION_JSON = "application/json";
-    public static final String TOKEN_PREFIX = "Bearer ";
-    public static final String API_URL = "https://api.acedata.cloud/suno/audios";
+    public static final String DEFAULT_BASE_URL = "https://api.acedata.cloud/suno";
+    private final WebClient webClient;
 
-    private static final int READ_TIMEOUT = 160; // 连接超时时间(秒),音乐生成时间较长,设置为 160s,后续可做callback
-
-    // 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
-    private final OkHttpClient client;
-
-    // TODO @xiaoxin:sunoConfig => config,简洁一点
-    public SunoApi(SunoConfig sunoConfig) {
-        this.client = new OkHttpClient().newBuilder().readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
-                .addInterceptor(chain -> {
-                    Request originalRequest = chain.request();
-                    Request requestWithUserAgent = originalRequest.newBuilder()
-                            .header("Authorization", TOKEN_PREFIX + sunoConfig.getToken())
-                            .build();
-                    return chain.proceed(requestWithUserAgent);
-                })
+    public SunoApi(SunoConfig config) {
+        this.webClient = WebClient.builder()
+                .baseUrl(DEFAULT_BASE_URL)
+                .defaultHeaders(ApiUtils.getJsonContentHeaders(config.getToken()))
                 .build();
     }
 
     // TODO @芋艿:方法名,要考虑下;
-    public SunoResponse musicGen(SunoRequest sunoRequest) {
-        Request request = new Request.Builder()
-                .url(API_URL)
-                .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), JsonUtils.toJsonString(sunoRequest)))
-                .build();
-
-        try (Response response = client.newCall(request).execute()) {
-            if (!response.isSuccessful()) {
-                log.error("suno调用失败! response: {}", response);
-                throw new IllegalStateException("suno调用失败!" + response);
-            }
-            return JsonUtils.parseObject(response.body().string(), SunoResponse.class);
-        } catch (IOException ioException) {
-            throw new RuntimeException(ioException);
-        }
+    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();
     }
 
-    // TODO @xiaoxin:看看是不是使用 record 特性,简化下;
-
     /**
-     * 请求数据对象,用于生成音乐音频
+     * 请求数据对象,用于生成音乐音频。
+     *
+     * @param prompt      用于生成音乐音频的提示
+     * @param lyric       用于生成音乐音频的歌词
+     * @param custom      指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
+     * @param title       音乐音频的标题
+     * @param style       音乐音频的风格
+     * @param callbackUrl 音乐音频生成后回调的 URL
      */
-    @Data
-    @Accessors(chain = true)
     @JsonInclude(value = JsonInclude.Include.NON_NULL)
-    public static class SunoRequest {
-        /**
-         * 用于生成音乐音频的提示
-         */
-        private String prompt;
-
-        /**
-         * 用于生成音乐音频的歌词
-         */
-        private String lyric;
-
-        /**
-         * 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
-         */
-        private boolean custom;
-
-        /**
-         * 音乐音频的标题
-         */
-        private String title;
-
-        /**
-         * 音乐音频的风格
-         */
-        private String style;
+    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);
+        }
 
-        /**
-         * 音乐音频生成后回调的 URL
-         */
-        private String callbackUrl;
     }
 
-    // TODO @xiaoxin:看看是不是使用 record 特性,简化下;
-
     /**
-     * SunoAPI 响应的数据
+     * SunoAPI 响应的数据。
+     *
+     * @param success 表示请求是否成功
+     * @param taskId  任务 ID
+     * @param data    音乐数据列表
      */
-    @Data
-    public static class SunoResponse {
-        /**
-         * 表示请求是否成功
-         */
-        private boolean success;
-
-        /**
-         * 任务 ID
-         */
-        @JsonProperty("task_id")
-        private String taskId;
-
-        /**
-         * 音乐数据列表
-         */
-        private List<MusicData> data;
-
+    public record SunoResp(
+            boolean success,
+            @JsonProperty("task_id") String taskId,
+            List<MusicData> 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     音乐音频的风格
          */
-        @Data
-        static class MusicData {
-            /**
-             * 音乐数据的 ID
-             */
-            private String id;
-
-            /**
-             * 音乐音频的标题
-             */
-            private String title;
-
-            /**
-             * 音乐音频的图片 URL
-             */
-            @JsonProperty("image_url")
-            private String imageUrl;
-
-            /**
-             * 音乐音频的歌词
-             */
-            private String lyric;
-
-            /**
-             * 音乐音频的 URL
-             */
-            @JsonProperty("audio_url")
-            private String audioUrl;
-
-            /**
-             * 音乐视频的 URL
-             */
-            @JsonProperty("video_url")
-            private String videoUrl;
-
-            /**
-             * 音乐音频的创建时间
-             */
-            @JsonProperty("created_at")
-            private String createdAt;
-
-            /**
-             * 使用的模型名称
-             */
-            private String model;
-
-            /**
-             * 生成音乐音频的提示
-             */
-            private String prompt;
-
-            /**
-             * 音乐音频的风格
-             */
-            private String 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
+        ) {
         }
     }
-
-
 }

+ 5 - 6
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 {
 
     @Before
     public void setup() {
-        String token = "13f13540dd3f4ae9885f63ac9f5d0b9f";
+        String token = "16b4356581984d538652354b60d69ff0";
         this.sunoConfig = new SunoConfig(token);
     }
 
     @Test
     public void generateMusic() {
         SunoApi sunoApi = new SunoApi(sunoConfig);
-        SunoApi.SunoRequest sunoRequest = new SunoApi
-                .SunoRequest()
-                .setPrompt("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
-        SunoApi.SunoResponse sunoResponse = sunoApi.musicGen(sunoRequest);
-        System.out.println(sunoResponse);
+        SunoApi.SunoReq sunoReq = new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
+
+        SunoApi.SunoResp sunoResp = sunoApi.musicGen(sunoReq);
+        System.out.println(sunoResp);
     }
 
 }

+ 1 - 1
yudao-server/src/main/resources/application.yaml

@@ -201,7 +201,7 @@ yudao.ai:
     channel-id: 1237948819677904960
   suno:
     enable: true
-    token: 13f13540dd3f4ae9885f63ac9f5d0b9f
+    token: 16b4356581984d538652354b60d69ff0
 
 --- #################### 芋道相关配置 ####################