diff --git a/scripts/test_material_category_filter.md b/scripts/test_material_category_filter.md new file mode 100644 index 0000000..0849524 --- /dev/null +++ b/scripts/test_material_category_filter.md @@ -0,0 +1,200 @@ +# 素材分类筛选功能测试指南 + +## 🧪 测试准备 + +### 1. 确保分类数据存在 +在测试前,确保系统中已有分类数据: +- 进入"资源分类管理"页面 +- 创建几个测试分类(如:商业、教育、科技、娱乐) +- 为每个分类设置不同的颜色和AI提示 + +### 2. 准备测试素材 +上传一些测试素材,文件名和标签包含分类关键词: + +**商业类素材**: +- 文件名:`商业宣传片.mp4`, `business_intro.mp4` +- 标签:["商业", "企业", "宣传"] + +**教育类素材**: +- 文件名:`教育培训视频.mp4`, `education_demo.mp4` +- 标签:["教育", "培训", "学习"] + +**科技类素材**: +- 文件名:`科技产品演示.mp4`, `tech_showcase.mp4` +- 标签:["科技", "技术", "创新"] + +## 🔍 测试用例 + +### 测试用例1: 基础分类筛选 + +**步骤**: +1. 进入任意项目详情页面 +2. 查看素材管理区域的分类筛选器 +3. 验证"全部"按钮显示总素材数量 +4. 点击"商业"分类按钮 +5. 验证只显示商业类素材 + +**预期结果**: +- ✅ 分类按钮正确显示 +- ✅ 数量统计准确 +- ✅ 筛选结果正确 +- ✅ 按钮状态正确切换 + +### 测试用例2: 分类数量统计 + +**步骤**: +1. 查看每个分类按钮上的数量显示 +2. 手动计算符合条件的素材数量 +3. 对比系统显示的数量 + +**预期结果**: +- ✅ 数量统计准确 +- ✅ 总数等于各分类数量之和(考虑重复分类) + +### 测试用例3: 组合筛选 + +**步骤**: +1. 选择"商业"分类 +2. 再选择"已使用"状态 +3. 验证结果同时满足两个条件 + +**预期结果**: +- ✅ 多重筛选正确工作 +- ✅ 筛选条件叠加生效 + +### 测试用例4: 分类颜色显示 + +**步骤**: +1. 查看各分类按钮的颜色 +2. 对比分类管理中设置的颜色 +3. 验证选中状态的颜色变化 + +**预期结果**: +- ✅ 分类颜色正确显示 +- ✅ 选中状态高亮显示 + +### 测试用例5: 空状态处理 + +**步骤**: +1. 选择一个没有匹配素材的分类 +2. 验证空状态显示 + +**预期结果**: +- ✅ 显示"没有找到匹配的素材" +- ✅ 提示调整筛选条件 + +### 测试用例6: 分类匹配规则 + +**步骤**: +1. 创建包含特定标签的素材 +2. 验证标签匹配是否正确 +3. 测试文件名匹配 +4. 测试AI提示关键词匹配 + +**预期结果**: +- ✅ 标签匹配正确 +- ✅ 文件名匹配正确 +- ✅ AI提示匹配正确 + +## 📊 性能测试 + +### 大量分类测试 +1. 创建20+个分类 +2. 验证界面显示是否正常 +3. 测试筛选性能 + +### 大量素材测试 +1. 上传100+个素材 +2. 验证分类统计性能 +3. 测试筛选响应速度 + +## 🐛 边界情况测试 + +### 特殊字符处理 +- 测试包含特殊字符的分类名称 +- 测试包含特殊字符的文件名和标签 + +### 空数据处理 +- 测试没有分类数据的情况 +- 测试没有素材的情况 +- 测试分类加载失败的情况 + +### 网络异常处理 +- 测试网络断开时的行为 +- 测试分类服务异常时的处理 + +## 📝 测试记录模板 + +``` +测试日期: ____ +测试人员: ____ +浏览器: ____ +版本: ____ + +测试用例1: 基础分类筛选 +- 分类按钮显示: ✅/❌ +- 数量统计: ✅/❌ +- 筛选结果: ✅/❌ +- 状态切换: ✅/❌ +- 备注: ____ + +测试用例2: 分类数量统计 +- 数量准确性: ✅/❌ +- 总数计算: ✅/❌ +- 备注: ____ + +测试用例3: 组合筛选 +- 多重筛选: ✅/❌ +- 条件叠加: ✅/❌ +- 备注: ____ + +测试用例4: 分类颜色显示 +- 颜色正确: ✅/❌ +- 选中高亮: ✅/❌ +- 备注: ____ + +测试用例5: 空状态处理 +- 空状态显示: ✅/❌ +- 提示信息: ✅/❌ +- 备注: ____ + +测试用例6: 分类匹配规则 +- 标签匹配: ✅/❌ +- 文件名匹配: ✅/❌ +- AI提示匹配: ✅/❌ +- 备注: ____ + +总体评价: ____ +发现问题: ____ +改进建议: ____ +``` + +## 🔧 常见问题排查 + +### 问题1: 分类不显示 +**排查步骤**: +1. 检查浏览器控制台错误 +2. 验证分类服务API是否正常 +3. 检查分类数据是否存在且is_active=true + +### 问题2: 数量统计错误 +**排查步骤**: +1. 检查匹配规则逻辑 +2. 验证素材标签和文件名数据 +3. 测试AI提示关键词设置 + +### 问题3: 筛选不生效 +**排查步骤**: +1. 检查筛选逻辑实现 +2. 验证状态管理是否正确 +3. 测试组合筛选的条件叠加 + +### 问题4: 界面显示异常 +**排查步骤**: +1. 检查CSS样式是否正确 +2. 验证响应式布局 +3. 测试不同浏览器兼容性 + +--- + +通过系统的测试,确保素材分类筛选功能稳定可靠,为用户提供良好的使用体验! diff --git a/src/services/materialImportService.ts b/src/services/materialImportService.ts new file mode 100644 index 0000000..48d37bd --- /dev/null +++ b/src/services/materialImportService.ts @@ -0,0 +1,294 @@ +/** + * 素材导入服务 + * 处理素材导入、模特选择和自动分镜功能 + */ + +import { invoke } from '@tauri-apps/api/core' +import { VideoSegment } from './mediaService' +import { Model } from './modelService' + +export interface ImportMaterialConfig { + // 基础配置 + projectId: string + sourceDirectory: string + selectedModelIds: string[] + + // 导入选项 + includeSubdirectories: boolean + fileTypes: string[] // 支持的文件类型 + + // 自动分镜配置 + enableAutoSegmentation: boolean + segmentationConfig: { + method: 'scene' | 'time' | 'smart' // 分镜方法 + sceneThreshold: number // 场景变化阈值 (0-1) + timeInterval: number // 时间间隔分镜(秒) + minSegmentDuration: number // 最小片段时长(秒) + maxSegmentDuration: number // 最大片段时长(秒) + } + + // 处理选项 + generateThumbnails: boolean + extractMetadata: boolean + autoTagging: boolean +} + +export interface ImportProgress { + stage: 'scanning' | 'importing' | 'segmenting' | 'processing' | 'completed' | 'error' + progress: number // 0-100 + message: string + currentFile?: string + processedFiles: number + totalFiles: number + segmentedVideos: number + generatedSegments: number +} + +export interface ImportResult { + success: boolean + message: string + importedMaterials: VideoSegment[] + generatedSegments: VideoSegment[] + skippedFiles: string[] + errors: string[] + statistics: { + totalFiles: number + importedFiles: number + segmentedVideos: number + totalSegments: number + processingTime: number + } +} + +export interface FileInfo { + path: string + name: string + size: number + type: string + duration?: number + resolution?: string + isSupported: boolean +} + +export class MaterialImportService { + /** + * 获取默认导入配置 + */ + static getDefaultConfig(projectId: string): ImportMaterialConfig { + return { + projectId, + sourceDirectory: '', + selectedModelIds: [], + includeSubdirectories: true, + fileTypes: ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'm4v'], + enableAutoSegmentation: true, + segmentationConfig: { + method: 'smart', + sceneThreshold: 0.3, + timeInterval: 30, + minSegmentDuration: 3, + maxSegmentDuration: 60 + }, + generateThumbnails: true, + extractMetadata: true, + autoTagging: true + } + } + + /** + * 扫描目录获取可导入的文件 + */ + static async scanDirectory(directoryPath: string, config: ImportMaterialConfig): Promise { + try { + const result = await invoke('scan_import_directory', { + directoryPath, + includeSubdirectories: config.includeSubdirectories, + fileTypes: config.fileTypes + }) + + return result as FileInfo[] + } catch (error) { + console.error('Failed to scan directory:', error) + throw new Error(`扫描目录失败: ${error}`) + } + } + + /** + * 开始导入素材 + */ + static async startImport(config: ImportMaterialConfig): Promise<{ taskId: string }> { + try { + const result = await invoke('start_material_import', { config }) + return result as { taskId: string } + } catch (error) { + console.error('Failed to start import:', error) + throw new Error(`开始导入失败: ${error}`) + } + } + + /** + * 获取导入进度 + */ + static async getImportProgress(taskId: string): Promise { + try { + const result = await invoke('get_import_progress', { taskId }) + return result as ImportProgress + } catch (error) { + console.error('Failed to get import progress:', error) + throw new Error(`获取导入进度失败: ${error}`) + } + } + + /** + * 取消导入任务 + */ + static async cancelImport(taskId: string): Promise { + try { + await invoke('cancel_import', { taskId }) + return true + } catch (error) { + console.error('Failed to cancel import:', error) + return false + } + } + + /** + * 获取导入结果 + */ + static async getImportResult(taskId: string): Promise { + try { + const result = await invoke('get_import_result', { taskId }) + return result as ImportResult + } catch (error) { + console.error('Failed to get import result:', error) + throw new Error(`获取导入结果失败: ${error}`) + } + } + + /** + * 验证导入配置 + */ + static validateConfig(config: ImportMaterialConfig): string[] { + const errors: string[] = [] + + if (!config.projectId) { + errors.push('项目ID不能为空') + } + + if (!config.sourceDirectory) { + errors.push('请选择素材目录') + } + + if (config.fileTypes.length === 0) { + errors.push('请至少选择一种文件类型') + } + + if (config.enableAutoSegmentation) { + const segConfig = config.segmentationConfig + + if (segConfig.sceneThreshold < 0 || segConfig.sceneThreshold > 1) { + errors.push('场景变化阈值必须在0-1之间') + } + + if (segConfig.timeInterval <= 0) { + errors.push('时间间隔必须大于0') + } + + if (segConfig.minSegmentDuration <= 0) { + errors.push('最小片段时长必须大于0') + } + + if (segConfig.maxSegmentDuration <= segConfig.minSegmentDuration) { + errors.push('最大片段时长必须大于最小片段时长') + } + } + + return errors + } + + /** + * 估算导入时间 + */ + static estimateImportTime(fileCount: number, config: ImportMaterialConfig): number { + // 基础导入时间(每个文件) + let timePerFile = 5 // 秒 + + // 自动分镜增加时间 + if (config.enableAutoSegmentation) { + timePerFile += 15 + } + + // 生成缩略图增加时间 + if (config.generateThumbnails) { + timePerFile += 3 + } + + // 元数据提取增加时间 + if (config.extractMetadata) { + timePerFile += 2 + } + + // 自动标签增加时间 + if (config.autoTagging) { + timePerFile += 5 + } + + return Math.round(fileCount * timePerFile) + } + + /** + * 获取支持的文件类型 + */ + static getSupportedFileTypes(): Array<{value: string, label: string, description: string}> { + return [ + { value: 'mp4', label: 'MP4', description: '最常用的视频格式' }, + { value: 'avi', label: 'AVI', description: '经典视频格式' }, + { value: 'mov', label: 'MOV', description: 'QuickTime视频格式' }, + { value: 'mkv', label: 'MKV', description: '高质量视频格式' }, + { value: 'wmv', label: 'WMV', description: 'Windows媒体格式' }, + { value: 'flv', label: 'FLV', description: 'Flash视频格式' }, + { value: 'm4v', label: 'M4V', description: 'iTunes视频格式' }, + { value: 'webm', label: 'WebM', description: 'Web视频格式' }, + { value: '3gp', label: '3GP', description: '移动设备格式' } + ] + } + + /** + * 获取分镜方法选项 + */ + static getSegmentationMethods(): Array<{value: string, label: string, description: string}> { + return [ + { + value: 'smart', + label: '智能分镜', + description: '结合场景变化和时间间隔的智能分镜' + }, + { + value: 'scene', + label: '场景分镜', + description: '根据场景变化自动分镜' + }, + { + value: 'time', + label: '时间分镜', + description: '按固定时间间隔分镜' + } + ] + } + + /** + * 预览分镜结果 + */ + static async previewSegmentation( + filePath: string, + config: ImportMaterialConfig['segmentationConfig'] + ): Promise> { + try { + const result = await invoke('preview_segmentation', { filePath, config }) + return result as Array<{start: number, end: number, thumbnail?: string}> + } catch (error) { + console.error('Failed to preview segmentation:', error) + throw new Error(`预览分镜失败: ${error}`) + } + } +}