Commit 5ba38dbdaae93cd321bc71b1255796a48c0ef8a5

Authored by xiaoxin
1 parent 88142ed7

【解决todo】AI:音乐接入

yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateEnum.java
@@ -12,9 +12,8 @@ import lombok.Getter; @@ -12,9 +12,8 @@ import lombok.Getter;
12 @Getter 12 @Getter
13 public enum AiMusicGenerateEnum { 13 public enum AiMusicGenerateEnum {
14 14
15 - // TODO @xin:用数字哈。项目目前枚举都是数字  
16 - LYRIC("lyric", "歌词模式"),  
17 - DESCRIPTION("description", "描述模式"); 15 + LYRIC("1", "歌词模式"),
  16 + DESCRIPTION("2", "描述模式");
18 17
19 /** 18 /**
20 * 模式 19 * 模式
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
@@ -12,27 +12,18 @@ import lombok.Getter; @@ -12,27 +12,18 @@ import lombok.Getter;
12 @Getter 12 @Getter
13 public enum AiMusicStatusEnum { 13 public enum AiMusicStatusEnum {
14 14
15 - // TODO @xin:用数字哈。项目目前枚举都是数字  
16 // @xin 文档中无失败这个返回值 15 // @xin 文档中无失败这个返回值
17 - STREAMING("streaming", "进行中"),  
18 - COMPLETE("complete", "完成"); 16 + STREAMING("10", "进行中"),
  17 + COMPLETE("20", "完成");
19 18
20 /** 19 /**
21 * 状态 20 * 状态
22 */ 21 */
23 private final String status; 22 private final String status;
  23 +
24 /** 24 /**
25 * 状态名 25 * 状态名
26 */ 26 */
27 private final String name; 27 private final String name;
28 28
29 - public static AiMusicStatusEnum valueOfStatus(String status) {  
30 - for (AiMusicStatusEnum statusEnum : AiMusicStatusEnum.values()) {  
31 - if (statusEnum.getStatus().equals(status)) {  
32 - return statusEnum;  
33 - }  
34 - }  
35 - throw new IllegalArgumentException("未知会话状态: " + status);  
36 - }  
37 -  
38 } 29 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
1 package cn.iocoder.yudao.module.ai.controller.admin.music; 1 package cn.iocoder.yudao.module.ai.controller.admin.music;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; 4 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
5 import cn.iocoder.yudao.module.ai.service.music.AiMusicService; 5 import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
6 import io.swagger.v3.oas.annotations.Operation; 6 import io.swagger.v3.oas.annotations.Operation;
7 import io.swagger.v3.oas.annotations.tags.Tag; 7 import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import jakarta.annotation.Resource;
8 import jakarta.validation.Valid; 9 import jakarta.validation.Valid;
9 -import lombok.RequiredArgsConstructor;  
10 import org.springframework.web.bind.annotation.PostMapping; 10 import org.springframework.web.bind.annotation.PostMapping;
11 import org.springframework.web.bind.annotation.RequestBody; 11 import org.springframework.web.bind.annotation.RequestBody;
12 import org.springframework.web.bind.annotation.RequestMapping; 12 import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,16 +19,15 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -19,16 +19,15 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
19 @Tag(name = "管理后台 - AI 音乐") 19 @Tag(name = "管理后台 - AI 音乐")
20 @RestController 20 @RestController
21 @RequestMapping("/ai/music") 21 @RequestMapping("/ai/music")
22 -@RequiredArgsConstructor // TODO @xin:通过 @Resource 注入哈  
23 public class AiMusicController { 22 public class AiMusicController {
24 23
25 - // TODO @xin:变量不用有 ai 前缀  
26 - private final AiMusicService aiMusicService; 24 + @Resource
  25 + private AiMusicService musicService;
27 26
28 @PostMapping("/generate") 27 @PostMapping("/generate")
29 @Operation(summary = "音乐生成") 28 @Operation(summary = "音乐生成")
30 - public CommonResult<List<Long>> generateMusic(@RequestBody @Valid SunoReqVO sunoReqVO) {  
31 - return success(aiMusicService.generateMusic(sunoReqVO)); 29 + public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
  30 + return success(musicService.generateMusic(reqVO));
32 } 31 }
33 32
34 } 33 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +/**
  10 + * @author xiaoxin
  11 + */
  12 +@Schema(description = "管理后台 - 音乐生成 Request VO")
  13 +@Data
  14 +public class AiSunoGenerateReqVO {
  15 +
  16 + @Schema(description = "用于生成音乐音频的提示", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
  17 + private String prompt;
  18 +
  19 + @Schema(description = "是否纯音乐", example = "true")
  20 + private Boolean makeInstrumental;
  21 +
  22 + @Schema(description = "模型版本, 默认 chirp-v3.5", example = "chirp-v3.5")
  23 + private String modelVersion;// 参见 AiModelEnum 枚举
  24 +
  25 + @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
  26 + private List<String> tags;
  27 +
  28 + @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星")
  29 + private String title;
  30 +
  31 + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
  32 + @NotBlank(message = "平台不能为空")
  33 + private String platform;// 参见 AiPlatformEnum 枚举
  34 +
  35 + @Schema(description = "生成模式 1(歌词模式), 2(描述模式)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  36 + @NotBlank(message = "生成模式不能为空")
  37 + private String generateMode;// 参见 AiMusicGenerateEnum 枚举
  38 +
  39 +}
0 \ No newline at end of file 40 \ 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 deleted
1 -package cn.iocoder.yudao.module.ai.controller.admin.music.vo;  
2 -  
3 -import io.swagger.v3.oas.annotations.media.Schema;  
4 -import jakarta.validation.constraints.NotBlank;  
5 -import lombok.Data;  
6 -  
7 -import java.util.List;  
8 -  
9 -// TODO @xin:1)ai 前缀;2)AiSunoGenerateReqVO,要有生成哈。3)swaggger 缺少的属性,也最好加下,类似 example,类上的 swagger 等  
10 -/**  
11 - * @author xiaoxin  
12 - */  
13 -@Data  
14 -public class SunoReqVO {  
15 -  
16 - @Schema(description = "用于生成音乐音频的提示")  
17 - private String prompt;  
18 -  
19 - @Schema(description = "是否纯音乐")  
20 - private Boolean makeInstrumental;  
21 -  
22 - // TODO @xin:我们自己是不是用 modelVersion?还是什么梗精准,减少非必要的缩写  
23 - @Schema(description = "模型版本")  
24 - private String mv;  
25 -  
26 - @Schema(description = "音乐风格")  
27 - private List<String> tags;  
28 -  
29 - @Schema(description = "音乐/歌曲名称")  
30 - private String title;  
31 -  
32 - @Schema(description = "平台")  
33 - @NotBlank(message = "平台不能为空")  
34 - private String platform;  
35 -  
36 - @Schema(description = "生成模式 lyric(歌词模式), description(描述模式)")  
37 - @NotBlank(message = "生成模式不能为空")  
38 - private String generateMode;  
39 -  
40 -}  
41 \ No newline at end of file 0 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
1 package cn.iocoder.yudao.module.ai.dal.dataobject.music; 1 package cn.iocoder.yudao.module.ai.dal.dataobject.music;
2 2
3 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;  
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; 4 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
6 import com.baomidou.mybatisplus.annotation.IdType; 5 import com.baomidou.mybatisplus.annotation.IdType;
7 import com.baomidou.mybatisplus.annotation.TableField; 6 import com.baomidou.mybatisplus.annotation.TableField;
8 import com.baomidou.mybatisplus.annotation.TableId; 7 import com.baomidou.mybatisplus.annotation.TableId;
9 import com.baomidou.mybatisplus.annotation.TableName; 8 import com.baomidou.mybatisplus.annotation.TableName;
10 -import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; 9 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
11 import lombok.Data; 10 import lombok.Data;
12 11
13 import java.util.List; 12 import java.util.List;
14 13
15 -// TODO @xin:注释完善下 14 +
16 /** 15 /**
17 - * @Author xiaoxin  
18 - * @Date 2024/6/5 16 + * AI 音乐 DO
  17 + *
  18 + * @author xiaoxin
19 */ 19 */
20 @TableName("ai_music") 20 @TableName("ai_music")
21 @Data 21 @Data
@@ -98,25 +98,11 @@ public class AiMusicDO extends BaseDO { @@ -98,25 +98,11 @@ public class AiMusicDO extends BaseDO {
98 /** 98 /**
99 * 音乐风格标签 99 * 音乐风格标签
100 */ 100 */
101 - @TableField(typeHandler = AiMusicTagsHandler.class) 101 + @TableField(typeHandler = JacksonTypeHandler.class)
102 private List<String> tags; 102 private List<String> tags;
103 103
104 /** 104 /**
105 * 任务编号 105 * 任务编号
106 */ 106 */
107 private String taskId; 107 private String taskId;
108 -  
109 - // TODO @xin:用 @TableField(typeHandler = JacksonTypeHandler.class) 替代即可  
110 - public static class AiMusicTagsHandler extends AbstractJsonTypeHandler<Object> {  
111 -  
112 - @Override  
113 - protected Object parse(String json) {  
114 - return JsonUtils.parseArray(json, String.class);  
115 - }  
116 -  
117 - @Override  
118 - protected String toJson(Object obj) {  
119 - return JsonUtils.toJsonString(obj);  
120 - }  
121 - }  
122 } 108 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/SunoJob.java
1 package cn.iocoder.yudao.module.ai.job; 1 package cn.iocoder.yudao.module.ai.job;
2 2
3 -import cn.hutool.core.collection.CollUtil;  
4 -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;  
5 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; 3 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
6 -import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;  
7 -import cn.iocoder.yudao.module.ai.service.music.AiMusicConvert;  
8 import cn.iocoder.yudao.module.ai.service.music.AiMusicService; 4 import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
9 import jakarta.annotation.Resource; 5 import jakarta.annotation.Resource;
10 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
11 import org.springframework.stereotype.Component; 7 import org.springframework.stereotype.Component;
12 8
13 -import java.util.ArrayList;  
14 -import java.util.HashMap;  
15 -import java.util.List;  
16 -import java.util.Map;  
17 9
18 -// TODO @xin:不要直接叫这个名字哈,要有它的目的  
19 /** 10 /**
20 - * 处理 Suno Job 11 + * 同步 Suno 任务状态以及回写对应的音乐信息 Job
  12 + *
21 * @author xiaoxin 13 * @author xiaoxin
22 */ 14 */
23 @Component 15 @Component
@@ -25,37 +17,12 @@ import java.util.Map; @@ -25,37 +17,12 @@ import java.util.Map;
25 public class SunoJob implements JobHandler { 17 public class SunoJob implements JobHandler {
26 18
27 @Resource 19 @Resource
28 - private SunoApi sunoApi;  
29 - @Resource  
30 private AiMusicService musicService; 20 private AiMusicService musicService;
31 21
32 @Override 22 @Override
33 public String execute(String param) { 23 public String execute(String param) {
34 - // TODO @xin:可以考虑,整个逻辑都下沉到 Service 里,有点类似 AccessLogCleanJob  
35 - List<AiMusicDO> unCompletedTask = musicService.getUnCompletedTask();  
36 -  
37 - if (CollUtil.isEmpty(unCompletedTask)) {  
38 - log.info("Suno 无进行中任务需要更新!");  
39 - return "Suno 无进行中任务需要更新!";  
40 - }  
41 -  
42 -  
43 - log.info("Suno 开始同步, 共 [{}] 个任务!", unCompletedTask.size());  
44 - //GET 请求,为避免参数过长,分批次处理  
45 - CollUtil.split(unCompletedTask, 4)  
46 - .forEach(chunk -> {  
47 - Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId);  
48 - List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));  
49 - if (CollUtil.isNotEmpty(musicTaskList)) {  
50 - List<AiMusicDO> aiMusicDOS = AiMusicConvert.convertFrom(musicTaskList);  
51 - //回填id  
52 - aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId())));  
53 - musicService.updateBatch(aiMusicDOS);  
54 - } else {  
55 - log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet());  
56 - }  
57 - });  
58 -  
59 - return "Suno 同步 - ".concat(String.valueOf(unCompletedTask.size())).concat(" 个任务!"); 24 + Integer count = musicService.syncMusicTask();
  25 + log.info("[execute][Suno 同步任务数量 [{}] 个]", count);
  26 + return String.format("Suno 同步 - [%s] 个任务", count);
60 } 27 }
61 } 28 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicConvert.java deleted
1 -package cn.iocoder.yudao.module.ai.service.music;  
2 -  
3 -import cn.hutool.core.text.StrPool;  
4 -import cn.hutool.core.util.StrUtil;  
5 -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;  
6 -import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;  
7 -  
8 -import java.util.List;  
9 -import java.util.stream.Collectors;  
10 -  
11 -/**  
12 - * AI 音乐 Convert  
13 - *  
14 - * @author xiaoxin  
15 - */  
16 -public class AiMusicConvert {  
17 -  
18 - public static AiMusicDO convertFrom(SunoApi.MusicData musicData) {  
19 - return new AiMusicDO()  
20 - .setTaskId(musicData.id())  
21 - .setPrompt(musicData.prompt())  
22 - .setGptDescriptionPrompt(musicData.gptDescriptionPrompt())  
23 - .setAudioUrl(musicData.audioUrl())  
24 - .setVideoUrl(musicData.videoUrl())  
25 - .setImageUrl(musicData.imageUrl())  
26 - .setLyric(musicData.lyric())  
27 - .setTitle(musicData.title())  
28 - .setStatus(musicData.status())  
29 - .setModel(musicData.modelName())  
30 - .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null);  
31 - }  
32 -  
33 - // TODO @xin:一般情况下,不用 convert,直接逻辑里 convert 就好啦。  
34 - public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> list) {  
35 - // TODO @xin:可以使用 CollectionUtils.convertList 简洁一点  
36 - return list.stream()  
37 - .map(AiMusicConvert::convertFrom)  
38 - .collect(Collectors.toList());  
39 - }  
40 -  
41 -  
42 -}  
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
1 package cn.iocoder.yudao.module.ai.service.music; 1 package cn.iocoder.yudao.module.ai.service.music;
2 2
3 -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; 3 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
4 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; 4 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
5 5
6 import java.util.List; 6 import java.util.List;
@@ -18,7 +18,7 @@ public interface AiMusicService { @@ -18,7 +18,7 @@ public interface AiMusicService {
18 * @param reqVO 请求参数 18 * @param reqVO 请求参数
19 * @return 生成的音乐ID 19 * @return 生成的音乐ID
20 */ 20 */
21 - List<Long> generateMusic(SunoReqVO reqVO); 21 + List<Long> generateMusic(AiSunoGenerateReqVO reqVO);
22 22
23 /** 23 /**
24 * 获取未完成状态的任务 24 * 获取未完成状态的任务
@@ -27,6 +27,20 @@ public interface AiMusicService { @@ -27,6 +27,20 @@ public interface AiMusicService {
27 */ 27 */
28 List<AiMusicDO> getUnCompletedTask(); 28 List<AiMusicDO> getUnCompletedTask();
29 29
30 - Boolean updateBatch(List<AiMusicDO> aiMusicDOList); 30 +
  31 + /**
  32 + * 同步音乐任务
  33 + *
  34 + * @return 同步数量
  35 + */
  36 + Integer syncMusicTask();
  37 +
  38 + /**
  39 + * 批量更新音乐信息
  40 + *
  41 + * @param musicDOS 音乐信息
  42 + * @return 是否成功
  43 + */
  44 + Boolean updateBatch(List<AiMusicDO> musicDOS);
31 45
32 } 46 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
@@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.ai.service.music; @@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.ai.service.music;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.text.StrPool; 4 import cn.hutool.core.text.StrPool;
  5 +import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
6 -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO; 7 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
7 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; 9 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
8 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; 10 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
9 import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum; 11 import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateEnum;
@@ -13,8 +15,7 @@ import jakarta.annotation.Resource; @@ -13,8 +15,7 @@ import jakarta.annotation.Resource;
13 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
14 import org.springframework.stereotype.Service; 16 import org.springframework.stereotype.Service;
15 17
16 -import java.util.Collections;  
17 -import java.util.List; 18 +import java.util.*;
18 import java.util.stream.Collectors; 19 import java.util.stream.Collectors;
19 20
20 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 21 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -35,13 +36,22 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -35,13 +36,22 @@ public class AiMusicServiceImpl implements AiMusicService {
35 private AiMusicMapper musicMapper; 36 private AiMusicMapper musicMapper;
36 37
37 @Override 38 @Override
38 - public List<Long> generateMusic(SunoReqVO reqVO) {  
39 - // TODO @xin:是不是可以 if else 调用对应的 API,然后 insertMusicData 逻辑  
40 - AiMusicGenerateEnum generateEnum = AiMusicGenerateEnum.valueOfMode(reqVO.getGenerateMode());  
41 - return switch (generateEnum) {  
42 - case DESCRIPTION -> descriptionMode(reqVO);  
43 - case LYRIC -> lyricMode(reqVO);  
44 - }; 39 + public List<Long> generateMusic(AiSunoGenerateReqVO reqVO) {
  40 + List<SunoApi.MusicData> musicDataList;
  41 + if (Objects.equals(AiMusicGenerateEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
  42 + //歌词模式
  43 + SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
  44 + musicDataList = sunoApi.customGenerate(sunoReq);
  45 + } else if (Objects.equals(AiMusicGenerateEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
  46 + //描述模式
  47 + SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental());
  48 + musicDataList = sunoApi.generate(sunoReq);
  49 + } else {
  50 + log.error("未知的生成模式:{}", reqVO.getGenerateMode());
  51 + throw new IllegalArgumentException("未知的生成模式");
  52 + }
  53 + // 2. 插入数据库
  54 + return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform());
45 } 55 }
46 56
47 @Override 57 @Override
@@ -50,36 +60,33 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -50,36 +60,33 @@ public class AiMusicServiceImpl implements AiMusicService {
50 } 60 }
51 61
52 @Override 62 @Override
53 - public Boolean updateBatch(List<AiMusicDO> aiMusicDOList) {  
54 - return musicMapper.updateBatch(aiMusicDOList);  
55 - }  
56 -  
57 - /**  
58 - * 描述模式生成音乐  
59 - *  
60 - * @param reqVO 请求参数  
61 - * @return 生成的音乐ID集合  
62 - */  
63 - public List<Long> descriptionMode(SunoReqVO reqVO) {  
64 - // 1. 异步生成  
65 - SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getMakeInstrumental());  
66 - List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq);  
67 - // 2. 插入数据库  
68 - return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); 63 + public Integer syncMusicTask() {
  64 + List<AiMusicDO> unCompletedTask = this.getUnCompletedTask();
  65 + if (CollUtil.isEmpty(unCompletedTask)) {
  66 + log.info("Suno 无进行中任务需要更新!");
  67 + return 0;
  68 + }
  69 + log.info("Suno 开始同步, 共 [{}] 个任务!", unCompletedTask.size());
  70 + //GET 请求,为避免参数过长,分批次处理
  71 + CollUtil.split(unCompletedTask, 4)
  72 + .forEach(chunk -> {
  73 + Map<String, Long> taskIdMap = CollUtil.toMap(chunk, new HashMap<>(), AiMusicDO::getTaskId, AiMusicDO::getId);
  74 + List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
  75 + if (CollUtil.isNotEmpty(musicTaskList)) {
  76 + List<AiMusicDO> aiMusicDOS = buildMusicDOList(musicTaskList);
  77 + //回填id
  78 + aiMusicDOS.forEach(aiMusicDO -> aiMusicDO.setId(taskIdMap.get(aiMusicDO.getTaskId())));
  79 + this.updateBatch(aiMusicDOS);
  80 + } else {
  81 + log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet());
  82 + }
  83 + });
  84 + return unCompletedTask.size();
69 } 85 }
70 86
71 - /**  
72 - * 歌词模式生成音乐  
73 - *  
74 - * @param reqVO 请求参数  
75 - * @return 生成的音乐ID集合  
76 - */  
77 - public List<Long> lyricMode(SunoReqVO reqVO) {  
78 - // 1. 异步生成  
79 - SunoApi.MusicGenerateRequest sunoReq = new SunoApi.MusicGenerateRequest(reqVO.getPrompt(), reqVO.getMv(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());  
80 - List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq);  
81 - // 2. 插入数据库  
82 - return insertMusicData(musicDataList, reqVO.getGenerateMode(), reqVO.getPlatform()); 87 + @Override
  88 + public Boolean updateBatch(List<AiMusicDO> musicDOS) {
  89 + return musicMapper.updateBatch(musicDOS);
83 } 90 }
84 91
85 /** 92 /**
@@ -92,7 +99,7 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -92,7 +99,7 @@ public class AiMusicServiceImpl implements AiMusicService {
92 if (CollUtil.isEmpty(musicDataList)) { 99 if (CollUtil.isEmpty(musicDataList)) {
93 return Collections.emptyList(); 100 return Collections.emptyList();
94 } 101 }
95 - List<AiMusicDO> aiMusicDOList = AiMusicConvert.convertFrom(musicDataList).stream() 102 + List<AiMusicDO> aiMusicDOList = buildMusicDOList(musicDataList).stream()
96 .map(musicDO -> musicDO.setUserId(getLoginUserId()) 103 .map(musicDO -> musicDO.setUserId(getLoginUserId())
97 .setGenerateMode(generateMode) 104 .setGenerateMode(generateMode)
98 .setPlatform(platform)) 105 .setPlatform(platform))
@@ -103,4 +110,24 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -103,4 +110,24 @@ public class AiMusicServiceImpl implements AiMusicService {
103 .collect(Collectors.toList()); 110 .collect(Collectors.toList());
104 } 111 }
105 112
  113 + /**
  114 + * 构建 AiMusicDO 集合
  115 + *
  116 + * @param musicTaskList suno 音乐任务列表
  117 + * @return AiMusicDO 集合
  118 + */
  119 + private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicTaskList) {
  120 + return CollectionUtils.convertList(musicTaskList, musicData -> new AiMusicDO()
  121 + .setTaskId(musicData.id())
  122 + .setPrompt(musicData.prompt())
  123 + .setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
  124 + .setAudioUrl(musicData.audioUrl())
  125 + .setVideoUrl(musicData.videoUrl())
  126 + .setImageUrl(musicData.imageUrl())
  127 + .setLyric(musicData.lyric())
  128 + .setTitle(musicData.title())
  129 + .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.COMPLETE.getStatus() : AiMusicStatusEnum.STREAMING.getStatus())
  130 + .setModel(musicData.modelName())
  131 + .setTags(StrUtil.isNotBlank(musicData.tags()) ? List.of(musicData.tags().split(StrPool.COMMA)) : null));
  132 + }
106 } 133 }