oxify_connect_vision/
diagnostics.rs

1//! Diagnostic utilities for vision/OCR troubleshooting.
2//!
3//! This module provides system diagnostics, error suggestions,
4//! and troubleshooting helpers for common vision/OCR issues.
5
6use std::path::Path;
7
8/// System diagnostic information.
9#[derive(Debug, Clone)]
10pub struct SystemDiagnostics {
11    /// Operating system name
12    pub os: String,
13    /// CPU architecture
14    pub arch: String,
15    /// Available memory (MB)
16    pub memory_mb: Option<u64>,
17    /// CUDA availability
18    pub cuda_available: bool,
19    /// CoreML availability
20    pub coreml_available: bool,
21    /// Tesseract installation detected
22    pub tesseract_installed: bool,
23}
24
25impl SystemDiagnostics {
26    /// Collect system diagnostics.
27    pub fn collect() -> Self {
28        Self {
29            os: std::env::consts::OS.to_string(),
30            arch: std::env::consts::ARCH.to_string(),
31            memory_mb: Self::get_available_memory(),
32            cuda_available: Self::check_cuda(),
33            coreml_available: Self::check_coreml(),
34            tesseract_installed: Self::check_tesseract(),
35        }
36    }
37
38    /// Get available system memory in MB.
39    fn get_available_memory() -> Option<u64> {
40        // Basic memory check - could be enhanced with sysinfo crate
41        #[cfg(target_os = "linux")]
42        {
43            std::fs::read_to_string("/proc/meminfo")
44                .ok()
45                .and_then(|content| {
46                    content
47                        .lines()
48                        .find(|line| line.starts_with("MemAvailable:"))
49                        .and_then(|line| {
50                            line.split_whitespace()
51                                .nth(1)
52                                .and_then(|s| s.parse::<u64>().ok())
53                                .map(|kb| kb / 1024)
54                        })
55                })
56        }
57        #[cfg(not(target_os = "linux"))]
58        {
59            None
60        }
61    }
62
63    /// Check if CUDA is available.
64    fn check_cuda() -> bool {
65        #[cfg(feature = "cuda")]
66        {
67            // Check for CUDA library
68            std::process::Command::new("nvidia-smi")
69                .output()
70                .map(|output| output.status.success())
71                .unwrap_or(false)
72        }
73        #[cfg(not(feature = "cuda"))]
74        {
75            false
76        }
77    }
78
79    /// Check if CoreML is available.
80    fn check_coreml() -> bool {
81        #[cfg(all(target_os = "macos", feature = "coreml"))]
82        {
83            true
84        }
85        #[cfg(not(all(target_os = "macos", feature = "coreml")))]
86        {
87            false
88        }
89    }
90
91    /// Check if Tesseract is installed.
92    fn check_tesseract() -> bool {
93        #[cfg(feature = "tesseract")]
94        {
95            std::process::Command::new("tesseract")
96                .arg("--version")
97                .output()
98                .map(|output| output.status.success())
99                .unwrap_or(false)
100        }
101        #[cfg(not(feature = "tesseract"))]
102        {
103            false
104        }
105    }
106}
107
108/// Error diagnostic information with suggestions.
109#[derive(Debug, Clone)]
110pub struct ErrorDiagnostic {
111    /// Error category
112    pub category: ErrorCategory,
113    /// Suggested fixes
114    pub suggestions: Vec<String>,
115    /// Documentation links
116    pub docs_links: Vec<String>,
117    /// System diagnostics (optional)
118    pub system_info: Option<SystemDiagnostics>,
119}
120
121/// Categories of errors for diagnostic purposes.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum ErrorCategory {
124    /// Model loading issues
125    ModelLoad,
126    /// Model not loaded
127    ModelNotLoaded,
128    /// Image format/decoding issues
129    ImageFormat,
130    /// ONNX Runtime issues
131    OnnxRuntime,
132    /// Tesseract issues
133    Tesseract,
134    /// Configuration issues
135    Configuration,
136    /// Resource exhaustion
137    Resources,
138    /// GPU/Hardware issues
139    Hardware,
140    /// Other/unknown
141    Other,
142}
143
144impl ErrorDiagnostic {
145    /// Create diagnostic for model loading error.
146    pub fn model_load(path: &str) -> Self {
147        let mut suggestions = vec![
148            "Verify the model file exists at the specified path".to_string(),
149            "Check file permissions (read access required)".to_string(),
150            "Ensure the model file is in ONNX format".to_string(),
151            format!("Path provided: {}", path),
152        ];
153
154        // Check if path exists
155        if !Path::new(path).exists() {
156            suggestions.push("ERROR: Path does not exist".to_string());
157            suggestions.push("Download models from provider documentation".to_string());
158        } else if !Path::new(path).is_file() {
159            suggestions.push("ERROR: Path is a directory, not a file".to_string());
160            suggestions.push("Point to the .onnx model file directly".to_string());
161        }
162
163        Self {
164            category: ErrorCategory::ModelLoad,
165            suggestions,
166            docs_links: vec![
167                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#provider-setup".to_string(),
168            ],
169            system_info: Some(SystemDiagnostics::collect()),
170        }
171    }
172
173    /// Create diagnostic for model not loaded error.
174    pub fn model_not_loaded() -> Self {
175        Self {
176            category: ErrorCategory::ModelNotLoaded,
177            suggestions: vec![
178                "Call provider.load_model().await before processing images".to_string(),
179                "Ensure load_model() completed successfully (check for errors)".to_string(),
180                "Example: let provider = create_provider(&config)?; provider.load_model().await?;".to_string(),
181            ],
182            docs_links: vec![
183                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#quick-start".to_string(),
184            ],
185            system_info: None,
186        }
187    }
188
189    /// Create diagnostic for image format error.
190    pub fn image_format(error_msg: &str) -> Self {
191        let mut suggestions = vec![
192            "Supported formats: PNG, JPEG, WebP, TIFF, BMP".to_string(),
193            "Check image file is not corrupted".to_string(),
194            "Verify image dimensions are reasonable (1-10000 pixels)".to_string(),
195        ];
196
197        if error_msg.contains("decode") {
198            suggestions
199                .push("Try opening the image in an image viewer to verify it's valid".to_string());
200            suggestions
201                .push("Consider converting to PNG format for better compatibility".to_string());
202        }
203
204        Self {
205            category: ErrorCategory::ImageFormat,
206            suggestions,
207            docs_links: vec![
208                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#troubleshooting".to_string(),
209            ],
210            system_info: None,
211        }
212    }
213
214    /// Create diagnostic for ONNX Runtime error.
215    pub fn onnx_runtime(error_msg: &str) -> Self {
216        let system_info = SystemDiagnostics::collect();
217        let mut suggestions = vec![];
218
219        // GPU-related errors
220        if error_msg.contains("CUDA") || error_msg.contains("cuda") {
221            suggestions.push("CUDA error detected".to_string());
222            if system_info.cuda_available {
223                suggestions.push("CUDA runtime detected but model execution failed".to_string());
224                suggestions.push("Check NVIDIA driver version: nvidia-smi".to_string());
225                suggestions.push("Verify GPU has sufficient memory".to_string());
226            } else {
227                suggestions.push("CUDA not available on this system".to_string());
228                suggestions.push("Install NVIDIA drivers and CUDA toolkit".to_string());
229                suggestions.push("Or disable GPU: set use_gpu=false in config".to_string());
230            }
231        }
232
233        // CoreML-related errors
234        if error_msg.contains("CoreML") || error_msg.contains("coreml") {
235            suggestions.push("CoreML error detected".to_string());
236            if !system_info.coreml_available {
237                suggestions.push("CoreML is only available on macOS".to_string());
238                suggestions.push("Disable GPU or use CUDA on other platforms".to_string());
239            } else {
240                suggestions.push("CoreML detected but execution failed".to_string());
241                suggestions.push("Check macOS version (CoreML requires 10.13+)".to_string());
242            }
243        }
244
245        // Memory errors
246        if error_msg.contains("memory") || error_msg.contains("Memory") {
247            suggestions.push("Memory allocation failed".to_string());
248            if let Some(mem_mb) = system_info.memory_mb {
249                suggestions.push(format!("Available memory: {} MB", mem_mb));
250                if mem_mb < 2048 {
251                    suggestions.push("WARNING: Low memory detected (< 2GB available)".to_string());
252                    suggestions.push("Close other applications to free memory".to_string());
253                    suggestions.push("Consider using mock provider for testing".to_string());
254                }
255            }
256            suggestions.push("Try processing smaller images".to_string());
257            suggestions.push("Reduce cache size if enabled".to_string());
258        }
259
260        // Model compatibility
261        if error_msg.contains("opset") || error_msg.contains("Opset") {
262            suggestions.push("ONNX opset version mismatch".to_string());
263            suggestions.push("Update ONNX Runtime: cargo update ort".to_string());
264            suggestions.push("Or download compatible model version".to_string());
265        }
266
267        // Generic fallback
268        if suggestions.is_empty() {
269            suggestions.push("ONNX Runtime execution failed".to_string());
270            suggestions.push("Check model file integrity".to_string());
271            suggestions.push("Try re-downloading the model files".to_string());
272            suggestions.push(format!("System: {} {}", system_info.os, system_info.arch));
273        }
274
275        Self {
276            category: ErrorCategory::OnnxRuntime,
277            suggestions,
278            docs_links: vec![
279                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#onnx-runtime-errors".to_string(),
280            ],
281            system_info: Some(system_info),
282        }
283    }
284
285    /// Create diagnostic for Tesseract error.
286    pub fn tesseract(error_msg: &str) -> Self {
287        let system_info = SystemDiagnostics::collect();
288        let mut suggestions = vec![];
289
290        if !system_info.tesseract_installed {
291            suggestions.push("Tesseract OCR not detected on system".to_string());
292            suggestions.push("Install Tesseract:".to_string());
293
294            match system_info.os.as_str() {
295                "linux" => {
296                    suggestions.push("  Ubuntu/Debian: sudo apt install tesseract-ocr".to_string());
297                    suggestions.push("  Fedora: sudo dnf install tesseract".to_string());
298                }
299                "macos" => {
300                    suggestions.push("  macOS: brew install tesseract".to_string());
301                }
302                "windows" => {
303                    suggestions.push(
304                        "  Windows: Download from https://github.com/UB-Mannheim/tesseract/wiki"
305                            .to_string(),
306                    );
307                }
308                _ => {
309                    suggestions
310                        .push("  See: https://github.com/tesseract-ocr/tesseract/wiki".to_string());
311                }
312            }
313        } else if error_msg.contains("language") || error_msg.contains("lang") {
314            suggestions.push("Language data file missing".to_string());
315            suggestions.push("Install language packs:".to_string());
316            suggestions.push("  Ubuntu/Debian: sudo apt install tesseract-ocr-<lang>".to_string());
317            suggestions.push("  Example: sudo apt install tesseract-ocr-jpn".to_string());
318            suggestions.push("Check installed languages: tesseract --list-langs".to_string());
319        } else {
320            suggestions.push("Tesseract execution failed".to_string());
321            suggestions.push("Verify Tesseract is working: tesseract --version".to_string());
322            suggestions.push("Check image quality (DPI, contrast, noise)".to_string());
323        }
324
325        Self {
326            category: ErrorCategory::Tesseract,
327            suggestions,
328            docs_links: vec![
329                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#tesseract-installation".to_string(),
330            ],
331            system_info: Some(system_info),
332        }
333    }
334
335    /// Create diagnostic for configuration error.
336    pub fn configuration(config_issue: &str) -> Self {
337        let mut suggestions = vec![
338            format!("Configuration issue: {}", config_issue),
339            "Review provider configuration parameters".to_string(),
340        ];
341
342        if config_issue.contains("model_path") {
343            suggestions
344                .push("model_path is required for ONNX providers (Surya, PaddleOCR)".to_string());
345            suggestions.push(
346                "Example: VisionProviderConfig::surya(\"/path/to/models\", false)".to_string(),
347            );
348        }
349
350        if config_issue.contains("language") {
351            suggestions.push("Check language code format".to_string());
352            suggestions.push("Examples: 'en', 'ja', 'zh', 'ko', 'de', 'fr'".to_string());
353        }
354
355        Self {
356            category: ErrorCategory::Configuration,
357            suggestions,
358            docs_links: vec![
359                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#quick-start".to_string(),
360            ],
361            system_info: None,
362        }
363    }
364
365    /// Create diagnostic for resource exhaustion.
366    pub fn resource_exhaustion(resource: &str) -> Self {
367        let system_info = SystemDiagnostics::collect();
368        let mut suggestions = vec![format!("Resource exhausted: {}", resource)];
369
370        if resource.contains("memory") {
371            if let Some(mem_mb) = system_info.memory_mb {
372                suggestions.push(format!("Available memory: {} MB", mem_mb));
373            }
374            suggestions.push("Close unnecessary applications".to_string());
375            suggestions.push("Process images in smaller batches".to_string());
376            suggestions.push("Reduce cache size".to_string());
377            suggestions.push("Consider using CPU instead of GPU".to_string());
378        }
379
380        if resource.contains("cache") {
381            suggestions.push("Clear cache: cache.clear()".to_string());
382            suggestions.push("Reduce cache size: cache.set_max_entries(100)".to_string());
383        }
384
385        Self {
386            category: ErrorCategory::Resources,
387            suggestions,
388            docs_links: vec![
389                "https://github.com/cool-japan/oxify/blob/main/crates/oxify-connect-vision/README.md#memory-issues".to_string(),
390            ],
391            system_info: Some(system_info),
392        }
393    }
394
395    /// Format diagnostic as a user-friendly string.
396    pub fn format(&self) -> String {
397        let mut output = String::new();
398
399        output.push_str("\n╔══════════════════════════════════════════════════════════════╗\n");
400        output.push_str("║  DIAGNOSTIC INFORMATION                                      ║\n");
401        output.push_str("╚══════════════════════════════════════════════════════════════╝\n\n");
402
403        // Suggestions
404        if !self.suggestions.is_empty() {
405            output.push_str("💡 Suggestions:\n");
406            for (i, suggestion) in self.suggestions.iter().enumerate() {
407                output.push_str(&format!("   {}. {}\n", i + 1, suggestion));
408            }
409            output.push('\n');
410        }
411
412        // Documentation links
413        if !self.docs_links.is_empty() {
414            output.push_str("📚 Documentation:\n");
415            for link in &self.docs_links {
416                output.push_str(&format!("   {}\n", link));
417            }
418            output.push('\n');
419        }
420
421        // System information
422        if let Some(ref sys_info) = self.system_info {
423            output.push_str("🖥️  System Information:\n");
424            output.push_str(&format!("   OS: {} ({})\n", sys_info.os, sys_info.arch));
425            if let Some(mem_mb) = sys_info.memory_mb {
426                output.push_str(&format!("   Available Memory: {} MB\n", mem_mb));
427            }
428            output.push_str(&format!("   CUDA Available: {}\n", sys_info.cuda_available));
429            output.push_str(&format!(
430                "   CoreML Available: {}\n",
431                sys_info.coreml_available
432            ));
433            output.push_str(&format!(
434                "   Tesseract Installed: {}\n",
435                sys_info.tesseract_installed
436            ));
437        }
438
439        output
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_system_diagnostics_collect() {
449        let diag = SystemDiagnostics::collect();
450        assert!(!diag.os.is_empty());
451        assert!(!diag.arch.is_empty());
452    }
453
454    #[test]
455    fn test_error_diagnostic_model_load() {
456        let diag = ErrorDiagnostic::model_load("/nonexistent/path/model.onnx");
457        assert_eq!(diag.category, ErrorCategory::ModelLoad);
458        assert!(!diag.suggestions.is_empty());
459        assert!(!diag.docs_links.is_empty());
460    }
461
462    #[test]
463    fn test_error_diagnostic_model_not_loaded() {
464        let diag = ErrorDiagnostic::model_not_loaded();
465        assert_eq!(diag.category, ErrorCategory::ModelNotLoaded);
466        assert!(!diag.suggestions.is_empty());
467    }
468
469    #[test]
470    fn test_error_diagnostic_image_format() {
471        let diag = ErrorDiagnostic::image_format("decode error");
472        assert_eq!(diag.category, ErrorCategory::ImageFormat);
473        assert!(!diag.suggestions.is_empty());
474    }
475
476    #[test]
477    fn test_error_diagnostic_onnx_runtime() {
478        let diag = ErrorDiagnostic::onnx_runtime("CUDA memory allocation failed");
479        assert_eq!(diag.category, ErrorCategory::OnnxRuntime);
480        assert!(!diag.suggestions.is_empty());
481        assert!(diag.system_info.is_some());
482    }
483
484    #[test]
485    fn test_error_diagnostic_tesseract() {
486        let diag = ErrorDiagnostic::tesseract("language not found");
487        assert_eq!(diag.category, ErrorCategory::Tesseract);
488        assert!(!diag.suggestions.is_empty());
489    }
490
491    #[test]
492    fn test_error_diagnostic_format() {
493        let diag = ErrorDiagnostic::model_not_loaded();
494        let formatted = diag.format();
495        assert!(formatted.contains("Suggestions"));
496        assert!(formatted.contains("Documentation"));
497    }
498
499    #[test]
500    fn test_error_categories() {
501        let categories = vec![
502            ErrorCategory::ModelLoad,
503            ErrorCategory::ModelNotLoaded,
504            ErrorCategory::ImageFormat,
505            ErrorCategory::OnnxRuntime,
506            ErrorCategory::Tesseract,
507            ErrorCategory::Configuration,
508            ErrorCategory::Resources,
509            ErrorCategory::Hardware,
510            ErrorCategory::Other,
511        ];
512
513        for category in categories {
514            // Ensure Debug works
515            let _ = format!("{:?}", category);
516        }
517    }
518}