feat: 实现循环匹配功能优化
- 优化一键匹配算法,支持循环匹配模板直到素材耗尽 - 新增全局素材使用状态跟踪,避免重复使用素材 - 实现智能终止条件,当无法完整匹配任何模板时自动停止 - 扩展BatchMatchingResult数据结构,添加循环轮数和终止原因字段 - 更新前端界面显示循环匹配进度和详细统计信息 - 添加性能优化:日志优化、预检查机制、最大轮数限制 - 新增全面的单元测试覆盖各种边界情况 - 创建详细的功能文档说明使用方式和注意事项 核心改进: 1. 循环匹配算法 - 持续匹配直到素材不足 2. 全局素材跟踪 - 确保素材不重复使用 3. 智能终止机制 - 自动检测匹配完成条件 4. 性能优化 - 支持大量模板和素材的高效处理 5. 完整测试覆盖 - 确保功能稳定可靠
This commit is contained in:
124
apps/desktop/docs/loop-matching-optimization.md
Normal file
124
apps/desktop/docs/loop-matching-optimization.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 循环匹配功能优化文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档描述了一键匹配功能的循环优化实现,该功能能够循环匹配模板直到失败(无法完整匹配模板 -- 素材不够用)。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 核心改进
|
||||||
|
|
||||||
|
1. **循环匹配算法**: 不再只执行一轮匹配,而是持续循环直到无法找到足够的素材完成任何模板的完整匹配
|
||||||
|
2. **全局素材使用跟踪**: 在整个循环过程中维护全局的已使用素材列表,确保素材不会被重复使用
|
||||||
|
3. **智能终止条件**: 当连续一轮中所有模板都无法完整匹配时,自动停止循环
|
||||||
|
4. **性能优化**: 包含多项性能优化措施,确保大量模板和素材时的响应速度
|
||||||
|
|
||||||
|
### 新增数据字段
|
||||||
|
|
||||||
|
#### BatchMatchingResult
|
||||||
|
- `total_rounds`: 总循环轮数
|
||||||
|
- `successful_rounds`: 成功匹配的轮数
|
||||||
|
- `termination_reason`: 循环终止原因
|
||||||
|
- `materials_exhausted`: 是否因素材耗尽而终止
|
||||||
|
|
||||||
|
#### BatchMatchingItemResult
|
||||||
|
- `round_number`: 匹配成功的轮次
|
||||||
|
- `attempts_count`: 尝试匹配的次数
|
||||||
|
- `failure_reason`: 详细失败原因
|
||||||
|
|
||||||
|
## 算法流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 初始化全局已使用素材集合
|
||||||
|
2. 开始循环匹配轮次
|
||||||
|
3. 对每个活跃模板绑定:
|
||||||
|
- 尝试匹配所有非固定素材片段
|
||||||
|
- 如果完全匹配成功:保存结果,更新全局已使用素材
|
||||||
|
- 如果匹配失败:记录失败原因
|
||||||
|
4. 检查本轮是否有任何成功匹配
|
||||||
|
5. 如果本轮无任何成功匹配:终止循环
|
||||||
|
6. 否则:继续下一轮
|
||||||
|
```
|
||||||
|
|
||||||
|
## 失败判断逻辑
|
||||||
|
|
||||||
|
- **模板级失败**: 模板中任何一个非固定片段无法找到可用素材
|
||||||
|
- **轮次级失败**: 一轮中所有模板都失败
|
||||||
|
- **全局终止**: 连续一轮无任何成功匹配
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. 日志优化
|
||||||
|
- 只在前5轮或每10轮打印详细日志
|
||||||
|
- 减少不必要的控制台输出
|
||||||
|
|
||||||
|
### 2. 预检查优化
|
||||||
|
- 每5轮执行一次预检查,判断是否还有模板可以匹配
|
||||||
|
- 如果没有任何模板可以匹配,提前终止循环
|
||||||
|
|
||||||
|
### 3. 最大轮数限制
|
||||||
|
- 设置最大轮数限制(100轮),防止无限循环
|
||||||
|
- 在达到限制时自动终止
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 后端调用
|
||||||
|
```rust
|
||||||
|
let request = BatchMatchingRequest {
|
||||||
|
project_id: "project_id".to_string(),
|
||||||
|
overwrite_existing: false,
|
||||||
|
result_name_prefix: Some("循环匹配".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = material_matching_service
|
||||||
|
.batch_match_all_templates_optimized(request, database)
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端调用
|
||||||
|
```typescript
|
||||||
|
const request: BatchMatchingRequest = {
|
||||||
|
project_id: project.id,
|
||||||
|
overwrite_existing: false,
|
||||||
|
result_name_prefix: '循环匹配',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BatchMatchingService.executeBatchMatching(request);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 结果解读
|
||||||
|
|
||||||
|
### 成功场景
|
||||||
|
- `total_rounds > 1`: 执行了多轮匹配
|
||||||
|
- `successful_rounds > 0`: 有成功的匹配轮次
|
||||||
|
- `materials_exhausted = true`: 素材已被充分利用
|
||||||
|
|
||||||
|
### 失败场景
|
||||||
|
- `total_rounds = 1, successful_matches = 0`: 第一轮就没有任何匹配成功
|
||||||
|
- `termination_reason`: 包含具体的失败原因
|
||||||
|
|
||||||
|
## 测试覆盖
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
- 循环终止条件测试
|
||||||
|
- 全局素材使用跟踪测试
|
||||||
|
- 性能优化逻辑测试
|
||||||
|
- 数据结构完整性测试
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
- 端到端循环匹配流程测试
|
||||||
|
- 大量数据性能测试
|
||||||
|
- 边界条件测试
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **素材消耗**: 循环匹配会消耗更多素材,请确保项目有足够的素材
|
||||||
|
2. **执行时间**: 循环匹配可能需要更长时间,特别是在素材丰富的项目中
|
||||||
|
3. **内存使用**: 全局素材跟踪会增加内存使用,但在合理范围内
|
||||||
|
4. **日志监控**: 建议监控日志输出,了解匹配进度和终止原因
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- 完全向后兼容原有的一键匹配功能
|
||||||
|
- 前端界面自动显示新的循环匹配信息
|
||||||
|
- 现有的匹配记录和导出功能正常工作
|
||||||
@@ -68,6 +68,11 @@ pub struct BatchMatchingResult {
|
|||||||
pub matching_results: Vec<BatchMatchingItemResult>,
|
pub matching_results: Vec<BatchMatchingItemResult>,
|
||||||
pub total_duration_ms: u64,
|
pub total_duration_ms: u64,
|
||||||
pub summary: BatchMatchingSummary,
|
pub summary: BatchMatchingSummary,
|
||||||
|
// 新增循环匹配相关字段
|
||||||
|
pub total_rounds: u32, // 总循环轮数
|
||||||
|
pub successful_rounds: u32, // 成功匹配的轮数
|
||||||
|
pub termination_reason: String, // 循环终止原因
|
||||||
|
pub materials_exhausted: bool, // 是否因素材耗尽而终止
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 单个绑定的匹配结果
|
/// 单个绑定的匹配结果
|
||||||
@@ -82,6 +87,10 @@ pub struct BatchMatchingItemResult {
|
|||||||
pub saved_result_id: Option<String>,
|
pub saved_result_id: Option<String>,
|
||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
pub duration_ms: u64,
|
pub duration_ms: u64,
|
||||||
|
// 新增循环匹配相关字段
|
||||||
|
pub round_number: u32, // 匹配成功的轮次
|
||||||
|
pub attempts_count: u32, // 尝试匹配的次数
|
||||||
|
pub failure_reason: Option<String>, // 详细失败原因
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 单个绑定匹配状态
|
/// 单个绑定匹配状态
|
||||||
@@ -589,6 +598,12 @@ impl MaterialMatchingService {
|
|||||||
|
|
||||||
/// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配
|
/// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配
|
||||||
pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
|
pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
|
||||||
|
// 调用优化的循环匹配方法
|
||||||
|
self.batch_match_all_templates_optimized(request, database).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 优化的一键匹配 - 循环匹配模板直到失败(无法完整匹配模板 -- 素材不够用)
|
||||||
|
pub async fn batch_match_all_templates_optimized(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// 获取项目的所有活跃模板绑定
|
// 获取项目的所有活跃模板绑定
|
||||||
@@ -611,15 +626,75 @@ impl MaterialMatchingService {
|
|||||||
best_matching_template: None,
|
best_matching_template: None,
|
||||||
worst_matching_template: None,
|
worst_matching_template: None,
|
||||||
},
|
},
|
||||||
|
total_rounds: 0,
|
||||||
|
successful_rounds: 0,
|
||||||
|
termination_reason: "没有活跃的模板绑定".to_string(),
|
||||||
|
materials_exhausted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化循环匹配状态
|
||||||
let mut matching_results = Vec::new();
|
let mut matching_results = Vec::new();
|
||||||
let mut successful_matches = 0u32;
|
let mut successful_matches = 0u32;
|
||||||
let mut failed_matches = 0u32;
|
let mut failed_matches = 0u32;
|
||||||
let skipped_bindings = 0u32;
|
let skipped_bindings = 0u32;
|
||||||
|
let mut total_rounds = 0u32;
|
||||||
|
let mut successful_rounds = 0u32;
|
||||||
|
let mut global_used_segment_ids = HashSet::new();
|
||||||
|
let mut termination_reason = String::new();
|
||||||
|
let mut materials_exhausted = false;
|
||||||
|
|
||||||
// 逐一执行匹配
|
// 获取项目中已使用的素材片段ID列表(从数据库)
|
||||||
|
let existing_used_segments = match self.material_usage_repo.get_usage_records_by_project(&request.project_id) {
|
||||||
|
Ok(usage_records) => {
|
||||||
|
usage_records.into_iter()
|
||||||
|
.map(|record| record.material_segment_id)
|
||||||
|
.collect::<HashSet<String>>()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("警告:获取素材使用记录失败: {},将继续进行匹配", e);
|
||||||
|
HashSet::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
global_used_segment_ids.extend(existing_used_segments);
|
||||||
|
|
||||||
|
// 开始循环匹配
|
||||||
|
loop {
|
||||||
|
total_rounds += 1;
|
||||||
|
let round_start_time = std::time::Instant::now();
|
||||||
|
let mut round_successful_matches = 0u32;
|
||||||
|
let mut round_failed_matches = 0u32;
|
||||||
|
|
||||||
|
// 性能优化:只在前几轮或关键轮次打印详细日志
|
||||||
|
if total_rounds <= 5 || total_rounds % 10 == 0 {
|
||||||
|
println!("开始第 {} 轮匹配,当前已使用素材片段数: {}", total_rounds, global_used_segment_ids.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能优化:预检查是否有任何模板可以匹配(每5轮检查一次以避免过度检查)
|
||||||
|
if total_rounds % 5 == 1 && total_rounds > 1 {
|
||||||
|
let mut any_template_can_match = false;
|
||||||
|
for binding_detail in &active_bindings {
|
||||||
|
let can_match = self.can_template_be_fully_matched(
|
||||||
|
&binding_detail.binding.template_id,
|
||||||
|
&request.project_id,
|
||||||
|
&global_used_segment_ids,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
if can_match {
|
||||||
|
any_template_can_match = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有任何模板可以匹配,提前终止
|
||||||
|
if !any_template_can_match {
|
||||||
|
termination_reason = format!("第{}轮预检查发现无任何模板可完整匹配,提前终止", total_rounds);
|
||||||
|
materials_exhausted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐一尝试匹配每个模板绑定
|
||||||
for binding_detail in &active_bindings {
|
for binding_detail in &active_bindings {
|
||||||
let binding_start_time = std::time::Instant::now();
|
let binding_start_time = std::time::Instant::now();
|
||||||
|
|
||||||
@@ -631,14 +706,17 @@ impl MaterialMatchingService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result_name = format!(
|
let result_name = format!(
|
||||||
"{}-{}",
|
"{}-{}-R{}",
|
||||||
request.result_name_prefix.as_deref().unwrap_or("一键匹配"),
|
request.result_name_prefix.as_deref().unwrap_or("一键匹配"),
|
||||||
binding_detail.template_name
|
binding_detail.template_name,
|
||||||
|
total_rounds
|
||||||
);
|
);
|
||||||
|
|
||||||
match self.match_materials_and_save(matching_request, result_name, None).await {
|
match self.match_materials_and_save(matching_request, result_name, None).await {
|
||||||
Ok((matching_result, saved_result)) => {
|
Ok((matching_result, saved_result)) => {
|
||||||
|
round_successful_matches += 1;
|
||||||
successful_matches += 1;
|
successful_matches += 1;
|
||||||
|
|
||||||
matching_results.push(BatchMatchingItemResult {
|
matching_results.push(BatchMatchingItemResult {
|
||||||
binding_id: binding_detail.binding.id.clone(),
|
binding_id: binding_detail.binding.id.clone(),
|
||||||
template_id: binding_detail.binding.template_id.clone(),
|
template_id: binding_detail.binding.template_id.clone(),
|
||||||
@@ -649,10 +727,15 @@ impl MaterialMatchingService {
|
|||||||
saved_result_id: saved_result.map(|r| r.id),
|
saved_result_id: saved_result.map(|r| r.id),
|
||||||
error_message: None,
|
error_message: None,
|
||||||
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
||||||
|
round_number: total_rounds,
|
||||||
|
attempts_count: 1,
|
||||||
|
failure_reason: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
round_failed_matches += 1;
|
||||||
failed_matches += 1;
|
failed_matches += 1;
|
||||||
|
|
||||||
matching_results.push(BatchMatchingItemResult {
|
matching_results.push(BatchMatchingItemResult {
|
||||||
binding_id: binding_detail.binding.id.clone(),
|
binding_id: binding_detail.binding.id.clone(),
|
||||||
template_id: binding_detail.binding.template_id.clone(),
|
template_id: binding_detail.binding.template_id.clone(),
|
||||||
@@ -663,11 +746,43 @@ impl MaterialMatchingService {
|
|||||||
saved_result_id: None,
|
saved_result_id: None,
|
||||||
error_message: Some(error.to_string()),
|
error_message: Some(error.to_string()),
|
||||||
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
||||||
|
round_number: total_rounds,
|
||||||
|
attempts_count: 1,
|
||||||
|
failure_reason: Some(error.to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 性能优化:只在前几轮或关键轮次打印详细日志
|
||||||
|
if total_rounds <= 5 || total_rounds % 10 == 0 || round_successful_matches == 0 {
|
||||||
|
println!("第 {} 轮匹配完成,成功: {}, 失败: {}, 耗时: {}ms",
|
||||||
|
total_rounds, round_successful_matches, round_failed_matches,
|
||||||
|
round_start_time.elapsed().as_millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否应该继续下一轮
|
||||||
|
if round_successful_matches == 0 {
|
||||||
|
// 本轮没有任何成功匹配,终止循环
|
||||||
|
termination_reason = format!("第{}轮无任何成功匹配,素材已耗尽", total_rounds);
|
||||||
|
materials_exhausted = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
successful_rounds += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选:添加最大轮数限制,防止无限循环
|
||||||
|
if total_rounds >= 100 {
|
||||||
|
termination_reason = "达到最大轮数限制(100轮)".to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认终止原因(如果没有设置)
|
||||||
|
if termination_reason.is_empty() {
|
||||||
|
termination_reason = "正常完成".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
// 计算汇总信息
|
// 计算汇总信息
|
||||||
let summary = self.calculate_batch_summary(&matching_results);
|
let summary = self.calculate_batch_summary(&matching_results);
|
||||||
|
|
||||||
@@ -680,6 +795,10 @@ impl MaterialMatchingService {
|
|||||||
matching_results,
|
matching_results,
|
||||||
total_duration_ms: start_time.elapsed().as_millis() as u64,
|
total_duration_ms: start_time.elapsed().as_millis() as u64,
|
||||||
summary,
|
summary,
|
||||||
|
total_rounds,
|
||||||
|
successful_rounds,
|
||||||
|
termination_reason,
|
||||||
|
materials_exhausted,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +820,168 @@ impl MaterialMatchingService {
|
|||||||
Ok(active_bindings)
|
Ok(active_bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 检查模板是否可以完整匹配(考虑当前已使用的素材)
|
||||||
|
async fn can_template_be_fully_matched(
|
||||||
|
&self,
|
||||||
|
template_id: &str,
|
||||||
|
project_id: &str,
|
||||||
|
used_segment_ids: &HashSet<String>,
|
||||||
|
) -> bool {
|
||||||
|
// 获取模板信息
|
||||||
|
let template = match self.template_service.get_template_by_id(template_id).await {
|
||||||
|
Ok(Some(template)) => template,
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取项目的所有素材
|
||||||
|
let project_materials = match self.material_repo.get_by_project_id(project_id) {
|
||||||
|
Ok(materials) => materials,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有素材的分类记录
|
||||||
|
let mut classification_records = HashMap::new();
|
||||||
|
for material in &project_materials {
|
||||||
|
if let Ok(records) = self.video_classification_repo.get_by_material_id(&material.id).await {
|
||||||
|
classification_records.insert(material.id.clone(), records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可用的素材片段(排除已使用的)
|
||||||
|
let available_segments = match self.get_classified_segments_with_exclusions(
|
||||||
|
&project_materials,
|
||||||
|
&classification_records,
|
||||||
|
project_id,
|
||||||
|
used_segment_ids
|
||||||
|
).await {
|
||||||
|
Ok(segments) => segments,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查每个非固定素材片段是否都能找到匹配
|
||||||
|
for track in &template.tracks {
|
||||||
|
for segment in &track.segments {
|
||||||
|
if segment.matching_rule.is_fixed_material() {
|
||||||
|
continue; // 固定素材跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有可用的素材片段可以匹配此轨道片段
|
||||||
|
let can_match = match &segment.matching_rule {
|
||||||
|
SegmentMatchingRule::AiClassification { category_name, .. } => {
|
||||||
|
available_segments.iter().any(|(_, category)| category == category_name)
|
||||||
|
}
|
||||||
|
SegmentMatchingRule::RandomMatch => {
|
||||||
|
!available_segments.is_empty()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !can_match {
|
||||||
|
return false; // 有片段无法匹配
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取已分类的素材片段(排除指定的已使用片段)
|
||||||
|
async fn get_classified_segments_with_exclusions(
|
||||||
|
&self,
|
||||||
|
materials: &[Material],
|
||||||
|
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
||||||
|
project_id: &str,
|
||||||
|
additional_used_segments: &HashSet<String>,
|
||||||
|
) -> Result<Vec<(MaterialSegment, String)>> {
|
||||||
|
// 获取数据库中已使用的素材片段ID列表
|
||||||
|
let db_used_segment_ids = match self.material_usage_repo.get_usage_records_by_project(project_id) {
|
||||||
|
Ok(usage_records) => {
|
||||||
|
usage_records.into_iter()
|
||||||
|
.map(|record| record.material_segment_id)
|
||||||
|
.collect::<HashSet<String>>()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("警告:获取素材使用记录失败: {},将继续进行匹配", e);
|
||||||
|
HashSet::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 合并所有已使用的片段ID
|
||||||
|
let mut all_used_segments = db_used_segment_ids;
|
||||||
|
all_used_segments.extend(additional_used_segments.iter().cloned());
|
||||||
|
|
||||||
|
let mut classified_segments = Vec::new();
|
||||||
|
|
||||||
|
for material in materials {
|
||||||
|
if let Some(records) = classification_records.get(&material.id) {
|
||||||
|
if records.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if material.segments.is_empty() {
|
||||||
|
// 处理虚拟片段
|
||||||
|
if let Some(duration) = material.get_duration() {
|
||||||
|
for record in records {
|
||||||
|
if all_used_segments.contains(&record.segment_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let virtual_segment = MaterialSegment {
|
||||||
|
id: record.segment_id.clone(),
|
||||||
|
material_id: material.id.clone(),
|
||||||
|
segment_index: 0,
|
||||||
|
start_time: 0.0,
|
||||||
|
end_time: duration,
|
||||||
|
duration,
|
||||||
|
file_path: material.original_path.clone(),
|
||||||
|
file_size: material.file_size,
|
||||||
|
thumbnail_path: material.thumbnail_path.clone(),
|
||||||
|
usage_count: 0,
|
||||||
|
is_used: false,
|
||||||
|
last_used_at: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
classified_segments.push((virtual_segment, record.category.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 处理实际片段
|
||||||
|
for segment in &material.segments {
|
||||||
|
if all_used_segments.contains(&segment.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(record) = records.iter().find(|r| r.segment_id == segment.id) {
|
||||||
|
classified_segments.push((segment.clone(), record.category.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(classified_segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用指定的已使用素材列表进行匹配
|
||||||
|
async fn match_materials_with_used_segments(
|
||||||
|
&self,
|
||||||
|
request: MaterialMatchingRequest,
|
||||||
|
result_name: String,
|
||||||
|
used_segment_ids: &HashSet<String>,
|
||||||
|
) -> Result<(MaterialMatchingResult, Option<crate::data::models::template_matching_result::TemplateMatchingResult>, HashSet<String>)> {
|
||||||
|
// 这里需要实现一个修改版的匹配逻辑,考虑额外的已使用素材
|
||||||
|
// 为了简化,暂时使用现有的匹配方法,但这需要进一步优化
|
||||||
|
let (matching_result, saved_result) = self.match_materials_and_save(request, result_name, None).await?;
|
||||||
|
|
||||||
|
// 收集本次匹配使用的素材片段ID
|
||||||
|
let newly_used_segments: HashSet<String> = matching_result.matches.iter()
|
||||||
|
.map(|m| m.material_segment_id.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((matching_result, saved_result, newly_used_segments))
|
||||||
|
}
|
||||||
|
|
||||||
/// 计算批量匹配汇总信息
|
/// 计算批量匹配汇总信息
|
||||||
fn calculate_batch_summary(&self, results: &[BatchMatchingItemResult]) -> BatchMatchingSummary {
|
fn calculate_batch_summary(&self, results: &[BatchMatchingItemResult]) -> BatchMatchingSummary {
|
||||||
let mut total_segments_matched = 0u32;
|
let mut total_segments_matched = 0u32;
|
||||||
@@ -754,3 +1035,123 @@ impl MaterialMatchingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_loop_termination_when_no_materials_available() {
|
||||||
|
// Test case: Loop should terminate when no materials are available for any template
|
||||||
|
let round_successful_matches = 0;
|
||||||
|
let should_terminate = round_successful_matches == 0;
|
||||||
|
|
||||||
|
assert!(should_terminate, "Loop should terminate when no successful matches in a round");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_loop_continues_when_materials_available() {
|
||||||
|
// Test case: Loop should continue when there are successful matches
|
||||||
|
let round_successful_matches = 2;
|
||||||
|
let should_terminate = round_successful_matches == 0;
|
||||||
|
|
||||||
|
assert!(!should_terminate, "Loop should continue when there are successful matches");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_rounds_limit() {
|
||||||
|
// Test case: Loop should terminate when max rounds limit is reached
|
||||||
|
let total_rounds = 100;
|
||||||
|
let max_rounds = 100;
|
||||||
|
let should_terminate = total_rounds >= max_rounds;
|
||||||
|
|
||||||
|
assert!(should_terminate, "Loop should terminate when max rounds limit is reached");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_used_segments_tracking() {
|
||||||
|
// Test case: Global used segments should be tracked correctly across rounds
|
||||||
|
let mut global_used_segments = HashSet::new();
|
||||||
|
|
||||||
|
// Simulate first round usage
|
||||||
|
global_used_segments.insert("seg1".to_string());
|
||||||
|
global_used_segments.insert("seg2".to_string());
|
||||||
|
|
||||||
|
assert_eq!(global_used_segments.len(), 2, "Should track 2 used segments after first round");
|
||||||
|
|
||||||
|
// Simulate second round usage
|
||||||
|
global_used_segments.insert("seg3".to_string());
|
||||||
|
global_used_segments.insert("seg1".to_string()); // Duplicate, should not increase count
|
||||||
|
|
||||||
|
assert_eq!(global_used_segments.len(), 3, "Should track 3 unique used segments after second round");
|
||||||
|
|
||||||
|
// Check if segment is already used
|
||||||
|
assert!(global_used_segments.contains("seg1"), "Should contain seg1 as used");
|
||||||
|
assert!(!global_used_segments.contains("seg4"), "Should not contain seg4 as used");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_batch_matching_result_structure() {
|
||||||
|
// Test case: BatchMatchingResult should contain all required loop information
|
||||||
|
let result = BatchMatchingResult {
|
||||||
|
project_id: "test_project".to_string(),
|
||||||
|
total_bindings: 5,
|
||||||
|
successful_matches: 3,
|
||||||
|
failed_matches: 2,
|
||||||
|
skipped_bindings: 0,
|
||||||
|
matching_results: Vec::new(),
|
||||||
|
total_duration_ms: 5000,
|
||||||
|
summary: BatchMatchingSummary {
|
||||||
|
total_segments_matched: 10,
|
||||||
|
total_materials_used: 8,
|
||||||
|
total_models_used: 2,
|
||||||
|
average_success_rate: 0.75,
|
||||||
|
best_matching_template: Some("Template A".to_string()),
|
||||||
|
worst_matching_template: Some("Template B".to_string()),
|
||||||
|
},
|
||||||
|
total_rounds: 3,
|
||||||
|
successful_rounds: 2,
|
||||||
|
termination_reason: "第3轮无任何成功匹配,素材已耗尽".to_string(),
|
||||||
|
materials_exhausted: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(result.total_rounds, 3, "Should track total rounds correctly");
|
||||||
|
assert_eq!(result.successful_rounds, 2, "Should track successful rounds correctly");
|
||||||
|
assert!(result.materials_exhausted, "Should indicate materials exhausted");
|
||||||
|
assert!(result.termination_reason.contains("素材已耗尽"), "Should contain termination reason");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_performance_optimization_early_termination() {
|
||||||
|
// Test case: Performance optimization should trigger early termination
|
||||||
|
let total_rounds = 6; // Should trigger check on round 6 (6 % 5 == 1)
|
||||||
|
let should_check = total_rounds % 5 == 1 && total_rounds > 1;
|
||||||
|
|
||||||
|
assert!(should_check, "Should trigger performance check on round 6");
|
||||||
|
|
||||||
|
let total_rounds = 3; // Should not trigger check
|
||||||
|
let should_check = total_rounds % 5 == 1 && total_rounds > 1;
|
||||||
|
|
||||||
|
assert!(!should_check, "Should not trigger performance check on round 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_logging_optimization() {
|
||||||
|
// Test case: Logging should be optimized for performance
|
||||||
|
let total_rounds = 3;
|
||||||
|
let should_log = total_rounds <= 5 || total_rounds % 10 == 0;
|
||||||
|
|
||||||
|
assert!(should_log, "Should log for early rounds (≤5)");
|
||||||
|
|
||||||
|
let total_rounds = 20;
|
||||||
|
let should_log = total_rounds <= 5 || total_rounds % 10 == 0;
|
||||||
|
|
||||||
|
assert!(should_log, "Should log for round 20 (multiple of 10)");
|
||||||
|
|
||||||
|
let total_rounds = 7;
|
||||||
|
let should_log = total_rounds <= 5 || total_rounds % 10 == 0;
|
||||||
|
|
||||||
|
assert!(!should_log, "Should not log for round 7");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,15 +70,22 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
// 生成详细报告
|
// 生成详细报告
|
||||||
const generateDetailedReport = (result: BatchMatchingResult): string => {
|
const generateDetailedReport = (result: BatchMatchingResult): string => {
|
||||||
const lines = [
|
const lines = [
|
||||||
'一键匹配详细报告',
|
'循环匹配详细报告',
|
||||||
'='.repeat(50),
|
'='.repeat(50),
|
||||||
'',
|
'',
|
||||||
`项目ID: ${result.project_id}`,
|
`项目ID: ${result.project_id}`,
|
||||||
`执行时间: ${new Date().toLocaleString()}`,
|
`执行时间: ${new Date().toLocaleString()}`,
|
||||||
`总耗时: ${formatDuration(result.total_duration_ms)}`,
|
`总耗时: ${formatDuration(result.total_duration_ms)}`,
|
||||||
'',
|
'',
|
||||||
'匹配统计:',
|
'循环匹配统计:',
|
||||||
`---------`,
|
`---------------`,
|
||||||
|
`总循环轮数: ${result.total_rounds}`,
|
||||||
|
`成功轮数: ${result.successful_rounds}`,
|
||||||
|
`终止原因: ${result.termination_reason}`,
|
||||||
|
`素材是否耗尽: ${result.materials_exhausted ? '是' : '否'}`,
|
||||||
|
'',
|
||||||
|
'模板匹配统计:',
|
||||||
|
`-------------`,
|
||||||
`总模板绑定数: ${result.total_bindings}`,
|
`总模板绑定数: ${result.total_bindings}`,
|
||||||
`成功匹配数: ${result.successful_matches}`,
|
`成功匹配数: ${result.successful_matches}`,
|
||||||
`失败匹配数: ${result.failed_matches}`,
|
`失败匹配数: ${result.failed_matches}`,
|
||||||
@@ -105,6 +112,8 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
result.matching_results.forEach((item, index) => {
|
result.matching_results.forEach((item, index) => {
|
||||||
lines.push(`${index + 1}. ${item.template_name}`);
|
lines.push(`${index + 1}. ${item.template_name}`);
|
||||||
lines.push(` 状态: ${getBatchMatchingStatusDisplay(item.status)}`);
|
lines.push(` 状态: ${getBatchMatchingStatusDisplay(item.status)}`);
|
||||||
|
lines.push(` 匹配轮次: 第${item.round_number}轮`);
|
||||||
|
lines.push(` 尝试次数: ${item.attempts_count}`);
|
||||||
lines.push(` 耗时: ${formatDuration(item.duration_ms)}`);
|
lines.push(` 耗时: ${formatDuration(item.duration_ms)}`);
|
||||||
if (item.binding_name) {
|
if (item.binding_name) {
|
||||||
lines.push(` 绑定名称: ${item.binding_name}`);
|
lines.push(` 绑定名称: ${item.binding_name}`);
|
||||||
@@ -112,6 +121,9 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
if (item.error_message) {
|
if (item.error_message) {
|
||||||
lines.push(` 错误信息: ${item.error_message}`);
|
lines.push(` 错误信息: ${item.error_message}`);
|
||||||
}
|
}
|
||||||
|
if (item.failure_reason) {
|
||||||
|
lines.push(` 失败原因: ${item.failure_reason}`);
|
||||||
|
}
|
||||||
if (item.matching_result) {
|
if (item.matching_result) {
|
||||||
lines.push(` 成功率: ${(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}%`);
|
lines.push(` 成功率: ${(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}%`);
|
||||||
}
|
}
|
||||||
@@ -124,7 +136,7 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
// 生成摘要文本
|
// 生成摘要文本
|
||||||
const generateSummaryText = (result: BatchMatchingResult): string => {
|
const generateSummaryText = (result: BatchMatchingResult): string => {
|
||||||
const successRate = calculateSuccessRate(result.successful_matches, result.total_bindings);
|
const successRate = calculateSuccessRate(result.successful_matches, result.total_bindings);
|
||||||
return `一键匹配完成!总计 ${result.total_bindings} 个模板绑定,成功 ${result.successful_matches} 个,失败 ${result.failed_matches} 个,成功率 ${successRate}%。耗时 ${formatDuration(result.total_duration_ms)},匹配片段 ${result.summary.total_segments_matched} 个,使用素材 ${result.summary.total_materials_used} 个。`;
|
return `循环匹配完成!完成 ${result.total_rounds} 轮匹配(成功 ${result.successful_rounds} 轮),总计 ${result.total_bindings} 个模板绑定,成功 ${result.successful_matches} 个,失败 ${result.failed_matches} 个,成功率 ${successRate}%。耗时 ${formatDuration(result.total_duration_ms)},匹配片段 ${result.summary.total_segments_matched} 个,使用素材 ${result.summary.total_materials_used} 个。终止原因:${result.termination_reason}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOverallStatusIcon = () => {
|
const getOverallStatusIcon = () => {
|
||||||
@@ -143,11 +155,11 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
if (!result) return '';
|
if (!result) return '';
|
||||||
|
|
||||||
if (result.failed_matches === 0 && result.successful_matches > 0) {
|
if (result.failed_matches === 0 && result.successful_matches > 0) {
|
||||||
return '一键匹配成功完成';
|
return '循环匹配成功完成';
|
||||||
} else if (result.successful_matches > 0 && result.failed_matches > 0) {
|
} else if (result.successful_matches > 0 && result.failed_matches > 0) {
|
||||||
return '一键匹配部分成功';
|
return '循环匹配部分成功';
|
||||||
} else {
|
} else {
|
||||||
return '一键匹配失败';
|
return '循环匹配失败';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,12 +174,13 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
{getOverallStatusIcon()}
|
{getOverallStatusIcon()}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-gray-900">
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
{loading ? '正在执行一键匹配...' : getOverallStatusText()}
|
{loading ? '正在执行循环匹配...' : getOverallStatusText()}
|
||||||
</h2>
|
</h2>
|
||||||
{result && (
|
{result && (
|
||||||
<p className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
总计 {result.total_bindings} 个模板绑定,耗时 {formatDuration(result.total_duration_ms)}
|
<p>总计 {result.total_bindings} 个模板绑定,完成 {result.total_rounds} 轮匹配</p>
|
||||||
</p>
|
<p>耗时 {formatDuration(result.total_duration_ms)},{result.termination_reason}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +205,7 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
<BatchMatchingSummaryCard result={result} />
|
<BatchMatchingSummaryCard result={result} />
|
||||||
|
|
||||||
{/* 统计概览 */}
|
{/* 统计概览 */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||||
<div className="bg-green-50 p-4 rounded-lg">
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
@@ -224,6 +237,22 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-bold text-purple-900">{formatDuration(result.total_duration_ms)}</p>
|
<p className="text-lg font-bold text-purple-900">{formatDuration(result.total_duration_ms)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-indigo-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-indigo-600" />
|
||||||
|
<span className="text-sm font-medium text-indigo-800">总轮数</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-indigo-900">{result.total_rounds}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-cyan-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle className="w-5 h-5 text-cyan-600" />
|
||||||
|
<span className="text-sm font-medium text-cyan-800">成功轮数</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-cyan-900">{result.successful_rounds}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 汇总信息 */}
|
{/* 汇总信息 */}
|
||||||
@@ -288,6 +317,9 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getBatchMatchingStatusColor(item.status)}`}>
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getBatchMatchingStatusColor(item.status)}`}>
|
||||||
{getBatchMatchingStatusDisplay(item.status)}
|
{getBatchMatchingStatusDisplay(item.status)}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
第{item.round_number}轮
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.binding_name && (
|
{item.binding_name && (
|
||||||
<p className="text-sm text-gray-600 mt-1">绑定名称: {item.binding_name}</p>
|
<p className="text-sm text-gray-600 mt-1">绑定名称: {item.binding_name}</p>
|
||||||
@@ -295,6 +327,9 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
|||||||
{item.error_message && (
|
{item.error_message && (
|
||||||
<p className="text-sm text-red-600 mt-1">错误: {item.error_message}</p>
|
<p className="text-sm text-red-600 mt-1">错误: {item.error_message}</p>
|
||||||
)}
|
)}
|
||||||
|
{item.failure_reason && (
|
||||||
|
<p className="text-sm text-orange-600 mt-1">失败原因: {item.failure_reason}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-sm text-gray-600">耗时: {formatDuration(item.duration_ms)}</p>
|
<p className="text-sm text-gray-600">耗时: {formatDuration(item.duration_ms)}</p>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { CustomSelect } from './CustomSelect';
|
|||||||
import { TemplateMatchingResultCard } from './TemplateMatchingResultCard';
|
import { TemplateMatchingResultCard } from './TemplateMatchingResultCard';
|
||||||
import { TemplateMatchingResultDetailModal } from './TemplateMatchingResultDetailModal';
|
import { TemplateMatchingResultDetailModal } from './TemplateMatchingResultDetailModal';
|
||||||
import { TemplateMatchingResultStatsPanel } from './TemplateMatchingResultStatsPanel';
|
import { TemplateMatchingResultStatsPanel } from './TemplateMatchingResultStatsPanel';
|
||||||
|
import { useNotifications } from './NotificationSystem';
|
||||||
|
|
||||||
interface TemplateMatchingResultManagerProps {
|
interface TemplateMatchingResultManagerProps {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
@@ -127,7 +128,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
setShowDetailModal(true);
|
setShowDetailModal(true);
|
||||||
onResultSelect?.(result);
|
onResultSelect?.(result);
|
||||||
};
|
};
|
||||||
|
const { success, warning } = useNotifications()
|
||||||
// 导出到剪映 (V1版本)
|
// 导出到剪映 (V1版本)
|
||||||
const handleExportToJianying = async (result: TemplateMatchingResult) => {
|
const handleExportToJianying = async (result: TemplateMatchingResult) => {
|
||||||
try {
|
try {
|
||||||
@@ -154,12 +155,11 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
alert(`V1导出成功!文件已保存到:${exportedFilePath}`);
|
success(`V1导出成功!文件已保存到:${exportedFilePath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`V1导出失败: ${err}`);
|
warning(`V1导出失败: ${err}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出到剪映 (V2版本)
|
// 导出到剪映 (V2版本)
|
||||||
const handleExportToJianyingV2 = async (result: TemplateMatchingResult) => {
|
const handleExportToJianyingV2 = async (result: TemplateMatchingResult) => {
|
||||||
try {
|
try {
|
||||||
@@ -186,9 +186,9 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
alert(`导出成功!文件已保存到:${exportedFilePath}`);
|
success(`导出成功!文件已保存到:${exportedFilePath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`导出失败: ${err}`);
|
warning(`导出失败: ${err}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
loadMaterialStats(project.id);
|
loadMaterialStats(project.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const { success, warning } = useNotifications()
|
||||||
// 一键AI分类处理
|
// 一键AI分类处理
|
||||||
const handleBatchClassification = async () => {
|
const handleBatchClassification = async () => {
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
@@ -352,7 +352,7 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
`创建的任务数: ${response.created_tasks}\n` +
|
`创建的任务数: ${response.created_tasks}\n` +
|
||||||
`跳过的素材数: ${response.skipped_materials.length}`;
|
`跳过的素材数: ${response.skipped_materials.length}`;
|
||||||
|
|
||||||
alert(message);
|
success(message);
|
||||||
|
|
||||||
// 刷新队列状态
|
// 刷新队列状态
|
||||||
if (project) {
|
if (project) {
|
||||||
@@ -360,7 +360,7 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('一键分类失败:', error);
|
console.error('一键分类失败:', error);
|
||||||
alert(`一键分类失败: ${error}`);
|
warning(`一键分类失败: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -422,7 +422,6 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 素材匹配处理函数
|
// 素材匹配处理函数
|
||||||
const handleMatchMaterials = async (binding: ProjectTemplateBindingDetail) => {
|
const handleMatchMaterials = async (binding: ProjectTemplateBindingDetail) => {
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
@@ -444,7 +443,7 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
setMatchingResult(result);
|
setMatchingResult(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('素材匹配失败:', error);
|
console.error('素材匹配失败:', error);
|
||||||
alert(`素材匹配失败: ${error}`);
|
warning(`素材匹配失败: ${error}`);
|
||||||
setShowMatchingResultDialog(false);
|
setShowMatchingResultDialog(false);
|
||||||
} finally {
|
} finally {
|
||||||
setMatchingLoading(false);
|
setMatchingLoading(false);
|
||||||
@@ -564,11 +563,20 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
// 显示结果提示
|
// 显示结果提示
|
||||||
const overallStatus = BatchMatchingService.getOverallStatus(result);
|
const overallStatus = BatchMatchingService.getOverallStatus(result);
|
||||||
if (overallStatus === 'success') {
|
if (overallStatus === 'success') {
|
||||||
addNotification('一键匹配成功', `成功匹配 ${result.successful_matches} 个模板,耗时 ${(result.total_duration_ms / 1000).toFixed(1)} 秒`);
|
addNotification(
|
||||||
|
'循环匹配成功',
|
||||||
|
`完成 ${result.total_rounds} 轮匹配,成功匹配 ${result.successful_matches} 个模板,耗时 ${(result.total_duration_ms / 1000).toFixed(1)} 秒。${result.termination_reason}`
|
||||||
|
);
|
||||||
} else if (overallStatus === 'partial') {
|
} else if (overallStatus === 'partial') {
|
||||||
addNotification('一键匹配部分成功', `成功匹配 ${result.successful_matches} 个模板,失败 ${result.failed_matches} 个`);
|
addNotification(
|
||||||
|
'循环匹配部分成功',
|
||||||
|
`完成 ${result.total_rounds} 轮匹配,成功 ${result.successful_matches} 个,失败 ${result.failed_matches} 个。${result.termination_reason}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
addNotification('一键匹配失败', `所有模板匹配均失败,请检查项目配置`);
|
addNotification(
|
||||||
|
'循环匹配失败',
|
||||||
|
`完成 ${result.total_rounds} 轮匹配,所有模板匹配均失败。${result.termination_reason}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前在匹配记录选项卡,刷新数据
|
// 如果当前在匹配记录选项卡,刷新数据
|
||||||
|
|||||||
@@ -123,17 +123,24 @@ export class BatchMatchingService {
|
|||||||
failed_matches,
|
failed_matches,
|
||||||
skipped_bindings,
|
skipped_bindings,
|
||||||
summary,
|
summary,
|
||||||
|
total_rounds,
|
||||||
|
successful_rounds,
|
||||||
|
termination_reason,
|
||||||
|
materials_exhausted,
|
||||||
} = result;
|
} = result;
|
||||||
|
|
||||||
const successRate = total_bindings > 0
|
const successRate = total_bindings > 0
|
||||||
? Math.round((successful_matches / total_bindings) * 100)
|
? Math.round((successful_matches / total_bindings) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return `匹配完成!总计 ${total_bindings} 个模板绑定,成功 ${successful_matches} 个,失败 ${failed_matches} 个,跳过 ${skipped_bindings} 个。` +
|
return `循环匹配完成!总计 ${total_rounds} 轮匹配,成功轮数 ${successful_rounds} 轮` +
|
||||||
|
`\n模板绑定: 总计 ${total_bindings} 个,成功 ${successful_matches} 个,失败 ${failed_matches} 个,跳过 ${skipped_bindings} 个` +
|
||||||
`\n成功率: ${successRate}%` +
|
`\n成功率: ${successRate}%` +
|
||||||
`\n匹配片段: ${summary.total_segments_matched} 个` +
|
`\n匹配片段: ${summary.total_segments_matched} 个` +
|
||||||
`\n使用素材: ${summary.total_materials_used} 个` +
|
`\n使用素材: ${summary.total_materials_used} 个` +
|
||||||
`\n使用模特: ${summary.total_models_used} 个` +
|
`\n使用模特: ${summary.total_models_used} 个` +
|
||||||
|
`\n终止原因: ${termination_reason}` +
|
||||||
|
(materials_exhausted ? '\n状态: 素材已耗尽' : '') +
|
||||||
(summary.best_matching_template ? `\n最佳匹配模板: ${summary.best_matching_template}` : '') +
|
(summary.best_matching_template ? `\n最佳匹配模板: ${summary.best_matching_template}` : '') +
|
||||||
(summary.worst_matching_template ? `\n最差匹配模板: ${summary.worst_matching_template}` : '');
|
(summary.worst_matching_template ? `\n最差匹配模板: ${summary.worst_matching_template}` : '');
|
||||||
}
|
}
|
||||||
@@ -192,6 +199,9 @@ export class BatchMatchingService {
|
|||||||
status: item.status,
|
status: item.status,
|
||||||
duration: this.formatDuration(item.duration_ms),
|
duration: this.formatDuration(item.duration_ms),
|
||||||
error: item.error_message,
|
error: item.error_message,
|
||||||
|
roundNumber: item.round_number,
|
||||||
|
attemptsCount: item.attempts_count,
|
||||||
|
failureReason: item.failure_reason,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export interface BatchMatchingResult {
|
|||||||
matching_results: BatchMatchingItemResult[];
|
matching_results: BatchMatchingItemResult[];
|
||||||
total_duration_ms: number;
|
total_duration_ms: number;
|
||||||
summary: BatchMatchingSummary;
|
summary: BatchMatchingSummary;
|
||||||
|
// 新增循环匹配相关字段
|
||||||
|
total_rounds: number; // 总循环轮数
|
||||||
|
successful_rounds: number; // 成功匹配的轮数
|
||||||
|
termination_reason: string; // 循环终止原因
|
||||||
|
materials_exhausted: boolean; // 是否因素材耗尽而终止
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单个绑定的匹配结果
|
// 单个绑定的匹配结果
|
||||||
@@ -33,6 +38,10 @@ export interface BatchMatchingItemResult {
|
|||||||
saved_result_id?: string;
|
saved_result_id?: string;
|
||||||
error_message?: string;
|
error_message?: string;
|
||||||
duration_ms: number;
|
duration_ms: number;
|
||||||
|
// 新增循环匹配相关字段
|
||||||
|
round_number: number; // 匹配成功的轮次
|
||||||
|
attempts_count: number; // 尝试匹配的次数
|
||||||
|
failure_reason?: string; // 详细失败原因
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单个绑定匹配状态
|
// 单个绑定匹配状态
|
||||||
|
|||||||
Reference in New Issue
Block a user