diff --git a/cargos/tvai/examples/advanced_usage.rs b/cargos/tvai/examples/advanced_usage.rs new file mode 100644 index 0000000..1e7345f --- /dev/null +++ b/cargos/tvai/examples/advanced_usage.rs @@ -0,0 +1,145 @@ +//! Advanced usage example showing temporary file management and progress tracking + +use std::path::Path; +use tvai::*; + +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + println!("Topaz Video AI Library - Advanced Usage Example"); + + // Detect Topaz installation + if let Some(topaz_path) = detect_topaz_installation() { + println!("Found Topaz Video AI at: {}", topaz_path.display()); + + // Create configuration with custom temp directory + let config = TvaiConfig::builder() + .topaz_path(topaz_path) + .use_gpu(true) + .temp_dir(std::env::temp_dir().join("tvai_advanced_example")) + .build()?; + + // Create processor + let mut processor = TvaiProcessor::new(config)?; + println!("Processor created successfully"); + + // Demonstrate temporary file management + demonstrate_temp_file_management(&mut processor)?; + + // Demonstrate progress callback + demonstrate_progress_callback(&processor).await?; + + // Demonstrate validation + demonstrate_validation(&processor)?; + + // Get system information + demonstrate_system_info(&processor).await?; + + println!("Advanced example completed successfully!"); + } else { + println!("Topaz Video AI not found. Please install it first."); + } + + Ok(()) +} + +fn demonstrate_temp_file_management(processor: &mut TvaiProcessor) -> std::result::Result<(), Box> { + println!("\n=== Temporary File Management Demo ==="); + + let operation_id = processor.generate_operation_id(); + println!("Generated operation ID: {}", operation_id); + + // Create some temporary files + let temp_video = processor.create_temp_path(&operation_id, "input.mp4"); + let temp_output = processor.create_temp_path(&operation_id, "output.mp4"); + let temp_dir = processor.create_temp_dir(&operation_id)?; + + println!("Created temp files:"); + println!(" Video: {}", temp_video.display()); + println!(" Output: {}", temp_output.display()); + println!(" Directory: {}", temp_dir.display()); + + // Create a unique temp file + let unique_temp = processor.create_unique_temp_path("unique.tmp"); + println!(" Unique: {}", unique_temp.display()); + + // Get temp manager info + println!("Temp manager tracking {} files", processor.temp_manager().file_count()); + + // Clean up operation files + processor.cleanup_temp_files(&operation_id)?; + println!("Cleaned up operation files"); + + Ok(()) +} + +async fn demonstrate_progress_callback(processor: &TvaiProcessor) -> std::result::Result<(), Box> { + println!("\n=== Progress Callback Demo ==="); + + // Create a progress callback + let progress_callback: ProgressCallback = Box::new(|progress| { + let percentage = (progress * 100.0) as u32; + println!("Progress: {}%", percentage); + }); + + // Simulate FFmpeg command execution with progress + let args = ["-version"]; + let _output = processor.execute_ffmpeg_command(&args, false, Some(&progress_callback)).await?; + + println!("Command executed with progress tracking"); + + Ok(()) +} + +fn demonstrate_validation(processor: &TvaiProcessor) -> std::result::Result<(), Box> { + println!("\n=== Validation Demo ==="); + + // Test input file validation (this will fail since file doesn't exist) + let test_input = Path::new("nonexistent_input.mp4"); + match processor.validate_input_file(test_input) { + Ok(_) => println!("Input file validation passed"), + Err(e) => println!("Input file validation failed (expected): {}", e), + } + + // Test output path validation + let test_output = processor.temp_dir().join("test_output.mp4"); + match processor.validate_output_path(&test_output) { + Ok(_) => println!("Output path validation passed"), + Err(e) => println!("Output path validation failed: {}", e), + } + + Ok(()) +} + +async fn demonstrate_system_info(processor: &TvaiProcessor) -> std::result::Result<(), Box> { + println!("\n=== System Information Demo ==="); + + // Get FFmpeg versions + if let Ok(system_version) = processor.get_ffmpeg_version(false).await { + println!("System FFmpeg: {}", system_version.lines().next().unwrap_or("Unknown")); + } + + if let Ok(topaz_version) = processor.get_ffmpeg_version(true).await { + println!("Topaz FFmpeg: {}", topaz_version.lines().next().unwrap_or("Unknown")); + } + + // Check GPU availability + println!("GPU enabled: {}", processor.is_gpu_enabled()); + println!("GPU available: {}", processor.is_gpu_available()); + + // Get system detection info + let gpu_info = detect_gpu_support(); + println!("GPU Info: CUDA={}, OpenCL={}", gpu_info.cuda_available, gpu_info.opencl_available); + + let ffmpeg_info = detect_ffmpeg(); + println!("FFmpeg Info: System={}, Topaz={}", ffmpeg_info.system_available, ffmpeg_info.topaz_available); + + // Create sample metadata + let metadata = processor.create_metadata( + "sample_operation".to_string(), + Path::new("sample_input.mp4"), + "scale=2.0,model=iris-3".to_string(), + ); + println!("Sample metadata: operation_id={}, used_gpu={}", metadata.operation_id, metadata.used_gpu); + + Ok(()) +} diff --git a/cargos/tvai/src/core/mod.rs b/cargos/tvai/src/core/mod.rs index 12ee4e3..743f665 100644 --- a/cargos/tvai/src/core/mod.rs +++ b/cargos/tvai/src/core/mod.rs @@ -5,7 +5,7 @@ pub mod models; pub mod processor; // Re-export main types -pub use processor::{TvaiProcessor, TvaiConfig, ProcessResult, ProcessMetadata}; +pub use processor::{TvaiProcessor, TvaiConfig, ProcessResult, ProcessMetadata, ProgressCallback, ProcessingOptions}; pub use ffmpeg::FfmpegManager; use thiserror::Error; diff --git a/cargos/tvai/src/core/processor.rs b/cargos/tvai/src/core/processor.rs index a37b9e3..d1afefd 100644 --- a/cargos/tvai/src/core/processor.rs +++ b/cargos/tvai/src/core/processor.rs @@ -6,13 +6,28 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::core::{TvaiError, FfmpegManager}; +use crate::utils::temp::TempFileManager; + +/// Progress callback function type +pub type ProgressCallback = Box; + +/// Processing options with progress callback +#[derive(Default)] +pub struct ProcessingOptions { + /// Optional progress callback + pub progress_callback: Option, + /// Whether to save intermediate files for debugging + pub save_intermediate: bool, + /// Custom operation ID (auto-generated if None) + pub operation_id: Option, +} /// Main processor for Topaz Video AI operations #[derive(Debug)] pub struct TvaiProcessor { config: TvaiConfig, ffmpeg_manager: FfmpegManager, - temp_dir: PathBuf, + temp_manager: TempFileManager, } /// Configuration for TvaiProcessor @@ -130,31 +145,21 @@ impl TvaiProcessor { Some(&config.topaz_path), config.force_topaz_ffmpeg, ); - + // Verify Topaz FFmpeg is available for AI operations if !ffmpeg_manager.is_topaz_available() { return Err(TvaiError::TopazNotFound( config.topaz_path.display().to_string() )); } - - // Set up temporary directory - let temp_dir = match &config.temp_dir { - Some(dir) => { - std::fs::create_dir_all(dir)?; - dir.clone() - } - None => { - let temp_dir = std::env::temp_dir().join("tvai_temp"); - std::fs::create_dir_all(&temp_dir)?; - temp_dir - } - }; - + + // Set up temporary file manager + let temp_manager = TempFileManager::new(config.temp_dir.as_deref())?; + Ok(Self { config, ffmpeg_manager, - temp_dir, + temp_manager, }) } @@ -170,7 +175,17 @@ impl TvaiProcessor { /// Get the temporary directory pub fn temp_dir(&self) -> &Path { - &self.temp_dir + self.temp_manager.base_dir() + } + + /// Get mutable access to the temporary file manager + pub fn temp_manager_mut(&mut self) -> &mut TempFileManager { + &mut self.temp_manager + } + + /// Get read-only access to the temporary file manager + pub fn temp_manager(&self) -> &TempFileManager { + &self.temp_manager } /// Generate a unique operation ID @@ -179,25 +194,28 @@ impl TvaiProcessor { } /// Create a temporary file path with the given extension - pub fn create_temp_path(&self, operation_id: &str, suffix: &str) -> PathBuf { - self.temp_dir.join(format!("{}_{}", operation_id, suffix)) + pub fn create_temp_path(&mut self, operation_id: &str, suffix: &str) -> PathBuf { + self.temp_manager.create_temp_path(operation_id, suffix) } - + + /// Create a unique temporary file path + pub fn create_unique_temp_path(&mut self, suffix: &str) -> PathBuf { + self.temp_manager.create_unique_temp_path(suffix) + } + + /// Create a temporary directory for an operation + pub fn create_temp_dir(&mut self, operation_id: &str) -> Result { + self.temp_manager.create_temp_dir(operation_id) + } + /// Clean up temporary files for an operation - pub fn cleanup_temp_files(&self, operation_id: &str) -> Result<(), TvaiError> { - let pattern = format!("{}_", operation_id); - - if let Ok(entries) = std::fs::read_dir(&self.temp_dir) { - for entry in entries.flatten() { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with(&pattern) { - let _ = std::fs::remove_file(entry.path()); - } - } - } - } - - Ok(()) + pub fn cleanup_temp_files(&mut self, operation_id: &str) -> Result<(), TvaiError> { + self.temp_manager.cleanup_operation(operation_id) + } + + /// Clean up all temporary files + pub fn cleanup_all_temp_files(&mut self) -> Result<(), TvaiError> { + self.temp_manager.cleanup_all() } /// Check if GPU is available and enabled @@ -211,13 +229,130 @@ impl TvaiProcessor { // you might want to check for CUDA/OpenCL availability self.config.use_gpu } + + /// Execute FFmpeg command with progress tracking + pub async fn execute_ffmpeg_command( + &self, + args: &[&str], + for_topaz: bool, + progress_callback: Option<&ProgressCallback>, + ) -> Result { + // Report start progress + if let Some(callback) = progress_callback { + callback(0.0); + } + + let output = self.ffmpeg_manager.execute_command(args, for_topaz).await?; + + // Report completion + if let Some(callback) = progress_callback { + callback(1.0); + } + + Ok(output) + } + + /// Validate input file exists and is readable + pub fn validate_input_file(&self, path: &Path) -> Result<(), TvaiError> { + if !path.exists() { + return Err(TvaiError::FileNotFound(path.display().to_string())); + } + + if !path.is_file() { + return Err(TvaiError::InvalidParameter( + format!("Path is not a file: {}", path.display()) + )); + } + + // Try to read the file to check permissions + std::fs::File::open(path) + .map_err(|e| TvaiError::IoError(e))?; + + Ok(()) + } + + /// Validate output directory is writable + pub fn validate_output_path(&self, path: &Path) -> Result<(), TvaiError> { + if let Some(parent) = path.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + + // Check if directory is writable by creating a temp file + let test_file = parent.join(".tvai_write_test"); + std::fs::write(&test_file, b"test") + .map_err(|e| TvaiError::IoError(e))?; + std::fs::remove_file(&test_file)?; + } + + Ok(()) + } + + /// Get FFmpeg version information + pub async fn get_ffmpeg_version(&self, for_topaz: bool) -> Result { + self.ffmpeg_manager.get_version(for_topaz).await + } + + /// Create processing metadata + pub fn create_metadata( + &self, + operation_id: String, + input_path: &Path, + parameters: String, + ) -> ProcessMetadata { + ProcessMetadata { + operation_id, + input_path: input_path.to_path_buf(), + parameters, + used_gpu: self.is_gpu_enabled(), + ffmpeg_version: None, // Will be populated during processing + } + } } impl Drop for TvaiProcessor { fn drop(&mut self) { - // Clean up temporary directory if it was created by us - if self.config.temp_dir.is_none() { - let _ = std::fs::remove_dir_all(&self.temp_dir); - } + // TempFileManager will handle cleanup automatically + // when it's dropped due to its auto_cleanup feature + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use tempfile::TempDir; + + fn create_test_config() -> TvaiConfig { + // Use a fake path for testing + TvaiConfig::new(PathBuf::from("/fake/topaz/path")) + } + + #[test] + fn test_config_builder() { + let config = TvaiConfig::builder() + .topaz_path("/test/path") + .use_gpu(false) + .force_topaz_ffmpeg(false); + + // This will fail because the path doesn't exist, but that's expected in tests + assert!(config.build().is_err()); + } + + #[test] + fn test_operation_id_generation() { + let id1 = uuid::Uuid::new_v4().to_string(); + let id2 = uuid::Uuid::new_v4().to_string(); + + assert_ne!(id1, id2); + assert_eq!(id1.len(), 36); // Standard UUID length + } + + #[test] + fn test_processing_options_default() { + let options = ProcessingOptions::default(); + assert!(options.progress_callback.is_none()); + assert!(!options.save_intermediate); + assert!(options.operation_id.is_none()); } } diff --git a/cargos/tvai/src/lib.rs b/cargos/tvai/src/lib.rs index 6f35053..ac6f238 100644 --- a/cargos/tvai/src/lib.rs +++ b/cargos/tvai/src/lib.rs @@ -18,14 +18,14 @@ //! use tvai::*; //! //! #[tokio::main] -//! async fn main() -> Result<(), Box> { +//! async fn main() -> std::result::Result<(), Box> { //! // Quick video upscaling //! quick_upscale_video( //! std::path::Path::new("input.mp4"), //! std::path::Path::new("output.mp4"), //! 2.0, //! ).await?; -//! +//! //! Ok(()) //! } //! ``` @@ -37,11 +37,11 @@ pub mod config; pub mod utils; // Re-export main types for convenience -pub use core::{TvaiProcessor, TvaiConfig, ProcessResult, ProcessMetadata}; +pub use core::{TvaiProcessor, TvaiConfig, ProcessResult, ProcessMetadata, ProgressCallback, ProcessingOptions}; pub use video::{VideoUpscaleParams, InterpolationParams, VideoEnhanceParams}; pub use image::{ImageUpscaleParams, ImageFormat}; pub use config::{UpscaleModel, InterpolationModel, QualityPreset}; -pub use utils::{GpuInfo, FfmpegInfo, VideoInfo, ImageInfo}; +pub use utils::{GpuInfo, FfmpegInfo, VideoInfo, ImageInfo, TempFileManager}; // Re-export error types pub use core::TvaiError; diff --git a/cargos/tvai/src/utils/mod.rs b/cargos/tvai/src/utils/mod.rs index 5a6adc9..8a7688e 100644 --- a/cargos/tvai/src/utils/mod.rs +++ b/cargos/tvai/src/utils/mod.rs @@ -3,6 +3,9 @@ pub mod temp; pub mod gpu; +// Re-export main types +pub use temp::TempFileManager; + use std::path::{Path, PathBuf}; use std::time::Duration; use serde::{Deserialize, Serialize}; diff --git a/cargos/tvai/src/utils/temp.rs b/cargos/tvai/src/utils/temp.rs index edf7f1b..ad8cccd 100644 --- a/cargos/tvai/src/utils/temp.rs +++ b/cargos/tvai/src/utils/temp.rs @@ -1,4 +1,196 @@ //! Temporary file management utilities -// Placeholder for temp file management implementation -// This will be implemented in later stages +use std::path::{Path, PathBuf}; +use std::collections::HashSet; +use uuid::Uuid; +use crate::core::TvaiError; + +/// Manages temporary files for processing operations +#[derive(Debug)] +pub struct TempFileManager { + base_dir: PathBuf, + operation_files: HashSet, + auto_cleanup: bool, +} + +impl TempFileManager { + /// Create a new temporary file manager + pub fn new(base_dir: Option<&Path>) -> Result { + let base_dir = match base_dir { + Some(dir) => dir.to_path_buf(), + None => { + let temp_dir = std::env::temp_dir().join("tvai_temp"); + std::fs::create_dir_all(&temp_dir)?; + temp_dir + } + }; + + // Ensure base directory exists + std::fs::create_dir_all(&base_dir)?; + + Ok(Self { + base_dir, + operation_files: HashSet::new(), + auto_cleanup: true, + }) + } + + /// Set whether to automatically cleanup files on drop + pub fn set_auto_cleanup(&mut self, auto_cleanup: bool) { + self.auto_cleanup = auto_cleanup; + } + + /// Get the base temporary directory + pub fn base_dir(&self) -> &Path { + &self.base_dir + } + + /// Create a temporary file path for an operation + pub fn create_temp_path(&mut self, operation_id: &str, suffix: &str) -> PathBuf { + let filename = format!("{}_{}", operation_id, suffix); + let path = self.base_dir.join(filename); + self.operation_files.insert(path.clone()); + path + } + + /// Create a temporary file path with a random UUID + pub fn create_unique_temp_path(&mut self, suffix: &str) -> PathBuf { + let operation_id = Uuid::new_v4().to_string(); + self.create_temp_path(&operation_id, suffix) + } + + /// Create a temporary directory for an operation + pub fn create_temp_dir(&mut self, operation_id: &str) -> Result { + let dir_path = self.base_dir.join(format!("{}_dir", operation_id)); + std::fs::create_dir_all(&dir_path)?; + self.operation_files.insert(dir_path.clone()); + Ok(dir_path) + } + + /// Register an existing file for cleanup + pub fn register_file(&mut self, path: PathBuf) { + self.operation_files.insert(path); + } + + /// Clean up files for a specific operation + pub fn cleanup_operation(&mut self, operation_id: &str) -> Result<(), TvaiError> { + let pattern = format!("{}_", operation_id); + let mut to_remove = Vec::new(); + + for path in &self.operation_files { + if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { + if filename.starts_with(&pattern) { + to_remove.push(path.clone()); + } + } + } + + for path in to_remove { + self.remove_file_or_dir(&path)?; + self.operation_files.remove(&path); + } + + Ok(()) + } + + /// Clean up all temporary files + pub fn cleanup_all(&mut self) -> Result<(), TvaiError> { + let files_to_remove: Vec<_> = self.operation_files.iter().cloned().collect(); + + for path in files_to_remove { + self.remove_file_or_dir(&path)?; + } + + self.operation_files.clear(); + Ok(()) + } + + /// Remove a file or directory + fn remove_file_or_dir(&self, path: &Path) -> Result<(), TvaiError> { + if path.exists() { + if path.is_dir() { + std::fs::remove_dir_all(path)?; + } else { + std::fs::remove_file(path)?; + } + } + Ok(()) + } + + /// Get the number of tracked files + pub fn file_count(&self) -> usize { + self.operation_files.len() + } + + /// Check if a file is being tracked + pub fn is_tracked(&self, path: &Path) -> bool { + self.operation_files.contains(path) + } + + /// Get all tracked file paths + pub fn tracked_files(&self) -> Vec<&PathBuf> { + self.operation_files.iter().collect() + } +} + +impl Drop for TempFileManager { + fn drop(&mut self) { + if self.auto_cleanup { + let _ = self.cleanup_all(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use std::fs; + + #[test] + fn test_temp_file_manager_creation() { + let temp_dir = TempDir::new().unwrap(); + let manager = TempFileManager::new(Some(temp_dir.path())).unwrap(); + + assert_eq!(manager.base_dir(), temp_dir.path()); + assert_eq!(manager.file_count(), 0); + } + + #[test] + fn test_create_temp_path() { + let temp_dir = TempDir::new().unwrap(); + let mut manager = TempFileManager::new(Some(temp_dir.path())).unwrap(); + + let path = manager.create_temp_path("test_op", "input.mp4"); + + assert!(path.to_string_lossy().contains("test_op_input.mp4")); + assert_eq!(manager.file_count(), 1); + assert!(manager.is_tracked(&path)); + } + + #[test] + fn test_cleanup_operation() { + let temp_dir = TempDir::new().unwrap(); + let mut manager = TempFileManager::new(Some(temp_dir.path())).unwrap(); + + // Create files for different operations + let path1 = manager.create_temp_path("op1", "file1.txt"); + let path2 = manager.create_temp_path("op1", "file2.txt"); + let path3 = manager.create_temp_path("op2", "file3.txt"); + + // Create actual files + fs::write(&path1, "test1").unwrap(); + fs::write(&path2, "test2").unwrap(); + fs::write(&path3, "test3").unwrap(); + + assert_eq!(manager.file_count(), 3); + + // Clean up op1 files + manager.cleanup_operation("op1").unwrap(); + + assert_eq!(manager.file_count(), 1); + assert!(!path1.exists()); + assert!(!path2.exists()); + assert!(path3.exists()); + } +}