From e582aaad2eeabe4411eec239b67a83c02ee72325 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Wed, 29 May 2024 15:17:15 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI=EF=BC=9A?= =?UTF-8?q?suno=E9=9F=B3=E4=B9=90=E7=94=9F=E6=88=90=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/music/MusicController.java | 31 +++++++++ .../admin/music/vo/MusicDataVO.java | 64 +++++++++++++++++++ .../controller/admin/music/vo/SunoReqVO.java | 40 ++++++++++++ .../controller/admin/music/vo/SunoRespVO.java | 29 +++++++++ .../module/ai/service/music/MusicService.java | 19 ++++++ .../ai/service/music/MusicServiceImpl.java | 25 ++++++++ .../ai/config/YudaoAiAutoConfiguration.java | 17 +++-- .../ai/config/YudaoAiProperties.java | 11 ++++ .../ai/core/model/suno/SunoConfig.java | 21 ++++++ .../ai/core/model/suno/{ => api}/SunoApi.java | 32 ++++++---- .../yudao/framework/ai/suno/SunoTests.java | 19 ++++-- .../src/main/resources/application.yaml | 3 + 12 files changed, 287 insertions(+), 24 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java rename yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/{ => api}/SunoApi.java (79%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/MusicController.java new file mode 100644 index 0000000000..ae66d71aa7 --- /dev/null +++ b/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 musicGen(@RequestBody @Valid SunoReqVO sunoReqVO) { + return success(musicService.musicGen(sunoReqVO)); + } +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/MusicDataVO.java new file mode 100644 index 0000000000..ce372aff94 --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java new file mode 100644 index 0000000000..ae40cf5402 --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoRespVO.java new file mode 100644 index 0000000000..bbb4264c77 --- /dev/null +++ b/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 data; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java new file mode 100644 index 0000000000..c0e7b34f18 --- /dev/null +++ b/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); +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java new file mode 100644 index 0000000000..64173d2dc4 --- /dev/null +++ b/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); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 55705c40ff..5472e2fafe 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/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 requestTemplates = new HashMap<>(); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 16a6bc40ee..4a7dd69de6 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/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; + } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java new file mode 100644 index 0000000000..0d717f6cd4 --- /dev/null +++ b/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; +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java similarity index 79% rename from yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoApi.java rename to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java index 0198197f1c..5da45fceda 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoApi.java +++ b/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 { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java index 30883a6e1e..58d1609f79 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java +++ b/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); } } diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 37e0f5b59f..417787ba4c 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/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 --- #################### 芋道相关配置 ####################