Commit 4a6a027beb09bf3f3f401ba46bcbaf199899ca21

Authored by xiaoxin
1 parent f300b8a1

【解决todo】AI 音乐:1.处理失败任务 2.两种生成类型提示词合并,库中区分

yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
... ... @@ -16,7 +16,8 @@ import java.util.Arrays;
16 16 public enum AiMusicStatusEnum implements IntArrayValuable {
17 17  
18 18 IN_PROGRESS(10, "进行中"),
19   - SUCCESS(20, "已完成");
  19 + SUCCESS(20, "已完成"),
  20 + FAIL(30, "已失败");
20 21  
21 22 /**
22 23 * 状态
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
... ... @@ -38,10 +38,6 @@ public class AiMusicController {
38 38 @PostMapping("/generate")
39 39 @Operation(summary = "音乐生成")
40 40 public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
41   - if (true) {
42   - musicService.syncMusic();
43   - return null;
44   - }
45 41 return success(musicService.generateMusic(getLoginUserId(), reqVO));
46 42 }
47 43  
... ... @@ -64,12 +60,11 @@ public class AiMusicController {
64 60 return success(BeanUtils.toBean(music, AiMusicRespVO.class));
65 61 }
66 62  
67   - // TODO @xin:这个搞成 updateMy ,修改【我的】音乐。方便后续支持其它字段;另外,需要校验下,更新的音乐,是不是我的!
68   - @PostMapping("/updateTitle-my")
  63 + @PostMapping("/update-my")
69 64 @Operation(summary = "修改【我的】音乐 目前只支持修改标题")
70 65 @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星")
71   - public CommonResult<Boolean> updateMusicTitle(AiMusicUpdateTitleReqVO updateReqVO) {
72   - musicService.updateMusicTitle(updateReqVO);
  66 + public CommonResult<Boolean> updateMy(AiMusicUpdateReqVO updateReqVO) {
  67 + musicService.updateMyMusic(updateReqVO, getLoginUserId());
73 68 return success(true);
74 69 }
75 70  
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java
... ... @@ -15,4 +15,7 @@ public class AiMusicUpdateReqVO {
15 15 @Schema(description = "是否发布", example = "true")
16 16 private Boolean publicStatus;
17 17  
  18 + @Schema(description = "音乐名称", example = "夜空中最亮的星")
  19 + private String title;
  20 +
18 21 }
19 22 \ 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/AiMusicUpdateTitleReqVO.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.NotNull;
5   -import lombok.Data;
6   -
7   -@Schema(description = "管理后台 - AI 音乐修改名称 Request VO")
8   -@Data
9   -public class AiMusicUpdateTitleReqVO {
10   -
11   - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
12   - private Long id;
13   -
14   - @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星")
15   - @NotNull(message = "音乐名称不能为空")
16   - private String title;
17   -
18   -}
19 0 \ 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/AiSunoGenerateReqVO.java
... ... @@ -24,10 +24,10 @@ public class AiSunoGenerateReqVO {
24 24 @NotNull(message = "生成模式不能为空")
25 25 private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举
26 26  
27   - // TODO @xin:方案一:prompt => lyric 歌词;gptDescriptionPrompt => description 描述(db 那字段也改下,避免和 gpt 直接耦合);这样搞完后,会更统一好理解一点
28   - // TODO @xin:方案二:还是之前的做法,都用 prompt;不过最终 gptDescriptionPrompt 还是存储 description 算描述。可以微信一起讨论下。
29 27 @Schema(description = "用于生成音乐音频的歌词提示",
30 28 example = """
  29 + 1.描述模式:创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。
  30 + 2.歌词模式:
31 31 [Verse]
32 32 阳光下奔跑 多么欢快
33 33 假期就要来 心都飞起来
... ... @@ -39,11 +39,7 @@ public class AiSunoGenerateReqVO {
39 39 日子太短暂 别再等待
40 40 马上放假了 梦想起飞
41 41 """)
42   - private String prompt; // 歌词模式用
43   -
44   - @Schema(description = "用于生成音乐音频的描述",
45   - example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
46   - private String gptDescriptionPrompt; // 描述模式用
  42 + private String prompt;
47 43  
48 44 @Schema(description = "是否纯音乐", example = "true")
49 45 private Boolean makeInstrumental;
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
... ... @@ -75,11 +75,7 @@ public class AiMusicDO extends BaseDO {
75 75 /**
76 76 * 描述词
77 77 */
78   - private String gptDescriptionPrompt;
79   - /**
80   - * 提示词
81   - */
82   - private String prompt;
  78 + private String description;
83 79  
84 80 /**
85 81 * 平台
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
... ... @@ -38,11 +38,11 @@ public interface AiMusicService {
38 38 void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO);
39 39  
40 40 /**
41   - * 更新音乐名称
  41 + * 更新我的音乐
42 42 *
43 43 * @param updateReqVO 更新信息
44 44 */
45   - void updateMusicTitle(@Valid AiMusicUpdateTitleReqVO updateReqVO);
  45 + void updateMyMusic(@Valid AiMusicUpdateReqVO updateReqVO, Long userId);
46 46  
47 47 /**
48 48 * 删除AI 音乐
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
... ... @@ -3,11 +3,14 @@ package cn.iocoder.yudao.module.ai.service.music;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.text.StrPool;
5 5 import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.ObjectUtil;
6 7 import cn.hutool.core.util.StrUtil;
7 8 import cn.hutool.http.HttpUtil;
8 9 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
9 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10   -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*;
  11 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
  12 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO;
  13 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
11 14 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
12 15 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
13 16 import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
... ... @@ -48,18 +51,18 @@ public class AiMusicServiceImpl implements AiMusicService {
48 51 public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
49 52 // 1. 调用 Suno 生成音乐
50 53 SunoApi sunoApi = apiKeyService.getSunoApi();
51   - // TODO @xin:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的
  54 + // TODO 芋艿:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的 --xin:大部分ok的,补充了error_message
52 55 List<SunoApi.MusicData> musicDataList;
53   - if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
54   - // 1.1 歌词模式
  56 + if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
  57 + // 1.1 描述模式
  58 + SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
  59 + reqVO.getPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental());
  60 + musicDataList = sunoApi.generate(generateRequest);
  61 + } else if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
  62 + // 1.2 歌词模式
55 63 SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
56 64 reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
57 65 musicDataList = sunoApi.customGenerate(generateRequest);
58   - } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
59   - // 1.2 描述模式
60   - SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
61   - reqVO.getGptDescriptionPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental());
62   - musicDataList = sunoApi.generate(generateRequest);
63 66 } else {
64 67 throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO));
65 68 }
... ... @@ -108,9 +111,12 @@ public class AiMusicServiceImpl implements AiMusicService {
108 111 }
109 112  
110 113 @Override
111   - public void updateMusicTitle(AiMusicUpdateTitleReqVO updateReqVO) {
112   - // 校验存在
113   - validateMusicExists(updateReqVO.getId());
  114 + public void updateMyMusic(AiMusicUpdateReqVO updateReqVO, Long userId) {
  115 + // 校验音乐是否存在
  116 + AiMusicDO musicDO = validateMusicExists(updateReqVO.getId());
  117 + if (ObjUtil.notEqual(musicDO.getUserId(), userId)) {
  118 + throw exception(MUSIC_NOT_EXISTS);
  119 + }
114 120 // 更新
115 121 musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setTitle(updateReqVO.getTitle()));
116 122 }
... ... @@ -156,29 +162,38 @@ public class AiMusicServiceImpl implements AiMusicService {
156 162 * @return AiMusicDO 集合
157 163 */
158 164 private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
159   - // TODO @xin:它有 status = error 状态,表示失败噢。
160   - return convertList(musicList, musicData -> new AiMusicDO()
161   - .setTaskId(musicData.id()).setModel(musicData.modelName())
162   - .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
163   - // TODO @xin:只有在完成的状态,在下载文件
164   - .setAudioUrl(downloadFile(musicData.audioUrl()))
165   - .setVideoUrl(downloadFile(musicData.videoUrl()))
166   - .setImageUrl(downloadFile(musicData.imageUrl()))
167   - .setTitle(musicData.title()).setDuration(musicData.duration())
168   - .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
169   - .setStatus(Objects.equals("complete", musicData.status()) ?
170   - AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
  165 + return convertList(musicList, musicData -> {
  166 + Integer status;
  167 + if (Objects.equals("complete", musicData.status())) {
  168 + status = AiMusicStatusEnum.SUCCESS.getStatus();
  169 + } else if (Objects.equals("error", musicData.status())) {
  170 + status = AiMusicStatusEnum.FAIL.getStatus();
  171 + } else {
  172 + status = AiMusicStatusEnum.IN_PROGRESS.getStatus();
  173 + }
  174 + return new AiMusicDO()
  175 + .setTaskId(musicData.id()).setModel(musicData.modelName())
  176 + .setDescription(musicData.gptDescriptionPrompt())
  177 + .setAudioUrl(downloadFile(status, musicData.audioUrl()))
  178 + .setVideoUrl(downloadFile(status, musicData.videoUrl()))
  179 + .setImageUrl(downloadFile(status, musicData.imageUrl()))
  180 + .setTitle(musicData.title()).setDuration(musicData.duration())
  181 + .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
  182 + .setErrorMessage(musicData.errorMessage())
  183 + .setStatus(status);
  184 + });
171 185 }
172 186  
173 187 /**
174   - * 将生成的音频文件上传到文件服务器
  188 + * 音乐生成好后,将音频文件上传到文件服务器
175 189 *
176   - * @param url 音频文件地址
  190 + * @param status 音乐状态
  191 + * @param url 音频文件地址
177 192 * @return 内部文件地址
178 193 */
179   - private String downloadFile(String url) {
180   - if (StrUtil.isBlank(url)) {
181   - return null;
  194 + private String downloadFile(Integer status, String url) {
  195 + if (StrUtil.isBlank(url) || ObjectUtil.notEqual(status, AiMusicStatusEnum.SUCCESS.getStatus())) {
  196 + return url;
182 197 }
183 198 try {
184 199 byte[] bytes = HttpUtil.downloadBytes(url);
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java
... ... @@ -175,6 +175,7 @@ public class SunoApi {
175 175 @JsonProperty("model_name") String modelName,
176 176 String status,
177 177 @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt,
  178 + @JsonProperty("error_message") String errorMessage,
178 179 String prompt,
179 180 String type,
180 181 String tags,
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java
... ... @@ -17,8 +17,8 @@ public class SunoTests {
17 17  
18 18 @Before
19 19 public void setup() {
20   -// String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app";
21   - String url = "http://127.0.0.1:3001";
  20 + String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app";
  21 +// String url = "http://127.0.0.1:3001";
22 22 this.sunoApi = new SunoApi(url);
23 23 }
24 24  
... ...