# 混剪逻辑与页面实现规格(基于阿里云ICE) ## 📋 核心需求 **输入**:用户选择素材分组中的视频 **输出**:2-3个**不同内容**的混剪视频 **规则**: - 每个素材可设置截取时长:**3s-15s**(默认3s) - 总时长限制:**15s-60s** - 素材处理:**从每条视频截取指定时长片段**,非简单拼接 - **视频差异性**:生成的2-3个视频必须内容不同,每个视频使用不同的素材组合 **底层引擎**:阿里云 ICE (Intelligent Collaboration Editor) --- ## 🎯 核心交互逻辑 ### 多视频生成机制 #### 素材分配算法 **核心原则**: 1. 确保每个生成的视频内容不同 2. **每个视频内素材时长随机截取**(3s-15s范围内) 3. **最后一个视频补全所有素材**,确保无遗漏 **算法一:分组循环分配(推荐)** ``` 场景:用户选择 9 个素材,生成 3 个视频 分配逻辑: ├─ 视频1:使用素材 [1, 2, 3] (随机时长:5s, 8s, 4s) ├─ 视频2:使用素材 [4, 5, 6] (随机时长:6s, 3s, 9s) └─ 视频3:使用素材 [7, 8, 9, 1, 2, 3, 4, 5, 6] (补全所有剩余素材,随机时长) 结果: - 视频1:3个素材,总时长约17s - 视频2:3个素材,总时长约18s - 视频3:9个素材,随机分配时长,总时长约27s ``` **算法二:交错分配** ``` 场景:用户选择 12 个素材,生成 2 个视频 分配逻辑: ├─ 视频1:使用素材 [1, 3, 5, 7, 9, 11] (奇数位,随机时长) └─ 视频2:使用素材 [2, 4, 6, 8, 10, 12, 1, 2, 3, 4, 5, 6] (偶数位 + 补全奇数位) 结果: - 视频1:6个素材,随机时长,总时长约18s - 视频2:12个素材(补全),随机时长,总时长约36s ``` **算法三:智能平衡分配** ``` 场景:用户选择 10 个素材,生成 3 个视频 分配逻辑: ├─ 视频1:素材 [1, 2, 3, 4] (4个素材,随机时长) ├─ 视频2:素材 [5, 6, 7, 8] (4个素材,随机时长) └─ 视频3:素材 [9, 10, 1, 2, 3, 4, 5, 6, 7, 8] (2个 + 补全前8个) 结果: - 视频1、2:各4个素材,随机时长 - 视频3:10个素材(补全所有),随机时长 ``` #### 随机时长截取 **规则**: - 每个素材的时长在3s-15s范围内**随机生成** - 随机种子基于素材ID和视频序号,确保可重现性 - 最后一个视频的素材时长也随机生成 **示例**: ``` 素材1(ID=123): - 视频1中使用:随机生成 7s - 视频2中使用:随机生成 4s - 视频3中使用:随机生成 12s 素材2(ID=456): - 视频1中使用:随机生成 9s - 视频2中使用:随机生成 6s - 视频3中使用:随机生成 3s ``` #### 最后一个视频补全逻辑 **补全规则**: - 最后一个视频(N号视频)必须包含**所有素材** - 前面(N-1)个视频使用部分素材,避免重复 - 补全时保持素材的随机时长特性 **实现流程**: ``` 1. 计算每个视频应分配的基础素材数 baseCount = 素材总数 ÷ 生成数量 2. 前(N-1)个视频各分配 baseCount 个素材 3. 最后一个视频分配: - 剩余所有素材(素材总数 - baseCount × (N-1)) - + 循环补全前面(N-1)个视频使用过的素材 - 确保包含所有素材 ``` #### 容错机制设计 **1. 视频时长不足容错** ``` 当素材时长不足时: ├─ 自动循环使用素材补充时长 ├─ 调整素材截取起始点(从素材中间截取) └─ 确保最终时长达到目标时长 ``` **2. ICE API调用容错** ``` 调用失败处理: ├─ 重试机制:最多重试3次,间隔时间递增(1s, 3s, 9s) ├─ 部分成功:如果部分视频成功,返回成功的视频 └─ 失败补偿:失败的视频可手动重试 ``` **3. 随机时长生成容错** ``` 随机生成失败时: ├─ 使用默认时长(5s)作为fallback ├─ 记录异常日志用于调试 └─ 继续后续流程 ``` **4. 素材不足容错** ``` 素材数量不足时: ├─ 循环使用素材填充所有视频 ├─ 调整素材时长(延长或缩短) └─ 保证每个视频至少包含2个素材 ``` **5. 任务状态容错** ``` 多视频状态跟踪: ├─ 部分成功:至少1个视频成功即算任务部分成功 ├─ 状态同步:定期同步所有视频状态 └─ 失败补偿:提供"补全失败视频"功能 ``` #### 目标时长分配 **每个视频的目标时长** = 总时长 ÷ 生成数量 **示例**: - 总时长:45s - 生成数量:3个 - 目标时长:15s/视频 --- ## 🏗️ 系统架构 ``` ┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 前端页面 │ │ 后端API │ │ 阿里云ICE │ │ │ │ │ │ │ │ /material/mix │◄──►│ MixTaskController│◄──►│ IceClient │ │ (独立混剪页面) │ │ MixTaskService │ │ │ │ │ │ │ │ Timeline构建 │ │ 素材时长选择 │ │ 时长校验 │ │ 片段截取 │ │ 总时长计算 │ │ 参数转换 │ │ 视频合成 │ └─────────────────────┘ └─────────────────┘ └─────────────────┘ ``` --- ## 📄 前端页面设计 ### 页面布局 ``` ┌────────────────────────────────────────────────────────────────────────┐ │ 📹 智能混剪 [返回素材列表] │ ├───────────────────────────────┬────────────────────────────────────────┤ │ │ │ │ ┌─ 混剪参数 ─────────────┐ │ ┌─ 素材预览 ────────────────────┐ │ │ │ │ │ │ │ │ │ │ 选择素材分组: │ │ │ ┌────┐ ┌────┐ ┌────┐ │ │ │ │ [下拉选择分组 ▼] │ │ │ │ 01 │ │ 02 │ │ 03 │ ... │ │ │ │ │ │ │ │封面│ │封面│ │封面│ │ │ │ │ 视频标题: │ │ │ │ │ │ │ │ │ │ │ │ │ [输入标题____________] │ │ │ │3s▼ │ │5s▼ │ │3s▼ │ │ │ │ │ │ │ │ └────┘ └────┘ └────┘ │ │ │ │ 生成数量: │ │ │ │ │ │ │ ○ 1个 ○ 2个 ● 3个 │ │ │ 已选择 5 个素材 │ │ │ │ │ │ │ │ │ │ │ ──────────────────────│ │ └──────────────────────────────┘ │ │ │ │ │ │ │ │ 📊 时长统计 │ │ ┌─ 时长概览 ────────────────────┐ │ │ │ ├─ 已选素材: 5 个 │ │ │ │ │ │ │ ├─ 总时长: 18s │ │ │ ████████████░░░░░░ 18s/60s │ │ │ │ └─ 限制: 15s-60s ✅ │ │ │ ✅ 时长合规 │ │ │ │ │ │ │ │ │ │ │ [🚀 开始混剪] │ │ └──────────────────────────────┘ │ │ │ │ │ │ │ └────────────────────────┘ │ │ │ │ │ └───────────────────────────────┴────────────────────────────────────────┘ ``` ### 核心组件实现 ```vue ``` --- ## 📡 后端API接口变更 ### 创建混剪任务(新格式) ```http POST /api/mix/create Content-Type: application/json { "title": "美食纪录片", "materials": [ { "fileId": 123, "fileUrl": "https://xxx.com/video1.mp4", "duration": 3 }, { "fileId": 456, "fileUrl": "https://xxx.com/video2.mp4", "duration": 5 }, { "fileId": 789, "fileUrl": "https://xxx.com/video3.mp4", "duration": 8 }, { "fileId": 101, "fileUrl": "https://xxx.com/video4.mp4", "duration": 4 }, { "fileId": 102, "fileUrl": "https://xxx.com/video5.mp4", "duration": 6 } ], "produceCount": 2 } ``` **返回**:`{"code": 0, "data": 12345}` (任务ID) **后端处理**: - 解析materials列表和produceCount - 使用分配算法将素材分配给每个视频 - 生成2个不同的视频,每个视频使用不同的素材组合 ### MixTaskSaveReqVO(新格式) ```java @Data public class MixTaskSaveReqVO { @NotBlank(message = "视频标题不能为空") private String title; @NotEmpty(message = "素材列表不能为空") @Valid private List materials; @NotNull(message = "生成数量不能为空") @Min(1) @Max(3) private Integer produceCount; @Data public static class MaterialItem { private Long fileId; // 素材文件ID private String fileUrl; // 素材URL @Min(3) @Max(15) private Integer duration; // 截取时长(秒): 3-15 } } ``` ### 素材分配算法实现 ```java /** * 素材分配算法 * 根据素材数量和生成数量,智能分配素材到每个视频 * 特性: * 1. 随机时长截取(3s-15s) * 2. 最后一个视频补全所有素材 */ public class MaterialDistribution { /** * 分配素材到多个视频(分组循环分配) * * @param materials 原始素材列表 * @param videoCount 生成视频数量 * @return 每个视频的素材列表(包含随机时长) */ public static List> distribute(List materials, int videoCount) { List> result = new ArrayList<>(); int materialCount = materials.size(); int baseCount = materialCount / videoCount; // 前(N-1)个视频:分配基础素材 Set usedMaterialIds = new HashSet<>(); for (int i = 0; i < videoCount - 1; i++) { List videoMaterials = new ArrayList<>(); for (int j = 0; j < baseCount; j++) { int index = i * baseCount + j; if (index < materialCount) { MaterialItem material = materials.get(index); usedMaterialIds.add(material.getFileId()); // 生成随机时长 material.setDuration(generateRandomDuration(material.getFileId(), i)); videoMaterials.add(material); } } result.add(videoMaterials); } // 最后一个视频:补全所有素材 List lastVideoMaterials = new ArrayList<>(); // 添加未使用的素材 for (MaterialItem material : materials) { if (!usedMaterialIds.contains(material.getFileId())) { material.setDuration(generateRandomDuration(material.getFileId(), videoCount - 1)); lastVideoMaterials.add(material); } } // 循环补全已使用的素材(随机选择部分) List recycledMaterials = new ArrayList<>(); for (MaterialItem material : materials) { if (usedMaterialIds.contains(material.getFileId())) { // 随机决定是否循环使用该素材 if (Math.random() > 0.3) { // 70%概率循环使用 MaterialItem recycled = new MaterialItem(); recycled.setFileId(material.getFileId()); recycled.setFileUrl(material.getFileUrl()); recycled.setDuration(generateRandomDuration(material.getFileId(), videoCount - 1)); recycledMaterials.add(recycled); } } } lastVideoMaterials.addAll(recycledMaterials); result.add(lastVideoMaterials); return result; } /** * 生成随机时长(3s-15s) * 使用素材ID和视频序号作为随机种子,确保可重现性 */ private static int generateRandomDuration(Long materialId, int videoIndex) { Random random = new Random(materialId * 1000L + videoIndex); return random.nextInt(13) + 3; // 3-15 inclusive } /** * 交错分配算法(奇偶分配)+ 补全逻辑 */ public static List> distributeByParity(List materials, int videoCount) { List> result = new ArrayList<>(); // 创建视频列表 for (int i = 0; i < videoCount; i++) { result.add(new ArrayList<>()); } // 按奇偶分配素材 Set usedMaterialIds = new HashSet<>(); for (int i = 0; i < materials.size(); i++) { int videoIndex = i % videoCount; MaterialItem material = materials.get(i); material.setDuration(generateRandomDuration(material.getFileId(), videoIndex)); result.get(videoIndex).add(material); usedMaterialIds.add(material.getFileId()); } // 最后一个视频补全所有素材 List lastVideo = result.get(videoCount - 1); for (MaterialItem material : materials) { if (!usedMaterialIds.contains(material.getFileId())) { MaterialItem supplement = new MaterialItem(); supplement.setFileId(material.getFileId()); supplement.setFileUrl(material.getFileUrl()); supplement.setDuration(generateRandomDuration(material.getFileId(), videoCount - 1)); lastVideo.add(supplement); } } return result; } } ``` --- ## 💾 数据模型变更 ### MixTaskDO(数据库表) ```java @TableName("tik_mix_task") public class MixTaskDO extends TenantBaseDO { private Long id; private Long userId; private String title; // 素材配置(JSON格式存储) // 格式: [{"fileId":123,"fileUrl":"...","duration":3}, ...] private String materialsJson; private Integer produceCount; private String jobIds; private String outputUrls; private String status; private Integer progress; private String errorMsg; private LocalDateTime finishTime; } ``` ### 数据库字段变更 ```sql -- 新增 materials_json 字段,替代原来的 video_urls ALTER TABLE tik_mix_task ADD COLUMN materials_json TEXT COMMENT '素材配置JSON'; -- 可选:保留 video_urls 做兼容,或迁移后删除 ``` --- ## ☁️ 阿里云ICE集成变更 ### Timeline 构建逻辑 ```java public String buildTimeline(List materials) { // ICE Timeline 结构 // 每个素材需要指定: // 1. MediaURL: 视频源地址 // 2. In: 开始时间(通常为0) // 3. Out: 结束时间 = duration StringBuilder tracks = new StringBuilder(); float currentTime = 0; for (MaterialItem material : materials) { tracks.append(String.format(""" { "MediaURL": "%s", "In": 0, "Out": %d, "TimelineIn": %.2f, "TimelineOut": %.2f } """, material.getFileUrl(), material.getDuration(), currentTime, currentTime + material.getDuration() )); currentTime += material.getDuration(); } return buildFullTimeline(tracks.toString()); } ``` ### ICE 关键参数说明 | 参数 | 说明 | |------|------| | MediaURL | 素材视频URL | | In | 素材起始时间(秒),从原视频的哪个时间点开始截取 | | Out | 素材结束时间(秒),截取到原视频的哪个时间点 | | TimelineIn | 在输出视频中的起始时间 | | TimelineOut | 在输出视频中的结束时间 | --- ## ⚠️ 校验规则 ### 前端校验 ```javascript // 时长校验规则 const validateDuration = () => { const total = totalDuration.value // 1. 总时长不能小于15秒 if (total < 15) { return { valid: false, msg: '总时长不足15秒' } } // 2. 总时长不能超过60秒 if (total > 60) { return { valid: false, msg: '总时长超过60秒' } } // 3. 至少选择1个素材 if (Object.keys(selectedMaterials.value).length === 0) { return { valid: false, msg: '请至少选择1个素材' } } return { valid: true } } ``` ### 后端校验 ```java public void validateMixTask(MixTaskSaveReqVO req) { // 1. 素材列表不能为空 if (req.getMaterials() == null || req.getMaterials().isEmpty()) { throw new ServiceException("素材列表不能为空"); } // 2. 计算总时长 int totalDuration = req.getMaterials().stream() .mapToInt(MaterialItem::getDuration) .sum(); // 3. 总时长校验 if (totalDuration < 15) { throw new ServiceException("总时长不能小于15秒"); } if (totalDuration > 60) { throw new ServiceException("总时长不能超过60秒"); } // 4. 单个素材时长校验 for (MaterialItem item : req.getMaterials()) { if (item.getDuration() < 3 || item.getDuration() > 15) { throw new ServiceException("单个素材时长需在3-15秒之间"); } } } ``` --- ## 🔄 完整流程 ``` 1. 用户进入混剪页面 (/material/mix) ↓ 2. 选择素材分组 ↓ 3. 加载分组内的视频素材 ↓ 4. 点击素材卡片进行选择 ↓ 5. 为每个选中的素材设置截取时长(3s-15s) ├─ 默认: 3s └─ 可选: 3s/5s/8s/10s/15s ↓ 6. 实时计算并显示总时长 ├─ 小于15s: 警告提示,禁止提交 ├─ 15s-60s: 正常,允许提交 └─ 大于60s: 警告提示,禁止提交 ↓ 7. 填写视频标题、选择生成数量(1-3个) ↓ 8. 点击"开始混剪" ↓ 9. 调用 POST /api/mix/create { title: "xxx", materials: [ { fileId: 1, fileUrl: "...", duration: 3 }, { fileId: 2, fileUrl: "...", duration: 5 }, { fileId: 3, fileUrl: "...", duration: 4 }, { fileId: 4, fileUrl: "...", duration: 6 }, { fileId: 5, fileUrl: "...", duration: 5 } ], produceCount: 2 } ↓ 10. 后端处理:素材分配算法 ├─ 计算目标时长:23s ÷ 2 = 11.5s ≈ 12s/视频 ├─ 分配素材: │ ├─ 视频1: [素材1(随机7s), 素材2(随机4s), 素材3(随机9s)] │ └─ 视频2: [素材4,5, 素材1,2,3,4,5 (补全所有)] │ └─ 包含所有9个素材,每个素材随机时长 ├─ **随机时长生成**:基于素材ID和视频序号生成可重现的随机值 └─ 构建多个ICE Timeline(每个视频独立Timeline) ↓ 11. 批量提交到ICE(2个任务) ├─ 任务1: 视频1的Timeline ├─ 任务2: 视频2的Timeline └─ 任务状态:pending → running → success/failed ↓ 12. 任务完成,生成2个不同内容的视频 ├─ 视频1: 使用素材1,2,3 ├─ 视频2: 使用素材4,5,1 └─ 用户下载/预览 ``` --- ## 📝 实现清单 ### 前端任务 - [x] 修改 Mix.vue:素材选择改为 Map 结构存储(fileId → duration) - [x] 添加时长选择下拉框(每个素材卡片) - [x] 添加总时长实时计算和显示 - [x] 添加时长校验警告提示 - [x] 修改提交数据格式(materials 数组替代 videoUrls) - [ ] **新增:多视频生成提示** - [ ] 在页面显示"将生成X个不同内容的视频"提示 - [ ] 优化生成数量选择UI,突出差异性 ### 后端任务 - [x] 修改 MixTaskSaveReqVO:新增 MaterialItem 内部类 - [x] 修改 MixTaskDO:新增 materialsJson 字段 - [x] 修改 MixTaskService:添加时长校验逻辑 - [x] 修改 ICE Timeline 构建:支持每个素材不同时长截取 - [x] 数据库迁移:新增 materials_json 字段 - [ ] **新增:多视频生成核心功能** - [ ] 实现 MaterialDistribution 素材分配算法类 - [ ] **新增:随机时长生成功能** - [ ] 实现 generateRandomDuration() 方法 - [ ] 使用素材ID和视频序号作为随机种子 - [ ] 确保随机值的可重现性 - [ ] **新增:最后一个视频补全功能** - [ ] 实现补全逻辑:最后一个视频包含所有素材 - [ ] 前(N-1)个视频避免重复使用素材 - [ ] 循环补全机制:随机选择部分素材再次使用 - [ ] 修改 createMixTask:支持批量生成不同视频 - [ ] 修改 submitToICE:循环提交多个视频任务 - [ ] 修改任务状态管理:支持多任务状态跟踪 - [ ] 更新任务完成逻辑:合并多个视频的结果 ### 算法实现任务 - [ ] **素材分配算法** - [x] 平均分配算法(默认) - [x] 交错分配算法(奇偶分配) - [ ] 智能平衡分配算法 - [ ] 素材不足时的循环分配处理 - [ ] **随机时长算法** - [x] 基于素材ID和视频序号的随机种子生成 - [x] 3s-15s范围内随机时长生成 - [x] 可重现性保证(相同输入产生相同输出) - [ ] **补全逻辑算法** - [x] 最后一个视频包含所有素材 - [x] 前(N-1)个视频去重分配 - [ ] 循环补全策略优化(70%概率循环使用) ### 容错机制实现任务 - [ ] **ICE API调用容错** - [ ] 实现重试机制(最多3次,间隔1s/3s/9s) - [ ] 部分成功处理:返回成功的视频 - [ ] 失败补偿:提供手动重试功能 - [ ] **视频时长不足容错** - [ ] 自动循环使用素材补充时长 - [ ] 调整素材截取起始点 - [ ] 确保最终时长达到目标 - [ ] **随机时长生成容错** - [ ] 添加默认时长fallback(5s) - [ ] 异常日志记录 - [ ] 继续流程机制 - [ ] **素材不足容错** - [ ] 循环使用素材填充 - [ ] 动态调整素材时长 - [ ] 保证每个视频至少2个素材 - [ ] **任务状态容错** - [ ] 部分成功状态定义(≥1个视频成功) - [ ] 定期状态同步机制 - [ ] 补全失败视频功能 ### 数据模型增强 - [ ] **新增:视频批次表**(可选) - [ ] 创建 tik_mix_video_batch 表 - [ ] 存储每个混剪任务生成的多个视频信息 - [ ] 记录素材分配结果和时长信息 ## ⚠️ 重大变更说明 ### 变更概述 本次更新引入**多视频差异化生成**功能,这是对原有混剪逻辑的重大升级。 ### 核心变更点 #### 1. 数据流变更 **原逻辑**: ``` 用户选择素材 → 生成1个视频(使用所有素材) ``` **新逻辑**: ``` 用户选择素材 → 分配算法 → 生成N个不同视频(每个视频使用不同素材组合) ``` #### 2. 技术实现复杂度 - **原实现**:单次ICE提交,1个Timeline,1个输出 - **新实现**:N次ICE提交,N个Timeline,N个输出,需要状态管理 #### 3. 资源消耗 - **CPU**:增加素材分配计算开销 - **内存**:需要存储多个视频的素材分配信息 - **API调用**:ICE API调用次数翻倍(N倍) - **存储**:任务表存储更多jobId和outputUrl ### 设计权衡 #### 优势 ✅ **用户体验提升**:一次操作获得多个不同视频,提高创作效率 ✅ **内容多样性**:避免生成的视频内容重复 ✅ **灵活性**:支持多种素材分配策略,满足不同需求 #### 挑战 ⚠️ **复杂度增加**:需要处理多个视频的状态同步 ⚠️ **失败处理**:部分视频失败时的补偿机制 ⚠️ **资源消耗**:N倍ICE调用可能带来成本压力 ### 兼容性考虑 - **向后兼容**:现有API保持不变,新增字段可选 - **数据迁移**:无需迁移,已有任务不受影响 - **平滑升级**:老版本任务继续使用原有逻辑 ### 性能优化建议 1. **并发提交**:多个ICE任务并发提交,提高效率 2. **状态缓存**:批量查询任务状态,减少API调用 3. **结果合并**:异步合并多个视频的完成状态 --- *文件:mix-logic-spec.md* *基于:阿里云ICE混剪接口 + 素材库系统* *更新时间:2025-12-14* *版本:v2.0 - 多视频差异化生成*