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)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Region {
Box([f64; 4]),
Point([f64; 2]),
Color(FrameColor),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FrameColor {
Red,
Green,
Blue,
}
impl FrameColor {
pub fn name(self) -> &'static str {
match self {
FrameColor::Red => "red",
FrameColor::Green => "green",
FrameColor::Blue => "blue",
}
}
pub fn from_name(name: &str) -> Option<Self> {
[FrameColor::Red, FrameColor::Green, FrameColor::Blue]
.into_iter()
.find(|color| color.name() == name)
}
}
impl Region {
pub fn prefix(&self, width: u32, height: u32) -> String {
let grid = |coords: &[f64]| {
let scaled: Vec<String> = coords
.iter()
.enumerate()
.map(|(k, &v)| {
let side = f64::from(if k % 2 == 0 { width } else { height });
format!("{}", (v / side * 1000.0) as i64)
})
.collect();
format!("[{}] ", scaled.join(", "))
};
match self {
Region::Box(coords) => grid(coords),
Region::Point(coords) => grid(coords),
Region::Color(color) => format!("[{}] ", color.name()),
}
}
}
pub fn ocr_prompt_ids(tk: &Tiktoken, format: bool, region: &str) -> FocrResult<Vec<u32>> {
tk.encode(&ocr_prompt(format, region))
}
fn ocr_prompt(format: bool, region: &str) -> String {
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: " };
format!(
"{system}<|im_end|><|im_start|>user\n<img>{imgpad}</img>\n{region}{instruction}<|im_end|><|im_start|>assistant\n"
)
}
pub fn recognize(
weights: &Weights,
statics: &GotStatics,
tk: &Tiktoken,
img: &DynamicImage,
max_new: usize,
format: bool,
region: &str,
) -> FocrResult<String> {
let tv = Instant::now();
let image = preprocess::got_view_tensor(img);
let prompt_ids = ocr_prompt_ids(tk, format, region)?;
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,
regions: &[String],
) -> FocrResult<Vec<String>> {
let tv = Instant::now();
let mut embeds_list: Vec<Mat> = Vec::with_capacity(imgs.len());
for (k, img) in imgs.iter().enumerate() {
let region = regions.get(k).map_or("", String::as_str);
let prompt_ids = ocr_prompt_ids(tk, format, region)?;
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_reading(asset: &str, square_pad: bool) {
let (Ok(model), Ok(tkp)) = (
std::env::var("FOCR_GOT_MODEL"),
std::env::var("FOCR_GOT_TIKTOKEN"),
) else {
return;
};
let dir = std::path::Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/format_corpus"
));
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(dir.join(format!("{asset}.png"))).expect("corpus image");
let img = if square_pad {
preprocess::pad_to_white_square(&img)
} else {
img
};
let statics = hydrate_statics(&weights, "model.vision_tower_high", false).expect("statics");
let ffn = weights
.record("model.layers.0.mlp.up_proj.weight")
.expect("the decoder FFN tensor");
let int8 = ffn.dtype == crate::native_engine::weights::DType::QInt8PerChan;
let text = if int8 {
recognize(&weights, &statics, &tk, &img, 512, true, "").expect("recognize --format")
} else {
let image = preprocess::got_view_tensor(&img);
let prompt = ocr_prompt_ids(&tk, true, "").expect("prompt");
let embeds = build_inputs_embeds(&weights, &statics, &image, &prompt).expect("embeds");
let cfg = DecoderConfig::got_ocr2();
let ids = decoder_qwen2::generate_greedy(&weights, &cfg, &embeds, 512, EOS_ID)
.expect("f32 greedy decode");
tk.decode_skip_special(&ids)
.expect("decode")
.trim()
.to_string()
};
eprintln!("[format corpus {asset}] {text:?}");
let reading = format!("{text}\n");
let suffix = match (square_pad, int8) {
(false, true) => "got",
(false, false) => "f32.got",
(true, true) => "task.got",
(true, false) => "task.f32.got",
};
let frozen = dir.join(format!("{asset}.{suffix}.txt"));
if matches!(std::env::var("UPDATE_GOLDENS").ok().as_deref(), Some("1")) {
std::fs::write(&frozen, &reading).expect("freeze the reading");
return;
}
assert_eq!(
reading,
std::fs::read_to_string(&frozen).expect("frozen reading"),
"{asset}: the `--format` reading changed. Review it, freeze it with \
UPDATE_GOLDENS=1, and rescore with scripts/got_cer.py --format-corpus"
);
}
#[test]
fn format_corpus_formula_e2e() {
format_corpus_reading("formula", false);
}
#[test]
fn format_corpus_table_e2e() {
format_corpus_reading("table", false);
}
#[test]
fn format_corpus_chart_e2e() {
format_corpus_reading("chart", false);
}
#[test]
fn format_corpus_chart_task_e2e() {
format_corpus_reading("chart", true);
}
#[test]
fn format_corpus_molecule_e2e() {
format_corpus_reading("molecule", false);
}
#[test]
fn format_corpus_music_e2e() {
format_corpus_reading("music", false);
}
#[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"
);
let boxed = ocr_prompt_ids(&tk, false, "[166, 125, 833, 875] ").unwrap();
assert!(boxed.len() > plain.len());
assert_eq!(
boxed[..280],
plain[..280],
"the system turn and image splice"
);
assert_eq!(boxed[boxed.len() - 6..], plain[plain.len() - 6..]);
}
#[test]
fn a_region_prefixes_the_instruction_as_upstream_chat_does() {
let (w, h) = (600, 400);
let cases = [
(
Region::Box([100.0, 50.0, 500.0, 350.0]),
"[166, 125, 833, 875] ",
),
(Region::Point([300.0, 200.0]), "[500, 500] "),
(
Region::Box([0.5, 399.9, 599.99, 400.0]),
"[0, 999, 999, 1000] ",
),
(Region::Box([700.0, 0.0, 0.0, 0.0]), "[1166, 0, 0, 0] "),
(Region::Color(FrameColor::Red), "[red] "),
(Region::Color(FrameColor::Green), "[green] "),
(Region::Color(FrameColor::Blue), "[blue] "),
];
for (region, want) in cases {
assert_eq!(region.prefix(w, h), want, "{region:?}");
}
let prompt = ocr_prompt(true, &Region::Color(FrameColor::Red).prefix(w, h));
assert!(
prompt.ends_with("</img>\n[red] OCR with format: <|im_end|><|im_start|>assistant\n"),
"{prompt:?}"
);
assert!(ocr_prompt(false, "").ends_with("</img>\nOCR: <|im_end|><|im_start|>assistant\n"));
}
#[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)
}
}