mirror of
				https://github.com/YunaiV/ruoyi-vue-pro.git
				synced 2025-10-31 02:28:03 +08:00 
			
		
		
		
	!8 【解决todo】AI 音乐:1.处理失败任务 2.两种生成类型提示词合并,库中区分
Merge pull request !8 from 小新/master-jdk21-ai
This commit is contained in:
		| @ -16,7 +16,8 @@ import java.util.Arrays; | |||||||
| public enum AiMusicStatusEnum implements IntArrayValuable { | public enum AiMusicStatusEnum implements IntArrayValuable { | ||||||
|  |  | ||||||
|     IN_PROGRESS(10, "进行中"), |     IN_PROGRESS(10, "进行中"), | ||||||
|     SUCCESS(20, "已完成"); |     SUCCESS(20, "已完成"), | ||||||
|  |     FAIL(30, "已失败"); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 状态 |      * 状态 | ||||||
|  | |||||||
| @ -38,10 +38,6 @@ public class AiMusicController { | |||||||
|     @PostMapping("/generate") |     @PostMapping("/generate") | ||||||
|     @Operation(summary = "音乐生成") |     @Operation(summary = "音乐生成") | ||||||
|     public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) { |     public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) { | ||||||
|         if (true) { |  | ||||||
|             musicService.syncMusic(); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         return success(musicService.generateMusic(getLoginUserId(), reqVO)); |         return success(musicService.generateMusic(getLoginUserId(), reqVO)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -64,12 +60,11 @@ public class AiMusicController { | |||||||
|         return success(BeanUtils.toBean(music, AiMusicRespVO.class)); |         return success(BeanUtils.toBean(music, AiMusicRespVO.class)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO @xin:这个搞成 updateMy ,修改【我的】音乐。方便后续支持其它字段;另外,需要校验下,更新的音乐,是不是我的! |     @PostMapping("/update-my") | ||||||
|     @PostMapping("/updateTitle-my") |  | ||||||
|     @Operation(summary = "修改【我的】音乐 目前只支持修改标题") |     @Operation(summary = "修改【我的】音乐 目前只支持修改标题") | ||||||
|     @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") |     @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") | ||||||
|     public CommonResult<Boolean> updateMusicTitle(AiMusicUpdateTitleReqVO updateReqVO) { |     public CommonResult<Boolean> updateMy(AiMusicUpdateReqVO updateReqVO) { | ||||||
|         musicService.updateMusicTitle(updateReqVO); |         musicService.updateMyMusic(updateReqVO, getLoginUserId()); | ||||||
|         return success(true); |         return success(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -15,4 +15,7 @@ public class AiMusicUpdateReqVO { | |||||||
|     @Schema(description = "是否发布", example = "true") |     @Schema(description = "是否发布", example = "true") | ||||||
|     private Boolean publicStatus; |     private Boolean publicStatus; | ||||||
|  |  | ||||||
|  |     @Schema(description = "音乐名称", example = "夜空中最亮的星") | ||||||
|  |     private String title; | ||||||
|  |  | ||||||
| } | } | ||||||
| @ -1,18 +0,0 @@ | |||||||
| package cn.iocoder.yudao.module.ai.controller.admin.music.vo; |  | ||||||
|  |  | ||||||
| import io.swagger.v3.oas.annotations.media.Schema; |  | ||||||
| import jakarta.validation.constraints.NotNull; |  | ||||||
| import lombok.Data; |  | ||||||
|  |  | ||||||
| @Schema(description = "管理后台 - AI 音乐修改名称 Request VO") |  | ||||||
| @Data |  | ||||||
| public class AiMusicUpdateTitleReqVO { |  | ||||||
|  |  | ||||||
|     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") |  | ||||||
|     private Long id; |  | ||||||
|  |  | ||||||
|     @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星") |  | ||||||
|     @NotNull(message = "音乐名称不能为空") |  | ||||||
|     private String title; |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @ -24,10 +24,10 @@ public class AiSunoGenerateReqVO { | |||||||
|     @NotNull(message = "生成模式不能为空") |     @NotNull(message = "生成模式不能为空") | ||||||
|     private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举 |     private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举 | ||||||
|  |  | ||||||
|     // TODO @xin:方案一:prompt => lyric 歌词;gptDescriptionPrompt => description 描述(db 那字段也改下,避免和 gpt 直接耦合);这样搞完后,会更统一好理解一点 |  | ||||||
|     // TODO @xin:方案二:还是之前的做法,都用 prompt;不过最终 gptDescriptionPrompt 还是存储 description 算描述。可以微信一起讨论下。 |  | ||||||
|     @Schema(description = "用于生成音乐音频的歌词提示", |     @Schema(description = "用于生成音乐音频的歌词提示", | ||||||
|             example = """ |             example = """ | ||||||
|  |                     1.描述模式:创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。 | ||||||
|  |                     2.歌词模式: | ||||||
|                     [Verse] |                     [Verse] | ||||||
|                     阳光下奔跑 多么欢快 |                     阳光下奔跑 多么欢快 | ||||||
|                     假期就要来 心都飞起来 |                     假期就要来 心都飞起来 | ||||||
| @ -39,11 +39,7 @@ public class AiSunoGenerateReqVO { | |||||||
|                     日子太短暂 别再等待 |                     日子太短暂 别再等待 | ||||||
|                     马上放假了 梦想起飞 |                     马上放假了 梦想起飞 | ||||||
|                     """) |                     """) | ||||||
|     private String prompt; // 歌词模式用 |     private String prompt; | ||||||
|  |  | ||||||
|     @Schema(description = "用于生成音乐音频的描述", |  | ||||||
|             example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") |  | ||||||
|     private String gptDescriptionPrompt; // 描述模式用 |  | ||||||
|  |  | ||||||
|     @Schema(description = "是否纯音乐", example = "true") |     @Schema(description = "是否纯音乐", example = "true") | ||||||
|     private Boolean makeInstrumental; |     private Boolean makeInstrumental; | ||||||
|  | |||||||
| @ -75,11 +75,7 @@ public class AiMusicDO extends BaseDO { | |||||||
|     /** |     /** | ||||||
|      * 描述词 |      * 描述词 | ||||||
|      */ |      */ | ||||||
|     private String gptDescriptionPrompt; |     private String description; | ||||||
|     /** |  | ||||||
|      * 提示词 |  | ||||||
|      */ |  | ||||||
|     private String prompt; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 平台 |      * 平台 | ||||||
|  | |||||||
| @ -38,11 +38,11 @@ public interface AiMusicService { | |||||||
|     void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO); |     void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 更新音乐名称 |      * 更新我的音乐 | ||||||
|      * |      * | ||||||
|      * @param updateReqVO 更新信息 |      * @param updateReqVO 更新信息 | ||||||
|      */ |      */ | ||||||
|     void updateMusicTitle(@Valid AiMusicUpdateTitleReqVO updateReqVO); |     void updateMyMusic(@Valid AiMusicUpdateReqVO updateReqVO, Long userId); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 删除AI 音乐 |      * 删除AI 音乐 | ||||||
|  | |||||||
| @ -3,11 +3,14 @@ package cn.iocoder.yudao.module.ai.service.music; | |||||||
| import cn.hutool.core.collection.CollUtil; | import cn.hutool.core.collection.CollUtil; | ||||||
| import cn.hutool.core.text.StrPool; | import cn.hutool.core.text.StrPool; | ||||||
| import cn.hutool.core.util.ObjUtil; | import cn.hutool.core.util.ObjUtil; | ||||||
|  | import cn.hutool.core.util.ObjectUtil; | ||||||
| import cn.hutool.core.util.StrUtil; | import cn.hutool.core.util.StrUtil; | ||||||
| import cn.hutool.http.HttpUtil; | import cn.hutool.http.HttpUtil; | ||||||
| import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||||||
| import cn.iocoder.yudao.framework.common.pojo.PageResult; | import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||||||
| import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*; | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; | ||||||
|  | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO; | ||||||
|  | import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; | ||||||
| import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | 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.dal.mysql.music.AiMusicMapper; | ||||||
| import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; | import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; | ||||||
| @ -48,18 +51,18 @@ public class AiMusicServiceImpl implements AiMusicService { | |||||||
|     public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { |     public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { | ||||||
|         // 1. 调用 Suno 生成音乐 |         // 1. 调用 Suno 生成音乐 | ||||||
|         SunoApi sunoApi = apiKeyService.getSunoApi(); |         SunoApi sunoApi = apiKeyService.getSunoApi(); | ||||||
|         // TODO @xin:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的 |         // TODO 芋艿:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的  --xin:大部分ok的,补充了error_message | ||||||
|         List<SunoApi.MusicData> musicDataList; |         List<SunoApi.MusicData> musicDataList; | ||||||
|         if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { |         if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { | ||||||
|             // 1.1 歌词模式 |             // 1.1 描述模式 | ||||||
|  |             SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest( | ||||||
|  |                     reqVO.getPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental()); | ||||||
|  |             musicDataList = sunoApi.generate(generateRequest); | ||||||
|  |         } else if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { | ||||||
|  |             // 1.2 歌词模式 | ||||||
|             SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest( |             SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest( | ||||||
|                     reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); |                     reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); | ||||||
|             musicDataList = sunoApi.customGenerate(generateRequest); |             musicDataList = sunoApi.customGenerate(generateRequest); | ||||||
|         } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { |  | ||||||
|             // 1.2 描述模式 |  | ||||||
|             SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest( |  | ||||||
|                     reqVO.getGptDescriptionPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental()); |  | ||||||
|             musicDataList = sunoApi.generate(generateRequest); |  | ||||||
|         } else { |         } else { | ||||||
|             throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO)); |             throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO)); | ||||||
|         } |         } | ||||||
| @ -108,9 +111,12 @@ public class AiMusicServiceImpl implements AiMusicService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void updateMusicTitle(AiMusicUpdateTitleReqVO updateReqVO) { |     public void updateMyMusic(AiMusicUpdateReqVO updateReqVO, Long userId) { | ||||||
|         // 校验存在 |         // 校验音乐是否存在 | ||||||
|         validateMusicExists(updateReqVO.getId()); |         AiMusicDO musicDO = validateMusicExists(updateReqVO.getId()); | ||||||
|  |         if (ObjUtil.notEqual(musicDO.getUserId(), userId)) { | ||||||
|  |             throw exception(MUSIC_NOT_EXISTS); | ||||||
|  |         } | ||||||
|         // 更新 |         // 更新 | ||||||
|         musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setTitle(updateReqVO.getTitle())); |         musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setTitle(updateReqVO.getTitle())); | ||||||
|     } |     } | ||||||
| @ -156,29 +162,38 @@ public class AiMusicServiceImpl implements AiMusicService { | |||||||
|      * @return AiMusicDO 集合 |      * @return AiMusicDO 集合 | ||||||
|      */ |      */ | ||||||
|     private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) { |     private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) { | ||||||
|         // TODO @xin:它有 status = error 状态,表示失败噢。 |         return convertList(musicList, musicData -> { | ||||||
|         return convertList(musicList, musicData -> new AiMusicDO() |             Integer status; | ||||||
|  |             if (Objects.equals("complete", musicData.status())) { | ||||||
|  |                 status = AiMusicStatusEnum.SUCCESS.getStatus(); | ||||||
|  |             } else if (Objects.equals("error", musicData.status())) { | ||||||
|  |                 status = AiMusicStatusEnum.FAIL.getStatus(); | ||||||
|  |             } else { | ||||||
|  |                 status = AiMusicStatusEnum.IN_PROGRESS.getStatus(); | ||||||
|  |             } | ||||||
|  |             return new AiMusicDO() | ||||||
|                     .setTaskId(musicData.id()).setModel(musicData.modelName()) |                     .setTaskId(musicData.id()).setModel(musicData.modelName()) | ||||||
|                 .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) |                     .setDescription(musicData.gptDescriptionPrompt()) | ||||||
|                 // TODO @xin:只有在完成的状态,在下载文件 |                     .setAudioUrl(downloadFile(status, musicData.audioUrl())) | ||||||
|                 .setAudioUrl(downloadFile(musicData.audioUrl())) |                     .setVideoUrl(downloadFile(status, musicData.videoUrl())) | ||||||
|                 .setVideoUrl(downloadFile(musicData.videoUrl())) |                     .setImageUrl(downloadFile(status, musicData.imageUrl())) | ||||||
|                 .setImageUrl(downloadFile(musicData.imageUrl())) |  | ||||||
|                     .setTitle(musicData.title()).setDuration(musicData.duration()) |                     .setTitle(musicData.title()).setDuration(musicData.duration()) | ||||||
|                     .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA)) |                     .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA)) | ||||||
|                 .setStatus(Objects.equals("complete", musicData.status()) ? |                     .setErrorMessage(musicData.errorMessage()) | ||||||
|                         AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus())); |                     .setStatus(status); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 将生成的音频文件上传到文件服务器 |      * 音乐生成好后,将音频文件上传到文件服务器 | ||||||
|      * |      * | ||||||
|  |      * @param status 音乐状态 | ||||||
|      * @param url    音频文件地址 |      * @param url    音频文件地址 | ||||||
|      * @return 内部文件地址 |      * @return 内部文件地址 | ||||||
|      */ |      */ | ||||||
|     private String downloadFile(String url) { |     private String downloadFile(Integer status, String url) { | ||||||
|         if (StrUtil.isBlank(url)) { |         if (StrUtil.isBlank(url) || ObjectUtil.notEqual(status, AiMusicStatusEnum.SUCCESS.getStatus())) { | ||||||
|             return null; |             return url; | ||||||
|         } |         } | ||||||
|         try { |         try { | ||||||
|             byte[] bytes = HttpUtil.downloadBytes(url); |             byte[] bytes = HttpUtil.downloadBytes(url); | ||||||
|  | |||||||
| @ -175,6 +175,7 @@ public class SunoApi { | |||||||
|             @JsonProperty("model_name") String modelName, |             @JsonProperty("model_name") String modelName, | ||||||
|             String status, |             String status, | ||||||
|             @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt, |             @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt, | ||||||
|  |             @JsonProperty("error_message") String errorMessage, | ||||||
|             String prompt, |             String prompt, | ||||||
|             String type, |             String type, | ||||||
|             String tags, |             String tags, | ||||||
|  | |||||||
| @ -17,8 +17,8 @@ public class SunoTests { | |||||||
|  |  | ||||||
|     @Before |     @Before | ||||||
|     public void setup() { |     public void setup() { | ||||||
| //        String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app"; |         String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app"; | ||||||
|         String url = "http://127.0.0.1:3001"; | //        String url = "http://127.0.0.1:3001"; | ||||||
|         this.sunoApi = new SunoApi(url); |         this.sunoApi = new SunoApi(url); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 芋道源码
					芋道源码