feat: 实现AI视频分类功能
新功能: - 集成Google Gemini API进行视频智能分类 - 实现任务队列系统支持批量处理 - 添加实时进度显示和状态管理 - 自动文件整理到分类文件夹 架构改进: - 遵循Tauri开发规范的分层架构设计 - 完整的数据模型和仓库层实现 - 异步任务处理和错误处理机制 - 类型安全的前后端通信接口 用户界面: - MaterialCard组件添加AI分类按钮 - VideoClassificationProgress进度显示组件 - 优美的动画效果和响应式设计 - 符合前端开发规范的UI/UX优化 数据库扩展: - 新增video_classification_records表 - 新增video_classification_tasks表 - 完整的索引优化和外键约束 技术实现: - Rust后端服务层完整实现 - React/TypeScript前端状态管理 - Zustand状态存储和API封装 - 完善的错误处理和用户提示 文档: - 完整的功能文档和API说明 - 架构设计和使用流程说明 - 开发规范遵循情况说明 Closes #AI视频分类功能开发
This commit is contained in:
307
apps/desktop/src-tauri/src/data/models/video_classification.rs
Normal file
307
apps/desktop/src-tauri/src/data/models/video_classification.rs
Normal file
@@ -0,0 +1,307 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// AI视频分类记录数据模型
|
||||
/// 遵循 Tauri 开发规范的数据模型设计原则
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoClassificationRecord {
|
||||
/// 分类记录唯一标识符
|
||||
pub id: String,
|
||||
/// 素材片段ID
|
||||
pub segment_id: String,
|
||||
/// 素材ID
|
||||
pub material_id: String,
|
||||
/// 项目ID
|
||||
pub project_id: String,
|
||||
/// 分类结果
|
||||
pub category: String,
|
||||
/// 置信度 (0.0 - 1.0)
|
||||
pub confidence: f64,
|
||||
/// 分类理由
|
||||
pub reasoning: String,
|
||||
/// 关键特征
|
||||
pub features: Vec<String>,
|
||||
/// 商品匹配度
|
||||
pub product_match: bool,
|
||||
/// 质量评分 (0.0 - 1.0)
|
||||
pub quality_score: f64,
|
||||
/// Gemini文件URI
|
||||
pub gemini_file_uri: Option<String>,
|
||||
/// 原始AI响应
|
||||
pub raw_response: Option<String>,
|
||||
/// 分类状态
|
||||
pub status: ClassificationStatus,
|
||||
/// 错误信息
|
||||
pub error_message: Option<String>,
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 更新时间
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// AI视频分类任务数据模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoClassificationTask {
|
||||
/// 任务唯一标识符
|
||||
pub id: String,
|
||||
/// 素材片段ID
|
||||
pub segment_id: String,
|
||||
/// 素材ID
|
||||
pub material_id: String,
|
||||
/// 项目ID
|
||||
pub project_id: String,
|
||||
/// 视频文件路径
|
||||
pub video_file_path: String,
|
||||
/// 任务状态
|
||||
pub status: TaskStatus,
|
||||
/// 任务优先级
|
||||
pub priority: i32,
|
||||
/// 重试次数
|
||||
pub retry_count: i32,
|
||||
/// 最大重试次数
|
||||
pub max_retries: i32,
|
||||
/// Gemini文件URI(上传后获得)
|
||||
pub gemini_file_uri: Option<String>,
|
||||
/// 使用的提示词
|
||||
pub prompt_text: Option<String>,
|
||||
/// 错误信息
|
||||
pub error_message: Option<String>,
|
||||
/// 开始处理时间
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
/// 完成时间
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 更新时间
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 分类状态枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ClassificationStatus {
|
||||
/// 已分类
|
||||
Classified,
|
||||
/// 分类失败
|
||||
Failed,
|
||||
/// 需要人工审核
|
||||
NeedsReview,
|
||||
}
|
||||
|
||||
/// 任务状态枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TaskStatus {
|
||||
/// 等待处理
|
||||
Pending,
|
||||
/// 上传中
|
||||
Uploading,
|
||||
/// 分析中
|
||||
Analyzing,
|
||||
/// 已完成
|
||||
Completed,
|
||||
/// 失败
|
||||
Failed,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// 批量分类请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BatchClassificationRequest {
|
||||
/// 素材ID
|
||||
pub material_id: String,
|
||||
/// 项目ID
|
||||
pub project_id: String,
|
||||
/// 是否覆盖已有分类
|
||||
pub overwrite_existing: bool,
|
||||
/// 任务优先级
|
||||
pub priority: Option<i32>,
|
||||
}
|
||||
|
||||
/// 分类任务查询参数
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ClassificationTaskQuery {
|
||||
/// 项目ID过滤
|
||||
pub project_id: Option<String>,
|
||||
/// 素材ID过滤
|
||||
pub material_id: Option<String>,
|
||||
/// 状态过滤
|
||||
pub status: Option<TaskStatus>,
|
||||
/// 分页大小
|
||||
pub limit: Option<i32>,
|
||||
/// 分页偏移
|
||||
pub offset: Option<i32>,
|
||||
}
|
||||
|
||||
/// 分类统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClassificationStats {
|
||||
/// 总任务数
|
||||
pub total_tasks: i32,
|
||||
/// 等待中任务数
|
||||
pub pending_tasks: i32,
|
||||
/// 处理中任务数
|
||||
pub processing_tasks: i32,
|
||||
/// 已完成任务数
|
||||
pub completed_tasks: i32,
|
||||
/// 失败任务数
|
||||
pub failed_tasks: i32,
|
||||
/// 总分类记录数
|
||||
pub total_classifications: i32,
|
||||
/// 平均置信度
|
||||
pub average_confidence: f64,
|
||||
/// 平均质量评分
|
||||
pub average_quality_score: f64,
|
||||
}
|
||||
|
||||
/// Gemini API响应结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeminiClassificationResponse {
|
||||
/// 分类结果
|
||||
pub category: String,
|
||||
/// 置信度
|
||||
pub confidence: f64,
|
||||
/// 分类理由
|
||||
pub reasoning: String,
|
||||
/// 关键特征
|
||||
pub features: Vec<String>,
|
||||
/// 商品匹配度
|
||||
pub product_match: bool,
|
||||
/// 质量评分
|
||||
pub quality_score: f64,
|
||||
}
|
||||
|
||||
impl VideoClassificationRecord {
|
||||
/// 创建新的分类记录
|
||||
pub fn new(
|
||||
segment_id: String,
|
||||
material_id: String,
|
||||
project_id: String,
|
||||
gemini_response: GeminiClassificationResponse,
|
||||
gemini_file_uri: Option<String>,
|
||||
raw_response: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
segment_id,
|
||||
material_id,
|
||||
project_id,
|
||||
category: gemini_response.category,
|
||||
confidence: gemini_response.confidence,
|
||||
reasoning: gemini_response.reasoning,
|
||||
features: gemini_response.features,
|
||||
product_match: gemini_response.product_match,
|
||||
quality_score: gemini_response.quality_score,
|
||||
gemini_file_uri,
|
||||
raw_response,
|
||||
status: ClassificationStatus::Classified,
|
||||
error_message: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记为失败
|
||||
pub fn mark_as_failed(&mut self, error_message: String) {
|
||||
self.status = ClassificationStatus::Failed;
|
||||
self.error_message = Some(error_message);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 标记为需要审核
|
||||
pub fn mark_as_needs_review(&mut self, reason: String) {
|
||||
self.status = ClassificationStatus::NeedsReview;
|
||||
self.error_message = Some(reason);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 是否需要人工审核(低置信度或低质量)
|
||||
pub fn needs_review(&self) -> bool {
|
||||
self.confidence < 0.7 || self.quality_score < 0.6
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoClassificationTask {
|
||||
/// 创建新的分类任务
|
||||
pub fn new(
|
||||
segment_id: String,
|
||||
material_id: String,
|
||||
project_id: String,
|
||||
video_file_path: String,
|
||||
priority: Option<i32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
segment_id,
|
||||
material_id,
|
||||
project_id,
|
||||
video_file_path,
|
||||
status: TaskStatus::Pending,
|
||||
priority: priority.unwrap_or(0),
|
||||
retry_count: 0,
|
||||
max_retries: 3,
|
||||
gemini_file_uri: None,
|
||||
prompt_text: None,
|
||||
error_message: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始处理任务
|
||||
pub fn start_processing(&mut self) {
|
||||
self.status = TaskStatus::Uploading;
|
||||
self.started_at = Some(Utc::now());
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 设置为分析状态
|
||||
pub fn set_analyzing(&mut self, gemini_file_uri: String, prompt_text: String) {
|
||||
self.status = TaskStatus::Analyzing;
|
||||
self.gemini_file_uri = Some(gemini_file_uri);
|
||||
self.prompt_text = Some(prompt_text);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 完成任务
|
||||
pub fn complete(&mut self) {
|
||||
self.status = TaskStatus::Completed;
|
||||
self.completed_at = Some(Utc::now());
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 任务失败
|
||||
pub fn fail(&mut self, error_message: String) {
|
||||
self.status = TaskStatus::Failed;
|
||||
self.error_message = Some(error_message);
|
||||
self.retry_count += 1;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 是否可以重试
|
||||
pub fn can_retry(&self) -> bool {
|
||||
self.retry_count < self.max_retries && self.status == TaskStatus::Failed
|
||||
}
|
||||
|
||||
/// 重置为等待状态(用于重试)
|
||||
pub fn reset_for_retry(&mut self) {
|
||||
self.status = TaskStatus::Pending;
|
||||
self.error_message = None;
|
||||
self.started_at = None;
|
||||
self.completed_at = None;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClassificationStatus {
|
||||
fn default() -> Self {
|
||||
ClassificationStatus::Classified
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TaskStatus {
|
||||
fn default() -> Self {
|
||||
TaskStatus::Pending
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user