Skip to content

Commit eaf99b4

Browse files
Enhance Colpali and ColSmol integration with new embedding functionality
- Updated `colpali.py` to enable loading of the Colpali model directly, while commenting out the ONNX model loading for clarity. - Enhanced the `ColpaliModel` in `colpali.rs` to support both Colpali and ColSmol models based on the model ID provided. - Introduced a new `ColSmolEmbedder` in `colsmol.rs` for embedding images and files, with improved preprocessing capabilities. - Updated the `colsmol` module in `mod.rs` to include the new `ColSmolEmbedder`. - Refactored example usage in `colsmol.rs` to demonstrate the new embedding functionality with command-line argument parsing. - Added tests for the new embedding features to ensure reliability and correctness.
1 parent c20f9f5 commit eaf99b4

12 files changed

Lines changed: 2525 additions & 76 deletions

File tree

examples/colpali.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66

77
# Load the model
8-
# model: ColpaliModel = ColpaliModel.from_pretrained("vidore/colpali-v1.2-merged", None)
8+
model: ColpaliModel = ColpaliModel.from_pretrained("vidore/colpali-v1.2-merged", None)
99

1010
# Load ONNX Model
11-
model: ColpaliModel = ColpaliModel.from_pretrained_onnx(
12-
"starlight-ai/colpali-v1.2-merged-onnx", None
13-
)
11+
# model: ColpaliModel = ColpaliModel.from_pretrained_onnx(
12+
# "starlight-ai/colpali-v1.2-merged-onnx", None
13+
# )
1414

1515
# Get all PDF files in the directory
1616
directory = Path("test_files")

python/src/models/colpali.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use embed_anything::embeddings::local::colpali::ColPaliEmbed;
22
use embed_anything::embeddings::local::colpali::ColPaliEmbedder;
33
use embed_anything::embeddings::local::colpali_ort::OrtColPaliEmbedder;
4+
use embed_anything::embeddings::local::colsmol::ColSmolEmbedder;
45
use pyo3::exceptions::PyValueError;
56
use pyo3::prelude::*;
67
use pyo3::PyResult;
@@ -16,20 +17,40 @@ impl ColpaliModel {
1617
#[new]
1718
#[pyo3(signature = (model_id, revision=None))]
1819
pub fn new(model_id: &str, revision: Option<&str>) -> PyResult<Self> {
19-
let model = ColPaliEmbedder::new(model_id, revision)
20-
.map_err(|e| PyValueError::new_err(e.to_string()))?;
20+
let model_id_small = model_id.to_lowercase();
21+
let model = if model_id_small.contains("colpali") {
22+
Box::new(
23+
ColPaliEmbedder::new(model_id, revision)
24+
.map_err(|e| PyValueError::new_err(e.to_string()))?,
25+
) as Box<dyn ColPaliEmbed + Send + Sync>
26+
} else if model_id_small.contains("colsmol") {
27+
Box::new(
28+
ColSmolEmbedder::new(model_id, revision)
29+
.map_err(|e| PyValueError::new_err(e.to_string()))?,
30+
) as Box<dyn ColPaliEmbed + Send + Sync>
31+
} else {
32+
return Err(PyValueError::new_err("Invalid model ID"));
33+
};
2134
Ok(Self {
22-
model: Box::new(model),
35+
model,
2336
})
2437
}
2538

2639
#[staticmethod]
2740
#[pyo3(signature = (model_id, revision=None))]
2841
pub fn from_pretrained(model_id: &str, revision: Option<&str>) -> PyResult<Self> {
29-
let model = ColPaliEmbedder::new(model_id, revision)
30-
.map_err(|e| PyValueError::new_err(e.to_string()))?;
42+
let model_id_small = model_id.to_lowercase();
43+
let model = if model_id_small.contains("colpali") {
44+
Box::new(ColPaliEmbedder::new(model_id, revision)
45+
.map_err(|e| PyValueError::new_err(e.to_string()))?) as Box<dyn ColPaliEmbed + Send + Sync>
46+
} else if model_id_small.contains("colsmol") {
47+
Box::new(ColSmolEmbedder::new(model_id, revision)
48+
.map_err(|e| PyValueError::new_err(e.to_string()))?) as Box<dyn ColPaliEmbed + Send + Sync>
49+
} else {
50+
return Err(PyValueError::new_err("Invalid model ID"));
51+
};
3152
Ok(Self {
32-
model: Box::new(model),
53+
model,
3354
})
3455
}
3556

rust/examples/colsmol.rs

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
use clap::{Parser, ValueEnum};
22

3-
#[cfg(feature = "ort")]
4-
use embed_anything::embeddings::local::colsmol_ort::{OrtColSmolEmbedder, ColSmolEmbed};
5-
6-
3+
use embed_anything::embeddings::local::{colpali::ColPaliEmbed, colsmol::ColSmolEmbedder};
74

85
#[derive(Parser, Debug, Clone, ValueEnum)]
96
enum ModelType {
107
Ort,
8+
Normal,
119
}
1210

1311
#[derive(Parser, Debug)]
@@ -23,34 +21,22 @@ fn main() -> Result<(), anyhow::Error> {
2321

2422
let colpali_model = match args.model_type {
2523
ModelType::Ort => {
26-
#[cfg(feature = "ort")]
27-
{
28-
Box::new(OrtColSmolEmbedder::new(
29-
"onnx-community/colSmol-256M-ONNX",
30-
None,
31-
Some("onnx"),
32-
)?) as Box<dyn ColSmolEmbed>
33-
}
34-
#[cfg(not(feature = "ort"))]
35-
{
36-
panic!("ORT is not supported without ORT");
37-
}
24+
panic!("ORT is not supported without ORT");
25+
}
26+
ModelType::Normal => {
27+
Box::new(ColSmolEmbedder::new("akshayballal/colSmol-256M-merged", None)?)
28+
as Box<dyn ColPaliEmbed>
3829
}
39-
4030
};
4131
// ... rest of the code ...
4232

43-
let image_path = "test.jpg";
44-
let image = image::open(image_path)?;
45-
let embed_data = colpali_model.embed_image(image_path.into(), None)?;
46-
println!("{:?}", embed_data);
47-
// let file_path = "test_files/colpali.pdf";
48-
// let batch_size = 4;
49-
// let embed_data = colpali_model.embed_file(file_path.into(), batch_size)?;
50-
// println!("{:?}", embed_data.len());
33+
let file_path = "test_files/colpali.pdf";
34+
let batch_size = 2;
35+
let embed_data = colpali_model.embed_file(file_path.into(), batch_size)?;
36+
println!("{:?}", embed_data.len());
5137

52-
// let prompt = "What is attention?";
53-
// let query_embeddings = colpali_model.embed_query(prompt)?;
54-
// println!("{:?}", query_embeddings.len());
38+
let prompt = "What is attention?";
39+
let query_embeddings = colpali_model.embed_query(prompt)?;
40+
println!("{:?}", query_embeddings.len());
5541
Ok(())
5642
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use std::collections::HashMap;
2+
use std::path::PathBuf;
3+
use std::sync::RwLock;
4+
5+
use base64::Engine;
6+
use candle_core::{DType, Device};
7+
use candle_nn::VarBuilder;
8+
use image::ImageFormat;
9+
10+
use crate::embeddings::embed::{EmbedData, EmbeddingResult};
11+
use crate::embeddings::local::colpali::{get_images_from_pdf, ColPaliEmbed};
12+
use crate::embeddings::select_device;
13+
use crate::models::idefics3::model::{ColIdefics3Model, Idefics3Config};
14+
use crate::{
15+
models::idefics3::tensor_processing::Idefics3Processor,
16+
};
17+
18+
pub struct ColSmolEmbedder {
19+
pub model: RwLock<ColIdefics3Model>,
20+
pub processor: Idefics3Processor,
21+
pub device: Device,
22+
}
23+
24+
impl ColSmolEmbedder {
25+
pub fn new(model_id: &str, revision: Option<&str>) -> Result<Self, anyhow::Error> {
26+
let api = hf_hub::api::sync::Api::new()?;
27+
let repo: hf_hub::api::sync::ApiRepo = match revision {
28+
Some(rev) => api.repo(hf_hub::Repo::with_revision(
29+
model_id.to_string(),
30+
hf_hub::RepoType::Model,
31+
rev.to_string(),
32+
)),
33+
None => api.repo(hf_hub::Repo::new(
34+
model_id.to_string(),
35+
hf_hub::RepoType::Model,
36+
)),
37+
};
38+
39+
let model_file = repo.get("model.safetensors")?;
40+
let device = select_device();
41+
let dtype = if device.is_cuda() {
42+
DType::BF16
43+
} else {
44+
DType::F32
45+
};
46+
47+
let vb = unsafe {
48+
VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)?
49+
};
50+
let config_file = repo.get("config.json")?;
51+
52+
let processor = Idefics3Processor::from_pretrained("akshayballal/colSmol-256M-merged")?;
53+
let config: Idefics3Config = serde_json::from_slice(&std::fs::read(config_file)?)?;
54+
55+
56+
let model = ColIdefics3Model::load(&config, false, vb)?;
57+
58+
Ok(Self {
59+
model: RwLock::new(model),
60+
processor,
61+
device,
62+
})
63+
}
64+
}
65+
66+
impl ColPaliEmbed for ColSmolEmbedder {
67+
fn embed(
68+
&self,
69+
text_batch: &[&str],
70+
batch_size: Option<usize>,
71+
) -> Result<Vec<EmbeddingResult>, anyhow::Error> {
72+
let mut encodings = Vec::new();
73+
for mini_text_batch in text_batch.chunks(batch_size.unwrap_or(32)) {
74+
let (input_ids, attention_mask) = self
75+
.processor
76+
.tokenize_batch(mini_text_batch.to_vec(), &self.device)?;
77+
let batch_encodings = self
78+
.model
79+
.write()
80+
.map_err(|e| anyhow::anyhow!("{}", e))?
81+
.forward(&input_ids, &attention_mask, &None, &None)?
82+
.to_dtype(DType::F32)?;
83+
84+
encodings.extend(
85+
batch_encodings
86+
.to_vec3::<f32>()?
87+
.iter()
88+
.map(|x| EmbeddingResult::MultiVector(x.to_vec())),
89+
);
90+
}
91+
Ok(encodings)
92+
}
93+
94+
fn embed_query(&self, query: &str) -> anyhow::Result<Vec<EmbedData>> {
95+
let (input_ids, attention_mask) =
96+
self.processor.tokenize_batch(vec![query], &self.device)?;
97+
98+
let encoding = self
99+
.model
100+
.write()
101+
.map_err(|e| anyhow::anyhow!("{}", e))?
102+
.forward(&input_ids, &attention_mask, &None, &None)?
103+
.to_dtype(DType::F32)?
104+
.to_vec3::<f32>()?
105+
.into_iter()
106+
.map(|x| EmbeddingResult::MultiVector(x.to_vec()));
107+
108+
Ok(encoding
109+
.map(|x| EmbedData::new(x.clone(), None, None))
110+
.collect::<Vec<_>>())
111+
}
112+
113+
fn embed_image(
114+
&self,
115+
image_path: PathBuf,
116+
metadata: Option<HashMap<String, String>>,
117+
) -> anyhow::Result<EmbedData> {
118+
let image = image::open(image_path)?;
119+
let (input_ids, attention_mask, pixel_values, pixel_attention_mask) =
120+
self.processor.preprocess(&[image], &self.device)?;
121+
let encoding = self
122+
.model
123+
.write()
124+
.unwrap()
125+
.forward(
126+
&input_ids,
127+
&attention_mask,
128+
&Some(pixel_values),
129+
&pixel_attention_mask,
130+
)?
131+
.to_dtype(DType::F32)?
132+
.to_vec3::<f32>()?
133+
.into_iter()
134+
.map(|x| EmbeddingResult::MultiVector(x.to_vec()))
135+
.collect::<Vec<_>>();
136+
137+
Ok(EmbedData::new(encoding[0].clone(), None, metadata))
138+
}
139+
140+
fn embed_image_batch(&self, image_paths: &[PathBuf]) -> anyhow::Result<Vec<EmbedData>> {
141+
let images = image_paths
142+
.iter()
143+
.map(|path| image::open(path))
144+
.collect::<Result<Vec<_>, _>>()?;
145+
let (input_ids, attention_mask, pixel_values, pixel_attention_mask) =
146+
self.processor.preprocess(&images, &self.device)?;
147+
let encodings = self
148+
.model
149+
.write()
150+
.unwrap()
151+
.forward(
152+
&input_ids,
153+
&attention_mask,
154+
&Some(pixel_values),
155+
&pixel_attention_mask,
156+
)?
157+
.to_dtype(DType::F32)?
158+
.to_vec3::<f32>()?;
159+
160+
Ok(encodings
161+
.into_iter()
162+
.map(|x| EmbedData::new(EmbeddingResult::MultiVector(x), None, None))
163+
.collect::<Vec<_>>())
164+
}
165+
fn embed_file(&self, file_path: PathBuf, batch_size: usize) -> anyhow::Result<Vec<EmbedData>> {
166+
let pages = get_images_from_pdf(&file_path)?;
167+
let mut embed_data = Vec::new();
168+
for (index, batch) in pages.chunks(batch_size).enumerate() {
169+
let start_page = index * batch_size + 1;
170+
let end_page = start_page + batch.len();
171+
let page_numbers = (start_page..=end_page).collect::<Vec<_>>();
172+
let (input_ids, attention_mask, pixel_values, pixel_attention_mask) =
173+
self.processor.preprocess(batch, &self.device)?;
174+
175+
let image_embeddings = self
176+
.model
177+
.write()
178+
.map_err(|e| anyhow::anyhow!("{}", e))?
179+
.forward(
180+
&input_ids,
181+
&attention_mask,
182+
&Some(pixel_values),
183+
&pixel_attention_mask,
184+
)?
185+
.to_dtype(DType::F32)?
186+
.to_vec3::<f32>()?
187+
.into_iter()
188+
.map(|x| EmbeddingResult::MultiVector(x.to_vec()));
189+
190+
// zip the embeddings with the page numbers
191+
let embed_data_batch = image_embeddings
192+
.zip(page_numbers.into_iter())
193+
.zip(batch.iter())
194+
.map(|((embedding, page_number), page_image)| {
195+
let mut metadata = HashMap::new();
196+
197+
let mut buf = Vec::new();
198+
let mut cursor = std::io::Cursor::new(&mut buf);
199+
page_image.write_to(&mut cursor, ImageFormat::Png).unwrap();
200+
let engine = base64::engine::general_purpose::STANDARD;
201+
let base64_image = engine.encode(&buf);
202+
203+
metadata.insert("page_number".to_string(), page_number.to_string());
204+
metadata.insert(
205+
"file_path".to_string(),
206+
file_path.to_str().unwrap_or("").to_string(),
207+
);
208+
metadata.insert("image".to_string(), base64_image);
209+
EmbedData::new(embedding, None, Some(metadata))
210+
});
211+
embed_data.extend(embed_data_batch);
212+
}
213+
Ok(embed_data)
214+
}
215+
}

rust/src/embeddings/local/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ pub mod colbert;
55
pub mod colpali;
66
#[cfg(feature = "ort")]
77
pub mod colpali_ort;
8-
#[cfg(feature = "ort")]
9-
pub mod colsmol_ort;
108
pub mod jina;
119
pub mod model_info;
1210
pub mod modernbert;
@@ -18,3 +16,4 @@ pub mod pooling;
1816
pub mod text_embedding;
1917
pub mod model2vec;
2018
pub mod qwen3;
19+
pub mod colsmol;

0 commit comments

Comments
 (0)