Browse Source

!8 【解决todo】AI 音乐:1.处理失败任务 2.两种生成类型提示词合并,库中区分
Merge pull request !8 from 小新/master-jdk21-ai

芋道源码 9 months ago
parent
commit
3f0d823a85

+ 2 - 1
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;
 public enum AiMusicStatusEnum implements IntArrayValuable {
 
     IN_PROGRESS(10, "进行中"),
-    SUCCESS(20, "已完成");
+    SUCCESS(20, "已完成"),
+    FAIL(30, "已失败");
 
     /**
      * 状态

+ 3 - 8
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 {
     @PostMapping("/generate")
     @Operation(summary = "音乐生成")
     public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
-        if (true) {
-            musicService.syncMusic();
-            return null;
-        }
         return success(musicService.generateMusic(getLoginUserId(), reqVO));
     }
 
@@ -64,12 +60,11 @@ public class AiMusicController {
         return success(BeanUtils.toBean(music, AiMusicRespVO.class));
     }
 
-    // TODO @xin:这个搞成 updateMy ,修改【我的】音乐。方便后续支持其它字段;另外,需要校验下,更新的音乐,是不是我的!
-    @PostMapping("/updateTitle-my")
+    @PostMapping("/update-my")
     @Operation(summary = "修改【我的】音乐 目前只支持修改标题")
     @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星")
-    public CommonResult<Boolean> updateMusicTitle(AiMusicUpdateTitleReqVO updateReqVO) {
-        musicService.updateMusicTitle(updateReqVO);
+    public CommonResult<Boolean> updateMy(AiMusicUpdateReqVO updateReqVO) {
+        musicService.updateMyMusic(updateReqVO, getLoginUserId());
         return success(true);
     }
 

+ 3 - 0
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 {
     @Schema(description = "是否发布", example = "true")
     private Boolean publicStatus;
 
+    @Schema(description = "音乐名称", example = "夜空中最亮的星")
+    private String title;
+
 }

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

@@ -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;
-
-}

+ 3 - 7
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 {
     @NotNull(message = "生成模式不能为空")
     private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举
 
-    // TODO @xin:方案一:prompt => lyric 歌词;gptDescriptionPrompt => description 描述(db 那字段也改下,避免和 gpt 直接耦合);这样搞完后,会更统一好理解一点
-    // TODO @xin:方案二:还是之前的做法,都用 prompt;不过最终 gptDescriptionPrompt 还是存储 description 算描述。可以微信一起讨论下。
     @Schema(description = "用于生成音乐音频的歌词提示",
             example = """
+                    1.描述模式:创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。
+                    2.歌词模式:
                     [Verse]
                     阳光下奔跑 多么欢快
                     假期就要来 心都飞起来
@@ -39,11 +39,7 @@ public class AiSunoGenerateReqVO {
                     日子太短暂 别再等待
                     马上放假了 梦想起飞
                     """)
-    private String prompt; // 歌词模式用
-
-    @Schema(description = "用于生成音乐音频的描述",
-            example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
-    private String gptDescriptionPrompt; // 描述模式用
+    private String prompt;
 
     @Schema(description = "是否纯音乐", example = "true")
     private Boolean makeInstrumental;

+ 1 - 5
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 {
     /**
      * 描述词
      */
-    private String gptDescriptionPrompt;
-    /**
-     * 提示词
-     */
-    private String prompt;
+    private String description;
 
     /**
      * 平台

+ 2 - 2
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 {
     void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO);
 
     /**
-     * 更新音乐名称
+     * 更新我的音乐
      *
      * @param updateReqVO 更新信息
      */
-    void updateMusicTitle(@Valid AiMusicUpdateTitleReqVO updateReqVO);
+    void updateMyMusic(@Valid AiMusicUpdateReqVO updateReqVO, Long userId);
 
     /**
      * 删除AI 音乐

+ 44 - 29
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;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.text.StrPool;
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.ai.controller.admin.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.mysql.music.AiMusicMapper;
 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) {
         // 1. 调用 Suno 生成音乐
         SunoApi sunoApi = apiKeyService.getSunoApi();
-        // TODO @xin:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的
+        // TODO 芋艿:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的  --xin:大部分ok的,补充了error_message
         List<SunoApi.MusicData> musicDataList;
-        if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
-            // 1.1 歌词模式
+        if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
+            // 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(
                     reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
             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 {
             throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO));
         }
@@ -108,9 +111,12 @@ public class AiMusicServiceImpl implements AiMusicService {
     }
 
     @Override
-    public void updateMusicTitle(AiMusicUpdateTitleReqVO updateReqVO) {
-        // 校验存在
-        validateMusicExists(updateReqVO.getId());
+    public void updateMyMusic(AiMusicUpdateReqVO updateReqVO, Long userId) {
+        // 校验音乐是否存在
+        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()));
     }
@@ -156,29 +162,38 @@ public class AiMusicServiceImpl implements AiMusicService {
      * @return AiMusicDO 集合
      */
     private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
-        // TODO @xin:它有 status = error 状态,表示失败噢。
-        return convertList(musicList, musicData -> new AiMusicDO()
-                .setTaskId(musicData.id()).setModel(musicData.modelName())
-                .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
-                // TODO @xin:只有在完成的状态,在下载文件
-                .setAudioUrl(downloadFile(musicData.audioUrl()))
-                .setVideoUrl(downloadFile(musicData.videoUrl()))
-                .setImageUrl(downloadFile(musicData.imageUrl()))
-                .setTitle(musicData.title()).setDuration(musicData.duration())
-                .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
-                .setStatus(Objects.equals("complete", musicData.status()) ?
-                        AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
+        return convertList(musicList, musicData -> {
+            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())
+                    .setDescription(musicData.gptDescriptionPrompt())
+                    .setAudioUrl(downloadFile(status, musicData.audioUrl()))
+                    .setVideoUrl(downloadFile(status, musicData.videoUrl()))
+                    .setImageUrl(downloadFile(status, musicData.imageUrl()))
+                    .setTitle(musicData.title()).setDuration(musicData.duration())
+                    .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
+                    .setErrorMessage(musicData.errorMessage())
+                    .setStatus(status);
+        });
     }
 
     /**
-     * 将生成的音频文件上传到文件服务器
+     * 音乐生成好后,将音频文件上传到文件服务器
      *
-     * @param url 音频文件地址
+     * @param status 音乐状态
+     * @param url    音频文件地址
      * @return 内部文件地址
      */
-    private String downloadFile(String url) {
-        if (StrUtil.isBlank(url)) {
-            return null;
+    private String downloadFile(Integer status, String url) {
+        if (StrUtil.isBlank(url) || ObjectUtil.notEqual(status, AiMusicStatusEnum.SUCCESS.getStatus())) {
+            return url;
         }
         try {
             byte[] bytes = HttpUtil.downloadBytes(url);

+ 1 - 0
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 {
             @JsonProperty("model_name") String modelName,
             String status,
             @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt,
+            @JsonProperty("error_message") String errorMessage,
             String prompt,
             String type,
             String tags,

+ 2 - 2
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 {
 
     @Before
     public void setup() {
-//        String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app";
-        String url = "http://127.0.0.1:3001";
+        String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app";
+//        String url = "http://127.0.0.1:3001";
         this.sunoApi = new SunoApi(url);
     }