use crate::llm::types::{ChatMessage, ContentPart, ImageDetail, ImageUrl, MessageContent, Role};
use anyhow::Result;
use std::path::Path;
pub fn encode_image_data_url(path: &Path) -> Result<String> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD_NO_PAD;
use std::fs;
let bytes = fs::read(path)?;
let mime_type = infer::get_from_path(path)?
.ok_or_else(|| anyhow::anyhow!("Unknown MIME type for: {:?}", path))?
.mime_type()
.to_string();
let base64 = STANDARD_NO_PAD.encode(&bytes);
Ok(format!("data:{};base64,{}", mime_type, base64))
}
pub fn encode_image_url(path: &Path) -> Result<ImageUrl> {
let url = encode_image_data_url(path)?;
Ok(ImageUrl { url, detail: None })
}
pub fn build_vision_message(text: &str, image_paths: &[String]) -> Result<MessageContent> {
let mut parts = vec![ContentPart::Text {
text: text.to_string(),
}];
for path_str in image_paths {
let path = Path::new(path_str);
let image_url = encode_image_url(path)?;
parts.push(ContentPart::Image { image_url });
}
Ok(MessageContent::Parts(parts))
}
pub fn build_vision_chat_message(text: &str, image_paths: &[String]) -> Result<ChatMessage> {
let content = build_vision_message(text, image_paths)?;
Ok(ChatMessage {
role: Role::User,
content: Some(content),
name: None,
tool_calls: None,
tool_call_id: None,
})
}
pub fn build_vision_chat_message_single(text: &str, image_path: &str) -> Result<ChatMessage> {
build_vision_chat_message(text, &[image_path.to_string()])
}
pub fn image_url_from_string(url: impl Into<String>) -> ImageUrl {
ImageUrl {
url: url.into(),
detail: None,
}
}
pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> ImageUrl {
ImageUrl {
url: url.into(),
detail: Some(detail),
}
}
pub trait ImageDetailExt {
fn as_str(&self) -> &str;
}
impl ImageDetailExt for ImageDetail {
fn as_str(&self) -> &str {
match self {
ImageDetail::Low => "low",
ImageDetail::High => "high",
ImageDetail::Auto => "auto",
}
}
}
pub fn is_image_file(path: &Path) -> bool {
match path.extension().and_then(|e| e.to_str()) {
Some(ext) => matches!(
ext.to_lowercase().as_str(),
"png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp"
),
None => false,
}
}
pub fn get_mime_type(path: &Path) -> Result<String> {
infer::get_from_path(path)?
.ok_or_else(|| anyhow::anyhow!("Unknown MIME type for: {:?}", path))
.map(|info| info.mime_type().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_image_file() {
assert!(is_image_file(Path::new("test.png")));
assert!(is_image_file(Path::new("test.JPG")));
assert!(is_image_file(Path::new("test.jpeg")));
assert!(!is_image_file(Path::new("test.txt")));
assert!(!is_image_file(Path::new("test.pdf")));
}
#[test]
fn test_image_detail_as_str() {
assert_eq!(ImageDetail::Low.as_str(), "low");
assert_eq!(ImageDetail::High.as_str(), "high");
assert_eq!(ImageDetail::Auto.as_str(), "auto");
}
#[test]
fn test_image_url_from_string() {
let url = image_url_from_string("https://example.com/image.png");
assert_eq!(url.url, "https://example.com/image.png");
assert!(url.detail.is_none());
}
#[test]
fn test_image_url_with_detail() {
let url = image_url_with_detail("https://example.com/image.png", ImageDetail::High);
assert_eq!(url.url, "https://example.com/image.png");
assert!(url.detail.is_some());
}
}