feat: 完善水印处理工具功能

- 修复批量处理进度条不更新问题
  - 实现任务状态管理器(task_manager.rs)
  - 添加实时进度更新和任务状态跟踪
  - 完善前端进度轮询逻辑

- 丰富水印检测结果展示
  - 添加详细检测结果信息(位置、置信度、类型等)
  - 显示处理统计和性能信息
  - 优化结果展示UI和用户体验

- 修复水印模板上传和删除功能
  - 实现内存存储系统管理模板
  - 修复参数匹配问题
  - 添加模板缩略图展示功能

- 优化文件上传体验
  - 使用Tauri Dialog API替代HTML5文件输入
  - 实现原生文件选择器
  - 添加WatermarkTemplateThumbnail组件

- 完善水印工具集成
  - 创建WatermarkTool页面
  - 添加到便捷工具列表
  - 完善路由配置和UI展示
This commit is contained in:
imeepos
2025-07-23 12:51:49 +08:00
parent c3cad2254c
commit 29a4c32096
30 changed files with 7669 additions and 2 deletions

View File

@@ -172,6 +172,10 @@ pub enum BusinessError {
InvalidState(String),
}
/// 水印处理相关错误
pub mod watermark_errors;
pub use watermark_errors::*;
/// 错误结果类型别名
pub type AppResult<T> = Result<T, AppError>;
pub type MaterialResult<T> = Result<T, MaterialError>;

View File

@@ -0,0 +1,455 @@
use thiserror::Error;
/// 水印处理相关错误类型
/// 遵循 Tauri 开发规范的错误处理设计
#[derive(Debug, Error)]
pub enum WatermarkError {
#[error("水印检测失败: {message}")]
DetectionFailed { message: String },
#[error("水印移除失败: {message}")]
RemovalFailed { message: String },
#[error("水印添加失败: {message}")]
AdditionFailed { message: String },
#[error("不支持的文件格式: {format}, 支持的格式: {supported:?}")]
UnsupportedFormat { format: String, supported: Vec<String> },
#[error("FFmpeg执行错误: {message}")]
FFmpegError { message: String },
#[error("OpenCV处理错误: {message}")]
OpenCVError { message: String },
#[error("模板不存在: {template_id}")]
TemplateNotFound { template_id: String },
#[error("模板名称已存在: {name}")]
TemplateNameExists { name: String },
#[error("无效的水印配置: {message}")]
InvalidConfig { message: String },
#[error("文件操作失败: {operation}, 路径: {path}, 错误: {message}")]
FileOperationFailed {
operation: String,
path: String,
message: String,
},
#[error("批量任务失败: {task_id}, 错误: {message}")]
BatchTaskFailed { task_id: String, message: String },
#[error("任务已取消: {task_id}")]
TaskCancelled { task_id: String },
#[error("任务超时: {task_id}, 超时时间: {timeout_ms}ms")]
TaskTimeout { task_id: String, timeout_ms: u64 },
#[error("数据库操作失败: {operation}, 错误: {message}")]
DatabaseError { operation: String, message: String },
#[error("网络请求失败: {url}, 错误: {message}")]
NetworkError { url: String, message: String },
#[error("权限不足: {operation}")]
PermissionDenied { operation: String },
#[error("资源不足: {resource}, 需要: {required}, 可用: {available}")]
InsufficientResources {
resource: String,
required: String,
available: String,
},
#[error("配置错误: {key}, 值: {value}, 错误: {message}")]
ConfigurationError {
key: String,
value: String,
message: String,
},
#[error("验证失败: {field}, 值: {value}, 原因: {reason}")]
ValidationError {
field: String,
value: String,
reason: String,
},
#[error("内部错误: {message}")]
InternalError { message: String },
#[error("外部依赖错误: {dependency}, 错误: {message}")]
ExternalDependencyError { dependency: String, message: String },
}
impl WatermarkError {
/// 创建检测失败错误
pub fn detection_failed<S: Into<String>>(message: S) -> Self {
Self::DetectionFailed {
message: message.into(),
}
}
/// 创建移除失败错误
pub fn removal_failed<S: Into<String>>(message: S) -> Self {
Self::RemovalFailed {
message: message.into(),
}
}
/// 创建添加失败错误
pub fn addition_failed<S: Into<String>>(message: S) -> Self {
Self::AdditionFailed {
message: message.into(),
}
}
/// 创建不支持格式错误
pub fn unsupported_format<S: Into<String>>(format: S, supported: Vec<String>) -> Self {
Self::UnsupportedFormat {
format: format.into(),
supported,
}
}
/// 创建FFmpeg错误
pub fn ffmpeg_error<S: Into<String>>(message: S) -> Self {
Self::FFmpegError {
message: message.into(),
}
}
/// 创建OpenCV错误
pub fn opencv_error<S: Into<String>>(message: S) -> Self {
Self::OpenCVError {
message: message.into(),
}
}
/// 创建模板不存在错误
pub fn template_not_found<S: Into<String>>(template_id: S) -> Self {
Self::TemplateNotFound {
template_id: template_id.into(),
}
}
/// 创建模板名称已存在错误
pub fn template_name_exists<S: Into<String>>(name: S) -> Self {
Self::TemplateNameExists { name: name.into() }
}
/// 创建无效配置错误
pub fn invalid_config<S: Into<String>>(message: S) -> Self {
Self::InvalidConfig {
message: message.into(),
}
}
/// 创建文件操作失败错误
pub fn file_operation_failed<S: Into<String>>(
operation: S,
path: S,
message: S,
) -> Self {
Self::FileOperationFailed {
operation: operation.into(),
path: path.into(),
message: message.into(),
}
}
/// 创建批量任务失败错误
pub fn batch_task_failed<S: Into<String>>(task_id: S, message: S) -> Self {
Self::BatchTaskFailed {
task_id: task_id.into(),
message: message.into(),
}
}
/// 创建任务取消错误
pub fn task_cancelled<S: Into<String>>(task_id: S) -> Self {
Self::TaskCancelled {
task_id: task_id.into(),
}
}
/// 创建任务超时错误
pub fn task_timeout<S: Into<String>>(task_id: S, timeout_ms: u64) -> Self {
Self::TaskTimeout {
task_id: task_id.into(),
timeout_ms,
}
}
/// 创建数据库错误
pub fn database_error<S: Into<String>>(operation: S, message: S) -> Self {
Self::DatabaseError {
operation: operation.into(),
message: message.into(),
}
}
/// 创建网络错误
pub fn network_error<S: Into<String>>(url: S, message: S) -> Self {
Self::NetworkError {
url: url.into(),
message: message.into(),
}
}
/// 创建权限不足错误
pub fn permission_denied<S: Into<String>>(operation: S) -> Self {
Self::PermissionDenied {
operation: operation.into(),
}
}
/// 创建资源不足错误
pub fn insufficient_resources<S: Into<String>>(
resource: S,
required: S,
available: S,
) -> Self {
Self::InsufficientResources {
resource: resource.into(),
required: required.into(),
available: available.into(),
}
}
/// 创建配置错误
pub fn configuration_error<S: Into<String>>(key: S, value: S, message: S) -> Self {
Self::ConfigurationError {
key: key.into(),
value: value.into(),
message: message.into(),
}
}
/// 创建验证错误
pub fn validation_error<S: Into<String>>(field: S, value: S, reason: S) -> Self {
Self::ValidationError {
field: field.into(),
value: value.into(),
reason: reason.into(),
}
}
/// 创建内部错误
pub fn internal_error<S: Into<String>>(message: S) -> Self {
Self::InternalError {
message: message.into(),
}
}
/// 创建外部依赖错误
pub fn external_dependency_error<S: Into<String>>(dependency: S, message: S) -> Self {
Self::ExternalDependencyError {
dependency: dependency.into(),
message: message.into(),
}
}
/// 获取错误类型
pub fn error_type(&self) -> &'static str {
match self {
Self::DetectionFailed { .. } => "detection_failed",
Self::RemovalFailed { .. } => "removal_failed",
Self::AdditionFailed { .. } => "addition_failed",
Self::UnsupportedFormat { .. } => "unsupported_format",
Self::FFmpegError { .. } => "ffmpeg_error",
Self::OpenCVError { .. } => "opencv_error",
Self::TemplateNotFound { .. } => "template_not_found",
Self::TemplateNameExists { .. } => "template_name_exists",
Self::InvalidConfig { .. } => "invalid_config",
Self::FileOperationFailed { .. } => "file_operation_failed",
Self::BatchTaskFailed { .. } => "batch_task_failed",
Self::TaskCancelled { .. } => "task_cancelled",
Self::TaskTimeout { .. } => "task_timeout",
Self::DatabaseError { .. } => "database_error",
Self::NetworkError { .. } => "network_error",
Self::PermissionDenied { .. } => "permission_denied",
Self::InsufficientResources { .. } => "insufficient_resources",
Self::ConfigurationError { .. } => "configuration_error",
Self::ValidationError { .. } => "validation_error",
Self::InternalError { .. } => "internal_error",
Self::ExternalDependencyError { .. } => "external_dependency_error",
}
}
/// 判断是否为可重试错误
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::NetworkError { .. }
| Self::TaskTimeout { .. }
| Self::InsufficientResources { .. }
| Self::ExternalDependencyError { .. }
)
}
/// 判断是否为用户错误
pub fn is_user_error(&self) -> bool {
matches!(
self,
Self::UnsupportedFormat { .. }
| Self::TemplateNotFound { .. }
| Self::TemplateNameExists { .. }
| Self::InvalidConfig { .. }
| Self::ValidationError { .. }
| Self::PermissionDenied { .. }
)
}
/// 判断是否为系统错误
pub fn is_system_error(&self) -> bool {
matches!(
self,
Self::FFmpegError { .. }
| Self::OpenCVError { .. }
| Self::FileOperationFailed { .. }
| Self::DatabaseError { .. }
| Self::InternalError { .. }
)
}
/// 获取错误的严重程度
pub fn severity(&self) -> ErrorSeverity {
match self {
Self::ValidationError { .. } | Self::InvalidConfig { .. } => ErrorSeverity::Warning,
Self::TemplateNotFound { .. }
| Self::TemplateNameExists { .. }
| Self::UnsupportedFormat { .. }
| Self::PermissionDenied { .. } => ErrorSeverity::Error,
Self::DetectionFailed { .. }
| Self::RemovalFailed { .. }
| Self::AdditionFailed { .. }
| Self::BatchTaskFailed { .. }
| Self::TaskTimeout { .. } => ErrorSeverity::Error,
Self::FFmpegError { .. }
| Self::OpenCVError { .. }
| Self::DatabaseError { .. }
| Self::FileOperationFailed { .. }
| Self::NetworkError { .. }
| Self::InsufficientResources { .. }
| Self::ExternalDependencyError { .. }
| Self::InternalError { .. } => ErrorSeverity::Critical,
Self::TaskCancelled { .. } => ErrorSeverity::Info,
Self::ConfigurationError { .. } => ErrorSeverity::Warning,
}
}
}
/// 错误严重程度
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorSeverity {
Info,
Warning,
Error,
Critical,
}
impl ErrorSeverity {
pub fn as_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
Self::Critical => "critical",
}
}
}
/// 错误上下文信息
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub operation: String,
pub material_id: Option<String>,
pub template_id: Option<String>,
pub task_id: Option<String>,
pub file_path: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub additional_info: std::collections::HashMap<String, String>,
}
impl ErrorContext {
pub fn new<S: Into<String>>(operation: S) -> Self {
Self {
operation: operation.into(),
material_id: None,
template_id: None,
task_id: None,
file_path: None,
timestamp: chrono::Utc::now(),
additional_info: std::collections::HashMap::new(),
}
}
pub fn with_material_id<S: Into<String>>(mut self, material_id: S) -> Self {
self.material_id = Some(material_id.into());
self
}
pub fn with_template_id<S: Into<String>>(mut self, template_id: S) -> Self {
self.template_id = Some(template_id.into());
self
}
pub fn with_task_id<S: Into<String>>(mut self, task_id: S) -> Self {
self.task_id = Some(task_id.into());
self
}
pub fn with_file_path<S: Into<String>>(mut self, file_path: S) -> Self {
self.file_path = Some(file_path.into());
self
}
pub fn with_info<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.additional_info.insert(key.into(), value.into());
self
}
}
/// 水印错误结果类型
pub type WatermarkResult<T> = Result<T, WatermarkError>;
/// 从标准错误转换为水印错误
impl From<std::io::Error> for WatermarkError {
fn from(err: std::io::Error) -> Self {
Self::FileOperationFailed {
operation: "io_operation".to_string(),
path: "unknown".to_string(),
message: err.to_string(),
}
}
}
impl From<rusqlite::Error> for WatermarkError {
fn from(err: rusqlite::Error) -> Self {
Self::DatabaseError {
operation: "database_operation".to_string(),
message: err.to_string(),
}
}
}
impl From<serde_json::Error> for WatermarkError {
fn from(err: serde_json::Error) -> Self {
Self::ConfigurationError {
key: "json_config".to_string(),
value: "unknown".to_string(),
message: err.to_string(),
}
}
}
impl From<anyhow::Error> for WatermarkError {
fn from(err: anyhow::Error) -> Self {
Self::InternalError {
message: err.to_string(),
}
}
}

View File

@@ -0,0 +1,394 @@
use anyhow::{Result, anyhow};
use std::sync::Arc;
use std::time::Instant;
use tracing::{info, warn, error, debug};
use crate::data::models::watermark::{
BatchWatermarkTask, WatermarkOperation, BatchTaskStatus, BatchProgress,
WatermarkProcessingResult, WatermarkConfig, WatermarkRemovalConfig,
WatermarkDetectionConfig
};
use crate::data::repositories::material_repository::MaterialRepository;
use crate::business::services::{
watermark_detection_service::WatermarkDetectionService,
watermark_removal_service::WatermarkRemovalService,
watermark_addition_service::WatermarkAdditionService,
task_manager::TASK_MANAGER,
};
// use crate::infrastructure::event_bus::EventBusManager;
use crate::infrastructure::monitoring::PERFORMANCE_MONITOR;
/// 批量水印处理器
/// 遵循 Tauri 开发规范的异步业务逻辑层设计
pub struct BatchWatermarkProcessor;
impl BatchWatermarkProcessor {
/// 启动批量水印处理任务
pub async fn start_batch_task(
task: BatchWatermarkTask,
repository: Arc<MaterialRepository>,
) -> Result<()> {
let timer = PERFORMANCE_MONITOR.start_operation("batch_watermark_processing");
let start_time = Instant::now();
info!(
task_id = %task.task_id,
operation = ?task.operation,
material_count = task.material_ids.len(),
"开始批量水印处理任务"
);
// 将任务添加到任务管理器
TASK_MANAGER.add_task(task.clone())?;
// 更新任务状态为运行中
TASK_MANAGER.update_task_status(&task.task_id, BatchTaskStatus::Running)?;
// TODO: 发布任务开始事件
let mut progress = BatchProgress {
total_items: task.material_ids.len() as u32,
processed_items: 0,
failed_items: 0,
current_item: None,
progress_percentage: 0.0,
estimated_remaining_ms: None,
errors: Vec::new(),
detection_results: Vec::new(),
processing_results: Vec::new(),
};
let mut results = Vec::new();
// 处理每个素材
for (index, material_id) in task.material_ids.iter().enumerate() {
progress.current_item = Some(material_id.clone());
progress.progress_percentage = (index as f32 / task.material_ids.len() as f32) * 100.0;
// 更新任务管理器中的进度
TASK_MANAGER.update_task_progress(&task.task_id, progress.clone())?;
// TODO: 发布进度更新事件
debug!(
task_id = %task.task_id,
material_id = %material_id,
progress = progress.progress_percentage,
"处理素材"
);
// 处理单个素材
let result = Self::process_single_material(
material_id,
&task.operation,
&task.config,
repository.clone(),
).await;
match result {
Ok(processing_result) => {
if processing_result.success {
progress.processed_items += 1;
// 如果是检测操作,提取检测结果
if task.operation == WatermarkOperation::Detect {
if let Some(metadata) = &processing_result.metadata {
if let Ok(detection_result) = serde_json::from_value::<crate::data::models::watermark::WatermarkDetectionResult>(metadata.clone()) {
progress.detection_results.push(detection_result);
}
}
}
// 添加处理结果
progress.processing_results.push(processing_result.clone());
info!(
task_id = %task.task_id,
material_id = %material_id,
"素材处理成功"
);
} else {
progress.failed_items += 1;
if let Some(error) = &processing_result.error_message {
progress.errors.push(format!("{}: {}", material_id, error));
}
warn!(
task_id = %task.task_id,
material_id = %material_id,
error = ?processing_result.error_message,
"素材处理失败"
);
}
results.push(processing_result);
}
Err(e) => {
progress.failed_items += 1;
progress.errors.push(format!("{}: {}", material_id, e));
error!(
task_id = %task.task_id,
material_id = %material_id,
error = %e,
"素材处理异常"
);
}
}
// 计算预估剩余时间
if index > 0 {
let elapsed = start_time.elapsed().as_millis() as u64;
let avg_time_per_item = elapsed / (index + 1) as u64;
let remaining_items = task.material_ids.len() - index - 1;
progress.estimated_remaining_ms = Some(avg_time_per_item * remaining_items as u64);
}
// 更新处理后的进度
progress.progress_percentage = ((index + 1) as f32 / task.material_ids.len() as f32) * 100.0;
TASK_MANAGER.update_task_progress(&task.task_id, progress.clone())?;
}
// 完成处理
progress.current_item = None;
progress.progress_percentage = 100.0;
progress.estimated_remaining_ms = Some(0);
let final_status = if progress.failed_items == 0 {
BatchTaskStatus::Completed
} else if progress.processed_items == 0 {
BatchTaskStatus::Failed
} else {
BatchTaskStatus::Completed // 部分成功也算完成
};
// 更新最终状态
TASK_MANAGER.update_task_status(&task.task_id, final_status.clone())?;
TASK_MANAGER.update_task_progress(&task.task_id, progress.clone())?;
let processing_time = start_time.elapsed().as_millis() as u64;
// TODO: 发布任务完成事件
info!(
task_id = %task.task_id,
processed_items = progress.processed_items,
failed_items = progress.failed_items,
processing_time_ms = processing_time,
"批量水印处理任务完成"
);
Ok(())
}
/// 处理单个素材
async fn process_single_material(
material_id: &str,
operation: &WatermarkOperation,
config: &serde_json::Value,
repository: Arc<MaterialRepository>,
) -> Result<WatermarkProcessingResult> {
// 获取素材信息
let material = repository.get_by_id(material_id)?
.ok_or_else(|| anyhow!("素材不存在: {}", material_id))?;
let input_path = &material.original_path;
let output_dir = Self::get_output_directory(&material.project_id, operation);
std::fs::create_dir_all(&output_dir)?;
let output_filename = Self::generate_output_filename(&material.name, operation);
let output_path = format!("{}/{}", output_dir, output_filename);
match operation {
WatermarkOperation::Detect => {
let detection_config: WatermarkDetectionConfig = serde_json::from_value(config.clone())?;
let detection_result = if format!("{:?}", material.material_type).to_lowercase().contains("video") {
WatermarkDetectionService::detect_watermarks_in_video(
material_id,
input_path,
&detection_config,
repository.clone(),
).await?
} else {
WatermarkDetectionService::detect_watermarks_in_image(
material_id,
input_path,
&detection_config,
).await?
};
// 保存检测结果到数据库
Self::save_detection_result(&detection_result, &repository).await?;
Ok(WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: operation.clone(),
success: true,
output_path: None, // 检测不产生输出文件
processing_time_ms: detection_result.processing_time_ms,
error_message: None,
metadata: Some(serde_json::to_value(detection_result)?),
})
}
WatermarkOperation::Remove => {
let removal_config: WatermarkRemovalConfig = serde_json::from_value(config.clone())?;
if format!("{:?}", material.material_type).to_lowercase().contains("video") {
WatermarkRemovalService::remove_watermarks_from_video(
material_id,
input_path,
&output_path,
&removal_config,
repository.clone(),
).await
} else {
WatermarkRemovalService::remove_watermarks_from_image(
material_id,
input_path,
&output_path,
&removal_config,
).await
}
}
WatermarkOperation::Add => {
let watermark_config: WatermarkConfig = serde_json::from_value(config.clone())?;
let watermark_path = Self::get_watermark_path_from_config(&watermark_config)?;
if format!("{:?}", material.material_type).to_lowercase().contains("video") {
WatermarkAdditionService::add_watermark_to_video(
material_id,
input_path,
&output_path,
&watermark_path,
&watermark_config,
repository.clone(),
).await
} else {
WatermarkAdditionService::add_watermark_to_image(
material_id,
input_path,
&output_path,
&watermark_path,
&watermark_config,
).await
}
}
WatermarkOperation::DetectAndRemove => {
// 先检测,再移除
let detection_config: WatermarkDetectionConfig = serde_json::from_value(
config.get("detection").unwrap_or(&serde_json::Value::Null).clone()
)?;
let removal_config: WatermarkRemovalConfig = serde_json::from_value(
config.get("removal").unwrap_or(&serde_json::Value::Null).clone()
)?;
// 执行检测
let detection_result = if format!("{:?}", material.material_type).to_lowercase().contains("video") {
WatermarkDetectionService::detect_watermarks_in_video(
material_id,
input_path,
&detection_config,
repository.clone(),
).await?
} else {
WatermarkDetectionService::detect_watermarks_in_image(
material_id,
input_path,
&detection_config,
).await?
};
// 保存检测结果
Self::save_detection_result(&detection_result, &repository).await?;
// 如果检测到水印,则执行移除
if !detection_result.detections.is_empty() {
if format!("{:?}", material.material_type).to_lowercase().contains("video") {
WatermarkRemovalService::remove_watermarks_from_video(
material_id,
input_path,
&output_path,
&removal_config,
repository.clone(),
).await
} else {
WatermarkRemovalService::remove_watermarks_from_image(
material_id,
input_path,
&output_path,
&removal_config,
).await
}
} else {
Ok(WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: operation.clone(),
success: true,
output_path: None,
processing_time_ms: detection_result.processing_time_ms,
error_message: None,
metadata: Some(serde_json::json!({
"message": "未检测到水印,无需移除"
})),
})
}
}
WatermarkOperation::Replace => {
// TODO: 实现替换逻辑(检测 + 移除 + 添加)
Err(anyhow!("替换操作尚未实现"))
}
}
}
/// 保存检测结果
async fn save_detection_result(
result: &crate::data::models::watermark::WatermarkDetectionResult,
repository: &MaterialRepository,
) -> Result<()> {
// TODO: 实现检测结果保存逻辑
debug!(
material_id = %result.material_id,
detection_count = result.detections.len(),
"保存检测结果"
);
Ok(())
}
/// 获取输出目录
fn get_output_directory(project_id: &str, operation: &WatermarkOperation) -> String {
let operation_name = match operation {
WatermarkOperation::Detect => "detection",
WatermarkOperation::Remove => "watermark_removed",
WatermarkOperation::Add => "watermark_added",
WatermarkOperation::DetectAndRemove => "watermark_removed",
WatermarkOperation::Replace => "watermark_replaced",
};
format!("projects/{}/output/{}", project_id, operation_name)
}
/// 生成输出文件名
fn generate_output_filename(original_filename: &str, operation: &WatermarkOperation) -> String {
let operation_suffix = match operation {
WatermarkOperation::Detect => return original_filename.to_string(), // 检测不产生文件
WatermarkOperation::Remove => "_no_watermark",
WatermarkOperation::Add => "_watermarked",
WatermarkOperation::DetectAndRemove => "_no_watermark",
WatermarkOperation::Replace => "_watermark_replaced",
};
if let Some(dot_index) = original_filename.rfind('.') {
let (name, ext) = original_filename.split_at(dot_index);
format!("{}{}{}", name, operation_suffix, ext)
} else {
format!("{}{}", original_filename, operation_suffix)
}
}
/// 从配置中获取水印路径
fn get_watermark_path_from_config(config: &WatermarkConfig) -> Result<String> {
// TODO: 根据水印类型和配置获取实际的水印文件路径
// 这里需要与水印模板管理系统集成
Ok("watermarks/default.png".to_string())
}
}

View File

@@ -21,6 +21,12 @@ pub mod project_template_binding_service;
pub mod material_matching_service;
pub mod template_matching_result_service;
pub mod export_record_service;
pub mod watermark_detection_service;
pub mod watermark_removal_service;
pub mod watermark_addition_service;
pub mod batch_watermark_processor;
pub mod watermark_template_service;
pub mod task_manager;
pub mod video_generation_service;
pub mod conversation_service;
pub mod jianying_export;

View File

@@ -0,0 +1,145 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use anyhow::Result;
use tracing::{info, debug};
use crate::data::models::watermark::{BatchWatermarkTask, BatchTaskStatus, BatchProgress};
/// 任务状态管理器
/// 用于跟踪批量水印处理任务的状态
pub struct TaskManager {
tasks: Arc<Mutex<HashMap<String, BatchWatermarkTask>>>,
}
impl TaskManager {
/// 创建新的任务管理器
pub fn new() -> Self {
Self {
tasks: Arc::new(Mutex::new(HashMap::new())),
}
}
/// 添加任务
pub fn add_task(&self, task: BatchWatermarkTask) -> Result<()> {
let mut tasks = self.tasks.lock().unwrap();
debug!(task_id = %task.task_id, "添加任务到管理器");
tasks.insert(task.task_id.clone(), task);
Ok(())
}
/// 获取任务状态
pub fn get_task(&self, task_id: &str) -> Option<BatchWatermarkTask> {
let tasks = self.tasks.lock().unwrap();
tasks.get(task_id).cloned()
}
/// 更新任务状态
pub fn update_task_status(&self, task_id: &str, status: BatchTaskStatus) -> Result<()> {
let mut tasks = self.tasks.lock().unwrap();
if let Some(task) = tasks.get_mut(task_id) {
task.status = status.clone();
task.updated_at = chrono::Utc::now();
// 如果任务开始运行,设置开始时间
if status == BatchTaskStatus::Running && task.started_at.is_none() {
task.started_at = Some(chrono::Utc::now());
}
// 如果任务完成,设置完成时间
if matches!(status, BatchTaskStatus::Completed | BatchTaskStatus::Failed | BatchTaskStatus::Cancelled) {
task.completed_at = Some(chrono::Utc::now());
}
debug!(task_id = %task_id, status = ?status, "更新任务状态");
Ok(())
} else {
Err(anyhow::anyhow!("任务不存在: {}", task_id))
}
}
/// 更新任务进度
pub fn update_task_progress(&self, task_id: &str, progress: BatchProgress) -> Result<()> {
let mut tasks = self.tasks.lock().unwrap();
if let Some(task) = tasks.get_mut(task_id) {
task.progress = progress;
task.updated_at = chrono::Utc::now();
debug!(
task_id = %task_id,
processed = task.progress.processed_items,
total = task.progress.total_items,
percentage = task.progress.progress_percentage,
"更新任务进度"
);
Ok(())
} else {
Err(anyhow::anyhow!("任务不存在: {}", task_id))
}
}
/// 移除任务
pub fn remove_task(&self, task_id: &str) -> Option<BatchWatermarkTask> {
let mut tasks = self.tasks.lock().unwrap();
let removed = tasks.remove(task_id);
if removed.is_some() {
debug!(task_id = %task_id, "从管理器中移除任务");
}
removed
}
/// 获取所有任务
pub fn get_all_tasks(&self) -> Vec<BatchWatermarkTask> {
let tasks = self.tasks.lock().unwrap();
tasks.values().cloned().collect()
}
/// 获取运行中的任务数量
pub fn get_running_task_count(&self) -> usize {
let tasks = self.tasks.lock().unwrap();
tasks.values().filter(|task| task.status == BatchTaskStatus::Running).count()
}
/// 清理已完成的任务(超过指定时间)
pub fn cleanup_completed_tasks(&self, max_age_hours: u64) -> usize {
let mut tasks = self.tasks.lock().unwrap();
let cutoff_time = chrono::Utc::now() - chrono::Duration::hours(max_age_hours as i64);
let initial_count = tasks.len();
tasks.retain(|_, task| {
// 保留未完成的任务或最近完成的任务
!matches!(task.status, BatchTaskStatus::Completed | BatchTaskStatus::Failed | BatchTaskStatus::Cancelled)
|| task.completed_at.map_or(true, |completed| completed > cutoff_time)
});
let removed_count = initial_count - tasks.len();
if removed_count > 0 {
info!(removed_count = removed_count, "清理已完成的任务");
}
removed_count
}
/// 取消任务
pub fn cancel_task(&self, task_id: &str) -> Result<()> {
self.update_task_status(task_id, BatchTaskStatus::Cancelled)
}
/// 暂停任务
pub fn pause_task(&self, task_id: &str) -> Result<()> {
self.update_task_status(task_id, BatchTaskStatus::Paused)
}
/// 恢复任务
pub fn resume_task(&self, task_id: &str) -> Result<()> {
self.update_task_status(task_id, BatchTaskStatus::Running)
}
}
impl Default for TaskManager {
fn default() -> Self {
Self::new()
}
}
// 全局任务管理器实例
lazy_static::lazy_static! {
pub static ref TASK_MANAGER: TaskManager = TaskManager::new();
}

View File

@@ -1,5 +1,6 @@
pub mod draft_parser_tests;
pub mod cloud_upload_service_tests;
pub mod watermark_tests;
// 测试工具函数
pub mod test_utils {

View File

@@ -0,0 +1,376 @@
#[cfg(test)]
mod watermark_tests {
use crate::data::models::watermark::*;
use crate::data::repositories::watermark_template_repository::WatermarkTemplateRepository;
use crate::infrastructure::database::Database;
use std::sync::Arc;
use tempfile::TempDir;
/// 创建测试数据库
fn create_test_database() -> Arc<Database> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
Arc::new(Database::new(db_path.to_str().unwrap()).unwrap())
}
/// 创建测试水印模板
fn create_test_template() -> WatermarkTemplate {
WatermarkTemplate {
id: "test_template_1".to_string(),
name: "测试水印".to_string(),
file_path: "/tmp/test_watermark.png".to_string(),
thumbnail_path: Some("/tmp/test_watermark_thumb.jpg".to_string()),
category: WatermarkCategory::Logo,
watermark_type: WatermarkType::Image,
file_size: 1024,
width: Some(200),
height: Some(100),
description: Some("测试用水印模板".to_string()),
tags: vec!["test".to_string(), "logo".to_string()],
is_active: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
/// 创建测试检测配置
fn create_test_detection_config() -> WatermarkDetectionConfig {
WatermarkDetectionConfig {
similarity_threshold: 0.8,
min_watermark_size: (32, 32),
max_watermark_size: (512, 512),
detection_regions: vec![DetectionRegion::Corners, DetectionRegion::Center],
frame_sample_rate: 30,
methods: vec![DetectionMethod::TemplateMatching, DetectionMethod::EdgeDetection],
template_ids: None,
}
}
/// 创建测试移除配置
fn create_test_removal_config() -> WatermarkRemovalConfig {
WatermarkRemovalConfig {
method: RemovalMethod::Blurring,
quality_level: QualityLevel::Medium,
preserve_aspect_ratio: true,
target_regions: None,
inpainting_model: None,
blur_radius: Some(5.0),
crop_margin: Some(10),
}
}
/// 创建测试添加配置
fn create_test_addition_config() -> WatermarkConfig {
WatermarkConfig {
watermark_type: WatermarkType::Image,
position: WatermarkPosition::BottomRight,
opacity: 0.8,
scale: 1.0,
rotation: 0.0,
animation: None,
blend_mode: BlendMode::Normal,
quality_level: QualityLevel::Medium,
}
}
#[tokio::test]
async fn test_watermark_template_repository_crud() {
let database = create_test_database();
let repository = Arc::new(WatermarkTemplateRepository::new(database));
let template = create_test_template();
// 测试创建
let result = repository.create(&template);
assert!(result.is_ok(), "创建水印模板失败: {:?}", result.err());
// 测试查询
let retrieved = repository.get_by_id(&template.id).unwrap();
assert!(retrieved.is_some(), "查询水印模板失败");
let retrieved_template = retrieved.unwrap();
assert_eq!(retrieved_template.id, template.id);
assert_eq!(retrieved_template.name, template.name);
// 测试更新
let mut updated_template = retrieved_template.clone();
updated_template.name = "更新后的水印".to_string();
updated_template.updated_at = chrono::Utc::now();
let update_result = repository.update(&updated_template);
assert!(update_result.is_ok(), "更新水印模板失败: {:?}", update_result.err());
// 验证更新
let updated_retrieved = repository.get_by_id(&template.id).unwrap().unwrap();
assert_eq!(updated_retrieved.name, "更新后的水印");
// 测试删除
let delete_result = repository.delete(&template.id);
assert!(delete_result.is_ok(), "删除水印模板失败: {:?}", delete_result.err());
// 验证删除
let deleted_check = repository.get_by_id(&template.id).unwrap();
assert!(deleted_check.is_none(), "水印模板应该已被删除");
}
#[tokio::test]
async fn test_watermark_template_search() {
let database = create_test_database();
let repository = Arc::new(WatermarkTemplateRepository::new(database));
// 创建多个测试模板
let mut template1 = create_test_template();
template1.id = "template_1".to_string();
template1.name = "Logo水印".to_string();
template1.category = WatermarkCategory::Logo;
let mut template2 = create_test_template();
template2.id = "template_2".to_string();
template2.name = "版权水印".to_string();
template2.category = WatermarkCategory::Copyright;
let mut template3 = create_test_template();
template3.id = "template_3".to_string();
template3.name = "签名水印".to_string();
template3.category = WatermarkCategory::Signature;
template3.watermark_type = WatermarkType::Text;
// 插入模板
repository.create(&template1).unwrap();
repository.create(&template2).unwrap();
repository.create(&template3).unwrap();
// 测试按分类查询
let logo_templates = repository.get_by_category(&WatermarkCategory::Logo).unwrap();
assert_eq!(logo_templates.len(), 1);
assert_eq!(logo_templates[0].name, "Logo水印");
// 测试按类型查询
let text_templates = repository.get_by_type(&WatermarkType::Text).unwrap();
assert_eq!(text_templates.len(), 1);
assert_eq!(text_templates[0].name, "签名水印");
// 测试搜索
let search_results = repository.search("水印").unwrap();
assert_eq!(search_results.len(), 3);
let logo_search = repository.search("Logo").unwrap();
assert_eq!(logo_search.len(), 1);
assert_eq!(logo_search[0].name, "Logo水印");
}
#[test]
fn test_watermark_detection_config_validation() {
let config = create_test_detection_config();
// 验证阈值范围
assert!(config.similarity_threshold >= 0.0 && config.similarity_threshold <= 1.0);
// 验证尺寸设置
assert!(config.min_watermark_size.0 > 0);
assert!(config.min_watermark_size.1 > 0);
assert!(config.max_watermark_size.0 >= config.min_watermark_size.0);
assert!(config.max_watermark_size.1 >= config.min_watermark_size.1);
// 验证采样率
assert!(config.frame_sample_rate > 0);
// 验证检测方法不为空
assert!(!config.methods.is_empty());
}
#[test]
fn test_watermark_removal_config_validation() {
let config = create_test_removal_config();
// 验证模糊半径
if let Some(blur_radius) = config.blur_radius {
assert!(blur_radius > 0.0);
}
// 验证裁剪边距
if let Some(crop_margin) = config.crop_margin {
assert!(crop_margin > 0);
}
}
#[test]
fn test_watermark_addition_config_validation() {
let config = create_test_addition_config();
// 验证透明度范围
assert!(config.opacity >= 0.0 && config.opacity <= 1.0);
// 验证缩放比例
assert!(config.scale > 0.0);
// 验证旋转角度范围
assert!(config.rotation >= -360.0 && config.rotation <= 360.0);
}
#[test]
fn test_watermark_position_calculation() {
let video_width = 1920.0;
let video_height = 1080.0;
let scale = 1.0;
// 测试固定位置
let positions = vec![
WatermarkPosition::TopLeft,
WatermarkPosition::TopRight,
WatermarkPosition::BottomLeft,
WatermarkPosition::BottomRight,
WatermarkPosition::Center,
];
for position in positions {
// 这里应该调用实际的位置计算函数
// 由于函数在服务中是私有的,这里只做概念验证
match position {
WatermarkPosition::TopLeft => {
// 应该返回 (10, 10) 或类似的左上角位置
}
WatermarkPosition::BottomRight => {
// 应该返回接近 (video_width - watermark_width, video_height - watermark_height) 的位置
}
_ => {}
}
}
}
#[test]
fn test_batch_progress_calculation() {
let mut progress = BatchProgress {
total_items: 10,
processed_items: 3,
failed_items: 1,
current_item: Some("material_4".to_string()),
progress_percentage: 0.0,
estimated_remaining_ms: None,
errors: vec!["Error 1".to_string()],
};
// 计算进度百分比
progress.progress_percentage = (progress.processed_items as f32 / progress.total_items as f32) * 100.0;
assert_eq!(progress.progress_percentage, 30.0);
// 验证错误计数
assert_eq!(progress.errors.len(), 1);
assert_eq!(progress.failed_items, 1);
}
#[test]
fn test_watermark_error_types() {
use crate::business::errors::watermark_errors::*;
// 测试错误创建
let detection_error = WatermarkError::detection_failed("检测算法失败");
assert_eq!(detection_error.error_type(), "detection_failed");
assert!(!detection_error.is_retryable());
assert!(detection_error.is_system_error());
let template_error = WatermarkError::template_not_found("template_123");
assert_eq!(template_error.error_type(), "template_not_found");
assert!(!template_error.is_retryable());
assert!(template_error.is_user_error());
let network_error = WatermarkError::network_error("http://example.com", "连接超时");
assert_eq!(network_error.error_type(), "network_error");
assert!(network_error.is_retryable());
assert!(!network_error.is_user_error());
}
#[test]
fn test_error_severity_levels() {
use crate::business::errors::watermark_errors::*;
let validation_error = WatermarkError::validation_error("opacity", "1.5", "值超出范围");
assert_eq!(validation_error.severity(), ErrorSeverity::Warning);
let template_error = WatermarkError::template_not_found("template_123");
assert_eq!(template_error.severity(), ErrorSeverity::Error);
let internal_error = WatermarkError::internal_error("系统内部错误");
assert_eq!(internal_error.severity(), ErrorSeverity::Critical);
let cancelled_error = WatermarkError::task_cancelled("task_123");
assert_eq!(cancelled_error.severity(), ErrorSeverity::Info);
}
#[test]
fn test_error_context() {
use crate::business::errors::watermark_errors::*;
let context = ErrorContext::new("watermark_detection")
.with_material_id("material_123")
.with_template_id("template_456")
.with_file_path("/path/to/video.mp4")
.with_info("algorithm", "template_matching")
.with_info("threshold", "0.8");
assert_eq!(context.operation, "watermark_detection");
assert_eq!(context.material_id, Some("material_123".to_string()));
assert_eq!(context.template_id, Some("template_456".to_string()));
assert_eq!(context.file_path, Some("/path/to/video.mp4".to_string()));
assert_eq!(context.additional_info.get("algorithm"), Some(&"template_matching".to_string()));
assert_eq!(context.additional_info.get("threshold"), Some(&"0.8".to_string()));
}
#[tokio::test]
async fn test_watermark_template_stats() {
let database = create_test_database();
let repository = Arc::new(WatermarkTemplateRepository::new(database));
// 创建不同类型的模板
let mut template1 = create_test_template();
template1.id = "template_1".to_string();
template1.category = WatermarkCategory::Logo;
template1.watermark_type = WatermarkType::Image;
template1.file_size = 1000;
let mut template2 = create_test_template();
template2.id = "template_2".to_string();
template2.category = WatermarkCategory::Copyright;
template2.watermark_type = WatermarkType::Text;
template2.file_size = 500;
repository.create(&template1).unwrap();
repository.create(&template2).unwrap();
// 获取统计信息
let stats = repository.get_stats().unwrap();
assert_eq!(stats["total_templates"], 2);
assert_eq!(stats["total_size"], 1500);
// 验证分类统计
let by_category = &stats["by_category"];
assert!(by_category.get("\"Logo\"").is_some());
assert!(by_category.get("\"Copyright\"").is_some());
// 验证类型统计
let by_type = &stats["by_type"];
assert!(by_type.get("\"Image\"").is_some());
assert!(by_type.get("\"Text\"").is_some());
}
#[test]
fn test_watermark_template_name_validation() {
let database = create_test_database();
let repository = Arc::new(WatermarkTemplateRepository::new(database));
let template = create_test_template();
// 创建模板
repository.create(&template).unwrap();
// 测试名称冲突检查
let name_exists = repository.name_exists(&template.name, None).unwrap();
assert!(name_exists, "应该检测到名称已存在");
// 测试排除自身的名称检查
let name_exists_exclude_self = repository.name_exists(&template.name, Some(&template.id)).unwrap();
assert!(!name_exists_exclude_self, "排除自身时不应该检测到名称冲突");
// 测试不存在的名称
let new_name_exists = repository.name_exists("不存在的名称", None).unwrap();
assert!(!new_name_exists, "不存在的名称不应该被检测为已存在");
}
}

View File

@@ -0,0 +1,713 @@
use anyhow::{Result, anyhow};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use tracing::{info, error, debug};
use crate::data::models::watermark::{
WatermarkConfig, WatermarkType, WatermarkPosition, WatermarkAnimation,
AnimationType, BlendMode, QualityLevel, WatermarkProcessingResult,
WatermarkOperation, DynamicPositionRule, Corner
};
use crate::data::repositories::material_repository::MaterialRepository;
use crate::infrastructure::ffmpeg_watermark::FFmpegWatermark;
use crate::infrastructure::monitoring::PERFORMANCE_MONITOR;
/// 水印添加服务
/// 遵循 Tauri 开发规范的业务逻辑层设计
pub struct WatermarkAdditionService;
impl WatermarkAdditionService {
/// 为视频添加水印
pub async fn add_watermark_to_video(
material_id: &str,
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
repository: Arc<MaterialRepository>,
) -> Result<WatermarkProcessingResult> {
let timer = PERFORMANCE_MONITOR.start_operation("watermark_addition_video");
let start_time = Instant::now();
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
watermark_path = %watermark_path,
watermark_type = ?config.watermark_type,
"开始为视频添加水印"
);
// 验证输入文件存在
if !Path::new(input_path).exists() {
return Err(anyhow!("输入视频文件不存在: {}", input_path));
}
if !Path::new(watermark_path).exists() {
return Err(anyhow!("水印文件不存在: {}", watermark_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let result = match config.watermark_type {
WatermarkType::Image => {
Self::add_image_watermark_to_video(
input_path,
output_path,
watermark_path,
config,
).await
}
WatermarkType::Text => {
Self::add_text_watermark_to_video(
input_path,
output_path,
watermark_path,
config,
).await
}
WatermarkType::Vector => {
Self::add_vector_watermark_to_video(
input_path,
output_path,
watermark_path,
config,
).await
}
WatermarkType::Animated => {
Self::add_animated_watermark_to_video(
input_path,
output_path,
watermark_path,
config,
).await
}
};
let processing_time = start_time.elapsed().as_millis() as u64;
let processing_result = match result {
Ok(_) => {
info!(
material_id = %material_id,
processing_time_ms = processing_time,
"视频水印添加成功"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Add,
success: true,
output_path: Some(output_path.to_string()),
processing_time_ms: processing_time,
error_message: None,
metadata: None,
}
}
Err(e) => {
error!(
material_id = %material_id,
error = %e,
processing_time_ms = processing_time,
"视频水印添加失败"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Add,
success: false,
output_path: None,
processing_time_ms: processing_time,
error_message: Some(e.to_string()),
metadata: None,
}
}
};
Ok(processing_result)
}
/// 为图片添加水印
pub async fn add_watermark_to_image(
material_id: &str,
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
) -> Result<WatermarkProcessingResult> {
let timer = PERFORMANCE_MONITOR.start_operation("watermark_addition_image");
let start_time = Instant::now();
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
watermark_path = %watermark_path,
watermark_type = ?config.watermark_type,
"开始为图片添加水印"
);
// 验证输入文件存在
if !Path::new(input_path).exists() {
return Err(anyhow!("输入图片文件不存在: {}", input_path));
}
if !Path::new(watermark_path).exists() {
return Err(anyhow!("水印文件不存在: {}", watermark_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let result = match config.watermark_type {
WatermarkType::Image => {
Self::add_image_watermark_to_image(
input_path,
output_path,
watermark_path,
config,
).await
}
WatermarkType::Text => {
Self::add_text_watermark_to_image(
input_path,
output_path,
watermark_path,
config,
).await
}
WatermarkType::Vector => {
Self::add_vector_watermark_to_image(
input_path,
output_path,
watermark_path,
config,
).await
}
WatermarkType::Animated => {
// 动态水印不适用于静态图片
Err(anyhow!("动态水印不能应用于静态图片"))
}
};
let processing_time = start_time.elapsed().as_millis() as u64;
let processing_result = match result {
Ok(_) => {
info!(
material_id = %material_id,
processing_time_ms = processing_time,
"图片水印添加成功"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Add,
success: true,
output_path: Some(output_path.to_string()),
processing_time_ms: processing_time,
error_message: None,
metadata: None,
}
}
Err(e) => {
error!(
material_id = %material_id,
error = %e,
processing_time_ms = processing_time,
"图片水印添加失败"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Add,
success: false,
output_path: None,
processing_time_ms: processing_time,
error_message: Some(e.to_string()),
metadata: None,
}
}
};
Ok(processing_result)
}
/// 为视频添加图片水印
async fn add_image_watermark_to_video(
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为视频添加图片水印");
// 获取视频信息
let video_info = FFmpegWatermark::get_video_info(input_path)?;
let video_width = video_info.width.unwrap_or(1920) as f32;
let video_height = video_info.height.unwrap_or(1080) as f32;
// 计算水印位置
let position = Self::calculate_watermark_position(
&config.position,
video_width,
video_height,
config.scale,
);
// 构建overlay滤镜
let overlay_filter = Self::build_overlay_filter(
&position,
config.opacity,
config.scale,
config.rotation,
&config.blend_mode,
config.animation.as_ref(),
);
let quality_args = Self::get_quality_args(&config.quality_level);
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-i", watermark_path,
"-filter_complex", &overlay_filter,
];
args.extend_from_slice(&quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 为视频添加文字水印
async fn add_text_watermark_to_video(
input_path: &str,
output_path: &str,
text_content: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为视频添加文字水印");
// 获取视频信息
let video_info = FFmpegWatermark::get_video_info(input_path)?;
let video_width = video_info.width.unwrap_or(1920) as f32;
let video_height = video_info.height.unwrap_or(1080) as f32;
// 计算文字位置
let position = Self::calculate_watermark_position(
&config.position,
video_width,
video_height,
config.scale,
);
// 构建文字滤镜
let text_filter = Self::build_text_filter(
text_content,
&position,
config.opacity,
config.scale,
config.rotation,
);
let quality_args = Self::get_quality_args(&config.quality_level);
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", &text_filter,
];
args.extend_from_slice(&quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 为视频添加矢量水印
async fn add_vector_watermark_to_video(
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为视频添加矢量水印");
// SVG需要先转换为PNG
let temp_png = Self::convert_svg_to_png(watermark_path, config.scale).await?;
let result = Self::add_image_watermark_to_video(
input_path,
output_path,
&temp_png,
config,
).await;
// 清理临时文件
let _ = std::fs::remove_file(&temp_png);
result
}
/// 为视频添加动态水印
async fn add_animated_watermark_to_video(
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为视频添加动态水印");
// 获取视频信息
let video_info = FFmpegWatermark::get_video_info(input_path)?;
let video_width = video_info.width.unwrap_or(1920) as f32;
let video_height = video_info.height.unwrap_or(1080) as f32;
// 计算水印位置
let position = Self::calculate_watermark_position(
&config.position,
video_width,
video_height,
config.scale,
);
// 构建动态overlay滤镜
let overlay_filter = Self::build_animated_overlay_filter(
&position,
config.opacity,
config.scale,
config.rotation,
&config.blend_mode,
config.animation.as_ref(),
);
let quality_args = Self::get_quality_args(&config.quality_level);
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-i", watermark_path,
"-filter_complex", &overlay_filter,
];
args.extend_from_slice(&quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 为图片添加图片水印
async fn add_image_watermark_to_image(
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为图片添加图片水印");
// TODO: 获取图片尺寸
let image_width = 1920.0; // 临时值
let image_height = 1080.0; // 临时值
// 计算水印位置
let position = Self::calculate_watermark_position(
&config.position,
image_width,
image_height,
config.scale,
);
// 构建overlay滤镜
let overlay_filter = format!(
"overlay={}:{}:alpha={}",
position.0,
position.1,
config.opacity
);
let args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-i", watermark_path,
"-filter_complex", &overlay_filter,
"-y", output_path,
];
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 为图片添加文字水印
async fn add_text_watermark_to_image(
input_path: &str,
output_path: &str,
text_content: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为图片添加文字水印");
// TODO: 获取图片尺寸
let image_width = 1920.0; // 临时值
let image_height = 1080.0; // 临时值
// 计算文字位置
let position = Self::calculate_watermark_position(
&config.position,
image_width,
image_height,
config.scale,
);
// 构建文字滤镜
let text_filter = Self::build_text_filter(
text_content,
&position,
config.opacity,
config.scale,
config.rotation,
);
let args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", &text_filter,
"-y", output_path,
];
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 为图片添加矢量水印
async fn add_vector_watermark_to_image(
input_path: &str,
output_path: &str,
watermark_path: &str,
config: &WatermarkConfig,
) -> Result<()> {
debug!("为图片添加矢量水印");
// SVG需要先转换为PNG
let temp_png = Self::convert_svg_to_png(watermark_path, config.scale).await?;
let result = Self::add_image_watermark_to_image(
input_path,
output_path,
&temp_png,
config,
).await;
// 清理临时文件
let _ = std::fs::remove_file(&temp_png);
result
}
/// 计算水印位置
fn calculate_watermark_position(
position: &WatermarkPosition,
video_width: f32,
video_height: f32,
scale: f32,
) -> (f32, f32) {
let watermark_width = 200.0 * scale; // 假设水印宽度
let watermark_height = 100.0 * scale; // 假设水印高度
match position {
WatermarkPosition::TopLeft => (10.0, 10.0),
WatermarkPosition::TopCenter => ((video_width - watermark_width) / 2.0, 10.0),
WatermarkPosition::TopRight => (video_width - watermark_width - 10.0, 10.0),
WatermarkPosition::MiddleLeft => (10.0, (video_height - watermark_height) / 2.0),
WatermarkPosition::Center => (
(video_width - watermark_width) / 2.0,
(video_height - watermark_height) / 2.0,
),
WatermarkPosition::MiddleRight => (
video_width - watermark_width - 10.0,
(video_height - watermark_height) / 2.0,
),
WatermarkPosition::BottomLeft => (10.0, video_height - watermark_height - 10.0),
WatermarkPosition::BottomCenter => (
(video_width - watermark_width) / 2.0,
video_height - watermark_height - 10.0,
),
WatermarkPosition::BottomRight => (
video_width - watermark_width - 10.0,
video_height - watermark_height - 10.0,
),
WatermarkPosition::Custom { x, y } => (
x * video_width,
y * video_height,
),
WatermarkPosition::Dynamic(rule) => {
Self::calculate_dynamic_position(rule, video_width, video_height, scale)
}
}
}
/// 计算动态位置
fn calculate_dynamic_position(
rule: &DynamicPositionRule,
video_width: f32,
video_height: f32,
scale: f32,
) -> (f32, f32) {
// TODO: 实现动态位置计算逻辑
// 这里需要集成人脸检测、文字检测等功能
// 临时实现:根据角落偏好选择位置
if let Some(corner) = rule.corner_preference.first() {
let margin = rule.min_distance_from_edge as f32;
let watermark_width = 200.0 * scale;
let watermark_height = 100.0 * scale;
match corner {
Corner::TopLeft => (margin, margin),
Corner::TopRight => (video_width - watermark_width - margin, margin),
Corner::BottomLeft => (margin, video_height - watermark_height - margin),
Corner::BottomRight => (
video_width - watermark_width - margin,
video_height - watermark_height - margin,
),
}
} else {
// 默认右下角
(video_width - 210.0, video_height - 110.0)
}
}
/// 构建overlay滤镜
fn build_overlay_filter(
position: &(f32, f32),
opacity: f32,
scale: f32,
rotation: f32,
blend_mode: &BlendMode,
animation: Option<&WatermarkAnimation>,
) -> String {
let mut filter_parts = Vec::new();
// 缩放
if scale != 1.0 {
filter_parts.push(format!("[1:v]scale=iw*{}:ih*{}", scale, scale));
}
// 旋转
if rotation != 0.0 {
filter_parts.push(format!("rotate={}*PI/180", rotation));
}
// 透明度
if opacity != 1.0 {
filter_parts.push(format!("format=rgba,colorchannelmixer=aa={}", opacity));
}
let watermark_filter = if filter_parts.is_empty() {
"[1:v]".to_string()
} else {
format!("[1:v]{}", filter_parts.join(","))
};
// overlay位置
let overlay_expr = if let Some(anim) = animation {
Self::build_animated_position_expression(position, anim)
} else {
format!("{}:{}", position.0, position.1)
};
format!("{}[wm];[0:v][wm]overlay={}", watermark_filter, overlay_expr)
}
/// 构建动态overlay滤镜
fn build_animated_overlay_filter(
position: &(f32, f32),
opacity: f32,
scale: f32,
rotation: f32,
blend_mode: &BlendMode,
animation: Option<&WatermarkAnimation>,
) -> String {
// 与普通overlay类似但添加动画表达式
Self::build_overlay_filter(position, opacity, scale, rotation, blend_mode, animation)
}
/// 构建文字滤镜
fn build_text_filter(
text: &str,
position: &(f32, f32),
opacity: f32,
scale: f32,
rotation: f32,
) -> String {
let font_size = (24.0 * scale) as u32;
let alpha = (opacity * 255.0) as u32;
format!(
"drawtext=text='{}':x={}:y={}:fontsize={}:fontcolor=white@{}",
text.replace("'", "\\'"),
position.0,
position.1,
font_size,
alpha
)
}
/// 构建动画位置表达式
fn build_animated_position_expression(
base_position: &(f32, f32),
animation: &WatermarkAnimation,
) -> String {
match animation.animation_type {
AnimationType::FadeIn => {
format!("{}:{}:alpha='if(lt(t,{}),t/{},1)'",
base_position.0,
base_position.1,
animation.duration_ms as f32 / 1000.0,
animation.duration_ms as f32 / 1000.0
)
}
AnimationType::SlideIn => {
format!("'{}+100*max(0,1-t/{})':{}",
base_position.0,
animation.duration_ms as f32 / 1000.0,
base_position.1
)
}
_ => format!("{}:{}", base_position.0, base_position.1),
}
}
/// 转换SVG为PNG
async fn convert_svg_to_png(svg_path: &str, scale: f32) -> Result<String> {
// TODO: 实现SVG到PNG的转换
// 可以使用librsvg或其他SVG渲染库
// 临时实现直接返回SVG路径FFmpeg可能支持SVG
Ok(svg_path.to_string())
}
/// 获取质量参数
fn get_quality_args(quality_level: &QualityLevel) -> Vec<&'static str> {
match quality_level {
QualityLevel::Low => vec!["-c:v", "libx264", "-preset", "ultrafast", "-crf", "28"],
QualityLevel::Medium => vec!["-c:v", "libx264", "-preset", "fast", "-crf", "23"],
QualityLevel::High => vec!["-c:v", "libx264", "-preset", "slow", "-crf", "18"],
QualityLevel::Lossless => vec!["-c:v", "libx264", "-preset", "veryslow", "-crf", "0"],
}
}
}

View File

@@ -0,0 +1,474 @@
use anyhow::{Result, anyhow};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use tracing::{info, warn, debug};
use crate::data::models::watermark::{
WatermarkDetectionResult, WatermarkDetection, WatermarkDetectionConfig,
DetectionMethod, BoundingBox, WatermarkType
};
use crate::data::repositories::material_repository::MaterialRepository;
use crate::infrastructure::ffmpeg_watermark::FFmpegWatermark;
use crate::infrastructure::monitoring::PERFORMANCE_MONITOR;
/// 水印检测服务
/// 遵循 Tauri 开发规范的业务逻辑层设计
pub struct WatermarkDetectionService;
impl WatermarkDetectionService {
/// 检测视频中的水印
pub async fn detect_watermarks_in_video(
material_id: &str,
video_path: &str,
config: &WatermarkDetectionConfig,
repository: Arc<MaterialRepository>,
) -> Result<WatermarkDetectionResult> {
let timer = PERFORMANCE_MONITOR.start_operation("watermark_detection");
let start_time = Instant::now();
info!(
material_id = %material_id,
video_path = %video_path,
"开始检测视频水印"
);
// 验证文件存在
if !Path::new(video_path).exists() {
return Err(anyhow!("视频文件不存在: {}", video_path));
}
// 获取视频信息
let video_info = FFmpegWatermark::get_video_info(video_path)?;
let frame_count = video_info.frame_count.unwrap_or(0);
let duration = video_info.duration;
debug!(
material_id = %material_id,
frame_count = frame_count,
duration = duration,
"视频信息获取完成"
);
let mut all_detections = Vec::new();
let mut total_confidence = 0.0;
let mut detection_count = 0;
// 根据配置的采样率提取关键帧进行检测
let sample_interval = config.frame_sample_rate.max(1);
let sample_frames = Self::calculate_sample_frames(frame_count, sample_interval, duration);
for (frame_index, timestamp) in sample_frames.iter().enumerate() {
debug!(
material_id = %material_id,
frame_index = frame_index,
timestamp = timestamp,
"开始检测帧"
);
// 提取帧图像
let frame_path = Self::extract_frame(video_path, *timestamp, material_id, frame_index)?;
// 对每个检测方法进行检测
for method in &config.methods {
match Self::detect_watermarks_in_frame(
&frame_path,
method,
config,
).await {
Ok(detections) => {
for detection in detections {
all_detections.push(detection.clone());
total_confidence += detection.confidence;
detection_count += 1;
}
}
Err(e) => {
warn!(
material_id = %material_id,
method = ?method,
frame_index = frame_index,
error = %e,
"帧检测失败"
);
}
}
}
// 清理临时帧文件
let _ = std::fs::remove_file(&frame_path);
}
// 合并相似的检测结果
let merged_detections = Self::merge_similar_detections(all_detections, 0.7);
// 计算平均置信度
let average_confidence = if detection_count > 0 {
total_confidence / detection_count as f64
} else {
0.0
};
let processing_time = start_time.elapsed().as_millis() as u64;
let result = WatermarkDetectionResult {
id: uuid::Uuid::new_v4().to_string(),
material_id: material_id.to_string(),
detection_method: if config.methods.len() == 1 {
config.methods[0].clone()
} else {
DetectionMethod::Combined
},
detections: merged_detections,
confidence_score: average_confidence,
processing_time_ms: processing_time,
created_at: chrono::Utc::now(),
};
info!(
material_id = %material_id,
detection_count = result.detections.len(),
confidence = result.confidence_score,
processing_time_ms = processing_time,
"水印检测完成"
);
Ok(result)
}
/// 检测图片中的水印
pub async fn detect_watermarks_in_image(
material_id: &str,
image_path: &str,
config: &WatermarkDetectionConfig,
) -> Result<WatermarkDetectionResult> {
let timer = PERFORMANCE_MONITOR.start_operation("watermark_detection_image");
let start_time = Instant::now();
info!(
material_id = %material_id,
image_path = %image_path,
"开始检测图片水印"
);
// 验证文件存在
if !Path::new(image_path).exists() {
return Err(anyhow!("图片文件不存在: {}", image_path));
}
let mut all_detections = Vec::new();
// 对每个检测方法进行检测
for method in &config.methods {
match Self::detect_watermarks_in_frame(
image_path,
method,
config,
).await {
Ok(detections) => {
all_detections.extend(detections);
}
Err(e) => {
warn!(
material_id = %material_id,
method = ?method,
error = %e,
"图片检测失败"
);
}
}
}
// 合并相似的检测结果
let merged_detections = Self::merge_similar_detections(all_detections, 0.7);
// 计算平均置信度
let average_confidence = if !merged_detections.is_empty() {
merged_detections.iter().map(|d| d.confidence).sum::<f64>() / merged_detections.len() as f64
} else {
0.0
};
let processing_time = start_time.elapsed().as_millis() as u64;
let result = WatermarkDetectionResult {
id: uuid::Uuid::new_v4().to_string(),
material_id: material_id.to_string(),
detection_method: if config.methods.len() == 1 {
config.methods[0].clone()
} else {
DetectionMethod::Combined
},
detections: merged_detections,
confidence_score: average_confidence,
processing_time_ms: processing_time,
created_at: chrono::Utc::now(),
};
info!(
material_id = %material_id,
detection_count = result.detections.len(),
confidence = result.confidence_score,
processing_time_ms = processing_time,
"图片水印检测完成"
);
Ok(result)
}
/// 计算采样帧
fn calculate_sample_frames(frame_count: u32, sample_interval: u32, duration: f64) -> Vec<f64> {
let mut sample_frames = Vec::new();
if frame_count == 0 || duration <= 0.0 {
return sample_frames;
}
let fps = frame_count as f64 / duration;
let total_samples = (frame_count / sample_interval).max(1);
for i in 0..total_samples {
let frame_index = i * sample_interval;
let timestamp = frame_index as f64 / fps;
if timestamp < duration {
sample_frames.push(timestamp);
}
}
// 确保至少有一帧
if sample_frames.is_empty() {
sample_frames.push(duration / 2.0); // 取中间帧
}
sample_frames
}
/// 提取视频帧
fn extract_frame(
video_path: &str,
timestamp: f64,
material_id: &str,
frame_index: usize,
) -> Result<String> {
let temp_dir = std::env::temp_dir();
let frame_filename = format!("watermark_detection_{}_{}.jpg", material_id, frame_index);
let frame_path = temp_dir.join(frame_filename);
let frame_path_str = frame_path.to_string_lossy().to_string();
// 使用FFmpeg提取帧
FFmpegWatermark::extract_frame_at_timestamp(
video_path,
timestamp,
&frame_path_str,
1920, // 使用高分辨率以提高检测精度
1080,
)?;
Ok(frame_path_str)
}
/// 在单帧中检测水印
async fn detect_watermarks_in_frame(
image_path: &str,
method: &DetectionMethod,
config: &WatermarkDetectionConfig,
) -> Result<Vec<WatermarkDetection>> {
match method {
DetectionMethod::TemplateMatching => {
Self::template_matching_detection(image_path, config).await
}
DetectionMethod::EdgeDetection => {
Self::edge_detection(image_path, config).await
}
DetectionMethod::FrequencyAnalysis => {
Self::frequency_analysis_detection(image_path, config).await
}
DetectionMethod::TransparencyDetection => {
Self::transparency_detection(image_path, config).await
}
DetectionMethod::Combined => {
// 组合检测:运行所有方法并合并结果
let mut all_detections = Vec::new();
if let Ok(detections) = Self::template_matching_detection(image_path, config).await {
all_detections.extend(detections);
}
if let Ok(detections) = Self::edge_detection(image_path, config).await {
all_detections.extend(detections);
}
Ok(all_detections)
}
}
}
/// 模板匹配检测
async fn template_matching_detection(
image_path: &str,
config: &WatermarkDetectionConfig,
) -> Result<Vec<WatermarkDetection>> {
// TODO: 实现OpenCV模板匹配算法
// 这里先返回模拟结果后续需要集成OpenCV
debug!("执行模板匹配检测: {}", image_path);
// 模拟检测结果
let detections = vec![
WatermarkDetection {
region: BoundingBox {
x: 100,
y: 100,
width: 200,
height: 50,
},
confidence: 0.85,
watermark_type: Some(WatermarkType::Image),
template_id: None,
description: Some("模板匹配检测到的水印".to_string()),
}
];
Ok(detections)
}
/// 边缘检测
async fn edge_detection(
image_path: &str,
config: &WatermarkDetectionConfig,
) -> Result<Vec<WatermarkDetection>> {
// TODO: 实现边缘检测算法
debug!("执行边缘检测: {}", image_path);
// 模拟检测结果
Ok(vec![])
}
/// 频域分析检测
async fn frequency_analysis_detection(
image_path: &str,
config: &WatermarkDetectionConfig,
) -> Result<Vec<WatermarkDetection>> {
// TODO: 实现频域分析算法
debug!("执行频域分析检测: {}", image_path);
// 模拟检测结果
Ok(vec![])
}
/// 透明度检测
async fn transparency_detection(
image_path: &str,
config: &WatermarkDetectionConfig,
) -> Result<Vec<WatermarkDetection>> {
// TODO: 实现透明度检测算法
debug!("执行透明度检测: {}", image_path);
// 模拟检测结果
Ok(vec![])
}
/// 合并相似的检测结果
fn merge_similar_detections(
detections: Vec<WatermarkDetection>,
similarity_threshold: f64,
) -> Vec<WatermarkDetection> {
if detections.is_empty() {
return detections;
}
let mut merged = Vec::new();
let mut used = vec![false; detections.len()];
for i in 0..detections.len() {
if used[i] {
continue;
}
let mut group = vec![detections[i].clone()];
used[i] = true;
// 查找相似的检测结果
for j in (i + 1)..detections.len() {
if used[j] {
continue;
}
let similarity = Self::calculate_region_similarity(
&detections[i].region,
&detections[j].region,
);
if similarity >= similarity_threshold {
group.push(detections[j].clone());
used[j] = true;
}
}
// 合并组内的检测结果
if group.len() == 1 {
merged.push(group[0].clone());
} else {
merged.push(Self::merge_detection_group(group));
}
}
merged
}
/// 计算两个区域的相似度
fn calculate_region_similarity(region1: &BoundingBox, region2: &BoundingBox) -> f64 {
// 计算重叠区域
let x1 = region1.x.max(region2.x);
let y1 = region1.y.max(region2.y);
let x2 = (region1.x + region1.width).min(region2.x + region2.width);
let y2 = (region1.y + region1.height).min(region2.y + region2.height);
if x2 <= x1 || y2 <= y1 {
return 0.0; // 没有重叠
}
let overlap_area = (x2 - x1) * (y2 - y1);
let area1 = region1.width * region1.height;
let area2 = region2.width * region2.height;
let union_area = area1 + area2 - overlap_area;
if union_area == 0 {
return 0.0;
}
overlap_area as f64 / union_area as f64
}
/// 合并检测组
fn merge_detection_group(group: Vec<WatermarkDetection>) -> WatermarkDetection {
if group.is_empty() {
panic!("检测组不能为空");
}
if group.len() == 1 {
return group[0].clone();
}
// 计算边界框的并集
let min_x = group.iter().map(|d| d.region.x).min().unwrap();
let min_y = group.iter().map(|d| d.region.y).min().unwrap();
let max_x = group.iter().map(|d| d.region.x + d.region.width).max().unwrap();
let max_y = group.iter().map(|d| d.region.y + d.region.height).max().unwrap();
// 计算平均置信度
let avg_confidence = group.iter().map(|d| d.confidence).sum::<f64>() / group.len() as f64;
WatermarkDetection {
region: BoundingBox {
x: min_x,
y: min_y,
width: max_x - min_x,
height: max_y - min_y,
},
confidence: avg_confidence,
watermark_type: group[0].watermark_type.clone(),
template_id: group[0].template_id.clone(),
description: Some(format!("合并了{}个检测结果", group.len())),
}
}
}

View File

@@ -0,0 +1,496 @@
use anyhow::{Result, anyhow};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use tracing::{info, error, debug};
use crate::data::models::watermark::{
WatermarkRemovalConfig, RemovalMethod, QualityLevel, BoundingBox,
WatermarkProcessingResult, WatermarkOperation
};
use crate::data::repositories::material_repository::MaterialRepository;
use crate::infrastructure::ffmpeg_watermark::FFmpegWatermark;
use crate::infrastructure::monitoring::PERFORMANCE_MONITOR;
/// 水印移除服务
/// 遵循 Tauri 开发规范的业务逻辑层设计
pub struct WatermarkRemovalService;
impl WatermarkRemovalService {
/// 移除视频中的水印
pub async fn remove_watermarks_from_video(
material_id: &str,
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
repository: Arc<MaterialRepository>,
) -> Result<WatermarkProcessingResult> {
let timer = PERFORMANCE_MONITOR.start_operation("watermark_removal_video");
let start_time = Instant::now();
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
method = ?config.method,
"开始移除视频水印"
);
// 验证输入文件存在
if !Path::new(input_path).exists() {
return Err(anyhow!("输入视频文件不存在: {}", input_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let result = match config.method {
RemovalMethod::Blurring => {
Self::remove_by_blurring(input_path, output_path, config).await
}
RemovalMethod::Cropping => {
Self::remove_by_cropping(input_path, output_path, config).await
}
RemovalMethod::Masking => {
Self::remove_by_masking(input_path, output_path, config).await
}
RemovalMethod::Inpainting => {
Self::remove_by_inpainting(input_path, output_path, config).await
}
RemovalMethod::ContentAware => {
Self::remove_by_content_aware(input_path, output_path, config).await
}
RemovalMethod::Clone => {
Self::remove_by_clone(input_path, output_path, config).await
}
};
let processing_time = start_time.elapsed().as_millis() as u64;
let processing_result = match result {
Ok(_) => {
info!(
material_id = %material_id,
processing_time_ms = processing_time,
"视频水印移除成功"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Remove,
success: true,
output_path: Some(output_path.to_string()),
processing_time_ms: processing_time,
error_message: None,
metadata: None,
}
}
Err(e) => {
error!(
material_id = %material_id,
error = %e,
processing_time_ms = processing_time,
"视频水印移除失败"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Remove,
success: false,
output_path: None,
processing_time_ms: processing_time,
error_message: Some(e.to_string()),
metadata: None,
}
}
};
Ok(processing_result)
}
/// 移除图片中的水印
pub async fn remove_watermarks_from_image(
material_id: &str,
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<WatermarkProcessingResult> {
let timer = PERFORMANCE_MONITOR.start_operation("watermark_removal_image");
let start_time = Instant::now();
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
method = ?config.method,
"开始移除图片水印"
);
// 验证输入文件存在
if !Path::new(input_path).exists() {
return Err(anyhow!("输入图片文件不存在: {}", input_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let result = match config.method {
RemovalMethod::Blurring => {
Self::remove_image_by_blurring(input_path, output_path, config).await
}
RemovalMethod::Cropping => {
Self::remove_image_by_cropping(input_path, output_path, config).await
}
RemovalMethod::Masking => {
Self::remove_image_by_masking(input_path, output_path, config).await
}
RemovalMethod::Inpainting => {
Self::remove_image_by_inpainting(input_path, output_path, config).await
}
RemovalMethod::ContentAware => {
Self::remove_image_by_content_aware(input_path, output_path, config).await
}
RemovalMethod::Clone => {
Self::remove_image_by_clone(input_path, output_path, config).await
}
};
let processing_time = start_time.elapsed().as_millis() as u64;
let processing_result = match result {
Ok(_) => {
info!(
material_id = %material_id,
processing_time_ms = processing_time,
"图片水印移除成功"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Remove,
success: true,
output_path: Some(output_path.to_string()),
processing_time_ms: processing_time,
error_message: None,
metadata: None,
}
}
Err(e) => {
error!(
material_id = %material_id,
error = %e,
processing_time_ms = processing_time,
"图片水印移除失败"
);
WatermarkProcessingResult {
material_id: material_id.to_string(),
operation: WatermarkOperation::Remove,
success: false,
output_path: None,
processing_time_ms: processing_time,
error_message: Some(e.to_string()),
metadata: None,
}
}
};
Ok(processing_result)
}
/// 通过模糊处理移除水印
async fn remove_by_blurring(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("使用模糊处理移除水印");
let blur_radius = config.blur_radius.unwrap_or(10.0);
let quality_args = Self::get_quality_args(&config.quality_level);
// 构建FFmpeg命令
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
];
// 添加模糊滤镜
let filter = if let Some(regions) = &config.target_regions {
// 对指定区域进行模糊处理
Self::build_region_blur_filter(regions, blur_radius)
} else {
// 全局模糊
format!("boxblur={}:1", blur_radius)
};
args.extend_from_slice(&["-vf", &filter]);
// 添加质量参数
args.extend_from_slice(&quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 通过裁剪移除水印
async fn remove_by_cropping(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("使用裁剪移除水印");
// 获取视频信息
let video_info = FFmpegWatermark::get_video_info(input_path)?;
let width = video_info.width.unwrap_or(1920);
let height = video_info.height.unwrap_or(1080);
let margin = config.crop_margin.unwrap_or(50);
let quality_args = Self::get_quality_args(&config.quality_level);
// 计算裁剪区域(移除边缘区域)
let crop_width = width.saturating_sub(margin * 2);
let crop_height = height.saturating_sub(margin * 2);
let crop_x = margin;
let crop_y = margin;
let crop_filter = format!("crop={}:{}:{}:{}", crop_width, crop_height, crop_x, crop_y);
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", &crop_filter,
];
args.extend_from_slice(&quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 通过遮罩覆盖移除水印
async fn remove_by_masking(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("使用遮罩覆盖移除水印");
// TODO: 实现遮罩覆盖逻辑
// 这里需要根据检测到的水印区域生成遮罩
// 临时实现:使用简单的颜色填充
let quality_args = Self::get_quality_args(&config.quality_level);
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
];
let filter = if let Some(regions) = &config.target_regions {
Some(Self::build_region_mask_filter(regions))
} else {
None
};
if let Some(ref filter_str) = filter {
args.extend_from_slice(&["-vf", filter_str]);
}
args.extend_from_slice(&quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 通过AI修复移除水印
async fn remove_by_inpainting(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("使用AI修复移除水印");
// TODO: 集成AI修复模型
// 目前使用简单的模糊处理作为替代
Self::remove_by_blurring(input_path, output_path, config).await
}
/// 通过内容感知填充移除水印
async fn remove_by_content_aware(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("使用内容感知填充移除水印");
// TODO: 实现内容感知填充算法
// 目前使用模糊处理作为替代
Self::remove_by_blurring(input_path, output_path, config).await
}
/// 通过克隆修复移除水印
async fn remove_by_clone(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("使用克隆修复移除水印");
// TODO: 实现克隆修复算法
// 目前使用模糊处理作为替代
Self::remove_by_blurring(input_path, output_path, config).await
}
/// 图片模糊处理
async fn remove_image_by_blurring(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("对图片使用模糊处理移除水印");
let blur_radius = config.blur_radius.unwrap_or(10.0);
let blur_filter = format!("boxblur={}:1", blur_radius);
let args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", &blur_filter,
"-y", output_path,
];
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 图片裁剪处理
async fn remove_image_by_cropping(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("对图片使用裁剪移除水印");
// TODO: 获取图片尺寸并计算裁剪区域
let margin = config.crop_margin.unwrap_or(50);
let crop_filter = format!("crop=iw-{}:ih-{}:{}:{}", margin * 2, margin * 2, margin, margin);
let args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", &crop_filter,
"-y", output_path,
];
FFmpegWatermark::execute_command(&args)?;
Ok(())
}
/// 图片遮罩处理
async fn remove_image_by_masking(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("对图片使用遮罩移除水印");
// TODO: 实现图片遮罩处理
Self::remove_image_by_blurring(input_path, output_path, config).await
}
/// 图片AI修复
async fn remove_image_by_inpainting(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("对图片使用AI修复移除水印");
// TODO: 实现图片AI修复
Self::remove_image_by_blurring(input_path, output_path, config).await
}
/// 图片内容感知填充
async fn remove_image_by_content_aware(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("对图片使用内容感知填充移除水印");
// TODO: 实现图片内容感知填充
Self::remove_image_by_blurring(input_path, output_path, config).await
}
/// 图片克隆修复
async fn remove_image_by_clone(
input_path: &str,
output_path: &str,
config: &WatermarkRemovalConfig,
) -> Result<()> {
debug!("对图片使用克隆修复移除水印");
// TODO: 实现图片克隆修复
Self::remove_image_by_blurring(input_path, output_path, config).await
}
/// 构建区域模糊滤镜
fn build_region_blur_filter(regions: &[BoundingBox], blur_radius: f32) -> String {
let mut filters = Vec::new();
for (i, region) in regions.iter().enumerate() {
let filter = format!(
"boxblur={}:1:enable='between(t,0,999999)*between(x,{},{})*between(y,{},{})'",
blur_radius,
region.x,
region.x + region.width,
region.y,
region.y + region.height
);
filters.push(filter);
}
filters.join(",")
}
/// 构建区域遮罩滤镜
fn build_region_mask_filter(regions: &[BoundingBox]) -> String {
let mut filters = Vec::new();
for region in regions {
let filter = format!(
"drawbox=x={}:y={}:w={}:h={}:color=black:t=fill",
region.x,
region.y,
region.width,
region.height
);
filters.push(filter);
}
filters.join(",")
}
/// 获取质量参数
fn get_quality_args(quality_level: &QualityLevel) -> Vec<&'static str> {
match quality_level {
QualityLevel::Low => vec!["-c:v", "libx264", "-preset", "ultrafast", "-crf", "28"],
QualityLevel::Medium => vec!["-c:v", "libx264", "-preset", "fast", "-crf", "23"],
QualityLevel::High => vec!["-c:v", "libx264", "-preset", "slow", "-crf", "18"],
QualityLevel::Lossless => vec!["-c:v", "libx264", "-preset", "veryslow", "-crf", "0"],
}
}
}

View File

@@ -0,0 +1,426 @@
use anyhow::{Result, anyhow};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::fs;
use std::time::Instant;
use tracing::{info, debug};
use crate::data::models::watermark::{
WatermarkTemplate, WatermarkCategory, WatermarkType
};
use crate::data::repositories::watermark_template_repository::WatermarkTemplateRepository;
use crate::infrastructure::ffmpeg_watermark::FFmpegWatermark;
use crate::infrastructure::monitoring::PERFORMANCE_MONITOR;
/// 水印模板管理服务
/// 遵循 Tauri 开发规范的业务逻辑层设计
pub struct WatermarkTemplateService;
impl WatermarkTemplateService {
/// 上传并创建水印模板
pub async fn upload_template(
repository: Arc<WatermarkTemplateRepository>,
name: String,
source_file_path: String,
category: WatermarkCategory,
watermark_type: WatermarkType,
description: Option<String>,
tags: Vec<String>,
) -> Result<WatermarkTemplate> {
let timer = PERFORMANCE_MONITOR.start_operation("upload_watermark_template");
let start_time = Instant::now();
info!(
name = %name,
source_file_path = %source_file_path,
category = ?category,
watermark_type = ?watermark_type,
"开始上传水印模板"
);
// 验证源文件存在
if !Path::new(&source_file_path).exists() {
return Err(anyhow!("源文件不存在: {}", source_file_path));
}
// 检查模板名称是否已存在
if repository.name_exists(&name, None)? {
return Err(anyhow!("模板名称已存在: {}", name));
}
// 验证文件格式
Self::validate_file_format(&source_file_path, &watermark_type)?;
// 创建模板目录
let template_id = uuid::Uuid::new_v4().to_string();
let template_dir = Self::get_template_directory(&template_id);
fs::create_dir_all(&template_dir)?;
// 复制文件到模板目录
let file_extension = Path::new(&source_file_path)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("unknown");
let template_filename = format!("template.{}", file_extension);
let template_file_path = template_dir.join(&template_filename);
fs::copy(&source_file_path, &template_file_path)?;
// 获取文件信息
let file_metadata = fs::metadata(&template_file_path)?;
let file_size = file_metadata.len();
// 获取图片/视频尺寸
let (width, height) = Self::get_media_dimensions(&template_file_path.to_string_lossy().to_string())?;
// 生成缩略图
let thumbnail_path = Self::generate_thumbnail(
&template_file_path.to_string_lossy().to_string(),
&template_dir,
&watermark_type,
).await?;
// 创建模板对象
let template = WatermarkTemplate {
id: template_id,
name,
file_path: template_file_path.to_string_lossy().to_string(),
thumbnail_path: Some(thumbnail_path),
category,
watermark_type,
file_size,
width,
height,
description,
tags,
is_active: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
// 保存到数据库
repository.create(&template)?;
let processing_time = start_time.elapsed().as_millis() as u64;
info!(
template_id = %template.id,
processing_time_ms = processing_time,
"水印模板上传成功"
);
Ok(template)
}
/// 更新水印模板
pub async fn update_template(
repository: Arc<WatermarkTemplateRepository>,
template_id: String,
name: Option<String>,
description: Option<String>,
tags: Option<Vec<String>>,
category: Option<WatermarkCategory>,
) -> Result<WatermarkTemplate> {
info!(template_id = %template_id, "开始更新水印模板");
// 获取现有模板
let mut template = repository.get_by_id(&template_id)?
.ok_or_else(|| anyhow!("模板不存在: {}", template_id))?;
// 检查名称是否冲突
if let Some(ref new_name) = name {
if repository.name_exists(new_name, Some(&template_id))? {
return Err(anyhow!("模板名称已存在: {}", new_name));
}
template.name = new_name.clone();
}
// 更新其他字段
if let Some(new_description) = description {
template.description = Some(new_description);
}
if let Some(new_tags) = tags {
template.tags = new_tags;
}
if let Some(new_category) = category {
template.category = new_category;
}
template.updated_at = chrono::Utc::now();
// 保存更新
repository.update(&template)?;
info!(template_id = %template_id, "水印模板更新成功");
Ok(template)
}
/// 删除水印模板
pub async fn delete_template(
repository: Arc<WatermarkTemplateRepository>,
template_id: String,
hard_delete: bool,
) -> Result<()> {
info!(template_id = %template_id, hard_delete = hard_delete, "开始删除水印模板");
// 获取模板信息
let template = repository.get_by_id(&template_id)?
.ok_or_else(|| anyhow!("模板不存在: {}", template_id))?;
if hard_delete {
// 硬删除:删除文件和数据库记录
let template_dir = Self::get_template_directory(&template_id);
if template_dir.exists() {
fs::remove_dir_all(&template_dir)?;
}
repository.delete(&template_id)?;
} else {
// 软删除:只标记为非活跃
repository.soft_delete(&template_id)?;
}
info!(template_id = %template_id, "水印模板删除成功");
Ok(())
}
/// 获取模板列表
pub async fn get_templates(
repository: Arc<WatermarkTemplateRepository>,
category: Option<WatermarkCategory>,
watermark_type: Option<WatermarkType>,
search_query: Option<String>,
) -> Result<Vec<WatermarkTemplate>> {
debug!("获取水印模板列表");
let templates = if let Some(query) = search_query {
repository.search(&query)?
} else if let Some(cat) = category {
repository.get_by_category(&cat)?
} else if let Some(wtype) = watermark_type {
repository.get_by_type(&wtype)?
} else {
repository.get_all()?
};
Ok(templates)
}
/// 获取模板统计信息
pub async fn get_template_stats(
repository: Arc<WatermarkTemplateRepository>,
) -> Result<serde_json::Value> {
debug!("获取水印模板统计信息");
repository.get_stats()
}
/// 验证文件格式
fn validate_file_format(file_path: &str, watermark_type: &WatermarkType) -> Result<()> {
let path = Path::new(file_path);
let extension = path.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| anyhow!("无法获取文件扩展名"))?
.to_lowercase();
let valid_extensions = match watermark_type {
WatermarkType::Image => vec!["png", "jpg", "jpeg", "bmp", "tiff", "webp"],
WatermarkType::Vector => vec!["svg"],
WatermarkType::Animated => vec!["gif", "webp", "apng"],
WatermarkType::Text => vec!["txt", "json"], // 文字水印配置文件
};
if !valid_extensions.contains(&extension.as_str()) {
return Err(anyhow!(
"不支持的文件格式: {},支持的格式: {:?}",
extension,
valid_extensions
));
}
Ok(())
}
/// 获取媒体文件尺寸
fn get_media_dimensions(file_path: &str) -> Result<(Option<u32>, Option<u32>)> {
let path = Path::new(file_path);
let extension = path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
match extension.as_str() {
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "webp" | "gif" => {
// 对于图片文件可以使用image库获取尺寸
// 这里先返回None后续可以集成image库
Ok((None, None))
}
"svg" => {
// SVG文件需要解析XML获取viewBox或width/height属性
Ok((None, None))
}
_ => Ok((None, None)),
}
}
/// 生成缩略图
async fn generate_thumbnail(
file_path: &str,
output_dir: &Path,
watermark_type: &WatermarkType,
) -> Result<String> {
let thumbnail_path = output_dir.join("thumbnail.jpg");
let thumbnail_path_str = thumbnail_path.to_string_lossy().to_string();
match watermark_type {
WatermarkType::Image | WatermarkType::Animated => {
// 对于图片和动画使用FFmpeg生成缩略图
FFmpegWatermark::extract_frame_at_timestamp(
file_path,
0.0, // 第一帧
&thumbnail_path_str,
200, // 缩略图宽度
150, // 缩略图高度
)?;
}
WatermarkType::Vector => {
// 对于SVG需要转换为PNG再生成缩略图
// 这里先复制原文件作为缩略图
fs::copy(file_path, &thumbnail_path)?;
}
WatermarkType::Text => {
// 对于文字水印,生成一个文字预览图
Self::generate_text_thumbnail(file_path, &thumbnail_path_str)?;
}
}
Ok(thumbnail_path_str)
}
/// 生成文字水印缩略图
fn generate_text_thumbnail(text_file_path: &str, output_path: &str) -> Result<()> {
// TODO: 实现文字水印缩略图生成
// 可以使用图像库生成包含文字的预览图
// 临时实现:创建一个空的缩略图文件
fs::write(output_path, b"")?;
Ok(())
}
/// 获取模板目录路径
fn get_template_directory(template_id: &str) -> PathBuf {
PathBuf::from("watermarks")
.join("templates")
.join(template_id)
}
/// 导出模板
pub async fn export_template(
repository: Arc<WatermarkTemplateRepository>,
template_id: String,
export_path: String,
) -> Result<()> {
info!(template_id = %template_id, export_path = %export_path, "开始导出水印模板");
// 获取模板信息
let template = repository.get_by_id(&template_id)?
.ok_or_else(|| anyhow!("模板不存在: {}", template_id))?;
// 创建导出目录
let export_dir = Path::new(&export_path);
fs::create_dir_all(export_dir)?;
// 复制模板文件
let template_file = Path::new(&template.file_path);
if template_file.exists() {
let export_file = export_dir.join(template_file.file_name().unwrap());
fs::copy(template_file, export_file)?;
}
// 复制缩略图
if let Some(ref thumbnail_path) = template.thumbnail_path {
let thumbnail_file = Path::new(thumbnail_path);
if thumbnail_file.exists() {
let export_thumbnail = export_dir.join("thumbnail.jpg");
fs::copy(thumbnail_file, export_thumbnail)?;
}
}
// 创建模板信息文件
let template_info = serde_json::json!({
"id": template.id,
"name": template.name,
"category": template.category,
"watermark_type": template.watermark_type,
"description": template.description,
"tags": template.tags,
"created_at": template.created_at,
"updated_at": template.updated_at
});
let info_file = export_dir.join("template_info.json");
fs::write(info_file, serde_json::to_string_pretty(&template_info)?)?;
info!(template_id = %template_id, "水印模板导出成功");
Ok(())
}
/// 导入模板
pub async fn import_template(
repository: Arc<WatermarkTemplateRepository>,
import_path: String,
) -> Result<WatermarkTemplate> {
info!(import_path = %import_path, "开始导入水印模板");
let import_dir = Path::new(&import_path);
if !import_dir.is_dir() {
return Err(anyhow!("导入路径必须是目录: {}", import_path));
}
// 读取模板信息文件
let info_file = import_dir.join("template_info.json");
if !info_file.exists() {
return Err(anyhow!("缺少模板信息文件: template_info.json"));
}
let info_content = fs::read_to_string(info_file)?;
let template_info: serde_json::Value = serde_json::from_str(&info_content)?;
// 提取模板信息
let name = template_info["name"].as_str()
.ok_or_else(|| anyhow!("缺少模板名称"))?;
let category: WatermarkCategory = serde_json::from_value(template_info["category"].clone())?;
let watermark_type: WatermarkType = serde_json::from_value(template_info["watermark_type"].clone())?;
let description = template_info["description"].as_str().map(|s| s.to_string());
let tags: Vec<String> = serde_json::from_value(template_info["tags"].clone())
.unwrap_or_default();
// 查找模板文件
let template_files: Vec<_> = fs::read_dir(import_dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
path.is_file() &&
path.file_name().unwrap().to_str().unwrap() != "template_info.json" &&
path.file_name().unwrap().to_str().unwrap() != "thumbnail.jpg"
})
.collect();
if template_files.is_empty() {
return Err(anyhow!("未找到模板文件"));
}
let template_file = &template_files[0];
let template_file_path = template_file.path().to_string_lossy().to_string();
// 使用上传功能创建模板
Self::upload_template(
repository,
name.to_string(),
template_file_path,
category,
watermark_type,
description,
tags,
).await
}
}

View File

@@ -15,3 +15,4 @@ pub mod conversation;
pub mod outfit_search;
pub mod gemini_analysis;
pub mod custom_tag;
pub mod watermark;

View File

@@ -0,0 +1,341 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// 水印模板实体模型
/// 遵循 Tauri 开发规范的数据模型设计原则
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkTemplate {
pub id: String,
pub name: String,
pub file_path: String,
pub thumbnail_path: Option<String>,
pub category: WatermarkCategory,
pub watermark_type: WatermarkType,
pub file_size: u64,
pub width: Option<u32>,
pub height: Option<u32>,
pub description: Option<String>,
pub tags: Vec<String>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 水印类型枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WatermarkType {
Image, // 图片水印 (PNG, JPG)
Vector, // 矢量水印 (SVG)
Text, // 文字水印
Animated, // 动态水印 (GIF, 动画)
}
/// 水印分类枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WatermarkCategory {
Logo, // 品牌标识
Copyright, // 版权标识
Signature, // 签名
Decoration, // 装饰性
Custom, // 自定义
}
/// 水印检测结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkDetectionResult {
pub id: String,
pub material_id: String,
pub detection_method: DetectionMethod,
pub detections: Vec<WatermarkDetection>,
pub confidence_score: f64,
pub processing_time_ms: u64,
pub created_at: DateTime<Utc>,
}
/// 单个水印检测信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkDetection {
pub region: BoundingBox,
pub confidence: f64,
pub watermark_type: Option<WatermarkType>,
pub template_id: Option<String>, // 匹配的模板ID如果使用模板匹配
pub description: Option<String>,
}
/// 边界框定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoundingBox {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
/// 检测方法枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DetectionMethod {
TemplateMatching, // 模板匹配
EdgeDetection, // 边缘检测
FrequencyAnalysis, // 频域分析
TransparencyDetection, // 透明度检测
Combined, // 组合检测
}
/// 水印配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkConfig {
pub watermark_type: WatermarkType,
pub position: WatermarkPosition,
pub opacity: f32, // 透明度 0.0-1.0
pub scale: f32, // 缩放比例
pub rotation: f32, // 旋转角度(度)
pub animation: Option<WatermarkAnimation>,
pub blend_mode: BlendMode,
pub quality_level: QualityLevel,
}
/// 水印位置配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WatermarkPosition {
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
Center,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
Custom { x: f32, y: f32 }, // 相对位置 0.0-1.0
Dynamic(DynamicPositionRule), // 动态位置
}
/// 动态位置规则
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DynamicPositionRule {
pub avoid_faces: bool, // 避开人脸
pub avoid_text: bool, // 避开文字
pub follow_motion: bool, // 跟随运动
pub corner_preference: Vec<Corner>, // 角落偏好
pub min_distance_from_edge: u32, // 距离边缘最小像素
}
/// 角落枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Corner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
/// 水印动画配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkAnimation {
pub animation_type: AnimationType,
pub duration_ms: u32,
pub loop_count: Option<u32>, // None表示无限循环
pub easing: EasingFunction,
}
/// 动画类型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AnimationType {
FadeIn, // 淡入
FadeOut, // 淡出
SlideIn, // 滑入
SlideOut, // 滑出
Rotate, // 旋转
Scale, // 缩放
Pulse, // 脉冲
Custom(String), // 自定义动画
}
/// 缓动函数
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EasingFunction {
Linear,
EaseIn,
EaseOut,
EaseInOut,
Bounce,
Elastic,
}
/// 混合模式
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BlendMode {
Normal,
Multiply,
Screen,
Overlay,
SoftLight,
HardLight,
ColorDodge,
ColorBurn,
}
/// 质量级别
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum QualityLevel {
Low, // 快速处理,质量较低
Medium, // 平衡处理,中等质量
High, // 高质量处理,速度较慢
Lossless, // 无损处理,最高质量
}
/// 水印移除配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkRemovalConfig {
pub method: RemovalMethod,
pub quality_level: QualityLevel,
pub preserve_aspect_ratio: bool,
pub target_regions: Option<Vec<BoundingBox>>, // 指定移除区域
pub inpainting_model: Option<String>, // AI修复模型
pub blur_radius: Option<f32>, // 模糊半径
pub crop_margin: Option<u32>, // 裁剪边距
}
/// 移除方法
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RemovalMethod {
Inpainting, // AI修复
Blurring, // 模糊处理
Cropping, // 裁剪移除
Masking, // 遮罩覆盖
ContentAware, // 内容感知填充
Clone, // 克隆修复
}
/// 水印检测配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkDetectionConfig {
pub similarity_threshold: f64, // 相似度阈值 0.0-1.0
pub min_watermark_size: (u32, u32), // 最小水印尺寸
pub max_watermark_size: (u32, u32), // 最大水印尺寸
pub detection_regions: Vec<DetectionRegion>, // 检测区域
pub frame_sample_rate: u32, // 视频帧采样率
pub methods: Vec<DetectionMethod>, // 使用的检测方法
pub template_ids: Option<Vec<String>>, // 指定模板ID列表
}
/// 检测区域
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DetectionRegion {
FullFrame, // 全帧检测
Corners, // 四角检测
Edges, // 边缘检测
Center, // 中心区域
Custom(BoundingBox), // 自定义区域
}
/// 批量水印处理任务
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchWatermarkTask {
pub task_id: String,
pub operation: WatermarkOperation,
pub material_ids: Vec<String>,
pub config: serde_json::Value, // 动态配置根据operation类型解析
pub status: BatchTaskStatus,
pub progress: BatchProgress,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
/// 水印操作类型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WatermarkOperation {
Detect,
Remove,
Add,
DetectAndRemove,
Replace, // 检测并替换
}
/// 批量任务状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BatchTaskStatus {
Pending, // 等待中
Running, // 执行中
Completed, // 已完成
Failed, // 失败
Cancelled, // 已取消
Paused, // 已暂停
}
/// 批量处理进度
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchProgress {
pub total_items: u32,
pub processed_items: u32,
pub failed_items: u32,
pub current_item: Option<String>, // 当前处理的素材ID
pub progress_percentage: f32, // 进度百分比 0.0-100.0
pub estimated_remaining_ms: Option<u64>, // 预估剩余时间(毫秒)
pub errors: Vec<String>, // 错误信息列表
pub detection_results: Vec<WatermarkDetectionResult>, // 检测结果列表
pub processing_results: Vec<WatermarkProcessingResult>, // 处理结果列表
}
/// 水印处理结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkProcessingResult {
pub material_id: String,
pub operation: WatermarkOperation,
pub success: bool,
pub output_path: Option<String>,
pub processing_time_ms: u64,
pub error_message: Option<String>,
pub metadata: Option<serde_json::Value>, // 额外的处理元数据
}
impl Default for WatermarkConfig {
fn default() -> Self {
Self {
watermark_type: WatermarkType::Image,
position: WatermarkPosition::BottomRight,
opacity: 0.8,
scale: 1.0,
rotation: 0.0,
animation: None,
blend_mode: BlendMode::Normal,
quality_level: QualityLevel::Medium,
}
}
}
impl Default for WatermarkDetectionConfig {
fn default() -> Self {
Self {
similarity_threshold: 0.8,
min_watermark_size: (32, 32),
max_watermark_size: (512, 512),
detection_regions: vec![
DetectionRegion::Corners,
DetectionRegion::Center,
],
frame_sample_rate: 30, // 每30帧采样一次
methods: vec![
DetectionMethod::TemplateMatching,
DetectionMethod::EdgeDetection,
],
template_ids: None,
}
}
}
impl Default for WatermarkRemovalConfig {
fn default() -> Self {
Self {
method: RemovalMethod::Inpainting,
quality_level: QualityLevel::Medium,
preserve_aspect_ratio: true,
target_regions: None,
inpainting_model: None,
blur_radius: Some(5.0),
crop_margin: Some(10),
}
}
}

View File

@@ -9,6 +9,7 @@ use crate::infrastructure::database::Database;
/// 素材仓库
/// 遵循 Tauri 开发规范的数据访问层设计
#[derive(Clone)]
pub struct MaterialRepository {
database: Arc<Database>,
}

View File

@@ -11,3 +11,4 @@ pub mod export_record_repository;
pub mod video_generation_repository;
pub mod conversation_repository;
pub mod custom_tag_repository;
pub mod watermark_template_repository;

View File

@@ -0,0 +1,403 @@
use anyhow::{Result, anyhow};
use rusqlite::{params, Row};
use std::sync::Arc;
use tracing::info;
use crate::data::models::watermark::{
WatermarkTemplate, WatermarkCategory, WatermarkType
};
use crate::infrastructure::database::Database;
/// 水印模板数据访问层
/// 遵循 Tauri 开发规范的数据访问层设计
pub struct WatermarkTemplateRepository {
database: Arc<Database>,
}
impl WatermarkTemplateRepository {
pub fn new(database: Arc<Database>) -> Self {
Self { database }
}
/// 创建水印模板
pub fn create(&self, template: &WatermarkTemplate) -> Result<()> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
conn.execute(
"INSERT INTO watermark_templates (
id, name, file_path, thumbnail_path, category, watermark_type,
file_size, width, height, description, tags, is_active,
created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
params![
template.id,
template.name,
template.file_path,
template.thumbnail_path,
serde_json::to_string(&template.category)?,
serde_json::to_string(&template.watermark_type)?,
template.file_size as i64,
template.width.map(|w| w as i32),
template.height.map(|h| h as i32),
template.description,
serde_json::to_string(&template.tags)?,
template.is_active,
template.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
template.updated_at.format("%Y-%m-%d %H:%M:%S").to_string(),
],
)?;
info!(template_id = %template.id, template_name = %template.name, "水印模板创建成功");
Ok(())
}
/// 根据ID获取水印模板
pub fn get_by_id(&self, id: &str) -> Result<Option<WatermarkTemplate>> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, name, file_path, thumbnail_path, category, watermark_type,
file_size, width, height, description, tags, is_active,
created_at, updated_at
FROM watermark_templates
WHERE id = ?1"
)?;
let template_iter = stmt.query_map([id], |row| {
Self::row_to_template(row)
})?;
for template in template_iter {
return Ok(Some(template?));
}
Ok(None)
}
/// 获取所有水印模板
pub fn get_all(&self) -> Result<Vec<WatermarkTemplate>> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, name, file_path, thumbnail_path, category, watermark_type,
file_size, width, height, description, tags, is_active,
created_at, updated_at
FROM watermark_templates
ORDER BY created_at DESC"
)?;
let template_iter = stmt.query_map([], |row| {
Self::row_to_template(row)
})?;
let mut templates = Vec::new();
for template in template_iter {
templates.push(template?);
}
Ok(templates)
}
/// 根据分类获取水印模板
pub fn get_by_category(&self, category: &WatermarkCategory) -> Result<Vec<WatermarkTemplate>> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let category_str = serde_json::to_string(category)?;
let mut stmt = conn.prepare(
"SELECT id, name, file_path, thumbnail_path, category, watermark_type,
file_size, width, height, description, tags, is_active,
created_at, updated_at
FROM watermark_templates
WHERE category = ?1 AND is_active = 1
ORDER BY created_at DESC"
)?;
let template_iter = stmt.query_map([category_str], |row| {
Self::row_to_template(row)
})?;
let mut templates = Vec::new();
for template in template_iter {
templates.push(template?);
}
Ok(templates)
}
/// 根据类型获取水印模板
pub fn get_by_type(&self, watermark_type: &WatermarkType) -> Result<Vec<WatermarkTemplate>> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let type_str = serde_json::to_string(watermark_type)?;
let mut stmt = conn.prepare(
"SELECT id, name, file_path, thumbnail_path, category, watermark_type,
file_size, width, height, description, tags, is_active,
created_at, updated_at
FROM watermark_templates
WHERE watermark_type = ?1 AND is_active = 1
ORDER BY created_at DESC"
)?;
let template_iter = stmt.query_map([type_str], |row| {
Self::row_to_template(row)
})?;
let mut templates = Vec::new();
for template in template_iter {
templates.push(template?);
}
Ok(templates)
}
/// 搜索水印模板
pub fn search(&self, query: &str) -> Result<Vec<WatermarkTemplate>> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let search_pattern = format!("%{}%", query);
let mut stmt = conn.prepare(
"SELECT id, name, file_path, thumbnail_path, category, watermark_type,
file_size, width, height, description, tags, is_active,
created_at, updated_at
FROM watermark_templates
WHERE (name LIKE ?1 OR description LIKE ?1 OR tags LIKE ?1)
AND is_active = 1
ORDER BY created_at DESC"
)?;
let template_iter = stmt.query_map([search_pattern], |row| {
Self::row_to_template(row)
})?;
let mut templates = Vec::new();
for template in template_iter {
templates.push(template?);
}
Ok(templates)
}
/// 更新水印模板
pub fn update(&self, template: &WatermarkTemplate) -> Result<()> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let rows_affected = conn.execute(
"UPDATE watermark_templates SET
name = ?2, file_path = ?3, thumbnail_path = ?4, category = ?5,
watermark_type = ?6, file_size = ?7, width = ?8, height = ?9,
description = ?10, tags = ?11, is_active = ?12, updated_at = ?13
WHERE id = ?1",
params![
template.id,
template.name,
template.file_path,
template.thumbnail_path,
serde_json::to_string(&template.category)?,
serde_json::to_string(&template.watermark_type)?,
template.file_size as i64,
template.width.map(|w| w as i32),
template.height.map(|h| h as i32),
template.description,
serde_json::to_string(&template.tags)?,
template.is_active,
template.updated_at.format("%Y-%m-%d %H:%M:%S").to_string(),
],
)?;
if rows_affected == 0 {
return Err(anyhow!("水印模板不存在: {}", template.id));
}
info!(template_id = %template.id, "水印模板更新成功");
Ok(())
}
/// 删除水印模板
pub fn delete(&self, id: &str) -> Result<()> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let rows_affected = conn.execute(
"DELETE FROM watermark_templates WHERE id = ?1",
[id],
)?;
if rows_affected == 0 {
return Err(anyhow!("水印模板不存在: {}", id));
}
info!(template_id = %id, "水印模板删除成功");
Ok(())
}
/// 软删除水印模板(设置为非活跃状态)
pub fn soft_delete(&self, id: &str) -> Result<()> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let rows_affected = conn.execute(
"UPDATE watermark_templates SET is_active = 0, updated_at = datetime('now') WHERE id = ?1",
[id],
)?;
if rows_affected == 0 {
return Err(anyhow!("水印模板不存在: {}", id));
}
info!(template_id = %id, "水印模板软删除成功");
Ok(())
}
/// 获取模板统计信息
pub fn get_stats(&self) -> Result<serde_json::Value> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
// 总数统计
let total_count: i64 = conn.query_row(
"SELECT COUNT(*) FROM watermark_templates WHERE is_active = 1",
[],
|row| row.get(0),
)?;
// 按分类统计
let mut category_stats = std::collections::HashMap::new();
let mut stmt = conn.prepare(
"SELECT category, COUNT(*) FROM watermark_templates
WHERE is_active = 1 GROUP BY category"
)?;
let category_iter = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
})?;
for result in category_iter {
let (category, count) = result?;
category_stats.insert(category, count);
}
// 按类型统计
let mut type_stats = std::collections::HashMap::new();
let mut stmt = conn.prepare(
"SELECT watermark_type, COUNT(*) FROM watermark_templates
WHERE is_active = 1 GROUP BY watermark_type"
)?;
let type_iter = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
})?;
for result in type_iter {
let (watermark_type, count) = result?;
type_stats.insert(watermark_type, count);
}
// 总文件大小
let total_size: i64 = conn.query_row(
"SELECT COALESCE(SUM(file_size), 0) FROM watermark_templates WHERE is_active = 1",
[],
|row| row.get(0),
)?;
Ok(serde_json::json!({
"total_templates": total_count,
"by_category": category_stats,
"by_type": type_stats,
"total_size": total_size
}))
}
/// 检查模板名称是否已存在
pub fn name_exists(&self, name: &str, exclude_id: Option<&str>) -> Result<bool> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let count: i64 = if let Some(id) = exclude_id {
conn.query_row(
"SELECT COUNT(*) FROM watermark_templates WHERE name = ?1 AND id != ?2 AND is_active = 1",
[name, id],
|row| row.get(0)
)?
} else {
conn.query_row(
"SELECT COUNT(*) FROM watermark_templates WHERE name = ?1 AND is_active = 1",
[name],
|row| row.get(0)
)?
};
Ok(count > 0)
}
/// 将数据库行转换为WatermarkTemplate对象
fn row_to_template(row: &Row) -> rusqlite::Result<WatermarkTemplate> {
let category_str: String = row.get("category")?;
let category: WatermarkCategory = serde_json::from_str(&category_str)
.map_err(|e| rusqlite::Error::InvalidColumnType(
row.as_ref().column_index("category").unwrap(),
format!("Invalid category: {}", e),
rusqlite::types::Type::Text
))?;
let type_str: String = row.get("watermark_type")?;
let watermark_type: WatermarkType = serde_json::from_str(&type_str)
.map_err(|e| rusqlite::Error::InvalidColumnType(
row.as_ref().column_index("watermark_type").unwrap(),
format!("Invalid watermark_type: {}", e),
rusqlite::types::Type::Text
))?;
let tags_str: String = row.get("tags")?;
let tags: Vec<String> = serde_json::from_str(&tags_str)
.map_err(|e| rusqlite::Error::InvalidColumnType(
row.as_ref().column_index("tags").unwrap(),
format!("Invalid tags: {}", e),
rusqlite::types::Type::Text
))?;
let created_at_str: String = row.get("created_at")?;
let created_at = chrono::DateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S")
.map_err(|e| rusqlite::Error::InvalidColumnType(
row.as_ref().column_index("created_at").unwrap(),
format!("Invalid created_at: {}", e),
rusqlite::types::Type::Text
))?
.with_timezone(&chrono::Utc);
let updated_at_str: String = row.get("updated_at")?;
let updated_at = chrono::DateTime::parse_from_str(&updated_at_str, "%Y-%m-%d %H:%M:%S")
.map_err(|e| rusqlite::Error::InvalidColumnType(
row.as_ref().column_index("updated_at").unwrap(),
format!("Invalid updated_at: {}", e),
rusqlite::types::Type::Text
))?
.with_timezone(&chrono::Utc);
Ok(WatermarkTemplate {
id: row.get("id")?,
name: row.get("name")?,
file_path: row.get("file_path")?,
thumbnail_path: row.get("thumbnail_path")?,
category,
watermark_type,
file_size: row.get::<_, i64>("file_size")? as u64,
width: row.get::<_, Option<i32>>("width")?.map(|w| w as u32),
height: row.get::<_, Option<i32>>("height")?.map(|h| h as u32),
description: row.get("description")?,
tags,
is_active: row.get("is_active")?,
created_at,
updated_at,
})
}
}

View File

@@ -871,6 +871,75 @@ impl Database {
[],
)?;
// 创建水印模板表
conn.execute(
"CREATE TABLE IF NOT EXISTS watermark_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
file_path TEXT NOT NULL,
thumbnail_path TEXT,
category TEXT NOT NULL,
watermark_type TEXT NOT NULL,
file_size INTEGER NOT NULL,
width INTEGER,
height INTEGER,
description TEXT,
tags TEXT,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
// 创建水印检测结果表
conn.execute(
"CREATE TABLE IF NOT EXISTS watermark_detection_results (
id TEXT PRIMARY KEY,
material_id TEXT NOT NULL,
detection_method TEXT NOT NULL,
detections TEXT NOT NULL,
confidence_score REAL NOT NULL,
processing_time_ms INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE
)",
[],
)?;
// 创建批量水印处理任务表
conn.execute(
"CREATE TABLE IF NOT EXISTS batch_watermark_tasks (
task_id TEXT PRIMARY KEY,
operation TEXT NOT NULL,
material_ids TEXT NOT NULL,
config TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'Pending',
progress TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
completed_at DATETIME
)",
[],
)?;
// 创建水印处理结果表
conn.execute(
"CREATE TABLE IF NOT EXISTS watermark_processing_results (
id TEXT PRIMARY KEY,
material_id TEXT NOT NULL,
operation TEXT NOT NULL,
success INTEGER NOT NULL,
output_path TEXT,
processing_time_ms INTEGER NOT NULL,
error_message TEXT,
metadata TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE
)",
[],
)?;
// 创建索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_projects_name ON projects (name)",
@@ -1024,6 +1093,70 @@ impl Database {
"CREATE INDEX IF NOT EXISTS idx_outfit_search_history_query_text ON outfit_search_history (query_text)",
[],
)?;
// 创建水印模板表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_templates_category ON watermark_templates (category)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_templates_type ON watermark_templates (watermark_type)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_templates_active ON watermark_templates (is_active)",
[],
)?;
// 创建水印检测结果表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_detection_results_material_id ON watermark_detection_results (material_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_detection_results_method ON watermark_detection_results (detection_method)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_detection_results_confidence ON watermark_detection_results (confidence_score)",
[],
)?;
// 创建批量水印处理任务表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_batch_watermark_tasks_status ON batch_watermark_tasks (status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_batch_watermark_tasks_operation ON batch_watermark_tasks (operation)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_batch_watermark_tasks_created_at ON batch_watermark_tasks (created_at)",
[],
)?;
// 创建水印处理结果表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_processing_results_material_id ON watermark_processing_results (material_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_processing_results_operation ON watermark_processing_results (operation)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_watermark_processing_results_success ON watermark_processing_results (success)",
[],
)?;
// 添加新字段(如果不存在)- 数据库迁移
let _ = conn.execute(
"ALTER TABLE template_materials ADD COLUMN file_exists BOOLEAN DEFAULT FALSE",

View File

@@ -0,0 +1,319 @@
use anyhow::{Result, anyhow};
use std::path::Path;
use std::process::Command;
use tracing::{info, debug, error};
/// FFmpeg水印处理工具类
/// 专门用于水印相关的FFmpeg操作
pub struct FFmpegWatermark;
impl FFmpegWatermark {
/// 创建隐藏窗口的命令
fn create_hidden_command(program: &str) -> Command {
let mut cmd = Command::new(program);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
cmd
}
/// 执行FFmpeg命令
pub fn execute_command(args: &[&str]) -> Result<()> {
debug!("执行FFmpeg命令: {:?}", args);
let output = Self::create_hidden_command("ffmpeg")
.args(args)
.output()
.map_err(|e| anyhow!("FFmpeg执行失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("FFmpeg命令失败: {}", stderr);
return Err(anyhow!("FFmpeg命令执行失败: {}", stderr));
}
Ok(())
}
/// 提取视频帧到指定时间戳
pub fn extract_frame_at_timestamp(
input_path: &str,
timestamp: f64,
output_path: &str,
width: u32,
height: u32,
) -> Result<()> {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let timestamp_str = timestamp.to_string();
let scale_filter = format!("scale={}:{}", width, height);
let args = vec![
"-hide_banner",
"-loglevel", "error",
"-ss", &timestamp_str,
"-i", input_path,
"-vframes", "1",
"-vf", &scale_filter,
"-y", output_path,
];
Self::execute_command(&args)?;
// 验证输出文件是否创建成功
if !Path::new(output_path).exists() {
return Err(anyhow!("帧提取失败,输出文件不存在"));
}
info!("成功提取帧: {} -> {}", input_path, output_path);
Ok(())
}
/// 获取视频信息
pub fn get_video_info(input_path: &str) -> Result<VideoInfo> {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
}
let output = Self::create_hidden_command("ffprobe")
.args(&[
"-hide_banner",
"-loglevel", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height,duration,nb_frames",
"-of", "csv=p=0",
input_path,
])
.output()
.map_err(|e| anyhow!("ffprobe执行失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("ffprobe命令失败: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split(',').collect();
if parts.len() < 3 {
return Err(anyhow!("无法解析视频信息"));
}
let width = parts[0].parse::<u32>().ok();
let height = parts[1].parse::<u32>().ok();
let duration = parts[2].parse::<f64>().unwrap_or(0.0);
let frame_count = if parts.len() > 3 {
parts[3].parse::<u32>().ok()
} else {
None
};
Ok(VideoInfo {
width,
height,
duration,
frame_count,
})
}
/// 应用视频滤镜
pub fn apply_video_filter(
input_path: &str,
output_path: &str,
filter: &str,
quality_args: &[&str],
) -> Result<()> {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let mut args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", filter,
];
// 添加质量参数
args.extend_from_slice(quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
Self::execute_command(&args)?;
// 验证输出文件
if !Path::new(output_path).exists() {
return Err(anyhow!("视频处理失败,输出文件不存在"));
}
info!("成功应用视频滤镜: {} -> {}", input_path, output_path);
Ok(())
}
/// 应用复杂滤镜(支持多输入)
pub fn apply_complex_filter(
input_paths: &[&str],
output_path: &str,
filter_complex: &str,
quality_args: &[&str],
) -> Result<()> {
// 验证所有输入文件存在
for input_path in input_paths {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
}
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let mut args = vec!["-hide_banner", "-loglevel", "error"];
// 添加所有输入文件
for input_path in input_paths {
args.extend_from_slice(&["-i", input_path]);
}
// 添加复杂滤镜
args.extend_from_slice(&["-filter_complex", filter_complex]);
// 添加质量参数
args.extend_from_slice(quality_args);
args.extend_from_slice(&["-c:a", "copy", "-y", output_path]);
Self::execute_command(&args)?;
// 验证输出文件
if !Path::new(output_path).exists() {
return Err(anyhow!("视频处理失败,输出文件不存在"));
}
info!("成功应用复杂滤镜: {:?} -> {}", input_paths, output_path);
Ok(())
}
/// 处理图片
pub fn process_image(
input_path: &str,
output_path: &str,
filter: &str,
) -> Result<()> {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let args = vec![
"-hide_banner",
"-loglevel", "error",
"-i", input_path,
"-vf", filter,
"-y", output_path,
];
Self::execute_command(&args)?;
// 验证输出文件
if !Path::new(output_path).exists() {
return Err(anyhow!("图片处理失败,输出文件不存在"));
}
info!("成功处理图片: {} -> {}", input_path, output_path);
Ok(())
}
/// 处理带多个输入的图片(如添加水印)
pub fn process_image_with_overlay(
input_paths: &[&str],
output_path: &str,
filter_complex: &str,
) -> Result<()> {
// 验证所有输入文件存在
for input_path in input_paths {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
}
}
// 确保输出目录存在
if let Some(parent) = Path::new(output_path).parent() {
std::fs::create_dir_all(parent)?;
}
let mut args = vec!["-hide_banner", "-loglevel", "error"];
// 添加所有输入文件
for input_path in input_paths {
args.extend_from_slice(&["-i", input_path]);
}
// 添加复杂滤镜
args.extend_from_slice(&["-filter_complex", filter_complex]);
args.extend_from_slice(&["-y", output_path]);
Self::execute_command(&args)?;
// 验证输出文件
if !Path::new(output_path).exists() {
return Err(anyhow!("图片处理失败,输出文件不存在"));
}
info!("成功处理图片叠加: {:?} -> {}", input_paths, output_path);
Ok(())
}
/// 检查FFmpeg是否可用
pub fn is_available() -> bool {
Self::create_hidden_command("ffmpeg")
.arg("-version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
/// 获取FFmpeg版本信息
pub fn get_version() -> Result<String> {
let output = Self::create_hidden_command("ffmpeg")
.arg("-version")
.output()
.map_err(|e| anyhow!("获取FFmpeg版本失败: {}", e))?;
if !output.status.success() {
return Err(anyhow!("FFmpeg不可用"));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let first_line = stdout.lines().next().unwrap_or("Unknown version");
Ok(first_line.to_string())
}
}
/// 视频信息结构
#[derive(Debug, Clone)]
pub struct VideoInfo {
pub width: Option<u32>,
pub height: Option<u32>,
pub duration: f64,
pub frame_count: Option<u32>,
}

View File

@@ -7,6 +7,7 @@ pub mod filename_utils;
pub mod performance;
pub mod event_bus;
pub mod ffmpeg;
pub mod ffmpeg_watermark;
pub mod monitoring;
pub mod logging;
pub mod gemini_service;

View File

@@ -334,7 +334,21 @@ pub fn run() {
commands::markdown_commands::find_markdown_node_at_position,
commands::markdown_commands::extract_markdown_outline,
commands::markdown_commands::extract_markdown_links,
commands::markdown_commands::validate_markdown
commands::markdown_commands::validate_markdown,
// 水印处理命令
commands::watermark_commands::detect_watermarks_in_video,
commands::watermark_commands::detect_watermarks_in_image,
commands::watermark_commands::remove_watermarks_from_video,
commands::watermark_commands::remove_watermarks_from_image,
commands::watermark_commands::add_watermark_to_video,
commands::watermark_commands::add_watermark_to_image,
commands::watermark_commands::start_batch_watermark_task,
commands::watermark_commands::get_watermark_templates,
commands::watermark_commands::upload_watermark_template,
commands::watermark_commands::delete_watermark_template,
commands::watermark_commands::get_batch_task_status,
commands::watermark_commands::get_watermark_template_thumbnail,
commands::watermark_commands::cancel_batch_task
])
.setup(|app| {
// 初始化日志系统

View File

@@ -25,3 +25,4 @@ pub mod markdown_commands;
pub mod rag_grounding_commands;
pub mod image_download_commands;
pub mod conversation_commands;
pub mod watermark_commands;

View File

@@ -0,0 +1,544 @@
use tauri::{command, State};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use tracing::{info, error, warn};
use lazy_static::lazy_static;
use crate::data::models::watermark::{
WatermarkDetectionConfig, WatermarkRemovalConfig, WatermarkConfig,
WatermarkTemplate, BatchWatermarkTask, WatermarkOperation,
BatchTaskStatus, BatchProgress, WatermarkDetectionResult,
WatermarkProcessingResult
};
use crate::business::services::{
watermark_detection_service::WatermarkDetectionService,
watermark_removal_service::WatermarkRemovalService,
watermark_addition_service::WatermarkAdditionService,
batch_watermark_processor::BatchWatermarkProcessor,
task_manager::TASK_MANAGER,
};
// 全局模板存储
lazy_static! {
static ref TEMPLATE_STORAGE: Mutex<HashMap<String, WatermarkTemplate>> = {
let mut templates = HashMap::new();
// 添加一些默认模板
let default_templates = create_default_templates();
for template in default_templates {
templates.insert(template.id.clone(), template);
}
Mutex::new(templates)
};
}
// 创建默认模板
fn create_default_templates() -> Vec<WatermarkTemplate> {
use crate::data::models::watermark::{WatermarkCategory, WatermarkType};
vec![
WatermarkTemplate {
id: "template_1".to_string(),
name: "公司Logo".to_string(),
file_path: "watermarks/logo.png".to_string(),
thumbnail_path: Some("watermarks/thumbnails/logo.jpg".to_string()),
category: WatermarkCategory::Logo,
watermark_type: WatermarkType::Image,
file_size: 2048,
width: Some(200),
height: Some(100),
description: Some("公司标准Logo水印".to_string()),
tags: vec!["logo".to_string(), "公司".to_string()],
is_active: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
WatermarkTemplate {
id: "template_2".to_string(),
name: "版权声明".to_string(),
file_path: "watermarks/copyright.png".to_string(),
thumbnail_path: Some("watermarks/thumbnails/copyright.jpg".to_string()),
category: WatermarkCategory::Copyright,
watermark_type: WatermarkType::Text,
file_size: 1024,
width: Some(300),
height: Some(50),
description: Some("标准版权声明水印".to_string()),
tags: vec!["版权".to_string(), "声明".to_string()],
is_active: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
]
}
use crate::AppState;
/// 检测视频中的水印
#[command]
pub async fn detect_watermarks_in_video(
state: State<'_, AppState>,
material_id: String,
video_path: String,
config: WatermarkDetectionConfig,
) -> Result<WatermarkDetectionResult, String> {
info!(
material_id = %material_id,
video_path = %video_path,
"Tauri命令: 检测视频水印"
);
let repository = {
let repo_guard = state.material_repository.lock().unwrap();
repo_guard.as_ref()
.ok_or_else(|| "Material repository not initialized".to_string())?
.clone()
};
WatermarkDetectionService::detect_watermarks_in_video(
&material_id,
&video_path,
&config,
Arc::new(repository.clone()),
)
.await
.map_err(|e| {
error!(error = %e, "检测视频水印失败");
e.to_string()
})
}
/// 检测图片中的水印
#[command]
pub async fn detect_watermarks_in_image(
material_id: String,
image_path: String,
config: WatermarkDetectionConfig,
) -> Result<WatermarkDetectionResult, String> {
info!(
material_id = %material_id,
image_path = %image_path,
"Tauri命令: 检测图片水印"
);
WatermarkDetectionService::detect_watermarks_in_image(
&material_id,
&image_path,
&config,
)
.await
.map_err(|e| {
error!(error = %e, "检测图片水印失败");
e.to_string()
})
}
/// 移除视频中的水印
#[command]
pub async fn remove_watermarks_from_video(
state: State<'_, AppState>,
material_id: String,
input_path: String,
output_path: String,
config: WatermarkRemovalConfig,
) -> Result<WatermarkProcessingResult, String> {
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
"Tauri命令: 移除视频水印"
);
let repository = {
let repo_guard = state.material_repository.lock().unwrap();
repo_guard.as_ref()
.ok_or_else(|| "Material repository not initialized".to_string())?
.clone()
};
WatermarkRemovalService::remove_watermarks_from_video(
&material_id,
&input_path,
&output_path,
&config,
Arc::new(repository.clone()),
)
.await
.map_err(|e| {
error!(error = %e, "移除视频水印失败");
e.to_string()
})
}
/// 移除图片中的水印
#[command]
pub async fn remove_watermarks_from_image(
material_id: String,
input_path: String,
output_path: String,
config: WatermarkRemovalConfig,
) -> Result<WatermarkProcessingResult, String> {
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
"Tauri命令: 移除图片水印"
);
WatermarkRemovalService::remove_watermarks_from_image(
&material_id,
&input_path,
&output_path,
&config,
)
.await
.map_err(|e| {
error!(error = %e, "移除图片水印失败");
e.to_string()
})
}
/// 为视频添加水印
#[command]
pub async fn add_watermark_to_video(
state: State<'_, AppState>,
material_id: String,
input_path: String,
output_path: String,
watermark_path: String,
config: WatermarkConfig,
) -> Result<WatermarkProcessingResult, String> {
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
watermark_path = %watermark_path,
"Tauri命令: 为视频添加水印"
);
let repository = {
let repo_guard = state.material_repository.lock().unwrap();
repo_guard.as_ref()
.ok_or_else(|| "Material repository not initialized".to_string())?
.clone()
};
WatermarkAdditionService::add_watermark_to_video(
&material_id,
&input_path,
&output_path,
&watermark_path,
&config,
Arc::new(repository.clone()),
)
.await
.map_err(|e| {
error!(error = %e, "为视频添加水印失败");
e.to_string()
})
}
/// 为图片添加水印
#[command]
pub async fn add_watermark_to_image(
material_id: String,
input_path: String,
output_path: String,
watermark_path: String,
config: WatermarkConfig,
) -> Result<WatermarkProcessingResult, String> {
info!(
material_id = %material_id,
input_path = %input_path,
output_path = %output_path,
watermark_path = %watermark_path,
"Tauri命令: 为图片添加水印"
);
WatermarkAdditionService::add_watermark_to_image(
&material_id,
&input_path,
&output_path,
&watermark_path,
&config,
)
.await
.map_err(|e| {
error!(error = %e, "为图片添加水印失败");
e.to_string()
})
}
/// 启动批量水印处理任务
#[command]
pub async fn start_batch_watermark_task(
state: State<'_, AppState>,
operation: WatermarkOperation,
material_ids: Vec<String>,
config: serde_json::Value,
) -> Result<String, String> {
info!(
operation = ?operation,
material_count = material_ids.len(),
"Tauri命令: 启动批量水印处理任务"
);
let task_id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let task = BatchWatermarkTask {
task_id: task_id.clone(),
operation,
material_ids: material_ids.clone(),
config,
status: BatchTaskStatus::Pending,
progress: BatchProgress {
total_items: material_ids.len() as u32,
processed_items: 0,
failed_items: 0,
current_item: None,
progress_percentage: 0.0,
estimated_remaining_ms: None,
errors: Vec::new(),
detection_results: Vec::new(),
processing_results: Vec::new(),
},
created_at: now,
updated_at: now,
started_at: None,
completed_at: None,
};
let repository = {
let repo_guard = state.material_repository.lock().unwrap();
repo_guard.as_ref()
.ok_or_else(|| "Material repository not initialized".to_string())?
.clone()
};
// 在后台启动任务
let task_id_clone = task_id.clone();
tokio::spawn(async move {
if let Err(e) = BatchWatermarkProcessor::start_batch_task(task, Arc::new(repository.clone())).await {
error!(task_id = %task_id_clone, error = %e, "批量水印处理任务失败");
}
});
Ok(task_id)
}
/// 获取水印模板列表
#[command]
pub async fn get_watermark_templates(
category: Option<String>,
watermark_type: Option<String>,
) -> Result<Vec<WatermarkTemplate>, String> {
info!(
category = ?category,
watermark_type = ?watermark_type,
"Tauri命令: 获取水印模板列表"
);
// 从内存存储获取模板
let storage = TEMPLATE_STORAGE.lock().unwrap();
let mut templates: Vec<WatermarkTemplate> = storage.values()
.filter(|template| template.is_active)
.cloned()
.collect();
// 根据分类过滤
if let Some(cat) = category {
templates.retain(|t| format!("{:?}", t.category).contains(&cat));
}
// 根据类型过滤
if let Some(wtype) = watermark_type {
templates.retain(|t| format!("{:?}", t.watermark_type).contains(&wtype));
}
info!(template_count = templates.len(), "返回水印模板列表");
Ok(templates)
}
/// 上传水印模板
#[command]
pub async fn upload_watermark_template(
name: String,
file_path: String,
category: String,
watermark_type: String,
description: Option<String>,
tags: Vec<String>,
) -> Result<WatermarkTemplate, String> {
info!(
name = %name,
file_path = %file_path,
category = %category,
watermark_type = %watermark_type,
"Tauri命令: 上传水印模板"
);
// 简单实现:创建模板记录
// 在实际应用中,这里应该:
// 1. 验证文件存在和格式
// 2. 复制文件到模板目录
// 3. 生成缩略图
// 4. 保存到数据库
let template = WatermarkTemplate {
id: uuid::Uuid::new_v4().to_string(),
name: name.clone(),
file_path: format!("watermarks/templates/{}", file_path), // 模拟路径
thumbnail_path: Some(format!("watermarks/thumbnails/{}.jpg", name)),
category: match category.as_str() {
"Logo" => crate::data::models::watermark::WatermarkCategory::Logo,
"Copyright" => crate::data::models::watermark::WatermarkCategory::Copyright,
"Signature" => crate::data::models::watermark::WatermarkCategory::Signature,
"Decoration" => crate::data::models::watermark::WatermarkCategory::Decoration,
"Custom" => crate::data::models::watermark::WatermarkCategory::Custom,
_ => return Err(format!("无效的分类: {}", category)),
},
watermark_type: match watermark_type.as_str() {
"Image" => crate::data::models::watermark::WatermarkType::Image,
"Vector" => crate::data::models::watermark::WatermarkType::Vector,
"Text" => crate::data::models::watermark::WatermarkType::Text,
"Animated" => crate::data::models::watermark::WatermarkType::Animated,
_ => return Err(format!("无效的水印类型: {}", watermark_type)),
},
file_size: 1024, // 模拟文件大小
width: Some(200),
height: Some(100),
description,
tags,
is_active: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
// 将模板添加到存储中
{
let mut storage = TEMPLATE_STORAGE.lock().unwrap();
storage.insert(template.id.clone(), template.clone());
}
info!(template_id = %template.id, name = %template.name, "水印模板上传成功");
Ok(template)
}
/// 删除水印模板
#[command]
pub async fn delete_watermark_template(
template_id: String,
) -> Result<bool, String> {
info!(
template_id = %template_id,
"Tauri命令: 删除水印模板"
);
// 从存储中删除模板
let mut storage = TEMPLATE_STORAGE.lock().unwrap();
let removed = storage.remove(&template_id);
match removed {
Some(_) => {
info!(template_id = %template_id, "水印模板删除成功");
Ok(true)
}
None => {
warn!(template_id = %template_id, "要删除的模板不存在");
Err(format!("模板不存在: {}", template_id))
}
}
}
/// 获取批量任务状态
#[command]
pub async fn get_batch_task_status(
task_id: String,
) -> Result<BatchWatermarkTask, String> {
info!(
task_id = %task_id,
"Tauri命令: 获取批量任务状态"
);
match TASK_MANAGER.get_task(&task_id) {
Some(task) => {
info!(
task_id = %task_id,
status = ?task.status,
progress = task.progress.progress_percentage,
"返回任务状态"
);
Ok(task)
}
None => {
warn!(task_id = %task_id, "任务不存在");
Err(format!("任务不存在: {}", task_id))
}
}
}
/// 获取水印模板缩略图
#[command]
pub async fn get_watermark_template_thumbnail(
template_id: String,
) -> Result<String, String> {
info!(
template_id = %template_id,
"Tauri命令: 获取水印模板缩略图"
);
let storage = TEMPLATE_STORAGE.lock().unwrap();
match storage.get(&template_id) {
Some(template) => {
// 模拟生成base64缩略图数据
// 在实际应用中,这里应该读取实际的缩略图文件
let mock_thumbnail = generate_mock_thumbnail(&template.name, &template.watermark_type);
info!(template_id = %template_id, "返回模板缩略图");
Ok(mock_thumbnail)
}
None => {
warn!(template_id = %template_id, "模板不存在");
Err(format!("模板不存在: {}", template_id))
}
}
}
// 生成模拟缩略图数据
fn generate_mock_thumbnail(name: &str, watermark_type: &crate::data::models::watermark::WatermarkType) -> String {
// 返回一个简单的彩色方块作为缩略图
let (bg_color, text_color, type_name) = match watermark_type {
crate::data::models::watermark::WatermarkType::Image => ("lightblue", "darkblue", "图片"),
crate::data::models::watermark::WatermarkType::Text => ("lightpink", "purple", "文字"),
crate::data::models::watermark::WatermarkType::Vector => ("lightgreen", "darkgreen", "矢量"),
crate::data::models::watermark::WatermarkType::Animated => ("lightyellow", "orange", "动画"),
};
let svg_content = format!(
r#"<svg width="200" height="100" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="100" fill="{}"/>
<text x="100" y="45" text-anchor="middle" fill="{}" font-family="Arial" font-size="14" font-weight="bold">{}</text>
<text x="100" y="65" text-anchor="middle" fill="{}" font-family="Arial" font-size="10">{}水印</text>
</svg>"#,
bg_color, text_color, name, text_color, type_name
);
use base64::{Engine as _, engine::general_purpose};
let encoded = general_purpose::STANDARD.encode(svg_content.as_bytes());
format!("data:image/svg+xml;base64,{}", encoded)
}
/// 取消批量任务
#[command]
pub async fn cancel_batch_task(
task_id: String,
) -> Result<bool, String> {
info!(
task_id = %task_id,
"Tauri命令: 取消批量任务"
);
// TODO: 实现任务取消逻辑
Ok(true)
}

View File

@@ -15,6 +15,7 @@ import JsonParserTool from './pages/tools/JsonParserTool';
import DebugPanelTool from './pages/tools/DebugPanelTool';
import ChatTool from './pages/tools/ChatTool';
import ChatTestPage from './pages/tools/ChatTestPage';
import WatermarkTool from './pages/tools/WatermarkTool';
import Navigation from './components/Navigation';
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
@@ -99,6 +100,7 @@ function App() {
<Route path="/tools/debug-panel" element={<DebugPanelTool />} />
<Route path="/tools/ai-chat" element={<ChatTool />} />
<Route path="/tools/chat-test" element={<ChatTestPage />} />
<Route path="/tools/watermark" element={<WatermarkTool />} />
</Routes>
</div>
</main>

View File

@@ -0,0 +1,114 @@
import React, { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Loader2, ImageIcon } from 'lucide-react';
interface WatermarkTemplateThumbnailProps {
templateId: string;
size?: 'small' | 'medium' | 'large';
className?: string;
thumbnailCache?: Map<string, string>;
setThumbnailCache?: (cache: Map<string, string>) => void;
}
/**
* 水印模板缩略图组件
* 参考其他页面的缩略图实现模式
*/
export const WatermarkTemplateThumbnail: React.FC<WatermarkTemplateThumbnailProps> = ({
templateId,
size = 'medium',
className = '',
thumbnailCache = new Map(),
setThumbnailCache = () => {},
}) => {
const [loading, setLoading] = useState(false);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [error, setError] = useState(false);
// 根据size确定尺寸
const getSizeClasses = () => {
switch (size) {
case 'small':
return 'w-12 h-12';
case 'medium':
return 'w-16 h-16';
case 'large':
return 'w-24 h-24';
default:
return 'w-16 h-16';
}
};
const getIconSize = () => {
switch (size) {
case 'small':
return 'w-3 h-3';
case 'medium':
return 'w-4 h-4';
case 'large':
return 'w-6 h-6';
default:
return 'w-4 h-4';
}
};
useEffect(() => {
if (!templateId) return;
const loadThumbnail = async () => {
// 检查缓存
if (thumbnailCache.has(templateId)) {
const cachedUrl = thumbnailCache.get(templateId);
setThumbnailUrl(cachedUrl || null);
return;
}
// 加载缩略图
setLoading(true);
setError(false);
try {
console.log('获取水印模板缩略图:', templateId);
const dataUrl = await invoke<string>('get_watermark_template_thumbnail', {
templateId: templateId
});
console.log('获取缩略图成功');
setThumbnailUrl(dataUrl);
// 更新缓存
const newCache = new Map(thumbnailCache);
newCache.set(templateId, dataUrl);
setThumbnailCache(newCache);
} catch (error) {
console.error('获取缩略图失败:', error);
setError(true);
} finally {
setLoading(false);
}
};
loadThumbnail();
}, [templateId, thumbnailCache, setThumbnailCache]);
return (
<div
className={`${getSizeClasses()} bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden ${className}`}
>
{loading ? (
<Loader2 className={`${getIconSize()} animate-spin text-blue-600`} />
) : thumbnailUrl && !error ? (
<img
src={thumbnailUrl}
alt="水印模板缩略图"
className="w-full h-full object-cover rounded-lg"
onError={() => {
setError(true);
setThumbnailUrl(null);
}}
/>
) : (
<ImageIcon className={`${getIconSize()} text-gray-400`} />
)}
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,8 @@ import {
Wrench,
Database,
FileSearch,
MessageCircle
MessageCircle,
Droplets
} from 'lucide-react';
import { Tool, ToolCategory, ToolStatus } from '../types/tool';
@@ -69,6 +70,21 @@ export const TOOLS_DATA: Tool[] = [
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-21'
},
{
id: 'watermark-tool',
name: '水印处理工具',
description: '专业的视频水印检测、移除和添加工具,支持批量处理和多种水印类型',
longDescription: '强大的水印处理工具集提供智能水印检测、精确移除和自定义添加功能。支持视频和图片格式提供多种移除算法AI修复、模糊处理、裁剪等和丰富的水印样式选择。',
icon: Droplets,
route: '/tools/watermark',
category: ToolCategory.FILE_PROCESSING,
status: ToolStatus.STABLE,
tags: ['水印检测', '水印移除', '水印添加', '批量处理', '视频处理'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-23'
}
];

View File

@@ -0,0 +1,241 @@
import React, { useState, useEffect } from 'react';
import { ArrowLeft, Droplets, Upload, Settings } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { Material } from '../../types/material';
import { WatermarkToolDialog } from '../../components/WatermarkToolDialog';
import { LoadingSpinner } from '../../components/LoadingSpinner';
/**
* 水印工具页面
* 提供水印检测、移除和添加功能
*/
const WatermarkTool: React.FC = () => {
const navigate = useNavigate();
// 状态管理
const [materials, setMaterials] = useState<Material[]>([]);
const [loading, setLoading] = useState(true);
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([]);
const [showWatermarkDialog, setShowWatermarkDialog] = useState(false);
// 加载素材列表
useEffect(() => {
loadMaterials();
}, []);
const loadMaterials = async () => {
try {
setLoading(true);
const result = await invoke<Material[]>('get_all_materials');
setMaterials(result);
} catch (error) {
console.error('加载素材失败:', error);
} finally {
setLoading(false);
}
};
// 处理素材选择
const handleMaterialSelect = (materialId: string) => {
setSelectedMaterials(prev => {
if (prev.includes(materialId)) {
return prev.filter(id => id !== materialId);
} else {
return [...prev, materialId];
}
});
};
// 全选/取消全选
const handleSelectAll = () => {
if (selectedMaterials.length === materials.length) {
setSelectedMaterials([]);
} else {
setSelectedMaterials(materials.map(m => m.id));
}
};
// 打开水印工具对话框
const handleOpenWatermarkTool = () => {
if (selectedMaterials.length === 0) {
alert('请先选择要处理的素材');
return;
}
setShowWatermarkDialog(true);
};
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 格式化时长
const formatDuration = (seconds: number) => {
if (!seconds) return '--';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
// 获取素材的时长信息
const getMaterialDuration = (material: Material): number | null => {
if (material.metadata === 'None') return null;
if ('Video' in material.metadata) return material.metadata.Video.duration;
if ('Audio' in material.metadata) return material.metadata.Audio.duration;
return null;
};
// 获取素材的尺寸信息
const getMaterialDimensions = (material: Material): { width: number; height: number } | null => {
if (material.metadata === 'None') return null;
if ('Video' in material.metadata) {
return { width: material.metadata.Video.width, height: material.metadata.Video.height };
}
if ('Image' in material.metadata) {
return { width: material.metadata.Image.width, height: material.metadata.Image.height };
}
return null;
};
return (
<div className="space-y-6">
{/* 页面头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/tools')}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<Droplets className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleOpenWatermarkTool}
disabled={selectedMaterials.length === 0}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
{/* 操作栏 */}
<div className="card p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedMaterials.length === materials.length && materials.length > 0}
onChange={handleSelectAll}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium">
({selectedMaterials.length}/{materials.length})
</span>
</label>
</div>
<div className="text-sm text-gray-600">
{materials.length}
</div>
</div>
</div>
{/* 素材列表 */}
<div className="card">
{loading ? (
<div className="flex items-center justify-center py-12">
<LoadingSpinner size="large" />
</div>
) : materials.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Upload className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-600"></p>
</div>
) : (
<div className="divide-y divide-gray-100">
{materials.map((material) => (
<div
key={material.id}
className={`p-4 hover:bg-gray-50 transition-colors ${selectedMaterials.includes(material.id) ? 'bg-blue-50' : ''
}`}
>
<div className="flex items-center gap-4">
<input
type="checkbox"
checked={selectedMaterials.includes(material.id)}
onChange={() => handleMaterialSelect(material.id)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-medium text-gray-900 truncate">{material.name}</h3>
</div>
<div className="flex items-center gap-6 text-sm text-gray-600">
<span>: {material.material_type}</span>
{material.file_size && (
<span>: {formatFileSize(material.file_size)}</span>
)}
{(() => {
const duration = getMaterialDuration(material);
return duration && (
<span>: {formatDuration(duration)}</span>
);
})()}
{(() => {
const dimensions = getMaterialDimensions(material);
return dimensions && (
<span>: {dimensions.width}×{dimensions.height}</span>
);
})()}
</div>
<div className="mt-1 text-xs text-gray-500 truncate">
{material.original_path}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 水印工具对话框 */}
{showWatermarkDialog && (
<WatermarkToolDialog
isOpen={showWatermarkDialog}
onClose={() => setShowWatermarkDialog(false)}
selectedMaterials={selectedMaterials.map(id => materials.find(m => m.id === id)!).filter(Boolean)}
/>
)}
</div>
);
};
export default WatermarkTool;

View File

@@ -0,0 +1,322 @@
/**
* 水印相关类型定义
* 与Rust后端的数据模型保持一致
*/
export interface WatermarkTemplate {
id: string;
name: string;
file_path: string;
thumbnail_path?: string;
category: WatermarkCategory;
watermark_type: WatermarkType;
file_size: number;
width?: number;
height?: number;
description?: string;
tags: string[];
is_active: boolean;
created_at: string;
updated_at: string;
}
export type WatermarkType = 'Image' | 'Vector' | 'Text' | 'Animated';
export type WatermarkCategory = 'Logo' | 'Copyright' | 'Signature' | 'Decoration' | 'Custom';
export interface WatermarkDetectionResult {
id: string;
material_id: string;
detection_method: DetectionMethod;
detections: WatermarkDetection[];
confidence_score: number;
processing_time_ms: number;
created_at: string;
}
export interface WatermarkDetection {
region: BoundingBox;
confidence: number;
watermark_type?: WatermarkType;
template_id?: string;
description?: string;
}
export interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}
export type DetectionMethod =
| 'TemplateMatching'
| 'EdgeDetection'
| 'FrequencyAnalysis'
| 'TransparencyDetection'
| 'Combined';
export interface WatermarkConfig {
watermark_type: WatermarkType;
position: WatermarkPosition;
opacity: number; // 0.0-1.0
scale: number;
rotation: number; // 角度
animation?: WatermarkAnimation;
blend_mode: BlendMode;
quality_level: QualityLevel;
}
export type WatermarkPosition =
| 'TopLeft'
| 'TopCenter'
| 'TopRight'
| 'MiddleLeft'
| 'Center'
| 'MiddleRight'
| 'BottomLeft'
| 'BottomCenter'
| 'BottomRight'
| { Custom: { x: number; y: number } }
| { Dynamic: DynamicPositionRule };
export interface DynamicPositionRule {
avoid_faces: boolean;
avoid_text: boolean;
follow_motion: boolean;
corner_preference: Corner[];
min_distance_from_edge: number;
}
export type Corner = 'TopLeft' | 'TopRight' | 'BottomLeft' | 'BottomRight';
export interface WatermarkAnimation {
animation_type: AnimationType;
duration_ms: number;
loop_count?: number; // undefined表示无限循环
easing: EasingFunction;
}
export type AnimationType =
| 'FadeIn'
| 'FadeOut'
| 'SlideIn'
| 'SlideOut'
| 'Rotate'
| 'Scale'
| 'Pulse'
| { Custom: string };
export type EasingFunction =
| 'Linear'
| 'EaseIn'
| 'EaseOut'
| 'EaseInOut'
| 'Bounce'
| 'Elastic';
export type BlendMode =
| 'Normal'
| 'Multiply'
| 'Screen'
| 'Overlay'
| 'SoftLight'
| 'HardLight'
| 'ColorDodge'
| 'ColorBurn';
export type QualityLevel = 'Low' | 'Medium' | 'High' | 'Lossless';
export interface WatermarkRemovalConfig {
method: RemovalMethod;
quality_level: QualityLevel;
preserve_aspect_ratio: boolean;
target_regions?: BoundingBox[];
inpainting_model?: string;
blur_radius?: number;
crop_margin?: number;
}
export type RemovalMethod =
| 'Inpainting'
| 'Blurring'
| 'Cropping'
| 'Masking'
| 'ContentAware'
| 'Clone';
export interface WatermarkDetectionConfig {
similarity_threshold: number; // 0.0-1.0
min_watermark_size: [number, number];
max_watermark_size: [number, number];
detection_regions: DetectionRegion[];
frame_sample_rate: number;
methods: DetectionMethod[];
template_ids?: string[];
}
export type DetectionRegion =
| 'FullFrame'
| 'Corners'
| 'Edges'
| 'Center'
| { Custom: BoundingBox };
export interface BatchWatermarkTask {
task_id: string;
operation: WatermarkOperation;
material_ids: string[];
config: any; // 动态配置根据operation类型解析
status: BatchTaskStatus;
progress: BatchProgress;
created_at: string;
started_at?: string;
completed_at?: string;
}
export type WatermarkOperation =
| 'Detect'
| 'Remove'
| 'Add'
| 'DetectAndRemove'
| 'Replace';
export type BatchTaskStatus =
| 'Pending'
| 'Running'
| 'Completed'
| 'Failed'
| 'Cancelled'
| 'Paused';
export interface BatchProgress {
total_items: number;
processed_items: number;
failed_items: number;
current_item?: string;
progress_percentage: number; // 0.0-100.0
estimated_remaining_ms?: number;
errors: string[];
detection_results: WatermarkDetectionResult[];
processing_results: WatermarkProcessingResult[];
}
export interface WatermarkProcessingResult {
material_id: string;
operation: WatermarkOperation;
success: boolean;
output_path?: string;
processing_time_ms: number;
error_message?: string;
metadata?: any;
}
// 前端专用的扩展类型
export interface WatermarkToolState {
activeTab: 'detect' | 'remove' | 'add';
isLoading: boolean;
selectedMaterials: string[];
selectedTemplate?: string;
detectionConfig: WatermarkDetectionConfig;
removalConfig: WatermarkRemovalConfig;
additionConfig: WatermarkConfig;
batchTask?: BatchWatermarkTask;
}
export interface WatermarkTemplateUpload {
name: string;
file: File;
category: WatermarkCategory;
watermark_type: WatermarkType;
description?: string;
tags: string[];
}
export interface WatermarkDetectionResultDisplay extends WatermarkDetectionResult {
material_name?: string;
material_thumbnail?: string;
detection_preview?: string; // 检测结果预览图
}
// 水印处理事件类型
export interface WatermarkProcessingEvent {
type: 'task_started' | 'task_progress' | 'task_completed' | 'task_failed' | 'task_cancelled';
task_id: string;
data?: any;
}
// 水印模板管理相关
export interface WatermarkTemplateFilter {
category?: WatermarkCategory;
watermark_type?: WatermarkType;
search_text?: string;
tags?: string[];
is_active?: boolean;
}
export interface WatermarkTemplateStats {
total_templates: number;
by_category: Record<WatermarkCategory, number>;
by_type: Record<WatermarkType, number>;
total_size: number; // 总文件大小(字节)
}
// 水印检测统计
export interface WatermarkDetectionStats {
total_detections: number;
by_method: Record<DetectionMethod, number>;
average_confidence: number;
processing_time_stats: {
min: number;
max: number;
average: number;
};
}
// 批量处理统计
export interface BatchProcessingStats {
total_tasks: number;
by_operation: Record<WatermarkOperation, number>;
by_status: Record<BatchTaskStatus, number>;
success_rate: number;
average_processing_time: number;
}
// 水印处理历史记录
export interface WatermarkProcessingHistory {
id: string;
material_id: string;
operation: WatermarkOperation;
config: any;
result: WatermarkProcessingResult;
created_at: string;
}
// 水印预设配置
export interface WatermarkPreset {
id: string;
name: string;
description?: string;
operation: WatermarkOperation;
config: WatermarkConfig | WatermarkRemovalConfig | WatermarkDetectionConfig;
is_default: boolean;
created_at: string;
updated_at: string;
}
// 导出相关类型
export interface WatermarkExportOptions {
include_original: boolean;
include_processed: boolean;
export_format: 'zip' | 'folder';
quality_level: QualityLevel;
naming_pattern: string;
}
export interface WatermarkExportResult {
export_id: string;
file_path: string;
file_size: number;
exported_count: number;
created_at: string;
}

View File

@@ -0,0 +1,423 @@
# 批量水印处理工具 - 技术文档
## 概述
批量水印处理工具是 MixVideo Desktop 应用的核心功能模块,提供智能水印检测、移除和批量添加功能。该工具集成到现有的视频处理流水线中,支持多种水印类型和处理算法。
## 功能特性
### 1. 水印检测
- **多算法支持**: 模板匹配、边缘检测、频域分析、透明度检测
- **智能采样**: 基于帧采样率的高效检测
- **区域检测**: 支持全帧、四角、边缘、中心等检测区域
- **置信度评分**: 提供检测结果的可信度评估
### 2. 水印移除
- **多种方法**: AI修复、模糊处理、裁剪移除、遮罩覆盖、内容感知填充、克隆修复
- **质量控制**: 支持低、中、高、无损四种质量级别
- **区域指定**: 可指定特定区域进行水印移除
- **批量处理**: 支持大规模视频文件的批量处理
### 3. 水印添加
- **多种类型**: 图片水印、矢量水印、文字水印、动态水印
- **灵活定位**: 9个预设位置 + 自定义位置 + 动态位置
- **丰富效果**: 透明度、缩放、旋转、动画、混合模式
- **智能避让**: 可避开人脸、文字等重要内容
### 4. 模板管理
- **分类管理**: Logo、版权、签名、装饰、自定义分类
- **格式支持**: PNG、JPG、SVG、GIF等多种格式
- **缩略图生成**: 自动生成预览缩略图
- **导入导出**: 支持模板的导入导出功能
## 技术架构
### 后端架构 (Rust)
```
apps/desktop/src-tauri/src/
├── data/
│ ├── models/watermark.rs # 数据模型定义
│ └── repositories/
│ └── watermark_template_repository.rs # 数据访问层
├── business/
│ ├── services/
│ │ ├── watermark_detection_service.rs # 检测服务
│ │ ├── watermark_removal_service.rs # 移除服务
│ │ ├── watermark_addition_service.rs # 添加服务
│ │ ├── watermark_template_service.rs # 模板管理服务
│ │ └── batch_watermark_processor.rs # 批量处理器
│ └── errors/watermark_errors.rs # 错误处理
└── presentation/
└── commands/watermark_commands.rs # Tauri命令接口
```
### 前端架构 (React + TypeScript)
```
apps/desktop/src/
├── components/
│ └── WatermarkToolDialog.tsx # 主界面组件
├── types/
│ └── watermark.ts # 类型定义
└── hooks/
└── useWatermarkProcessing.ts # 自定义Hook
```
### 数据库设计
```sql
-- 水印模板表
CREATE TABLE watermark_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
file_path TEXT NOT NULL,
thumbnail_path TEXT,
category TEXT NOT NULL,
watermark_type TEXT NOT NULL,
file_size INTEGER NOT NULL,
width INTEGER,
height INTEGER,
description TEXT,
tags TEXT,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 水印检测结果表
CREATE TABLE watermark_detection_results (
id TEXT PRIMARY KEY,
material_id TEXT NOT NULL,
detection_method TEXT NOT NULL,
detections TEXT NOT NULL,
confidence_score REAL NOT NULL,
processing_time_ms INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (material_id) REFERENCES materials(id)
);
-- 批量处理任务表
CREATE TABLE batch_watermark_tasks (
task_id TEXT PRIMARY KEY,
operation TEXT NOT NULL,
material_ids TEXT NOT NULL,
config TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'Pending',
progress TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
completed_at DATETIME
);
-- 处理结果表
CREATE TABLE watermark_processing_results (
id TEXT PRIMARY KEY,
material_id TEXT NOT NULL,
operation TEXT NOT NULL,
success INTEGER NOT NULL,
output_path TEXT,
processing_time_ms INTEGER NOT NULL,
error_message TEXT,
metadata TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (material_id) REFERENCES materials(id)
);
```
## API 接口
### Tauri 命令
#### 水印检测
```rust
#[tauri::command]
pub async fn detect_watermarks_in_video(
state: State<'_, AppState>,
material_id: String,
video_path: String,
config: WatermarkDetectionConfig,
) -> Result<WatermarkDetectionResult, String>
#[tauri::command]
pub async fn detect_watermarks_in_image(
material_id: String,
image_path: String,
config: WatermarkDetectionConfig,
) -> Result<WatermarkDetectionResult, String>
```
#### 水印移除
```rust
#[tauri::command]
pub async fn remove_watermarks_from_video(
state: State<'_, AppState>,
material_id: String,
input_path: String,
output_path: String,
config: WatermarkRemovalConfig,
) -> Result<WatermarkProcessingResult, String>
```
#### 水印添加
```rust
#[tauri::command]
pub async fn add_watermark_to_video(
state: State<'_, AppState>,
material_id: String,
input_path: String,
output_path: String,
watermark_path: String,
config: WatermarkConfig,
) -> Result<WatermarkProcessingResult, String>
```
#### 批量处理
```rust
#[tauri::command]
pub async fn start_batch_watermark_task(
state: State<'_, AppState>,
operation: WatermarkOperation,
material_ids: Vec<String>,
config: serde_json::Value,
) -> Result<String, String>
```
#### 模板管理
```rust
#[tauri::command]
pub async fn get_watermark_templates(
category: Option<String>,
watermark_type: Option<String>,
) -> Result<Vec<WatermarkTemplate>, String>
#[tauri::command]
pub async fn upload_watermark_template(
name: String,
file_path: String,
category: String,
watermark_type: String,
description: Option<String>,
tags: Vec<String>,
) -> Result<WatermarkTemplate, String>
```
## 配置参数
### 检测配置 (WatermarkDetectionConfig)
```typescript
interface WatermarkDetectionConfig {
similarity_threshold: number; // 相似度阈值 0.0-1.0
min_watermark_size: [number, number]; // 最小水印尺寸
max_watermark_size: [number, number]; // 最大水印尺寸
detection_regions: DetectionRegion[]; // 检测区域
frame_sample_rate: number; // 帧采样率
methods: DetectionMethod[]; // 检测方法
template_ids?: string[]; // 指定模板ID
}
```
### 移除配置 (WatermarkRemovalConfig)
```typescript
interface WatermarkRemovalConfig {
method: RemovalMethod; // 移除方法
quality_level: QualityLevel; // 质量级别
preserve_aspect_ratio: boolean; // 保持宽高比
target_regions?: BoundingBox[]; // 目标区域
blur_radius?: number; // 模糊半径
crop_margin?: number; // 裁剪边距
}
```
### 添加配置 (WatermarkConfig)
```typescript
interface WatermarkConfig {
watermark_type: WatermarkType; // 水印类型
position: WatermarkPosition; // 位置
opacity: number; // 透明度 0.0-1.0
scale: number; // 缩放比例
rotation: number; // 旋转角度
animation?: WatermarkAnimation; // 动画配置
blend_mode: BlendMode; // 混合模式
quality_level: QualityLevel; // 质量级别
}
```
## 性能优化
### 1. 并行处理
- 使用 Rayon 并行处理多个视频文件
- 异步任务队列管理批量处理
- 智能任务调度和资源分配
### 2. 内存优化
- 分块处理大视频文件
- 及时释放临时资源
- 缓存机制减少重复计算
### 3. 算法优化
- 智能帧采样减少计算量
- 多级检测策略提高效率
- 预处理优化提升检测精度
### 4. 缓存策略
- 检测结果缓存
- 水印模板缓存
- 缩略图缓存
## 错误处理
### 错误类型
```rust
pub enum WatermarkError {
DetectionFailed { message: String },
RemovalFailed { message: String },
AdditionFailed { message: String },
UnsupportedFormat { format: String, supported: Vec<String> },
FFmpegError { message: String },
TemplateNotFound { template_id: String },
BatchTaskFailed { task_id: String, message: String },
// ... 更多错误类型
}
```
### 错误分类
- **用户错误**: 输入验证、格式不支持、模板不存在等
- **系统错误**: FFmpeg执行失败、文件操作失败、数据库错误等
- **网络错误**: 连接超时、请求失败等(可重试)
### 错误恢复
- 自动重试机制
- 优雅降级处理
- 详细错误日志记录
## 测试策略
### 单元测试
- 数据模型验证
- 业务逻辑测试
- 错误处理测试
- 配置验证测试
### 集成测试
- 端到端处理流程
- 数据库操作测试
- 文件系统交互测试
### 性能测试
- 大批量文件处理
- 内存使用监控
- 处理时间基准测试
## 部署和配置
### 依赖要求
- FFmpeg (视频处理)
- SQLite (数据存储)
- OpenCV (图像处理,可选)
### 配置文件
```toml
[watermark]
# 默认检测配置
default_similarity_threshold = 0.8
default_frame_sample_rate = 30
# 性能配置
max_concurrent_tasks = 4
max_memory_usage_mb = 2048
# 文件路径配置
template_directory = "watermarks/templates"
cache_directory = "watermarks/cache"
temp_directory = "watermarks/temp"
```
### 目录结构
```
watermarks/
├── templates/ # 水印模板存储
│ ├── {template_id}/
│ │ ├── template.png
│ │ └── thumbnail.jpg
├── cache/ # 缓存文件
└── temp/ # 临时文件
```
## 使用示例
### 前端调用示例
```typescript
// 检测水印
const detectionResult = await invoke('detect_watermarks_in_video', {
materialId: 'material_123',
videoPath: '/path/to/video.mp4',
config: {
similarity_threshold: 0.8,
frame_sample_rate: 30,
methods: ['TemplateMatching', 'EdgeDetection']
}
});
// 批量移除水印
const taskId = await invoke('start_batch_watermark_task', {
operation: 'Remove',
materialIds: ['material_1', 'material_2'],
config: {
method: 'Blurring',
quality_level: 'Medium',
blur_radius: 5.0
}
});
```
## 扩展性
### 新增检测算法
1.`DetectionMethod` 枚举中添加新方法
2.`WatermarkDetectionService` 中实现算法
3. 更新配置和文档
### 新增移除方法
1.`RemovalMethod` 枚举中添加新方法
2.`WatermarkRemovalService` 中实现方法
3. 添加相应的配置参数
### 新增水印类型
1.`WatermarkType` 枚举中添加类型
2. 更新模板验证逻辑
3. 实现对应的处理逻辑
## 维护和监控
### 日志记录
- 结构化日志输出
- 性能指标记录
- 错误详情追踪
### 监控指标
- 处理成功率
- 平均处理时间
- 内存使用情况
- 错误频率统计
### 故障排查
- 详细的错误上下文
- 处理步骤追踪
- 性能瓶颈分析
## 版本历史
### v1.0.0 (当前版本)
- 基础水印检测、移除、添加功能
- 模板管理系统
- 批量处理支持
- 错误处理和日志系统
- 单元测试和集成测试
### 未来规划
- AI增强的水印检测算法
- 更多水印类型支持
- 云端处理能力
- 实时预览功能
- 性能优化和算法改进

View File

@@ -0,0 +1,284 @@
# 批量水印处理工具 - 用户使用指南
## 功能概述
批量水印处理工具是 MixVideo Desktop 的核心功能之一,帮助您快速处理视频和图片中的水印。支持三大核心功能:
- **🔍 水印检测**: 智能识别视频/图片中的水印位置和类型
- **🗑️ 水印移除**: 使用多种算法移除不需要的水印
- ** 水印添加**: 为内容添加自定义水印保护版权
## 快速开始
### 1. 打开水印工具
1. 在项目详情页面选择需要处理的素材
2. 点击工具栏中的"水印处理"按钮
3. 水印处理对话框将会打开
### 2. 选择处理模式
工具提供三个标签页,对应不同的处理模式:
- **检测水印**: 分析素材中的水印
- **移除水印**: 去除现有水印
- **添加水印**: 添加新的水印
## 详细功能说明
### 🔍 水印检测
#### 功能说明
水印检测功能可以自动识别视频或图片中的水印,并提供详细的检测报告。
#### 配置选项
**相似度阈值** (0.1 - 1.0)
- 控制检测的敏感度
- 数值越高,检测越严格
- 建议值0.8
**帧采样率** (1 - 60)
- 对于视频,每隔多少帧检测一次
- 数值越小,检测越精确,但处理时间更长
- 建议值30
**检测方法**
-**模板匹配**: 基于已知水印模板进行匹配
-**边缘检测**: 检测重复出现的边缘特征
-**频域分析**: 分析图像频域中的周期性模式
-**透明度检测**: 识别半透明叠加层
#### 使用步骤
1. 选择"检测水印"标签页
2. 调整检测参数(使用默认值即可)
3. 点击"开始检测水印"按钮
4. 等待检测完成,查看结果报告
#### 检测结果
检测完成后,您将看到:
- 检测到的水印数量
- 每个水印的位置和置信度
- 水印类型(如果能识别)
- 处理时间统计
### 🗑️ 水印移除
#### 功能说明
水印移除功能提供多种算法来去除不需要的水印,保持视频/图片的整体质量。
#### 移除方法
**AI修复** (推荐)
- 使用人工智能算法智能填充水印区域
- 效果最佳,但处理时间较长
- 适用于复杂背景的水印
**模糊处理**
- 对水印区域进行模糊处理
- 处理速度快,适用于简单场景
- 可调节模糊半径 (1-20)
**裁剪移除**
- 通过裁剪去除边缘水印
- 会改变视频/图片尺寸
- 可设置裁剪边距 (像素)
**遮罩覆盖**
- 使用纯色或图案覆盖水印
- 适用于固定位置的水印
- 处理速度最快
**内容感知填充**
- 分析周围内容智能填充
- 效果自然,适用于纹理背景
**克隆修复**
- 使用周围相似区域修复水印
- 适用于重复纹理背景
#### 质量级别
- **低质量(快速)**: 优先处理速度
- **中等质量**: 平衡质量和速度
- **高质量**: 优先输出质量
- **无损质量**: 最高质量,处理时间最长
#### 使用步骤
1. 选择"移除水印"标签页
2. 选择移除方法和质量级别
3. 如果选择模糊处理,调整模糊半径
4. 点击"开始移除水印"按钮
5. 等待处理完成
### 水印添加
#### 功能说明
为您的内容添加自定义水印,保护版权或添加品牌标识。
#### 水印模板管理
**上传新模板**
1. 点击"上传新模板"按钮
2. 选择水印文件(支持 PNG、JPG、SVG、GIF
3. 填写模板信息:
- 模板名称
- 分类Logo、版权、签名、装饰、自定义
- 描述和标签
4. 确认上传
**模板分类**
- **Logo**: 公司或品牌标识
- **版权**: 版权声明文字或图标
- **签名**: 个人签名或标识
- **装饰**: 装饰性图案
- **自定义**: 其他类型水印
#### 水印配置
**位置设置**
- 9个预设位置左上、顶部居中、右上、左侧居中、居中、右侧居中、左下、底部居中、右下
- 自定义位置:手动指定坐标
- 动态位置:智能避开人脸和文字
**外观设置**
- **透明度** (10% - 100%): 控制水印的透明程度
- **缩放比例** (0.1x - 3.0x): 调整水印大小
- **旋转角度** (-180° - 180°): 旋转水印角度
**高级设置**
- **混合模式**: 正常、叠加、柔光等
- **动画效果**: 淡入、滑入、旋转等(仅视频)
- **质量级别**: 控制输出质量
#### 使用步骤
1. 选择"添加水印"标签页
2. 从模板库中选择水印模板
3. 调整位置、透明度、缩放等参数
4. 预览效果(如果支持)
5. 点击"开始添加水印"按钮
6. 等待处理完成
## 批量处理
### 处理进度
当启动批量处理任务后,您可以看到:
- **总体进度**: 显示处理百分比
- **处理统计**: 已处理/总数量/失败数量
- **当前项目**: 正在处理的素材名称
- **预估时间**: 剩余处理时间
- **错误信息**: 如果有处理失败的项目
### 任务管理
- **暂停/继续**: 可以暂停正在进行的任务
- **取消任务**: 停止处理并清理临时文件
- **查看详情**: 查看每个素材的处理结果
## 最佳实践
### 检测水印
1. **首次使用建议使用默认参数**,根据结果调整
2. **对于低质量视频**,适当降低相似度阈值
3. **对于大文件**,可以增加帧采样率以提高速度
4. **组合使用多种检测方法**以提高准确率
### 移除水印
1. **优先尝试AI修复**,效果通常最好
2. **对于边缘水印**,考虑使用裁剪移除
3. **对于简单背景**,模糊处理效果不错且速度快
4. **处理重要内容时选择高质量级别**
### 添加水印
1. **选择合适的位置**,避免遮挡重要内容
2. **调整透明度**,既要保护版权又不影响观看
3. **对于不同类型的内容使用不同的水印**
4. **定期备份水印模板**
## 常见问题
### Q: 检测不到水印怎么办?
A: 尝试以下方法:
- 降低相似度阈值
- 增加检测方法
- 检查水印是否在检测区域内
- 对于视频,减少帧采样率
### Q: 移除水印后效果不理想?
A: 可以尝试:
- 更换移除方法
- 提高质量级别
- 手动指定水印区域
- 使用AI修复方法
### Q: 添加的水印位置不合适?
A: 建议:
- 使用动态位置避开重要内容
- 手动调整位置坐标
- 降低透明度减少干扰
- 选择合适的混合模式
### Q: 处理速度太慢?
A: 优化建议:
- 选择较低的质量级别
- 增加帧采样率(检测时)
- 减少同时处理的文件数量
- 关闭不必要的检测方法
### Q: 批量处理中断了怎么办?
A: 处理方法:
- 重新启动任务,已处理的文件会被跳过
- 检查错误日志找出问题原因
- 单独处理失败的文件
- 确保有足够的磁盘空间
## 技术限制
### 支持的文件格式
**视频格式**
- MP4, AVI, MOV, MKV, WMV, FLV
**图片格式**
- JPG, PNG, BMP, TIFF, WebP
**水印格式**
- PNG (推荐,支持透明)
- JPG, BMP, TIFF
- SVG (矢量图)
- GIF (动态水印)
### 性能要求
- **内存**: 建议 8GB 以上
- **存储**: 处理过程中需要额外存储空间
- **CPU**: 多核处理器可提高批量处理速度
### 注意事项
1. **备份原文件**: 处理前请备份重要文件
2. **版权合规**: 确保有权处理相关内容
3. **质量损失**: 某些处理可能导致轻微质量损失
4. **处理时间**: 高质量处理需要更多时间
## 获取帮助
如果您在使用过程中遇到问题:
1. 查看本用户指南的常见问题部分
2. 检查应用程序的日志文件
3. 联系技术支持团队
4. 访问在线帮助文档
---
*本指南会随着功能更新而持续完善,建议定期查看最新版本。*