diff --git a/docs/usage.md b/docs/usage.md index ed03d25..c32a1b4 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -364,11 +364,11 @@ cargo run -p oar-ocr-vl --features cuda --example paddleocr_vl -- \ | `PaddleOcrVlTask::Spotting` | Text spotting (localization + recognition) | Structured text | | `PaddleOcrVlTask::Seal` | Seal recognition | Plain text | -## HunyuanOCR +## HunyuanOCR 1.5 -[HunyuanOCR](https://huggingface.co/tencent/HunyuanOCR) is a 1B parameter OCR expert VLM. It's available in the `oar-ocr-vl` crate and supports prompt-driven image-to-text OCR. +[HunyuanOCR 1.5](https://huggingface.co/tencent/HunyuanOCR) is a lightweight OCR expert VLM. It is available in the `oar-ocr-vl` crate and supports prompt-driven image-to-text OCR. `HunyuanOcr::from_dir` automatically detects 1.5 at the model repository root and remains compatible with archived 1.0 weights under `v1.0/`. -Note: inputs are automatically resized to satisfy the model's image/token limits (e.g., max side length 2048). +HunyuanOCR 1.5 inputs use the checkpoint's 16M-pixel budget (up to a 4K square input). The 2048 value in `vision_config.max_image_size` describes the learned positional-embedding base grid; larger input grids are interpolated, as in the official implementation. ### Downloading the Model @@ -380,6 +380,8 @@ git clone https://huggingface.co/tencent/HunyuanOCR hf download tencent/HunyuanOCR --local-dir HunyuanOCR ``` +The download places 1.5 weights directly in `HunyuanOCR/`. To use 1.0 instead, pass `HunyuanOCR/v1.0` as the model directory. + ### Basic Usage ```rust,no_run @@ -391,6 +393,7 @@ fn main() -> Result<(), Box> { let image = load_image("document.jpg")?; let device = parse_device("cpu")?; // or "cuda", "cuda:0" + // Repository root = HunyuanOCR 1.5; `HunyuanOCR/v1.0` also works. let model = HunyuanOcr::from_dir("HunyuanOCR", device)?; let prompt = "Detect and recognize text in the image, and output the text coordinates in a formatted manner."; diff --git a/oar-ocr-vl/README.md b/oar-ocr-vl/README.md index a8434a4..749ebdb 100644 --- a/oar-ocr-vl/README.md +++ b/oar-ocr-vl/README.md @@ -11,7 +11,7 @@ This crate provides native Rust inference for document VLMs using [Candle](https | [PaddleOCR-VL](https://huggingface.co/PaddlePaddle/PaddleOCR-VL) | 0.9B | SOTA document parsing VLM supporting 109 languages, text, tables, formulas, and 11 chart types | | [PaddleOCR-VL-1.5](https://huggingface.co/PaddlePaddle/PaddleOCR-VL-1.5) | 0.9B | Next-gen PaddleOCR-VL with 94.5% on OmniDocBench v1.5, adds text spotting and seal recognition | | [PaddleOCR-VL-1.6](https://huggingface.co/PaddlePaddle/PaddleOCR-VL-1.6) | 1.0B | Region-aware refinement on top of PaddleOCR-VL-1.5; 96.33% on OmniDocBench v1.6 (SOTA), drop-in compatible with the 1.5 loader | -| [HunyuanOCR](https://huggingface.co/tencent/HunyuanOCR) | 1B | End-to-end OCR VLM for multilingual document parsing, text spotting, and information extraction | +| [HunyuanOCR 1.5](https://huggingface.co/tencent/HunyuanOCR) | Lightweight | End-to-end OCR VLM for multilingual document parsing, text spotting, and information extraction (archived 1.0 weights also supported) | | [GLM-OCR](https://huggingface.co/zai-org/GLM-OCR) | 0.9B | #1 on OmniDocBench v1.5 (94.62), optimized for real-world scenarios with MTP loss and RL training | | [MinerU2.5](https://huggingface.co/opendatalab/MinerU2.5-2509-1.2B) | 1.2B | Decoupled document parsing VLM with strong text, formula, and table recognition | @@ -186,7 +186,7 @@ cargo run --release --features cuda --example paddleocr_vl -- \ seal.jpg ``` -### HunyuanOCR (Direct Inference) +### HunyuanOCR 1.5 (Direct Inference) ```bash cargo run --release --features cuda --example hunyuanocr -- \ @@ -196,6 +196,8 @@ cargo run --release --features cuda --example hunyuanocr -- \ document.jpg ``` +The model repository root contains HunyuanOCR 1.5. The loader detects it automatically; use `--model-dir models/HunyuanOCR/v1.0` for the archived 1.0 checkpoint. + ### GLM-OCR (Direct Inference) ```bash @@ -216,4 +218,3 @@ cargo run --release --features cuda --example mineru -- \ --device cuda:0 \ document.jpg ``` - diff --git a/oar-ocr-vl/examples/hunyuanocr.rs b/oar-ocr-vl/examples/hunyuanocr.rs index 38d7189..3d95b26 100644 --- a/oar-ocr-vl/examples/hunyuanocr.rs +++ b/oar-ocr-vl/examples/hunyuanocr.rs @@ -1,6 +1,7 @@ -//! HunyuanOCR Recognition Example (Candle-based) +//! HunyuanOCR 1.5 / 1.0 Recognition Example (Candle-based) //! -//! This example demonstrates how to run `tencent/HunyuanOCR` (HunYuanVL) in Rust. +//! This example demonstrates how to run `tencent/HunyuanOCR` (1.5 at the model +//! repository root, or archived 1.0 under `v1.0/`) in Rust. //! //! # Usage //! @@ -30,7 +31,7 @@ use oar_ocr_vl::utils::parse_device; #[derive(Parser)] #[command(name = "hunyuanocr")] -#[command(about = "HunyuanOCR Recognition Example - image-to-text using Candle")] +#[command(about = "HunyuanOCR 1.5/1.0 - image-to-text using Candle")] struct Args { /// Path to the HunyuanOCR model directory #[arg(short, long)] @@ -91,7 +92,8 @@ fn main() -> Result<(), Box> { let load_start = Instant::now(); let model = HunyuanOcr::from_dir(&args.model_dir, device)?; info!( - "Model loaded in {:.2}ms", + "HunyuanOCR {} loaded in {:.2}ms", + model.version(), load_start.elapsed().as_secs_f64() * 1000.0 ); diff --git a/oar-ocr-vl/src/hunyuanocr/config.rs b/oar-ocr-vl/src/hunyuanocr/config.rs index b09eb33..3d3b929 100644 --- a/oar-ocr-vl/src/hunyuanocr/config.rs +++ b/oar-ocr-vl/src/hunyuanocr/config.rs @@ -2,6 +2,26 @@ use oar_ocr_core::core::OCRError; use serde::Deserialize; use std::path::Path; +/// Checkpoint generation supported by the HunyuanOCR backend. +/// +/// The upstream config does not expose a dedicated version field. V1.5 uses +/// the newer nested `text_config` schema, while the archived V1.0 checkpoint +/// keeps all text fields at the top level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HunyuanOcrVersion { + V1, + V1_5, +} + +impl std::fmt::Display for HunyuanOcrVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::V1 => f.write_str("1.0"), + Self::V1_5 => f.write_str("1.5"), + } + } +} + #[derive(Debug, Clone, Deserialize)] pub struct HunyuanOcrRopeScaling { #[serde(default)] @@ -66,12 +86,31 @@ pub struct HunyuanOcrConfig { pub use_qk_norm: bool, pub rope_scaling: HunyuanOcrRopeScaling, pub vision_config: HunyuanOcrVisionConfig, + + /// Present in HunyuanOCR 1.5's Transformers config. The text backbone + /// parameters remain duplicated at the top level for checkpoint + /// compatibility, so only presence is needed for version detection. + #[serde(default)] + pub text_config: Option, } impl HunyuanOcrConfig { pub fn from_path(path: impl AsRef) -> Result { crate::utils::load_json_config(path, "HunyuanOCR", "config.json") } + + pub fn version(&self) -> HunyuanOcrVersion { + if self + .text_config + .as_ref() + .and_then(serde_json::Value::as_object) + .is_some() + { + HunyuanOcrVersion::V1_5 + } else { + HunyuanOcrVersion::V1 + } + } } #[derive(Debug, Clone, Deserialize)] diff --git a/oar-ocr-vl/src/hunyuanocr/mod.rs b/oar-ocr-vl/src/hunyuanocr/mod.rs index c6f6979..1f4aac1 100644 --- a/oar-ocr-vl/src/hunyuanocr/mod.rs +++ b/oar-ocr-vl/src/hunyuanocr/mod.rs @@ -1,7 +1,8 @@ -//! HunyuanOCR (HunYuanVL) Vision-Language model. +//! HunyuanOCR 1.5 / 1.0 (HunYuanVL) Vision-Language model. //! -//! This module provides native Rust inference for the `tencent/HunyuanOCR` model -//! (config `model_type=hunyuan_vl`) using Candle. +//! This module provides native Rust inference for the `tencent/HunyuanOCR` +//! model (1.5 at the repository root and archived 1.0 under `v1.0/`) using +//! Candle. mod config; mod llm; @@ -10,6 +11,7 @@ mod processing; mod vision; pub use config::{ - HunyuanOcrConfig, HunyuanOcrImageProcessorConfig, HunyuanOcrRopeScaling, HunyuanOcrVisionConfig, + HunyuanOcrConfig, HunyuanOcrImageProcessorConfig, HunyuanOcrRopeScaling, HunyuanOcrVersion, + HunyuanOcrVisionConfig, }; pub use model::HunyuanOcr; diff --git a/oar-ocr-vl/src/hunyuanocr/model.rs b/oar-ocr-vl/src/hunyuanocr/model.rs index 7de8f53..69f4f2f 100644 --- a/oar-ocr-vl/src/hunyuanocr/model.rs +++ b/oar-ocr-vl/src/hunyuanocr/model.rs @@ -1,6 +1,6 @@ //! HunyuanOCR model wrapper (HunYuanVLForConditionalGeneration). -use super::config::{HunyuanOcrConfig, HunyuanOcrImageProcessorConfig}; +use super::config::{HunyuanOcrConfig, HunyuanOcrImageProcessorConfig, HunyuanOcrVersion}; use super::llm::HunyuanLlm; use super::processing::{HunyuanOcrImageInputs, preprocess_image}; use super::vision::HunyuanVisionModel; @@ -14,20 +14,21 @@ use oar_ocr_core::core::OCRError; use std::path::Path; use tokenizers::Tokenizer; -/// Read `generation_config.json::repetition_penalty`. Returns 1.0 (no-op) if -/// the file is missing, unparseable, or the field is absent — matches -/// HuggingFace's default. Local HunyuanOCR config ships 1.03. -fn load_repetition_penalty(model_dir: &Path) -> f64 { +/// Read `generation_config.json::repetition_penalty`, using the checkpoint +/// generation's reference fallback when the field is absent. V1.0 ships 1.03 +/// in the file; V1.5 omits it there but specifies 1.08 for its benchmark and +/// client inference paths. +fn load_repetition_penalty(model_dir: &Path, default: f64) -> f64 { let path = model_dir.join("generation_config.json"); let Ok(contents) = std::fs::read_to_string(&path) else { - return 1.0; + return default; }; let Ok(v) = serde_json::from_str::(&contents) else { - return 1.0; + return default; }; v.get("repetition_penalty") .and_then(|x| x.as_f64()) - .unwrap_or(1.0) + .unwrap_or(default) } /// Read `generation_config.json::eos_token_id`. The official config provides @@ -106,6 +107,7 @@ pub struct HunyuanOcr { /// `A, B, … BZ, BZW, BZWW, BZWWZ …`) that never hit EOS. Default 1.0 means /// the value isn't applied. repetition_penalty: f64, + version: HunyuanOcrVersion, } impl HunyuanOcr { @@ -113,6 +115,7 @@ impl HunyuanOcr { let model_dir = model_dir.as_ref(); let cfg = HunyuanOcrConfig::from_path(model_dir.join("config.json"))?; + let version = cfg.version(); let image_cfg = HunyuanOcrImageProcessorConfig::from_path(model_dir.join("preprocessor_config.json"))?; @@ -148,9 +151,15 @@ impl HunyuanOcr { }; let llm = HunyuanLlm::load(&cfg, vb.pp("model"))?; - let vision = HunyuanVisionModel::load(&cfg.vision_config, vb.pp("vit"))?; + let vision = HunyuanVisionModel::load(&cfg.vision_config, version, vb.pp("vit"))?; - let repetition_penalty = load_repetition_penalty(model_dir); + // V1.5's generation_config.json omits the value, while its reference + // benchmark/client paths use 1.08. V1.0 ships its own value (1.03). + let default_repetition_penalty = match version { + HunyuanOcrVersion::V1 => 1.0, + HunyuanOcrVersion::V1_5 => 1.08, + }; + let repetition_penalty = load_repetition_penalty(model_dir, default_repetition_penalty); Ok(Self { device, @@ -162,9 +171,15 @@ impl HunyuanOcr { vision, stop_token_ids, repetition_penalty, + version, }) } + /// The checkpoint generation detected from `config.json`. + pub fn version(&self) -> HunyuanOcrVersion { + self.version + } + /// Generate OCR output for one or more images with custom instructions. /// /// Supports true GPU batching when multiple images are provided. @@ -269,11 +284,12 @@ impl HunyuanOcr { image, &self.image_cfg, &self.cfg.vision_config, + self.version, &self.device, self.dtype, )?; - let prompt = build_prompt(instruction); + let prompt = build_prompt(instruction, self.version); let enc = self .tokenizer .encode(prompt, false) @@ -565,11 +581,16 @@ impl HunyuanOcr { } } -fn build_prompt(instruction: &str) -> String { - // Matches the tokenizer chat_template in the model repo (empty system message). - // The model expects the generation prompt to end with `<|hy_User|>`. +fn build_prompt(instruction: &str, version: HunyuanOcrVersion) -> String { + // V1.0's reference invocation supplies an empty system message, which the + // template renders as placeholder no.3. V1.5's official invocation starts + // directly with the user message and therefore omits that token. + let system_prefix = match version { + HunyuanOcrVersion::V1 => "<|hy_place▁holder▁no▁3|>", + HunyuanOcrVersion::V1_5 => "", + }; format!( - "<|hy_begin▁of▁sentence|><|hy_place▁holder▁no▁3|>\ + "<|hy_begin▁of▁sentence|>{system_prefix}\ <|hy_place▁holder▁no▁100|><|hy_place▁holder▁no▁102|><|hy_place▁holder▁no▁101|>{instruction}\ <|hy_User|>" ) @@ -701,3 +722,22 @@ fn build_position_ids( ) }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn v15_prompt_omits_legacy_empty_system_token() { + let prompt = build_prompt("read", HunyuanOcrVersion::V1_5); + assert!(prompt.starts_with("<|hy_begin▁of▁sentence|><|hy_place▁holder▁no▁100|>")); + assert!(!prompt.contains("<|hy_place▁holder▁no▁3|>")); + assert!(prompt.ends_with("read<|hy_User|>")); + } + + #[test] + fn v1_prompt_keeps_empty_system_token() { + let prompt = build_prompt("read", HunyuanOcrVersion::V1); + assert!(prompt.starts_with("<|hy_begin▁of▁sentence|><|hy_place▁holder▁no▁3|>")); + } +} diff --git a/oar-ocr-vl/src/hunyuanocr/processing.rs b/oar-ocr-vl/src/hunyuanocr/processing.rs index 887876f..9a48584 100644 --- a/oar-ocr-vl/src/hunyuanocr/processing.rs +++ b/oar-ocr-vl/src/hunyuanocr/processing.rs @@ -1,4 +1,4 @@ -use super::config::{HunyuanOcrImageProcessorConfig, HunyuanOcrVisionConfig}; +use super::config::{HunyuanOcrImageProcessorConfig, HunyuanOcrVersion, HunyuanOcrVisionConfig}; use crate::utils::{ candle_to_ocr_processing, image::{clamp_to_max_image_size, image_to_chw, smart_resize}, @@ -65,6 +65,7 @@ pub fn preprocess_image( image: &RgbImage, cfg: &HunyuanOcrImageProcessorConfig, vision_cfg: &HunyuanOcrVisionConfig, + version: HunyuanOcrVersion, device: &Device, dtype: DType, ) -> Result { @@ -94,15 +95,23 @@ pub fn preprocess_image( let (h, w) = (image.height(), image.width()); let factor = (cfg.patch_size * cfg.merge_size) as u32; - let (rh, rw) = smart_resize_token_limited( - h, - w, - factor, - cfg.min_pixels, - cfg.max_pixels, - vision_cfg.img_max_token_num, - )?; - let (rh, rw) = clamp_to_max_image_size(rh, rw, factor, vision_cfg.max_image_size)?; + let (rh, rw) = match version { + HunyuanOcrVersion::V1 => { + let (rh, rw) = smart_resize_token_limited( + h, + w, + factor, + cfg.min_pixels, + cfg.max_pixels, + vision_cfg.img_max_token_num, + )?; + clamp_to_max_image_size(rh, rw, factor, vision_cfg.max_image_size)? + } + // V1.5 raises the processor budget to 16M pixels (4K square). + // `max_image_size` is the learned positional-embedding base grid, not + // an input side-length cap; the official processor only smart-resizes. + HunyuanOcrVersion::V1_5 => smart_resize(h, w, factor, cfg.min_pixels, cfg.max_pixels)?, + }; let resized = if rh != h || rw != w { image::imageops::resize(image, rw, rh, resize_filter) @@ -143,16 +152,6 @@ pub fn preprocess_image( ) })?; - // Sanity-check max image size constraint. - if rh as usize > vision_cfg.max_image_size || rw as usize > vision_cfg.max_image_size { - return Err(OCRError::InvalidInput { - message: format!( - "HunyuanOCR: resized image {rw}x{rh} exceeds max_image_size={}", - vision_cfg.max_image_size - ), - }); - } - Ok(HunyuanOcrImageInputs { pixel_values, grid_thw_merged, @@ -165,7 +164,7 @@ mod tests { use image::RgbImage; #[test] - fn test_preprocess_image_clamps_to_max_image_size() -> Result<(), Box> { + fn test_v1_preprocess_clamps_to_max_image_size() -> Result<(), Box> { let cfg = HunyuanOcrImageProcessorConfig { min_pixels: 0, max_pixels: 100_000_000, @@ -195,7 +194,14 @@ mod tests { }; let img = RgbImage::new(512, 3000); - let out = preprocess_image(&img, &cfg, &vision_cfg, &Device::Cpu, DType::F32)?; + let out = preprocess_image( + &img, + &cfg, + &vision_cfg, + HunyuanOcrVersion::V1, + &Device::Cpu, + DType::F32, + )?; let (_b, _c, rh, rw) = out.pixel_values.dims4()?; assert!(rh <= vision_cfg.max_image_size); assert!(rw <= vision_cfg.max_image_size); @@ -206,4 +212,48 @@ mod tests { assert_eq!(out.grid_thw_merged, (1, rh / factor, rw / factor)); Ok(()) } + + #[test] + fn test_v15_preprocess_allows_large_side() -> Result<(), Box> { + let cfg = HunyuanOcrImageProcessorConfig { + min_pixels: 0, + max_pixels: 100_000_000, + patch_size: 16, + resample: None, + temporal_patch_size: 1, + merge_size: 2, + image_mean: vec![0.5, 0.5, 0.5], + image_std: vec![0.5, 0.5, 0.5], + }; + let vision_cfg = HunyuanOcrVisionConfig { + hidden_size: 1, + intermediate_size: 1, + num_attention_heads: 1, + num_hidden_layers: 1, + num_channels: 3, + patch_size: 16, + spatial_merge_size: 2, + rms_norm_eps: 1e-5, + hidden_act: "gelu".to_string(), + add_patchemb_bias: false, + cat_extra_token: 1, + max_vit_seq_len: 65_536, + max_image_size: 2048, + img_max_token_num: 16_384, + interpolate_mode: "bilinear".to_string(), + }; + + let img = RgbImage::new(512, 3000); + let out = preprocess_image( + &img, + &cfg, + &vision_cfg, + HunyuanOcrVersion::V1_5, + &Device::Cpu, + DType::F32, + )?; + let (_b, _c, rh, rw) = out.pixel_values.dims4()?; + assert!(rh > vision_cfg.max_image_size || rw > vision_cfg.max_image_size); + Ok(()) + } } diff --git a/oar-ocr-vl/src/hunyuanocr/vision.rs b/oar-ocr-vl/src/hunyuanocr/vision.rs index f04f14b..0faab83 100644 --- a/oar-ocr-vl/src/hunyuanocr/vision.rs +++ b/oar-ocr-vl/src/hunyuanocr/vision.rs @@ -1,4 +1,4 @@ -use super::config::HunyuanOcrVisionConfig; +use super::config::{HunyuanOcrVersion, HunyuanOcrVisionConfig}; use crate::utils::{candle_to_ocr_inference, candle_to_ocr_processing}; use candle_core::{D, DType, Device, IndexOp, Tensor}; use candle_nn::{Conv2d, Conv2dConfig, LayerNorm, LayerNormConfig, Linear, Module}; @@ -15,10 +15,15 @@ const VIT_LATE_F32_THRESHOLD: usize = 20; struct VisionEmbeddings { patch_embedding: Conv2d, position_embedding: candle_nn::Embedding, + version: HunyuanOcrVersion, } impl VisionEmbeddings { - fn load(cfg: &HunyuanOcrVisionConfig, vb: candle_nn::VarBuilder) -> Result { + fn load( + cfg: &HunyuanOcrVisionConfig, + version: HunyuanOcrVersion, + vb: candle_nn::VarBuilder, + ) -> Result { let conv_cfg = Conv2dConfig { stride: cfg.patch_size, padding: 0, @@ -46,7 +51,24 @@ impl VisionEmbeddings { } .map_err(|e| candle_to_ocr_inference("HunyuanOCR", "load vit patch_embedding", e))?; - let num_positions = cfg.max_vit_seq_len + cfg.cat_extra_token; + // V1.5 raises max_vit_seq_len to 65,536 for 4K inputs, but retains + // the 2048px learned positional base (16,384 patches + cls slot). + // The input grid is interpolated from this base, matching upstream. + if cfg.patch_size == 0 || !cfg.max_image_size.is_multiple_of(cfg.patch_size) { + return Err(OCRError::ConfigError { + message: format!( + "HunyuanOCR: max_image_size {} must be divisible by patch_size {}", + cfg.max_image_size, cfg.patch_size + ), + }); + } + let position_edge = cfg.max_image_size / cfg.patch_size; + let num_positions = position_edge + .checked_mul(position_edge) + .and_then(|positions| positions.checked_add(cfg.cat_extra_token)) + .ok_or_else(|| OCRError::ConfigError { + message: "HunyuanOCR: position embedding size overflow".to_string(), + })?; let position_embedding = candle_nn::embedding(num_positions, cfg.hidden_size, vb.pp("position_embedding")) .map_err(|e| { @@ -56,6 +78,7 @@ impl VisionEmbeddings { Ok(Self { patch_embedding, position_embedding, + version, }) } @@ -140,7 +163,15 @@ impl VisionEmbeddings { ) })?; - let out = interpolate_bilinear_align_corners_false(&base, grid, grid, height, width, dim); + let out = interpolate_bilinear_align_corners_false( + &base, + grid, + grid, + height, + width, + dim, + self.version, + ); Tensor::from_vec(out, (height * width, dim), device) .map_err(|e| { candle_to_ocr_processing( @@ -722,8 +753,12 @@ pub struct HunyuanVisionModel { } impl HunyuanVisionModel { - pub fn load(cfg: &HunyuanOcrVisionConfig, vb: candle_nn::VarBuilder) -> Result { - let embeddings = VisionEmbeddings::load(cfg, vb.pp("embeddings"))?; + pub fn load( + cfg: &HunyuanOcrVisionConfig, + version: HunyuanOcrVersion, + vb: candle_nn::VarBuilder, + ) -> Result { + let embeddings = VisionEmbeddings::load(cfg, version, vb.pp("embeddings"))?; let mut layers = Vec::with_capacity(cfg.num_hidden_layers); for i in 0..cfg.num_hidden_layers { layers.push(VisionEncoderLayer::load( @@ -886,23 +921,23 @@ fn interpolate_bilinear_align_corners_false( out_h: usize, out_w: usize, dim: usize, + version: HunyuanOcrVersion, ) -> Vec { let mut out = vec![0f32; out_h * out_w * dim]; if in_h == 0 || in_w == 0 || out_h == 0 || out_w == 0 { return out; } - // Match upstream HF (`HunYuanVisionPatchEmbed.forward` in - // modeling_hunyuan_vl.py:143-148): the sample stride is computed from - // `(out_h + 0.1) / in_h` (a deliberate `+0.1` to "avoid floating point - // error in the interpolation" — see the comment + facebookresearch/dino#8). - // PyTorch's `interpolate(scale_factor)` then derives the source coord as - // `(out_x + 0.5) / scale_factor - 0.5`, which is *not* the same as - // `(out_x + 0.5) * (in / out) - 0.5` we used before. - let scale_factor_y = (out_h as f32 + 0.1) / in_h as f32; - let scale_factor_x = (out_w as f32 + 0.1) / in_w as f32; - let inv_scale_y = 1.0 / scale_factor_y; - let inv_scale_x = 1.0 / scale_factor_x; + let (inv_scale_y, inv_scale_x) = match version { + // V1.0's Transformers path passed scale_factor=(out + 0.1) / in. + HunyuanOcrVersion::V1 => ( + in_h as f32 / (out_h as f32 + 0.1), + in_w as f32 / (out_w as f32 + 0.1), + ), + // V1.5 passes an explicit output size to interpolate(), whose + // align_corners=false source scale is in/out. + HunyuanOcrVersion::V1_5 => (in_h as f32 / out_h as f32, in_w as f32 / out_w as f32), + }; for oy in 0..out_h { let fy = ((oy as f32 + 0.5) * inv_scale_y - 0.5).max(0.0); diff --git a/oar-ocr-vl/src/lib.rs b/oar-ocr-vl/src/lib.rs index ec421df..b21f909 100644 --- a/oar-ocr-vl/src/lib.rs +++ b/oar-ocr-vl/src/lib.rs @@ -5,7 +5,7 @@ //! ## Modules //! //! - `paddleocr_vl` - PaddleOCR-VL for OCR, table, formula, chart, spotting, and seal recognition -//! - `hunyuanocr` - HunyuanOCR OCR expert VLM +//! - `hunyuanocr` - HunyuanOCR 1.5 / 1.0 OCR expert VLM //! - `glmocr` - GLM-OCR OCR expert VLM //! - `mineru` - MinerU2.5 document parsing VLM (Qwen2-VL backbone) //! - `mineru_diffusion` - MinerU-Diffusion-V1 block-diffusion document OCR (Qwen2-VL vision + SDAR decoder) @@ -48,7 +48,7 @@ pub use paddleocr_vl::{ }; pub use glmocr::GlmOcr; -pub use hunyuanocr::HunyuanOcr; +pub use hunyuanocr::{HunyuanOcr, HunyuanOcrVersion}; pub use mineru::MinerU; pub use mineru_diffusion::{DiffusionGenerationConfig, MinerUDiffusion};