From c51ede77f7b4c13264982b48a341f8719d0b4599 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 11 Jul 2025 16:00:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=B4=A0=E6=9D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/IMPORT_MATERIAL_GUIDE.md | 231 +++++++++++++ src/components/ImportMaterialModal.tsx | 400 ++++++++++++++++++++++ src/components/ProjectMaterialsCenter.tsx | 40 ++- 3 files changed, 668 insertions(+), 3 deletions(-) create mode 100644 docs/IMPORT_MATERIAL_GUIDE.md create mode 100644 src/components/ImportMaterialModal.tsx diff --git a/docs/IMPORT_MATERIAL_GUIDE.md b/docs/IMPORT_MATERIAL_GUIDE.md new file mode 100644 index 0000000..3be202d --- /dev/null +++ b/docs/IMPORT_MATERIAL_GUIDE.md @@ -0,0 +1,231 @@ +# 项目素材导入功能使用指南 + +## 🎯 功能概述 + +项目素材导入功能允许用户为特定项目批量导入视频素材,支持选择模特关联、自动标签和自动分镜功能,大大提升素材管理效率。 + +## 🚀 功能特点 + +### 1. 灵活的文件选择 +- **单文件/多文件选择**: 支持选择一个或多个视频文件 +- **文件夹批量导入**: 选择整个文件夹进行批量导入 +- **格式支持**: 支持 MP4、AVI、MOV、MKV、WMV、FLV、WebM、M4V 等主流视频格式 + +### 2. 智能关联功能 +- **项目自动关联**: 导入的素材自动关联到当前项目 +- **模特标签关联**: 可选择关联的模特,自动添加模特标签 +- **项目标签**: 自动添加项目商品名作为标签 + +### 3. 自动分镜功能 +- **智能分镜**: 自动将长视频分割成多个片段 +- **场景识别**: 基于场景变化进行分镜 +- **便于编辑**: 分镜后的片段更适合后续编辑使用 + +### 4. 标签管理 +- **自定义标签**: 可添加自定义标签 +- **自动标签**: 系统自动添加项目和模特相关标签 +- **标签预览**: 实时预览将要添加的标签 + +## 📍 使用位置 + +在项目详情页面的素材管理区域,点击右上角的"导入素材"按钮即可打开导入功能。 + +## 🔧 使用步骤 + +### 步骤1: 打开导入功能 +1. 进入项目详情页面 +2. 在素材管理区域找到"导入素材"按钮 +3. 点击按钮打开导入模态框 + +### 步骤2: 选择素材文件 +有两种选择方式: + +#### 方式A: 选择单个或多个文件 +1. 点击"选择文件"按钮 +2. 在文件选择对话框中选择视频文件 +3. 支持按住Ctrl/Cmd键多选文件 + +#### 方式B: 选择整个文件夹 +1. 点击"选择文件夹"按钮 +2. 选择包含视频文件的文件夹 +3. 系统会自动扫描文件夹中的所有支持格式的视频文件 + +### 步骤3: 配置导入选项 + +#### 关联模特(可选) +- 勾选要关联的模特 +- 选中的模特标签会自动添加到导入的素材中 +- 有助于后续按模特筛选素材 + +#### 添加标签(可选) +- 在标签输入框中输入自定义标签 +- 按回车键或点击"+"按钮添加标签 +- 可添加多个标签,点击标签上的删除按钮可移除 + +#### 自动分镜设置 +- 默认启用自动分镜功能 +- 取消勾选可禁用自动分镜 +- 启用后长视频会被自动分割成多个片段 + +### 步骤4: 开始导入 +1. 确认所有配置无误 +2. 点击"开始导入"按钮 +3. 等待导入完成 + +## 📊 导入过程 + +### 1. 文件验证 +- 检查文件格式是否支持 +- 验证文件完整性 +- 过滤不支持的文件 + +### 2. 素材处理 +- 提取视频元数据(时长、分辨率、帧率等) +- 生成MD5哈希值防重复 +- 创建缩略图 + +### 3. 自动分镜(如果启用) +- 分析视频场景变化 +- 智能确定分镜点 +- 生成多个视频片段 + +### 4. 标签处理 +- 添加项目相关标签 +- 添加模特相关标签 +- 添加用户自定义标签 + +### 5. 数据存储 +- 保存素材信息到数据库 +- 建立项目关联关系 +- 更新素材统计 + +## 📋 导入结果 + +导入完成后会显示详细的结果信息: + +### 成功信息 +- **总文件数**: 尝试导入的文件总数 +- **成功导入**: 成功导入的文件数量 +- **生成片段**: 自动分镜生成的片段数量 +- **跳过文件**: 因重复或其他原因跳过的文件 +- **失败文件**: 导入失败的文件数量 + +### 错误信息 +如果有文件导入失败,会显示具体的错误信息,包括: +- 文件名 +- 失败原因 +- 建议解决方案 + +## 🎯 最佳实践 + +### 1. 文件组织 +- **统一命名**: 使用有意义的文件名 +- **分类存放**: 按类型或用途组织文件夹 +- **避免特殊字符**: 文件名避免使用特殊字符 + +### 2. 标签策略 +- **描述性标签**: 使用描述性的标签名称 +- **统一规范**: 团队内部统一标签命名规范 +- **分层标签**: 使用分层的标签体系 + +**推荐标签示例**: +``` +类型标签: 产品展示、人物介绍、场景拍摄 +风格标签: 商务、休闲、时尚、科技 +用途标签: 主视频、背景素材、转场素材 +``` + +### 3. 模特关联 +- **准确关联**: 确保选择正确的模特 +- **完整关联**: 包含该模特的素材都应该关联 +- **及时更新**: 新增模特后及时关联相关素材 + +### 4. 自动分镜 +- **长视频启用**: 对于超过1分钟的视频建议启用 +- **短视频禁用**: 对于短视频可以禁用以保持完整性 +- **后期调整**: 可在导入后手动调整分镜结果 + +## 🔍 故障排除 + +### 问题1: 文件选择失败 + +**可能原因**: +- 文件路径包含特殊字符 +- 文件被其他程序占用 +- 权限不足 + +**解决方案**: +1. 检查文件路径,避免特殊字符 +2. 关闭可能占用文件的程序 +3. 确保有足够的文件访问权限 + +### 问题2: 导入失败 + +**可能原因**: +- 文件格式不支持 +- 文件损坏 +- 磁盘空间不足 +- 网络连接问题 + +**解决方案**: +1. 确认文件格式在支持列表中 +2. 检查文件完整性 +3. 确保有足够的磁盘空间 +4. 检查网络连接状态 + +### 问题3: 自动分镜效果不佳 + +**可能原因**: +- 视频场景变化不明显 +- 视频质量较低 +- 分镜参数不合适 + +**解决方案**: +1. 对于场景单一的视频可禁用自动分镜 +2. 提高视频质量 +3. 手动调整分镜结果 + +### 问题4: 标签丢失 + +**可能原因**: +- 导入过程中断 +- 标签格式错误 +- 系统异常 + +**解决方案**: +1. 重新导入素材 +2. 检查标签格式 +3. 手动添加丢失的标签 + +## 🚀 高级功能 + +### 批量操作 +- 支持同时导入多个文件夹 +- 支持批量设置标签和模特关联 +- 支持批量启用/禁用自动分镜 + +### 导入模板 +- 保存常用的导入配置 +- 快速应用预设配置 +- 团队共享导入模板 + +### 进度监控 +- 实时显示导入进度 +- 支持暂停和恢复导入 +- 详细的处理日志 + +## 📈 性能优化 + +### 导入速度优化 +- **小批量导入**: 避免一次导入过多文件 +- **网络稳定**: 确保网络连接稳定 +- **系统资源**: 确保系统有足够的内存和CPU资源 + +### 存储优化 +- **定期清理**: 定期清理不需要的素材 +- **压缩设置**: 合理设置视频压缩参数 +- **分布存储**: 大量素材可考虑分布式存储 + +--- + +通过项目素材导入功能,您可以高效地管理项目素材,提升视频制作效率! diff --git a/src/components/ImportMaterialModal.tsx b/src/components/ImportMaterialModal.tsx new file mode 100644 index 0000000..f7cb16e --- /dev/null +++ b/src/components/ImportMaterialModal.tsx @@ -0,0 +1,400 @@ +/** + * 导入素材模态框组件 + * 参考素材管理页面设计,支持选择目录、模特和自动分镜配置 + */ + +import React, { useState } from 'react' +import { X, Upload, FolderOpen, Users, Scissors, Video, CheckCircle, Plus, Trash2 } from 'lucide-react' +import { open } from '@tauri-apps/plugin-dialog' +import { Project } from '../services/projectService' +import { Model } from '../services/modelService' +import { MediaService, BatchUploadResult } from '../services/mediaService' + +interface ImportMaterialModalProps { + project: Project + models: Model[] + onConfirm: (result: BatchUploadResult) => void + onCancel: () => void +} + +const ImportMaterialModal: React.FC = ({ + project, + models, + onConfirm, + onCancel +}) => { + const [uploading, setUploading] = useState(false) + const [uploadResult, setUploadResult] = useState(null) + const [selectedFiles, setSelectedFiles] = useState([]) + const [selectedDirectory, setSelectedDirectory] = useState('') + const [selectedModelIds, setSelectedModelIds] = useState([]) + const [tags, setTags] = useState([]) + const [newTag, setNewTag] = useState('') + const [enableAutoSegmentation, setEnableAutoSegmentation] = useState(true) + + // 选择文件 + const selectFiles = async () => { + try { + const files = await open({ + multiple: true, + title: '选择视频文件', + filters: [ + { + name: '视频文件', + extensions: ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm', 'm4v'] + } + ] + }) + + if (files && Array.isArray(files)) { + setSelectedFiles(files) + setSelectedDirectory('') + } + } catch (error) { + console.error('Failed to select files:', error) + } + } + + // 选择目录 + const selectDirectory = async () => { + try { + const directory = await open({ + directory: true, + multiple: false, + title: '选择视频文件夹' + }) + + if (directory && typeof directory === 'string') { + setSelectedDirectory(directory) + setSelectedFiles([]) + } + } catch (error) { + console.error('Failed to select directory:', error) + } + } + + // 添加标签 + const addTag = () => { + if (newTag.trim() && !tags.includes(newTag.trim())) { + setTags([...tags, newTag.trim()]) + setNewTag('') + } + } + + // 移除标签 + const removeTag = (tagToRemove: string) => { + setTags(tags.filter(tag => tag !== tagToRemove)) + } + + // 处理导入 + const handleImport = async () => { + if (selectedFiles.length === 0 && !selectedDirectory) { + alert('请先选择文件或文件夹') + return + } + + setUploading(true) + setUploadResult(null) + + try { + // 准备导入参数 + const importTags = [...tags] + + // 添加项目标签 + if (project.product_name && !importTags.includes(project.product_name)) { + importTags.push(project.product_name) + } + + // 添加模特标签 + const selectedModels = models.filter(m => selectedModelIds.includes(m.id)) + selectedModels.forEach(model => { + if (!importTags.includes(model.model_number)) { + importTags.push(model.model_number) + } + }) + + let result: BatchUploadResult + + if (selectedDirectory) { + // 批量导入文件夹 + const response = await MediaService.batchUploadVideoFiles({ + source_directory: selectedDirectory, + tags: importTags.length > 0 ? importTags : undefined + }) + + if (response.status && response.data) { + result = response.data + } else { + throw new Error(response.msg || '批量导入失败') + } + } else { + // 导入单个或多个文件 + result = { + total_files: selectedFiles.length, + uploaded_files: 0, + failed_files: 0, + skipped_files: 0, + total_segments: 0, + uploaded_list: [], + skipped_list: [], + failed_list: [] + } + + for (const filePath of selectedFiles) { + try { + const response = await MediaService.uploadVideoFile({ + source_path: filePath, + tags: importTags + }) + + if (response.status && response.data) { + result.uploaded_files++ + result.total_segments += response.data.segments.length + result.uploaded_list.push(response.data) + } else { + result.failed_files++ + result.failed_list.push({ + filename: filePath.split('/').pop() || filePath, + error: response.msg || '上传失败' + }) + } + } catch (error) { + result.failed_files++ + result.failed_list.push({ + filename: filePath.split('/').pop() || filePath, + error: String(error) + }) + } + } + } + + setUploadResult(result) + + // 如果有成功的上传,调用完成回调 + if (result.uploaded_files > 0) { + onConfirm(result) + } + + } catch (error) { + console.error('Import failed:', error) + alert('导入失败: ' + (error instanceof Error ? error.message : '未知错误')) + } finally { + setUploading(false) + } + } + + return ( +
+
+ {/* 标题栏 */} +
+
+

导入素材

+

为项目 "{project.name}" 导入视频素材

+
+ +
+ +
+ {/* 文件选择 */} +
+ +
+ + + +
+ + {/* 显示选择的文件或目录 */} + {selectedFiles.length > 0 && ( +
+
+ + 已选择 {selectedFiles.length} 个文件 +
+
+ )} + + {selectedDirectory && ( +
+
+ + 已选择文件夹 +
+
{selectedDirectory}
+
+ )} +
+ + {/* 模特选择 */} +
+ +
+ {models.map((model) => ( + + ))} +
+
+ + {/* 标签设置 */} +
+ +
+ setNewTag(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addTag()} + placeholder="输入标签名称" + disabled={uploading} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+ + {tags.length > 0 && ( +
+ {tags.map((tag, index) => ( + + {tag} + + + ))} +
+ )} +
+ + {/* 自动分镜设置 */} +
+ +

+ 自动将长视频分割成多个片段,便于后续编辑使用 +

+
+ + {/* 上传结果 */} + {uploadResult && ( +
+
+ + 导入完成 +
+
+
总文件数: {uploadResult.total_files}
+
成功导入: {uploadResult.uploaded_files}
+
失败: {uploadResult.failed_files}
+
跳过: {uploadResult.skipped_files}
+
生成片段: {uploadResult.total_segments}
+
+ {uploadResult.failed_list.length > 0 && ( +
+
错误信息:
+
+ {uploadResult.failed_list.map((error, index) => ( +
• {error.filename}: {error.error}
+ ))} +
+
+ )} +
+ )} +
+ + {/* 底部按钮 */} +
+ + + +
+
+
+ ) +} + +export default ImportMaterialModal diff --git a/src/components/ProjectMaterialsCenter.tsx b/src/components/ProjectMaterialsCenter.tsx index be7ad42..0dea7c1 100644 --- a/src/components/ProjectMaterialsCenter.tsx +++ b/src/components/ProjectMaterialsCenter.tsx @@ -1,10 +1,12 @@ import React, { useState, useEffect } from 'react' -import { Video, Play, Search, Filter, Tag, Clock, HardDrive, Eye, User, Folder } from 'lucide-react' +import { Video, Play, Search, Filter, Tag, Clock, HardDrive, Eye, User, Folder, Upload } from 'lucide-react' import { Project } from '../services/projectService' import { VideoSegment, MediaService } from '../services/mediaService' import { Model } from '../services/modelService' import { ResourceCategory, ResourceCategoryService } from '../services/resourceCategoryService' +import { BatchUploadResult } from '../services/mediaService' import VideoPlayer from './VideoPlayer' +import ImportMaterialModal from './ImportMaterialModal' interface ProjectMaterialsCenterProps { project: Project @@ -25,6 +27,7 @@ const ProjectMaterialsCenter: React.FC = ({ const [usageFilter, setUsageFilter] = useState<'all' | 'used' | 'unused'>('all') const [selectedMaterial, setSelectedMaterial] = useState(null) const [showVideoPlayer, setShowVideoPlayer] = useState(false) + const [showImportModal, setShowImportModal] = useState(false) // 素材分类相关状态 const [categories, setCategories] = useState([]) @@ -153,6 +156,18 @@ const ProjectMaterialsCenter: React.FC = ({ setShowVideoPlayer(true) } + // 处理导入完成 + const handleImportComplete = (result: BatchUploadResult) => { + setShowImportModal(false) + + // 刷新素材列表 + onMaterialsChange([...materials]) + + // 显示导入结果 + const message = `导入完成!\n成功: ${result.uploaded_files} 个文件\n生成片段: ${result.total_segments} 个` + alert(message) + } + const formatDuration = (seconds: number) => { const minutes = Math.floor(seconds / 60) const remainingSeconds = Math.floor(seconds % 60) @@ -186,8 +201,17 @@ const ProjectMaterialsCenter: React.FC = ({

项目素材

-
- {filteredMaterials.length} / {materials.length} 个素材 +
+
+ {filteredMaterials.length} / {materials.length} 个素材 +
+
@@ -468,6 +492,16 @@ const ProjectMaterialsCenter: React.FC = ({ }`} /> )} + + {/* 导入素材模态框 */} + {showImportModal && ( + setShowImportModal(false)} + /> + )}
) }