feat: 添加素材导入时的模特绑定功能
- 在 CreateMaterialRequest 中添加 model_id 字段 - 更新 Material 实体添加 new_with_model 方法支持创建时绑定模特 - 修改 MaterialService 和 AsyncMaterialService 支持导入时模特绑定 - 在 MaterialImportDialog 中添加模特选择下拉框 - 支持在导入素材时可选择绑定到特定模特 - 遵循 Tauri 开发规范的组件设计和业务逻辑分层
This commit is contained in:
parent
b86a8a5c23
commit
6f888295bb
|
|
@ -77,6 +77,7 @@ impl AsyncMaterialService {
|
||||||
let file_path_clone = file_path.clone();
|
let file_path_clone = file_path.clone();
|
||||||
let config_clone = config.clone();
|
let config_clone = config.clone();
|
||||||
let event_bus_clone = Arc::clone(&event_bus);
|
let event_bus_clone = Arc::clone(&event_bus);
|
||||||
|
let model_id_clone = request.model_id.clone();
|
||||||
|
|
||||||
match Self::process_single_file_async(
|
match Self::process_single_file_async(
|
||||||
repository_clone,
|
repository_clone,
|
||||||
|
|
@ -84,6 +85,7 @@ impl AsyncMaterialService {
|
||||||
&file_path_clone,
|
&file_path_clone,
|
||||||
&config_clone,
|
&config_clone,
|
||||||
event_bus_clone,
|
event_bus_clone,
|
||||||
|
model_id_clone,
|
||||||
).await {
|
).await {
|
||||||
Ok(Some(material)) => {
|
Ok(Some(material)) => {
|
||||||
info!(
|
info!(
|
||||||
|
|
@ -153,6 +155,7 @@ impl AsyncMaterialService {
|
||||||
file_path: &str,
|
file_path: &str,
|
||||||
config: &MaterialProcessingConfig,
|
config: &MaterialProcessingConfig,
|
||||||
event_bus: Arc<EventBusManager>,
|
event_bus: Arc<EventBusManager>,
|
||||||
|
model_id: Option<String>,
|
||||||
) -> Result<Option<Material>> {
|
) -> Result<Option<Material>> {
|
||||||
let _timer = PERFORMANCE_MONITOR.start_operation("async_process_single_file");
|
let _timer = PERFORMANCE_MONITOR.start_operation("async_process_single_file");
|
||||||
|
|
||||||
|
|
@ -190,14 +193,15 @@ impl AsyncMaterialService {
|
||||||
// 确定素材类型
|
// 确定素材类型
|
||||||
let material_type = MaterialType::from_extension(extension);
|
let material_type = MaterialType::from_extension(extension);
|
||||||
|
|
||||||
// 创建素材对象
|
// 创建素材对象(带模特绑定)
|
||||||
let material = Material::new(
|
let material = Material::new_with_model(
|
||||||
project_id.to_string(),
|
project_id.to_string(),
|
||||||
file_name.clone(),
|
file_name.clone(),
|
||||||
file_path.to_string(),
|
file_path.to_string(),
|
||||||
file_size,
|
file_size,
|
||||||
md5_hash,
|
md5_hash,
|
||||||
material_type,
|
material_type,
|
||||||
|
model_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ impl MaterialService {
|
||||||
for file_path in &request.file_paths {
|
for file_path in &request.file_paths {
|
||||||
debug!(file_path = %file_path, "处理文件");
|
debug!(file_path = %file_path, "处理文件");
|
||||||
|
|
||||||
match Self::process_single_file(repository, &request.project_id, file_path, config) {
|
match Self::process_single_file(repository, &request.project_id, file_path, config, request.model_id.clone()) {
|
||||||
Ok(Some(material)) => {
|
Ok(Some(material)) => {
|
||||||
info!(
|
info!(
|
||||||
file_path = %file_path,
|
file_path = %file_path,
|
||||||
|
|
@ -98,6 +98,7 @@ impl MaterialService {
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
file_path: &str,
|
file_path: &str,
|
||||||
config: &MaterialProcessingConfig,
|
config: &MaterialProcessingConfig,
|
||||||
|
model_id: Option<String>,
|
||||||
) -> Result<Option<Material>> {
|
) -> Result<Option<Material>> {
|
||||||
let _timer = PERFORMANCE_MONITOR.start_operation("process_single_file");
|
let _timer = PERFORMANCE_MONITOR.start_operation("process_single_file");
|
||||||
|
|
||||||
|
|
@ -132,14 +133,15 @@ impl MaterialService {
|
||||||
// 确定素材类型
|
// 确定素材类型
|
||||||
let material_type = MaterialType::from_extension(extension);
|
let material_type = MaterialType::from_extension(extension);
|
||||||
|
|
||||||
// 创建素材对象
|
// 创建素材对象(带模特绑定)
|
||||||
let material = Material::new(
|
let material = Material::new_with_model(
|
||||||
project_id.to_string(),
|
project_id.to_string(),
|
||||||
file_name,
|
file_name,
|
||||||
file_path.to_string(),
|
file_path.to_string(),
|
||||||
file_size,
|
file_size,
|
||||||
md5_hash,
|
md5_hash,
|
||||||
material_type,
|
material_type,
|
||||||
|
model_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ pub struct CreateMaterialRequest {
|
||||||
pub file_paths: Vec<String>,
|
pub file_paths: Vec<String>,
|
||||||
pub auto_process: bool,
|
pub auto_process: bool,
|
||||||
pub max_segment_duration: Option<f64>, // 最大片段时长(秒)
|
pub max_segment_duration: Option<f64>, // 最大片段时长(秒)
|
||||||
|
pub model_id: Option<String>, // 可选的模特绑定ID
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 视频切分模式
|
/// 视频切分模式
|
||||||
|
|
@ -241,6 +242,37 @@ impl Material {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 创建新的素材实例(带模特绑定)
|
||||||
|
pub fn new_with_model(
|
||||||
|
project_id: String,
|
||||||
|
name: String,
|
||||||
|
original_path: String,
|
||||||
|
file_size: u64,
|
||||||
|
md5_hash: String,
|
||||||
|
material_type: MaterialType,
|
||||||
|
model_id: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
project_id,
|
||||||
|
model_id,
|
||||||
|
name,
|
||||||
|
original_path,
|
||||||
|
file_size,
|
||||||
|
md5_hash,
|
||||||
|
material_type,
|
||||||
|
processing_status: ProcessingStatus::Pending,
|
||||||
|
metadata: MaterialMetadata::None,
|
||||||
|
scene_detection: None,
|
||||||
|
segments: Vec::new(),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
processed_at: None,
|
||||||
|
error_message: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 更新处理状态
|
/// 更新处理状态
|
||||||
pub fn update_status(&mut self, status: ProcessingStatus, error_message: Option<String>) {
|
pub fn update_status(&mut self, status: ProcessingStatus, error_message: Option<String>) {
|
||||||
self.processing_status = status;
|
self.processing_status = status;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { X, Upload, FileText, AlertCircle, CheckCircle, Loader2 } from 'lucide-r
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { useMaterialStore } from '../store/materialStore';
|
import { useMaterialStore } from '../store/materialStore';
|
||||||
import { CreateMaterialRequest, MaterialImportResult } from '../types/material';
|
import { CreateMaterialRequest, MaterialImportResult } from '../types/material';
|
||||||
|
import { Model } from '../types/model';
|
||||||
|
import { MaterialModelBindingService } from '../services/materialModelBindingService';
|
||||||
|
import { CustomSelect } from './CustomSelect';
|
||||||
|
|
||||||
interface MaterialImportDialogProps {
|
interface MaterialImportDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -46,6 +49,11 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
||||||
const [recursiveScan, setRecursiveScan] = useState(true);
|
const [recursiveScan, setRecursiveScan] = useState(true);
|
||||||
const [selectedFileTypes, setSelectedFileTypes] = useState<string[]>([]);
|
const [selectedFileTypes, setSelectedFileTypes] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 模特绑定相关状态
|
||||||
|
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||||||
|
const [availableModels, setAvailableModels] = useState<Model[]>([]);
|
||||||
|
const [loadingModels, setLoadingModels] = useState(false);
|
||||||
|
|
||||||
// 检查 FFmpeg 可用性
|
// 检查 FFmpeg 可用性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
@ -58,10 +66,30 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
setStep('select');
|
setStep('select');
|
||||||
|
setSelectedModelId('');
|
||||||
clearError();
|
clearError();
|
||||||
}
|
}
|
||||||
}, [isOpen, clearError]);
|
}, [isOpen, clearError]);
|
||||||
|
|
||||||
|
// 加载可用模特
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadAvailableModels();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const loadAvailableModels = async () => {
|
||||||
|
setLoadingModels(true);
|
||||||
|
try {
|
||||||
|
const models = await MaterialModelBindingService.getAllModels();
|
||||||
|
setAvailableModels(models.filter(model => model.is_active));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载模特列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingModels(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 设置事件监听器用于接收导入进度更新
|
// 设置事件监听器用于接收导入进度更新
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
@ -179,6 +207,7 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
||||||
file_paths: selectedFiles,
|
file_paths: selectedFiles,
|
||||||
auto_process: autoProcess,
|
auto_process: autoProcess,
|
||||||
max_segment_duration: maxSegmentDuration,
|
max_segment_duration: maxSegmentDuration,
|
||||||
|
model_id: selectedModelId || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('开始异步导入:', request);
|
console.log('开始异步导入:', request);
|
||||||
|
|
@ -390,6 +419,30 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 模特绑定选择 */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="modelSelect" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
绑定模特(可选)
|
||||||
|
</label>
|
||||||
|
<CustomSelect
|
||||||
|
value={selectedModelId}
|
||||||
|
onChange={setSelectedModelId}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: '不绑定模特' },
|
||||||
|
...availableModels.map(model => ({
|
||||||
|
value: model.id,
|
||||||
|
label: model.stage_name || model.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
placeholder={loadingModels ? '加载中...' : '选择模特'}
|
||||||
|
disabled={loadingModels}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
选择模特后,导入的素材将自动绑定到该模特
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ export interface CreateMaterialRequest {
|
||||||
file_paths: string[];
|
file_paths: string[];
|
||||||
auto_process: boolean;
|
auto_process: boolean;
|
||||||
max_segment_duration?: number;
|
max_segment_duration?: number;
|
||||||
|
model_id?: string; // 可选的模特绑定ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaterialProcessingConfig {
|
export interface MaterialProcessingConfig {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue