Commit 949d5a18151f15d258dc3ef69dacbc7f23fc8585
1 parent
2e9915b7
【新增】AI:音乐接入 API KEY 管理
Showing
13 changed files
with
108 additions
and
48 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
| @@ -5,9 +5,9 @@ Authorization: {{token}} | @@ -5,9 +5,9 @@ Authorization: {{token}} | ||
| 5 | 5 | ||
| 6 | { | 6 | { |
| 7 | "platform": "Suno", | 7 | "platform": "Suno", |
| 8 | - "generateMode": 1, | ||
| 9 | - "prompt": "来一首快乐的歌曲", | ||
| 10 | - "modelVersion": "chirp-v3.5", | 8 | + "generateMode": 2, |
| 9 | + "prompt": "周末啦!", | ||
| 10 | + "model": "chirp-v3.5", | ||
| 11 | "tags": ["Happy"], | 11 | "tags": ["Happy"], |
| 12 | "title": "Happy Song" | 12 | "title": "Happy Song" |
| 13 | } | 13 | } |
| @@ -19,8 +19,8 @@ Authorization: {{token}} | @@ -19,8 +19,8 @@ Authorization: {{token}} | ||
| 19 | 19 | ||
| 20 | { | 20 | { |
| 21 | "platform": "Suno", | 21 | "platform": "Suno", |
| 22 | - "generateMode": 2, | ||
| 23 | - "prompt": "来一首快乐的歌曲", | ||
| 24 | - "makeInstrumental": false, | ||
| 25 | - "title": "Happy Song" | 22 | + "generateMode": 1, |
| 23 | + "model": "chirp-v3.5", | ||
| 24 | + "gptDescriptionPrompt": "今天是星球六,结果是个下雨天,希望心情很美丽", | ||
| 25 | + "makeInstrumental": false | ||
| 26 | } | 26 | } |
| 27 | \ No newline at end of file | 27 | \ No newline at end of file |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
| @@ -35,6 +35,16 @@ public class AiMusicController { | @@ -35,6 +35,16 @@ public class AiMusicController { | ||
| 35 | return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); | 35 | return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | + @PostMapping("/generate") | ||
| 39 | + @Operation(summary = "音乐生成") | ||
| 40 | + public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) { | ||
| 41 | + if (true) { | ||
| 42 | + musicService.syncMusic(); | ||
| 43 | + return null; | ||
| 44 | + } | ||
| 45 | + return success(musicService.generateMusic(getLoginUserId(), reqVO)); | ||
| 46 | + } | ||
| 47 | + | ||
| 38 | @Operation(summary = "删除【我的】音乐记录") | 48 | @Operation(summary = "删除【我的】音乐记录") |
| 39 | @DeleteMapping("/delete-my") | 49 | @DeleteMapping("/delete-my") |
| 40 | @Parameter(name = "id", required = true, description = "音乐编号", example = "1024") | 50 | @Parameter(name = "id", required = true, description = "音乐编号", example = "1024") |
| @@ -54,6 +64,7 @@ public class AiMusicController { | @@ -54,6 +64,7 @@ public class AiMusicController { | ||
| 54 | return success(BeanUtils.toBean(music, AiMusicRespVO.class)); | 64 | return success(BeanUtils.toBean(music, AiMusicRespVO.class)); |
| 55 | } | 65 | } |
| 56 | 66 | ||
| 67 | + // TODO @xin:这个搞成 updateMy ,修改【我的】音乐。方便后续支持其它字段;另外,需要校验下,更新的音乐,是不是我的! | ||
| 57 | @PostMapping("/updateTitle-my") | 68 | @PostMapping("/updateTitle-my") |
| 58 | @Operation(summary = "修改【我的】音乐 目前只支持修改标题") | 69 | @Operation(summary = "修改【我的】音乐 目前只支持修改标题") |
| 59 | @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") | 70 | @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") |
| @@ -62,12 +73,6 @@ public class AiMusicController { | @@ -62,12 +73,6 @@ public class AiMusicController { | ||
| 62 | return success(true); | 73 | return success(true); |
| 63 | } | 74 | } |
| 64 | 75 | ||
| 65 | - @PostMapping("/generate") | ||
| 66 | - @Operation(summary = "音乐生成") | ||
| 67 | - public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) { | ||
| 68 | - return success(musicService.generateMusic(getLoginUserId(), reqVO)); | ||
| 69 | - } | ||
| 70 | - | ||
| 71 | // ================ 音乐管理 ================ | 76 | // ================ 音乐管理 ================ |
| 72 | 77 | ||
| 73 | @GetMapping("/page") | 78 | @GetMapping("/page") |
| @@ -87,11 +92,11 @@ public class AiMusicController { | @@ -87,11 +92,11 @@ public class AiMusicController { | ||
| 87 | return success(true); | 92 | return success(true); |
| 88 | } | 93 | } |
| 89 | 94 | ||
| 90 | - @PutMapping("/update-public-status") | ||
| 91 | - @Operation(summary = "更新音乐发布状态") | 95 | + @PutMapping("/update") |
| 96 | + @Operation(summary = "更新音乐") | ||
| 92 | @PreAuthorize("@ss.hasPermission('ai:music:update')") | 97 | @PreAuthorize("@ss.hasPermission('ai:music:update')") |
| 93 | - public CommonResult<Boolean> updateMusicPublicStatus(@Valid @RequestBody AiMusicUpdatePublicStatusReqVO updateReqVO) { | ||
| 94 | - musicService.updateMusicPublicStatus(updateReqVO); | 98 | + public CommonResult<Boolean> updateMusic(@Valid @RequestBody AiMusicUpdateReqVO updateReqVO) { |
| 99 | + musicService.updateMusic(updateReqVO); | ||
| 95 | return success(true); | 100 | return success(true); |
| 96 | } | 101 | } |
| 97 | 102 |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
| @@ -52,6 +52,9 @@ public class AiMusicRespVO { | @@ -52,6 +52,9 @@ public class AiMusicRespVO { | ||
| 52 | @Schema(description = "音乐风格标签") | 52 | @Schema(description = "音乐风格标签") |
| 53 | private List<String> tags; | 53 | private List<String> tags; |
| 54 | 54 | ||
| 55 | + @Schema(description = "音乐时长", example = "[\"pop\",\"jazz\",\"punk\"]") | ||
| 56 | + private Double duration; | ||
| 57 | + | ||
| 55 | @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | 58 | @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") |
| 56 | private Boolean publicStatus; | 59 | private Boolean publicStatus; |
| 57 | 60 | ||
| @@ -61,9 +64,6 @@ public class AiMusicRespVO { | @@ -61,9 +64,6 @@ public class AiMusicRespVO { | ||
| 61 | @Schema(description = "错误信息") | 64 | @Schema(description = "错误信息") |
| 62 | private String errorMessage; | 65 | private String errorMessage; |
| 63 | 66 | ||
| 64 | - @Schema(description = "音乐时长") | ||
| 65 | - private Double duration; | ||
| 66 | - | ||
| 67 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 67 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 68 | private LocalDateTime createTime; | 68 | private LocalDateTime createTime; |
| 69 | 69 |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdatePublicStatusReqVO.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java
| @@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema; | @@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | import jakarta.validation.constraints.NotNull; | 4 | import jakarta.validation.constraints.NotNull; |
| 5 | import lombok.Data; | 5 | import lombok.Data; |
| 6 | 6 | ||
| 7 | -@Schema(description = "管理后台 - AI 音乐修改发布状态 Request VO") | 7 | +@Schema(description = "管理后台 - AI 音乐修改 Request VO") |
| 8 | @Data | 8 | @Data |
| 9 | -public class AiMusicUpdatePublicStatusReqVO { | 9 | +public class AiMusicUpdateReqVO { |
| 10 | 10 | ||
| 11 | @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") | 11 | @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") |
| 12 | + @NotNull(message = "编号不能为空") | ||
| 12 | private Long id; | 13 | private Long id; |
| 13 | 14 | ||
| 14 | - @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | ||
| 15 | - @NotNull(message = "是否发布不能为空") | 15 | + @Schema(description = "是否发布", example = "true") |
| 16 | private Boolean publicStatus; | 16 | private Boolean publicStatus; |
| 17 | 17 | ||
| 18 | } | 18 | } |
| 19 | \ No newline at end of file | 19 | \ 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
| @@ -20,11 +20,13 @@ public class AiSunoGenerateReqVO { | @@ -20,11 +20,13 @@ public class AiSunoGenerateReqVO { | ||
| 20 | * 1. 描述模式:描述词 + 是否纯音乐 + 模型 | 20 | * 1. 描述模式:描述词 + 是否纯音乐 + 模型 |
| 21 | * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型 | 21 | * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型 |
| 22 | */ | 22 | */ |
| 23 | - @Schema(description = "生成模式 1.描述模式 2. 歌词模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | 23 | + @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") |
| 24 | @NotNull(message = "生成模式不能为空") | 24 | @NotNull(message = "生成模式不能为空") |
| 25 | private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举 | 25 | private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举 |
| 26 | 26 | ||
| 27 | - @Schema(description = "歌词模式用:用于生成音乐音频的歌词提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, | 27 | + // TODO @xin:方案一:prompt => lyric 歌词;gptDescriptionPrompt => description 描述(db 那字段也改下,避免和 gpt 直接耦合);这样搞完后,会更统一好理解一点 |
| 28 | + // TODO @xin:方案二:还是之前的做法,都用 prompt;不过最终 gptDescriptionPrompt 还是存储 description 算描述。可以微信一起讨论下。 | ||
| 29 | + @Schema(description = "用于生成音乐音频的歌词提示", | ||
| 28 | example = """ | 30 | example = """ |
| 29 | [Verse] | 31 | [Verse] |
| 30 | 阳光下奔跑 多么欢快 | 32 | 阳光下奔跑 多么欢快 |
| @@ -37,23 +39,23 @@ public class AiSunoGenerateReqVO { | @@ -37,23 +39,23 @@ public class AiSunoGenerateReqVO { | ||
| 37 | 日子太短暂 别再等待 | 39 | 日子太短暂 别再等待 |
| 38 | 马上放假了 梦想起飞 | 40 | 马上放假了 梦想起飞 |
| 39 | """) | 41 | """) |
| 40 | - private String prompt; | 42 | + private String prompt; // 歌词模式用 |
| 41 | 43 | ||
| 42 | - @Schema(description = "描述模式用:用于生成音乐音频的描述", requiredMode = Schema.RequiredMode.NOT_REQUIRED, | 44 | + @Schema(description = "用于生成音乐音频的描述", |
| 43 | example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") | 45 | example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") |
| 44 | - private String gptDescriptionPrompt; | 46 | + private String gptDescriptionPrompt; // 描述模式用 |
| 45 | 47 | ||
| 46 | - @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true") | 48 | + @Schema(description = "是否纯音乐", example = "true") |
| 47 | private Boolean makeInstrumental; | 49 | private Boolean makeInstrumental; |
| 48 | 50 | ||
| 49 | @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5") | 51 | @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5") |
| 50 | @NotEmpty(message = "模型不能为空") | 52 | @NotEmpty(message = "模型不能为空") |
| 51 | private String model; // 参见 AiModelEnum 枚举 | 53 | private String model; // 参见 AiModelEnum 枚举 |
| 52 | 54 | ||
| 53 | - @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]") | 55 | + @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]") |
| 54 | private List<String> tags; | 56 | private List<String> tags; |
| 55 | 57 | ||
| 56 | - @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星") | 58 | + @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星") |
| 57 | private String title; | 59 | private String title; |
| 58 | 60 | ||
| 59 | } | 61 | } |
| 60 | \ No newline at end of file | 62 | \ 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
| @@ -99,6 +99,11 @@ public class AiMusicDO extends BaseDO { | @@ -99,6 +99,11 @@ public class AiMusicDO extends BaseDO { | ||
| 99 | private List<String> tags; | 99 | private List<String> tags; |
| 100 | 100 | ||
| 101 | /** | 101 | /** |
| 102 | + * 音乐时长 | ||
| 103 | + */ | ||
| 104 | + private Double duration; | ||
| 105 | + | ||
| 106 | + /** | ||
| 102 | * 是否公开 | 107 | * 是否公开 |
| 103 | */ | 108 | */ |
| 104 | private Boolean publicStatus; | 109 | private Boolean publicStatus; |
| @@ -113,10 +118,4 @@ public class AiMusicDO extends BaseDO { | @@ -113,10 +118,4 @@ public class AiMusicDO extends BaseDO { | ||
| 113 | */ | 118 | */ |
| 114 | private String errorMessage; | 119 | private String errorMessage; |
| 115 | 120 | ||
| 116 | - | ||
| 117 | - /** | ||
| 118 | - * 音乐时长 | ||
| 119 | - */ | ||
| 120 | - private Double duration; | ||
| 121 | - | ||
| 122 | } | 121 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
| 1 | package cn.iocoder.yudao.module.ai.service.model; | 1 | package cn.iocoder.yudao.module.ai.service.model; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | 3 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| 4 | +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||
| 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 5 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; | 6 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; |
| 6 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; | 7 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; |
| @@ -91,4 +92,13 @@ public interface AiApiKeyService { | @@ -91,4 +92,13 @@ public interface AiApiKeyService { | ||
| 91 | */ | 92 | */ |
| 92 | ImageClient getImageClient(AiPlatformEnum platform); | 93 | ImageClient getImageClient(AiPlatformEnum platform); |
| 93 | 94 | ||
| 95 | + /** | ||
| 96 | + * 获得 SunoApi 对象 | ||
| 97 | + * | ||
| 98 | + * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择 | ||
| 99 | + * | ||
| 100 | + * @return SunoApi 对象 | ||
| 101 | + */ | ||
| 102 | + SunoApi getSunoApi(); | ||
| 103 | + | ||
| 94 | } | 104 | } |
| 95 | \ No newline at end of file | 105 | \ No newline at end of file |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | 3 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| 4 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; | 4 | import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; |
| 5 | +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||
| 5 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 7 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 8 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| @@ -111,4 +112,14 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { | @@ -111,4 +112,14 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { | ||
| 111 | return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl()); | 112 | return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl()); |
| 112 | } | 113 | } |
| 113 | 114 | ||
| 115 | + @Override | ||
| 116 | + public SunoApi getSunoApi() { | ||
| 117 | + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( | ||
| 118 | + AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); | ||
| 119 | + if (apiKey == null) { | ||
| 120 | + return null; | ||
| 121 | + } | ||
| 122 | + return clientFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); | ||
| 123 | + } | ||
| 124 | + | ||
| 114 | } | 125 | } |
| 115 | \ No newline at end of file | 126 | \ No newline at end of file |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
| @@ -35,7 +35,7 @@ public interface AiMusicService { | @@ -35,7 +35,7 @@ public interface AiMusicService { | ||
| 35 | * | 35 | * |
| 36 | * @param updateReqVO 更新信息 | 36 | * @param updateReqVO 更新信息 |
| 37 | */ | 37 | */ |
| 38 | - void updateMusicPublicStatus(@Valid AiMusicUpdatePublicStatusReqVO updateReqVO); | 38 | + void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO); |
| 39 | 39 | ||
| 40 | /** | 40 | /** |
| 41 | * 更新音乐名称 | 41 | * 更新音乐名称 |
| @@ -83,4 +83,5 @@ public interface AiMusicService { | @@ -83,4 +83,5 @@ public interface AiMusicService { | ||
| 83 | * @return 音乐分页 | 83 | * @return 音乐分页 |
| 84 | */ | 84 | */ |
| 85 | PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId); | 85 | PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId); |
| 86 | + | ||
| 86 | } | 87 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
| @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; | ||
| 12 | import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; | 12 | import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; |
| 13 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; | 13 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; |
| 14 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; | 14 | import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; |
| 15 | +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; | ||
| 15 | import cn.iocoder.yudao.module.infra.api.file.FileApi; | 16 | import cn.iocoder.yudao.module.infra.api.file.FileApi; |
| 16 | import jakarta.annotation.Resource; | 17 | import jakarta.annotation.Resource; |
| 17 | import lombok.extern.slf4j.Slf4j; | 18 | import lombok.extern.slf4j.Slf4j; |
| @@ -35,7 +36,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXIS | @@ -35,7 +36,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXIS | ||
| 35 | public class AiMusicServiceImpl implements AiMusicService { | 36 | public class AiMusicServiceImpl implements AiMusicService { |
| 36 | 37 | ||
| 37 | @Resource | 38 | @Resource |
| 38 | - private SunoApi sunoApi; | 39 | + private AiApiKeyService apiKeyService; |
| 39 | 40 | ||
| 40 | @Resource | 41 | @Resource |
| 41 | private AiMusicMapper musicMapper; | 42 | private AiMusicMapper musicMapper; |
| @@ -46,6 +47,8 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -46,6 +47,8 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 46 | @Override | 47 | @Override |
| 47 | public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { | 48 | public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { |
| 48 | // 1. 调用 Suno 生成音乐 | 49 | // 1. 调用 Suno 生成音乐 |
| 50 | + SunoApi sunoApi = apiKeyService.getSunoApi(); | ||
| 51 | + // TODO @xin:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的 | ||
| 49 | List<SunoApi.MusicData> musicDataList; | 52 | List<SunoApi.MusicData> musicDataList; |
| 50 | if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { | 53 | if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { |
| 51 | // 1.1 歌词模式 | 54 | // 1.1 歌词模式 |
| @@ -80,6 +83,7 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -80,6 +83,7 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 80 | log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); | 83 | log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); |
| 81 | 84 | ||
| 82 | // GET 请求,为避免参数过长,分批次处理 | 85 | // GET 请求,为避免参数过长,分批次处理 |
| 86 | + SunoApi sunoApi = apiKeyService.getSunoApi(); | ||
| 83 | CollUtil.split(streamingTask, 36).forEach(chunkList -> { | 87 | CollUtil.split(streamingTask, 36).forEach(chunkList -> { |
| 84 | Map<String, Long> taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId); | 88 | Map<String, Long> taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId); |
| 85 | List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); | 89 | List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); |
| @@ -96,7 +100,7 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -96,7 +100,7 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 96 | } | 100 | } |
| 97 | 101 | ||
| 98 | @Override | 102 | @Override |
| 99 | - public void updateMusicPublicStatus(AiMusicUpdatePublicStatusReqVO updateReqVO) { | 103 | + public void updateMusic(AiMusicUpdateReqVO updateReqVO) { |
| 100 | // 校验存在 | 104 | // 校验存在 |
| 101 | validateMusicExists(updateReqVO.getId()); | 105 | validateMusicExists(updateReqVO.getId()); |
| 102 | // 更新 | 106 | // 更新 |
| @@ -152,11 +156,16 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -152,11 +156,16 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 152 | * @return AiMusicDO 集合 | 156 | * @return AiMusicDO 集合 |
| 153 | */ | 157 | */ |
| 154 | private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) { | 158 | private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) { |
| 159 | + // TODO @xin:它有 status = error 状态,表示失败噢。 | ||
| 155 | return convertList(musicList, musicData -> new AiMusicDO() | 160 | return convertList(musicList, musicData -> new AiMusicDO() |
| 156 | .setTaskId(musicData.id()).setModel(musicData.modelName()) | 161 | .setTaskId(musicData.id()).setModel(musicData.modelName()) |
| 157 | .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) | 162 | .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt()) |
| 158 | - .setAudioUrl(createFile(musicData.audioUrl())).setVideoUrl(createFile(musicData.videoUrl())).setImageUrl(createFile(musicData.imageUrl())).setDuration(musicData.duration()) | ||
| 159 | - .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA)) | 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)) | ||
| 160 | .setStatus(Objects.equals("complete", musicData.status()) ? | 169 | .setStatus(Objects.equals("complete", musicData.status()) ? |
| 161 | AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus())); | 170 | AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus())); |
| 162 | } | 171 | } |
| @@ -167,12 +176,17 @@ public class AiMusicServiceImpl implements AiMusicService { | @@ -167,12 +176,17 @@ public class AiMusicServiceImpl implements AiMusicService { | ||
| 167 | * @param url 音频文件地址 | 176 | * @param url 音频文件地址 |
| 168 | * @return 内部文件地址 | 177 | * @return 内部文件地址 |
| 169 | */ | 178 | */ |
| 170 | - private String createFile(String url) { | 179 | + private String downloadFile(String url) { |
| 171 | if (StrUtil.isBlank(url)) { | 180 | if (StrUtil.isBlank(url)) { |
| 172 | return null; | 181 | return null; |
| 173 | } | 182 | } |
| 174 | - byte[] bytes = HttpUtil.downloadBytes(url); | ||
| 175 | - return fileApi.createFile(bytes); | 183 | + try { |
| 184 | + byte[] bytes = HttpUtil.downloadBytes(url); | ||
| 185 | + return fileApi.createFile(bytes); | ||
| 186 | + } catch (Exception e) { | ||
| 187 | + log.error("[downloadFile][url({}) 下载失败]", url, e); | ||
| 188 | + return url; | ||
| 189 | + } | ||
| 176 | } | 190 | } |
| 177 | 191 | ||
| 178 | /** | 192 | /** |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
| 1 | package cn.iocoder.yudao.framework.ai.core.factory; | 1 | package cn.iocoder.yudao.framework.ai.core.factory; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | 3 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| 4 | +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||
| 4 | import org.springframework.ai.chat.StreamingChatClient; | 5 | import org.springframework.ai.chat.StreamingChatClient; |
| 5 | import org.springframework.ai.image.ImageClient; | 6 | import org.springframework.ai.image.ImageClient; |
| 6 | 7 | ||
| @@ -55,4 +56,15 @@ public interface AiClientFactory { | @@ -55,4 +56,15 @@ public interface AiClientFactory { | ||
| 55 | */ | 56 | */ |
| 56 | ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url); | 57 | ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url); |
| 57 | 58 | ||
| 59 | + /** | ||
| 60 | + * 基于指定配置,获得 SunoApi 对象 | ||
| 61 | + * | ||
| 62 | + * 如果不存在,则进行创建 | ||
| 63 | + * | ||
| 64 | + * @param apiKey API KEY | ||
| 65 | + * @param url API URL | ||
| 66 | + * @return SunoApi 对象 | ||
| 67 | + */ | ||
| 68 | + SunoApi getOrCreateSunoApi(String apiKey, String url); | ||
| 69 | + | ||
| 58 | } | 70 | } |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
| @@ -9,6 +9,7 @@ import cn.hutool.extra.spring.SpringUtil; | @@ -9,6 +9,7 @@ import cn.hutool.extra.spring.SpringUtil; | ||
| 9 | import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; | 9 | import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; |
| 10 | import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; | 10 | import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; |
| 11 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | 11 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| 12 | +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | ||
| 12 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; | 13 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; |
| 13 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; | 14 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; |
| 14 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi; | 15 | import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi; |
| @@ -109,6 +110,11 @@ public class AiClientFactoryImpl implements AiClientFactory { | @@ -109,6 +110,11 @@ public class AiClientFactoryImpl implements AiClientFactory { | ||
| 109 | } | 110 | } |
| 110 | } | 111 | } |
| 111 | 112 | ||
| 113 | + @Override | ||
| 114 | + public SunoApi getOrCreateSunoApi(String apiKey, String url) { | ||
| 115 | + return new SunoApi(url); | ||
| 116 | + } | ||
| 117 | + | ||
| 112 | private static String buildClientCacheKey(Class<?> clazz, Object... params) { | 118 | private static String buildClientCacheKey(Class<?> clazz, Object... params) { |
| 113 | if (ArrayUtil.isEmpty(params)) { | 119 | if (ArrayUtil.isEmpty(params)) { |
| 114 | return clazz.getName(); | 120 | return clazz.getName(); |
yudao-server/src/main/resources/application.yaml
| @@ -201,8 +201,8 @@ yudao.ai: | @@ -201,8 +201,8 @@ yudao.ai: | ||
| 201 | notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify | 201 | notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify |
| 202 | suno: | 202 | suno: |
| 203 | enable: true | 203 | enable: true |
| 204 | - base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app | ||
| 205 | -# base-url: http://127.0.0.1:3001 | 204 | +# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app |
| 205 | + base-url: http://127.0.0.1:3001 | ||
| 206 | 206 | ||
| 207 | --- #################### 芋道相关配置 #################### | 207 | --- #################### 芋道相关配置 #################### |
| 208 | 208 |