Instructions to use Gabriel/diffu-minicpm-ocr-htr-jev with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Gabriel/diffu-minicpm-ocr-htr-jev with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="Gabriel/diffu-minicpm-ocr-htr-jev") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("Gabriel/diffu-minicpm-ocr-htr-jev") model = AutoModelForMultimodalLM.from_pretrained("Gabriel/diffu-minicpm-ocr-htr-jev", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - MiniCPM-OCR
How to use Gabriel/diffu-minicpm-ocr-htr-jev with MiniCPM-OCR:
pip install "minicpm-ocr[all]" huggingface_hub # Deploy Gabriel/diffu-minicpm-ocr-htr-jev as a recognition service before running the Python example: # https://github.com/OpenBMB/MiniCPM-OCR/blob/main/docs/vllm_deploy_en.md # vLLM: load the model above; use --port 8080, # --served-model-name minicpm-ocr and --max-model-len 40960. # Layout detection uses cuda:0; recognition runs on the deployed service.
from importlib.resources import files from pathlib import Path from minicpmvocr import load_config, pipelined_run from huggingface_hub import snapshot_download layout_model = snapshot_download("PaddlePaddle/PP-DocLayoutV3") cfg = load_config( str(files("minicpmvocr").joinpath("configs/minicpmvocr.yaml")), overrides={ "input": {"file_path": str(Path("demo.png").resolve())}, "engine": { "type": "vllm", "server_url": "http://127.0.0.1:8080", "model_name": "minicpm-ocr", "max_model_len": 40960, }, "layout": {"model_dir": layout_model, "device": "cuda:0"}, # Render PDFs in-process so this example also works without a main guard. "pipeline": {"overlap_layout": True, "render_workers": 1}, }, ) pipelined_run(cfg) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Gabriel/diffu-minicpm-ocr-htr-jev with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Gabriel/diffu-minicpm-ocr-htr-jev" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Gabriel/diffu-minicpm-ocr-htr-jev", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/Gabriel/diffu-minicpm-ocr-htr-jev
- SGLang
How to use Gabriel/diffu-minicpm-ocr-htr-jev with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Gabriel/diffu-minicpm-ocr-htr-jev" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Gabriel/diffu-minicpm-ocr-htr-jev", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Gabriel/diffu-minicpm-ocr-htr-jev" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Gabriel/diffu-minicpm-ocr-htr-jev", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use Gabriel/diffu-minicpm-ocr-htr-jev with Docker Model Runner:
docker model run hf.co/Gabriel/diffu-minicpm-ocr-htr-jev
diffu-minicpm-ocr-htr-jev
MiniCPM-OCR-Jev fine-tuned as a proof-reader for Swedish handwritten archive lines. Given a line's crop and a candidate transcription (with the candidate rendered as an image), it returns the probability that the transcription matches the line. It can pick the best of several readings, rank readers, and flag doubtful readings or labels for human review. It is part of diffu, beside the reader diffu-minicpm-ocr-htr.
Training
- Pairs: from every GT-Lejonet training line (February 2026, the Swedish Lion's training side):
- the line's crop with its GT text (a match);
- about as many wrong candidates (rule perturbations of the GT text, and the line of the same page closest to it in length);
- diffu-minicpm-ocr-htr's readings of 100,000 training lines, a match where the reading equals the GT after normalisation and a mismatch where it does not.
- Rendering: each candidate is rendered by diffu's renderer (
htr-train-jev-render-1+ae7b7855e115+pillow-12.2.0+freetype-2.14.3). Scores depend on the renderer, so render with the same one. - Recipe: OpenBMB's (full fine-tune of the language model, the vision encoder and aligner frozen, learning rate 1e-5, ZeRO-3), 2 epochs, 128 pairs a step, unpacked, on two RTX PRO 6000 Blackwell GPUs.
- Checkpoint: checkpoint-45486, the last.
- Curves: the training and evaluation metrics are in this repo's
runs/folder (TensorBoard, the Training metrics tab).
Evaluation
On its held-out dev pairs (from training lines) at the last evaluation: accuracy 95.6%, loss 0.1315 (step 45486/45486).
On 2,000 pairs drawn from the test lines' pairs (786 correct transcriptions, 395 wrong readings by the reader and 818 rule-made wrong candidates), scored in vLLM as below:
| AUROC | At a threshold of 0.5 | |
|---|---|---|
| All pairs | 0.870 | 77.8% judged right |
| The correct text against rule-made wrong candidates | 0.988 | 96.9% of the correct kept, 93.9% of the wrong caught |
| The correct text against the reader's own wrong readings | 0.624 | 6.6% of the wrong caught |
OpenBMB's checkpoint before this fine-tuning, given the same prompt and rendering (not the ones it was trained with), scores an AUROC of 0.594 on these pairs.
It does not yet catch the reader's own mistakes. They are small (a median CER of 5%, a letter or two in a line) and were 1.2 % of its training pairs (35,578 of 3.0 million). A threshold of 0.97 catches 74% of them but rejects 55% of the correct texts. Use it for gross mismatches (another line's text, dropped or swapped words); for the reader's near-misses it needs many more real misreadings to train on.
This has not yet been measured on JevOCRBench, nor as a label filter on machine-transcribed lines.
Use
Score a line with the inputs made exactly as in training, or the scores shift:
- the crop: the line's bounding box, white outside its polygon, 48 pixels high;
- the prompt: diffu's own, in English (
diffu_htr_train.jev.prompt.model_prompt), not OpenBMB's; - the rendering: diffu's renderer
htr-train-jev-render-1+ae7b7855e115+pillow-12.2.0+freetype-2.14.3(diffu_htr_train.jev.render.render_text, DejaVu Sans, drawn with the Pillow that string names).
So OpenBMB's Jev services, which render and prompt their own way, do not fit this model. Set up vLLM (a nightly from 29 October 2026 or later, with transformers 5.22.1) and the two functions:
uv venv --python 3.12 jev && source jev/bin/activate
echo "transformers==5.22.1" > overrides.txt
uv pip install --override overrides.txt "pillow==12.2.0" \
"vllm @ https://wheels.vllm.ai/fa3b9d1377219247060d3c8b49eae199c9e7e154/vllm-0.33.0rc1.dev234%2Bgfa3b9d137-cp38-abi3-manylinux_2_28_x86_64.whl" \
"git+https://github.com/Borg93/diffu2#subdirectory=packages/htr_train"
Then score a line:
import math
from pathlib import Path
from PIL import Image
from vllm import LLM, PoolingParams
from diffu_htr_train.jev.prompt import model_prompt
from diffu_htr_train.jev.render import render_text
if __name__ == "__main__":
jev = LLM(
model="Gabriel/diffu-minicpm-ocr-htr-jev",
runner="pooling",
convert="classify",
hf_overrides={"num_labels": 2},
limit_mm_per_prompt={"image": 2},
)
crop = Image.open("line.jpg").convert("RGB")
text = "Anno 1649 den 6 Aprilis"
rendering = render_text(text, Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), crop.size)
[out] = jev.classify(
[{"prompt": model_prompt(text), "multi_modal_data": {"image": [crop, rendering]}}],
pooling_params=PoolingParams(use_activation=False),
)
no, yes = out.outputs.probs
print(1 / (1 + math.exp(no - yes))) # the probability that the text matches the line
Without vLLM, transformers gives the same scores (within 0.01 on 60 test pairs):
import torch
from transformers import AutoProcessor, Qwen3_5ForSequenceClassification
processor = AutoProcessor.from_pretrained("Gabriel/diffu-minicpm-ocr-htr-jev")
jev = Qwen3_5ForSequenceClassification.from_pretrained("Gabriel/diffu-minicpm-ocr-htr-jev", num_labels=2, dtype=torch.bfloat16).to("cuda")
inputs = processor(text=[model_prompt(text)], images=[crop, rendering], return_tensors="pt").to("cuda")
with torch.inference_mode():
print(float(torch.softmax(jev(**inputs).logits[0].float(), -1)[1]))
The score is not calibrated: choose a threshold on labelled lines of your own.
Licence
Apache-2.0, as its base model.
- Downloads last month
- -