mixvideo-v2/apps/desktop/src/services/materialMatchingService.ts

262 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 素材匹配服务
* 遵循前端开发规范的服务层设计
*/
import { invoke } from '@tauri-apps/api/core';
import {
MaterialMatchingRequest,
MaterialMatchingResult,
ProjectMaterialMatchingStats,
TemplateBindingMatchingValidation,
MatchingError,
MatchingErrorType
} from '../types/materialMatching';
export class MaterialMatchingService {
/**
* 执行素材匹配
*/
static async executeMatching(request: MaterialMatchingRequest): Promise<MaterialMatchingResult> {
try {
return await invoke<MaterialMatchingResult>('execute_material_matching', { request });
} catch (error) {
throw this.handleMatchingError(error);
}
}
/**
* 获取项目的素材匹配统计信息
*/
static async getProjectMaterialStats(projectId: string): Promise<ProjectMaterialMatchingStats> {
try {
return await invoke<ProjectMaterialMatchingStats>('get_project_material_stats_for_matching', {
projectId
});
} catch (error) {
throw this.handleMatchingError(error);
}
}
/**
* 验证模板绑定是否可以进行素材匹配
*/
static async validateTemplateBinding(bindingId: string): Promise<TemplateBindingMatchingValidation> {
try {
return await invoke<TemplateBindingMatchingValidation>('validate_template_binding_for_matching', {
bindingId
});
} catch (error) {
throw this.handleMatchingError(error);
}
}
/**
* 检查项目是否准备好进行素材匹配
*/
static async checkProjectReadiness(projectId: string): Promise<{
isReady: boolean;
issues: string[];
stats: ProjectMaterialMatchingStats;
}> {
const stats = await this.getProjectMaterialStats(projectId);
const issues: string[] = [];
// 检查是否有素材
if (stats.total_materials === 0) {
issues.push('项目中没有素材,请先导入素材');
}
// 检查是否有已分类的片段
if (stats.classified_segments === 0) {
issues.push('没有已分类的素材片段请先进行AI分类');
}
// 检查分类率是否足够
if (stats.classification_rate < 0.5) {
issues.push(`分类率较低 (${(stats.classification_rate * 100).toFixed(1)}%),建议提高分类覆盖率`);
}
// 检查是否有可用的分类类别
if (stats.available_categories.length === 0) {
issues.push('没有可用的AI分类类别');
}
return {
isReady: issues.length === 0,
issues,
stats
};
}
/**
* 预估匹配结果
*/
static async estimateMatchingResult(request: MaterialMatchingRequest): Promise<{
estimated_matches: number;
estimated_failures: number;
estimated_success_rate: number;
potential_issues: string[];
}> {
// 获取项目统计信息
const stats = await this.getProjectMaterialStats(request.project_id);
// 验证模板绑定
const validation = await this.validateTemplateBinding(request.binding_id);
const potential_issues: string[] = [];
// 基于统计信息估算匹配结果
let estimated_matches = Math.min(
validation.matchable_segments,
Math.floor(stats.classified_segments * 0.8) // 假设80%的已分类片段可以匹配
);
let estimated_failures = validation.total_segments - estimated_matches;
// 检查潜在问题
if (stats.available_models < 2) {
potential_issues.push('模特数量较少,可能影响匹配多样性');
estimated_matches = Math.floor(estimated_matches * 0.7);
}
if (stats.classification_rate < 0.7) {
potential_issues.push('分类率较低,可能导致匹配失败');
estimated_matches = Math.floor(estimated_matches * 0.8);
}
if (stats.available_categories.length < 3) {
potential_issues.push('可用分类类别较少');
estimated_matches = Math.floor(estimated_matches * 0.9);
}
estimated_failures = validation.total_segments - estimated_matches;
const estimated_success_rate = validation.total_segments > 0
? estimated_matches / validation.total_segments
: 0;
return {
estimated_matches,
estimated_failures,
estimated_success_rate,
potential_issues
};
}
/**
* 格式化匹配统计信息为显示文本
*/
static formatMatchingStats(result: MaterialMatchingResult): {
summary: string;
details: string[];
} {
const { statistics } = result;
const summary = `匹配完成:${statistics.matched_segments}/${statistics.total_segments} 个片段 (${Math.min(statistics.success_rate * 100, 100).toFixed(1)}%)`;
const details = [
`成功匹配:${statistics.matched_segments} 个片段`,
`匹配失败:${statistics.failed_segments} 个片段`,
`使用素材:${statistics.used_materials}`,
`涉及模特:${statistics.used_models}`,
`成功率:${Math.min(statistics.success_rate * 100, 100).toFixed(1)}%`
];
return { summary, details };
}
/**
* 获取匹配失败原因的分类统计
*/
static analyzeFailureReasons(result: MaterialMatchingResult): {
[reason: string]: {
count: number;
segments: string[];
};
} {
const analysis: { [reason: string]: { count: number; segments: string[] } } = {};
result.failed_segments.forEach(failure => {
if (!analysis[failure.failure_reason]) {
analysis[failure.failure_reason] = {
count: 0,
segments: []
};
}
analysis[failure.failure_reason].count++;
analysis[failure.failure_reason].segments.push(failure.track_segment_name);
});
return analysis;
}
/**
* 生成匹配改进建议
*/
static generateImprovementSuggestions(result: MaterialMatchingResult, stats: ProjectMaterialMatchingStats): string[] {
const suggestions: string[] = [];
// 基于失败原因生成建议
const failureAnalysis = this.analyzeFailureReasons(result);
Object.keys(failureAnalysis).forEach(reason => {
const count = failureAnalysis[reason].count;
if (reason.includes('没有找到分类')) {
suggestions.push(`${count} 个片段因缺少对应分类而匹配失败,建议增加相关分类的素材`);
} else if (reason.includes('时长要求')) {
suggestions.push(`${count} 个片段因时长不足而匹配失败,建议导入更长的素材片段`);
} else if (reason.includes('可用素材')) {
suggestions.push(`${count} 个片段因没有可用素材而匹配失败,建议增加更多素材`);
}
});
// 基于统计信息生成建议
if (stats.classification_rate < 0.8) {
suggestions.push('建议对更多素材进行AI分类以提高匹配成功率');
}
if (stats.available_models < 3) {
suggestions.push('建议增加更多模特以提高匹配多样性');
}
if (result.statistics.success_rate < 0.6) {
suggestions.push('匹配成功率较低,建议检查模板的匹配规则设置');
}
return suggestions;
}
/**
* 处理匹配错误
*/
private static handleMatchingError(error: any): MatchingError {
let errorType: MatchingErrorType = MatchingErrorType.NoClassifiedSegments;
let message = '素材匹配失败';
if (typeof error === 'string') {
message = error;
if (error.includes('模板不存在')) {
errorType = MatchingErrorType.TemplateNotFound;
} else if (error.includes('项目不存在')) {
errorType = MatchingErrorType.ProjectNotFound;
} else if (error.includes('没有已分类')) {
errorType = MatchingErrorType.NoClassifiedSegments;
} else if (error.includes('时长不足')) {
errorType = MatchingErrorType.InsufficientDuration;
}
} else if (error?.message) {
message = error.message;
}
return {
type: errorType,
message,
details: error
};
}
}