use crate::b64::base64_encode;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ImageRef {
Bytes { media_type: String, bytes: Vec<u8> },
Base64 { media_type: String, base64: String },
Url(String),
}
impl ImageRef {
pub fn bytes(media_type: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
ImageRef::Bytes {
media_type: media_type.into(),
bytes: bytes.into(),
}
}
pub fn as_base64(&self) -> Option<(&str, std::borrow::Cow<'_, str>)> {
match self {
ImageRef::Bytes { media_type, bytes } => {
Some((media_type, base64_encode(bytes).into()))
}
ImageRef::Base64 { media_type, base64 } => Some((media_type, base64.as_str().into())),
ImageRef::Url(_) => None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ImageRequest {
pub prompt: String,
pub size: Option<String>,
pub n: u8,
pub references: Vec<ImageRef>,
}
impl ImageRequest {
pub fn new(prompt: impl Into<String>) -> Self {
ImageRequest {
prompt: prompt.into(),
size: None,
n: 1,
references: Vec::new(),
}
}
pub fn with_size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn with_n(mut self, n: u8) -> Self {
self.n = n;
self
}
pub fn with_references(mut self, references: Vec<ImageRef>) -> Self {
self.references = references;
self
}
pub fn count(&self) -> usize {
self.n.max(1) as usize
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedImage {
pub media_type: String,
#[serde(with = "crate::b64::serde_bytes_b64")]
pub bytes: Vec<u8>,
}
impl GeneratedImage {
pub fn new(media_type: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
GeneratedImage {
media_type: media_type.into(),
bytes: bytes.into(),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ImageError {
Transport(String),
Provider(String),
BadInput(String),
Unsupported(String),
RateLimited(String),
}
impl fmt::Display for ImageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImageError::Transport(s) => write!(f, "image transport: {s}"),
ImageError::Provider(s) => write!(f, "image provider: {s}"),
ImageError::BadInput(s) => write!(f, "image bad input: {s}"),
ImageError::Unsupported(s) => write!(f, "image unsupported: {s}"),
ImageError::RateLimited(s) => write!(f, "image rate limited: {s}"),
}
}
}
impl std::error::Error for ImageError {}
#[async_trait]
pub trait ImageModel: Send + Sync + 'static {
async fn generate(&self, req: &ImageRequest) -> Result<Vec<GeneratedImage>, ImageError>;
fn handle(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_normalises_zero_to_one() {
assert_eq!(ImageRequest::new("x").with_n(0).count(), 1);
assert_eq!(ImageRequest::new("x").count(), 1);
assert_eq!(ImageRequest::new("x").with_n(4).count(), 4);
}
#[test]
fn image_ref_bytes_and_base64_agree() {
let raw = ImageRef::bytes("image/png", b"foobar".to_vec());
let (mt, b64) = raw.as_base64().unwrap();
assert_eq!(mt, "image/png");
assert_eq!(b64.as_ref(), "Zm9vYmFy");
let pre = ImageRef::Base64 {
media_type: "image/png".into(),
base64: "Zm9vYmFy".into(),
};
assert_eq!(pre.as_base64().unwrap().1.as_ref(), b64.as_ref());
}
#[test]
fn image_ref_url_has_no_local_payload() {
assert!(
ImageRef::Url("https://x/a.png".into())
.as_base64()
.is_none()
);
}
#[test]
fn generated_image_round_trips_through_json() {
let img = GeneratedImage::new("image/jpeg", vec![0xff, 0xd8, 0xff, 0xe0, 0x00]);
let json = serde_json::to_string(&img).unwrap();
assert!(
json.contains("\"/9j/4AA=\""),
"expected base64 payload, got {json}"
);
assert_eq!(serde_json::from_str::<GeneratedImage>(&json).unwrap(), img);
}
}