use serde::Serialize;
use validator::Validate;
use super::super::traits::*;
#[derive(Clone, Serialize, Validate)]
#[validate(schema(function = "validate_image_body"))]
pub struct ImageGenBody<N>
where
N: ImageGen,
{
pub model: N,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1))]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<ImageQuality>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<ImageSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub watermark_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 6, max = 128))]
pub user_id: Option<String>,
}
impl<N> std::fmt::Debug for ImageGenBody<N>
where
N: ImageGen + std::fmt::Debug,
{
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ImageGenBody")
.field("model", &self.model)
.field("prompt_configured", &self.prompt.is_some())
.field("quality", &self.quality)
.field("size", &self.size)
.field("watermark_enabled", &self.watermark_enabled)
.field("user_id", &self.user_id.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
fn validate_image_body<N>(body: &ImageGenBody<N>) -> Result<(), validator::ValidationError>
where
N: ImageGen,
{
if body
.prompt
.as_deref()
.is_none_or(|prompt| prompt.trim().is_empty())
{
return Err(validator::ValidationError::new("prompt_required"));
}
let model = N::NAME;
if model == "glm-image" && matches!(body.quality, Some(ImageQuality::Standard)) {
return Err(validator::ValidationError::new(
"quality_not_supported_by_model",
));
}
if body
.size
.as_ref()
.is_some_and(|size| !size.is_valid_for_model(model))
{
return Err(validator::ValidationError::new("invalid_image_size"));
}
Ok(())
}
impl<N> ImageGenBody<N>
where
N: ImageGen,
{
pub fn new(model: N) -> Self {
Self {
model,
prompt: None,
quality: None,
size: None,
watermark_enabled: None,
user_id: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ImageQuality {
#[serde(rename = "hd")]
Hd,
#[serde(rename = "standard")]
Standard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ImageSize {
Size1280x1280,
Size1568x1056,
Size1056x1568,
Size1472x1088,
Size1088x1472,
Size1728x960,
Size960x1728,
Size1024x1024,
Size768x1344,
Size864x1152,
Size1344x768,
Size1152x864,
Size1440x720,
Size720x1440,
Custom {
width: u32,
height: u32,
},
}
impl serde::Serialize for ImageSize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let dimensions = match self {
ImageSize::Size1280x1280 => "1280x1280",
ImageSize::Size1568x1056 => "1568x1056",
ImageSize::Size1056x1568 => "1056x1568",
ImageSize::Size1472x1088 => "1472x1088",
ImageSize::Size1088x1472 => "1088x1472",
ImageSize::Size1728x960 => "1728x960",
ImageSize::Size960x1728 => "960x1728",
ImageSize::Size1024x1024 => "1024x1024",
ImageSize::Size768x1344 => "768x1344",
ImageSize::Size864x1152 => "864x1152",
ImageSize::Size1344x768 => "1344x768",
ImageSize::Size1152x864 => "1152x864",
ImageSize::Size1440x720 => "1440x720",
ImageSize::Size720x1440 => "720x1440",
ImageSize::Custom { width, height } => {
return serializer.collect_str(&format_args!("{width}x{height}"));
},
};
serializer.serialize_str(dimensions)
}
}
impl ImageSize {
pub fn is_valid(&self) -> bool {
let (width, height) = self.dimensions();
dimensions_are_valid(width, height, 512, 16, 1 << 21)
}
fn is_valid_for_model(&self, model: &str) -> bool {
if model == "glm-image"
&& matches!(
self,
Self::Size1280x1280
| Self::Size1568x1056
| Self::Size1056x1568
| Self::Size1472x1088
| Self::Size1088x1472
| Self::Size1728x960
| Self::Size960x1728
)
{
return true;
}
let (width, height) = self.dimensions();
if model == "glm-image" {
dimensions_are_valid(width, height, 1024, 32, 1 << 22)
} else {
dimensions_are_valid(width, height, 512, 16, 1 << 21)
}
}
pub fn dimensions(&self) -> (u32, u32) {
match self {
ImageSize::Size1280x1280 => (1280, 1280),
ImageSize::Size1568x1056 => (1568, 1056),
ImageSize::Size1056x1568 => (1056, 1568),
ImageSize::Size1472x1088 => (1472, 1088),
ImageSize::Size1088x1472 => (1088, 1472),
ImageSize::Size1728x960 => (1728, 960),
ImageSize::Size960x1728 => (960, 1728),
ImageSize::Size1024x1024 => (1024, 1024),
ImageSize::Size768x1344 => (768, 1344),
ImageSize::Size864x1152 => (864, 1152),
ImageSize::Size1344x768 => (1344, 768),
ImageSize::Size1152x864 => (1152, 864),
ImageSize::Size1440x720 => (1440, 720),
ImageSize::Size720x1440 => (720, 1440),
ImageSize::Custom { width, height } => (*width, *height),
}
}
}
fn dimensions_are_valid(
width: u32,
height: u32,
minimum: u32,
divisor: u32,
maximum_pixels: u64,
) -> bool {
(minimum..=2048).contains(&width)
&& (minimum..=2048).contains(&height)
&& width.is_multiple_of(divisor)
&& height.is_multiple_of(divisor)
&& u64::from(width) * u64::from(height) <= maximum_pixels
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::gen_image::{CogView4, GlmImage};
#[test]
fn body_validation_requires_prompt_and_valid_custom_size() {
assert!(ImageGenBody::new(CogView4 {}).validate().is_err());
let mut body = ImageGenBody::new(CogView4 {});
body.prompt = Some("image".into());
assert!(body.validate().is_ok());
body.size = Some(ImageSize::Custom {
width: 513,
height: 512,
});
assert!(body.validate().is_err());
}
#[test]
fn body_validation_applies_model_specific_size_and_quality_rules() {
let mut glm = ImageGenBody::new(GlmImage {});
glm.prompt = Some("image".into());
glm.size = Some(ImageSize::Size960x1728);
glm.quality = Some(ImageQuality::Hd);
assert!(glm.validate().is_ok());
glm.quality = Some(ImageQuality::Standard);
assert!(glm.validate().is_err());
let mut cogview = ImageGenBody::new(CogView4 {});
cogview.prompt = Some("image".into());
cogview.size = Some(ImageSize::Size1568x1056);
assert!(cogview.validate().is_ok());
cogview.size = Some(ImageSize::Size1280x1280);
assert!(cogview.validate().is_ok());
}
#[test]
fn debug_redacts_prompt_and_user_identifier() {
let mut body = ImageGenBody::new(GlmImage {});
body.prompt = Some("private image prompt".to_owned());
body.user_id = Some("private-user".to_owned());
let debug = format!("{body:?}");
assert!(!debug.contains("private image prompt"));
assert!(!debug.contains("private-user"));
assert!(debug.contains("prompt_configured: true"));
}
}