【增加】AI Music支持描述模式、歌词模式生成音乐

This commit is contained in:
xiaoxin
2024-06-07 14:51:45 +08:00
parent 085ebfc792
commit f1292fb7fe
18 changed files with 291 additions and 311 deletions

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.ai.controller.admin.music;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
import cn.iocoder.yudao.module.ai.service.music.MusicService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -13,6 +13,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - AI 音乐生成")
@@ -23,9 +25,16 @@ public class MusicController {
private final MusicService musicService;
@PostMapping("/suno-gen")
@Operation(summary = "音乐生成")
public CommonResult<SunoRespVO> musicGen(@RequestBody @Valid SunoReqVO sunoReqVO) {
return success(musicService.musicGen(sunoReqVO));
@PostMapping("generate/description-mode")
@Operation(summary = "音乐生成-描述模式")
public CommonResult<List<Long>> descriptionMode(@RequestBody @Valid SunoReqVO sunoReqVO) {
return success(musicService.descriptionMode(sunoReqVO));
}
@PostMapping("generate/lyric-mode")
@Operation(summary = "音乐生成-歌词模式")
public CommonResult<List<Long>> lyricMode(@RequestBody @Valid SunoLyricModeVO sunoLyricModeVO) {
return success(musicService.lyricMode(sunoLyricModeVO));
}
}

View File

@@ -1,85 +0,0 @@
package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
import java.util.stream.Collectors;
/**
* 表示单个音乐数据的类
*/
@Data
public class MusicDataVO {
/**
* 音乐数据的 ID
*/
private String id;
/**
* 音乐音频的标题
*/
private String title;
/**
* 音乐音频的图片 URL
*/
@JsonProperty("image_url")
private String imageUrl;
/**
* 音乐音频的歌词
*/
private String lyric;
/**
* 音乐音频的 URL
*/
@JsonProperty("audio_url")
private String audioUrl;
/**
* 音乐视频的 URL
*/
@JsonProperty("video_url")
private String videoUrl;
/**
* 音乐音频的创建时间
*/
@JsonProperty("created_at")
private String createdAt;
/**
* 使用的模型名称
*/
private String model;
/**
* 生成音乐音频的提示
*/
private String prompt;
/**
* 音乐音频的风格
*/
private String style;
public static List<MusicDataVO> convertFrom(List<AceDataSunoApi.SunoResp.MusicData> musicDataList) {
return musicDataList.stream().map(musicData -> {
MusicDataVO musicDataVO = new MusicDataVO();
musicDataVO.setId(musicData.id());
musicDataVO.setTitle(musicData.title());
musicDataVO.setImageUrl(musicData.imageUrl());
musicDataVO.setLyric(musicData.lyric());
musicDataVO.setAudioUrl(musicData.audioUrl());
musicDataVO.setVideoUrl(musicData.videoUrl());
musicDataVO.setCreatedAt(musicData.createdAt());
musicDataVO.setModel(musicData.model());
musicDataVO.setPrompt(musicData.prompt());
musicDataVO.setStyle(musicData.style());
return musicDataVO;
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
import lombok.Data;
/**
* @Author jxli@quant360.com
* @Date 2024/6/7
*/
@Data
public class SunoLyricModeVO extends SunoReqVO {
/**
* 标签/音乐风格
*/
private String tags;
/**
* 音乐名称
*/
private String title;
}

View File

@@ -2,39 +2,21 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class SunoReqVO {
/**
* 用于生成音乐音频的提示
*/
private String prompt;
/**
* 用于生成音乐音频的歌词
* 是否纯音乐
*/
private String lyric;
private boolean makeInstrumental;
/**
* 指示音乐音频是否为定制,如果为 true则从歌词生成否则从提示生成
* //todo 首次请求返回的模型是对的后续更新音频返回的模型又变成v3.5了
* 模型版本 {@link cn.iocoder.yudao.module.ai.enums.AiModelEnum} Suno
*/
private boolean custom;
/**
* 音乐音频的标题
*/
private String title;
/**
* 音乐音频的风格
*/
private String style;
/**
* 音乐音频生成后回调的 URL
*/
private String callbackUrl;
private String mv;
}

View File

@@ -1,40 +0,0 @@
package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* API 响应的数据
*/
@Data
public class SunoRespVO {
/**
* 表示请求是否成功
*/
private boolean success;
/**
* 任务 ID
*/
@JsonProperty("task_id")
private String taskId;
/**
* 音乐数据列表
*/
private List<MusicDataVO> data;
//把 SunoResp转为本vo类
public static SunoRespVO convertFrom(AceDataSunoApi.SunoResp sunoResp) {
SunoRespVO sunoRespVO = new SunoRespVO();
sunoRespVO.setSuccess(sunoResp.success());
sunoRespVO.setTaskId(sunoResp.taskId());
sunoRespVO.setData(MusicDataVO.convertFrom(sunoResp.data()));
return sunoRespVO;
}
}

View File

@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.music;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author xiaoxin
* @Date 2024/6/5
*/
@TableName("ai_music")
@Data
public class AiMusicDO extends BaseDO {
@TableId(type = IdType.AUTO)
@Schema(description = "编号")
private Long id;
@Schema(description = "用户编号")
private Long userId;
@Schema(description = "音乐名称")
private String title;
@Schema(description = "图片地址")
private String imageUrl;
@Schema(description = "歌词")
private String lyric;
@Schema(description = "音频地址")
private String audioUrl;
@Schema(description = "视频地址")
private String videoUrl;
@Schema(description = "音乐状态")
private String status;
@Schema(description = "描述词")
private String gptDescriptionPrompt;
@Schema(description = "提示词")
private String prompt;
@Schema(description = "模型")
private String model;
@Schema(description = "错误信息")
private String errorMessage;
@Schema(description = "音乐风格标签")
private String tags;
@Schema(description = "任务id")
private String taskId;
public static AiMusicDO convertFrom(SunoApi.MusicData musicData) {
return new AiMusicDO()
.setTaskId(musicData.id())
.setPrompt(musicData.prompt())
.setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
.setAudioUrl(musicData.audioUrl())
.setVideoUrl(musicData.videoUrl())
.setImageUrl(musicData.imageUrl())
.setLyric(musicData.lyric())
.setTitle(musicData.title())
.setStatus(musicData.status())
.setModel(musicData.modelName())
.setTags(musicData.tags());
}
public static List<AiMusicDO> convertFrom(List<SunoApi.MusicData> musicDataList) {
return musicDataList.stream()
.map(AiMusicDO::convertFrom)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.ai.dal.mysql.music;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
import org.apache.ibatis.annotations.Mapper;
/**
* @Author xiaoxin
* @Date 2024/6/5
*/
@Mapper
public interface AiMusicMapper extends BaseMapperX<AiMusicDO> {
}

View File

@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.ai.service.music;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
import java.util.List;
/**
* @Author xiaoxin
@@ -10,10 +12,13 @@ import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
public interface MusicService {
/**
* 音乐生成
*
* @param sunoReqVO 请求实体
* @return 响应实体
* 音乐生成-描述模式
*/
SunoRespVO musicGen(SunoReqVO sunoReqVO);
List<Long> descriptionMode(SunoReqVO reqVO);
/**
* 音乐生成-歌词模式
**/
List<Long> lyricMode(SunoLyricModeVO reqVO);
}

View File

@@ -1,10 +1,26 @@
package cn.iocoder.yudao.module.ai.service.music;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.AceDataSunoApi;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.StrPool;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoLyricModeVO;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.SunoRespVO;
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.AiMusicStatusEnum;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* @Author xiaoxin
@@ -12,20 +28,70 @@ import org.springframework.stereotype.Service;
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MusicServiceImpl implements MusicService {
private final AceDataSunoApi aceDataSunoApi;
private final SunoApi sunoApi;
private final AiMusicMapper musicMapper;
private final Queue<String> taskQueue = new ConcurrentLinkedQueue<>();
@Override
public SunoRespVO musicGen(SunoReqVO sunoReqVO) {
AceDataSunoApi.SunoResp sunoResp = aceDataSunoApi.musicGen(new AceDataSunoApi.SunoReq(
sunoReqVO.getPrompt(),
sunoReqVO.getLyric(),
sunoReqVO.isCustom(),
sunoReqVO.getTitle(),
sunoReqVO.getStyle(),
sunoReqVO.getCallbackUrl()
));
return SunoRespVO.convertFrom(sunoResp);
public List<Long> descriptionMode(SunoReqVO reqVO) {
SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.isMakeInstrumental());
//默认异步
List<SunoApi.MusicData> musicDataList = sunoApi.generate(sunoReq);
return insertMusicData(musicDataList);
}
@Override
public List<Long> lyricMode(SunoLyricModeVO reqVO) {
SunoApi.SunoReq sunoReq = new SunoApi.SunoReq(reqVO.getPrompt(), reqVO.getMv(), reqVO.getTags(), reqVO.getTitle());
//默认异步
List<SunoApi.MusicData> musicDataList = sunoApi.customGenerate(sunoReq);
return insertMusicData(musicDataList);
}
/**
* 新增音乐数据并提交 suno任务
*
* @param musicDataList 音乐数据列表
* @return 音乐id集合
*/
private List<Long> insertMusicData(List<SunoApi.MusicData> musicDataList) {
if (CollUtil.isEmpty(musicDataList)) {
return Collections.emptyList();
}
return AiMusicDO.convertFrom(musicDataList).stream()
.peek(musicDO -> musicMapper.insert(musicDO.setUserId(getLoginUserId())))
.peek(e -> Optional.of(e.getTaskId()).ifPresent(taskQueue::add))
.map(AiMusicDO::getId)
.collect(Collectors.toList());
}
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
@Transactional
public void flushSunoTask() {
if (CollUtil.isEmpty(taskQueue)) {
return;
}
CollUtil.split(taskQueue, 5).
stream().map(chunk -> CollUtil.join(chunk, StrPool.COMMA))
.forEach(taskIds -> {
List<SunoApi.MusicData> musicData = sunoApi.selectById(taskIds);
musicData.stream()
.map(AiMusicDO::convertFrom)
.forEach(musicDO -> {
//更新音乐生成结果
musicMapper.update(musicDO, Wrappers.<AiMusicDO>lambdaUpdate().eq(AiMusicDO::getTaskId, musicDO.getTaskId()));
//完成后剔除任务
if (Objects.equals(AiMusicStatusEnum.COMPLETE.getStatus(), musicDO.getStatus())) {
taskQueue.remove(musicDO.getTaskId());
}
});
});
}
}