diff --git a/python_core/ai_video/api_client.py b/python_core/ai_video/api_client.py index e4dc43a..d044fcb 100644 --- a/python_core/ai_video/api_client.py +++ b/python_core/ai_video/api_client.py @@ -248,6 +248,20 @@ class APIClient: logger.info(progress_msg) if progress_callback: progress_callback(progress_msg) + + # Send detailed progress via JSON-RPC + from .json_rpc import create_progress_reporter + progress = create_progress_reporter() + progress.update( + step="running", + progress=min(100, (elapsed / timeout) * 100), + message=progress_msg, + details={ + "elapsed_seconds": elapsed, + "remaining_seconds": remaining, + "total_timeout": timeout + } + ) time.sleep(interval) elif status_result['msg'] == 'failed': diff --git a/src-tauri/src/commands/ai_video.rs b/src-tauri/src/commands/ai_video.rs new file mode 100644 index 0000000..b57739c --- /dev/null +++ b/src-tauri/src/commands/ai_video.rs @@ -0,0 +1,220 @@ +use serde::{Deserialize, Serialize}; +use std::process::Command; +use std::io::{BufRead, BufReader}; +use tauri_plugin_shell::ShellExt; + +#[derive(Debug, Deserialize)] +pub struct AIVideoRequest { + pub image_path: String, + pub prompt: String, + pub duration: String, + pub model_type: String, + pub output_path: Option, + pub timeout: Option, +} + +#[derive(Debug, Deserialize)] +pub struct BatchAIVideoRequest { + pub image_folder: String, + pub prompts: Vec, + pub output_folder: String, + pub duration: String, + pub model_type: String, + pub timeout: Option, +} + +async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Result { + println!("Executing Python command with args: {:?}", args); + + // 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!("Working directory: {:?}", project_root); + + // Try python3 first, then python (for Windows compatibility) + let python_cmd = if cfg!(target_os = "windows") { + "python" + } else { + "python3" + }; + + let mut cmd = Command::new(python_cmd); + cmd.current_dir(&project_root); + cmd.args(args); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + println!("Starting Python process..."); + let mut child = cmd.spawn() + .map_err(|e| format!("Failed to start Python process: {}", e))?; + + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + let stdout_reader = BufReader::new(stdout); + let stderr_reader = BufReader::new(stderr); + + let mut progress_messages = Vec::new(); + let mut final_result: Option = None; + + // Read stdout + for line in stdout_reader.lines() { + let line = line.map_err(|e| format!("Failed to read stdout: {}", e))?; + println!("Python stdout: {}", line); + + // 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::(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 - always update to get the latest + final_result = Some(json_str.to_string()); + println!("JSON-RPC result found: {}", 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::(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()); + } + } + } + } + + // Read stderr + for line in stderr_reader.lines() { + let line = line.map_err(|e| format!("Failed to read stderr: {}", e))?; + println!("Python stderr: {}", line); + } + + // Wait for process to complete + let exit_status = child.wait() + .map_err(|e| format!("Failed to wait for Python process: {}", e))?; + + println!("Python process terminated with code: {:?}", exit_status.code()); + println!("Python script exit code: {:?}", exit_status.code()); + + // Return the final result if we found one + if let Some(result) = final_result { + println!("Extracted JSON-RPC result: {}", result); + return Ok(result); + } + + // If no JSON-RPC result found, check exit code + if exit_status.success() { + Ok(r#"{"status": true, "msg": "Process completed successfully"}"#.to_string()) + } else { + Err(format!("Python script failed with exit code: {:?}", exit_status.code())) + } +} + +#[tauri::command] +pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result { + 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, + ]; + + if let Some(output_path) = request.output_path { + 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 { + 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 { + 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" + }; + + let output = Command::new(python_cmd) + .current_dir(&project_root) + .args(&["-c", "import sys; print(f'Python {sys.version}'); import requests; print('requests OK'); import PIL; print('PIL OK')"]) + .output() + .map_err(|e| format!("Failed to execute Python test: {}", e))?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + Ok(format!("Environment test passed:\n{}", stdout)) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!("Environment test failed:\n{}", stderr)) + } +} diff --git a/src-tauri/src/commands/basic.rs b/src-tauri/src/commands/basic.rs new file mode 100644 index 0000000..a2d2fc5 --- /dev/null +++ b/src-tauri/src/commands/basic.rs @@ -0,0 +1,4 @@ +#[tauri::command] +pub fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} diff --git a/src-tauri/src/commands/file_system.rs b/src-tauri/src/commands/file_system.rs new file mode 100644 index 0000000..4bdc85c --- /dev/null +++ b/src-tauri/src/commands/file_system.rs @@ -0,0 +1,90 @@ +#[tauri::command] +pub async fn select_image_file(app: tauri::AppHandle) -> Result { + use tauri_plugin_dialog::{DialogExt}; + + println!("Opening image file dialog..."); + + let file_path = app.dialog() + .file() + .add_filter("Image files", &["jpg", "jpeg", "png", "bmp", "gif", "tiff", "webp"]) + .blocking_pick_file(); + + match file_path { + Some(path) => { + let path_str = path.to_string_lossy().to_string(); + println!("Selected image file: {}", path_str); + Ok(path_str) + } + None => { + println!("No image file selected"); + Err("No image file selected".to_string()) + } + } +} + +#[tauri::command] +pub async fn select_folder(app: tauri::AppHandle) -> Result { + 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_lossy().to_string(); + println!("Selected folder: {}", path_str); + Ok(path_str) + } + None => { + println!("No folder selected"); + Err("No folder selected".to_string()) + } + } +} + +#[tauri::command] +pub async fn open_folder(folder_path: String) -> Result { + println!("Opening folder: {}", folder_path); + + #[cfg(target_os = "windows")] + { + use std::process::Command; + let result = Command::new("explorer") + .arg(&folder_path) + .spawn(); + + match result { + Ok(_) => Ok(format!("Opened folder: {}", folder_path)), + Err(e) => Err(format!("Failed to open folder: {}", e)) + } + } + + #[cfg(target_os = "macos")] + { + use std::process::Command; + let result = Command::new("open") + .arg(&folder_path) + .spawn(); + + match result { + Ok(_) => Ok(format!("Opened folder: {}", folder_path)), + Err(e) => Err(format!("Failed to open folder: {}", e)) + } + } + + #[cfg(target_os = "linux")] + { + use std::process::Command; + let result = Command::new("xdg-open") + .arg(&folder_path) + .spawn(); + + match result { + Ok(_) => Ok(format!("Opened folder: {}", folder_path)), + Err(e) => Err(format!("Failed to open folder: {}", e)) + } + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..b0d03bd --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,13 @@ +// Commands module organization +pub mod basic; +pub mod video; +pub mod ai_video; +pub mod file_system; +pub mod project; + +// Re-export all commands for easy access +pub use basic::*; +pub use video::*; +pub use ai_video::*; +pub use file_system::*; +pub use project::*; diff --git a/src-tauri/src/commands/project.rs b/src-tauri/src/commands/project.rs new file mode 100644 index 0000000..8918be1 --- /dev/null +++ b/src-tauri/src/commands/project.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +#[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, + pub audio_tracks: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VideoTrack { + pub id: String, + pub name: String, + pub file_path: String, + pub duration: f64, + pub start_time: f64, + pub end_time: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AudioTrack { + pub id: String, + pub name: String, + pub file_path: String, + pub duration: f64, + pub start_time: f64, + pub end_time: f64, + pub volume: f64, +} + +#[tauri::command] +pub async fn get_project_info(project_path: String) -> Result { + println!("Getting project info for: {}", project_path); + + // For now, return a mock project + // In a real implementation, you would load this from a project file + let project_info = ProjectInfo { + name: "Sample Project".to_string(), + path: project_path, + created_at: "2024-01-01T00:00:00Z".to_string(), + modified_at: "2024-01-01T00:00:00Z".to_string(), + video_tracks: vec![], + audio_tracks: vec![], + }; + + Ok(project_info) +} + +#[tauri::command] +pub async fn save_project(project_info: ProjectInfo) -> Result { + println!("Saving project: {}", project_info.name); + + // For now, just return success + // In a real implementation, you would save the project to a file + Ok(format!("Project '{}' saved successfully", project_info.name)) +} + +#[tauri::command] +pub async fn load_project(project_path: String) -> Result { + println!("Loading project from: {}", project_path); + + // For now, return a mock project + // In a real implementation, you would load this from the project file + let project_info = ProjectInfo { + name: "Loaded Project".to_string(), + path: project_path, + created_at: "2024-01-01T00:00:00Z".to_string(), + modified_at: "2024-01-01T00:00:00Z".to_string(), + video_tracks: vec![], + audio_tracks: vec![], + }; + + Ok(project_info) +} diff --git a/src-tauri/src/commands/video.rs b/src-tauri/src/commands/video.rs new file mode 100644 index 0000000..c9e799c --- /dev/null +++ b/src-tauri/src/commands/video.rs @@ -0,0 +1,35 @@ +use std::process::Command; +use std::path::Path; + +#[tauri::command] +pub async fn process_video(input_path: String, output_path: String) -> Result { + println!("Processing video: {} -> {}", input_path, output_path); + + // Validate input file exists + if !Path::new(&input_path).exists() { + return Err(format!("Input file does not exist: {}", input_path)); + } + + // For now, just copy the file as a placeholder + // In a real implementation, you would use FFmpeg or similar + let result = std::fs::copy(&input_path, &output_path); + + match result { + Ok(_) => Ok(format!("Video processed successfully: {}", output_path)), + Err(e) => Err(format!("Failed to process video: {}", e)) + } +} + +#[tauri::command] +pub async fn analyze_audio(file_path: String) -> Result { + println!("Analyzing audio: {}", file_path); + + // Validate input file exists + if !Path::new(&file_path).exists() { + return Err(format!("Audio file does not exist: {}", file_path)); + } + + // Placeholder for audio analysis + // In a real implementation, you would use audio analysis libraries + Ok(format!("Audio analysis completed for: {}", file_path)) +} diff --git a/src/components/AIVideoGenerator.tsx b/src/components/AIVideoGenerator.tsx index 8c3e376..79fa5eb 100644 --- a/src/components/AIVideoGenerator.tsx +++ b/src/components/AIVideoGenerator.tsx @@ -15,6 +15,7 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = const [selectedImage, setSelectedImage] = useState('') const [selectedFolder, setSelectedFolder] = useState('') const [customPrompt, setCustomPrompt] = useState('') + const [batchPrompts, setBatchPrompts] = useState(['']) const [outputFolder, setOutputFolder] = useState('') const [duration, setDuration] = useState('5') const [modelType, setModelType] = useState('lite') @@ -69,6 +70,23 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = }) } + // Batch prompts management + const addPrompt = () => { + setBatchPrompts([...batchPrompts, '']) + } + + const removePrompt = (index: number) => { + if (batchPrompts.length > 1) { + setBatchPrompts(batchPrompts.filter((_, i) => i !== index)) + } + } + + const updatePrompt = (index: number, value: string) => { + const newPrompts = [...batchPrompts] + newPrompts[index] = value + setBatchPrompts(newPrompts) + } + // Handle file selection const handleImageSelect = async () => { try { @@ -135,10 +153,14 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = return } - const prompts = customPrompt - ? customPrompt.split('\n').filter(p => p.trim()) + const prompts = batchPrompts.filter(p => p.trim()).length > 0 + ? batchPrompts.filter(p => p.trim()) : defaultPrompts + console.log('Batch processing with prompts:', prompts) + console.log('Image folder:', selectedFolder) + console.log('Output folder:', outputFolder) + await batchGenerateVideos({ image_folder: selectedFolder, prompts, @@ -300,18 +322,51 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = {/* Prompt Input */}
-