1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ImageAnalysis {
7 pub description: String,
9 pub objects: Vec<DetectedObject>,
11 pub text_content: Option<String>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DetectedObject {
18 pub label: String,
20 pub confidence: f32,
22 pub bounding_box: [f32; 4],
24}
25
26impl DetectedObject {
27 pub fn area(&self) -> f32 {
29 let width = (self.bounding_box[2] - self.bounding_box[0]).max(0.0);
30 let height = (self.bounding_box[3] - self.bounding_box[1]).max(0.0);
31 width * height
32 }
33}
34
35#[derive(Debug, Clone)]
37pub enum VisionError {
38 UnsupportedFormat(String),
39 EmptyImage,
40 AnalysisFailed(String),
41}
42
43impl std::fmt::Display for VisionError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 VisionError::UnsupportedFormat(fmt) => write!(f, "unsupported image format: {}", fmt),
47 VisionError::EmptyImage => write!(f, "image data is empty"),
48 VisionError::AnalysisFailed(e) => write!(f, "analysis failed: {}", e),
49 }
50 }
51}
52
53impl std::error::Error for VisionError {}
54
55#[async_trait]
57pub trait ImageAnalyzer: Send + Sync {
58 async fn analyze(&self, image: &[u8]) -> Result<ImageAnalysis, VisionError>;
60
61 fn name(&self) -> &str;
63}
64
65pub struct NoopAnalyzer;
67
68#[async_trait]
69impl ImageAnalyzer for NoopAnalyzer {
70 async fn analyze(&self, image: &[u8]) -> Result<ImageAnalysis, VisionError> {
71 if image.is_empty() {
72 return Err(VisionError::EmptyImage);
73 }
74 Err(VisionError::AnalysisFailed(
75 "no analysis backend configured".into(),
76 ))
77 }
78
79 fn name(&self) -> &str {
80 "noop"
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_detected_object_area() {
90 let obj = DetectedObject {
91 label: "cat".into(),
92 confidence: 0.9,
93 bounding_box: [0.1, 0.2, 0.5, 0.8],
94 };
95 let area = obj.area();
96 assert!((area - 0.24).abs() < 0.001);
98 }
99
100 #[test]
101 fn test_analysis_types() {
102 let analysis = ImageAnalysis {
103 description: "A cat sitting on a mat".into(),
104 objects: vec![DetectedObject {
105 label: "cat".into(),
106 confidence: 0.95,
107 bounding_box: [0.1, 0.1, 0.9, 0.9],
108 }],
109 text_content: None,
110 };
111 assert_eq!(analysis.objects.len(), 1);
112 assert_eq!(analysis.objects[0].label, "cat");
113 }
114
115 #[tokio::test]
116 async fn test_noop_analyzer_empty() {
117 let a = NoopAnalyzer;
118 let result = a.analyze(b"").await;
119 assert!(matches!(result, Err(VisionError::EmptyImage)));
120 }
121
122 #[tokio::test]
123 async fn test_noop_analyzer_no_backend() {
124 let a = NoopAnalyzer;
125 let result = a.analyze(b"fake image data").await;
126 assert!(matches!(result, Err(VisionError::AnalysisFailed(_))));
127 }
128}