use std::path::PathBuf;
use std::sync::Arc;
use cera::kv_cache::KvCompression;
use cera::model::Model;
use cera::tokenizer::BpeTokenizer;
use cera::{
FinishReason, GenerateOpts, ModalityCapabilities, ModalitySink, Session, SessionConfig,
};
fn make_session(model: Box<dyn Model>, tokenizer: BpeTokenizer, config: SessionConfig) -> Session {
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
Session::new(model, tokenizer, ModalityCapabilities::text_only(), config).unwrap()
}
fn find_model() -> Option<PathBuf> {
if let Ok(p) = std::env::var("CERA_MODEL") {
let pb = PathBuf::from(p);
return pb.exists().then_some(pb);
}
let home = std::env::var("HOME").ok()?;
let fallback =
PathBuf::from(&home).join(".leap/models/LFM2-VL-450M-Q4_0/LFM2-VL-450M-Q4_0.gguf");
fallback.exists().then_some(fallback)
}
struct CollectSink(Vec<u32>);
impl ModalitySink for CollectSink {
fn on_text_tokens(&mut self, tokens: &[u32]) {
self.0.extend_from_slice(tokens);
}
fn on_done(&mut self, _reason: FinishReason) {}
}
fn greedy_opts(max_tokens: u32) -> GenerateOpts {
GenerateOpts {
max_tokens,
temperature: 0.0,
..Default::default()
}
}
#[test]
#[ignore]
fn stochastic_generate_is_chainable_without_append() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("The capital of France is");
let mut session = make_session(
model,
tokenizer,
SessionConfig {
kv_compression: KvCompression::None,
seed: Some(42),
..Default::default()
},
);
session.append_tokens(&prompt_toks).unwrap();
let opts = GenerateOpts {
max_tokens: 4,
temperature: 0.8,
top_k: 40,
top_p: 0.9,
..Default::default()
};
let mut sink_a = CollectSink(Vec::new());
session.generate(&opts, &mut sink_a).unwrap();
assert_eq!(sink_a.0.len(), 4, "first call should generate 4 tokens");
let mut sink_b = CollectSink(Vec::new());
let summary = session.generate(&opts, &mut sink_b).unwrap();
assert_eq!(
summary.tokens_generated, 4,
"chained stochastic call should generate 4 tokens"
);
assert_eq!(sink_b.0.len(), 4);
assert_eq!(session.position() as usize, prompt_toks.len() + 8);
}
#[test]
#[ignore]
fn greedy_chain_requires_append_tokens() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("The capital of France is");
let mut session = make_session(
model,
tokenizer,
SessionConfig {
seed: Some(42),
..Default::default()
},
);
session.append_tokens(&prompt_toks).unwrap();
let mut sink_a = CollectSink(Vec::new());
session.generate(&greedy_opts(4), &mut sink_a).unwrap();
assert_eq!(sink_a.0.len(), 4);
let mut sink_err = CollectSink(Vec::new());
let err = session
.generate(&greedy_opts(4), &mut sink_err)
.unwrap_err();
assert!(
matches!(err, cera::CeraError::EmptyInput),
"expected EmptyInput on chained greedy generate without append, got: {err:?}"
);
let follow_up = session.tokenizer().encode(" and the currency is");
session.append_tokens(&follow_up).unwrap();
let mut sink_b = CollectSink(Vec::new());
let summary = session.generate(&greedy_opts(4), &mut sink_b).unwrap();
assert_eq!(summary.tokens_generated, 4);
}
#[test]
#[ignore]
fn no_kv_gap_after_bounded_generate_greedy() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("The capital of France is");
let mut session = make_session(
model,
tokenizer,
SessionConfig {
seed: Some(7),
..Default::default()
},
);
session.append_tokens(&prompt_toks).unwrap();
let mut sink = CollectSink(Vec::new());
session.generate(&greedy_opts(4), &mut sink).unwrap();
assert_eq!(sink.0.len(), 4);
let pos_after_gen = session.position() as usize;
assert_eq!(pos_after_gen, prompt_toks.len() + 4);
let follow_up = session.tokenizer().encode(" and the language is");
session.append_tokens(&follow_up).unwrap();
assert_eq!(
session.position() as usize,
pos_after_gen + follow_up.len(),
"append_tokens after bounded generate should advance position by the appended length"
);
let mut sink2 = CollectSink(Vec::new());
let summary = session.generate(&greedy_opts(4), &mut sink2).unwrap();
assert_eq!(summary.tokens_generated, 4);
assert_eq!(sink2.0.len(), 4);
let vocab_size = session.model().config().vocab_size as u32;
let eos = session.tokenizer().eos_token();
for t in &sink2.0 {
assert!(
*t < vocab_size,
"emitted token {t} outside vocab {vocab_size}"
);
assert!(
Some(*t) != eos,
"greedy continuation shouldn't immediately hit EOS on 'France is ... '"
);
}
}
#[test]
#[ignore]
fn reset_reseeds_sampler_for_reproducibility() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("Tell me a story about");
let mut session = make_session(
model,
tokenizer,
SessionConfig {
seed: Some(123),
..Default::default()
},
);
let opts = GenerateOpts {
max_tokens: 8,
temperature: 0.8,
top_k: 40,
top_p: 0.9,
..Default::default()
};
session.append_tokens(&prompt_toks).unwrap();
let mut sink_a = CollectSink(Vec::new());
session.generate(&opts, &mut sink_a).unwrap();
session.reset().unwrap();
session.append_tokens(&prompt_toks).unwrap();
let mut sink_b = CollectSink(Vec::new());
session.generate(&opts, &mut sink_b).unwrap();
assert_eq!(
sink_a.0, sink_b.0,
"same-seed run after reset() must produce identical stochastic output.\nrun A: {:?}\nrun B: {:?}",
sink_a.0, sink_b.0
);
}
#[test]
#[ignore]
fn position_handle_observes_progress() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("The capital of France is");
let mut session = make_session(
model,
tokenizer,
SessionConfig {
seed: Some(42),
..Default::default()
},
);
let pos_handle = session.position_handle();
assert_eq!(pos_handle.load(std::sync::atomic::Ordering::Relaxed), 0);
session.append_tokens(&prompt_toks).unwrap();
let after_append = pos_handle.load(std::sync::atomic::Ordering::Relaxed);
assert_eq!(after_append as usize, prompt_toks.len());
let mut sink = CollectSink(Vec::new());
session.generate(&greedy_opts(4), &mut sink).unwrap();
let after_gen = pos_handle.load(std::sync::atomic::Ordering::Relaxed);
assert_eq!(after_gen as usize, prompt_toks.len() + 4);
assert!(after_gen > after_append);
}
#[test]
#[ignore]
fn stochastic_split_matches_single_call_under_seed() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = Arc::new(cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap());
let prompt_toks = tokenizer.encode("Tell me a story about");
let stochastic_opts = |n: u32| GenerateOpts {
max_tokens: n,
temperature: 0.8,
top_k: 40,
top_p: 0.9,
..Default::default()
};
let baseline = {
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let model: Arc<dyn Model> = Arc::from(cera::model::load_model(gguf, None, 4096).unwrap());
let mut session = Session::new(
model,
Arc::clone(&tokenizer),
ModalityCapabilities::text_only(),
SessionConfig {
seed: Some(999),
..Default::default()
},
)
.unwrap();
session.append_tokens(&prompt_toks).unwrap();
let mut sink = CollectSink(Vec::new());
session.generate(&stochastic_opts(8), &mut sink).unwrap();
sink.0
};
let split = {
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let model: Arc<dyn Model> = Arc::from(cera::model::load_model(gguf, None, 4096).unwrap());
let mut session = Session::new(
model,
Arc::clone(&tokenizer),
ModalityCapabilities::text_only(),
SessionConfig {
seed: Some(999),
..Default::default()
},
)
.unwrap();
session.append_tokens(&prompt_toks).unwrap();
let mut sink1 = CollectSink(Vec::new());
session.generate(&stochastic_opts(4), &mut sink1).unwrap();
let mut sink2 = CollectSink(Vec::new());
session.generate(&stochastic_opts(4), &mut sink2).unwrap();
let mut all = sink1.0;
all.extend(sink2.0);
all
};
assert_eq!(
baseline, split,
"split stochastic generation must match single call with the same seed.\nbaseline: {baseline:?}\nsplit: {split:?}"
);
}
#[test]
#[ignore]
fn position_updates_per_token_during_decode() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("The capital of France is");
let mut session = make_session(
model,
tokenizer,
SessionConfig {
seed: Some(42),
..Default::default()
},
);
let pos_handle = session.position_handle();
session.append_tokens(&prompt_toks).unwrap();
let prefill_pos = pos_handle.load(std::sync::atomic::Ordering::Relaxed);
struct ProgressSink {
pos_handle: std::sync::Arc<std::sync::atomic::AtomicU32>,
observed: Vec<u32>,
}
impl ModalitySink for ProgressSink {
fn on_text_tokens(&mut self, _: &[u32]) {
self.observed
.push(self.pos_handle.load(std::sync::atomic::Ordering::Relaxed));
}
fn on_done(&mut self, _: FinishReason) {}
}
let opts = GenerateOpts {
max_tokens: 6,
temperature: 0.0,
flush_every_tokens: 1,
flush_every_ms: 0,
..Default::default()
};
let mut sink = ProgressSink {
pos_handle: pos_handle.clone(),
observed: Vec::new(),
};
session.generate(&opts, &mut sink).unwrap();
assert!(sink.observed.len() >= 2);
assert!(
sink.observed.last().copied().unwrap() > prefill_pos,
"position should advance during decode, but stayed at prefill pos {prefill_pos}. observed: {:?}",
sink.observed
);
for w in sink.observed.windows(2) {
assert!(w[0] <= w[1], "position went backwards: {:?}", sink.observed);
}
}
#[test]
#[ignore]
fn append_embeddings_advances_position_and_sets_logits() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let hidden_size = model.config().hidden_size;
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
let mut session = Session::new(
model,
tokenizer,
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
let n = 4;
let embeddings: Vec<f32> = (0..n * hidden_size)
.map(|i| ((i % 13) as f32) * 0.01 - 0.05)
.collect();
let pos_before = session.position();
session
.append_embeddings(&embeddings, n)
.expect("append_embeddings");
assert_eq!(
session.position(),
pos_before + n as u32,
"position must advance by exactly n_tokens"
);
let opts = greedy_opts(1);
let mut sink = CollectSink(Vec::new());
session.generate(&opts, &mut sink).expect("generate");
assert_eq!(sink.0.len(), 1, "should emit exactly one token");
}
#[test]
#[ignore]
fn forward_prefill_from_embeddings_matches_per_frame_loop() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf_a = cera::gguf::GgufFile::open(&model_path).unwrap();
let gguf_b = cera::gguf::GgufFile::open(&model_path).unwrap();
let model_a = cera::model::load_model(gguf_a, None, 4096).unwrap();
let model_b = cera::model::load_model(gguf_b, None, 4096).unwrap();
let cfg = model_a.config().clone();
let hidden_size = cfg.hidden_size;
let n: usize = 6;
let embeddings: Vec<f32> = (0..n * hidden_size)
.map(|i| (((i * 31 + 7) % 257) as f32) * 0.001 - 0.1)
.collect();
let mut state_a = cera::kv_cache::InferenceState::from_config(&cfg).unwrap();
let mut last_a: Vec<f32> = Vec::new();
for j in 0..n {
let frame = &embeddings[j * hidden_size..(j + 1) * hidden_size];
last_a = model_a.forward_from_embedding(frame, j, &mut state_a);
}
let mut state_b = cera::kv_cache::InferenceState::from_config(&cfg).unwrap();
let last_b = model_b.forward_prefill_from_embeddings(&embeddings, n, 0, &mut state_b);
assert_eq!(
last_a.len(),
last_b.len(),
"logit vector length mismatch between loop and batched paths"
);
let mut max_abs_diff = 0.0f32;
for (i, (a, b)) in last_a.iter().zip(last_b.iter()).enumerate() {
let d = (a - b).abs();
if d > max_abs_diff {
max_abs_diff = d;
}
assert!(
d < 1e-2,
"logit {i}: loop={a} batched={b} diff={d} (max so far {max_abs_diff})"
);
}
eprintln!("forward_prefill_from_embeddings parity: max |Δlogit| = {max_abs_diff:.4e}");
assert_eq!(state_a.seq_len, n);
assert_eq!(state_b.seq_len, n);
}
#[cfg(all(feature = "metal", target_os = "macos"))]
#[test]
#[ignore]
fn metal_forward_prefill_from_embeddings_matches_per_frame_loop() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
use cera::model::metal_lfm2::MetalLfm2Model;
let gguf_a = cera::gguf::GgufFile::open(&model_path).unwrap();
let gguf_b = cera::gguf::GgufFile::open(&model_path).unwrap();
let model_a = MetalLfm2Model::from_gguf(gguf_a, &model_path, 4096).unwrap();
let model_b = MetalLfm2Model::from_gguf(gguf_b, &model_path, 4096).unwrap();
let cfg = model_a.config().clone();
let hidden_size = cfg.hidden_size;
let n: usize = 6;
let embeddings: Vec<f32> = (0..n * hidden_size)
.map(|i| (((i * 31 + 7) % 257) as f32) * 0.001 - 0.1)
.collect();
let mut state_a = cera::kv_cache::InferenceState::from_config(&cfg).unwrap();
let mut last_a: Vec<f32> = Vec::new();
for j in 0..n {
let frame = &embeddings[j * hidden_size..(j + 1) * hidden_size];
last_a = model_a.forward_from_embedding(frame, j, &mut state_a);
}
let mut state_b = cera::kv_cache::InferenceState::from_config(&cfg).unwrap();
let last_b = model_b.forward_prefill_from_embeddings(&embeddings, n, 0, &mut state_b);
assert_eq!(
last_a.len(),
last_b.len(),
"logit vector length mismatch between Metal loop and batched paths"
);
let mut max_abs_diff = 0.0f32;
for (i, (a, b)) in last_a.iter().zip(last_b.iter()).enumerate() {
let d = (a - b).abs();
if d > max_abs_diff {
max_abs_diff = d;
}
assert!(
d < 5e-2,
"logit {i}: loop={a} batched={b} diff={d} (max so far {max_abs_diff})"
);
}
eprintln!("metal forward_prefill_from_embeddings parity: max |Δlogit| = {max_abs_diff:.4e}");
assert_eq!(state_a.seq_len, n);
assert_eq!(state_b.seq_len, n);
}
#[cfg(all(feature = "metal", target_os = "macos"))]
#[test]
#[ignore]
fn metal_concurrent_forward_prefill_does_not_corrupt() {
use std::sync::Arc;
use std::thread;
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
use cera::model::metal_lfm2::MetalLfm2Model;
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let shared_model: Arc<MetalLfm2Model> =
Arc::new(MetalLfm2Model::from_gguf(gguf, &model_path, 4096).unwrap());
let cfg = shared_model.config().clone();
let prompt_a: Vec<u32> = (10..30).collect();
let prompt_b: Vec<u32> = (50..70).collect();
let model_par_a = Arc::clone(&shared_model);
let model_par_b = Arc::clone(&shared_model);
let prompt_par_a = prompt_a.clone();
let prompt_par_b = prompt_b.clone();
let h_a = thread::spawn(move || {
let mut state = cera::kv_cache::InferenceState::from_config(model_par_a.config()).unwrap();
model_par_a.forward_prefill(&prompt_par_a, 0, &mut state)
});
let h_b = thread::spawn(move || {
let mut state = cera::kv_cache::InferenceState::from_config(model_par_b.config()).unwrap();
model_par_b.forward_prefill(&prompt_par_b, 0, &mut state)
});
let parallel_a = h_a.join().unwrap();
let parallel_b = h_b.join().unwrap();
assert!(
parallel_a.iter().all(|v| v.is_finite()),
"thread A produced non-finite logits — concurrent corruption?"
);
assert!(
parallel_b.iter().all(|v| v.is_finite()),
"thread B produced non-finite logits — concurrent corruption?"
);
assert_eq!(
parallel_a.len(),
cfg.vocab_size,
"thread A logits length mismatch"
);
assert_eq!(
parallel_b.len(),
cfg.vocab_size,
"thread B logits length mismatch"
);
eprintln!("concurrent forward_prefill: both threads produced finite logits");
}
#[test]
#[ignore]
fn append_audio_text_only_returns_unsupported_modality() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 1024).unwrap();
let mut session = make_session(model, tokenizer, SessionConfig::default());
let pcm = vec![0.0f32; 16_000];
let err = session.append_audio(&pcm, 16_000).unwrap_err();
assert!(
matches!(err, cera::CeraError::UnsupportedModality),
"expected UnsupportedModality for text-only session, got {err:?}"
);
}
#[test]
#[ignore]
fn append_audio_without_encoder_returns_backend_error() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 1024).unwrap();
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
let mut session = Session::new(
model,
tokenizer,
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
let pcm = vec![0.0f32; 16_000];
let err = session.append_audio(&pcm, 16_000).unwrap_err();
let cera::CeraError::Backend(msg) = err else {
panic!("expected Backend error for missing encoder, got {err:?}");
};
assert!(
msg.contains("attach_audio_encoder"),
"Backend error message should reference attach_audio_encoder; got: {msg}"
);
}
#[test]
#[ignore]
fn append_audio_wrong_sample_rate_returns_backend_error() {
use cera::model::audio_encoder::AudioEncoderWeights;
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let Ok(home) = std::env::var("HOME") else {
eprintln!("no HOME env — skipping");
return;
};
let mmproj_path = std::path::PathBuf::from(&home)
.join(".leap/models/LFM2.5-Audio-1.5B-Q4_0/mmproj-LFM2.5-Audio-1.5B-Q4_0.gguf");
if !mmproj_path.exists() {
eprintln!(
"no mmproj available at {} — skipping",
mmproj_path.display()
);
return;
}
let mmproj = cera::gguf::GgufFile::open_arc(&mmproj_path).unwrap();
let mut weights = AudioEncoderWeights::from_gguf(&mmproj).unwrap();
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 1024).unwrap();
weights.config.llm_hidden_size = model.config().hidden_size;
let encoder = Arc::new(weights);
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
let mut session = Session::new(
model,
tokenizer,
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
session.attach_audio_encoder(encoder);
let pcm = vec![0.0f32; 16_000];
let err = session.append_audio(&pcm, 24_000).unwrap_err();
let cera::CeraError::Backend(msg) = err else {
panic!("expected Backend error for sample-rate mismatch, got {err:?}");
};
assert!(
msg.contains("sample_rate") && msg.contains("16000"),
"Backend error should mention sample_rate + expected 16000; got: {msg}"
);
}
#[test]
#[ignore]
fn append_audio_end_to_end() {
use cera::model::audio_encoder::{AudioEncoderWeights, SAMPLE_RATE};
let Ok(home) = std::env::var("HOME") else {
eprintln!("no HOME env — skipping");
return;
};
let bundle = std::path::PathBuf::from(&home).join(".leap/models/LFM2.5-Audio-1.5B-Q4_0");
let primary = bundle.join("LFM2.5-Audio-1.5B-Q4_0.gguf");
let mmproj_path = bundle.join("mmproj-LFM2.5-Audio-1.5B-Q4_0.gguf");
if !primary.exists() || !mmproj_path.exists() {
eprintln!(
"no LFM2.5-Audio bundle (need {} and {}) — skipping",
primary.display(),
mmproj_path.display()
);
return;
}
let primary_gguf = cera::gguf::GgufFile::open(&primary).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&primary_gguf).unwrap();
let model = cera::model::load_model(primary_gguf, None, 2048).unwrap();
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
let mut session = Session::new(
model,
tokenizer,
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
let mmproj = cera::gguf::GgufFile::open_arc(&mmproj_path).unwrap();
let encoder = Arc::new(AudioEncoderWeights::from_gguf(&mmproj).unwrap());
session.attach_audio_encoder(encoder);
let n_samples = (SAMPLE_RATE as usize) / 2;
let pcm: Vec<f32> = (0..n_samples)
.map(|i| ((i as f32 * 0.07).sin() + (i as f32 * 0.013).sin()) * 0.05)
.collect();
let pos_before = session.position();
session
.append_audio(&pcm, SAMPLE_RATE)
.expect("append_audio");
let n_frames = session.position() - pos_before;
assert!(n_frames > 0, "encoder must produce at least one frame");
eprintln!(
"append_audio: 0.5 s @ {} Hz → {} encoder frames",
SAMPLE_RATE, n_frames
);
let opts = greedy_opts(1);
let mut sink = CollectSink(Vec::new());
let summary = session.generate(&opts, &mut sink).expect("generate");
eprintln!(
"post-audio generate: emitted {} tokens, finish_reason = {:?}",
sink.0.len(),
summary.finish_reason
);
assert!(
sink.0.len() <= 1,
"greedy(1) should emit 0 or 1 tokens, got {}",
sink.0.len()
);
}
#[test]
#[ignore]
fn reset_preserves_attached_audio_encoder() {
use cera::model::audio_encoder::AudioEncoderWeights;
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let Ok(home) = std::env::var("HOME") else {
eprintln!("no HOME env — skipping");
return;
};
let mmproj_path = std::path::PathBuf::from(&home)
.join(".leap/models/LFM2.5-Audio-1.5B-Q4_0/mmproj-LFM2.5-Audio-1.5B-Q4_0.gguf");
if !mmproj_path.exists() {
eprintln!(
"no mmproj available at {} — skipping",
mmproj_path.display()
);
return;
}
let mmproj = cera::gguf::GgufFile::open_arc(&mmproj_path).unwrap();
let encoder = Arc::new(AudioEncoderWeights::from_gguf(&mmproj).unwrap());
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 1024).unwrap();
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
let mut session = Session::new(
model,
tokenizer,
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
session.attach_audio_encoder(encoder);
session.reset().unwrap();
let pcm = vec![0.0f32; 16_000];
match session.append_audio(&pcm, 16_000) {
Err(cera::CeraError::Backend(msg)) => {
assert!(
!msg.contains("no audio encoder attached"),
"encoder should have been preserved across reset; got: {msg}"
);
}
Ok(()) => {
}
Err(other) => panic!("unexpected error after reset: {other:?}"),
}
}
#[test]
#[ignore]
fn append_audio_dimension_mismatch_returns_backend_error() {
use cera::model::audio_encoder::AudioEncoderWeights;
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let Ok(home) = std::env::var("HOME") else {
eprintln!("no HOME env — skipping");
return;
};
let mmproj_path = std::path::PathBuf::from(&home)
.join(".leap/models/LFM2.5-Audio-1.5B-Q4_0/mmproj-LFM2.5-Audio-1.5B-Q4_0.gguf");
if !mmproj_path.exists() {
eprintln!(
"no mmproj available at {} — skipping",
mmproj_path.display()
);
return;
}
let mmproj = cera::gguf::GgufFile::open_arc(&mmproj_path).unwrap();
let mut weights = AudioEncoderWeights::from_gguf(&mmproj).unwrap();
let actual = weights.config.llm_hidden_size;
weights.config.llm_hidden_size = actual + 1; let encoder = Arc::new(weights);
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 1024).unwrap();
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer = Arc::new(tokenizer);
let mut session = Session::new(
model,
tokenizer,
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
session.attach_audio_encoder(encoder);
let pcm = vec![0.0f32; 16_000];
let err = session.append_audio(&pcm, 16_000).unwrap_err();
let cera::CeraError::Backend(msg) = err else {
panic!("expected Backend error for dim mismatch, got {err:?}");
};
assert!(
msg.contains("llm_hidden_size") && msg.contains("hidden_size"),
"Backend error should mention both encoder llm_hidden_size and LLM hidden_size; got: {msg}"
);
}
#[test]
#[ignore]
fn engine_auto_attaches_audio_encoder_from_bundle() {
use cera::model::audio_encoder::SAMPLE_RATE;
use cera::{CeraEngine, EngineConfig};
let Ok(home) = std::env::var("HOME") else {
eprintln!("no HOME env — skipping");
return;
};
let bundle_dir = std::path::PathBuf::from(&home).join(".leap/models/LFM2.5-Audio-1.5B-Q4_0");
if !bundle_dir.is_dir() {
eprintln!(
"no LFM2.5-Audio bundle dir at {} — skipping",
bundle_dir.display()
);
return;
}
let engine = CeraEngine::from_path(&bundle_dir, EngineConfig::default()).unwrap();
assert!(
engine.audio_encoder().is_some(),
"engine should eagerly load mmproj for audio bundles"
);
let mut session = engine.new_session(SessionConfig::default()).unwrap();
let n_samples = (SAMPLE_RATE as usize) / 2;
let pcm: Vec<f32> = (0..n_samples)
.map(|i| ((i as f32 * 0.07).sin() + (i as f32 * 0.013).sin()) * 0.05)
.collect();
let pos_before = session.position();
session
.append_audio(&pcm, SAMPLE_RATE)
.expect("append_audio");
assert!(
session.position() > pos_before,
"append_audio must advance position when engine pre-attached the encoder"
);
let opts = greedy_opts(1);
let mut sink = CollectSink(Vec::new());
let _summary = session.generate(&opts, &mut sink).expect("generate");
}
#[test]
#[ignore]
fn engine_does_not_load_encoder_for_text_bundle() {
use cera::{CeraEngine, EngineConfig};
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let engine = CeraEngine::from_path(&model_path, EngineConfig::default()).unwrap();
assert!(
engine.audio_encoder().is_none(),
"text-path engine must not eagerly load any audio encoder"
);
}
fn read_wav_pcm16_mono_test(path: &std::path::Path) -> (Vec<f32>, u32) {
let buf = std::fs::read(path).expect("read fixture WAV");
assert!(buf.len() >= 12, "WAV too short for RIFF/WAVE header");
assert_eq!(&buf[0..4], b"RIFF", "missing RIFF header");
assert_eq!(&buf[8..12], b"WAVE", "missing WAVE header");
let read_u16 = |o: usize| -> u16 {
let end = o.checked_add(2).expect("WAV offset overflow");
assert!(
end <= buf.len(),
"WAV truncated: u16 at {o} OOB ({})",
buf.len()
);
u16::from_le_bytes(buf[o..end].try_into().unwrap())
};
let read_u32 = |o: usize| -> u32 {
let end = o.checked_add(4).expect("WAV offset overflow");
assert!(
end <= buf.len(),
"WAV truncated: u32 at {o} OOB ({})",
buf.len()
);
u32::from_le_bytes(buf[o..end].try_into().unwrap())
};
let mut o = 12usize;
let mut fmt_off: Option<usize> = None;
let mut data: Option<(usize, usize)> = None;
while o + 8 <= buf.len() {
let id = &buf[o..o + 4];
let sz = read_u32(o + 4) as usize;
let body = o + 8;
let chunk_end = body.checked_add(sz).expect("WAV chunk size overflow");
assert!(
chunk_end <= buf.len(),
"WAV chunk `{}` size {sz} exceeds file (body+sz={chunk_end} > len={})",
std::str::from_utf8(id).unwrap_or("?"),
buf.len()
);
if id == b"fmt " {
assert!(
sz >= 16,
"WAV `fmt ` chunk size {sz} < 16 (PCM minimum); file may be malformed"
);
fmt_off = Some(body);
} else if id == b"data" {
data = Some((body, sz));
}
o = body
.checked_add(sz)
.and_then(|x| x.checked_add(sz & 1))
.expect("WAV walk overflow");
}
let fmt = fmt_off.expect("no fmt chunk");
let (data_off, data_sz) = data.expect("no data chunk");
assert_eq!(read_u16(fmt), 1, "expected PCM (audio_format=1)");
assert_eq!(read_u16(fmt + 2), 1, "expected mono");
let sample_rate = read_u32(fmt + 4);
assert_eq!(read_u16(fmt + 14), 16, "expected 16-bit");
assert_eq!(
data_sz % 2,
0,
"WAV data chunk size {data_sz} is not a multiple of 2 (PCM16 frame)"
);
let n_samples = data_sz / 2;
let mut samples = Vec::with_capacity(n_samples);
for i in 0..n_samples {
let s = i16::from_le_bytes([buf[data_off + i * 2], buf[data_off + i * 2 + 1]]);
samples.push(s as f32 / 32768.0);
}
(samples, sample_rate)
}
#[test]
#[ignore]
fn asr_real_audio_matches_input_phrase() {
use cera::model::audio_encoder::AudioEncoderWeights;
use cera::tokenizer::{ChatMessage, apply_chat_template};
let Ok(home) = std::env::var("HOME") else {
eprintln!("no HOME env — skipping");
return;
};
let bundle = std::path::PathBuf::from(&home).join(".leap/models/LFM2.5-Audio-1.5B-Q4_0");
let primary = bundle.join("LFM2.5-Audio-1.5B-Q4_0.gguf");
let mmproj_path = bundle.join("mmproj-LFM2.5-Audio-1.5B-Q4_0.gguf");
if !primary.exists() || !mmproj_path.exists() {
eprintln!("LFM2.5-Audio bundle not present — skipping");
return;
}
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/audio/today_is_a_beautiful_day.wav");
let (pcm, sr) = read_wav_pcm16_mono_test(&fixture);
assert_eq!(sr, 16_000, "fixture must be 16 kHz");
let primary_gguf = cera::gguf::GgufFile::open(&primary).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&primary_gguf).unwrap();
let model = cera::model::load_model(primary_gguf, None, 4096).unwrap();
let model: Arc<dyn Model> = Arc::from(model);
let tokenizer_arc = Arc::new(tokenizer);
let mut session = Session::new(
Arc::clone(&model),
Arc::clone(&tokenizer_arc),
ModalityCapabilities {
text_in: true,
text_out: true,
audio_in: true,
audio_out: false,
image_in: false,
},
SessionConfig::default(),
)
.unwrap();
let mmproj = cera::gguf::GgufFile::open_arc(&mmproj_path).unwrap();
let encoder = Arc::new(AudioEncoderWeights::from_gguf(&mmproj).unwrap());
session.attach_audio_encoder(encoder);
let marker_candidates = [
"<|reserved_4|>",
"<|reserved_5|>",
"<|reserved_6|>",
"<|reserved_7|>",
];
let (marker_id, marker_name) = marker_candidates
.iter()
.find_map(|name| tokenizer_arc.special_token_id(name).map(|id| (id, *name)))
.expect("no marker token in vocab");
let messages = vec![
ChatMessage {
role: "system".into(),
content: "Perform ASR.".into(),
},
ChatMessage {
role: "user".into(),
content: marker_name.to_string(),
},
];
let formatted = apply_chat_template(&tokenizer_arc, &messages, true).unwrap();
let toks = tokenizer_arc.encode(&formatted);
let mut found: Option<usize> = None;
let mut count = 0usize;
for (i, &t) in toks.iter().enumerate() {
if t == marker_id {
count += 1;
if found.is_none() {
found = Some(i);
}
}
}
let split = match (count, found) {
(1, Some(idx)) => idx,
(0, _) => {
panic!("marker `{marker_name}` (id {marker_id}) not in encoded chat-template tokens")
}
(n, _) => {
panic!("marker `{marker_name}` (id {marker_id}) appears {n} times in encoded tokens")
}
};
let (prefix, suffix) = (&toks[..split], &toks[split + 1..]);
if !prefix.is_empty() {
session.append_tokens(prefix).expect("append prefix");
}
session.append_audio(&pcm, sr).expect("append_audio");
if !suffix.is_empty() {
session.append_tokens(suffix).expect("append suffix");
}
let opts = greedy_opts(24);
let mut sink = CollectSink(Vec::new());
session.generate(&opts, &mut sink).expect("generate");
let decoded = tokenizer_arc.decode(&sink.0);
eprintln!("ASR transcription: {decoded:?}");
const EXPECTED: &str = "today is a beautiful day.";
let normalized = decoded.trim().to_lowercase();
assert_eq!(
normalized, EXPECTED,
"expected ASR transcription to be {EXPECTED:?} (matching llama.cpp's reference output), \
got {normalized:?} (raw: {decoded:?})"
);
}
#[test]
#[ignore]
fn ignore_eos_forces_exact_token_count() {
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("The capital of France is");
let mut session = make_session(model, tokenizer, SessionConfig::default());
session.append_tokens(&prompt_toks).unwrap();
let mut sink = CollectSink(Vec::new());
let summary = session.generate(&greedy_opts(1), &mut sink).unwrap();
assert_eq!(
summary.tokens_generated, 1,
"greedy continuation of the probe prompt should emit one token"
);
let first_token = sink.0[0];
assert_eq!(summary.prompt_eval_tokens, prompt_toks.len() as u32);
let suffix = &prompt_toks[..prompt_toks.len().min(2)];
session.append_tokens(suffix).unwrap();
let mut sink_chain = CollectSink(Vec::new());
let summary2 = session.generate(&greedy_opts(1), &mut sink_chain).unwrap();
assert_eq!(summary2.prompt_eval_tokens, suffix.len() as u32);
session.reset().unwrap();
session.append_tokens(&prompt_toks).unwrap();
let stop_opts = GenerateOpts {
stop_tokens: vec![first_token],
..greedy_opts(8)
};
let mut sink = CollectSink(Vec::new());
let summary = session.generate(&stop_opts, &mut sink).unwrap();
assert!(matches!(summary.finish_reason, FinishReason::Stop));
assert_eq!(summary.tokens_generated, 0);
session.reset().unwrap();
session.append_tokens(&prompt_toks).unwrap();
let ignore_opts = GenerateOpts {
ignore_eos: true,
..stop_opts
};
let mut sink = CollectSink(Vec::new());
let summary = session.generate(&ignore_opts, &mut sink).unwrap();
assert!(matches!(summary.finish_reason, FinishReason::MaxTokens));
assert_eq!(summary.tokens_generated, 8);
assert_eq!(sink.0.len(), 8);
assert_eq!(sink.0[0], first_token);
}
#[test]
#[ignore]
fn ignore_eos_does_not_override_active_grammar() {
use std::sync::Arc;
let Some(model_path) = find_model() else {
eprintln!("no model available — skipping");
return;
};
let gguf = cera::gguf::GgufFile::open(&model_path).unwrap();
let tokenizer = cera::tokenizer::BpeTokenizer::from_gguf(&gguf).unwrap();
let model = cera::model::load_model(gguf, None, 4096).unwrap();
let prompt_toks = tokenizer.encode("Answer:");
let mut session = make_session(model, tokenizer, SessionConfig::default());
session.append_tokens(&prompt_toks).unwrap();
let grammar = Arc::new(cera::grammar::Grammar::parse(r#"root ::= "yes""#).unwrap());
let opts = GenerateOpts {
max_tokens: 32,
temperature: 0.0,
grammar: Some(grammar),
ignore_eos: true,
..Default::default()
};
let mut sink = CollectSink(Vec::new());
let summary = session.generate(&opts, &mut sink).unwrap();
assert!(
matches!(summary.finish_reason, FinishReason::Stop),
"expected clean Stop, got {:?}",
summary.finish_reason
);
if let Some(eos) = session.tokenizer().eos_token() {
assert!(
!sink.0.contains(&eos),
"EOS must not leak into grammar output"
);
}
}