package com.iailab.framework.ai.core.model.suno.api; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.text.StrPool; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; /** * Suno API *

* 对接 Suno Proxy:suno-api * * @author xiaoxin */ @Slf4j public class SunoApi { private final WebClient webClient; private final Predicate STATUS_PREDICATE = status -> !status.is2xxSuccessful(); private final Function>> EXCEPTION_FUNCTION = reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> { HttpRequest request = response.request(); log.error("[suno-api] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]", request.getMethod(), request.getURI(), reqParam, responseBody); sink.error(new IllegalStateException("[suno-api] 调用失败!")); }); public SunoApi(String baseUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) .defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) .build(); } public List generate(MusicGenerateRequest request) { return this.webClient.post() .uri("/api/generate") .body(Mono.just(request), MusicGenerateRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request)) .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } public List customGenerate(MusicGenerateRequest request) { return this.webClient.post() .uri("/api/custom_generate") .body(Mono.just(request), MusicGenerateRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request)) .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } public LyricsData generateLyrics(String prompt) { return this.webClient.post() .uri("/api/generate_lyrics") .body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt)) .bodyToMono(LyricsData.class) .block(); } public List getMusicList(List ids) { return this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/api/get") .queryParam("ids", CollUtil.join(ids, StrPool.COMMA)) .build()) .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(ids)) .bodyToMono(new ParameterizedTypeReference>() { }) .block(); } public LimitUsageData getLimitUsage() { return this.webClient.get() .uri("/api/get_limit") .retrieve() .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(null)) .bodyToMono(LimitUsageData.class) .block(); } /** * 根据提示生成音频 * * @param prompt 用于生成音乐音频的提示 * @param tags 音乐风格 * @param title 音乐名称 * @param model 模型 * @param waitAudio false 表示后台模式,仅返回音频任务信息,需要调用 get API 获取详细的音频信息。 * true 表示同步模式,API 最多等待 100s,音频生成完毕后直接返回音频链接等信息,建议在 GPT 等 agent 中使用。 * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成 */ @JsonInclude(value = JsonInclude.Include.NON_NULL) public record MusicGenerateRequest( String prompt, String tags, String title, String model, @JsonProperty("wait_audio") boolean waitAudio, @JsonProperty("make_instrumental") boolean makeInstrumental ) { public MusicGenerateRequest(String prompt) { this(prompt, null, null, null, false, false); } public MusicGenerateRequest(String prompt, String model, boolean makeInstrumental) { this(prompt, null, null, model, false, makeInstrumental); } public MusicGenerateRequest(String prompt, String model, String tags, String title) { this(prompt, tags, title, model, false, false); } } /** * Suno API 响应的音频数据 * * @param id 音乐数据的 ID * @param title 音乐音频的标题 * @param imageUrl 音乐音频的图片 URL * @param lyric 音乐音频的歌词 * @param audioUrl 音乐音频的 URL * @param videoUrl 音乐视频的 URL * @param createdAt 音乐音频的创建时间 * @param modelName 模型名称 * @param status submitted、queued、streaming、complete * @param gptDescriptionPrompt 描述词 * @param prompt 生成音乐音频的提示 * @param type 操作类型 * @param tags 音乐类型标签 * @param duration 音乐时长 */ public record MusicData( String id, String title, @JsonProperty("image_url") String imageUrl, String lyric, @JsonProperty("audio_url") String audioUrl, @JsonProperty("video_url") String videoUrl, @JsonProperty("created_at") String createdAt, @JsonProperty("model_name") String modelName, String status, @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt, @JsonProperty("error_message") String errorMessage, String prompt, String type, String tags, Double duration ) { } /** * Suno API 响应的歌词数据。 * * @param text 歌词 * @param title 标题 * @param status 状态 */ public record LyricsData( String text, String title, String status ) { } /** * Suno API 响应的限额数据,目前每日免费 50 */ public record LimitUsageData( @JsonProperty("credits_left") Long creditsLeft, String period, @JsonProperty("monthly_limit") Long monthlyLimit, @JsonProperty("monthly_usage") Long monthlyUsage ) { } }