Files
mxivideo/src-tauri/src/commands.rs
root 66ec36b474 fix: 修复 JSON-RPC 通信中的状态识别和结果解析问题
🔧 关键修复:

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 支持 ✓

现在前端应该能正确显示视频生成的成功状态!
2025-07-10 13:07:50 +08:00

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(&current_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(&current_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())
}
}
}