feat: 实现项目详情页面和素材导入功能
- 添加项目详情页面路由和组件 - 实现素材数据模型和数据库表结构 - 集成FFmpeg进行视频元数据提取和场景检测 - 实现视频自动切分功能(基于场景检测和时长限制) - 开发素材导入UI界面和进度显示 - 添加素材管理相关的Tauri命令 - 完善错误处理和性能优化 - 添加单元测试覆盖核心功能 主要功能: - 项目详情页面展示项目信息和素材统计 - 素材导入支持多种格式(视频、音频、图片、文档) - MD5重复检测避免重复导入 - FFmpeg集成提取视频/音频元数据 - 智能场景检测和视频切分 - 二次切分处理超长视频片段 - 响应式UI设计和用户友好的导入流程
This commit is contained in:
294
apps/desktop/src-tauri/src/data/models/material.rs
Normal file
294
apps/desktop/src-tauri/src/data/models/material.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// 素材类型枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum MaterialType {
|
||||
Video,
|
||||
Audio,
|
||||
Image,
|
||||
Document,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl MaterialType {
|
||||
pub fn from_extension(ext: &str) -> Self {
|
||||
match ext.to_lowercase().as_str() {
|
||||
"mp4" | "avi" | "mov" | "mkv" | "wmv" | "flv" | "webm" | "m4v" => MaterialType::Video,
|
||||
"mp3" | "wav" | "flac" | "aac" | "ogg" | "wma" | "m4a" => MaterialType::Audio,
|
||||
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "webp" | "svg" => MaterialType::Image,
|
||||
"pdf" | "doc" | "docx" | "txt" | "md" | "rtf" => MaterialType::Document,
|
||||
_ => MaterialType::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材处理状态枚举
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ProcessingStatus {
|
||||
Pending, // 待处理
|
||||
Processing, // 处理中
|
||||
Completed, // 处理完成
|
||||
Failed, // 处理失败
|
||||
Skipped, // 跳过(重复文件)
|
||||
}
|
||||
|
||||
/// 视频元数据结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoMetadata {
|
||||
pub duration: f64, // 时长(秒)
|
||||
pub width: u32, // 宽度
|
||||
pub height: u32, // 高度
|
||||
pub fps: f64, // 帧率
|
||||
pub bitrate: u64, // 比特率
|
||||
pub codec: String, // 编码格式
|
||||
pub format: String, // 容器格式
|
||||
pub has_audio: bool, // 是否包含音频
|
||||
pub audio_codec: Option<String>, // 音频编码格式
|
||||
pub audio_bitrate: Option<u64>, // 音频比特率
|
||||
pub audio_sample_rate: Option<u32>, // 音频采样率
|
||||
}
|
||||
|
||||
/// 音频元数据结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AudioMetadata {
|
||||
pub duration: f64, // 时长(秒)
|
||||
pub bitrate: u64, // 比特率
|
||||
pub codec: String, // 编码格式
|
||||
pub sample_rate: u32, // 采样率
|
||||
pub channels: u32, // 声道数
|
||||
pub format: String, // 音频格式
|
||||
}
|
||||
|
||||
/// 图片元数据结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImageMetadata {
|
||||
pub width: u32, // 宽度
|
||||
pub height: u32, // 高度
|
||||
pub format: String, // 图片格式
|
||||
pub color_space: Option<String>, // 色彩空间
|
||||
pub has_alpha: bool, // 是否有透明通道
|
||||
pub dpi: Option<u32>, // DPI
|
||||
}
|
||||
|
||||
/// 素材元数据联合体
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MaterialMetadata {
|
||||
Video(VideoMetadata),
|
||||
Audio(AudioMetadata),
|
||||
Image(ImageMetadata),
|
||||
None,
|
||||
}
|
||||
|
||||
/// 场景检测结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SceneDetection {
|
||||
pub scenes: Vec<SceneSegment>,
|
||||
pub total_scenes: u32,
|
||||
pub detection_method: String,
|
||||
pub threshold: f64,
|
||||
}
|
||||
|
||||
/// 场景片段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SceneSegment {
|
||||
pub start_time: f64, // 开始时间(秒)
|
||||
pub end_time: f64, // 结束时间(秒)
|
||||
pub duration: f64, // 片段时长(秒)
|
||||
pub scene_id: u32, // 场景ID
|
||||
pub confidence: f64, // 检测置信度
|
||||
}
|
||||
|
||||
/// 素材实体模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Material {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub original_path: String,
|
||||
pub file_size: u64,
|
||||
pub md5_hash: String,
|
||||
pub material_type: MaterialType,
|
||||
pub processing_status: ProcessingStatus,
|
||||
pub metadata: MaterialMetadata,
|
||||
pub scene_detection: Option<SceneDetection>,
|
||||
pub segments: Vec<MaterialSegment>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub processed_at: Option<DateTime<Utc>>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// 素材片段(切分后的片段)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MaterialSegment {
|
||||
pub id: String,
|
||||
pub material_id: String,
|
||||
pub segment_index: u32,
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
pub duration: f64,
|
||||
pub file_path: String,
|
||||
pub file_size: u64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 创建素材请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateMaterialRequest {
|
||||
pub project_id: String,
|
||||
pub file_paths: Vec<String>,
|
||||
pub auto_process: bool,
|
||||
pub max_segment_duration: Option<f64>, // 最大片段时长(秒)
|
||||
}
|
||||
|
||||
/// 素材处理配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MaterialProcessingConfig {
|
||||
pub max_segment_duration: f64, // 最大片段时长(秒)
|
||||
pub scene_detection_threshold: f64, // 场景检测阈值
|
||||
pub enable_scene_detection: bool, // 是否启用场景检测
|
||||
pub video_quality: String, // 视频质量设置
|
||||
pub audio_quality: String, // 音频质量设置
|
||||
pub output_format: String, // 输出格式
|
||||
}
|
||||
|
||||
impl Default for MaterialProcessingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_segment_duration: 300.0, // 5分钟
|
||||
scene_detection_threshold: 0.3,
|
||||
enable_scene_detection: true,
|
||||
video_quality: "medium".to_string(),
|
||||
audio_quality: "medium".to_string(),
|
||||
output_format: "mp4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材导入结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MaterialImportResult {
|
||||
pub total_files: u32,
|
||||
pub processed_files: u32,
|
||||
pub skipped_files: u32,
|
||||
pub failed_files: u32,
|
||||
pub created_materials: Vec<Material>,
|
||||
pub errors: Vec<String>,
|
||||
pub processing_time: f64, // 处理时间(秒)
|
||||
}
|
||||
|
||||
/// 素材统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MaterialStats {
|
||||
pub total_materials: u32,
|
||||
pub video_count: u32,
|
||||
pub audio_count: u32,
|
||||
pub image_count: u32,
|
||||
pub other_count: u32,
|
||||
pub total_size: u64,
|
||||
pub total_duration: f64, // 总时长(秒)
|
||||
pub processing_status_counts: std::collections::HashMap<String, u32>,
|
||||
}
|
||||
|
||||
impl Material {
|
||||
/// 创建新的素材实例
|
||||
pub fn new(
|
||||
project_id: String,
|
||||
name: String,
|
||||
original_path: String,
|
||||
file_size: u64,
|
||||
md5_hash: String,
|
||||
material_type: MaterialType,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
project_id,
|
||||
name,
|
||||
original_path,
|
||||
file_size,
|
||||
md5_hash,
|
||||
material_type,
|
||||
processing_status: ProcessingStatus::Pending,
|
||||
metadata: MaterialMetadata::None,
|
||||
scene_detection: None,
|
||||
segments: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
processed_at: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新处理状态
|
||||
pub fn update_status(&mut self, status: ProcessingStatus, error_message: Option<String>) {
|
||||
self.processing_status = status;
|
||||
self.error_message = error_message;
|
||||
self.updated_at = Utc::now();
|
||||
|
||||
if matches!(status, ProcessingStatus::Completed | ProcessingStatus::Failed) {
|
||||
self.processed_at = Some(Utc::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置元数据
|
||||
pub fn set_metadata(&mut self, metadata: MaterialMetadata) {
|
||||
self.metadata = metadata;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 添加场景检测结果
|
||||
pub fn set_scene_detection(&mut self, scene_detection: SceneDetection) {
|
||||
self.scene_detection = Some(scene_detection);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 添加片段
|
||||
pub fn add_segment(&mut self, segment: MaterialSegment) {
|
||||
self.segments.push(segment);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 获取总时长
|
||||
pub fn get_duration(&self) -> Option<f64> {
|
||||
match &self.metadata {
|
||||
MaterialMetadata::Video(video) => Some(video.duration),
|
||||
MaterialMetadata::Audio(audio) => Some(audio.duration),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否需要切分
|
||||
pub fn needs_segmentation(&self, max_duration: f64) -> bool {
|
||||
if let Some(duration) = self.get_duration() {
|
||||
duration > max_duration
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaterialSegment {
|
||||
/// 创建新的素材片段
|
||||
pub fn new(
|
||||
material_id: String,
|
||||
segment_index: u32,
|
||||
start_time: f64,
|
||||
end_time: f64,
|
||||
file_path: String,
|
||||
file_size: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
material_id,
|
||||
segment_index,
|
||||
start_time,
|
||||
end_time,
|
||||
duration: end_time - start_time,
|
||||
file_path,
|
||||
file_size,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod project;
|
||||
pub mod material;
|
||||
|
||||
Reference in New Issue
Block a user