feat: 实现导出到剪映功能 (v0.1.28)
- 新增剪映导出数据结构定义 (jianying_export.rs) - 实现模板匹配结果导出到剪映格式的服务逻辑 - 添加导出到剪映的Tauri命令接口 - 在匹配记录卡片中添加导出按钮 - 实现文件保存对话框和用户交互 - 支持根据模板匹配结果生成完整的draft_content.json - 自动替换匹配素材路径并生成随机素材ID - 去除无引用素材,优化导出文件大小 功能特点: - 完全符合剪映draft_content.json格式规范 - 准确的时间轴和素材路径映射 - 用户友好的导出界面和反馈 - 遵循promptx/tauri-desktop-app-expert开发规范
This commit is contained in:
511
apps/desktop/src-tauri/src/business/services/jianying_export.rs
Normal file
511
apps/desktop/src-tauri/src/business/services/jianying_export.rs
Normal file
@@ -0,0 +1,511 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
|
||||
/// 剪映草稿内容结构 - 用于导出
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingDraftContent {
|
||||
pub canvas_config: JianYingCanvasConfig,
|
||||
pub color_space: i32,
|
||||
pub config: JianYingConfig,
|
||||
pub cover: Option<String>,
|
||||
pub create_time: i64,
|
||||
pub duration: u64,
|
||||
pub extra_info: Option<String>,
|
||||
pub fps: f64,
|
||||
pub free_render_index_mode_on: bool,
|
||||
pub group_container: Option<String>,
|
||||
pub id: String,
|
||||
pub keyframe_graph_list: Vec<String>,
|
||||
pub keyframes: JianYingKeyframes,
|
||||
pub last_modified_platform: JianYingPlatform,
|
||||
pub materials: JianYingMaterials,
|
||||
pub render_index_track_mode_on: bool,
|
||||
pub retouch_cover: Option<String>,
|
||||
pub source: String,
|
||||
pub static_cover_image_path: String,
|
||||
pub time_marks: Option<String>,
|
||||
pub tracks: Vec<JianYingTrack>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingCanvasConfig {
|
||||
pub height: u32,
|
||||
pub ratio: String,
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingConfig {
|
||||
pub adjust_max_index: i32,
|
||||
pub attachment_info: Vec<String>,
|
||||
pub combination_max_index: i32,
|
||||
pub export_range: Option<String>,
|
||||
pub extract_audio_last_index: i32,
|
||||
pub lyrics_recognition_id: String,
|
||||
pub lyrics_sync: bool,
|
||||
pub lyrics_taskinfo: Vec<String>,
|
||||
pub maintrack_adsorb: bool,
|
||||
pub material_save_mode: i32,
|
||||
pub multi_language_current: String,
|
||||
pub multi_language_list: Vec<String>,
|
||||
pub multi_language_main: String,
|
||||
pub multi_language_mode: String,
|
||||
pub original_sound_last_index: i32,
|
||||
pub record_audio_last_index: i32,
|
||||
pub sticker_max_index: i32,
|
||||
pub subtitle_keywords_config: Option<String>,
|
||||
pub subtitle_recognition_id: String,
|
||||
pub subtitle_sync: bool,
|
||||
pub subtitle_taskinfo: Vec<String>,
|
||||
pub system_font_list: Vec<String>,
|
||||
pub video_mute: bool,
|
||||
pub zoom_info_params: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingKeyframes {
|
||||
pub adjusts: Vec<String>,
|
||||
pub audios: Vec<String>,
|
||||
pub effects: Vec<String>,
|
||||
pub filters: Vec<String>,
|
||||
pub handwrites: Vec<String>,
|
||||
pub stickers: Vec<String>,
|
||||
pub texts: Vec<String>,
|
||||
pub videos: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingPlatform {
|
||||
pub app_id: i32,
|
||||
pub app_source: String,
|
||||
pub app_version: String,
|
||||
pub device_id: String,
|
||||
pub hard_disk_id: String,
|
||||
pub mac_address: String,
|
||||
pub os: String,
|
||||
pub os_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingMaterials {
|
||||
pub ai_translates: Vec<String>,
|
||||
pub audio_balances: Vec<String>,
|
||||
pub audio_effects: Vec<String>,
|
||||
pub audio_fades: Vec<String>,
|
||||
pub audio_track_indexes: Vec<String>,
|
||||
pub audios: Vec<JianYingAudio>,
|
||||
pub beats: Vec<String>,
|
||||
pub canvases: Vec<String>,
|
||||
pub chromas: Vec<String>,
|
||||
pub color_curves: Vec<String>,
|
||||
pub digital_humans: Vec<String>,
|
||||
pub drafts: Vec<String>,
|
||||
pub effects: Vec<String>,
|
||||
pub flowers: Vec<String>,
|
||||
pub green_screens: Vec<String>,
|
||||
pub handwrites: Vec<String>,
|
||||
pub hsl: Vec<String>,
|
||||
pub images: Vec<String>,
|
||||
pub log_color_wheels: Vec<String>,
|
||||
pub loudnesses: Vec<String>,
|
||||
pub manual_deformations: Vec<String>,
|
||||
pub masks: Vec<String>,
|
||||
pub material_animations: Vec<String>,
|
||||
pub material_colors: Vec<String>,
|
||||
pub multi_language_refs: Vec<String>,
|
||||
pub placeholders: Vec<String>,
|
||||
pub plugin_effects: Vec<String>,
|
||||
pub primary_color_wheels: Vec<String>,
|
||||
pub realtime_denoises: Vec<String>,
|
||||
pub shapes: Vec<String>,
|
||||
pub smart_crops: Vec<String>,
|
||||
pub smart_relights: Vec<String>,
|
||||
pub sound_channel_mappings: Vec<String>,
|
||||
pub speeds: Vec<JianYingSpeed>,
|
||||
pub stickers: Vec<String>,
|
||||
pub tail_leaders: Vec<String>,
|
||||
pub text_templates: Vec<String>,
|
||||
pub texts: Vec<String>,
|
||||
pub time_marks: Vec<String>,
|
||||
pub transitions: Vec<String>,
|
||||
pub video_effects: Vec<String>,
|
||||
pub video_trackings: Vec<String>,
|
||||
pub videos: Vec<JianYingVideo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingTrack {
|
||||
pub attribute: i32,
|
||||
pub flag: i32,
|
||||
pub id: String,
|
||||
pub is_default_name: bool,
|
||||
pub name: String,
|
||||
pub segments: Vec<JianYingSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingSegment {
|
||||
pub caption_info: Option<String>,
|
||||
pub cartoon: bool,
|
||||
pub clip: JianYingClip,
|
||||
pub common_keyframes: Vec<String>,
|
||||
pub enable_adjust: bool,
|
||||
pub enable_color_correct_adjust: bool,
|
||||
pub enable_color_curves: bool,
|
||||
pub enable_color_match_adjust: bool,
|
||||
pub enable_color_wheels: bool,
|
||||
pub enable_lut: bool,
|
||||
pub enable_smart_color_adjust: bool,
|
||||
pub extra_material_refs: Vec<String>,
|
||||
pub group_id: String,
|
||||
pub hdr_settings: JianYingHdrSettings,
|
||||
pub id: String,
|
||||
pub intensifies_audio: bool,
|
||||
pub is_placeholder: bool,
|
||||
pub is_tone_modify: bool,
|
||||
pub keyframe_refs: Vec<String>,
|
||||
pub last_nonzero_volume: f64,
|
||||
pub material_id: String,
|
||||
pub render_index: i32,
|
||||
pub responsive_layout: JianYingResponsiveLayout,
|
||||
pub reverse: bool,
|
||||
pub source_timerange: JianYingTimeRange,
|
||||
pub speed: f64,
|
||||
pub target_timerange: JianYingTimeRange,
|
||||
pub template_id: String,
|
||||
pub template_scene: String,
|
||||
pub track_attribute: i32,
|
||||
pub track_render_index: i32,
|
||||
pub uniform_scale: JianYingUniformScale,
|
||||
pub visible: bool,
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingAudio {
|
||||
pub app_id: i32,
|
||||
pub category_id: String,
|
||||
pub category_name: String,
|
||||
pub check_flag: i32,
|
||||
pub copyright_limit_type: String,
|
||||
pub duration: u64,
|
||||
pub effect_id: String,
|
||||
pub formula_id: String,
|
||||
pub id: String,
|
||||
pub intensifies_path: String,
|
||||
pub is_ai_clone_tone: bool,
|
||||
pub is_text_edit_overdub: bool,
|
||||
pub is_ugc: bool,
|
||||
pub local_material_id: String,
|
||||
pub music_id: String,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub query: String,
|
||||
pub request_id: String,
|
||||
pub resource_id: String,
|
||||
pub search_id: String,
|
||||
pub source_from: String,
|
||||
pub source_platform: i32,
|
||||
pub team_id: String,
|
||||
pub text_id: String,
|
||||
pub tone_category_id: String,
|
||||
pub tone_category_name: String,
|
||||
pub tone_effect_id: String,
|
||||
pub tone_effect_name: String,
|
||||
pub tone_platform: String,
|
||||
pub tone_second_category_id: String,
|
||||
pub tone_second_category_name: String,
|
||||
pub tone_speaker: String,
|
||||
pub tone_type: String,
|
||||
#[serde(rename = "type")]
|
||||
pub material_type: String,
|
||||
pub video_id: String,
|
||||
pub wave_points: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingSpeed {
|
||||
pub curve_speed: Option<String>,
|
||||
pub id: String,
|
||||
pub mode: i32,
|
||||
pub speed: f64,
|
||||
#[serde(rename = "type")]
|
||||
pub speed_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingVideo {
|
||||
pub aigc_type: String,
|
||||
pub audio_fade: Option<String>,
|
||||
pub cartoon_path: String,
|
||||
pub category_id: String,
|
||||
pub category_name: String,
|
||||
pub check_flag: i32,
|
||||
pub crop: JianYingCrop,
|
||||
pub crop_ratio: String,
|
||||
pub crop_scale: f64,
|
||||
pub duration: u64,
|
||||
pub extra_type_option: i32,
|
||||
pub formula_id: String,
|
||||
pub freeze: Option<String>,
|
||||
pub has_audio: bool,
|
||||
pub height: u32,
|
||||
pub id: String,
|
||||
pub intensifies_audio_path: String,
|
||||
pub intensifies_path: String,
|
||||
pub is_ai_generate_content: bool,
|
||||
pub is_copyright: bool,
|
||||
pub is_text_edit_overdub: bool,
|
||||
pub is_unified_beauty_mode: bool,
|
||||
pub local_id: String,
|
||||
pub local_material_id: String,
|
||||
pub material_id: String,
|
||||
pub material_name: String,
|
||||
pub material_url: String,
|
||||
pub matting: JianYingMatting,
|
||||
pub media_path: String,
|
||||
pub object_locked: Option<String>,
|
||||
pub origin_material_id: String,
|
||||
pub path: String,
|
||||
pub picture_from: String,
|
||||
pub picture_set_category_id: String,
|
||||
pub picture_set_category_name: String,
|
||||
pub request_id: String,
|
||||
pub reverse_intensifies_path: String,
|
||||
pub reverse_path: String,
|
||||
pub smart_motion: Option<String>,
|
||||
pub source: i32,
|
||||
pub source_platform: i32,
|
||||
pub stable: JianYingStable,
|
||||
pub team_id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub video_type: String,
|
||||
pub video_algorithm: JianYingVideoAlgorithm,
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
// 辅助结构体
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingClip {
|
||||
pub alpha: f64,
|
||||
pub flip: JianYingFlip,
|
||||
pub rotation: f64,
|
||||
pub scale: JianYingScale,
|
||||
pub transform: JianYingTransform,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingFlip {
|
||||
pub horizontal: bool,
|
||||
pub vertical: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingScale {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingTransform {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingHdrSettings {
|
||||
pub intensity: f64,
|
||||
pub mode: i32,
|
||||
pub nits: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingResponsiveLayout {
|
||||
pub enable: bool,
|
||||
pub horizontal_pos_layout: i32,
|
||||
pub size_layout: i32,
|
||||
pub target_follow: String,
|
||||
pub vertical_pos_layout: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingTimeRange {
|
||||
pub duration: u64,
|
||||
pub start: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingUniformScale {
|
||||
pub on: bool,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingCrop {
|
||||
pub lower_left_x: f64,
|
||||
pub lower_left_y: f64,
|
||||
pub lower_right_x: f64,
|
||||
pub lower_right_y: f64,
|
||||
pub upper_left_x: f64,
|
||||
pub upper_left_y: f64,
|
||||
pub upper_right_x: f64,
|
||||
pub upper_right_y: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingMatting {
|
||||
pub flag: i32,
|
||||
pub has_use_quick_brush: bool,
|
||||
pub has_use_quick_eraser: bool,
|
||||
#[serde(rename = "interactiveTime")]
|
||||
pub interactive_time: Vec<String>,
|
||||
pub path: String,
|
||||
pub strokes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingStable {
|
||||
pub matrix_path: String,
|
||||
pub stable_level: i32,
|
||||
pub time_range: JianYingTimeRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JianYingVideoAlgorithm {
|
||||
pub algorithms: Vec<String>,
|
||||
pub complement_frame_config: Option<String>,
|
||||
pub deflicker: Option<String>,
|
||||
pub gameplay_configs: Vec<String>,
|
||||
pub motion_blur_config: Option<String>,
|
||||
pub noise_reduction: Option<String>,
|
||||
pub path: String,
|
||||
pub quality_enhance: Option<String>,
|
||||
pub time_range: Option<JianYingTimeRange>,
|
||||
}
|
||||
|
||||
/// 剪映导出服务
|
||||
pub struct JianYingExportService;
|
||||
|
||||
impl JianYingExportService {
|
||||
/// 创建默认的剪映草稿内容
|
||||
pub fn create_default_draft_content() -> JianYingDraftContent {
|
||||
let draft_id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
JianYingDraftContent {
|
||||
canvas_config: JianYingCanvasConfig {
|
||||
height: 1920,
|
||||
ratio: "9:16".to_string(),
|
||||
width: 1080,
|
||||
},
|
||||
color_space: 0,
|
||||
config: JianYingConfig {
|
||||
adjust_max_index: 1,
|
||||
attachment_info: vec![],
|
||||
combination_max_index: 1,
|
||||
export_range: None,
|
||||
extract_audio_last_index: 1,
|
||||
lyrics_recognition_id: String::new(),
|
||||
lyrics_sync: true,
|
||||
lyrics_taskinfo: vec![],
|
||||
maintrack_adsorb: true,
|
||||
material_save_mode: 0,
|
||||
multi_language_current: "none".to_string(),
|
||||
multi_language_list: vec![],
|
||||
multi_language_main: "none".to_string(),
|
||||
multi_language_mode: "none".to_string(),
|
||||
original_sound_last_index: 1,
|
||||
record_audio_last_index: 1,
|
||||
sticker_max_index: 1,
|
||||
subtitle_keywords_config: None,
|
||||
subtitle_recognition_id: String::new(),
|
||||
subtitle_sync: true,
|
||||
subtitle_taskinfo: vec![],
|
||||
system_font_list: vec![],
|
||||
video_mute: false,
|
||||
zoom_info_params: None,
|
||||
},
|
||||
cover: None,
|
||||
create_time: now,
|
||||
duration: 0,
|
||||
extra_info: None,
|
||||
fps: 30.0,
|
||||
free_render_index_mode_on: false,
|
||||
group_container: None,
|
||||
id: draft_id,
|
||||
keyframe_graph_list: vec![],
|
||||
keyframes: JianYingKeyframes {
|
||||
adjusts: vec![],
|
||||
audios: vec![],
|
||||
effects: vec![],
|
||||
filters: vec![],
|
||||
handwrites: vec![],
|
||||
stickers: vec![],
|
||||
texts: vec![],
|
||||
videos: vec![],
|
||||
},
|
||||
last_modified_platform: JianYingPlatform {
|
||||
app_id: 3704,
|
||||
app_source: "lv".to_string(),
|
||||
app_version: "5.9.0".to_string(),
|
||||
device_id: "0594836068dad896e25a104fc9dbabab".to_string(),
|
||||
hard_disk_id: "92ff8fc0225cc7379b7488c983cc022b".to_string(),
|
||||
mac_address: "32d6cbfd9256fd8884fac27c2658c25c".to_string(),
|
||||
os: "windows".to_string(),
|
||||
os_version: "10.0.26100".to_string(),
|
||||
},
|
||||
materials: JianYingMaterials {
|
||||
ai_translates: vec![],
|
||||
audio_balances: vec![],
|
||||
audio_effects: vec![],
|
||||
audio_fades: vec![],
|
||||
audio_track_indexes: vec![],
|
||||
audios: vec![],
|
||||
beats: vec![],
|
||||
canvases: vec![],
|
||||
chromas: vec![],
|
||||
color_curves: vec![],
|
||||
digital_humans: vec![],
|
||||
drafts: vec![],
|
||||
effects: vec![],
|
||||
flowers: vec![],
|
||||
green_screens: vec![],
|
||||
handwrites: vec![],
|
||||
hsl: vec![],
|
||||
images: vec![],
|
||||
log_color_wheels: vec![],
|
||||
loudnesses: vec![],
|
||||
manual_deformations: vec![],
|
||||
masks: vec![],
|
||||
material_animations: vec![],
|
||||
material_colors: vec![],
|
||||
multi_language_refs: vec![],
|
||||
placeholders: vec![],
|
||||
plugin_effects: vec![],
|
||||
primary_color_wheels: vec![],
|
||||
realtime_denoises: vec![],
|
||||
shapes: vec![],
|
||||
smart_crops: vec![],
|
||||
smart_relights: vec![],
|
||||
sound_channel_mappings: vec![],
|
||||
speeds: vec![],
|
||||
stickers: vec![],
|
||||
tail_leaders: vec![],
|
||||
text_templates: vec![],
|
||||
texts: vec![],
|
||||
time_marks: vec![],
|
||||
transitions: vec![],
|
||||
video_effects: vec![],
|
||||
video_trackings: vec![],
|
||||
videos: vec![],
|
||||
},
|
||||
render_index_track_mode_on: true,
|
||||
retouch_cover: None,
|
||||
source: "default".to_string(),
|
||||
static_cover_image_path: String::new(),
|
||||
time_marks: None,
|
||||
tracks: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ pub mod project_template_binding_service;
|
||||
pub mod material_matching_service;
|
||||
pub mod template_matching_result_service;
|
||||
pub mod video_generation_service;
|
||||
pub mod jianying_export;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::sync::Arc;
|
||||
use chrono::Utc;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::data::models::template_matching_result::{
|
||||
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
|
||||
CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions,
|
||||
MatchingResultStatus,
|
||||
};
|
||||
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||
use crate::business::services::material_matching_service::{
|
||||
MaterialMatchingResult as ServiceMatchingResult, SegmentMatch, FailedSegmentMatch,
|
||||
MaterialMatchingResult as ServiceMatchingResult,
|
||||
};
|
||||
use crate::business::services::jianying_export::JianYingExportService;
|
||||
use crate::data::repositories::material_repository::MaterialRepository;
|
||||
|
||||
/// 模板匹配结果服务
|
||||
/// 遵循 Tauri 开发规范的业务逻辑设计原则
|
||||
@@ -296,3 +298,278 @@ pub struct MatchingStatistics {
|
||||
pub total_models: u32,
|
||||
pub average_success_rate: f64,
|
||||
}
|
||||
|
||||
impl TemplateMatchingResultService {
|
||||
/// 导出匹配结果到剪映格式
|
||||
/// 根据模板匹配结果生成剪映可导入的 draft_content.json 文件
|
||||
pub async fn export_to_jianying(
|
||||
&self,
|
||||
result_id: &str,
|
||||
output_path: &str,
|
||||
material_repository: Arc<MaterialRepository>,
|
||||
) -> Result<String> {
|
||||
// 获取匹配结果详情
|
||||
let detail = self.get_matching_result_detail(result_id).await?
|
||||
.ok_or_else(|| anyhow!("匹配结果不存在: {}", result_id))?;
|
||||
|
||||
// 创建基础的剪映草稿内容
|
||||
let mut draft_content = JianYingExportService::create_default_draft_content();
|
||||
|
||||
// 设置基本信息
|
||||
draft_content.duration = self.calculate_total_duration(&detail)?;
|
||||
|
||||
// 生成素材列表 - 替换匹配的素材路径
|
||||
let (materials, material_id_map) = self.generate_materials(&detail, material_repository).await?;
|
||||
draft_content.materials.videos = materials;
|
||||
|
||||
// 生成轨道和片段
|
||||
let tracks = self.generate_tracks(&detail, &material_id_map)?;
|
||||
draft_content.tracks = tracks;
|
||||
|
||||
// 生成输出文件路径
|
||||
let output_file_path = if output_path.ends_with(".json") {
|
||||
output_path.to_string()
|
||||
} else {
|
||||
format!("{}/draft_content_{}.json", output_path, result_id)
|
||||
};
|
||||
|
||||
// 序列化并写入文件
|
||||
let json_content = serde_json::to_string_pretty(&draft_content)
|
||||
.map_err(|e| anyhow!("序列化失败: {}", e))?;
|
||||
|
||||
std::fs::write(&output_file_path, json_content)
|
||||
.map_err(|e| anyhow!("写入文件失败: {}", e))?;
|
||||
|
||||
Ok(output_file_path)
|
||||
}
|
||||
|
||||
/// 计算总时长
|
||||
fn calculate_total_duration(&self, detail: &TemplateMatchingResultDetail) -> Result<u64> {
|
||||
let mut total_duration = 0u64;
|
||||
|
||||
for segment_result in &detail.segment_results {
|
||||
if segment_result.end_time > total_duration {
|
||||
total_duration = segment_result.end_time;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_duration)
|
||||
}
|
||||
|
||||
/// 生成素材列表,替换匹配的素材路径
|
||||
async fn generate_materials(
|
||||
&self,
|
||||
detail: &TemplateMatchingResultDetail,
|
||||
material_repository: Arc<MaterialRepository>,
|
||||
) -> Result<(Vec<crate::business::services::jianying_export::JianYingVideo>, std::collections::HashMap<String, String>)> {
|
||||
use crate::business::services::jianying_export::*;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
let mut videos = Vec::new();
|
||||
let mut material_id_map = HashMap::new();
|
||||
|
||||
for segment_result in &detail.segment_results {
|
||||
// 为每个匹配的素材生成新的UUID
|
||||
let new_material_id = Uuid::new_v4().to_string();
|
||||
material_id_map.insert(segment_result.track_segment_id.clone(), new_material_id.clone());
|
||||
|
||||
// 查询MaterialSegment获取文件路径
|
||||
let material_segment = material_repository.get_segment_by_id_sync(&segment_result.material_segment_id)?
|
||||
.ok_or_else(|| anyhow!("找不到素材片段: {}", segment_result.material_segment_id))?;
|
||||
|
||||
// 创建剪映视频素材
|
||||
let video = JianYingVideo {
|
||||
aigc_type: "none".to_string(),
|
||||
audio_fade: None,
|
||||
cartoon_path: String::new(),
|
||||
category_id: String::new(),
|
||||
category_name: "local".to_string(),
|
||||
check_flag: 63487,
|
||||
crop: JianYingCrop {
|
||||
lower_left_x: 0.0,
|
||||
lower_left_y: 1.0,
|
||||
lower_right_x: 1.0,
|
||||
lower_right_y: 1.0,
|
||||
upper_left_x: 0.0,
|
||||
upper_left_y: 0.0,
|
||||
upper_right_x: 1.0,
|
||||
upper_right_y: 0.0,
|
||||
},
|
||||
crop_ratio: "free".to_string(),
|
||||
crop_scale: 1.0,
|
||||
duration: segment_result.segment_duration,
|
||||
extra_type_option: 0,
|
||||
formula_id: String::new(),
|
||||
freeze: None,
|
||||
has_audio: false,
|
||||
height: 1920, // 默认高度
|
||||
id: new_material_id,
|
||||
intensifies_audio_path: String::new(),
|
||||
intensifies_path: String::new(),
|
||||
is_ai_generate_content: false,
|
||||
is_copyright: true,
|
||||
is_text_edit_overdub: false,
|
||||
is_unified_beauty_mode: false,
|
||||
local_id: String::new(),
|
||||
local_material_id: Uuid::new_v4().to_string(),
|
||||
material_id: String::new(),
|
||||
material_name: Path::new(&material_segment.file_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unknown.mp4")
|
||||
.to_string(),
|
||||
material_url: String::new(),
|
||||
matting: JianYingMatting {
|
||||
flag: 0,
|
||||
has_use_quick_brush: false,
|
||||
has_use_quick_eraser: false,
|
||||
interactive_time: vec![],
|
||||
path: String::new(),
|
||||
strokes: vec![],
|
||||
},
|
||||
media_path: String::new(),
|
||||
object_locked: None,
|
||||
origin_material_id: String::new(),
|
||||
path: material_segment.file_path.clone(), // 使用匹配的素材路径
|
||||
picture_from: "none".to_string(),
|
||||
picture_set_category_id: String::new(),
|
||||
picture_set_category_name: String::new(),
|
||||
request_id: String::new(),
|
||||
reverse_intensifies_path: String::new(),
|
||||
reverse_path: String::new(),
|
||||
smart_motion: None,
|
||||
source: 0,
|
||||
source_platform: 0,
|
||||
stable: JianYingStable {
|
||||
matrix_path: String::new(),
|
||||
stable_level: 0,
|
||||
time_range: JianYingTimeRange {
|
||||
duration: 0,
|
||||
start: 0,
|
||||
},
|
||||
},
|
||||
team_id: String::new(),
|
||||
video_type: "video".to_string(),
|
||||
video_algorithm: JianYingVideoAlgorithm {
|
||||
algorithms: vec![],
|
||||
complement_frame_config: None,
|
||||
deflicker: None,
|
||||
gameplay_configs: vec![],
|
||||
motion_blur_config: None,
|
||||
noise_reduction: None,
|
||||
path: String::new(),
|
||||
quality_enhance: None,
|
||||
time_range: None,
|
||||
},
|
||||
width: 1080, // 默认宽度
|
||||
};
|
||||
|
||||
videos.push(video);
|
||||
}
|
||||
|
||||
Ok((videos, material_id_map))
|
||||
}
|
||||
|
||||
/// 生成轨道和片段
|
||||
fn generate_tracks(
|
||||
&self,
|
||||
detail: &TemplateMatchingResultDetail,
|
||||
material_id_map: &std::collections::HashMap<String, String>,
|
||||
) -> Result<Vec<crate::business::services::jianying_export::JianYingTrack>> {
|
||||
use crate::business::services::jianying_export::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
let mut tracks = Vec::new();
|
||||
|
||||
// 创建主视频轨道
|
||||
let track_id = Uuid::new_v4().to_string();
|
||||
let mut segments = Vec::new();
|
||||
|
||||
for segment_result in &detail.segment_results {
|
||||
if let Some(material_id) = material_id_map.get(&segment_result.track_segment_id) {
|
||||
let segment = JianYingSegment {
|
||||
caption_info: None,
|
||||
cartoon: false,
|
||||
clip: JianYingClip {
|
||||
alpha: 1.0,
|
||||
flip: JianYingFlip {
|
||||
horizontal: false,
|
||||
vertical: false,
|
||||
},
|
||||
rotation: 0.0,
|
||||
scale: JianYingScale { x: 1.0, y: 1.0 },
|
||||
transform: JianYingTransform { x: 0.0, y: 0.0 },
|
||||
},
|
||||
common_keyframes: vec![],
|
||||
enable_adjust: true,
|
||||
enable_color_correct_adjust: false,
|
||||
enable_color_curves: true,
|
||||
enable_color_match_adjust: false,
|
||||
enable_color_wheels: true,
|
||||
enable_lut: true,
|
||||
enable_smart_color_adjust: false,
|
||||
extra_material_refs: vec![],
|
||||
group_id: String::new(),
|
||||
hdr_settings: JianYingHdrSettings {
|
||||
intensity: 1.0,
|
||||
mode: 1,
|
||||
nits: 1000,
|
||||
},
|
||||
id: Uuid::new_v4().to_string(),
|
||||
intensifies_audio: false,
|
||||
is_placeholder: false,
|
||||
is_tone_modify: false,
|
||||
keyframe_refs: vec![],
|
||||
last_nonzero_volume: 1.0,
|
||||
material_id: material_id.clone(),
|
||||
render_index: 0,
|
||||
responsive_layout: JianYingResponsiveLayout {
|
||||
enable: false,
|
||||
horizontal_pos_layout: 0,
|
||||
size_layout: 0,
|
||||
target_follow: String::new(),
|
||||
vertical_pos_layout: 0,
|
||||
},
|
||||
reverse: false,
|
||||
source_timerange: JianYingTimeRange {
|
||||
duration: segment_result.segment_duration,
|
||||
start: 0, // 从素材开始位置
|
||||
},
|
||||
speed: 1.0, // 默认播放速度
|
||||
target_timerange: JianYingTimeRange {
|
||||
duration: segment_result.segment_duration,
|
||||
start: segment_result.start_time,
|
||||
},
|
||||
template_id: String::new(),
|
||||
template_scene: "default".to_string(),
|
||||
track_attribute: 0,
|
||||
track_render_index: 0,
|
||||
uniform_scale: JianYingUniformScale {
|
||||
on: true,
|
||||
value: 1.0,
|
||||
},
|
||||
visible: true,
|
||||
volume: 1.0,
|
||||
};
|
||||
|
||||
segments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
let track = JianYingTrack {
|
||||
attribute: 0,
|
||||
flag: 0,
|
||||
id: track_id,
|
||||
is_default_name: true,
|
||||
name: String::new(),
|
||||
segments,
|
||||
};
|
||||
|
||||
tracks.push(track);
|
||||
Ok(tracks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ pub fn run() {
|
||||
commands::template_matching_result_commands::create_matching_result,
|
||||
commands::template_matching_result_commands::get_matching_result_by_id,
|
||||
commands::template_matching_result_commands::get_matching_result_status_options,
|
||||
commands::template_matching_result_commands::export_matching_result_to_jianying,
|
||||
// MaterialSegment聚合视图命令
|
||||
commands::material_segment_view_commands::get_project_segment_view,
|
||||
commands::material_segment_view_commands::get_project_segment_view_with_query,
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::data::models::template_matching_result::{
|
||||
TemplateMatchingResultQueryOptions, MatchingResultStatus,
|
||||
};
|
||||
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||
use crate::data::repositories::material_repository::MaterialRepository;
|
||||
use crate::infrastructure::database::Database;
|
||||
|
||||
/// 保存匹配结果到数据库
|
||||
@@ -231,3 +232,32 @@ pub async fn get_matching_result_status_options() -> Result<Vec<(String, String)
|
||||
("Cancelled".to_string(), "已取消".to_string()),
|
||||
])
|
||||
}
|
||||
|
||||
/// 导出匹配结果到剪映格式
|
||||
#[command]
|
||||
pub async fn export_matching_result_to_jianying(
|
||||
result_id: String,
|
||||
output_path: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<String, String> {
|
||||
println!("🎬 开始导出匹配结果到剪映格式");
|
||||
println!("📋 导出参数:");
|
||||
println!(" - 匹配结果ID: {}", result_id);
|
||||
println!(" - 输出路径: {}", output_path);
|
||||
|
||||
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let material_repository = Arc::new(MaterialRepository::new(database.inner().clone())
|
||||
.map_err(|e| format!("创建MaterialRepository失败: {}", e))?);
|
||||
let service = TemplateMatchingResultService::new(repository);
|
||||
|
||||
match service.export_to_jianying(&result_id, &output_path, material_repository).await {
|
||||
Ok(file_path) => {
|
||||
println!("✅ 导出成功: {}", file_path);
|
||||
Ok(file_path)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 导出失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ interface TemplateMatchingResultCardProps {
|
||||
onViewDetail: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit?: () => void;
|
||||
onExportToJianying?: () => void;
|
||||
}
|
||||
|
||||
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
||||
@@ -13,6 +14,7 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
||||
onViewDetail,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onExportToJianying,
|
||||
}) => {
|
||||
// 格式化时长显示
|
||||
const formatDuration = (ms: number): string => {
|
||||
@@ -186,6 +188,15 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
{onExportToJianying && (
|
||||
<button
|
||||
onClick={onExportToJianying}
|
||||
className="btn btn-success btn-sm"
|
||||
title="导出到剪映"
|
||||
>
|
||||
导出到剪映
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="btn btn-danger btn-sm"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import {
|
||||
TemplateMatchingResult,
|
||||
TemplateMatchingResultQueryOptions,
|
||||
@@ -127,6 +128,38 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
||||
onResultSelect?.(result);
|
||||
};
|
||||
|
||||
// 导出到剪映
|
||||
const handleExportToJianying = async (result: TemplateMatchingResult) => {
|
||||
try {
|
||||
// 选择保存路径
|
||||
const filePath = await save({
|
||||
title: '导出到剪映',
|
||||
defaultPath: `draft_content_${result.result_name}.json`,
|
||||
filters: [
|
||||
{
|
||||
name: 'JSON文件',
|
||||
extensions: ['json']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!filePath) {
|
||||
return; // 用户取消了保存
|
||||
}
|
||||
|
||||
// 调用后端导出API
|
||||
const exportedFilePath = await invoke<string>('export_matching_result_to_jianying', {
|
||||
resultId: result.id,
|
||||
outputPath: filePath,
|
||||
});
|
||||
|
||||
// 显示成功消息
|
||||
alert(`导出成功!文件已保存到:${exportedFilePath}`);
|
||||
} catch (err) {
|
||||
setError(`导出失败: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理过滤器变化
|
||||
const handleFilterChange = (newFilters: Partial<typeof filters>) => {
|
||||
setFilters(prev => ({ ...prev, ...newFilters }));
|
||||
@@ -278,6 +311,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
||||
result={result}
|
||||
onViewDetail={() => handleViewDetail(result)}
|
||||
onDelete={() => setDeleteConfirm({ show: true, result })}
|
||||
onExportToJianying={() => handleExportToJianying(result)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user