agentai_image/
generator.rs1use crate::provider::{
4 self, ImageEditRequest, ImageError, ImageModel, ImageProvider, ImageRequest, ImageResponse,
5};
6use agentai_core::message::Media;
7use agentai_core::storage::MediaSave;
8use std::sync::Arc;
9
10#[derive(Clone)]
24pub struct ImageGenerator {
25 provider: Arc<dyn ImageProvider>,
26 storage: Option<Arc<dyn MediaSave>>,
27 size: Option<String>,
28 quality: Option<String>,
29}
30
31impl std::fmt::Debug for ImageGenerator {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("ImageGenerator")
34 .field("model", &self.provider.model())
35 .field("storage", &self.storage.is_some())
36 .field("size", &self.size)
37 .field("quality", &self.quality)
38 .finish()
39 }
40}
41
42impl ImageGenerator {
43 pub fn new(model: &str) -> Result<Self, ImageError> {
46 Ok(Self::with_provider_arc(provider::from_model(model)?.into()))
47 }
48
49 pub fn from_provider(provider: impl ImageProvider) -> Self {
52 Self::with_provider_arc(Arc::new(provider))
53 }
54
55 fn with_provider_arc(provider: Arc<dyn ImageProvider>) -> Self {
56 Self {
57 provider,
58 storage: None,
59 size: None,
60 quality: None,
61 }
62 }
63
64 pub fn with_storage(mut self, storage: impl MediaSave) -> Self {
66 self.storage = Some(Arc::new(storage));
67 self
68 }
69
70 pub fn with_size(mut self, size: impl Into<String>) -> Self {
73 self.size = Some(size.into());
74 self
75 }
76
77 pub fn with_quality(mut self, quality: impl Into<String>) -> Self {
80 self.quality = Some(quality.into());
81 self
82 }
83
84 pub fn model(&self) -> Option<&str> {
86 self.provider.model()
87 }
88
89 pub fn size(&self) -> Option<&str> {
91 self.size.as_deref()
92 }
93
94 pub fn quality(&self) -> Option<&str> {
96 self.quality.as_deref()
97 }
98
99 pub fn supports_edit(&self) -> bool {
101 self.provider.supports_edit()
102 }
103
104 pub async fn list_models(&self) -> Result<Vec<ImageModel>, ImageError> {
106 self.provider.list_models().await
107 }
108
109 pub async fn generate(&self, prompt: impl Into<String>) -> Result<ImageResponse, ImageError> {
111 self.generate_with(ImageRequest::new(prompt)).await
112 }
113
114 pub async fn generate_with(
116 &self,
117 mut request: ImageRequest,
118 ) -> Result<ImageResponse, ImageError> {
119 request.size = request.size.or_else(|| self.size.clone());
120 request.quality = request.quality.or_else(|| self.quality.clone());
121 let name = request.name.take();
122 let response = self.provider.generate(request).await?;
123 self.store(response, name).await
124 }
125
126 pub async fn edit(
128 &self,
129 prompt: impl Into<String>,
130 images: Vec<Media>,
131 ) -> Result<ImageResponse, ImageError> {
132 self.edit_with(ImageEditRequest::new(prompt, images)).await
133 }
134
135 pub async fn edit_with(
137 &self,
138 mut request: ImageEditRequest,
139 ) -> Result<ImageResponse, ImageError> {
140 request.size = request.size.or_else(|| self.size.clone());
141 request.quality = request.quality.or_else(|| self.quality.clone());
142 let name = request.name.take();
143 let response = self.provider.edit(request).await?;
144 self.store(response, name).await
145 }
146
147 async fn store(
150 &self,
151 mut response: ImageResponse,
152 name: Option<String>,
153 ) -> Result<ImageResponse, ImageError> {
154 let extension = response
155 .image
156 .content_type
157 .rsplit('/')
158 .next()
159 .unwrap_or("png");
160 let name = name
161 .or(response.image.name.take())
162 .unwrap_or_else(|| "image".to_string());
163 response.image.name = Some(if name.contains('.') {
164 name
165 } else {
166 format!("{name}.{extension}")
167 });
168 if let Some(storage) = &self.storage {
169 response.id = Some(storage.save(&response.image).await?);
170 }
171 Ok(response)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::provider::openai::tests::{MockHttp, json_response, provider};
179 use agentai_core::storage::{MediaRead, MemoryStorage};
180 use serde_json::{Value, json};
181
182 #[tokio::test]
183 async fn defaults_fill_unset_parameters_and_the_storage_sets_the_id() {
184 let http = MockHttp::new(vec![
185 json_response(200, json!({"data": [{"b64_json": "aGk="}]})),
186 json_response(200, json!({"data": [{"b64_json": "aGk="}]})),
187 ]);
188 let storage = MemoryStorage::new();
189 let generator = ImageGenerator::from_provider(provider(&http))
190 .with_size("512x512")
191 .with_quality("low")
192 .with_storage(storage.clone());
193
194 let response = generator.generate("a cat").await.unwrap();
195 generator
196 .generate_with(ImageRequest::new("a dog").with_size("1024x1024"))
197 .await
198 .unwrap();
199
200 let body =
201 |i: usize| -> Value { serde_json::from_slice(&http.request(i).body.unwrap()).unwrap() };
202 assert_eq!(body(0)["size"], json!("512x512"));
203 assert_eq!(body(0)["quality"], json!("low"));
204 assert_eq!(body(1)["size"], json!("1024x1024"));
205 let id = response.id.clone().unwrap();
206 assert_eq!(storage.read(&id).await.unwrap(), response.image);
207 assert_eq!(response.image.name.as_deref(), Some("image.png"));
208 }
209
210 #[tokio::test]
211 async fn the_requested_name_reaches_the_storage_with_the_image_extension() {
212 let reply = || json_response(200, json!({"data": [{"b64_json": "aGk="}]}));
213 let http = MockHttp::new(vec![reply(), reply()]);
214 let storage = MemoryStorage::new();
215 let generator =
216 ImageGenerator::from_provider(provider(&http)).with_storage(storage.clone());
217
218 let plain = generator
219 .generate_with(ImageRequest::new("a cat").with_name("cat"))
220 .await
221 .unwrap();
222 let with_extension = generator
223 .generate_with(ImageRequest::new("a dog").with_name("dog.webp"))
224 .await
225 .unwrap();
226
227 assert_eq!(plain.id.as_deref(), Some("1-cat.png"));
228 assert_eq!(with_extension.id.as_deref(), Some("2-dog.webp"));
229 let body: Value = serde_json::from_slice(&http.request(0).body.unwrap()).unwrap();
230 assert!(body.get("name").is_none());
231 }
232}