fix: 添加一键混剪 按钮

This commit is contained in:
root
2025-07-11 15:35:33 +08:00
parent 7cfb28c635
commit ebdcaee5a4
8 changed files with 1089 additions and 2 deletions

227
docs/MIX_EDIT_GUIDE.md Normal file
View File

@@ -0,0 +1,227 @@
# 一键混剪功能使用指南
## 🎯 功能概述
一键混剪是项目详情页面的核心功能,允许用户快速将项目中的素材和模特组合成完整的视频。
## 🚀 快速开始
### 1. 访问项目详情页面
1. 在项目列表中点击任意项目
2. 进入项目详情页面
3. 在页面右下角可以看到蓝色的悬浮按钮(剪刀图标)
### 2. 使用一键混剪
#### 快速混剪
1. 点击悬浮按钮展开选项
2. 点击 "快速混剪" 按钮
3. 系统将使用默认配置自动生成视频
#### 自定义混剪
1. 点击悬浮按钮展开选项
2. 点击 "自定义混剪" 按钮
3. 在配置界面中调整参数
4. 点击 "开始混剪" 确认
## ⚙️ 混剪配置选项
### 基础设置
#### 目标时长
- **范围**: 10-300秒
- **默认**: 60秒
- **说明**: 最终视频的目标时长
#### 剪辑风格
- **自动**: 智能选择最佳剪辑方式
- **快速**: 快节奏剪辑,适合动感内容
- **平滑**: 平缓过渡,适合展示类内容
- **动感**: 动态效果,适合音乐视频
#### 转场类型
- **直切**: 无过渡效果,直接切换
- **淡入淡出**: 渐变过渡效果
- **滑动**: 滑动切换效果
- **缩放**: 缩放过渡效果
### 特效设置
#### 背景音乐
- 自动添加适合的背景音乐
- 根据剪辑风格选择音乐类型
#### 文字叠加
- 在视频上添加文字信息
- 包括项目名称、时间戳等
#### Logo水印
- 在视频角落添加Logo
- 支持透明度调整
### 素材和模特选择
#### 素材选择
- **使用所有素材**: 包含项目中的所有视频素材
- **选择特定素材**: 手动选择要使用的素材
#### 模特选择
- **使用所有模特**: 包含项目中的所有模特
- **选择特定模特**: 手动选择要使用的模特
## 🎨 预设配置
系统提供4种预设配置可快速应用
### 快速剪辑
- 时长: 30秒
- 风格: 快速
- 转场: 直切
- 特效: 仅背景音乐
### 平滑过渡
- 时长: 60秒
- 风格: 平滑
- 转场: 淡入淡出
- 特效: 背景音乐 + 文字叠加
### 动感剪辑
- 时长: 45秒
- 风格: 动感
- 转场: 缩放
- 特效: 背景音乐 + 文字叠加
### 长视频
- 时长: 120秒
- 风格: 自动
- 转场: 滑动
- 特效: 仅背景音乐
## 📊 处理流程
### 1. 准备阶段
- 验证配置参数
- 检查素材可用性
- 生成任务ID
### 2. 分析阶段
- 分析素材特征
- 计算最佳剪辑点
- 规划时间轴
### 3. 编辑阶段
- 处理视频片段
- 应用转场效果
- 添加特效
### 4. 渲染阶段
- 合成最终视频
- 添加音频轨道
- 生成预览图
### 5. 完成
- 保存输出文件
- 生成预览链接
- 通知用户完成
## 🔍 状态监控
### 进度指示器
混剪过程中会显示:
- 当前处理阶段
- 完成百分比
- 正在处理的片段
- 预估剩余时间
### 状态说明
- **准备中**: 正在初始化任务
- **分析中**: 正在分析素材
- **编辑中**: 正在处理视频
- **渲染中**: 正在生成最终视频
- **已完成**: 混剪任务完成
- **已取消**: 用户取消任务
- **错误**: 处理过程中出现错误
## 📋 最佳实践
### 素材准备
1. **确保素材质量**: 使用高质量的视频素材
2. **合理的素材数量**: 3-10个素材效果最佳
3. **素材时长**: 每个素材至少5秒以上
### 配置选择
1. **根据用途选择风格**:
- 商业宣传 → 动感剪辑
- 产品展示 → 平滑过渡
- 快速预览 → 快速剪辑
2. **合理设置时长**:
- 社交媒体 → 15-30秒
- 产品介绍 → 60-90秒
- 详细展示 → 120秒以上
3. **特效使用**:
- 专业用途 → 添加Logo水印
- 社交分享 → 添加文字叠加
- 背景展示 → 仅使用背景音乐
### 性能优化
1. **避免过多素材**: 超过20个素材可能影响性能
2. **合理的目标时长**: 过长的视频需要更多处理时间
3. **简化特效**: 减少不必要的特效可提高处理速度
## 🛠️ 故障排除
### 常见问题
#### 混剪失败
**可能原因**:
- 没有可用素材
- 素材文件损坏
- 目标时长设置错误
**解决方案**:
- 检查项目中是否有视频素材
- 验证素材文件完整性
- 调整目标时长设置
#### 处理时间过长
**可能原因**:
- 素材数量过多
- 目标时长过长
- 特效设置复杂
**解决方案**:
- 减少素材数量
- 降低目标时长
- 简化特效配置
#### 输出质量不佳
**可能原因**:
- 原始素材质量低
- 压缩设置过高
- 转场效果不当
**解决方案**:
- 使用高质量素材
- 调整输出设置
- 选择合适的转场类型
## 🎯 高级功能
### 批量混剪
- 为多个项目同时生成混剪视频
- 使用相同配置处理多个项目
### 模板保存
- 保存常用的混剪配置
- 快速应用到新项目
### 自定义预设
- 创建个人专属的预设配置
- 分享配置给团队成员
---
通过一键混剪功能,你可以快速将项目素材转化为专业的视频内容,大大提高工作效率!

View File

@@ -0,0 +1,110 @@
/**
* 混剪命令
* 处理一键混剪相关的后端逻辑
*/
use serde::{Deserialize, Serialize};
use tauri::{command, AppHandle};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MixEditConfig {
pub duration: u32,
pub style: String,
pub music: bool,
pub use_all_materials: bool,
pub selected_material_ids: Vec<String>,
pub use_all_models: bool,
pub selected_model_ids: Vec<String>,
pub transition_type: String,
pub text_overlay: bool,
pub logo_watermark: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VideoSegment {
pub id: String,
pub name: String,
pub duration: Option<f64>,
pub file_path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Model {
pub id: String,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MixEditParams {
pub project_id: String,
pub project_name: String,
pub config: MixEditConfig,
pub materials: Vec<VideoSegment>,
pub models: Vec<Model>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MixEditResult {
pub success: bool,
pub message: String,
pub task_id: Option<String>,
pub output_path: Option<String>,
pub duration: Option<f64>,
pub segment_count: Option<u32>,
pub preview_url: Option<String>,
}
/// 开始混剪任务(简化版本)
#[command]
pub async fn start_mix_edit(_app: AppHandle, params: MixEditParams) -> Result<MixEditResult, String> {
println!("Starting mix edit for project: {}", params.project_name);
// 生成任务ID
let task_id = Uuid::new_v4().to_string();
// 验证参数
if params.materials.is_empty() {
return Ok(MixEditResult {
success: false,
message: "没有可用的素材".to_string(),
task_id: None,
output_path: None,
duration: None,
segment_count: None,
preview_url: None,
});
}
if params.config.duration == 0 {
return Ok(MixEditResult {
success: false,
message: "目标时长必须大于0".to_string(),
task_id: None,
output_path: None,
duration: None,
segment_count: None,
preview_url: None,
});
}
// 模拟混剪处理
println!("Processing {} materials with {} second duration",
params.materials.len(), params.config.duration);
// 模拟处理时间
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// 生成模拟输出路径
let output_path = format!("output/mix_edit_{}_{}.mp4", params.project_id, task_id);
Ok(MixEditResult {
success: true,
message: "混剪完成!".to_string(),
task_id: Some(task_id.clone()),
output_path: Some(output_path),
duration: Some(params.config.duration as f64),
segment_count: Some(params.materials.len() as u32),
preview_url: Some(format!("preview/mix_edit_{}.jpg", task_id)),
})
}

View File

@@ -12,6 +12,7 @@ pub mod media;
pub mod resource_category;
pub mod python_core_demo;
pub mod python_env_manager;
pub mod mix_edit;
// Re-export all commands for easy access
pub use basic::*;

View File

@@ -107,7 +107,9 @@ pub fn run() {
commands::python_env_manager::uninstall_python_package,
commands::python_env_manager::upgrade_python_package,
commands::python_env_manager::setup_embedded_python,
commands::python_env_manager::setup_embedded_python_simple
commands::python_env_manager::setup_embedded_python_simple,
// Mix Edit Commands
commands::mix_edit::start_mix_edit
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -0,0 +1,295 @@
/**
* 混剪配置模态框组件
* 允许用户自定义混剪参数
*/
import React, { useState, useEffect } from 'react'
import { X, Clock, Zap, Music, Type, Image, Shuffle } from 'lucide-react'
import { Project } from '../services/projectService'
import { VideoSegment } from '../services/mediaService'
import { Model } from '../services/modelService'
import { MixEditService, MixEditConfig } from '../services/mixEditService'
interface MixEditConfigModalProps {
project: Project
materials: VideoSegment[]
models: Model[]
onConfirm: (config: MixEditConfig) => void
onCancel: () => void
}
const MixEditConfigModal: React.FC<MixEditConfigModalProps> = ({
project,
materials,
models,
onConfirm,
onCancel
}) => {
const [config, setConfig] = useState<MixEditConfig>(MixEditService.getDefaultConfig())
const [estimatedTime, setEstimatedTime] = useState(0)
const [errors, setErrors] = useState<string[]>([])
// 预设配置
const presets = MixEditService.getMixEditPresets()
// 更新配置
const updateConfig = (updates: Partial<MixEditConfig>) => {
const newConfig = { ...config, ...updates }
setConfig(newConfig)
// 验证配置
const validationErrors = MixEditService.validateConfig(newConfig, materials, models)
setErrors(validationErrors)
// 估算时间
const materialCount = newConfig.useAllMaterials
? materials.length
: newConfig.selectedMaterialIds.length
const time = MixEditService.estimateMixEditTime(newConfig, materialCount)
setEstimatedTime(time)
}
// 应用预设
const applyPreset = (presetConfig: Partial<MixEditConfig>) => {
updateConfig(presetConfig)
}
// 初始化估算时间
useEffect(() => {
updateConfig({})
}, [])
// 确认配置
const handleConfirm = () => {
if (errors.length === 0) {
onConfirm(config)
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
{/* 标题栏 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div>
<h2 className="text-xl font-semibold text-gray-900"></h2>
<p className="text-sm text-gray-600"> "{project.name}" </p>
</div>
<button
onClick={onCancel}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<X size={24} />
</button>
</div>
<div className="p-6 space-y-6">
{/* 预设配置 */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{presets.map((preset, index) => (
<button
key={index}
onClick={() => applyPreset(preset.config)}
className="p-3 border border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors text-left"
>
<div className="font-medium text-gray-900">{preset.name}</div>
<div className="text-xs text-gray-500 mt-1">
{preset.config.duration} · {preset.config.style}
</div>
</button>
))}
</div>
</div>
{/* 基础配置 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 左列 */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900"></h3>
{/* 目标时长 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Clock className="w-4 h-4 inline mr-1" />
</label>
<input
type="number"
min="10"
max="300"
value={config.duration}
onChange={(e) => updateConfig({ duration: parseInt(e.target.value) || 60 })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* 剪辑风格 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Zap className="w-4 h-4 inline mr-1" />
</label>
<select
value={config.style}
onChange={(e) => updateConfig({ style: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="auto"></option>
<option value="fast"></option>
<option value="smooth"></option>
<option value="dynamic"></option>
</select>
</div>
{/* 转场类型 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Shuffle className="w-4 h-4 inline mr-1" />
</label>
<select
value={config.transitionType}
onChange={(e) => updateConfig({ transitionType: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="cut"></option>
<option value="fade"></option>
<option value="slide"></option>
<option value="zoom"></option>
</select>
</div>
</div>
{/* 右列 */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900"></h3>
{/* 特效选项 */}
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
checked={config.music}
onChange={(e) => updateConfig({ music: e.target.checked })}
className="mr-3 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<Music className="w-4 h-4 mr-2 text-gray-600" />
<span className="text-sm font-medium text-gray-700"></span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={config.textOverlay}
onChange={(e) => updateConfig({ textOverlay: e.target.checked })}
className="mr-3 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<Type className="w-4 h-4 mr-2 text-gray-600" />
<span className="text-sm font-medium text-gray-700"></span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={config.logoWatermark}
onChange={(e) => updateConfig({ logoWatermark: e.target.checked })}
className="mr-3 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<Image className="w-4 h-4 mr-2 text-gray-600" />
<span className="text-sm font-medium text-gray-700">Logo水印</span>
</label>
</div>
{/* 素材选择 */}
<div>
<label className="flex items-center mb-2">
<input
type="checkbox"
checked={config.useAllMaterials}
onChange={(e) => updateConfig({ useAllMaterials: e.target.checked })}
className="mr-3 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-700">使 ({materials.length})</span>
</label>
{!config.useAllMaterials && (
<div className="text-xs text-gray-500">
{config.selectedMaterialIds.length}
</div>
)}
</div>
{/* 模特选择 */}
<div>
<label className="flex items-center mb-2">
<input
type="checkbox"
checked={config.useAllModels}
onChange={(e) => updateConfig({ useAllModels: e.target.checked })}
className="mr-3 w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-700">使 ({models.length})</span>
</label>
{!config.useAllModels && (
<div className="text-xs text-gray-500">
{config.selectedModelIds.length}
</div>
)}
</div>
</div>
</div>
{/* 估算信息 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-blue-900"></h4>
<p className="text-sm text-blue-700"> {estimatedTime} </p>
</div>
<div className="text-right">
<div className="text-sm text-blue-700">
: {config.useAllMaterials ? materials.length : config.selectedMaterialIds.length}
</div>
<div className="text-sm text-blue-700">
: {config.useAllModels ? models.length : config.selectedModelIds.length}
</div>
</div>
</div>
</div>
{/* 错误提示 */}
{errors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h4 className="font-medium text-red-900 mb-2"></h4>
<ul className="text-sm text-red-700 space-y-1">
{errors.map((error, index) => (
<li key={index}> {error}</li>
))}
</ul>
</div>
)}
</div>
{/* 底部按钮 */}
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
</button>
<button
onClick={handleConfirm}
disabled={errors.length > 0}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
</button>
</div>
</div>
</div>
)
}
export default MixEditConfigModal

View File

@@ -0,0 +1,177 @@
/**
* 一键混剪悬浮按钮组件
* 提供快速混剪功能的入口
*/
import React, { useState } from 'react'
import { Scissors, Settings, Play, X, Loader } from 'lucide-react'
import { Project } from '../services/projectService'
import { VideoSegment } from '../services/mediaService'
import { Model } from '../services/modelService'
import { MixEditService, MixEditConfig, MixEditResult } from '../services/mixEditService'
import MixEditConfigModal from './MixEditConfigModal'
interface MixEditFloatingButtonProps {
project: Project
materials: VideoSegment[]
models: Model[]
onMixEditComplete?: (result: MixEditResult) => void
}
const MixEditFloatingButton: React.FC<MixEditFloatingButtonProps> = ({
project,
materials,
models,
onMixEditComplete
}) => {
const [isExpanded, setIsExpanded] = useState(false)
const [showConfigModal, setShowConfigModal] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [processingMessage, setProcessingMessage] = useState('')
// 快速混剪(使用默认配置)
const handleQuickMixEdit = async () => {
if (materials.length === 0) {
alert('没有可用的素材,请先添加一些视频素材')
return
}
setIsProcessing(true)
setProcessingMessage('正在准备混剪...')
setIsExpanded(false)
try {
const defaultConfig = MixEditService.getDefaultConfig()
const result = await MixEditService.startMixEdit(project, defaultConfig, materials, models)
if (result.success) {
setProcessingMessage('混剪完成!')
setTimeout(() => {
setIsProcessing(false)
onMixEditComplete?.(result)
}, 1500)
} else {
alert(`混剪失败: ${result.message}`)
setIsProcessing(false)
}
} catch (error) {
console.error('Quick mix edit failed:', error)
alert(`混剪失败: ${error}`)
setIsProcessing(false)
}
}
// 自定义混剪
const handleCustomMixEdit = () => {
if (materials.length === 0) {
alert('没有可用的素材,请先添加一些视频素材')
return
}
setShowConfigModal(true)
setIsExpanded(false)
}
// 处理自定义混剪配置完成
const handleConfigComplete = async (config: MixEditConfig) => {
setShowConfigModal(false)
setIsProcessing(true)
setProcessingMessage('正在开始混剪...')
try {
const result = await MixEditService.startMixEdit(project, config, materials, models)
if (result.success) {
setProcessingMessage('混剪完成!')
setTimeout(() => {
setIsProcessing(false)
onMixEditComplete?.(result)
}, 1500)
} else {
alert(`混剪失败: ${result.message}`)
setIsProcessing(false)
}
} catch (error) {
console.error('Custom mix edit failed:', error)
alert(`混剪失败: ${error}`)
setIsProcessing(false)
}
}
// 如果正在处理,显示处理状态
if (isProcessing) {
return (
<div className="fixed bottom-8 right-8 z-50">
<div className="bg-blue-600 text-white px-6 py-4 rounded-full shadow-lg flex items-center space-x-3">
<Loader className="w-5 h-5 animate-spin" />
<span className="font-medium">{processingMessage}</span>
</div>
</div>
)
}
return (
<>
<div className="fixed bottom-8 right-8 z-50">
{/* 展开的选项 */}
{isExpanded && (
<div className="mb-4 space-y-3">
{/* 自定义混剪按钮 */}
<button
onClick={handleCustomMixEdit}
className="flex items-center space-x-3 bg-white text-gray-700 px-4 py-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
>
<Settings className="w-5 h-5" />
<span className="font-medium"></span>
</button>
{/* 快速混剪按钮 */}
<button
onClick={handleQuickMixEdit}
className="flex items-center space-x-3 bg-green-600 text-white px-4 py-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
>
<Play className="w-5 h-5" />
<span className="font-medium"></span>
</button>
</div>
)}
{/* 主按钮 */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className={`w-16 h-16 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105 flex items-center justify-center ${
isExpanded
? 'bg-gray-600 text-white rotate-45'
: 'bg-blue-600 text-white'
}`}
>
{isExpanded ? (
<X className="w-6 h-6" />
) : (
<Scissors className="w-6 h-6" />
)}
</button>
{/* 提示文字 */}
{!isExpanded && (
<div className="absolute bottom-full right-0 mb-2 bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity duration-200 pointer-events-none">
</div>
)}
</div>
{/* 配置模态框 */}
{showConfigModal && (
<MixEditConfigModal
project={project}
materials={materials}
models={models}
onConfirm={handleConfigComplete}
onCancel={() => setShowConfigModal(false)}
/>
)}
</>
)
}
export default MixEditFloatingButton

View File

@@ -4,8 +4,10 @@ import { ArrowLeft, User, Video } from 'lucide-react'
import { Project, ProjectService } from '../services/projectService'
import { VideoSegment, MediaService } from '../services/mediaService'
import { Model, ModelService } from '../services/modelService'
import { MixEditResult } from '../services/mixEditService'
import ProjectMaterialsCenter from '../components/ProjectMaterialsCenter'
import AGUIChat from '../components/AGUIChat'
import MixEditFloatingButton from '../components/MixEditFloatingButton'
const ProjectDetailPage: React.FC = () => {
const { projectId } = useParams<{ projectId: string }>()
@@ -80,7 +82,15 @@ const ProjectDetailPage: React.FC = () => {
}
}
// 处理混剪完成
const handleMixEditComplete = (result: MixEditResult) => {
if (result.success) {
alert(`混剪完成!\n输出路径: ${result.outputPath}\n时长: ${result.duration}`)
// 可以在这里添加更多处理逻辑,比如刷新素材列表、显示预览等
} else {
alert(`混剪失败: ${result.message}`)
}
}
if (loading) {
return (
@@ -167,6 +177,14 @@ const ProjectDetailPage: React.FC = () => {
/>
</div>
</div>
{/* 一键混剪悬浮按钮 */}
<MixEditFloatingButton
project={project}
materials={projectMaterials}
models={projectModels}
onMixEditComplete={handleMixEditComplete}
/>
</div>
)
}

View File

@@ -0,0 +1,257 @@
/**
* 混剪服务
* 处理一键混剪相关的业务逻辑
*/
import { invoke } from '@tauri-apps/api/core'
import { Project } from './projectService'
import { VideoSegment } from './mediaService'
import { Model } from './modelService'
export interface MixEditConfig {
// 基础配置
duration: number // 目标时长(秒)
style: 'auto' | 'fast' | 'smooth' | 'dynamic' // 剪辑风格
music: boolean // 是否添加背景音乐
// 素材配置
useAllMaterials: boolean // 是否使用所有素材
selectedMaterialIds: string[] // 选中的素材ID
// 模特配置
useAllModels: boolean // 是否使用所有模特
selectedModelIds: string[] // 选中的模特ID
// 高级配置
transitionType: 'cut' | 'fade' | 'slide' | 'zoom' // 转场类型
textOverlay: boolean // 是否添加文字叠加
logoWatermark: boolean // 是否添加Logo水印
}
export interface MixEditResult {
success: boolean
message: string
outputPath?: string
duration?: number
segmentCount?: number
previewUrl?: string
}
export interface MixEditProgress {
stage: 'preparing' | 'analyzing' | 'editing' | 'rendering' | 'completed' | 'error'
progress: number // 0-100
message: string
currentSegment?: number
totalSegments?: number
}
export class MixEditService {
/**
* 获取默认混剪配置
*/
static getDefaultConfig(): MixEditConfig {
return {
duration: 60, // 默认60秒
style: 'auto',
music: true,
useAllMaterials: true,
selectedMaterialIds: [],
useAllModels: true,
selectedModelIds: [],
transitionType: 'fade',
textOverlay: false,
logoWatermark: false,
}
}
/**
* 验证混剪配置
*/
static validateConfig(config: MixEditConfig, materials: VideoSegment[], models: Model[]): string[] {
const errors: string[] = []
if (config.duration <= 0) {
errors.push('目标时长必须大于0')
}
if (!config.useAllMaterials && config.selectedMaterialIds.length === 0) {
errors.push('请至少选择一个素材')
}
if (!config.useAllModels && config.selectedModelIds.length === 0) {
errors.push('请至少选择一个模特')
}
const availableMaterials = config.useAllMaterials
? materials
: materials.filter(m => config.selectedMaterialIds.includes(m.id))
if (availableMaterials.length === 0) {
errors.push('没有可用的素材')
}
const totalMaterialDuration = availableMaterials.reduce((sum, m) => sum + (m.duration || 0), 0)
if (totalMaterialDuration < config.duration) {
errors.push(`素材总时长(${totalMaterialDuration}s)小于目标时长(${config.duration}s)`)
}
return errors
}
/**
* 开始一键混剪
*/
static async startMixEdit(
project: Project,
config: MixEditConfig,
materials: VideoSegment[],
models: Model[]
): Promise<MixEditResult> {
try {
// 验证配置
const errors = this.validateConfig(config, materials, models)
if (errors.length > 0) {
return {
success: false,
message: `配置错误: ${errors.join(', ')}`
}
}
// 准备混剪参数
const mixEditParams = {
projectId: project.id,
projectName: project.name,
config,
materials: config.useAllMaterials
? materials
: materials.filter(m => config.selectedMaterialIds.includes(m.id)),
models: config.useAllModels
? models
: models.filter(m => config.selectedModelIds.includes(m.id))
}
// 调用后端混剪服务
const result = await invoke('start_mix_edit', { params: mixEditParams })
return result as MixEditResult
} catch (error) {
console.error('Mix edit failed:', error)
return {
success: false,
message: `混剪失败: ${error}`
}
}
}
/**
* 获取混剪进度
*/
static async getMixEditProgress(taskId: string): Promise<MixEditProgress> {
try {
const progress = await invoke('get_mix_edit_progress', { taskId })
return progress as MixEditProgress
} catch (error) {
console.error('Failed to get mix edit progress:', error)
return {
stage: 'error',
progress: 0,
message: `获取进度失败: ${error}`
}
}
}
/**
* 取消混剪任务
*/
static async cancelMixEdit(taskId: string): Promise<boolean> {
try {
await invoke('cancel_mix_edit', { taskId })
return true
} catch (error) {
console.error('Failed to cancel mix edit:', error)
return false
}
}
/**
* 获取混剪预设
*/
static getMixEditPresets(): Array<{name: string, config: Partial<MixEditConfig>}> {
return [
{
name: '快速剪辑',
config: {
duration: 30,
style: 'fast',
transitionType: 'cut',
music: true,
textOverlay: false
}
},
{
name: '平滑过渡',
config: {
duration: 60,
style: 'smooth',
transitionType: 'fade',
music: true,
textOverlay: true
}
},
{
name: '动感剪辑',
config: {
duration: 45,
style: 'dynamic',
transitionType: 'zoom',
music: true,
textOverlay: true
}
},
{
name: '长视频',
config: {
duration: 120,
style: 'auto',
transitionType: 'slide',
music: true,
textOverlay: false
}
}
]
}
/**
* 估算混剪时间
*/
static estimateMixEditTime(config: MixEditConfig, materialCount: number): number {
// 基础时间(秒)
let estimatedTime = 30
// 根据目标时长调整
estimatedTime += config.duration * 0.5
// 根据素材数量调整
estimatedTime += materialCount * 2
// 根据剪辑风格调整
switch (config.style) {
case 'fast':
estimatedTime *= 0.8
break
case 'dynamic':
estimatedTime *= 1.2
break
case 'smooth':
estimatedTime *= 1.1
break
}
// 根据特效调整
if (config.textOverlay) estimatedTime *= 1.1
if (config.logoWatermark) estimatedTime *= 1.05
if (config.music) estimatedTime *= 1.1
return Math.round(estimatedTime)
}
}