565 lines
14 KiB
Markdown
565 lines
14 KiB
Markdown
# 剪映导出算法详细说明
|
||
|
||
## 概述
|
||
|
||
本文档详细描述了将模板匹配结果导出为剪映 `draft_content.json` 格式的完整算法机制。该算法负责将项目中的模板匹配数据转换为剪映可识别和导入的草稿文件格式。
|
||
|
||
## 算法流程图
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[开始导出] --> B[获取匹配结果详情]
|
||
B --> C[创建基础剪映草稿结构]
|
||
C --> D[计算总时长]
|
||
D --> E[生成素材列表]
|
||
E --> F[生成轨道和片段]
|
||
F --> G[序列化为JSON]
|
||
G --> H[写入文件]
|
||
H --> I[返回文件路径]
|
||
|
||
E --> E1[查询MaterialSegment]
|
||
E1 --> E2[清理文件路径]
|
||
E2 --> E3[生成随机素材ID]
|
||
E3 --> E4[创建JianYingVideo对象]
|
||
|
||
F --> F1[遍历匹配片段]
|
||
F1 --> F2[创建JianYingSegment]
|
||
F2 --> F3[设置时间轴映射]
|
||
F3 --> F4[关联素材ID]
|
||
```
|
||
|
||
## 核心算法组件
|
||
|
||
### 1. 数据输入
|
||
|
||
**输入参数**:
|
||
- `result_id`: 模板匹配结果ID
|
||
- `output_path`: 输出文件路径
|
||
- `material_repository`: 素材数据仓库
|
||
|
||
**数据源**:
|
||
- `TemplateMatchingResultDetail`: 包含匹配结果和片段信息
|
||
- `MaterialSegment`: 素材片段的文件路径和元数据
|
||
|
||
### 2. 路径标准化算法
|
||
|
||
```rust
|
||
fn normalize_windows_path(path: &str) -> String {
|
||
// 移除 \\?\ 前缀(Windows长路径UNC格式)
|
||
if path.starts_with("\\\\?\\") {
|
||
path.strip_prefix("\\\\?\\").unwrap_or(path).to_string()
|
||
} else {
|
||
path.to_string()
|
||
}
|
||
}
|
||
```
|
||
|
||
**目的**:清理Windows UNC路径前缀,确保剪映兼容性
|
||
|
||
### 3. 时长计算算法
|
||
|
||
```rust
|
||
fn calculate_total_duration(detail: &TemplateMatchingResultDetail) -> Result<u64> {
|
||
let mut total_duration = 0u64;
|
||
|
||
for segment_result in &detail.segment_results {
|
||
if segment_result.end_time > total_duration {
|
||
total_duration = segment_result.end_time;
|
||
}
|
||
}
|
||
|
||
Ok(total_duration)
|
||
}
|
||
```
|
||
|
||
**逻辑**:遍历所有匹配片段,找到最大的结束时间作为总时长
|
||
|
||
### 4. 素材生成算法
|
||
|
||
#### 4.1 素材ID映射机制
|
||
|
||
```rust
|
||
// 为每个匹配的素材生成新的UUID
|
||
let new_material_id = Uuid::new_v4().to_string();
|
||
material_id_map.insert(segment_result.track_segment_id.clone(), new_material_id.clone());
|
||
```
|
||
|
||
**映射关系**:
|
||
- `track_segment_id` → `new_material_id`
|
||
- 确保每个模板片段对应唯一的剪映素材ID
|
||
|
||
#### 4.2 素材对象创建
|
||
|
||
```rust
|
||
let video = JianYingVideo {
|
||
id: new_material_id,
|
||
path: normalized_path.clone(),
|
||
duration: segment_result.segment_duration,
|
||
material_name: Path::new(&normalized_path).file_name()...,
|
||
// ... 其他属性
|
||
};
|
||
```
|
||
|
||
**关键属性**:
|
||
- `id`: 随机生成的UUID
|
||
- `path`: 清理后的文件路径
|
||
- `duration`: 片段时长(微秒)
|
||
- `material_name`: 从路径提取的文件名
|
||
|
||
### 5. 轨道生成算法
|
||
|
||
#### 5.1 片段时间轴映射
|
||
|
||
```rust
|
||
source_timerange: JianYingTimeRange {
|
||
duration: segment_result.segment_duration,
|
||
start: 0, // 从素材开始位置
|
||
},
|
||
target_timerange: JianYingTimeRange {
|
||
duration: segment_result.segment_duration,
|
||
start: segment_result.start_time, // 在时间轴上的位置
|
||
},
|
||
```
|
||
|
||
**时间轴逻辑**:
|
||
- `source_timerange`: 素材内部的时间范围
|
||
- `target_timerange`: 在项目时间轴上的位置
|
||
|
||
#### 5.2 素材关联
|
||
|
||
```rust
|
||
if let Some(material_id) = material_id_map.get(&segment_result.track_segment_id) {
|
||
segment.material_id = material_id.clone();
|
||
}
|
||
```
|
||
|
||
**关联机制**:通过 `track_segment_id` 查找对应的素材ID
|
||
|
||
## 数据结构转换
|
||
|
||
### 输入数据结构
|
||
|
||
```rust
|
||
struct TemplateMatchingResultDetail {
|
||
matching_result: TemplateMatchingResult,
|
||
segment_results: Vec<MatchingSegmentResult>,
|
||
failed_segment_results: Vec<MatchingFailedSegmentResult>,
|
||
}
|
||
|
||
struct MatchingSegmentResult {
|
||
track_segment_id: String,
|
||
material_segment_id: String,
|
||
segment_duration: u64,
|
||
start_time: u64,
|
||
end_time: u64,
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 输出数据结构
|
||
|
||
```rust
|
||
struct JianYingDraftContent {
|
||
canvas_config: JianYingCanvasConfig,
|
||
duration: u64,
|
||
materials: JianYingMaterials,
|
||
tracks: Vec<JianYingTrack>,
|
||
// ...
|
||
}
|
||
|
||
struct JianYingVideo {
|
||
id: String,
|
||
path: String,
|
||
duration: u64,
|
||
material_name: String,
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## 算法特性
|
||
|
||
### 1. 数据完整性保证
|
||
|
||
- **素材引用检查**:只生成实际被引用的素材
|
||
- **路径有效性**:验证MaterialSegment存在性
|
||
- **时间轴一致性**:确保时间映射准确
|
||
|
||
### 2. 性能优化
|
||
|
||
- **批量查询**:一次性获取所有需要的MaterialSegment
|
||
- **内存效率**:使用HashMap进行O(1)的ID映射查找
|
||
- **路径缓存**:避免重复的路径清理操作
|
||
|
||
### 3. 错误处理
|
||
|
||
```rust
|
||
// 素材片段不存在的处理
|
||
let material_segment = material_repository.get_segment_by_id_sync(&segment_result.material_segment_id)?
|
||
.ok_or_else(|| anyhow!("找不到素材片段: {}", segment_result.material_segment_id))?;
|
||
```
|
||
|
||
**错误类型**:
|
||
- 匹配结果不存在
|
||
- 素材片段缺失
|
||
- 文件路径无效
|
||
- 序列化失败
|
||
|
||
## 兼容性考虑
|
||
|
||
### 1. 剪映版本兼容
|
||
|
||
- **平台信息**:设置为Windows平台
|
||
- **版本标识**:使用剪映5.9.0格式
|
||
- **字段完整性**:包含所有必需字段
|
||
|
||
### 2. 文件格式标准
|
||
|
||
- **编码格式**:UTF-8
|
||
- **JSON格式**:Pretty print,便于调试
|
||
- **路径格式**:标准Windows路径(移除UNC前缀)
|
||
|
||
## 使用示例
|
||
|
||
### 调用方式
|
||
|
||
```rust
|
||
let file_path = service.export_to_jianying(
|
||
"result_id_123",
|
||
"C:/output/draft_content.json",
|
||
material_repository
|
||
).await?;
|
||
```
|
||
|
||
### 输出文件结构
|
||
|
||
```json
|
||
{
|
||
"canvas_config": {
|
||
"height": 1920,
|
||
"ratio": "9:16",
|
||
"width": 1080
|
||
},
|
||
"duration": 30000000,
|
||
"materials": {
|
||
"videos": [
|
||
{
|
||
"id": "uuid-generated",
|
||
"path": "C:\\path\\to\\video.mp4",
|
||
"duration": 5000000,
|
||
"material_name": "video.mp4"
|
||
}
|
||
]
|
||
},
|
||
"tracks": [
|
||
{
|
||
"segments": [
|
||
{
|
||
"material_id": "uuid-generated",
|
||
"source_timerange": {
|
||
"duration": 5000000,
|
||
"start": 0
|
||
},
|
||
"target_timerange": {
|
||
"duration": 5000000,
|
||
"start": 0
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## 算法复杂度
|
||
|
||
- **时间复杂度**:O(n),其中n为匹配片段数量
|
||
- **空间复杂度**:O(n),主要用于存储素材映射和输出结构
|
||
- **I/O复杂度**:O(n),每个片段需要一次数据库查询
|
||
|
||
## 扩展性设计
|
||
|
||
### 1. 支持多轨道
|
||
|
||
当前实现创建单个视频轨道,可扩展为:
|
||
- 音频轨道支持
|
||
- 多视频轨道并行
|
||
- 字幕轨道集成
|
||
|
||
### 2. 高级特效支持
|
||
|
||
预留接口支持:
|
||
- 转场效果
|
||
- 滤镜应用
|
||
- 动画效果
|
||
|
||
### 3. 模板参数化
|
||
|
||
支持模板级别的配置:
|
||
- 画布尺寸自定义
|
||
- 帧率设置
|
||
- 质量参数调整
|
||
|
||
## 详细算法步骤
|
||
|
||
### 步骤1:数据预处理
|
||
|
||
```rust
|
||
// 1. 获取匹配结果详情
|
||
let detail = self.get_matching_result_detail(result_id).await?
|
||
.ok_or_else(|| anyhow!("匹配结果不存在: {}", result_id))?;
|
||
|
||
// 2. 验证数据完整性
|
||
if detail.segment_results.is_empty() {
|
||
return Err(anyhow!("没有匹配的片段可供导出"));
|
||
}
|
||
```
|
||
|
||
### 步骤2:基础结构初始化
|
||
|
||
```rust
|
||
// 创建默认的剪映草稿内容
|
||
let mut draft_content = JianYingExportService::create_default_draft_content();
|
||
|
||
// 设置项目基本信息
|
||
draft_content.id = Uuid::new_v4().to_string();
|
||
draft_content.create_time = Utc::now().timestamp();
|
||
```
|
||
|
||
### 步骤3:素材处理流水线
|
||
|
||
```mermaid
|
||
graph LR
|
||
A[遍历匹配片段] --> B[查询MaterialSegment]
|
||
B --> C[路径标准化]
|
||
C --> D[生成素材ID]
|
||
D --> E[创建JianYingVideo]
|
||
E --> F[添加到素材列表]
|
||
```
|
||
|
||
**详细实现**:
|
||
|
||
```rust
|
||
for segment_result in &detail.segment_results {
|
||
// 1. 数据库查询
|
||
let material_segment = material_repository
|
||
.get_segment_by_id_sync(&segment_result.material_segment_id)?
|
||
.ok_or_else(|| anyhow!("找不到素材片段: {}", segment_result.material_segment_id))?;
|
||
|
||
// 2. 路径清理
|
||
let normalized_path = Self::normalize_windows_path(&material_segment.file_path);
|
||
|
||
// 3. ID生成和映射
|
||
let new_material_id = Uuid::new_v4().to_string();
|
||
material_id_map.insert(segment_result.track_segment_id.clone(), new_material_id.clone());
|
||
|
||
// 4. 素材对象构建
|
||
let video = JianYingVideo {
|
||
id: new_material_id,
|
||
path: normalized_path,
|
||
duration: segment_result.segment_duration,
|
||
// ... 其他属性设置
|
||
};
|
||
|
||
videos.push(video);
|
||
}
|
||
```
|
||
|
||
### 步骤4:轨道构建算法
|
||
|
||
```rust
|
||
// 创建主视频轨道
|
||
let track_id = Uuid::new_v4().to_string();
|
||
let mut segments = Vec::new();
|
||
|
||
for segment_result in &detail.segment_results {
|
||
if let Some(material_id) = material_id_map.get(&segment_result.track_segment_id) {
|
||
let segment = JianYingSegment {
|
||
id: Uuid::new_v4().to_string(),
|
||
material_id: material_id.clone(),
|
||
|
||
// 关键:时间轴映射
|
||
source_timerange: JianYingTimeRange {
|
||
duration: segment_result.segment_duration,
|
||
start: 0, // 素材内部起始位置
|
||
},
|
||
target_timerange: JianYingTimeRange {
|
||
duration: segment_result.segment_duration,
|
||
start: segment_result.start_time, // 项目时间轴位置
|
||
},
|
||
|
||
// 播放控制
|
||
speed: 1.0,
|
||
volume: 1.0,
|
||
visible: true,
|
||
|
||
// 视觉效果
|
||
clip: JianYingClip {
|
||
alpha: 1.0,
|
||
scale: JianYingScale { x: 1.0, y: 1.0 },
|
||
transform: JianYingTransform { x: 0.0, y: 0.0 },
|
||
rotation: 0.0,
|
||
flip: JianYingFlip { horizontal: false, vertical: false },
|
||
},
|
||
};
|
||
|
||
segments.push(segment);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 步骤5:数据序列化和输出
|
||
|
||
```rust
|
||
// 1. 设置计算得出的总时长
|
||
draft_content.duration = self.calculate_total_duration(&detail)?;
|
||
|
||
// 2. 分配素材和轨道
|
||
draft_content.materials.videos = materials;
|
||
draft_content.tracks = tracks;
|
||
|
||
// 3. JSON序列化
|
||
let json_content = serde_json::to_string_pretty(&draft_content)
|
||
.map_err(|e| anyhow!("序列化失败: {}", e))?;
|
||
|
||
// 4. 文件写入
|
||
std::fs::write(&output_file_path, json_content)
|
||
.map_err(|e| anyhow!("写入文件失败: {}", e))?;
|
||
```
|
||
|
||
## 关键算法优化
|
||
|
||
### 1. 内存管理优化
|
||
|
||
```rust
|
||
// 使用HashMap进行快速ID查找,避免O(n²)复杂度
|
||
let mut material_id_map: HashMap<String, String> = HashMap::with_capacity(detail.segment_results.len());
|
||
|
||
// 预分配Vector容量
|
||
let mut videos = Vec::with_capacity(detail.segment_results.len());
|
||
```
|
||
|
||
### 2. 错误恢复机制
|
||
|
||
```rust
|
||
// 容错处理:跳过无效片段而不是整体失败
|
||
for segment_result in &detail.segment_results {
|
||
match material_repository.get_segment_by_id_sync(&segment_result.material_segment_id) {
|
||
Ok(Some(material_segment)) => {
|
||
// 正常处理
|
||
},
|
||
Ok(None) => {
|
||
eprintln!("警告:跳过缺失的素材片段 {}", segment_result.material_segment_id);
|
||
continue;
|
||
},
|
||
Err(e) => {
|
||
eprintln!("错误:查询素材片段失败 {}: {}", segment_result.material_segment_id, e);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 路径处理优化
|
||
|
||
```rust
|
||
// 缓存路径清理结果,避免重复处理
|
||
let mut path_cache: HashMap<String, String> = HashMap::new();
|
||
|
||
fn get_normalized_path(path: &str, cache: &mut HashMap<String, String>) -> String {
|
||
cache.entry(path.to_string())
|
||
.or_insert_with(|| Self::normalize_windows_path(path))
|
||
.clone()
|
||
}
|
||
```
|
||
|
||
## 测试和验证
|
||
|
||
### 单元测试覆盖
|
||
|
||
```rust
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_normalize_windows_path() {
|
||
assert_eq!(
|
||
normalize_windows_path("\\\\?\\C:\\test\\file.mp4"),
|
||
"C:\\test\\file.mp4"
|
||
);
|
||
assert_eq!(
|
||
normalize_windows_path("C:\\normal\\path.mp4"),
|
||
"C:\\normal\\path.mp4"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_calculate_total_duration() {
|
||
// 测试时长计算逻辑
|
||
}
|
||
}
|
||
```
|
||
|
||
### 集成测试场景
|
||
|
||
1. **空匹配结果**:验证错误处理
|
||
2. **单片段导出**:基础功能验证
|
||
3. **多片段导出**:复杂场景测试
|
||
4. **路径特殊字符**:兼容性测试
|
||
5. **大文件导出**:性能测试
|
||
|
||
## 故障排除指南
|
||
|
||
### 常见问题
|
||
|
||
1. **路径包含UNC前缀**
|
||
- 症状:导出的JSON包含 `\\?\` 前缀
|
||
- 解决:确保 `normalize_windows_path` 函数正常工作
|
||
|
||
2. **素材片段缺失**
|
||
- 症状:导出时报错"找不到素材片段"
|
||
- 解决:检查数据库完整性,确保MaterialSegment存在
|
||
|
||
3. **时间轴不匹配**
|
||
- 症状:剪映中素材位置错误
|
||
- 解决:验证 `start_time` 和 `duration` 计算逻辑
|
||
|
||
4. **文件路径无效**
|
||
- 症状:剪映无法找到素材文件
|
||
- 解决:确保导出时文件路径存在且可访问
|
||
|
||
### 调试技巧
|
||
|
||
```rust
|
||
// 添加详细日志
|
||
println!("🎬 处理片段: {} -> {}", segment_result.track_segment_id, new_material_id);
|
||
println!("📁 素材路径: {} -> {}", material_segment.file_path, normalized_path);
|
||
println!("⏱️ 时间轴: {}μs - {}μs ({}μs)",
|
||
segment_result.start_time,
|
||
segment_result.end_time,
|
||
segment_result.segment_duration
|
||
);
|
||
```
|
||
|
||
## 性能基准
|
||
|
||
### 典型性能指标
|
||
|
||
- **10个片段**:< 100ms
|
||
- **100个片段**:< 500ms
|
||
- **1000个片段**:< 2s
|
||
|
||
### 性能瓶颈分析
|
||
|
||
1. **数据库查询**:占总时间的60-70%
|
||
2. **JSON序列化**:占总时间的20-25%
|
||
3. **文件I/O**:占总时间的5-10%
|
||
4. **路径处理**:占总时间的1-5%
|
||
|
||
### 优化建议
|
||
|
||
1. **批量查询**:一次查询所有MaterialSegment
|
||
2. **异步处理**:使用async/await优化I/O
|
||
3. **内存池**:重用对象减少分配开销
|
||
4. **压缩输出**:可选的JSON压缩
|