mofa_foundation/llm/
vision.rs1use crate::llm::types::{ChatMessage, ContentPart, ImageDetail, ImageUrl, MessageContent, Role};
10use anyhow::Result;
11use std::path::Path;
12
13pub fn encode_image_data_url(path: &Path) -> Result<String> {
27 use base64::Engine;
28 use base64::engine::general_purpose::STANDARD_NO_PAD;
29 use std::fs;
30
31 let bytes = fs::read(path)?;
32
33 let mime_type = infer::get_from_path(path)?
34 .ok_or_else(|| anyhow::anyhow!("Unknown MIME type for: {:?}", path))?
35 .mime_type()
36 .to_string();
37
38 let base64 = STANDARD_NO_PAD.encode(&bytes);
39 Ok(format!("data:{};base64,{}", mime_type, base64))
40}
41
42pub fn encode_image_url(path: &Path) -> Result<ImageUrl> {
50 let url = encode_image_data_url(path)?;
51 Ok(ImageUrl { url, detail: None })
52}
53
54pub fn build_vision_message(text: &str, image_paths: &[String]) -> Result<MessageContent> {
71 let mut parts = vec![ContentPart::Text {
72 text: text.to_string(),
73 }];
74
75 for path_str in image_paths {
76 let path = Path::new(path_str);
77 let image_url = encode_image_url(path)?;
78 parts.push(ContentPart::Image { image_url });
79 }
80
81 Ok(MessageContent::Parts(parts))
82}
83
84pub fn build_vision_chat_message(text: &str, image_paths: &[String]) -> Result<ChatMessage> {
101 let content = build_vision_message(text, image_paths)?;
102
103 Ok(ChatMessage {
104 role: Role::User,
105 content: Some(content),
106 name: None,
107 tool_calls: None,
108 tool_call_id: None,
109 })
110}
111
112pub fn build_vision_chat_message_single(text: &str, image_path: &str) -> Result<ChatMessage> {
121 build_vision_chat_message(text, &[image_path.to_string()])
122}
123
124pub fn image_url_from_string(url: impl Into<String>) -> ImageUrl {
132 ImageUrl {
133 url: url.into(),
134 detail: None,
135 }
136}
137
138pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> ImageUrl {
147 ImageUrl {
148 url: url.into(),
149 detail: Some(detail),
150 }
151}
152
153pub trait ImageDetailExt {
155 fn as_str(&self) -> &str;
157}
158
159impl ImageDetailExt for ImageDetail {
160 fn as_str(&self) -> &str {
161 match self {
162 ImageDetail::Low => "low",
163 ImageDetail::High => "high",
164 ImageDetail::Auto => "auto",
165 }
166 }
167}
168
169pub fn is_image_file(path: &Path) -> bool {
177 match path.extension().and_then(|e| e.to_str()) {
178 Some(ext) => matches!(
179 ext.to_lowercase().as_str(),
180 "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp"
181 ),
182 None => false,
183 }
184}
185
186pub fn get_mime_type(path: &Path) -> Result<String> {
194 infer::get_from_path(path)?
195 .ok_or_else(|| anyhow::anyhow!("Unknown MIME type for: {:?}", path))
196 .map(|info| info.mime_type().to_string())
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_is_image_file() {
205 assert!(is_image_file(Path::new("test.png")));
206 assert!(is_image_file(Path::new("test.JPG")));
207 assert!(is_image_file(Path::new("test.jpeg")));
208 assert!(!is_image_file(Path::new("test.txt")));
209 assert!(!is_image_file(Path::new("test.pdf")));
210 }
211
212 #[test]
213 fn test_image_detail_as_str() {
214 assert_eq!(ImageDetail::Low.as_str(), "low");
215 assert_eq!(ImageDetail::High.as_str(), "high");
216 assert_eq!(ImageDetail::Auto.as_str(), "auto");
217 }
218
219 #[test]
220 fn test_image_url_from_string() {
221 let url = image_url_from_string("https://example.com/image.png");
222 assert_eq!(url.url, "https://example.com/image.png");
223 assert!(url.detail.is_none());
224 }
225
226 #[test]
227 fn test_image_url_with_detail() {
228 let url = image_url_with_detail("https://example.com/image.png", ImageDetail::High);
229 assert_eq!(url.url, "https://example.com/image.png");
230 assert!(url.detail.is_some());
231 }
232}