Browse Source

【新增】AI:音乐接入 API KEY 管理

YunaiV 11 months ago
parent
commit
949d5a1815
13 changed files with 108 additions and 48 deletions
  1. 7 7
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
  2. 15 10
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
  3. 3 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
  4. 4 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java
  5. 10 8
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
  6. 5 6
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
  7. 10 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
  8. 11 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
  9. 2 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
  10. 21 7
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
  11. 12 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
  12. 6 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
  13. 2 2
      yudao-server/src/main/resources/application.yaml

+ 7 - 7
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http

@@ -5,9 +5,9 @@ Authorization: {{token}}
 
 {
   "platform": "Suno",
-  "generateMode": 1,
-  "prompt": "来一首快乐的歌曲",
-  "modelVersion": "chirp-v3.5",
+  "generateMode": 2,
+  "prompt": "周末啦!",
+  "model": "chirp-v3.5",
   "tags": ["Happy"],
   "title": "Happy Song"
 }
@@ -19,8 +19,8 @@ Authorization: {{token}}
 
 {
   "platform": "Suno",
-  "generateMode": 2,
-  "prompt": "来一首快乐的歌曲",
-  "makeInstrumental": false,
-  "title": "Happy Song"
+  "generateMode": 1,
+  "model": "chirp-v3.5",
+  "gptDescriptionPrompt": "今天是星球六,结果是个下雨天,希望心情很美丽",
+  "makeInstrumental": false
 }

+ 15 - 10
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java

@@ -35,6 +35,16 @@ public class AiMusicController {
         return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
     }
 
+    @PostMapping("/generate")
+    @Operation(summary = "音乐生成")
+    public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
+        if (true) {
+            musicService.syncMusic();
+            return null;
+        }
+        return success(musicService.generateMusic(getLoginUserId(), reqVO));
+    }
+
     @Operation(summary = "删除【我的】音乐记录")
     @DeleteMapping("/delete-my")
     @Parameter(name = "id", required = true, description = "音乐编号", example = "1024")
@@ -54,6 +64,7 @@ public class AiMusicController {
         return success(BeanUtils.toBean(music, AiMusicRespVO.class));
     }
 
+    // TODO @xin:这个搞成 updateMy ,修改【我的】音乐。方便后续支持其它字段;另外,需要校验下,更新的音乐,是不是我的!
     @PostMapping("/updateTitle-my")
     @Operation(summary = "修改【我的】音乐 目前只支持修改标题")
     @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星")
@@ -62,12 +73,6 @@ public class AiMusicController {
         return success(true);
     }
 
-    @PostMapping("/generate")
-    @Operation(summary = "音乐生成")
-    public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
-        return success(musicService.generateMusic(getLoginUserId(), reqVO));
-    }
-
     // ================ 音乐管理 ================
 
     @GetMapping("/page")
@@ -87,11 +92,11 @@ public class AiMusicController {
         return success(true);
     }
 
-    @PutMapping("/update-public-status")
-    @Operation(summary = "更新音乐发布状态")
+    @PutMapping("/update")
+    @Operation(summary = "更新音乐")
     @PreAuthorize("@ss.hasPermission('ai:music:update')")
-    public CommonResult<Boolean> updateMusicPublicStatus(@Valid @RequestBody AiMusicUpdatePublicStatusReqVO updateReqVO) {
-        musicService.updateMusicPublicStatus(updateReqVO);
+    public CommonResult<Boolean> updateMusic(@Valid @RequestBody AiMusicUpdateReqVO updateReqVO) {
+        musicService.updateMusic(updateReqVO);
         return success(true);
     }
 

+ 3 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java

@@ -52,6 +52,9 @@ public class AiMusicRespVO {
     @Schema(description = "音乐风格标签")
     private List<String> tags;
 
+    @Schema(description = "音乐时长", example = "[\"pop\",\"jazz\",\"punk\"]")
+    private Double duration;
+
     @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     private Boolean publicStatus;
 
@@ -61,9 +64,6 @@ public class AiMusicRespVO {
     @Schema(description = "错误信息")
     private String errorMessage;
 
-    @Schema(description = "音乐时长")
-    private Double duration;
-
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 

+ 4 - 4
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdatePublicStatusReqVO.java → yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java

@@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-@Schema(description = "管理后台 - AI 音乐修改发布状态 Request VO")
+@Schema(description = "管理后台 - AI 音乐修改 Request VO")
 @Data
-public class AiMusicUpdatePublicStatusReqVO {
+public class AiMusicUpdateReqVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
+    @NotNull(message = "编号不能为空")
     private Long id;
 
-    @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    @NotNull(message = "是否发布不能为空")
+    @Schema(description = "是否发布", example = "true")
     private Boolean publicStatus;
 
 }

+ 10 - 8
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java

@@ -20,11 +20,13 @@ public class AiSunoGenerateReqVO {
      * 1. 描述模式:描述词 + 是否纯音乐 + 模型
      * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型
      */
-    @Schema(description = "生成模式 1.描述模式 2. 歌词模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @NotNull(message = "生成模式不能为空")
     private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举
 
-    @Schema(description = "歌词模式用:用于生成音乐音频的歌词提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED,
+    // TODO @xin:方案一:prompt => lyric 歌词;gptDescriptionPrompt => description 描述(db 那字段也改下,避免和 gpt 直接耦合);这样搞完后,会更统一好理解一点
+    // TODO @xin:方案二:还是之前的做法,都用 prompt;不过最终 gptDescriptionPrompt 还是存储 description 算描述。可以微信一起讨论下。
+    @Schema(description = "用于生成音乐音频的歌词提示",
             example = """
                     [Verse]
                     阳光下奔跑 多么欢快
@@ -37,23 +39,23 @@ public class AiSunoGenerateReqVO {
                     日子太短暂 别再等待
                     马上放假了 梦想起飞
                     """)
-    private String prompt;
+    private String prompt; // 歌词模式用
 
-    @Schema(description = "描述模式用:用于生成音乐音频的描述", requiredMode = Schema.RequiredMode.NOT_REQUIRED,
+    @Schema(description = "用于生成音乐音频的描述",
             example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
-    private String gptDescriptionPrompt;
+    private String gptDescriptionPrompt; // 描述模式用
 
-    @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true")
+    @Schema(description = "是否纯音乐", example = "true")
     private Boolean makeInstrumental;
 
     @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
     @NotEmpty(message = "模型不能为空")
     private String model; // 参见 AiModelEnum 枚举
 
-    @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]")
+    @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
     private List<String> tags;
 
-    @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星")
+    @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星")
     private String title;
 
 }

+ 5 - 6
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java

@@ -98,6 +98,11 @@ public class AiMusicDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> tags;
 
+    /**
+     * 音乐时长
+     */
+    private Double duration;
+
     /**
      * 是否公开
      */
@@ -113,10 +118,4 @@ public class AiMusicDO extends BaseDO {
      */
     private String errorMessage;
 
-
-    /**
-     * 音乐时长
-     */
-    private Double duration;
-
 }

+ 10 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.ai.service.model;
 
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
@@ -91,4 +92,13 @@ public interface AiApiKeyService {
      */
     ImageClient getImageClient(AiPlatformEnum platform);
 
+    /**
+     * 获得 SunoApi 对象
+     *
+     * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择
+     *
+     * @return SunoApi 对象
+     */
+    SunoApi getSunoApi();
+
 }

+ 11 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model;
 
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -111,4 +112,14 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
         return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl());
     }
 
+    @Override
+    public SunoApi getSunoApi() {
+        AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
+                AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
+        if (apiKey == null) {
+            return null;
+        }
+        return clientFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
+    }
+
 }

+ 2 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java

@@ -35,7 +35,7 @@ public interface AiMusicService {
      *
      * @param updateReqVO 更新信息
      */
-    void updateMusicPublicStatus(@Valid AiMusicUpdatePublicStatusReqVO updateReqVO);
+    void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO);
 
     /**
      * 更新音乐名称
@@ -83,4 +83,5 @@ public interface AiMusicService {
      * @return 音乐分页
      */
     PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId);
+
 }

+ 21 - 7
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
 import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
+import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
 import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
@@ -35,7 +36,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXIS
 public class AiMusicServiceImpl implements AiMusicService {
 
     @Resource
-    private SunoApi sunoApi;
+    private AiApiKeyService apiKeyService;
 
     @Resource
     private AiMusicMapper musicMapper;
@@ -46,6 +47,8 @@ public class AiMusicServiceImpl implements AiMusicService {
     @Override
     public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
         // 1. 调用 Suno 生成音乐
+        SunoApi sunoApi = apiKeyService.getSunoApi();
+        // TODO @xin:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的
         List<SunoApi.MusicData> musicDataList;
         if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
             // 1.1 歌词模式
@@ -80,6 +83,7 @@ public class AiMusicServiceImpl implements AiMusicService {
         log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size());
 
         // GET 请求,为避免参数过长,分批次处理
+        SunoApi sunoApi = apiKeyService.getSunoApi();
         CollUtil.split(streamingTask, 36).forEach(chunkList -> {
             Map<String, Long> taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId);
             List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
@@ -96,7 +100,7 @@ public class AiMusicServiceImpl implements AiMusicService {
     }
 
     @Override
-    public void updateMusicPublicStatus(AiMusicUpdatePublicStatusReqVO updateReqVO) {
+    public void updateMusic(AiMusicUpdateReqVO updateReqVO) {
         // 校验存在
         validateMusicExists(updateReqVO.getId());
         // 更新
@@ -152,11 +156,16 @@ public class AiMusicServiceImpl implements AiMusicService {
      * @return AiMusicDO 集合
      */
     private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
+        // TODO @xin:它有 status = error 状态,表示失败噢。
         return convertList(musicList, musicData -> new AiMusicDO()
                 .setTaskId(musicData.id()).setModel(musicData.modelName())
                 .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
-                .setAudioUrl(createFile(musicData.audioUrl())).setVideoUrl(createFile(musicData.videoUrl())).setImageUrl(createFile(musicData.imageUrl())).setDuration(musicData.duration())
-                .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
+                // TODO @xin:只有在完成的状态,在下载文件
+                .setAudioUrl(downloadFile(musicData.audioUrl()))
+                .setVideoUrl(downloadFile(musicData.videoUrl()))
+                .setImageUrl(downloadFile(musicData.imageUrl()))
+                .setTitle(musicData.title()).setDuration(musicData.duration())
+                .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
                 .setStatus(Objects.equals("complete", musicData.status()) ?
                         AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
     }
@@ -167,12 +176,17 @@ public class AiMusicServiceImpl implements AiMusicService {
      * @param url 音频文件地址
      * @return 内部文件地址
      */
-    private String createFile(String url) {
+    private String downloadFile(String url) {
         if (StrUtil.isBlank(url)) {
             return null;
         }
-        byte[] bytes = HttpUtil.downloadBytes(url);
-        return fileApi.createFile(bytes);
+        try {
+            byte[] bytes = HttpUtil.downloadBytes(url);
+            return fileApi.createFile(bytes);
+        } catch (Exception e) {
+            log.error("[downloadFile][url({}) 下载失败]", url, e);
+            return url;
+        }
     }
 
     /**

+ 12 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.ai.core.factory;
 
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import org.springframework.ai.chat.StreamingChatClient;
 import org.springframework.ai.image.ImageClient;
 
@@ -55,4 +56,15 @@ public interface AiClientFactory {
      */
     ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
 
+    /**
+     * 基于指定配置,获得 SunoApi 对象
+     *
+     * 如果不存在,则进行创建
+     *
+     * @param apiKey API KEY
+     * @param url API URL
+     * @return SunoApi 对象
+     */
+    SunoApi getOrCreateSunoApi(String apiKey, String url);
+
 }

+ 6 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java

@@ -9,6 +9,7 @@ import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
 import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+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.api.QianWenApi;
@@ -109,6 +110,11 @@ public class AiClientFactoryImpl implements AiClientFactory {
         }
     }
 
+    @Override
+    public SunoApi getOrCreateSunoApi(String apiKey, String url) {
+        return new SunoApi(url);
+    }
+
     private static String buildClientCacheKey(Class<?> clazz, Object... params) {
         if (ArrayUtil.isEmpty(params)) {
             return clazz.getName();

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

@@ -201,8 +201,8 @@ yudao.ai:
     notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
   suno:
     enable: true
-    base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
-#    base-url: http://127.0.0.1:3001
+#    base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
+    base-url: http://127.0.0.1:3001
 
 --- #################### 芋道相关配置 ####################