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 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 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 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 3 import cn.hutool.core.io.IoUtil;
4 4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
5 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 8 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
7 9 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
8 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 15 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
14 16 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
15 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 20 import org.springframework.ai.models.midjourney.MidjourneyConfig;
17 21 import org.springframework.ai.models.midjourney.MidjourneyMessage;
18 22 import org.springframework.ai.models.midjourney.api.MidjourneyInteractionsApi;
19 23 import org.springframework.ai.models.midjourney.webSocket.MidjourneyMessageHandler;
20 24 import org.springframework.ai.models.midjourney.webSocket.MidjourneyWebSocketStarter;
21 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 26 import org.springframework.boot.autoconfigure.AutoConfiguration;
29 27 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
30 28 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
... ... @@ -150,6 +148,13 @@ public class YudaoAiAutoConfiguration {
150 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 158 private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext,
154 159 YudaoAiProperties.MidjourneyProperties midjourneyProperties) {
155 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 26 private YiYanProperties yiyan;
27 27 private OpenAiImageProperties openAiImage;
28 28 private MidjourneyProperties midjourney;
  29 + private SunoProperties suno;
29 30  
30 31 @Data
31 32 @Accessors(chain = true)
... ... @@ -134,4 +135,14 @@ public class YudaoAiProperties {
134 135 */
135 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 6 import com.fasterxml.jackson.annotation.JsonInclude;
5 7 import com.fasterxml.jackson.annotation.JsonProperty;
6   -import com.fasterxml.jackson.databind.ObjectMapper;
7 8 import lombok.Data;
8 9 import lombok.experimental.Accessors;
9 10 import lombok.extern.slf4j.Slf4j;
... ... @@ -23,21 +24,26 @@ public class SunoApi {
23 24 public static final String APPLICATION_JSON = "application/json";
24 25 public static final String TOKEN_PREFIX = "Bearer ";
25 26 public static final String API_URL = "https://api.acedata.cloud/suno/audios";
26   - public static final String TEST_TOKEN = "13f13540dd3f4ae9885f63ac9f5d0b9f";
27 27 private static final int READ_TIMEOUT = 160; // 连接超时时间(秒),音乐生成时间较长,设置为 160s,后续可做callback
28 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 44 Request request = new Request.Builder()
38 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 47 .build();
42 48  
43 49 try (Response response = client.newCall(request).execute()) {
... ... @@ -45,7 +51,9 @@ public class SunoApi {
45 51 log.error("suno调用失败! response: {}", response);
46 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 98  
91 99  
92 100 /**
93   - * API 响应的数据
  101 + * SunoAPI 响应的数据
94 102 */
95 103 @Data
96 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 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 6 import org.junit.Test;
5 7  
6   -import java.io.IOException;
7   -
8 8 /**
9 9 * @Author xiaoxin
10 10 * @Date 2024/5/27
11 11 */
12 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 22 @Test
16   - public void generateMusic() throws IOException {
17   - SunoApi sunoApi = new SunoApi();
  23 + public void generateMusic() {
  24 + SunoApi sunoApi = new SunoApi(sunoConfig);
18 25 SunoApi.SunoRequest sunoRequest = new SunoApi
19 26 .SunoRequest()
20 27 .setPrompt("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
21   - SunoApi.SunoResponse sunoResponse = sunoApi.generateMusic(sunoRequest);
  28 + SunoApi.SunoResponse sunoResponse = sunoApi.musicGen(sunoRequest);
22 29 System.out.println(sunoResponse);
23 30 }
24 31 }
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -199,6 +199,9 @@ yudao.ai:
199 199 token: MTE4MjE3MjY2MjkxNTY3ODIzOA.GEV1SG.c49F8lZoGCUHwsj8O0UdodmM6nyQHvuD2fXflw
200 200 guild-id: 1237948819677904956
201 201 channel-id: 1237948819677904960
  202 + suno:
  203 + enable: true
  204 + token: 13f13540dd3f4ae9885f63ac9f5d0b9f
202 205  
203 206 --- #################### 芋道相关配置 ####################
204 207  
... ...