瀏覽代碼

【解决todo】AI Music: 结构优化,task状态同步使用统一标准Job

xiaoxin 1 年之前
父節點
當前提交
abd80fe390
共有 19 個文件被更改,包括 447 次插入313 次删除
  1. 1 2
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java
  2. 35 0
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateEnum.java
  3. 5 9
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
  4. 7 15
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
  5. 0 22
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoLyricModeVO.java
  6. 30 14
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/SunoReqVO.java
  7. 73 46
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
  8. 59 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/SunoJob.java
  9. 40 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicConvert.java
  10. 36 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
  11. 103 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
  12. 0 24
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicService.java
  13. 0 102
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/MusicServiceImpl.java
  14. 1 2
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
  15. 1 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
  16. 0 23
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/SunoConfig.java
  17. 48 45
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java
  18. 7 8
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/suno/SunoTests.java
  19. 1 1
      yudao-server/src/main/resources/application.yaml

+ 1 - 2
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java

@@ -50,8 +50,7 @@ public enum AiModelEnum {
     XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"),
     XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"),
 
-    // TODO @xin:// Suno;中间加个空格,会更清晰一点。一般来说,不同类型的单词之间,最好有空格。例如说,// 新增一个;再例如说;// 这是 1 个 create 逻辑
-    //Suno
+    // Suno 模型
     SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null),
     SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null),
     SUNO_3_5( "SUNO-3.5", "chirp-v3.5",null),

+ 35 - 0
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateEnum.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.ai.enums.music;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * AI 音乐状态的枚举
+ *
+ * @author xiaoxin
+ */
+@AllArgsConstructor
+@Getter
+public enum AiMusicGenerateEnum {
+
+    LYRIC("lyric", "歌词模式"),
+    DESCRIPTION("description", "描述模式");
+
+    /**
+     * 模式
+     */
+    private final String mode;
+    /**
+     * 模式名
+     */
+    private final String name;
+
+    public static AiMusicGenerateEnum valueOfMode(String mode) {
+        for (AiMusicGenerateEnum modeEnum : AiMusicGenerateEnum.values()) {
+            if (modeEnum.getMode().equals(mode)) {
+                return modeEnum;
+            }
+        }
+        throw new IllegalArgumentException("未知模式: " + mode);
+    }
+}

+ 5 - 9
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiMusicStatusEnum.java → yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java

@@ -1,22 +1,18 @@
-package cn.iocoder.yudao.module.ai.enums;
+package cn.iocoder.yudao.module.ai.enums.music;
 
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
-// TODO @xin:这个类,挪到 enums/music 包下;
-// TODO @xin:1)@author 这个是标准的 javadoc;2)@date 可以不要哈;3)可以加下枚举类的注释
 /**
- * @Author xiaoxin
- * @Date 2024/6/5
+ * AI 音乐状态的枚举
+ *
+ * @author xiaoxin
  */
 @AllArgsConstructor
 @Getter
 public enum AiMusicStatusEnum {
 
-    // TODO @xin:是不是收敛成,只有 3 个:进行中,成功,失败;类似 AiImageStatusEnum
-
-    SUBMITTED("submitted", "已提交"),
-    QUEUED("queued", "排队中"),
+    // @xin 文档中无失败这个返回值
     STREAMING("streaming", "进行中"),
     COMPLETE("complete", "完成");
 

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

@@ -1,9 +1,8 @@
 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.SunoLyricModeVO;
 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
-import cn.iocoder.yudao.module.ai.service.music.MusicService;
+import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
@@ -17,25 +16,18 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
-// TODO @xin:AI 前缀;都要加下哈
 @Tag(name = "管理后台 - AI 音乐生成")
 @RestController
 @RequestMapping("/ai/music")
 @RequiredArgsConstructor
-public class MusicController {
+public class AiMusicController {
 
-    private final MusicService musicService;
+    private final AiMusicService aiMusicService;
 
-    @PostMapping("generate/description-mode")
-    @Operation(summary = "音乐生成-描述模式")
-    public CommonResult<List<Long>> descriptionMode(@RequestBody @Valid SunoReqVO sunoReqVO) {
-        return success(musicService.descriptionMode(sunoReqVO));
-    }
-
-    @PostMapping("generate/lyric-mode")
-    @Operation(summary = "音乐生成-歌词模式")
-    public CommonResult<List<Long>> lyricMode(@RequestBody @Valid SunoLyricModeVO sunoLyricModeVO) {
-        return success(musicService.lyricMode(sunoLyricModeVO));
+    @PostMapping("/generate")
+    @Operation(summary = "音乐生成")
+    public CommonResult<List<Long>> generateMusic(@RequestBody @Valid SunoReqVO sunoReqVO) {
+        return success(aiMusicService.generateMusic(sunoReqVO));
     }
 
 }

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

@@ -1,22 +0,0 @@
-package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
-
-import lombok.Data;
-
-/**
- * @Author jxli@quant360.com
- * @Date 2024/6/7
- */
-@Data
-public class SunoLyricModeVO extends SunoReqVO {
-
-    /**
-     * 标签/音乐风格
-     */
-    private String tags;
-
-    /**
-     * 音乐名称
-     */
-    private String title;
-
-}

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

@@ -1,23 +1,39 @@
 package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
 
-import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
+import java.util.List;
+
+
+/**
+ * @author xiaoxin
+ */
 @Data
-@JsonInclude(value = JsonInclude.Include.NON_NULL) // TODO @xin:不用加这个哈
 public class SunoReqVO {
-    /**
-     * 用于生成音乐音频的提示
-     */
+
+    @Schema(description = "用于生成音乐音频的提示")
     private String prompt;
-    // TODO @xin:Boolean,不使用基本类型。
-    /**
-     *  是否纯音乐
-     */
-    private boolean makeInstrumental;
-    /**
-     * //todo 首次请求返回的模型是对的,后续更新音频返回的模型又变成v3.5了
-     * 模型版本  {@link cn.iocoder.yudao.module.ai.enums.AiModelEnum} Suno
-     */
+
+    @Schema(description = "是否纯音乐")
+    private Boolean makeInstrumental;
+
+    @Schema(description = "模型版本 ")
     private String mv;
+
+    @Schema(description = "音乐风格")
+    private List<String> tags;
+
+    @Schema(description = "音乐/歌曲名称")
+    private String title;
+
+    @Schema(description = "平台")
+    @NotBlank(message = "平台不能为空")
+    private String platform;
+
+    @Schema(description = "生成模式 lyric(歌词模式), description(描述模式)")
+    @NotBlank(message = "生成模式不能为空")
+    private String generateMode;
+
 }

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

@@ -1,15 +1,16 @@
 package cn.iocoder.yudao.module.ai.dal.dataobject.music;
 
-import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-import io.swagger.v3.oas.annotations.media.Schema;
+import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
 import lombok.Data;
 
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * @Author xiaoxin
@@ -19,77 +20,103 @@ import java.util.stream.Collectors;
 @Data
 public class AiMusicDO extends BaseDO {
 
-    // TODO @xin:@Schema 只在 VO 里使用,这里还是使用标准的注释哈
+    /**
+     * 编号
+     */
     @TableId(type = IdType.AUTO)
-    @Schema(description = "编号")
     private Long id;
 
-    @Schema(description = "用户编号")
+    /**
+     * 用户编号
+     */
     private Long userId;
 
-    @Schema(description = "音乐名称")
+    /**
+     * 音乐名称
+     */
     private String title;
 
-    @Schema(description = "图片地址")
+    /**
+     * 图片地址
+     */
     private String imageUrl;
 
-    @Schema(description = "歌词")
+    /**
+     * 歌词
+     */
     private String lyric;
 
-    @Schema(description = "音频地址")
+    /**
+     * 音频地址
+     */
     private String audioUrl;
 
-    @Schema(description = "视频地址")
+    /**
+     * 视频地址
+     */
     private String videoUrl;
 
-    // TODO @xin:需要关联下对应的枚举
-    @Schema(description = "音乐状态")
+    /**
+     * 音乐状态
+     * <p>
+     * 枚举 {@link AiMusicStatusEnum}
+     */
     private String status;
 
-    @Schema(description = "描述词")
+    /**
+     * 描述词
+     */
     private String gptDescriptionPrompt;
-
-    @Schema(description = "提示词")
+    /**
+     * 提示词
+     */
     private String prompt;
 
-    // TODO @xin:生成模式,需要记录下;歌词、描述
-
-    // TODO @xin:多存储一个平台,platform;考虑未来可能有别的音乐接口
-    @Schema(description = "模型")
+    /**
+     * 生成模式
+     */
+    private String generateMode;
+
+    /**
+     * 平台
+     * <p>
+     * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum}
+     */
+    private String platform;
+
+    /**
+     * 模型
+     */
     private String model;
 
-    @Schema(description = "错误信息")
+    /**
+     * 错误信息
+     */
     private String errorMessage;
 
-    // TODO @xin:tags 要不要使用 List<String>
 
-    @Schema(description = "音乐风格标签")
-    private String tags;
+    /**
+     * 音乐风格标签
+     */
+    @TableField(typeHandler = AiMusicTagsHandler.class)
+    private List<String> tags;
 
-    @Schema(description = "任务编号")
+    /**
+     * 任务编号
+     */
     private String taskId;
 
-    // TODO @xin:转换不放在 DO 里面哈。
-
-    public static AiMusicDO convertFrom(SunoApi.MusicData musicData) {
-        return new AiMusicDO()
-                .setTaskId(musicData.id())
-                .setPrompt(musicData.prompt())
-                .setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
-                .setAudioUrl(musicData.audioUrl())
-                .setVideoUrl(musicData.videoUrl())
-                .setImageUrl(musicData.imageUrl())
-                .setLyric(musicData.lyric())
-                .setTitle(musicData.title())
-                .setStatus(musicData.status())
-                .setModel(musicData.modelName())
-                .setTags(musicData.tags());
-    }
 
-    public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> musicDataList) {
-        return musicDataList.stream()
-                .map(AiMusicDO::convertFrom)
-                .collect(Collectors.toList());
-    }
+    public static class AiMusicTagsHandler extends AbstractJsonTypeHandler<Object> {
+
+        @Override
+        protected Object parse(String json) {
+            return JsonUtils.parseArray(json, String.class);
+        }
 
+        @Override
+        protected String toJson(Object obj) {
+            return JsonUtils.toJsonString(obj);
+        }
+    }
 }

+ 59 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/SunoJob.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.ai.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
+import cn.iocoder.yudao.module.ai.service.music.AiMusicConvert;
+import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 处理 Suno Job
+ * @author xiaoxin
+ */
+@Component
+@Slf4j
+public class SunoJob implements JobHandler {
+
+    @Resource
+    private SunoApi sunoApi;
+    @Resource
+    private AiMusicService musicService;
+
+    @Override
+    public String execute(String param) {
+        List<AiMusicDO> unCompletedTask = musicService.getUnCompletedTask();
+
+        if (CollUtil.isEmpty(unCompletedTask)) {
+            log.info("Suno 无进行中任务需要更新!");
+            return "Suno 无进行中任务需要更新!";
+        }
+
+
+        log.info("Suno 开始同步, 共 [{}] 个任务!", unCompletedTask.size());
+        //GET 请求,为避免参数过长,分批次处理
+        CollUtil.split(unCompletedTask, 4)
+                .forEach(chunk -> {
+                    Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId);
+                    List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
+                    if (CollUtil.isNotEmpty(musicTaskList)) {
+                        List<AiMusicDO> aiMusicDOS = AiMusicConvert.convertFrom(musicTaskList);
+                        //回填id
+                        aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId())));
+                        musicService.updateBatch(aiMusicDOS);
+                    } else {
+                        log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet());
+                    }
+                });
+
+        return "Suno 同步 - ".concat(String.valueOf(unCompletedTask.size())).concat(" 个任务!");
+    }
+}

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

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.ai.service.music;
+
+import cn.hutool.core.text.StrPool;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
+import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * AI 音乐 Convert
+ *
+ * @author xiaoxin
+ */
+public class AiMusicConvert {
+
+    public static AiMusicDO convertFrom(SunoApi.MusicData musicData) {
+        return new AiMusicDO()
+                .setTaskId(musicData.id())
+                .setPrompt(musicData.prompt())
+                .setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
+                .setAudioUrl(musicData.audioUrl())
+                .setVideoUrl(musicData.videoUrl())
+                .setImageUrl(musicData.imageUrl())
+                .setLyric(musicData.lyric())
+                .setTitle(musicData.title())
+                .setStatus(musicData.status())
+                .setModel(musicData.modelName())
+                .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null);
+    }
+
+    public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> musicDataList) {
+        return musicDataList.stream()
+                .map(AiMusicConvert::convertFrom)
+                .collect(Collectors.toList());
+    }
+
+
+}

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

@@ -0,0 +1,36 @@
+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.dal.dataobject.music.AiMusicDO;
+
+import java.util.List;
+
+/**
+ * AI 音乐 Service 接口
+ *
+ * @author xiaoxin
+ */
+public interface AiMusicService {
+
+
+    /**
+     * 音乐生成
+     *
+     * @param reqVO 请求参数
+     * @return 生成的音乐ID
+     */
+    List<Long> generateMusic(SunoReqVO reqVO);
+
+
+    /**
+     * 获取未完成状态的任务
+     *
+     * @return 未完成任务列表
+     */
+    List<AiMusicDO> getUnCompletedTask();
+
+
+    Boolean updateBatch(List<AiMusicDO> aiMusicDOList);
+
+
+}

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

@@ -0,0 +1,103 @@
+package cn.iocoder.yudao.module.ai.service.music;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.text.StrPool;
+import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
+import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
+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.AiMusicGenerateEnum;
+import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * AI 音乐 Service 实现类
+ * @author xiaoxin
+ */
+@Service
+@Slf4j
+public class AiMusicServiceImpl implements AiMusicService {
+
+    @Resource
+    private SunoApi sunoApi;
+    @Resource
+    private AiMusicMapper musicMapper;
+
+    @Override
+    public List<Long> generateMusic(SunoReqVO reqVO) {
+        AiMusicGenerateEnum generateEnum = AiMusicGenerateEnum.valueOfMode(reqVO.getGenerateMode());
+        return switch (generateEnum) {
+            case DESCRIPTION -> descriptionMode(reqVO);
+            case LYRIC -> lyricMode(reqVO);
+        };
+    }
+
+    @Override
+    public List<AiMusicDO> getUnCompletedTask() {
+        return musicMapper.selectList(new LambdaQueryWrapper<AiMusicDO>().ne(AiMusicDO::getStatus, AiMusicStatusEnum.COMPLETE.getStatus()));
+    }
+
+    @Override
+    public Boolean updateBatch(List<AiMusicDO> aiMusicDOList) {
+        return musicMapper.updateBatch(aiMusicDOList);
+    }
+
+    /**
+     * 描述模式生成音乐
+     *
+     * @param reqVO 请求参数
+     * @return 生成的音乐ID集合
+     */
+    public List<Long> descriptionMode(SunoReqVO reqVO) {
+        // 1. 异步生成
+        SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getMakeInstrumental());
+        List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq);
+        // 2. 插入数据库
+        return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform());
+    }
+
+    /**
+     * 歌词模式生成音乐
+     *
+     * @param reqVO 请求参数
+     * @return 生成的音乐ID集合
+     */
+    public List<Long> lyricMode(SunoReqVO reqVO) {
+        // 1. 异步生成
+        SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
+        List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq);
+        // 2. 插入数据库
+        return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform());
+    }
+
+    /**
+     * 新增音乐数据并提交 suno任务
+     *
+     * @param musicDataList 音乐数据列表
+     * @return 音乐id集合
+     */
+    private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList, String generateMode, String platform) {
+        if (CollUtil.isEmpty(musicDataList)) {
+            return Collections.emptyList();
+        }
+        List<AiMusicDO> aiMusicDOList = AiMusicConvert.convertFrom(musicDataList).stream()
+                .map(musicDO -> musicDO.setUserId(getLoginUserId())
+                        .setGenerateMode(generateMode)
+                        .setPlatform(platform))
+                .toList();
+        musicMapper.insertBatch(aiMusicDOList);
+        return aiMusicDOList.stream()
+                .map(AiMusicDO::getId)
+                .collect(Collectors.toList());
+    }
+
+}

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

@@ -1,24 +0,0 @@
-package cn.iocoder.yudao.module.ai.service.music;
-
-import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO;
-import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
-
-import java.util.List;
-
-/**
- * @Author xiaoxin
- * @Date 2024/5/29
- */
-public interface MusicService {
-
-    /**
-     * 音乐生成-描述模式
-     */
-    List<Long> descriptionMode(SunoReqVO reqVO);
-
-
-    /**
-     * 音乐生成-歌词模式
-     **/
-    List<Long> lyricMode(SunoLyricModeVO reqVO);
-}

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

@@ -1,102 +0,0 @@
-package cn.iocoder.yudao.module.ai.service.music;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.text.StrPool;
-import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
-import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO;
-import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
-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.AiMusicStatusEnum;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.*;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-
-/**
- * @Author xiaoxin
- * @Date 2024/5/29
- */
-@Service
-@RequiredArgsConstructor
-@Slf4j
-public class MusicServiceImpl implements MusicService {
-
-    // TODO @xin:使用 @Resource 注入,整个项目保持统一哈;
-    private final SunoApi sunoApi;
-    private final AiMusicMapper musicMapper;
-
-    private final Queue<String> taskQueue = new ConcurrentLinkedQueue<>();
-
-    // TODO @xin:要不把 descriptionMode、lyricMode 合并,同一个 generateMusic 方法,然后根据传入的 mode 模式:歌词、描述来区分?
-
-    @Override
-    public List<Long> descriptionMode(SunoReqVO reqVO) {
-        // 1. 异步生成
-        SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental());
-        List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq);
-        // 2. 插入数据库
-        return insertMusicData(musicDataList);
-    }
-
-    @Override
-    public List<Long> lyricMode(SunoLyricModeVO reqVO) {
-        // 1. 异步生成
-        SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle());
-        List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq);
-        // 2. 插入数据库
-        return insertMusicData(musicDataList);
-    }
-
-    /**
-     * 新增音乐数据并提交 suno任务
-     *
-     * @param musicDataList 音乐数据列表
-     * @return 音乐id集合
-     */
-    private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList) {
-        if (CollUtil.isEmpty(musicDataList)) {
-            return Collections.emptyList();
-        }
-        // TODO @xin:建议使用 insertBatch 方法,批量插入
-        return AiMusicDO.convertFrom(musicDataList).stream()
-                .peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId())))
-                .peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add))
-                .map(AiMusicDO::getId)
-                .collect(Collectors.toList());
-    }
-
-    // TODO @xin:这个,改成标准的 job 来实现哈。从数据库加载任务,然后执行。
-    @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
-    @Transactional
-    public void flushSunoTask() {
-        if (CollUtil.isEmpty(taskQueue)) {
-            return;
-        }
-        CollUtil.split(taskQueue, 5).
-                stream().map(chunk -> CollUtil.join(chunk, StrPool.COMMA))
-                .forEach(taskIds -> {
-                    List<SunoApi.MusicData> musicData = sunoApi.selectById(taskIds);
-                    musicData.stream()
-                            .map(AiMusicDO::convertFrom)
-                            .forEach(musicDO -> {
-                                //更新音乐生成结果
-                                musicMapper.update(musicDO, Wrappers.<AiMusicDO>lambdaUpdate().eq(AiMusicDO::getTaskId, musicDO.getTaskId()));
-                                //完成后剔除任务
-                                if (Objects.equals(AiMusicStatusEnum.COMPLETE.getStatus(), musicDO.getStatus())) {
-                                    taskQueue.remove(musicDO.getTaskId());
-                                }
-                            });
-                });
-
-    }
-}

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

@@ -3,7 +3,6 @@ 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;
@@ -151,7 +150,7 @@ public class YudaoAiAutoConfiguration {
     @Bean
     @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true")
     public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
-        return new SunoApi(new SunoConfig(yudaoAiProperties.getSuno().getBaseUrl()));
+        return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
     }
 
     private static @NotNull MidjourneyConfig getMidjourneyConfig(ApplicationContext applicationContext,

+ 1 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java

@@ -22,6 +22,7 @@ public enum AiPlatformEnum {
 
     STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
     MIDJOURNEY("midjourney", "midjourney"), // TODO MJ 提供的绘图,接入中
+    SUNO("Suno", "Suno"), // Suno AI
     ;
 
     /**

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

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.suno;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-// TODO @xin:不需要这个类哈,直接 SunoApi 传入 baseUrl 参数即可
-/**
- * Suno 配置类
- *
- * @author  xiaoxin
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class SunoConfig {
-
-    /**
-     * suno-api服务的基本路径
-     */
-    private String baseUrl;
-
-}

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

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.framework.ai.core.model.suno.api;
 
-import cn.iocoder.yudao.framework.ai.core.model.suno.SunoConfig;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.text.StrPool;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpRequest;
 import org.springframework.http.HttpStatusCode;
 import org.springframework.http.MediaType;
 import org.springframework.web.reactive.function.client.ClientResponse;
@@ -17,11 +19,10 @@ import java.util.function.Predicate;
 
 /**
  * Suno API
- * <br>
+ * <b>
  * 文档地址:https://github.com/status2xx/suno-api/blob/main/README_CN.md
  *
- * @Author xiaoxin
- * @Date 2024/6/3
+ * @author xiaoxin
  */
 @Slf4j
 public class SunoApi {
@@ -29,86 +30,88 @@ public class SunoApi {
     private final WebClient webClient;
 
     private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
-    private final Function<ClientResponse, Mono<? extends Throwable>> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class)
+
+    private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION = reqParam -> response -> response.bodyToMono(String.class)
             .handle((respBody, sink) -> {
-                // TODO @xin:最好是 request、response 都有哈
-                log.error("【suno-api】调用失败!resp: 【{}】", respBody);
-                sink.error(new IllegalStateException("【suno-api】调用失败!"));
+                HttpRequest request = response.request();
+                log.error("[suno-api] 调用失败!请求方式:[{}], 请求地址:[{}], 请求参数:[{}], 响应数据: [{}]", request.getMethod(), request.getURI(), reqParam, respBody);
+                sink.error(new IllegalStateException("[suno-api] 调用失败!"));
             });
 
-    public SunoApi(SunoConfig config) {
+
+    public SunoApi(String baseUrl) {
         this.webClient = WebClient.builder()
-                .baseUrl(config.getBaseUrl())
+                .baseUrl(baseUrl)
                 .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON))
                 .build();
     }
 
-    public List<MusicData> generate(SunoRequest request) {
+    public List<MusicData> generate(MusicGenerateRequest request) {
         return this.webClient.post()
                 .uri("/api/generate")
-                .body(Mono.just(request), SunoRequest.class)
+                .body(Mono.just(request), MusicGenerateRequest.class)
                 .retrieve()
-                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
-                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
+                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
+                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
+                })
                 .block();
     }
 
-    public List<MusicData> customGenerate(SunoRequest request) {
+    public List<MusicData> customGenerate(MusicGenerateRequest request) {
         return this.webClient.post()
                 .uri("/api/custom_generate")
-                .body(Mono.just(request), SunoRequest.class)
+                .body(Mono.just(request), MusicGenerateRequest.class)
                 .retrieve()
-                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
-                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
+                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
+                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
+                })
                 .block();
     }
 
-    // TODO @xin: 是不是叫 chatCompletion
-    public List<MusicData> doChatCompletion(String prompt) {
+    public List<MusicData> chatCompletion(String prompt) {
         return this.webClient.post()
                 .uri("/v1/chat/completions")
-                .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class)
+                .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class)
                 .retrieve()
-                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
-                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
+                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt))
+                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
+                })
                 .block();
     }
 
     public LyricsData generateLyrics(String prompt) {
         return this.webClient.post()
                 .uri("/api/generate_lyrics")
-                .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class)
+                .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class)
                 .retrieve()
-                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
+                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt))
                 .bodyToMono(LyricsData.class)
                 .block();
     }
 
-    // TODO @xin:应该传入 List<String> ids
-    // TODO @xin:方法名,建议使用 getMusicList
-    public List<MusicData> selectById(String ids) {
+    public List<MusicData> getMusicList(List<String> ids) {
         return this.webClient.get()
                 .uri(uriBuilder -> uriBuilder
                         .path("/api/get")
-                        .queryParam("ids", ids)
+                        .queryParam("ids", CollUtil.join(ids, StrPool.COMMA))
                         .build())
                 .retrieve()
-                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
-                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
+                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(ids))
+                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
+                })
                 .block();
     }
 
-    // TODO @xin:方法名,建议使用 getLimitUsage
-    public LimitData selectLimit() {
+    public LimitUsageData getLimitUsage() {
         return this.webClient.get()
                 .uri("/api/get_limit")
                 .retrieve()
-                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
-                .bodyToMono(LimitData.class)
+                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(null))
+                .bodyToMono(LimitUsageData.class)
                 .block();
     }
 
-    // TODO @xin:可以改成 MusicGenerateRequest
+
     /**
      * 根据提示生成音频
      *
@@ -121,7 +124,7 @@ public class SunoApi {
      * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
      */
     @JsonInclude(value = JsonInclude.Include.NON_NULL)
-    public record SunoRequest(
+    public record MusicGenerateRequest(
             String prompt,
             String tags,
             String title,
@@ -130,15 +133,15 @@ public class SunoApi {
             @JsonProperty("make_instrumental") boolean makeInstrumental
     ) {
 
-        public SunoRequest(String prompt) {
+        public MusicGenerateRequest(String prompt) {
             this(prompt, null, null, null, false, false);
         }
 
-        public SunoRequest(String prompt, String mv, boolean makeInstrumental) {
+        public MusicGenerateRequest(String prompt, String mv, boolean makeInstrumental) {
             this(prompt, null, null, mv, false, makeInstrumental);
         }
 
-        public SunoRequest(String prompt, String mv, String tags, String title) {
+        public MusicGenerateRequest(String prompt, String mv, String tags, String title) {
             this(prompt, tags, title, mv, false, false);
         }
 
@@ -154,12 +157,12 @@ public class SunoApi {
      * @param audioUrl             音乐音频的 URL
      * @param videoUrl             音乐视频的 URL
      * @param createdAt            音乐音频的创建时间
-     * @param modelName
+     * @param modelName            模型名称
      * @param status               submitted、queued、streaming、complete
-     * @param gptDescriptionPrompt
+     * @param gptDescriptionPrompt 描述词
      * @param prompt               生成音乐音频的提示
-     * @param type
-     * @param tags
+     * @param type                 操作类型
+     * @param tags                 音乐类型标签
      */
     public record MusicData(
             String id,
@@ -195,7 +198,7 @@ public class SunoApi {
     /**
      * Suno API 响应的限额数据,目前每日免费50
      */
-    public record LimitData(
+    public record LimitUsageData(
             @JsonProperty("credits_left") Long creditsLeft,
             String period,
             @JsonProperty("monthly_limit") Long monthlyLimit,

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.framework.ai.suno;
 
-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;
@@ -17,26 +16,26 @@ public class SunoTests {
 
     @Before
     public void setup() {
-        String url = "https://suno-ix9nve79x-status2xxs-projects.vercel.app";
-        this.sunoApi = new SunoApi(new SunoConfig(url));
+        String url = "https://suno-imrqwwui8-status2xxs-projects.vercel.app";
+        this.sunoApi = new SunoApi(url);
     }
 
     @Test
     public void selectById() {
-        System.out.println(sunoApi.selectById("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551"));
+        System.out.println(sunoApi.getMusicList(List.of("d460ddda-7c87-4f34-b751-419b08a590ca,ff90ea66-49cd-4fd2-b44c-44267dfd5551")));
 
     }
 
     @Test
     public void generate() {
-        List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"));
+        List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.MusicGenerateRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"));
         System.out.println(generate);
     }
 
 
     @Test
     public void doChatCompletion() {
-        List<SunoApi.MusicData> generate = sunoApi.doChatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
+        List<SunoApi.MusicData> generate = sunoApi.chatCompletion("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。");
         System.out.println(generate);
     }
 
@@ -50,8 +49,8 @@ public class SunoTests {
 
     @Test
     public void selectLimit() {
-        SunoApi.LimitData limitData = sunoApi.selectLimit();
-        System.out.println(limitData);
+        SunoApi.LimitUsageData limitUsageData = sunoApi.getLimitUsage();
+        System.out.println(limitUsageData);
     }
 
 

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

@@ -199,7 +199,7 @@ yudao.ai:
     channel-id: 1237948819677904960
   suno:
     enable: true
-    base-url: https://suno-ix9nve79x-status2xxs-projects.vercel.app
+    base-url: https://suno-imrqwwui8-status2xxs-projects.vercel.app
 
 --- #################### 芋道相关配置 ####################