🔧 关键修复: 1. Python 脚本最终结果输出: - 在函数结束前发送 JSON-RPC 格式的最终结果 - 成功时:rpc.success(result) - 失败时:rpc.error(JSONRPCError.GENERATION_FAILED, msg, details) - 确保最终结果是标准 JSON-RPC 2.0 格式 2. Rust 解析逻辑优化: - 区分进度通知和最终结果响应 - 优先返回 JSON-RPC 结果/错误响应 - 备用机制:检查直接 JSON 中的 status 字段 - 避免返回进度消息作为最终结果 3. 前端 JSON-RPC 响应处理: - 检测 jsonrpc: '2.0' 格式 - 提取 result 字段作为成功结果 - 处理 error 字段并抛出相应错误 - 保持向后兼容直接 JSON 格式 4. 错误处理链路完善: - Python 异常 → JSON-RPC 错误响应 - Rust 解析 → 提取错误信息 - 前端处理 → 显示具体错误原因 - 端到端的错误传播机制 ✅ 修复效果: - 正确识别成功/失败状态 ✓ - 返回最终结果而非进度消息 ✓ - 标准化的错误处理 ✓ - 完整的 JSON-RPC 2.0 支持 ✓ 现在前端应该能正确显示视频生成的成功状态!
517 lines
17 KiB
Rust
517 lines
17 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AIVideoRequest {
|
|
pub image_path: String,
|
|
pub prompt: String,
|
|
pub duration: String,
|
|
pub model_type: String,
|
|
pub output_path: Option<String>,
|
|
pub timeout: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BatchAIVideoRequest {
|
|
pub image_folder: String,
|
|
pub prompts: Vec<String>,
|
|
pub output_folder: String,
|
|
pub duration: String,
|
|
pub model_type: String,
|
|
pub timeout: Option<u32>,
|
|
}
|
|
use std::process::Command;
|
|
use tauri_plugin_shell::ShellExt;
|
|
use serde_json;
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct VideoProcessRequest {
|
|
pub input_path: String,
|
|
pub output_path: String,
|
|
pub operation: String,
|
|
pub parameters: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct AudioAnalysisRequest {
|
|
pub file_path: String,
|
|
pub analysis_type: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct ProjectInfo {
|
|
pub name: String,
|
|
pub path: String,
|
|
pub created_at: String,
|
|
pub modified_at: String,
|
|
pub video_tracks: Vec<VideoTrack>,
|
|
pub audio_tracks: Vec<AudioTrack>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct VideoTrack {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub file_path: String,
|
|
pub start_time: f64,
|
|
pub duration: f64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct AudioTrack {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub file_path: String,
|
|
pub start_time: f64,
|
|
pub duration: f64,
|
|
pub volume: f64,
|
|
}
|
|
|
|
// Helper function to execute Python commands using Tauri Shell Plugin
|
|
async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Result<String, String> {
|
|
use tauri_plugin_shell::process::CommandEvent;
|
|
|
|
println!("Executing Python command with args: {:?}", args);
|
|
|
|
// Get the current working directory and move up one level from src-tauri to project root
|
|
let current_dir = std::env::current_dir()
|
|
.map_err(|e| format!("Failed to get current directory: {}", e))?;
|
|
|
|
let project_root = if current_dir.ends_with("src-tauri") {
|
|
current_dir.parent().unwrap_or(¤t_dir).to_path_buf()
|
|
} else {
|
|
current_dir
|
|
};
|
|
|
|
println!("Current directory: {:?}", std::env::current_dir().unwrap_or_default());
|
|
println!("Project root: {:?}", project_root);
|
|
|
|
// Verify that the Python script exists
|
|
let script_path = project_root.join(&args[0]);
|
|
if !script_path.exists() {
|
|
return Err(format!("Python script not found: {:?}", script_path));
|
|
}
|
|
println!("Python script found: {:?}", script_path);
|
|
|
|
// Use Tauri Shell Plugin for better process management
|
|
let python_cmd = if cfg!(target_os = "windows") { "python" } else { "python3" };
|
|
|
|
let command = app.shell()
|
|
.command(python_cmd)
|
|
.current_dir(&project_root)
|
|
.args(args);
|
|
|
|
// Set environment variables for proper UTF-8 handling on Windows
|
|
let command = if cfg!(target_os = "windows") {
|
|
command
|
|
.env("PYTHONIOENCODING", "utf-8")
|
|
.env("PYTHONUTF8", "1")
|
|
} else {
|
|
command
|
|
};
|
|
|
|
let (mut rx, _child) = command
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to spawn Python process: {}", e))?;
|
|
|
|
let mut stdout_lines = Vec::new();
|
|
let mut stderr_lines = Vec::new();
|
|
let mut final_result: Option<String> = None;
|
|
let mut progress_messages = Vec::new();
|
|
|
|
// Collect output from the process
|
|
while let Some(event) = rx.recv().await {
|
|
match event {
|
|
CommandEvent::Stdout(data) => {
|
|
let line = String::from_utf8_lossy(&data);
|
|
println!("Python stdout: {}", line);
|
|
stdout_lines.push(line.to_string());
|
|
|
|
// Parse JSON-RPC messages
|
|
if line.starts_with("JSONRPC:") {
|
|
let json_str = &line[8..]; // Remove "JSONRPC:" prefix
|
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
|
|
if let Some(method) = json_value.get("method") {
|
|
if method == "progress" {
|
|
progress_messages.push(json_value);
|
|
println!("Progress: {}", json_str);
|
|
}
|
|
} else if json_value.get("result").is_some() || json_value.get("error").is_some() {
|
|
// This is a final result or error response
|
|
final_result = Some(json_str.to_string());
|
|
println!("Final JSON-RPC result: {}", json_str);
|
|
}
|
|
}
|
|
} else if line.trim().starts_with('{') && line.trim().ends_with('}') {
|
|
// Fallback: try to parse as direct JSON result
|
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line.trim()) {
|
|
// Check if this looks like a final result (has status field)
|
|
if json_value.get("status").is_some() {
|
|
final_result = Some(line.trim().to_string());
|
|
println!("Direct JSON result: {}", line.trim());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CommandEvent::Stderr(data) => {
|
|
let line = String::from_utf8_lossy(&data);
|
|
println!("Python stderr: {}", line);
|
|
stderr_lines.push(line.to_string());
|
|
}
|
|
CommandEvent::Terminated(payload) => {
|
|
println!("Python process terminated with code: {:?}", payload.code);
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Check if we have a successful termination event
|
|
let mut success = false;
|
|
let mut exit_code = None;
|
|
|
|
// Look for termination event in our collected events
|
|
// If we received a Terminated event, we already have the exit status
|
|
if let Some(last_event) = rx.try_recv().ok() {
|
|
if let CommandEvent::Terminated(payload) = last_event {
|
|
success = payload.code == Some(0);
|
|
exit_code = payload.code;
|
|
}
|
|
}
|
|
|
|
// If no termination event was captured, assume success if we have output
|
|
if exit_code.is_none() {
|
|
success = !stdout_lines.is_empty() || final_result.is_some();
|
|
exit_code = Some(if success { 0 } else { 1 });
|
|
}
|
|
|
|
println!("Python script exit code: {:?}", exit_code);
|
|
|
|
if success {
|
|
// Return final result if found, otherwise return full stdout
|
|
if let Some(result) = final_result {
|
|
println!("Extracted JSON-RPC result: {}", result);
|
|
Ok(result)
|
|
} else {
|
|
// Fallback: try to find JSON in the combined stdout
|
|
let full_output = stdout_lines.join("\n");
|
|
if let Some(json_start) = full_output.find('{') {
|
|
if let Some(json_end) = full_output.rfind('}') {
|
|
if json_end > json_start {
|
|
let json_str = &full_output[json_start..=json_end];
|
|
println!("Extracted JSON from full output: {}", json_str);
|
|
return Ok(json_str.to_string());
|
|
}
|
|
}
|
|
}
|
|
Ok(full_output)
|
|
}
|
|
} else {
|
|
let error_msg = format!(
|
|
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
|
|
exit_code,
|
|
stderr_lines.join("\n"),
|
|
stdout_lines.join("\n")
|
|
);
|
|
println!("Python script error: {}", error_msg);
|
|
Err(error_msg)
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn greet(name: &str) -> Result<String, String> {
|
|
Ok(format!("Hello, {}! Welcome to MixVideo V2!", name))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn process_video(request: VideoProcessRequest) -> Result<String, String> {
|
|
// Call Python video processing script
|
|
let output = Command::new("python3")
|
|
.arg("python_core/video_processing/core.py")
|
|
.arg("--input")
|
|
.arg(&request.input_path)
|
|
.arg("--output")
|
|
.arg(&request.output_path)
|
|
.arg("--operation")
|
|
.arg(&request.operation)
|
|
.arg("--parameters")
|
|
.arg(request.parameters.to_string())
|
|
.output()
|
|
.map_err(|e| format!("Failed to execute Python script: {}", e))?;
|
|
|
|
if output.status.success() {
|
|
let result = String::from_utf8_lossy(&output.stdout);
|
|
Ok(result.to_string())
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!("Python script error: {}", error))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn analyze_audio(request: AudioAnalysisRequest) -> Result<String, String> {
|
|
// Call Python audio analysis script
|
|
let output = Command::new("python3")
|
|
.arg("python_core/audio_processing/core.py")
|
|
.arg("--file")
|
|
.arg(&request.file_path)
|
|
.arg("--analysis")
|
|
.arg(&request.analysis_type)
|
|
.output()
|
|
.map_err(|e| format!("Failed to execute Python script: {}", e))?;
|
|
|
|
if output.status.success() {
|
|
let result = String::from_utf8_lossy(&output.stdout);
|
|
Ok(result.to_string())
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!("Python script error: {}", error))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_project_info(project_path: String) -> Result<ProjectInfo, String> {
|
|
// Call Python project management script
|
|
let output = Command::new("python3")
|
|
.arg("python_core/services/project_manager.py")
|
|
.arg("--action")
|
|
.arg("get_info")
|
|
.arg("--path")
|
|
.arg(&project_path)
|
|
.output()
|
|
.map_err(|e| format!("Failed to execute Python script: {}", e))?;
|
|
|
|
if output.status.success() {
|
|
let result = String::from_utf8_lossy(&output.stdout);
|
|
let project_info: ProjectInfo = serde_json::from_str(&result)
|
|
.map_err(|e| format!("Failed to parse project info: {}", e))?;
|
|
Ok(project_info)
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!("Python script error: {}", error))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn save_project(project_info: ProjectInfo) -> Result<String, String> {
|
|
// Call Python project management script to save
|
|
let project_json = serde_json::to_string(&project_info)
|
|
.map_err(|e| format!("Failed to serialize project: {}", e))?;
|
|
|
|
let output = Command::new("python3")
|
|
.arg("python_core/services/project_manager.py")
|
|
.arg("--action")
|
|
.arg("save")
|
|
.arg("--data")
|
|
.arg(&project_json)
|
|
.output()
|
|
.map_err(|e| format!("Failed to execute Python script: {}", e))?;
|
|
|
|
if output.status.success() {
|
|
let result = String::from_utf8_lossy(&output.stdout);
|
|
Ok(result.to_string())
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!("Python script error: {}", error))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn load_project(project_path: String) -> Result<ProjectInfo, String> {
|
|
// Call Python project management script to load
|
|
let output = Command::new("python3")
|
|
.arg("python_core/services/project_manager.py")
|
|
.arg("--action")
|
|
.arg("load")
|
|
.arg("--path")
|
|
.arg(&project_path)
|
|
.output()
|
|
.map_err(|e| format!("Failed to execute Python script: {}", e))?;
|
|
|
|
if output.status.success() {
|
|
let result = String::from_utf8_lossy(&output.stdout);
|
|
let project_info: ProjectInfo = serde_json::from_str(&result)
|
|
.map_err(|e| format!("Failed to parse project info: {}", e))?;
|
|
Ok(project_info)
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!("Python script error: {}", error))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result<String, String> {
|
|
let mut args = vec![
|
|
"python_core/ai_video/video_generator.py".to_string(),
|
|
"--action".to_string(),
|
|
"single".to_string(),
|
|
"--image".to_string(),
|
|
request.image_path,
|
|
"--prompt".to_string(),
|
|
request.prompt,
|
|
"--duration".to_string(),
|
|
request.duration,
|
|
"--model".to_string(),
|
|
request.model_type,
|
|
];
|
|
|
|
// Always provide an output path - use provided path or default to temp directory
|
|
let output_path = request.output_path.unwrap_or_else(|| {
|
|
if cfg!(target_os = "windows") {
|
|
"C:\\temp".to_string()
|
|
} else {
|
|
"/tmp".to_string()
|
|
}
|
|
});
|
|
args.push("--output".to_string());
|
|
args.push(output_path);
|
|
|
|
if let Some(timeout) = request.timeout {
|
|
args.push("--timeout".to_string());
|
|
args.push(timeout.to_string());
|
|
}
|
|
|
|
execute_python_command(app, &args).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn batch_generate_ai_videos(app: tauri::AppHandle, request: BatchAIVideoRequest) -> Result<String, String> {
|
|
let prompts_json = serde_json::to_string(&request.prompts)
|
|
.map_err(|e| format!("Failed to serialize prompts: {}", e))?;
|
|
|
|
let mut args = vec![
|
|
"python_core/ai_video/video_generator.py".to_string(),
|
|
"--action".to_string(),
|
|
"batch".to_string(),
|
|
"--folder".to_string(),
|
|
request.image_folder,
|
|
"--prompts".to_string(),
|
|
prompts_json,
|
|
"--output".to_string(),
|
|
request.output_folder,
|
|
"--duration".to_string(),
|
|
request.duration,
|
|
"--model".to_string(),
|
|
request.model_type,
|
|
];
|
|
|
|
if let Some(timeout) = request.timeout {
|
|
args.push("--timeout".to_string());
|
|
args.push(timeout.to_string());
|
|
}
|
|
|
|
execute_python_command(app, &args).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn test_ai_video_environment(_app: tauri::AppHandle) -> Result<String, String> {
|
|
println!("Testing AI video environment...");
|
|
|
|
// Get project root directory
|
|
let current_dir = std::env::current_dir()
|
|
.map_err(|e| format!("Failed to get current directory: {}", e))?;
|
|
|
|
let project_root = if current_dir.ends_with("src-tauri") {
|
|
current_dir.parent().unwrap_or(¤t_dir).to_path_buf()
|
|
} else {
|
|
current_dir
|
|
};
|
|
|
|
println!("Testing from project root: {:?}", project_root);
|
|
|
|
// Try python3 first, then python (for Windows compatibility)
|
|
let python_cmd = if cfg!(target_os = "windows") {
|
|
"python"
|
|
} else {
|
|
"python3"
|
|
};
|
|
|
|
// Test 1: Python availability
|
|
let python_test = Command::new(python_cmd)
|
|
.arg("--version")
|
|
.current_dir(&project_root)
|
|
.output()
|
|
.map_err(|e| format!("Python not found: {}", e))?;
|
|
|
|
if !python_test.status.success() {
|
|
return Err("Python3 is not available".to_string());
|
|
}
|
|
|
|
let python_version = String::from_utf8_lossy(&python_test.stdout);
|
|
println!("Python version: {}", python_version);
|
|
|
|
// Test 2: Check if AI video module can be imported
|
|
let module_test = Command::new(python_cmd)
|
|
.arg("-c")
|
|
.arg("import sys; sys.path.append('python_core'); from ai_video import VideoGenerator; print('AI video module imported successfully')")
|
|
.current_dir(&project_root)
|
|
.output()
|
|
.map_err(|e| format!("Failed to test module import: {}", e))?;
|
|
|
|
let module_output = String::from_utf8_lossy(&module_test.stdout);
|
|
let module_error = String::from_utf8_lossy(&module_test.stderr);
|
|
|
|
println!("Module test stdout: {}", module_output);
|
|
println!("Module test stderr: {}", module_error);
|
|
|
|
if !module_test.status.success() {
|
|
return Err(format!("AI video module import failed: {}", module_error));
|
|
}
|
|
|
|
let result = serde_json::json!({
|
|
"status": "success",
|
|
"python_version": python_version.trim(),
|
|
"module_import": module_test.status.success(),
|
|
"module_output": module_output.trim(),
|
|
"module_error": module_error.trim()
|
|
});
|
|
|
|
Ok(result.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn select_image_file(app: tauri::AppHandle) -> Result<String, String> {
|
|
use tauri_plugin_dialog::DialogExt;
|
|
|
|
println!("Opening image file dialog...");
|
|
|
|
let file_path = app.dialog()
|
|
.file()
|
|
.add_filter("Images", &["jpg", "jpeg", "png", "bmp", "gif", "tiff", "webp"])
|
|
.blocking_pick_file();
|
|
|
|
match file_path {
|
|
Some(path) => {
|
|
let path_str = path.to_string();
|
|
println!("Selected image file: {}", path_str);
|
|
Ok(path_str)
|
|
}
|
|
None => {
|
|
println!("No file selected");
|
|
Err("No file selected".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn select_folder(app: tauri::AppHandle) -> Result<String, String> {
|
|
use tauri_plugin_dialog::{DialogExt};
|
|
|
|
println!("Opening folder dialog...");
|
|
|
|
let folder_path = app.dialog()
|
|
.file()
|
|
.blocking_pick_folder();
|
|
|
|
match folder_path {
|
|
Some(path) => {
|
|
let path_str = path.to_string();
|
|
println!("Selected folder: {}", path_str);
|
|
Ok(path_str)
|
|
}
|
|
None => {
|
|
println!("No folder selected");
|
|
Err("No folder selected".to_string())
|
|
}
|
|
}
|
|
}
|