Skip to main content

agentai_image/
generator.rs

1//! [`ImageGenerator`]: generate and edit images from your code.
2
3use 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/// Generates and edits images with an [`ImageProvider`].
11///
12/// It is cheap to clone: clones share the provider and the storage.
13///
14/// ```rust,no_run
15/// # use agentai_image::ImageGenerator;
16/// # async fn run() -> Result<(), agentai_image::ImageError> {
17/// let generator = ImageGenerator::new("openai::gpt-image-1")?.with_size("1024x1024");
18/// let response = generator.generate("A lighthouse at dawn").await?;
19/// println!("{} image", response.image.content_type);
20/// # Ok(())
21/// # }
22/// ```
23#[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    /// Creates a generator for a model, see [`provider::from_model`]: the provider comes from
44    /// the name (`openai::gpt-image-1`) and its settings from environment variables.
45    pub fn new(model: &str) -> Result<Self, ImageError> {
46        Ok(Self::with_provider_arc(provider::from_model(model)?.into()))
47    }
48
49    /// Creates a generator with a provider you built, for example with the API key set in
50    /// code.
51    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    /// Saves every image in `storage` and sets [`ImageResponse::id`].
65    pub fn with_storage(mut self, storage: impl MediaSave) -> Self {
66        self.storage = Some(Arc::new(storage));
67        self
68    }
69
70    /// Size of every image that does not set its own, in the provider's format (for example
71    /// `1024x1024`).
72    pub fn with_size(mut self, size: impl Into<String>) -> Self {
73        self.size = Some(size.into());
74        self
75    }
76
77    /// Quality of every image that does not set its own, in the provider's format (for
78    /// example `high`).
79    pub fn with_quality(mut self, quality: impl Into<String>) -> Self {
80        self.quality = Some(quality.into());
81        self
82    }
83
84    /// Name of the model, if the provider has one.
85    pub fn model(&self) -> Option<&str> {
86        self.provider.model()
87    }
88
89    /// The default size.
90    pub fn size(&self) -> Option<&str> {
91        self.size.as_deref()
92    }
93
94    /// The default quality.
95    pub fn quality(&self) -> Option<&str> {
96        self.quality.as_deref()
97    }
98
99    /// Returns `true` when the provider can edit images.
100    pub fn supports_edit(&self) -> bool {
101        self.provider.supports_edit()
102    }
103
104    /// Lists the image models of the provider, see [`provider::list_models`].
105    pub async fn list_models(&self) -> Result<Vec<ImageModel>, ImageError> {
106        self.provider.list_models().await
107    }
108
109    /// Generates an image from a prompt.
110    pub async fn generate(&self, prompt: impl Into<String>) -> Result<ImageResponse, ImageError> {
111        self.generate_with(ImageRequest::new(prompt)).await
112    }
113
114    /// Generates an image from a full request. Unset size and quality take the defaults.
115    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    /// Edits images following a prompt.
127    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    /// Edits images with a full request. Unset size and quality take the defaults.
136    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    /// Names the image (the requested name, else the provider's, else `image`), adds the
148    /// extension of its type to a name without one, and saves it.
149    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}