Skip to main content

quantum_sdk/
image.rs

1use serde::{Deserialize, Deserializer, Serialize};
2
3use crate::client::Client;
4use crate::error::Result;
5
6/// Deserialize null as Default::default() (e.g. null → empty Vec).
7fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
8where
9    D: Deserializer<'de>,
10    T: Default + Deserialize<'de>,
11{
12    Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
13}
14
15/// Request body for image generation.
16#[derive(Debug, Clone, Serialize, Default)]
17pub struct ImageRequest {
18    /// Image generation model (e.g. "grok-imagine-image", "gpt-image-1", "dall-e-3").
19    pub model: String,
20
21    /// Describes the image to generate.
22    pub prompt: String,
23
24    /// Number of images to generate (default 1).
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub count: Option<i32>,
27
28    /// Output dimensions (e.g. "1024x1024", "1536x1024").
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub size: Option<String>,
31
32    /// Aspect ratio (e.g. "16:9", "1:1").
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub aspect_ratio: Option<String>,
35
36    /// Quality level (e.g. "standard", "hd").
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub quality: Option<String>,
39
40    /// Image format (e.g. "png", "jpeg", "webp").
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub output_format: Option<String>,
43
44    /// Style preset (e.g. "vivid", "natural"). DALL-E 3 specific.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub style: Option<String>,
47
48    /// Background mode (e.g. "auto", "transparent", "opaque"). GPT-Image specific.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub background: Option<String>,
51
52    /// Image URL or data URI for image-to-3D conversion (Meshy).
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub image_url: Option<String>,
55
56    // ── Meshy 3D generation options ──
57
58    /// Mesh topology: "triangle" or "quad".
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub topology: Option<String>,
61
62    /// Target polygon count (100-300,000).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub target_polycount: Option<i32>,
65
66    /// Symmetry mode: "auto", "on", or "off".
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub symmetry_mode: Option<String>,
69
70    /// Pose mode: "", "a-pose", or "t-pose".
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub pose_mode: Option<String>,
73
74    /// Generate PBR texture maps (base_color, metallic, roughness, normal).
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub enable_pbr: Option<bool>,
77}
78
79/// Response from image generation.
80#[derive(Debug, Clone, Deserialize)]
81pub struct ImageResponse {
82    /// Generated images.
83    #[serde(default, deserialize_with = "deserialize_null_as_default")]
84    pub images: Vec<GeneratedImage>,
85
86    /// Model that generated the images.
87    #[serde(default)]
88    pub model: String,
89
90    /// Total cost in ticks.
91    #[serde(default)]
92    pub cost_ticks: i64,
93
94    /// Post-deduction credit balance in ticks (Receipt Pattern). Zero for
95    /// free/cached calls or responses that predate the field.
96    #[serde(default)]
97    pub balance_after: i64,
98
99    /// Unique request identifier.
100    #[serde(default)]
101    pub request_id: String,
102}
103
104/// A single generated image.
105#[derive(Debug, Clone, Deserialize)]
106pub struct GeneratedImage {
107    /// Base64-encoded image data.
108    pub base64: String,
109
110    /// Image format (e.g. "png", "jpeg").
111    pub format: String,
112
113    /// Image index within the batch.
114    pub index: i32,
115}
116
117/// Request body for image editing.
118#[derive(Debug, Clone, Serialize, Default)]
119pub struct ImageEditRequest {
120    /// Editing model (e.g. "gpt-image-1", "grok-imagine-image").
121    pub model: String,
122
123    /// Describes the desired edit.
124    pub prompt: String,
125
126    /// Base64-encoded input images.
127    pub input_images: Vec<String>,
128
129    /// Number of edited images to generate (default 1).
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub count: Option<i32>,
132
133    /// Output dimensions.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub size: Option<String>,
136}
137
138/// Response from image editing (same shape as generation).
139pub type ImageEditResponse = ImageResponse;
140
141impl Client {
142    /// Generates images from a text prompt.
143    pub async fn generate_image(&self, req: &ImageRequest) -> Result<ImageResponse> {
144        let (mut resp, meta) = self
145            .post_json::<ImageRequest, ImageResponse>("/qai/v1/images/generate", req)
146            .await?;
147        if resp.cost_ticks == 0 {
148            resp.cost_ticks = meta.cost_ticks;
149        }
150        if resp.balance_after == 0 {
151            resp.balance_after = meta.balance_after;
152        }
153        if resp.request_id.is_empty() {
154            resp.request_id = meta.request_id;
155        }
156        Ok(resp)
157    }
158
159    /// Edits images using an AI model.
160    pub async fn edit_image(&self, req: &ImageEditRequest) -> Result<ImageEditResponse> {
161        let (mut resp, meta) = self
162            .post_json::<ImageEditRequest, ImageEditResponse>("/qai/v1/images/edit", req)
163            .await?;
164        if resp.cost_ticks == 0 {
165            resp.cost_ticks = meta.cost_ticks;
166        }
167        if resp.balance_after == 0 {
168            resp.balance_after = meta.balance_after;
169        }
170        if resp.request_id.is_empty() {
171            resp.request_id = meta.request_id;
172        }
173        Ok(resp)
174    }
175}