use image::DynamicImage;
use super::decoder_qwen2::{self, DecoderConfig};
use super::tensor::Mat;
use super::weights::Weights;
use super::{connector, decoder, vision_sam};
use crate::error::FocrResult;
use crate::preprocess;
use crate::tokenizer::tiktoken::Tiktoken;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
pub const EOS_ID: u32 = 151_645;
pub const MAX_NEW_TOKENS: usize = 4096;
fn no_repeat_ngram_override(default: usize) -> usize {
if let Some(n) = super::decode_overrides().no_repeat_ngram {
return n;
}
static N: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
N.get_or_init(|| {
std::env::var("FOCR_GOT_NO_REPEAT_NGRAM")
.ok()
.and_then(|v| v.trim().parse().ok())
})
.unwrap_or(default)
}
pub const IMG_PAD_ID: u32 = 151_859;
pub const IMAGE_TOKEN_LEN: usize = 256;
pub fn vision_features(weights: &Weights, statics: &GotStatics, image: &Mat) -> FocrResult<Mat> {
let side = (image.cols as f64).sqrt() as usize;
if side * side != image.cols || image.rows != 3 {
return Err(crate::FocrError::Other(anyhow::anyhow!(
"got vision: expected [3, side*side] input, got [{}, {}]",
image.rows,
image.cols
)));
}
let sam = match statics.sam.as_ref() {
Some(tower) => vision_sam::forward_with(tower, image, side, side)?, None => vision_sam::forward_streamed(weights, image, &statics.prefix)?,
};
let sam_t = transpose(&sam); statics.proj.apply(&sam_t) }
pub struct GotStatics {
pub sam: Option<vision_sam::SamWeights>,
pub proj: vision_sam::Linear,
pub embed: Mat,
pub prefix: String,
}
pub fn hydrate_statics(
weights: &Weights,
prefix: &str,
stream_vision: bool,
) -> FocrResult<GotStatics> {
let th = Instant::now();
let statics = GotStatics {
sam: if stream_vision {
None
} else {
Some(vision_sam::sam_weights_from(weights, prefix)?)
},
proj: vision_sam::Linear::from_row_major(
&weights.vec("model.mm_projector_vary.weight")?,
weights.vec("model.mm_projector_vary.bias")?,
1024,
1024,
)?,
embed: weights.mat("model.embed_tokens.weight")?,
prefix: prefix.to_string(),
};
super::timing_log(&format!(
" got.hydrate({}) {:.2}s",
if stream_vision { "streamed" } else { "cached" },
th.elapsed().as_secs_f64()
));
Ok(statics)
}
pub fn build_inputs_embeds(
weights: &Weights,
statics: &GotStatics,
image: &Mat,
prompt_ids: &[u32],
) -> FocrResult<Mat> {
let tokens = vision_features(weights, statics, image)?; let embed = &statics.embed; let (vocab, hidden) = (embed.rows, embed.cols);
let mut inputs_embeds = decoder::embed_tokens(&embed.data, vocab, hidden, prompt_ids)?;
let mask: Vec<bool> = prompt_ids.iter().map(|&id| id == IMG_PAD_ID).collect();
connector::masked_scatter(&mut inputs_embeds, &tokens, &mask)?;
Ok(inputs_embeds)
}
pub fn ocr_prompt_ids(tk: &Tiktoken, format: bool) -> FocrResult<Vec<u32>> {
let system = "<|im_start|>system\n You should follow the instructions carefully and explain your answers in detail.";
let imgpad = "<imgpad>".repeat(IMAGE_TOKEN_LEN);
let instruction = if format { "OCR with format: " } else { "OCR: " };
let prompt = format!(
"{system}<|im_end|><|im_start|>user\n<img>{imgpad}</img>\n{instruction}<|im_end|><|im_start|>assistant\n"
);
tk.encode(&prompt)
}
pub fn recognize(
weights: &Weights,
statics: &GotStatics,
tk: &Tiktoken,
img: &DynamicImage,
max_new: usize,
format: bool,
) -> FocrResult<String> {
let tv = Instant::now();
let image = preprocess::got_view_tensor(img);
let prompt_ids = ocr_prompt_ids(tk, format)?;
let inputs_embeds = build_inputs_embeds(weights, statics, &image, &prompt_ids)?;
super::timing_log(&format!(
" got.vision+splice {:.2}s",
tv.elapsed().as_secs_f64()
));
let tg = Instant::now();
let mut cfg = DecoderConfig::got_ocr2();
cfg.no_repeat_ngram_size = no_repeat_ngram_override(cfg.no_repeat_ngram_size);
let ids =
decoder_qwen2::generate_greedy_kvcache(weights, &cfg, &inputs_embeds, max_new, EOS_ID)?;
super::timing_log(&format!(
" got.generate {} tokens {:.2}s",
ids.len(),
tg.elapsed().as_secs_f64()
));
Ok(tk.decode_skip_special(&ids)?.trim().to_string())
}
pub fn recognize_batch(
weights: &Weights,
statics: &GotStatics,
tk: &Tiktoken,
imgs: &[&DynamicImage],
max_new: usize,
format: bool,
) -> FocrResult<Vec<String>> {
let prompt_ids = ocr_prompt_ids(tk, format)?;
let tv = Instant::now();
let mut embeds_list: Vec<Mat> = Vec::with_capacity(imgs.len());
for img in imgs {
let image = preprocess::got_view_tensor(img);
embeds_list.push(build_inputs_embeds(weights, statics, &image, &prompt_ids)?);
}
super::timing_log(&format!(
" got.vision+splice(batch of {}) {:.2}s",
imgs.len(),
tv.elapsed().as_secs_f64()
));
let tg = Instant::now();
let mut cfg = DecoderConfig::got_ocr2();
cfg.no_repeat_ngram_size = no_repeat_ngram_override(cfg.no_repeat_ngram_size);
let caps = vec![max_new; embeds_list.len()];
let id_streams =
decoder_qwen2::generate_greedy_batched(weights, &cfg, &embeds_list, &caps, EOS_ID)?;
super::timing_log(&format!(
" got.generate(batch of {}) {} tokens {:.2}s",
imgs.len(),
id_streams.iter().map(Vec::len).sum::<usize>(),
tg.elapsed().as_secs_f64()
));
id_streams
.iter()
.map(|ids| Ok(tk.decode_skip_special(ids)?.trim().to_string()))
.collect()
}
fn transpose(m: &Mat) -> Mat {
let (r, c) = (m.rows, m.cols);
let mut out = vec![0.0f32; r * c];
for i in 0..r {
for j in 0..c {
out[j * r + i] = m.data[i * c + j];
}
}
Mat::from_vec(c, r, out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn streamed_vision_is_bit_identical_to_cached() {
let Ok(model) = std::env::var("FOCR_GOT_MODEL") else {
return;
};
let weights = Weights::load(std::path::Path::new(&model)).expect("load GOT weights");
let img = image::open(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/sample_text.png"
))
.expect("sample image");
let image = preprocess::got_view_tensor(&img);
let cached = hydrate_statics(&weights, "model.vision_tower_high", false).expect("cached");
let streamed =
hydrate_statics(&weights, "model.vision_tower_high", true).expect("streamed");
assert!(cached.sam.is_some(), "cached arm retains the tower");
assert!(
streamed.sam.is_none(),
"streamed arm must NOT retain the tower — that is the whole point"
);
let a = vision_features(&weights, &cached, &image).expect("cached vision");
let b = vision_features(&weights, &streamed, &image).expect("streamed vision");
assert_eq!(a.shape(), b.shape());
assert_eq!(
a.data.iter().map(|f| f.to_bits()).collect::<Vec<u32>>(),
b.data.iter().map(|f| f.to_bits()).collect::<Vec<u32>>(),
"streamed GOT vision features must be bit-identical to the cached tower"
);
assert!(a.data.iter().any(|&v| v != 0.0), "features carry signal");
}
#[test]
fn recognize_reads_the_sample_image_e2e() {
let (Ok(model), Ok(tkp)) = (
std::env::var("FOCR_GOT_MODEL"),
std::env::var("FOCR_GOT_TIKTOKEN"),
) else {
return;
};
let weights = Weights::load(std::path::Path::new(&model)).expect("load GOT weights");
let tk = Tiktoken::from_qwen_tiktoken(&std::fs::read(&tkp).expect("qwen.tiktoken"))
.expect("tiktoken");
let img = image::open(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/sample_text.png"
))
.expect("sample image");
let statics = hydrate_statics(&weights, "model.vision_tower_high", false).expect("statics");
let text = recognize(&weights, &statics, &tk, &img, 64, false).expect("recognize");
eprintln!("[B11 e2e] {text:?}");
assert_eq!(
text,
"HelloGOT-OCR2.0 Thequickbrownfaxjumps overthelazydog. 1234567890+=% Invoice#A-4217Total:$1,234.56",
"GOT e2e OCR output regressed"
);
}
#[test]
fn recognize_batch_matches_sequential_e2e() {
let (Ok(model), Ok(tkp)) = (
std::env::var("FOCR_GOT_MODEL"),
std::env::var("FOCR_GOT_TIKTOKEN"),
) else {
eprintln!(
r#"{{"test":"got_batch_e2e","event":"result","result":"skip_no_model","reason":"FOCR_GOT_MODEL/FOCR_GOT_TIKTOKEN unset","native_path_ran":true,"fallback_target":"/nonexistent"}}"#
);
return;
};
let weights = Weights::load(std::path::Path::new(&model)).expect("load GOT weights");
let tk = Tiktoken::from_qwen_tiktoken(&std::fs::read(&tkp).expect("qwen.tiktoken"))
.expect("tiktoken");
let img1 = image::open(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/sample_text.png"
))
.expect("page 1");
let img2 = image::open(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/format_corpus/table.png"
))
.expect("page 2");
let statics = hydrate_statics(&weights, "model.vision_tower_high", false).expect("statics");
let solo: Vec<String> = [&img1, &img2]
.iter()
.map(|im| {
recognize(&weights, &statics, &tk, im, 64, false).expect("sequential recognize")
})
.collect();
let batched = recognize_batch(&weights, &statics, &tk, &[&img1, &img2], 64, false)
.expect("batched recognize");
assert_eq!(
solo, batched,
"A7.5 LOSSLESS contract broken: batched != sequential on the armed model"
);
eprintln!(
r#"{{"test":"got_batch_e2e","event":"result","result":"pass","pages":2,"identical":true}}"#
);
}
fn format_corpus_smoke(asset: &str, markers: &[&str]) {
let (Ok(model), Ok(tkp)) = (
std::env::var("FOCR_GOT_MODEL"),
std::env::var("FOCR_GOT_TIKTOKEN"),
) else {
return;
};
let path = std::path::Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/format_corpus"
))
.join(asset);
if !path.exists() {
eprintln!("[format corpus] {asset} not generated (optional asset) — skipping");
return;
}
let weights = Weights::load(std::path::Path::new(&model)).expect("load GOT weights");
let tk = Tiktoken::from_qwen_tiktoken(&std::fs::read(&tkp).expect("qwen.tiktoken"))
.expect("tiktoken");
let img = image::open(&path).expect("corpus image");
let statics = hydrate_statics(&weights, "model.vision_tower_high", false).expect("statics");
let text = recognize(&weights, &statics, &tk, &img, 512, true).expect("recognize --format");
eprintln!("[format corpus {asset}] {text:?}");
assert!(!text.is_empty(), "{asset}: `--format` output is empty");
assert!(
markers.iter().any(|m| text.contains(m)),
"{asset}: `--format` output {text:?} contains none of the lenient markers {markers:?}"
);
}
#[test]
fn format_corpus_formula_smoke_e2e() {
format_corpus_smoke("formula.png", &["=", "\\"]);
}
#[test]
fn format_corpus_table_smoke_e2e() {
format_corpus_smoke("table.png", &["&", "|", "tabular", "17", "42"]);
}
#[test]
fn format_corpus_chart_smoke_e2e() {
format_corpus_smoke("chart.png", &["7", "9", "Widget"]);
}
#[test]
fn format_corpus_molecule_smoke_e2e() {
format_corpus_smoke("molecule.png", &["=O"]);
}
#[test]
fn format_corpus_music_smoke_e2e() {
format_corpus_smoke("music.png", &["*", "kern", "="]);
}
#[test]
fn format_prompt_swaps_the_instruction() {
let Ok(tkp) = std::env::var("FOCR_GOT_TIKTOKEN") else {
return;
};
let tk = Tiktoken::from_qwen_tiktoken(&std::fs::read(&tkp).expect("qwen.tiktoken"))
.expect("tiktoken");
let plain = ocr_prompt_ids(&tk, false).unwrap();
let fmt = ocr_prompt_ids(&tk, true).unwrap();
assert_eq!(plain.len(), 287, "plain L0c prompt is 287 ids");
assert_eq!(
fmt.len(),
289,
"format adds 2 ids (OCR: -> OCR with format: )"
);
assert_eq!(
plain.iter().filter(|&&i| i == IMG_PAD_ID).count(),
IMAGE_TOKEN_LEN
);
assert_eq!(
fmt.iter().filter(|&&i| i == IMG_PAD_ID).count(),
IMAGE_TOKEN_LEN
);
assert!(
fmt.windows(5).any(|w| w == [93495, 448, 3561, 25, 220]),
"format instruction ids missing"
);
}
#[test]
fn vision_splice_matches_oracle_hidden0() {
let (Ok(model), Ok(img), Ok(h0)) = (
std::env::var("FOCR_GOT_MODEL"),
std::env::var("FOCR_ORACLE_IMAGE"),
std::env::var("FOCR_ORACLE_HIDDEN0"),
) else {
return;
};
let weights = Weights::load(std::path::Path::new(&model)).expect("load GOT weights");
const L0C: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/l0c_prompt.json"
));
let v: serde_json::Value = serde_json::from_str(L0C).unwrap();
let prompt_ids: Vec<u32> = v["ids"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
assert_eq!(
prompt_ids.iter().filter(|&&i| i == IMG_PAD_ID).count(),
IMAGE_TOKEN_LEN
);
let img_flat = read_f32_le(&img);
let side = 1024usize;
assert_eq!(img_flat.len(), 3 * side * side, "image not [3,1024,1024]");
let image = Mat::from_vec(3, side * side, img_flat);
let statics = hydrate_statics(&weights, "model.vision_tower_high", false).expect("statics");
let embeds = build_inputs_embeds(&weights, &statics, &image, &prompt_ids)
.expect("build inputs_embeds");
assert_eq!(embeds.rows, prompt_ids.len());
assert_eq!(embeds.cols, 1024);
let oracle = read_f32_le(&h0);
assert_eq!(oracle.len(), embeds.data.len(), "hidden0 shape mismatch");
let (cos, max_abs) = cosine_maxabs(&embeds.data, &oracle);
eprintln!(
"[B3 vision] inputs_embeds vs oracle hidden_0: cos={cos:.6} max_abs={max_abs:.4}"
);
assert!(
cos >= 0.999,
"inputs_embeds cosine {cos:.6} < 0.999 — vision/splice diverged"
);
}
fn read_f32_le(path: &str) -> Vec<f32> {
std::fs::read(path)
.expect("blob")
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_le_bytes(*c))
.collect()
}
fn cosine_maxabs(a: &[f32], b: &[f32]) -> (f64, f32) {
let dot: f64 = a
.iter()
.zip(b)
.map(|(&x, &y)| f64::from(x) * f64::from(y))
.sum();
let na: f64 = a
.iter()
.map(|&x| f64::from(x) * f64::from(x))
.sum::<f64>()
.sqrt();
let nb: f64 = b
.iter()
.map(|&y| f64::from(y) * f64::from(y))
.sum::<f64>()
.sqrt();
let max_abs = a
.iter()
.zip(b)
.map(|(&x, &y)| (x - y).abs())
.fold(0.0, f32::max);
(dot / (na * nb), max_abs)
}
}