mirror of
				https://github.com/YunaiV/ruoyi-vue-pro.git
				synced 2025-11-04 08:06:12 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master-jdk21-ai' into master-jdk21-ai
This commit is contained in:
		@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.ai.enums;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
 | 
			
		||||
// TODO @xin:这个类,挪到 enums/music 包下;
 | 
			
		||||
// TODO @xin:1)@author 这个是标准的 javadoc;2)@date 可以不要哈;3)可以加下枚举类的注释
 | 
			
		||||
/**
 | 
			
		||||
 * @Author xiaoxin
 | 
			
		||||
 * @Date 2024/6/5
 | 
			
		||||
@ -11,6 +13,8 @@ import lombok.Getter;
 | 
			
		||||
@Getter
 | 
			
		||||
public enum AiMusicStatusEnum {
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:是不是收敛成,只有 3 个:进行中,成功,失败;类似 AiImageStatusEnum
 | 
			
		||||
 | 
			
		||||
    SUBMITTED("submitted", "已提交"),
 | 
			
		||||
    QUEUED("queued", "排队中"),
 | 
			
		||||
    STREAMING("streaming", "进行中"),
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ public enum AiModelEnum {
 | 
			
		||||
    XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"),
 | 
			
		||||
    XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"),
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:// Suno;中间加个空格,会更清晰一点。一般来说,不同类型的单词之间,最好有空格。例如说,// 新增一个;再例如说;// 这是 1 个 create 逻辑
 | 
			
		||||
    //Suno
 | 
			
		||||
    SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null),
 | 
			
		||||
    SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null),
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import java.util.List;
 | 
			
		||||
 | 
			
		||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 | 
			
		||||
 | 
			
		||||
// TODO @xin:AI 前缀;都要加下哈
 | 
			
		||||
@Tag(name = "管理后台 - AI 音乐生成")
 | 
			
		||||
@RestController
 | 
			
		||||
@RequestMapping("/ai/music")
 | 
			
		||||
 | 
			
		||||
@ -4,12 +4,13 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
 | 
			
		||||
@JsonInclude(value = JsonInclude.Include.NON_NULL) // TODO @xin:不用加这个哈
 | 
			
		||||
public class SunoReqVO {
 | 
			
		||||
    /**
 | 
			
		||||
     * 用于生成音乐音频的提示
 | 
			
		||||
     */
 | 
			
		||||
    private String prompt;
 | 
			
		||||
    // TODO @xin:Boolean,不使用基本类型。
 | 
			
		||||
    /**
 | 
			
		||||
     *  是否纯音乐
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,8 @@ import java.util.stream.Collectors;
 | 
			
		||||
@TableName("ai_music")
 | 
			
		||||
@Data
 | 
			
		||||
public class AiMusicDO extends BaseDO {
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:@Schema 只在 VO 里使用,这里还是使用标准的注释哈
 | 
			
		||||
    @TableId(type = IdType.AUTO)
 | 
			
		||||
    @Schema(description = "编号")
 | 
			
		||||
    private Long id;
 | 
			
		||||
@ -40,6 +42,7 @@ public class AiMusicDO extends BaseDO {
 | 
			
		||||
    @Schema(description = "视频地址")
 | 
			
		||||
    private String videoUrl;
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:需要关联下对应的枚举
 | 
			
		||||
    @Schema(description = "音乐状态")
 | 
			
		||||
    private String status;
 | 
			
		||||
 | 
			
		||||
@ -49,19 +52,24 @@ public class AiMusicDO extends BaseDO {
 | 
			
		||||
    @Schema(description = "提示词")
 | 
			
		||||
    private String prompt;
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:生成模式,需要记录下;歌词、描述
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:多存储一个平台,platform;考虑未来可能有别的音乐接口
 | 
			
		||||
    @Schema(description = "模型")
 | 
			
		||||
    private String model;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "错误信息")
 | 
			
		||||
    private String errorMessage;
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:tags 要不要使用 List<String>
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "音乐风格标签")
 | 
			
		||||
    private String tags;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "任务id")
 | 
			
		||||
    @Schema(description = "任务编号")
 | 
			
		||||
    private String taskId;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:转换不放在 DO 里面哈。
 | 
			
		||||
 | 
			
		||||
    public static AiMusicDO convertFrom(SunoApi.MusicData musicData) {
 | 
			
		||||
        return new AiMusicDO()
 | 
			
		||||
@ -84,5 +92,4 @@ public class AiMusicDO extends BaseDO {
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,10 +5,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
 | 
			
		||||
import org.apache.ibatis.annotations.Mapper;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @Author xiaoxin
 | 
			
		||||
 * @Date 2024/6/5
 | 
			
		||||
 * AI 音乐 Mapper
 | 
			
		||||
 * @author  xiaoxin
 | 
			
		||||
 */
 | 
			
		||||
@Mapper
 | 
			
		||||
public interface AiMusicMapper extends BaseMapperX<AiMusicDO> {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,26 +31,29 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class MusicServiceImpl implements MusicService {
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:使用 @Resource 注入,整个项目保持统一哈;
 | 
			
		||||
    private final SunoApi sunoApi;
 | 
			
		||||
    private final AiMusicMapper musicMapper;
 | 
			
		||||
 | 
			
		||||
    private final Queue<String> taskQueue = new ConcurrentLinkedQueue<>();
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:要不把 descriptionMode、lyricMode 合并,同一个 generateMusic 方法,然后根据传入的 mode 模式:歌词、描述来区分?
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<Long> descriptionMode(SunoReqVO reqVO) {
 | 
			
		||||
        SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental());
 | 
			
		||||
        //默认异步
 | 
			
		||||
        // 1. 异步生成
 | 
			
		||||
        SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental());
 | 
			
		||||
        List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq);
 | 
			
		||||
        // 2. 插入数据库
 | 
			
		||||
        return insertMusicData(musicDataList);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<Long> lyricMode(SunoLyricModeVO reqVO) {
 | 
			
		||||
        SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle());
 | 
			
		||||
        //默认异步
 | 
			
		||||
        // 1. 异步生成
 | 
			
		||||
        SunoApi.SunoRequest sunoReq = new SunoApi.SunoRequest(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle());
 | 
			
		||||
        List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq);
 | 
			
		||||
        // 2. 插入数据库
 | 
			
		||||
        return insertMusicData(musicDataList);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -64,6 +67,7 @@ public class MusicServiceImpl implements MusicService {
 | 
			
		||||
        if (CollUtil.isEmpty(musicDataList)) {
 | 
			
		||||
            return Collections.emptyList();
 | 
			
		||||
        }
 | 
			
		||||
        // TODO @xin:建议使用 insertBatch 方法,批量插入
 | 
			
		||||
        return AiMusicDO.convertFrom(musicDataList).stream()
 | 
			
		||||
                .peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId())))
 | 
			
		||||
                .peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add))
 | 
			
		||||
@ -71,6 +75,7 @@ public class MusicServiceImpl implements MusicService {
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:这个,改成标准的 job 来实现哈。从数据库加载任务,然后执行。
 | 
			
		||||
    @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
 | 
			
		||||
    @Transactional
 | 
			
		||||
    public void flushSunoTask() {
 | 
			
		||||
 | 
			
		||||
@ -118,8 +118,9 @@ public class YudaoAiProperties {
 | 
			
		||||
    public static class SunoProperties {
 | 
			
		||||
 | 
			
		||||
        private boolean enable = false;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * suno-api 服务的基本地址
 | 
			
		||||
         * API 服务的基本地址
 | 
			
		||||
         */
 | 
			
		||||
        private String baseUrl;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,16 +4,20 @@ import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
 | 
			
		||||
// TODO @xin:不需要这个类哈,直接 SunoApi 传入 baseUrl 参数即可
 | 
			
		||||
/**
 | 
			
		||||
 * @Author xiaoxin
 | 
			
		||||
 * @Date 2024/5/29
 | 
			
		||||
 * Suno 配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author  xiaoxin
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class SunoConfig {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * suno-api服务的基本路径
 | 
			
		||||
     */
 | 
			
		||||
    private String baseUrl;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -27,14 +27,15 @@ import java.util.function.Predicate;
 | 
			
		||||
public class SunoApi {
 | 
			
		||||
 | 
			
		||||
    private final WebClient webClient;
 | 
			
		||||
 | 
			
		||||
    private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
 | 
			
		||||
    private final Function<ClientResponse, Mono<? extends Throwable>> EXCEPTION_FUNCTION = response -> response.bodyToMono(String.class)
 | 
			
		||||
            .handle((respBody, sink) -> {
 | 
			
		||||
                // TODO @xin:最好是 request、response 都有哈
 | 
			
		||||
                log.error("【suno-api】调用失败!resp: 【{}】", respBody);
 | 
			
		||||
                sink.error(new IllegalStateException("【suno-api】调用失败!"));
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public SunoApi(SunoConfig config) {
 | 
			
		||||
        this.webClient = WebClient.builder()
 | 
			
		||||
                .baseUrl(config.getBaseUrl())
 | 
			
		||||
@ -42,50 +43,49 @@ public class SunoApi {
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<MusicData> generate(SunoApi.SunoReq sunReq) {
 | 
			
		||||
    public List<MusicData> generate(SunoRequest request) {
 | 
			
		||||
        return this.webClient.post()
 | 
			
		||||
                .uri("/api/generate")
 | 
			
		||||
                .body(Mono.just(sunReq), SunoApi.SunoReq.class)
 | 
			
		||||
                .body(Mono.just(request), SunoRequest.class)
 | 
			
		||||
                .retrieve()
 | 
			
		||||
                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
 | 
			
		||||
                })
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
 | 
			
		||||
                .block();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<MusicData> customGenerate(SunoApi.SunoReq sunReq) {
 | 
			
		||||
    public List<MusicData> customGenerate(SunoRequest request) {
 | 
			
		||||
        return this.webClient.post()
 | 
			
		||||
                .uri("/api/custom_generate")
 | 
			
		||||
                .body(Mono.just(sunReq), SunoApi.SunoReq.class)
 | 
			
		||||
                .body(Mono.just(request), SunoRequest.class)
 | 
			
		||||
                .retrieve()
 | 
			
		||||
                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
 | 
			
		||||
                })
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
 | 
			
		||||
                .block();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO @xin: 是不是叫 chatCompletion
 | 
			
		||||
    public List<MusicData> doChatCompletion(String prompt) {
 | 
			
		||||
        return this.webClient.post()
 | 
			
		||||
                .uri("/v1/chat/completions")
 | 
			
		||||
                .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class)
 | 
			
		||||
                .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class)
 | 
			
		||||
                .retrieve()
 | 
			
		||||
                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
 | 
			
		||||
                })
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
 | 
			
		||||
                .block();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public LyricsData generateLyrics(String prompt) {
 | 
			
		||||
        return this.webClient.post()
 | 
			
		||||
                .uri("/api/generate_lyrics")
 | 
			
		||||
                .body(Mono.just(new SunoReq(prompt)), SunoApi.SunoReq.class)
 | 
			
		||||
                .body(Mono.just(new SunoRequest(prompt)), SunoRequest.class)
 | 
			
		||||
                .retrieve()
 | 
			
		||||
                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
 | 
			
		||||
                .bodyToMono(LyricsData.class)
 | 
			
		||||
                .block();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:应该传入 List<String> ids
 | 
			
		||||
    // TODO @xin:方法名,建议使用 getMusicList
 | 
			
		||||
    public List<MusicData> selectById(String ids) {
 | 
			
		||||
        return this.webClient.get()
 | 
			
		||||
                .uri(uriBuilder -> uriBuilder
 | 
			
		||||
@ -94,12 +94,11 @@ public class SunoApi {
 | 
			
		||||
                        .build())
 | 
			
		||||
                .retrieve()
 | 
			
		||||
                .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION)
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
 | 
			
		||||
                })
 | 
			
		||||
                .bodyToMono(new ParameterizedTypeReference<List<MusicData>>() { })
 | 
			
		||||
                .block();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:方法名,建议使用 getLimitUsage
 | 
			
		||||
    public LimitData selectLimit() {
 | 
			
		||||
        return this.webClient.get()
 | 
			
		||||
                .uri("/api/get_limit")
 | 
			
		||||
@ -109,7 +108,7 @@ public class SunoApi {
 | 
			
		||||
                .block();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // TODO @xin:可以改成 MusicGenerateRequest
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据提示生成音频
 | 
			
		||||
     *
 | 
			
		||||
@ -122,7 +121,7 @@ public class SunoApi {
 | 
			
		||||
     * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
 | 
			
		||||
     */
 | 
			
		||||
    @JsonInclude(value = JsonInclude.Include.NON_NULL)
 | 
			
		||||
    public record SunoReq(
 | 
			
		||||
    public record SunoRequest(
 | 
			
		||||
            String prompt,
 | 
			
		||||
            String tags,
 | 
			
		||||
            String title,
 | 
			
		||||
@ -130,23 +129,23 @@ public class SunoApi {
 | 
			
		||||
            @JsonProperty("wait_audio") boolean waitAudio,
 | 
			
		||||
            @JsonProperty("make_instrumental") boolean makeInstrumental
 | 
			
		||||
    ) {
 | 
			
		||||
        public SunoReq(String prompt) {
 | 
			
		||||
 | 
			
		||||
        public SunoRequest(String prompt) {
 | 
			
		||||
            this(prompt, null, null, null, false, false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public SunoReq(String prompt, String mv, boolean makeInstrumental) {
 | 
			
		||||
        public SunoRequest(String prompt, String mv, boolean makeInstrumental) {
 | 
			
		||||
            this(prompt, null, null, mv, false, makeInstrumental);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public SunoReq(String prompt, String mv, String tags, String title) {
 | 
			
		||||
        public SunoRequest(String prompt, String mv, String tags, String title) {
 | 
			
		||||
            this(prompt, tags, title, mv, false, false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * SunoAPI 响应的音频数据。
 | 
			
		||||
     * Suno API 响应的音频数据
 | 
			
		||||
     *
 | 
			
		||||
     * @param id                   音乐数据的 ID
 | 
			
		||||
     * @param title                音乐音频的标题
 | 
			
		||||
@ -179,7 +178,6 @@ public class SunoApi {
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Suno API 响应的歌词数据。
 | 
			
		||||
     *
 | 
			
		||||
@ -194,7 +192,6 @@ public class SunoApi {
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Suno API 响应的限额数据,目前每日免费50
 | 
			
		||||
     */
 | 
			
		||||
@ -206,5 +203,4 @@ public class SunoApi {
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ public class SunoTests {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void generate() {
 | 
			
		||||
        List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoReq("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"));
 | 
			
		||||
        List<SunoApi.MusicData> generate = sunoApi.generate(new SunoApi.SunoRequest("创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。"));
 | 
			
		||||
        System.out.println(generate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user