use core::fmt;
use std::fmt::Formatter;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::{ffi::CString, fmt::Display};
use ahash::AHasher;
use llama_cpp_2::{
model::{AddBos, LlamaModel},
mtmd::{
MtmdBitmap, MtmdContext, MtmdContextParams, MtmdInputChunkType, MtmdInputChunks,
MtmdInputText,
},
token::LlamaToken,
};
use std::hash::{Hash, Hasher};
use tracing::{info, warn};
use crate::{errors::MultimodalError, errors::TokenizationError};
#[derive(Clone, Debug)]
pub struct Prompt {
parts: Vec<PromptPart>,
}
impl Display for Prompt {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let marker = llama_cpp_2::mtmd::mtmd_default_marker();
let result = self
.parts
.iter()
.map(|part| match part {
PromptPart::Text(text) => text.clone(),
PromptPart::Image(_) | PromptPart::Audio(_) => marker.to_string(),
})
.collect::<Vec<String>>()
.join("");
write!(f, "{}", result)
}
}
impl Default for Prompt {
fn default() -> Self {
Self::new()
}
}
impl Prompt {
pub fn new() -> Self {
Self { parts: vec![] }
}
pub fn push_text(&mut self, text: impl Into<String>) {
if let Some(PromptPart::Text(last_text)) = self.parts.last_mut() {
last_text.push_str(&text.into());
} else {
self.parts.push(PromptPart::Text(text.into()));
}
}
pub fn push_image(&mut self, image_path: &Path) {
self.parts.push(PromptPart::Image(image_path.into()));
}
pub fn push_audio(&mut self, audio_path: &Path) {
self.parts.push(PromptPart::Audio(audio_path.into()));
}
pub fn extract_asset_paths(&self) -> Vec<&Path> {
self.parts
.iter()
.filter_map(|part| match part {
PromptPart::Image(path) | PromptPart::Audio(path) => Some(path.as_path()),
PromptPart::Text(_) => None,
})
.collect()
}
pub(crate) fn extract_media_assets(&self) -> Vec<&PromptPart> {
self.parts
.iter()
.filter(|part| !matches!(part, PromptPart::Text(_)))
.collect()
}
}
#[derive(Clone, Debug)]
pub(crate) enum PromptPart {
Text(String),
Image(PathBuf),
Audio(PathBuf),
}
pub trait Promptable {
fn to_prompt(&self) -> Prompt;
}
impl Promptable for String {
fn to_prompt(&self) -> Prompt {
Prompt {
parts: vec![PromptPart::Text(self.clone())],
}
}
}
impl Promptable for Prompt {
fn to_prompt(&self) -> Prompt {
self.clone()
}
}
impl Promptable for &str {
fn to_prompt(&self) -> Prompt {
Prompt {
parts: vec![PromptPart::Text(self.to_string())],
}
}
}
impl From<String> for Prompt {
fn from(s: String) -> Self {
Prompt {
parts: vec![PromptPart::Text(s)],
}
}
}
impl From<&str> for Prompt {
fn from(s: &str) -> Self {
Prompt {
parts: vec![PromptPart::Text(s.to_string())],
}
}
}
pub type ChunkId = String;
#[derive(Clone, Debug)]
pub enum TokenizerChunk {
Text(Vec<LlamaToken>, ChunkId),
Image(Rc<MtmdInputChunks>, ChunkId),
Audio(Rc<MtmdInputChunks>, ChunkId),
}
impl TokenizerChunk {
pub fn new_text(tokens: Vec<LlamaToken>) -> Self {
let mut hasher = AHasher::default();
tokens.hash(&mut hasher);
let id = hasher.finish().to_string();
Self::Text(tokens, id)
}
pub fn new_image(chunks: MtmdInputChunks) -> Self {
let id = (0..chunks.len()).find_map(|i| {
chunks
.get(i)
.filter(|c| c.chunk_type() == MtmdInputChunkType::Image)
.map(|c| c.id().unwrap_or_default())
});
Self::Image(Rc::new(chunks), id.unwrap_or_default())
}
pub fn new_audio(chunks: MtmdInputChunks) -> Self {
let id = (0..chunks.len()).find_map(|i| {
chunks
.get(i)
.filter(|c| c.chunk_type() == MtmdInputChunkType::Audio)
.map(|c| c.id().unwrap_or_default())
});
Self::Audio(Rc::new(chunks), id.unwrap_or_default())
}
pub fn id(&self) -> &str {
match self {
Self::Text(_, id) | Self::Image(_, id) | Self::Audio(_, id) => id,
}
}
pub fn n_tokens(&self) -> usize {
match self {
TokenizerChunk::Text(tokens, _) => tokens.len(),
TokenizerChunk::Image(chunks_rc, _) | TokenizerChunk::Audio(chunks_rc, _) => (0
..chunks_rc.len())
.map(|i| chunks_rc.get(i).map(|c| c.n_tokens()).unwrap_or(0))
.sum(),
}
}
}
#[derive(Clone, Debug)]
pub struct TokenizerChunks {
chunks: Vec<TokenizerChunk>,
}
impl Default for TokenizerChunks {
fn default() -> Self {
Self::new()
}
}
impl std::iter::IntoIterator for TokenizerChunks {
type Item = TokenizerChunk;
type IntoIter = std::vec::IntoIter<TokenizerChunk>;
fn into_iter(self) -> Self::IntoIter {
self.chunks.into_iter()
}
}
impl TokenizerChunks {
pub fn n_tokens(&self) -> usize {
self.chunks.iter().map(|chunk| chunk.n_tokens()).sum()
}
pub fn token_ids(&self) -> Vec<Option<i32>> {
self.chunks
.iter()
.flat_map(|chunk| match chunk {
TokenizerChunk::Text(tokens, _) => {
tokens.iter().map(|token| Some(token.0)).collect::<Vec<_>>()
}
TokenizerChunk::Image(_, _) | TokenizerChunk::Audio(_, _) => {
vec![None; chunk.n_tokens()]
}
})
.collect()
}
pub fn len(&self) -> usize {
self.chunks.len()
}
pub fn is_empty(&self) -> bool {
self.chunks.is_empty()
}
pub fn new() -> Self {
Self { chunks: vec![] }
}
pub fn iter(&self) -> impl Iterator<Item = &TokenizerChunk> {
self.chunks.iter()
}
pub fn get(&self, index: usize) -> Option<&TokenizerChunk> {
self.chunks.get(index)
}
pub fn list_ids(&self) -> Vec<&str> {
self.chunks.iter().map(|chunk| chunk.id()).collect()
}
pub fn append(&mut self, other: TokenizerChunk) -> &mut Self {
let next = match (self.chunks.pop(), other) {
(Some(TokenizerChunk::Text(tokens, _)), TokenizerChunk::Text(other_tokens, _)) => {
let tokens = tokens.into_iter().chain(other_tokens).collect::<Vec<_>>();
TokenizerChunk::new_text(tokens)
}
(Some(last), other) => {
self.chunks.push(last);
other
}
(_, other) => other,
};
self.chunks.push(next);
self
}
pub fn chunk_bounds(&self, index: usize) -> (usize, usize) {
let mut start = 0;
let mut i = 0;
while i < index {
start += self.chunks[i].n_tokens();
i += 1;
}
let end = start + self.chunks[i].n_tokens();
(start, end)
}
pub fn tail(&self, from_pos: usize) -> TokenizerChunks {
if from_pos >= self.n_tokens() {
return TokenizerChunks::new();
}
let mut pos = 0;
let mut i = 0;
while i < self.chunks.len() {
let chunk_size = self.chunks[i].n_tokens();
if pos + chunk_size > from_pos {
break;
}
pos += chunk_size;
i += 1;
}
let offset_in_chunk = from_pos - pos;
match &self.chunks[i] {
TokenizerChunk::Text(tokens, _) => {
let (_, tail_tokens) = tokens.split_at(offset_in_chunk);
let mut new_chunks = vec![TokenizerChunk::new_text(tail_tokens.to_vec())];
new_chunks.extend_from_slice(&self.chunks[i + 1..]);
TokenizerChunks { chunks: new_chunks }
}
TokenizerChunk::Image(_chunks, _) | TokenizerChunk::Audio(_chunks, _) => {
TokenizerChunks {
chunks: self.chunks[i..].to_vec(),
}
}
}
}
}
pub fn find_chunks_prefix_difference(old: &TokenizerChunks, new: &TokenizerChunks) -> usize {
let longest_common_chunk_prefix_index = new
.iter()
.zip(old.iter())
.position(|(a, b)| a.id() != b.id());
let Some(chunk_prefix_index) = longest_common_chunk_prefix_index else {
if old.len() >= new.len() {
return new.n_tokens();
} else {
return old.n_tokens();
}
};
let (new_start, _) = new.chunk_bounds(chunk_prefix_index);
if let (Some(TokenizerChunk::Text(new_tokens, _)), Some(TokenizerChunk::Text(old_tokens, _))) =
(new.get(chunk_prefix_index), old.get(chunk_prefix_index))
{
let longest_common_prefix_index = new_tokens
.iter()
.zip(old_tokens.iter())
.position(|(a, b)| a != b);
if let Some(token_prefix_index) = longest_common_prefix_index {
return new_start + token_prefix_index;
}
}
new_start
}
#[derive(Debug)]
pub struct ProjectionModel {
pub ctx: MtmdContext, }
impl ProjectionModel {
pub fn from_path(
path: &std::path::Path,
parent_model: &LlamaModel,
use_gpu: bool,
) -> Result<Self, MultimodalError> {
let n_threads = std::thread::available_parallelism()
.map(|p| p.get() as i32)
.unwrap_or(4);
let media_marker = llama_cpp_2::mtmd::mtmd_default_marker().to_string();
let mtmd_params = MtmdContextParams {
use_gpu,
print_timings: false,
n_threads,
media_marker: CString::new(media_marker.to_string())
.expect("Failed to create CString for marker"),
image_min_tokens: -1,
image_max_tokens: -1,
};
match MtmdContext::init_from_file(&path.to_string_lossy(), parent_model, &mtmd_params) {
Ok(ctx) => {
info!("MTMD context initialized successfully");
Ok(Self { ctx })
}
Err(e) => {
warn!(error = %e, "Failed to initialize MTMD context:");
Err(MultimodalError::ContextNotInitialized)
}
}
}
pub fn tokenize(&self, bitmap: &MtmdBitmap) -> Result<TokenizerChunk, TokenizationError> {
let media_marker = llama_cpp_2::mtmd::mtmd_default_marker().to_string();
let mtmd_chunks = self
.ctx
.tokenize(
MtmdInputText {
text: media_marker,
add_special: false,
parse_special: true,
},
&[bitmap],
)
.map_err(|e| TokenizationError::ProjectionTokenizationError(e.to_string()))?;
if bitmap.is_audio() {
Ok(TokenizerChunk::new_audio(mtmd_chunks))
} else {
Ok(TokenizerChunk::new_image(mtmd_chunks))
}
}
pub fn load_image(&self, path: &Path) -> Result<MtmdBitmap, MultimodalError> {
let p = path.to_string_lossy().into_owned();
let bitmap = MtmdBitmap::from_file(&self.ctx, p.as_str(), false).map_err(|e| {
MultimodalError::LoadImage {
path: p.clone(),
error: e.to_string(),
}
})?;
info!(path = %p, "Loading image for MTMD");
Ok(bitmap)
}
pub fn load_audio(&self, path: &Path) -> Result<MtmdBitmap, MultimodalError> {
let p = path.to_string_lossy().into_owned();
let bitmap = MtmdBitmap::from_file(&self.ctx, p.as_str(), false).map_err(|e| {
MultimodalError::LoadAudio {
path: p.clone(),
error: e.to_string(),
}
})?;
info!(path = %p, "Loading audio for MTMD");
Ok(bitmap)
}
}
#[derive(Debug)]
pub struct Tokenizer<'a> {
model: &'a LlamaModel,
projection_model: Option<&'a ProjectionModel>,
add_bos: AddBos,
}
impl<'a> Tokenizer<'a> {
pub fn new(
model: &'a LlamaModel,
projection_model: Option<&'a ProjectionModel>,
add_bos: AddBos,
) -> Self {
Self {
projection_model,
add_bos,
model,
}
}
pub fn tokenize(
&self,
rendered_chat: String,
bitmaps: Vec<&MtmdBitmap>,
) -> Result<TokenizerChunks, TokenizationError> {
let text_chunks = self.tokenize_text(&rendered_chat)?;
let n_image_markers = text_chunks.len() - 1;
if n_image_markers != bitmaps.len() {
let preview = rendered_chat.chars().take(200).collect::<String>();
return Err(TokenizationError::MediaMarkerMismatch {
n_markers: n_image_markers,
n_bitmaps: bitmaps.len(),
template_preview: preview,
});
}
let image_chunks = if !bitmaps.is_empty() {
self.tokenize_media(bitmaps)?
} else {
vec![]
};
let chunks = self
.interleave(text_chunks, image_chunks)
.into_iter()
.filter(|chunk| chunk.n_tokens() > 0)
.collect();
Ok(TokenizerChunks { chunks })
}
fn tokenize_text(&self, text: &str) -> Result<Vec<TokenizerChunk>, TokenizationError> {
let media_marker = llama_cpp_2::mtmd::mtmd_default_marker().to_string();
let splits = text
.split(media_marker.as_str())
.enumerate()
.map(|(idx, split)| {
self.model
.str_to_token(
split,
if idx == 0 {
self.add_bos
} else {
AddBos::Never
},
)
.map(TokenizerChunk::new_text)
.map_err(|e| TokenizationError::TextTokenizationFailed {
position: idx,
text_preview: split.chars().take(100).collect(),
error: e.to_string(),
})
})
.collect::<Result<Vec<TokenizerChunk>, TokenizationError>>()?;
Ok(splits)
}
fn tokenize_media(
&self,
bitmaps: Vec<&MtmdBitmap>,
) -> Result<Vec<TokenizerChunk>, TokenizationError> {
let projection_model = self.projection_model.as_ref().ok_or(
TokenizationError::ProjectionTokenizationError("Context not initialized".to_string()),
)?;
bitmaps
.iter()
.map(|bitmap| projection_model.tokenize(bitmap))
.collect::<Result<Vec<_>, TokenizationError>>()
}
fn interleave<T>(&self, v1: Vec<T>, v2: Vec<T>) -> Vec<T> {
let mut ai = v1.into_iter();
let mut bi = v2.into_iter();
let mut out = Vec::new();
loop {
match (ai.next(), bi.next()) {
(Some(x), Some(y)) => {
out.push(x);
out.push(y);
}
(Some(x), None) => {
out.push(x);
out.extend(ai);
break;
}
(None, Some(y)) => {
out.push(y);
out.extend(bi);
break;
}
(None, None) => break,
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use llama_cpp_2::mtmd::MtmdInputChunks;
fn create_text_chunk(tokens: Vec<i32>) -> TokenizerChunk {
let llama_tokens: Vec<LlamaToken> = tokens.into_iter().map(LlamaToken::new).collect();
TokenizerChunk::new_text(llama_tokens)
}
fn create_chunks(chunks: Vec<TokenizerChunk>) -> TokenizerChunks {
TokenizerChunks { chunks }
}
fn create_image_chunk(id: &str) -> TokenizerChunk {
let chunks = MtmdInputChunks::new();
TokenizerChunk::Image(Rc::new(chunks), id.to_string())
}
#[test]
fn test_text_only_identical() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]), create_text_chunk(vec![4, 5, 6]), ]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]), create_text_chunk(vec![4, 5, 6]), ]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 6); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_text_only_new_longer() {
let old = create_chunks(vec![create_text_chunk(vec![1, 2, 3])]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]), create_text_chunk(vec![4, 5, 6]), ]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 3); }
#[test]
fn test_text_only_new_shorter() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]), create_text_chunk(vec![4, 5, 6]), ]);
let new = create_chunks(vec![create_text_chunk(vec![1, 2, 3])]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_text_only_partial_overlap_in_chunk() {
let old = create_chunks(vec![create_text_chunk(vec![1, 2, 3, 4, 5])]);
let new = create_chunks(vec![create_text_chunk(vec![1, 2, 3, 6, 7])]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 2); }
#[test]
fn test_text_only_no_overlap() {
let old = create_chunks(vec![create_text_chunk(vec![1, 2, 3])]);
let new = create_chunks(vec![create_text_chunk(vec![4, 5, 6])]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 3); }
#[test]
fn test_text_only_empty_old() {
let old = create_chunks(vec![]);
let new = create_chunks(vec![create_text_chunk(vec![1, 2, 3])]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 3); }
#[test]
fn test_text_only_empty_new() {
let old = create_chunks(vec![create_text_chunk(vec![1, 2, 3])]);
let new = create_chunks(vec![]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_text_only_multiple_chunks_differ_at_boundary() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]), create_text_chunk(vec![4]), create_text_chunk(vec![5, 6, 7, 8]), ]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]), create_text_chunk(vec![4]), create_text_chunk(vec![9, 10, 11]), ]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 4); assert_eq!(new.tail(prefix_index).n_tokens(), 3); }
#[test]
fn test_image_only_identical() {
let old = create_chunks(vec![create_image_chunk("image_1")]);
let new = create_chunks(vec![create_image_chunk("image_1")]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_image_only_collision() {
let old = create_chunks(vec![create_image_chunk("image_1")]);
let new = create_chunks(vec![create_image_chunk("image_2")]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_image_new_longer() {
let old = create_chunks(vec![create_image_chunk("image_1")]);
let new = create_chunks(vec![
create_image_chunk("image_1"),
create_image_chunk("image_2"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_image_new_shorter() {
let old = create_chunks(vec![
create_image_chunk("image_1"),
create_image_chunk("image_2"),
]);
let new = create_chunks(vec![create_image_chunk("image_1")]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_mixed_text_then_image_identical() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_image_chunk("image_1"),
]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_image_chunk("image_1"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_mixed_text_then_image_image_collision() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_image_chunk("image_1"),
]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_image_chunk("image_2"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_mixed_text_collision_before_image() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_image_chunk("image_1"),
]);
let new = create_chunks(vec![
create_text_chunk(vec![4, 5, 6]),
create_image_chunk("image_1"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 3); }
#[test]
fn test_mixed_text_partial_collision_before_image() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3, 4, 5]), create_image_chunk("image_1"),
]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3, 6, 7]), create_image_chunk("image_1"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 2); }
#[test]
fn test_mixed_image_then_text_identical() {
let old = create_chunks(vec![
create_image_chunk("image_1"),
create_text_chunk(vec![1, 2, 3]),
]);
let new = create_chunks(vec![
create_image_chunk("image_1"),
create_text_chunk(vec![1, 2, 3]),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_mixed_image_then_text_text_differs() {
let old = create_chunks(vec![
create_image_chunk("image_1"),
create_text_chunk(vec![1, 2, 3]),
]);
let new = create_chunks(vec![
create_image_chunk("image_1"),
create_text_chunk(vec![4, 5, 6]),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 3); }
#[test]
fn test_mixed_complex_interleaving() {
let old = create_chunks(vec![
create_text_chunk(vec![1]),
create_image_chunk("image_1"),
create_text_chunk(vec![2]),
create_image_chunk("image_2"),
]);
let new = create_chunks(vec![
create_text_chunk(vec![1]),
create_image_chunk("image_1"),
create_text_chunk(vec![2]),
create_image_chunk("image_3"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 2); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_mixed_text_to_image_collision() {
let old = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_text_chunk(vec![4, 5, 6]),
]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_image_chunk("image_1"),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 3); assert_eq!(new.tail(prefix_index).n_tokens(), 0); }
#[test]
fn test_mixed_image_to_text_collision() {
let old = create_chunks(vec![
create_image_chunk("image_1"),
create_text_chunk(vec![4, 5, 6]),
]);
let new = create_chunks(vec![
create_text_chunk(vec![1, 2, 3]),
create_text_chunk(vec![4, 5, 6]),
]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0); assert_eq!(new.tail(prefix_index).n_tokens(), 6); }
#[test]
fn test_empty_both() {
let old = create_chunks(vec![]);
let new = create_chunks(vec![]);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 0);
assert_eq!(new.tail(prefix_index).n_tokens(), 0);
}
#[test]
fn test_very_long_common_prefix() {
let mut old_chunks = Vec::new();
let mut new_chunks = Vec::new();
for i in 0..100 {
old_chunks.push(create_text_chunk(vec![i, i + 1, i + 2]));
new_chunks.push(create_text_chunk(vec![i, i + 1, i + 2]));
}
old_chunks.push(create_text_chunk(vec![1000, 1001]));
new_chunks.push(create_text_chunk(vec![2000, 2001]));
let old = create_chunks(old_chunks);
let new = create_chunks(new_chunks);
let prefix_index = find_chunks_prefix_difference(&old, &new);
assert_eq!(prefix_index, 300); assert_eq!(new.tail(prefix_index).n_tokens(), 2); }
}