Преглед изворни кода

【新增】AI:suno音乐生成接口

xiaoxin пре 11 месеци
родитељ
комит
e582aaad2e
12 измењених фајлова са 287 додато и 24 уклоњено
  1. 31 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java
  2. 64 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java
  3. 40 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java
  4. 29 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java
  5. 19 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java
  6. 25 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java
  7. 11 6
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
  8. 11 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
  9. 21 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java
  10. 20 12
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java
  11. 13 6
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java
  12. 3 0
      yudao-server/src/main/resources/application.yaml

+ 31 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
+import cn.iocoder.yudao.module.ai.service.music.MusicService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - AI 音乐生成")
+@RestController
+@RequestMapping("/ai/music")
+@RequiredArgsConstructor
+public class MusicController {
+
+    private final MusicService musicService;
+
+    @PostMapping("/suno-gen")
+    @Operation(summary = "音乐生成")
+    public CommonResult<SunoRespVO> musicGen(@RequestBody @Valid SunoReqVO sunoReqVO) {
+        return success(musicService.musicGen(sunoReqVO));
+    }
+}

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

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 表示单个音乐数据的类
+ */
+@Data
+public  class MusicDataVO {
+    /**
+     * 音乐数据的 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;
+}

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

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@JsonInclude(value = JsonInclude.Include.NON_NULL)
+public class SunoReqVO {
+    /**
+     * 用于生成音乐音频的提示
+     */
+    private String prompt;
+
+    /**
+     * 用于生成音乐音频的歌词
+     */
+    private String lyric;
+
+    /**
+     * 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
+     */
+    private boolean custom;
+
+    /**
+     * 音乐音频的标题
+     */
+    private String title;
+
+    /**
+     * 音乐音频的风格
+     */
+    private String style;
+
+    /**
+     * 音乐音频生成后回调的 URL
+     */
+    private String callbackUrl;
+}

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

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * API 响应的数据
+ */
+@Data
+public class SunoRespVO {
+    /**
+     * 表示请求是否成功
+     */
+    private boolean success;
+
+    /**
+     * 任务 ID
+     */
+    @JsonProperty("task_id")
+    private String taskId;
+
+    /**
+     * 音乐数据列表
+     */
+    private List<MusicDataVO> data;
+
+}

+ 19 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.ai.service.music;
+
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
+
+/**
+ * @Author xiaoxin
+ * @Date 2024/5/29
+ */
+public interface MusicService {
+
+    /**
+     * 音乐生成
+     *
+     * @param sunoReqVO 请求实体
+     * @return 响应实体
+     */
+    SunoRespVO musicGen(SunoReqVO sunoReqVO);
+}

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

@@ -0,0 +1,25 @@
+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;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author xiaoxin
+ * @Date 2024/5/29
+ */
+@Service
+@RequiredArgsConstructor
+public class MusicServiceImpl implements MusicService {
+
+    private final SunoApi sunoApi;
+
+    @Override
+    public SunoRespVO musicGen(SunoReqVO sunoReqVO) {
+        SunoApi.SunoRequest req = BeanUtils.toBean(sunoReqVO, SunoApi.SunoRequest.class);
+        return BeanUtils.toBean(sunoApi.musicGen(req), SunoRespVO.class);
+    }
+}

+ 11 - 6
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.ai.config;
 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.tongyi.QianWenChatClient;
 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
@@ -13,18 +15,14 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
 import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.ai.models.midjourney.MidjourneyConfig;
 import org.springframework.ai.models.midjourney.MidjourneyMessage;
 import org.springframework.ai.models.midjourney.api.MidjourneyInteractionsApi;
 import org.springframework.ai.models.midjourney.webSocket.MidjourneyMessageHandler;
 import org.springframework.ai.models.midjourney.webSocket.MidjourneyWebSocketStarter;
 import org.springframework.ai.models.midjourney.webSocket.listener.MidjourneyMessageListener;
-import lombok.extern.slf4j.Slf4j;
-import org.jetbrains.annotations.NotNull;
-import org.springframework.ai.openai.OpenAiImageClient;
-import org.springframework.ai.openai.OpenAiImageOptions;
-import org.springframework.ai.openai.api.OpenAiImageApi;
-import org.springframework.ai.retry.RetryUtils;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -150,6 +148,13 @@ public class YudaoAiAutoConfiguration {
         return new MidjourneyInteractionsApi(midjourneyConfig);
     }
 
+    @Bean
+    @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true")
+    public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
+        // 创建 sunoApi
+        return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken()));
+    }
+
     private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext,
                                                                  YudaoAiProperties.MidjourneyProperties midjourneyProperties) {
         Map<String, String> requestTemplates = new HashMap<>();

+ 11 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java

@@ -26,6 +26,7 @@ public class YudaoAiProperties {
     private YiYanProperties yiyan;
     private OpenAiImageProperties openAiImage;
     private MidjourneyProperties midjourney;
+    private SunoProperties suno;
 
     @Data
     @Accessors(chain = true)
@@ -134,4 +135,14 @@ public class YudaoAiProperties {
          */
         private String channelId;
     }
+
+    @Data
+    @Accessors(chain = true)
+    public static class SunoProperties {
+        private boolean enable = false;
+        /**
+         * token
+         */
+        private String token;
+    }
 }

+ 21 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java

@@ -0,0 +1,21 @@
+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 {
+    /**
+     * token信息
+     */
+    private String token;
+}

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

@@ -1,9 +1,10 @@
-package cn.iocoder.yudao.framework.ai.core.model.suno;
+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 com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.Data;
 import lombok.experimental.Accessors;
 import lombok.extern.slf4j.Slf4j;
@@ -23,21 +24,26 @@ public class SunoApi {
     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 TEST_TOKEN = "13f13540dd3f4ae9885f63ac9f5d0b9f";
     private static final int READ_TIMEOUT = 160; // 连接超时时间(秒),音乐生成时间较长,设置为 160s,后续可做callback
     private final OkHttpClient client;
-    private final ObjectMapper objectMapper;
 
-    public SunoApi() {
-        this.client = new OkHttpClient().newBuilder().readTimeout(READ_TIMEOUT, TimeUnit.SECONDS).build();
-        this.objectMapper = new ObjectMapper();
+
+    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);
+                })
+                .build();
     }
 
-    public SunoResponse generateMusic(SunoRequest sunoRequest) throws IOException {
+    public SunoResponse musicGen(SunoRequest sunoRequest) {
         Request request = new Request.Builder()
                 .url(API_URL)
-                .header("Authorization", TOKEN_PREFIX + TEST_TOKEN)
-                .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), objectMapper.writeValueAsString(sunoRequest)))
+                .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), JsonUtils.toJsonString(sunoRequest)))
                 .build();
 
         try (Response response = client.newCall(request).execute()) {
@@ -45,7 +51,9 @@ public class SunoApi {
                 log.error("suno调用失败! response: {}", response);
                 throw new IllegalStateException("suno调用失败!" + response);
             }
-            return objectMapper.readValue(response.body().string(), SunoResponse.class);
+            return JsonUtils.parseObject(response.body().string(), SunoResponse.class);
+        } catch (IOException ioException) {
+            throw new RuntimeException(ioException);
         }
     }
 
@@ -90,7 +98,7 @@ public class SunoApi {
 
 
     /**
-     * API 响应的数据
+     * SunoAPI 响应的数据
      */
     @Data
     public static class SunoResponse {

+ 13 - 6
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java

@@ -1,24 +1,31 @@
 package cn.iocoder.yudao.framework.ai.suno;
 
-import cn.iocoder.yudao.framework.ai.core.model.suno.SunoApi;
+import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 /**
  * @Author xiaoxin
  * @Date 2024/5/27
  */
 public class SunoTests {
 
+    private SunoConfig sunoConfig;
+
+    @Before
+    public void setup() {
+        String token = "13f13540dd3f4ae9885f63ac9f5d0b9f";
+        this.sunoConfig = new SunoConfig(token);
+    }
 
     @Test
-    public void generateMusic() throws IOException {
-        SunoApi sunoApi = new SunoApi();
+    public void generateMusic() {
+        SunoApi sunoApi = new SunoApi(sunoConfig);
         SunoApi.SunoRequest sunoRequest = new SunoApi
                 .SunoRequest()
                 .setPrompt("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
-        SunoApi.SunoResponse sunoResponse = sunoApi.generateMusic(sunoRequest);
+        SunoApi.SunoResponse sunoResponse = sunoApi.musicGen(sunoRequest);
         System.out.println(sunoResponse);
     }
 }

+ 3 - 0
yudao-server/src/main/resources/application.yaml

@@ -199,6 +199,9 @@ yudao.ai:
     token: MTE4MjE3MjY2MjkxNTY3ODIzOA.GEV1SG.c49F8lZoGCUHwsj8O0UdodmM6nyQHvuD2fXflw
     guild-id: 1237948819677904956
     channel-id: 1237948819677904960
+  suno:
+    enable: true
+    token: 13f13540dd3f4ae9885f63ac9f5d0b9f
 
 --- #################### 芋道相关配置 ####################