feat: 优化功能
This commit is contained in:
@@ -50,8 +50,14 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
// 1. 校验时长
|
||||
validateDuration(createReqVO);
|
||||
|
||||
log.info("[MixTask][创建任务] userId={}, title={}, materialCount={}, produceCount={}",
|
||||
// 记录日志(支持新格式)
|
||||
if (createReqVO.isUsingNewFormat()) {
|
||||
log.info("[MixTask][创建任务] userId={}, title={}, sceneCount={}, produceCount={}",
|
||||
userId, createReqVO.getTitle(), createReqVO.getScenes().size(), createReqVO.getProduceCount());
|
||||
} else {
|
||||
log.info("[MixTask][创建任务] userId={}, title={}, materialCount={}, produceCount={}",
|
||||
userId, createReqVO.getTitle(), createReqVO.getMaterials().size(), createReqVO.getProduceCount());
|
||||
}
|
||||
|
||||
// 1. 创建初始任务对象
|
||||
MixTaskDO task = MixTaskUtils.createInitialTask(createReqVO, userId);
|
||||
@@ -387,10 +393,25 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
List<String> jobIdWithUrls = new ArrayList<>();
|
||||
int produceCount = createReqVO.getProduceCount();
|
||||
|
||||
// 循环生成多个视频,每个视频使用不同的截取起始点
|
||||
// 循环生成多个视频
|
||||
for (int videoIndex = 0; videoIndex < produceCount; videoIndex++) {
|
||||
List<MixTaskSaveReqVO.MaterialItem> selectedMaterials;
|
||||
|
||||
// 检查是否使用新格式(场景多候选)
|
||||
if (createReqVO.isUsingNewFormat() && createReqVO.getScenes() != null) {
|
||||
// 新格式:从每个场景的候选中随机选择一个素材
|
||||
selectedMaterials = selectRandomMaterialsFromScenes(
|
||||
createReqVO.getScenes(),
|
||||
videoIndex
|
||||
);
|
||||
} else {
|
||||
// 兼容旧格式:使用原有素材列表
|
||||
selectedMaterials = createReqVO.getMaterials();
|
||||
}
|
||||
|
||||
// 对选中的素材应用随机起点生成视频
|
||||
String jobIdWithUrl = batchProduceAlignment.produceSingleVideoWithOffset(
|
||||
createReqVO.getMaterials(),
|
||||
selectedMaterials,
|
||||
videoIndex,
|
||||
userId,
|
||||
createReqVO.getCropMode()
|
||||
@@ -411,6 +432,48 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景候选中随机选择素材(第一层随机)
|
||||
* @param scenes 场景配置列表
|
||||
* @param videoIndex 视频索引(用于随机种子)
|
||||
* @return 选中的素材列表
|
||||
*/
|
||||
private List<MixTaskSaveReqVO.MaterialItem> selectRandomMaterialsFromScenes(
|
||||
List<MixTaskSaveReqVO.SceneConfig> scenes, int videoIndex) {
|
||||
List<MixTaskSaveReqVO.MaterialItem> selected = new ArrayList<>();
|
||||
|
||||
for (int sceneIndex = 0; sceneIndex < scenes.size(); sceneIndex++) {
|
||||
MixTaskSaveReqVO.SceneConfig scene = scenes.get(sceneIndex);
|
||||
List<MixTaskSaveReqVO.MaterialItem> candidates = scene.getCandidates();
|
||||
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
log.warn("[MixTask][场景{}没有候选素材]", sceneIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 基于 videoIndex、sceneIndex 和素材ID创建确定性随机
|
||||
int seed = videoIndex * 1000 + sceneIndex * 100;
|
||||
if (!candidates.isEmpty() && candidates.get(0) != null) {
|
||||
seed += candidates.get(0).getFileId().intValue();
|
||||
}
|
||||
java.util.Random random = new java.util.Random(seed);
|
||||
|
||||
// 随机选择一个候选
|
||||
int selectedIndex = random.nextInt(candidates.size());
|
||||
MixTaskSaveReqVO.MaterialItem selectedCandidate = candidates.get(selectedIndex);
|
||||
|
||||
// 设置场景时长
|
||||
selectedCandidate.setDuration(scene.getDuration());
|
||||
|
||||
selected.add(selectedCandidate);
|
||||
|
||||
log.debug("[MixTask][视频{}场景{}选择素材{}]",
|
||||
videoIndex, sceneIndex, selectedCandidate.getFileId());
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
@@ -534,6 +597,81 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
* 校验混剪任务时长
|
||||
*/
|
||||
private void validateDuration(MixTaskSaveReqVO req) {
|
||||
// 检查是否使用新格式(场景多候选)
|
||||
if (req.isUsingNewFormat()) {
|
||||
// 新格式:验证场景配置
|
||||
validateScenesFormat(req);
|
||||
} else {
|
||||
// 兼容旧格式:验证素材列表
|
||||
validateMaterialsFormat(req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证新格式场景配置
|
||||
*/
|
||||
private void validateScenesFormat(MixTaskSaveReqVO req) {
|
||||
// 1. 场景列表不能为空
|
||||
if (req.getScenes() == null || req.getScenes().isEmpty()) {
|
||||
throw new IllegalArgumentException("场景列表不能为空");
|
||||
}
|
||||
|
||||
// 2. 验证每个场景
|
||||
for (int i = 0; i < req.getScenes().size(); i++) {
|
||||
MixTaskSaveReqVO.SceneConfig scene = req.getScenes().get(i);
|
||||
|
||||
// 场景序号验证
|
||||
if (scene.getIndex() == null) {
|
||||
scene.setIndex(i);
|
||||
}
|
||||
|
||||
// 候选列表验证
|
||||
if (scene.getCandidates() == null || scene.getCandidates().isEmpty()) {
|
||||
throw new IllegalArgumentException("场景" + i + "没有候选素材");
|
||||
}
|
||||
|
||||
// 候选数量限制(最多10个)
|
||||
if (scene.getCandidates().size() > 10) {
|
||||
throw new IllegalArgumentException("场景" + i + "候选数量不能超过10个");
|
||||
}
|
||||
|
||||
// 场景时长验证
|
||||
if (scene.getDuration() < 3 || scene.getDuration() > 5) {
|
||||
throw new IllegalArgumentException("场景" + i + "时长需在3-5秒之间,当前:" + scene.getDuration() + "秒");
|
||||
}
|
||||
|
||||
// 候选素材验证
|
||||
for (int j = 0; j < scene.getCandidates().size(); j++) {
|
||||
MixTaskSaveReqVO.MaterialItem candidate = scene.getCandidates().get(j);
|
||||
if (candidate.getFileId() == null) {
|
||||
throw new IllegalArgumentException("场景" + i + "候选" + j + "的文件ID不能为空");
|
||||
}
|
||||
if (candidate.getFileUrl() == null || candidate.getFileUrl().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("场景" + i + "候选" + j + "的文件URL不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 计算总时长
|
||||
int totalDuration = req.getScenes().stream()
|
||||
.mapToInt(MixTaskSaveReqVO.SceneConfig::getDuration)
|
||||
.sum();
|
||||
|
||||
// 4. 总时长校验(15s-30s)
|
||||
if (totalDuration < 15) {
|
||||
throw new IllegalArgumentException("总时长不能小于15秒,当前:" + totalDuration + "秒");
|
||||
}
|
||||
if (totalDuration > 30) {
|
||||
throw new IllegalArgumentException("总时长不能超过30秒,当前:" + totalDuration + "秒");
|
||||
}
|
||||
|
||||
log.info("[MixTask][新格式场景校验通过] totalDuration={}s, sceneCount={}", totalDuration, req.getScenes().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证旧格式素材列表
|
||||
*/
|
||||
private void validateMaterialsFormat(MixTaskSaveReqVO req) {
|
||||
// 1. 素材列表不能为空
|
||||
if (req.getMaterials() == null || req.getMaterials().isEmpty()) {
|
||||
throw new IllegalArgumentException("素材列表不能为空");
|
||||
@@ -559,6 +697,6 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[MixTask][时长校验通过] totalDuration={}s, materialCount={}", totalDuration, req.getMaterials().size());
|
||||
log.info("[MixTask][旧格式素材校验通过] totalDuration={}s, materialCount={}", totalDuration, req.getMaterials().size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,25 @@ public class MixTaskUtils {
|
||||
task.setTitle(reqVO.getTitle());
|
||||
task.setText(null); // 纯画面模式,不需要文案
|
||||
|
||||
// 存储素材配置JSON
|
||||
String materialsJson = JsonUtils.toJsonString(reqVO.getMaterials());
|
||||
// 存储素材配置JSON(新格式优先)
|
||||
String materialsJson;
|
||||
if (reqVO.isUsingNewFormat() && reqVO.getScenes() != null) {
|
||||
// 新格式:使用 getEffectiveMaterials() 转换 scenes 为 materials
|
||||
materialsJson = JsonUtils.toJsonString(reqVO.getEffectiveMaterials());
|
||||
} else {
|
||||
// 旧格式:直接使用 materials
|
||||
materialsJson = JsonUtils.toJsonString(reqVO.getMaterials());
|
||||
}
|
||||
task.setMaterialsJson(materialsJson);
|
||||
|
||||
// 兼容旧版本:同时存储 videoUrls(取第一个视频的URL,用于兼容查询)
|
||||
if (reqVO.getMaterials() != null && !reqVO.getMaterials().isEmpty()) {
|
||||
List<String> videoUrls = reqVO.getMaterials().stream()
|
||||
// 兼容旧版本:同时存储 videoUrls(用于兼容查询)
|
||||
// 优先使用新格式转换后的materials
|
||||
List<MixTaskSaveReqVO.MaterialItem> effectiveMaterials = reqVO.isUsingNewFormat()
|
||||
? reqVO.getEffectiveMaterials()
|
||||
: reqVO.getMaterials();
|
||||
|
||||
if (effectiveMaterials != null && !effectiveMaterials.isEmpty()) {
|
||||
List<String> videoUrls = effectiveMaterials.stream()
|
||||
.map(MixTaskSaveReqVO.MaterialItem::getFileUrl)
|
||||
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
|
||||
task.setVideoUrlList(videoUrls);
|
||||
|
||||
@@ -18,10 +18,43 @@ public class MixTaskSaveReqVO {
|
||||
@NotBlank(message = "视频标题不能为空")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "素材配置列表", required = true)
|
||||
@NotEmpty(message = "素材列表不能为空")
|
||||
@Schema(description = "素材配置列表(兼容旧版)")
|
||||
private List<MaterialItem> materials;
|
||||
|
||||
@Schema(description = "场景配置列表(新版本,支持多候选)")
|
||||
private List<SceneConfig> scenes;
|
||||
|
||||
/**
|
||||
* 获取有效的配置数据(优先使用新格式)
|
||||
*/
|
||||
public List<MaterialItem> getEffectiveMaterials() {
|
||||
// 优先使用新格式 scenes
|
||||
if (scenes != null && !scenes.isEmpty()) {
|
||||
// 转换场景格式为素材格式(取第一个候选)
|
||||
return scenes.stream()
|
||||
.map(scene -> {
|
||||
if (scene.getCandidates() != null && !scene.getCandidates().isEmpty()) {
|
||||
MaterialItem first = scene.getCandidates().get(0);
|
||||
first.setDuration(scene.getDuration());
|
||||
return first;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 兼容旧格式 materials
|
||||
return materials;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否使用新格式
|
||||
*/
|
||||
public boolean isUsingNewFormat() {
|
||||
return scenes != null && !scenes.isEmpty();
|
||||
}
|
||||
|
||||
@Schema(description = "生成数量", required = true, example = "1")
|
||||
@NotNull(message = "生成数量不能为空")
|
||||
private Integer produceCount = 1; // 默认生成1个
|
||||
@@ -50,4 +83,20 @@ public class MixTaskSaveReqVO {
|
||||
@Schema(description = "素材实际时长(秒)", example = "60")
|
||||
private Integer fileDuration;
|
||||
}
|
||||
|
||||
@Schema(description = "场景配置")
|
||||
@Data
|
||||
public static class SceneConfig {
|
||||
|
||||
@Schema(description = "场景序号", example = "0")
|
||||
private Integer index;
|
||||
|
||||
@Schema(description = "场景时长(秒)", required = true, example = "3")
|
||||
@NotNull(message = "场景时长不能为空")
|
||||
private Integer duration;
|
||||
|
||||
@Schema(description = "候选素材列表", required = true)
|
||||
@NotEmpty(message = "场景候选列表不能为空")
|
||||
private List<MaterialItem> candidates;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user