Commit e582aaad2eeabe4411eec239b67a83c02ee72325

Authored by xiaoxin
1 parent 447526a6

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

Showing 12 changed files with 287 additions and 24 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
  6 +import cn.iocoder.yudao.module.ai.service.music.MusicService;
  7 +import io.swagger.v3.oas.annotations.Operation;
  8 +import io.swagger.v3.oas.annotations.tags.Tag;
  9 +import jakarta.validation.Valid;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.web.bind.annotation.PostMapping;
  12 +import org.springframework.web.bind.annotation.RequestBody;
  13 +import org.springframework.web.bind.annotation.RequestMapping;
  14 +import org.springframework.web.bind.annotation.RestController;
  15 +
  16 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  17 +
  18 +@Tag(name = "管理后台 - AI 音乐生成")
  19 +@RestController
  20 +@RequestMapping("/ai/music")
  21 +@RequiredArgsConstructor
  22 +public class MusicController {
  23 +
  24 + private final MusicService musicService;
  25 +
  26 + @PostMapping("/suno-gen")
  27 + @Operation(summary = "音乐生成")
  28 + public CommonResult<SunoRespVO> musicGen(@RequestBody @Valid SunoReqVO sunoReqVO) {
  29 + return success(musicService.musicGen(sunoReqVO));
  30 + }
  31 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 表示单个音乐数据的类
  8 + */
  9 +@Data
  10 +public class MusicDataVO {
  11 + /**
  12 + * 音乐数据的 ID
  13 + */
  14 + private String id;
  15 +
  16 + /**
  17 + * 音乐音频的标题
  18 + */
  19 + private String title;
  20 +
  21 + /**
  22 + * 音乐音频的图片 URL
  23 + */
  24 + @JsonProperty("image_url")
  25 + private String imageUrl;
  26 +
  27 + /**
  28 + * 音乐音频的歌词
  29 + */
  30 + private String lyric;
  31 +
  32 + /**
  33 + * 音乐音频的 URL
  34 + */
  35 + @JsonProperty("audio_url")
  36 + private String audioUrl;
  37 +
  38 + /**
  39 + * 音乐视频的 URL
  40 + */
  41 + @JsonProperty("video_url")
  42 + private String videoUrl;
  43 +
  44 + /**
  45 + * 音乐音频的创建时间
  46 + */
  47 + @JsonProperty("created_at")
  48 + private String createdAt;
  49 +
  50 + /**
  51 + * 使用的模型名称
  52 + */
  53 + private String model;
  54 +
  55 + /**
  56 + * 生成音乐音频的提示
  57 + */
  58 + private String prompt;
  59 +
  60 + /**
  61 + * 音乐音频的风格
  62 + */
  63 + private String style;
  64 +}
0 \ No newline at end of file 65 \ 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/SunoReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonInclude;
  4 +import lombok.Data;
  5 +import lombok.experimental.Accessors;
  6 +
  7 +@Data
  8 +@Accessors(chain = true)
  9 +@JsonInclude(value = JsonInclude.Include.NON_NULL)
  10 +public class SunoReqVO {
  11 + /**
  12 + * 用于生成音乐音频的提示
  13 + */
  14 + private String prompt;
  15 +
  16 + /**
  17 + * 用于生成音乐音频的歌词
  18 + */
  19 + private String lyric;
  20 +
  21 + /**
  22 + * 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
  23 + */
  24 + private boolean custom;
  25 +
  26 + /**
  27 + * 音乐音频的标题
  28 + */
  29 + private String title;
  30 +
  31 + /**
  32 + * 音乐音频的风格
  33 + */
  34 + private String style;
  35 +
  36 + /**
  37 + * 音乐音频生成后回调的 URL
  38 + */
  39 + private String callbackUrl;
  40 +}
0 \ 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/controller/admin/music/vo/SunoRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * API 响应的数据
  10 + */
  11 +@Data
  12 +public class SunoRespVO {
  13 + /**
  14 + * 表示请求是否成功
  15 + */
  16 + private boolean success;
  17 +
  18 + /**
  19 + * 任务 ID
  20 + */
  21 + @JsonProperty("task_id")
  22 + private String taskId;
  23 +
  24 + /**
  25 + * 音乐数据列表
  26 + */
  27 + private List<MusicDataVO> data;
  28 +
  29 +}
0 \ No newline at end of file 30 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.music;
  2 +
  3 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
  5 +
  6 +/**
  7 + * @Author xiaoxin
  8 + * @Date 2024/5/29
  9 + */
  10 +public interface MusicService {
  11 +
  12 + /**
  13 + * 音乐生成
  14 + *
  15 + * @param sunoReqVO 请求实体
  16 + * @return 响应实体
  17 + */
  18 + SunoRespVO musicGen(SunoReqVO sunoReqVO);
  19 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.music;
  2 +
  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;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
  7 +import lombok.RequiredArgsConstructor;
  8 +import org.springframework.stereotype.Service;
  9 +
  10 +/**
  11 + * @Author xiaoxin
  12 + * @Date 2024/5/29
  13 + */
  14 +@Service
  15 +@RequiredArgsConstructor
  16 +public class MusicServiceImpl implements MusicService {
  17 +
  18 + private final SunoApi sunoApi;
  19 +
  20 + @Override
  21 + public SunoRespVO musicGen(SunoReqVO sunoReqVO) {
  22 + SunoApi.SunoRequest req = BeanUtils.toBean(sunoReqVO, SunoApi.SunoRequest.class);
  23 + return BeanUtils.toBean(sunoApi.musicGen(req), SunoRespVO.class);
  24 + }
  25 +}
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; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.ai.config;
3 import cn.hutool.core.io.IoUtil; 3 import cn.hutool.core.io.IoUtil;
4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; 4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
5 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; 5 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl;
  6 +import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
  7 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
6 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; 8 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
7 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; 9 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
8 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions; 10 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; @@ -13,18 +15,14 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
13 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient; 15 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
14 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions; 16 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
15 import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi; 17 import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.jetbrains.annotations.NotNull;
16 import org.springframework.ai.models.midjourney.MidjourneyConfig; 20 import org.springframework.ai.models.midjourney.MidjourneyConfig;
17 import org.springframework.ai.models.midjourney.MidjourneyMessage; 21 import org.springframework.ai.models.midjourney.MidjourneyMessage;
18 import org.springframework.ai.models.midjourney.api.MidjourneyInteractionsApi; 22 import org.springframework.ai.models.midjourney.api.MidjourneyInteractionsApi;
19 import org.springframework.ai.models.midjourney.webSocket.MidjourneyMessageHandler; 23 import org.springframework.ai.models.midjourney.webSocket.MidjourneyMessageHandler;
20 import org.springframework.ai.models.midjourney.webSocket.MidjourneyWebSocketStarter; 24 import org.springframework.ai.models.midjourney.webSocket.MidjourneyWebSocketStarter;
21 import org.springframework.ai.models.midjourney.webSocket.listener.MidjourneyMessageListener; 25 import org.springframework.ai.models.midjourney.webSocket.listener.MidjourneyMessageListener;
22 -import lombok.extern.slf4j.Slf4j;  
23 -import org.jetbrains.annotations.NotNull;  
24 -import org.springframework.ai.openai.OpenAiImageClient;  
25 -import org.springframework.ai.openai.OpenAiImageOptions;  
26 -import org.springframework.ai.openai.api.OpenAiImageApi;  
27 -import org.springframework.ai.retry.RetryUtils;  
28 import org.springframework.boot.autoconfigure.AutoConfiguration; 26 import org.springframework.boot.autoconfigure.AutoConfiguration;
29 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 27 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
30 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 28 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -150,6 +148,13 @@ public class YudaoAiAutoConfiguration { @@ -150,6 +148,13 @@ public class YudaoAiAutoConfiguration {
150 return new MidjourneyInteractionsApi(midjourneyConfig); 148 return new MidjourneyInteractionsApi(midjourneyConfig);
151 } 149 }
152 150
  151 + @Bean
  152 + @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true")
  153 + public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
  154 + // 创建 sunoApi
  155 + return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getToken()));
  156 + }
  157 +
153 private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext, 158 private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext,
154 YudaoAiProperties.MidjourneyProperties midjourneyProperties) { 159 YudaoAiProperties.MidjourneyProperties midjourneyProperties) {
155 Map<String, String> requestTemplates = new HashMap<>(); 160 Map<String, String> requestTemplates = new HashMap<>();
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 { @@ -26,6 +26,7 @@ public class YudaoAiProperties {
26 private YiYanProperties yiyan; 26 private YiYanProperties yiyan;
27 private OpenAiImageProperties openAiImage; 27 private OpenAiImageProperties openAiImage;
28 private MidjourneyProperties midjourney; 28 private MidjourneyProperties midjourney;
  29 + private SunoProperties suno;
29 30
30 @Data 31 @Data
31 @Accessors(chain = true) 32 @Accessors(chain = true)
@@ -134,4 +135,14 @@ public class YudaoAiProperties { @@ -134,4 +135,14 @@ public class YudaoAiProperties {
134 */ 135 */
135 private String channelId; 136 private String channelId;
136 } 137 }
  138 +
  139 + @Data
  140 + @Accessors(chain = true)
  141 + public static class SunoProperties {
  142 + private boolean enable = false;
  143 + /**
  144 + * token
  145 + */
  146 + private String token;
  147 + }
137 } 148 }
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.model.suno;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +import lombok.experimental.Accessors;
  7 +
  8 +/**
  9 + * @Author xiaoxin
  10 + * @Date 2024/5/29
  11 + */
  12 +@Data
  13 +@Accessors(chain = true)
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
  16 +public class SunoConfig {
  17 + /**
  18 + * token信息
  19 + */
  20 + private String token;
  21 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoApi.java renamed to 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; 1 +package cn.iocoder.yudao.framework.ai.core.model.suno.api;
2 2
3 3
  4 +import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 import com.fasterxml.jackson.annotation.JsonInclude; 6 import com.fasterxml.jackson.annotation.JsonInclude;
5 import com.fasterxml.jackson.annotation.JsonProperty; 7 import com.fasterxml.jackson.annotation.JsonProperty;
6 -import com.fasterxml.jackson.databind.ObjectMapper;  
7 import lombok.Data; 8 import lombok.Data;
8 import lombok.experimental.Accessors; 9 import lombok.experimental.Accessors;
9 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
@@ -23,21 +24,26 @@ public class SunoApi { @@ -23,21 +24,26 @@ public class SunoApi {
23 public static final String APPLICATION_JSON = "application/json"; 24 public static final String APPLICATION_JSON = "application/json";
24 public static final String TOKEN_PREFIX = "Bearer "; 25 public static final String TOKEN_PREFIX = "Bearer ";
25 public static final String API_URL = "https://api.acedata.cloud/suno/audios"; 26 public static final String API_URL = "https://api.acedata.cloud/suno/audios";
26 - public static final String TEST_TOKEN = "13f13540dd3f4ae9885f63ac9f5d0b9f";  
27 private static final int READ_TIMEOUT = 160; // 连接超时时间(秒),音乐生成时间较长,设置为 160s,后续可做callback 27 private static final int READ_TIMEOUT = 160; // 连接超时时间(秒),音乐生成时间较长,设置为 160s,后续可做callback
28 private final OkHttpClient client; 28 private final OkHttpClient client;
29 - private final ObjectMapper objectMapper;  
30 29
31 - public SunoApi() {  
32 - this.client = new OkHttpClient().newBuilder().readTimeout(READ_TIMEOUT, TimeUnit.SECONDS).build();  
33 - this.objectMapper = new ObjectMapper(); 30 +
  31 + public SunoApi(SunoConfig sunoConfig) {
  32 + this.client = new OkHttpClient().newBuilder().readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
  33 + .addInterceptor(chain -> {
  34 + Request originalRequest = chain.request();
  35 + Request requestWithUserAgent = originalRequest.newBuilder()
  36 + .header("Authorization", TOKEN_PREFIX + sunoConfig.getToken())
  37 + .build();
  38 + return chain.proceed(requestWithUserAgent);
  39 + })
  40 + .build();
34 } 41 }
35 42
36 - public SunoResponse generateMusic(SunoRequest sunoRequest) throws IOException { 43 + public SunoResponse musicGen(SunoRequest sunoRequest) {
37 Request request = new Request.Builder() 44 Request request = new Request.Builder()
38 .url(API_URL) 45 .url(API_URL)
39 - .header("Authorization", TOKEN_PREFIX + TEST_TOKEN)  
40 - .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), objectMapper.writeValueAsString(sunoRequest))) 46 + .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), JsonUtils.toJsonString(sunoRequest)))
41 .build(); 47 .build();
42 48
43 try (Response response = client.newCall(request).execute()) { 49 try (Response response = client.newCall(request).execute()) {
@@ -45,7 +51,9 @@ public class SunoApi { @@ -45,7 +51,9 @@ public class SunoApi {
45 log.error("suno调用失败! response: {}", response); 51 log.error("suno调用失败! response: {}", response);
46 throw new IllegalStateException("suno调用失败!" + response); 52 throw new IllegalStateException("suno调用失败!" + response);
47 } 53 }
48 - return objectMapper.readValue(response.body().string(), SunoResponse.class); 54 + return JsonUtils.parseObject(response.body().string(), SunoResponse.class);
  55 + } catch (IOException ioException) {
  56 + throw new RuntimeException(ioException);
49 } 57 }
50 } 58 }
51 59
@@ -90,7 +98,7 @@ public class SunoApi { @@ -90,7 +98,7 @@ public class SunoApi {
90 98
91 99
92 /** 100 /**
93 - * API 响应的数据 101 + * SunoAPI 响应的数据
94 */ 102 */
95 @Data 103 @Data
96 public static class SunoResponse { 104 public static class SunoResponse {
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java
1 package cn.iocoder.yudao.framework.ai.suno; 1 package cn.iocoder.yudao.framework.ai.suno;
2 2
3 -import cn.iocoder.yudao.framework.ai.core.model.suno.SunoApi; 3 +import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
  4 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
  5 +import org.junit.Before;
4 import org.junit.Test; 6 import org.junit.Test;
5 7
6 -import java.io.IOException;  
7 -  
8 /** 8 /**
9 * @Author xiaoxin 9 * @Author xiaoxin
10 * @Date 2024/5/27 10 * @Date 2024/5/27
11 */ 11 */
12 public class SunoTests { 12 public class SunoTests {
13 13
  14 + private SunoConfig sunoConfig;
  15 +
  16 + @Before
  17 + public void setup() {
  18 + String token = "13f13540dd3f4ae9885f63ac9f5d0b9f";
  19 + this.sunoConfig = new SunoConfig(token);
  20 + }
14 21
15 @Test 22 @Test
16 - public void generateMusic() throws IOException {  
17 - SunoApi sunoApi = new SunoApi(); 23 + public void generateMusic() {
  24 + SunoApi sunoApi = new SunoApi(sunoConfig);
18 SunoApi.SunoRequest sunoRequest = new SunoApi 25 SunoApi.SunoRequest sunoRequest = new SunoApi
19 .SunoRequest() 26 .SunoRequest()
20 .setPrompt("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"); 27 .setPrompt("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
21 - SunoApi.SunoResponse sunoResponse = sunoApi.generateMusic(sunoRequest); 28 + SunoApi.SunoResponse sunoResponse = sunoApi.musicGen(sunoRequest);
22 System.out.println(sunoResponse); 29 System.out.println(sunoResponse);
23 } 30 }
24 } 31 }
yudao-server/src/main/resources/application.yaml
@@ -199,6 +199,9 @@ yudao.ai: @@ -199,6 +199,9 @@ yudao.ai:
199 token: MTE4MjE3MjY2MjkxNTY3ODIzOA.GEV1SG.c49F8lZoGCUHwsj8O0UdodmM6nyQHvuD2fXflw 199 token: MTE4MjE3MjY2MjkxNTY3ODIzOA.GEV1SG.c49F8lZoGCUHwsj8O0UdodmM6nyQHvuD2fXflw
200 guild-id: 1237948819677904956 200 guild-id: 1237948819677904956
201 channel-id: 1237948819677904960 201 channel-id: 1237948819677904960
  202 + suno:
  203 + enable: true
  204 + token: 13f13540dd3f4ae9885f63ac9f5d0b9f
202 205
203 --- #################### 芋道相关配置 #################### 206 --- #################### 芋道相关配置 ####################
204 207