Image-Text-to-Text
Transformers
Safetensors
qwen3_vl
ocr
document-parsing
document-ai
layout
vision-language-model
conversational
Instructions to use hanji-dev/hanji-parse-4b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use hanji-dev/hanji-parse-4b with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="hanji-dev/hanji-parse-4b") 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)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("hanji-dev/hanji-parse-4b") model = AutoModelForMultimodalLM.from_pretrained("hanji-dev/hanji-parse-4b", 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=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use hanji-dev/hanji-parse-4b with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "hanji-dev/hanji-parse-4b" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "hanji-dev/hanji-parse-4b", "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/hanji-dev/hanji-parse-4b
- SGLang
How to use hanji-dev/hanji-parse-4b 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 "hanji-dev/hanji-parse-4b" \ --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": "hanji-dev/hanji-parse-4b", "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 "hanji-dev/hanji-parse-4b" \ --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": "hanji-dev/hanji-parse-4b", "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 hanji-dev/hanji-parse-4b with Docker Model Runner:
docker model run hf.co/hanji-dev/hanji-parse-4b
File size: 10,740 Bytes
b58d44a c0ad7ce b58d44a c0ad7ce b58d44a c0ad7ce b58d44a c0ad7ce b58d44a c0ad7ce b58d44a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | ---
license: apache-2.0
base_model: Qwen/Qwen3-VL-4B-Instruct
pipeline_tag: image-text-to-text
library_name: transformers
tags:
- ocr
- document-parsing
- document-ai
- layout
- vision-language-model
- qwen3_vl
---
# Hanji Parse 4B
**Hanji Parse 4B** is a document-parsing vision-language model fine-tuned from
[`Qwen/Qwen3-VL-4B-Instruct`](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct).
Given a page image, it emits a JSON array of layout-grounded content blocks —
each block a semantic section of the page (a paragraph, a heading with its
content, a key-value panel, or a whole table) with a bounding box and its
transcribed text. Tables are transcribed as GitHub-Flavored Markdown inside a
single block. Figures, photos, and signatures are returned as image blocks.
## Output contract
The model returns **JSON only**: an array of records
```json
[{"bbox_2d": [x1, y1, x2, y2], "text_content": "..."}]
```
- `bbox_2d` is `[left, top, right, bottom]` in **normalized 0–1000 page
coordinates** (divide by 1000 and multiply by the page width/height to
recover pixel boxes).
- Blocks are **semantic sections**, typically 5–30 per page — not one record
per line, cell, or field.
- Tables come back as **one block** containing a GitHub-Flavored Markdown
table; every cell rides inside the markdown.
- Non-text graphics (photos, charts, stamps, signatures) come back with
`text_content` = `"<image>"`.
- Checkboxes are transcribed inline as `[x]` / `[ ]` before their label.
- An empty page returns exactly `[]`.
## Usage — read this before running the model
### 1. Image preprocessing
- Downscale so the image is at most **2,000,000 pixels** (2 MP), preserving
aspect ratio. **Never upscale.**
- Floor each dimension to a **multiple of 32** (Qwen3-VL uses 16-px patches
with 2×2 spatial merge → 32 px per visual token).
- Use LANCZOS resampling; feed the result as a PNG.
```python
from PIL import Image
MAX_PIXELS, PATCH = 2_000_000, 32
def preprocess(img: Image.Image) -> Image.Image:
w, h = img.size
scale = min(1.0, (MAX_PIXELS / (w * h)) ** 0.5)
w, h = int(w * scale) // PATCH * PATCH, int(h * scale) // PATCH * PATCH
return img.convert("RGB").resize((w, h), Image.LANCZOS)
```
### 2. The prompt
Send the page image followed by exactly this text as the user turn. Do not
paraphrase, extend, or reformat it.
````text
Detect every BLOCK in this document and return a JSON array.
Schema: [{"bbox_2d":[x1,y1,x2,y2], "text_content":"..."}]
Coordinates: normalized 0-1000 page coordinates; [x1,y1,x2,y2] = [left,top,right,bottom].
EMPTY PAGE: If the page has no legible content, return exactly []. Otherwise, transcribe every legible content block; a page with only one legible item is not empty.
Your DEFAULT is to GROUP. Most such pages form 5-30 records; sparse pages may form only 1-4. More than 30 remains unusual. A
"block" is a semantic SECTION (a panel, a heading + its content, a key-value group,
or a whole table), NOT a single line, cell, or field. If you are emitting one record
per line, per cell, or per form field, STOP - that is WRONG. When a region is not a
clean table, you must STILL group it into section blocks; never fall back to
one-record-per-element.
Block categories:
- Text records: ONE record per block - a heading TOGETHER WITH the lines beneath it,
a paragraph, a list, or a key-value field group. text_content = the block's text,
with "\n" between its lines. DO NOT emit one record per line.
- Table records: a table or dense grid of cells, rendered as GitHub-Flavored Markdown
(| col | col |\n|---|---|\n| cell | cell |). Never emit one record per cell or
per row - the markdown carries every cell. A repeated item|amount list (receipt
lines, menu items) IS a table.
HEADERLESS TABLES: if a table has no visible column headings, DO NOT invent any.
Render only the visible rows/cells in their observed order. If Markdown syntax needs
a separator row, use empty header cells rather than synthetic names like "Column 1".
TABLE CELL TEXT: cell contents must be plain visible text. Do NOT add Markdown
emphasis or formatting inside cells (no **bold**, _italics_, backticks, or headings)
unless that formatting is the only way to preserve information that is visible on
the page.
TALL TABLES: A logical table on one page is ONE block regardless of row count. Include
every visible row in one GFM table. Its bbox_2d must tightly enclose the full table
from the first row through the last row.
- Image records: ONE record per photo, figure, chart, scan, or non-text graphic.
text_content = "<image>". Handwritten signatures, cursive e-signatures, initials,
signature scribbles, and signature marks are ALWAYS images - do NOT transcribe or
guess them, even if partly readable. Printed labels such as "Signature:" remain text.
Do NOT emit for logos < 40 px wide.
CRITICAL - transcribe MEANING, not layout glyphs:
- Fill-in / blank lines: emit ONLY the label, NOT the blank. Write "Name:" - never
"Name:________________". For signature fields, keep the printed label as text and
emit the actual signature mark itself as an image record with text_content="<image>".
- NEVER reproduce decorative rules or separators - rows of *, -, _, =, ., or any
repeated glyph. Omit them entirely; they are not content.
- Checkboxes / Y-N / selection fields: write the field and its options on one line with the marks inline - "<row label> Y [x] N [ ]", "<label>: [x] Yes [ ] No"; "[x]" filled, "[ ]" empty; one record per field, not per option.
- text_content must equal the VISIBLE text of the block - never pad, repeat, or
continue a character run. No single text block exceeds ~20 lines; split a longer
section at its sub-headings.
Grouping rules:
- A section heading and the content beneath it (down to the next heading) form ONE block.
- A field label and its value are ONE block - EVEN in a dense report header. Write
"Visit Date: 08/12/2025" as one record; NEVER split the label from its value
("Visit Date:" + "08/12/2025" as two records is WRONG).
- On forms, when a key has an associated value, merge the key and value into the same
block and bbox so the association is explicit. If several related key-value fields
are visually grouped, emit the group as one block with one "Label: value" line per field.
- A bordered or visually-grouped PANEL (e.g. a "PRESCRIBER INFORMATION" box with all
its fields) is ONE block - join its label:value pairs with "\n".
- A row of related cells that is NOT a clean table (a lab-result line, a transaction
line) is ONE block - join the cells into one line; NEVER one record per cell.
- NEVER emit a bare value, a single cell, or a lone field as its own record.
- Keep COLUMNS separate: two side-by-side panels (e.g. Patient | Ordering Provider,
Bill To | Ship To) are TWO blocks - never merge across the gutter.
- DO NOT merge unrelated neighboring panels or sections.
- Group by semantic relationship, NOT by bbox size. Small unrelated regions must stay
separate. Example: a page title at the top-left and a page number at the top-right
are TWO blocks, even if both boxes are small and on the same horizontal band. Only
group items that belong to the same section, panel, list, table, or key-value group.
- Prefer CORRECT grouping over tight boxes: a block's bbox may be wide and may lightly
touch a neighbor - do NOT over-split a section just to keep boxes small or separate.
- Page-edge content: if visible text touches or sits near the page boundary, inspect the
full edge carefully and make the bbox include the entire visible glyphs/block, even if
the box must start at 0 or end at 1000. Do NOT shrink edge boxes inward.
- Bbox coverage is strict: every transcribed character in text_content MUST be inside
that record's bbox_2d, with no clipped letters. This matters most for small regions,
rotated/non-horizontal text, page-edge text, headers/footers, stamps, and fax strips.
Use a tight box around the actual region, but never make it so tight that any visible
character you transcribed falls outside the box.
Output JSON only.
Checkboxes: transcribe every checkbox as [x] if marked or [ ] if unmarked, placed before its label (e.g. "[x] Allergies reviewed"; Y/N pairs as "Y [x] N [ ]"). Include every checkbox, including checkbox grids and Y/N option pairs.
````
### 3. Decoding
- **Greedy**: `temperature 0.0`, `top_p 1.0`, no repetition penalty.
- `max_new_tokens = 8192`.
- Strongly recommended: **JSON-schema-constrained decoding** (xgrammar in
SGLang, `guided_json` in vLLM) with this schema:
```json
{
"type": "array",
"items": {
"type": "object",
"properties": {
"bbox_2d": {
"type": "array",
"items": {"type": "integer"},
"minItems": 4,
"maxItems": 4
},
"text_content": {"type": "string"}
},
"required": ["bbox_2d", "text_content"]
}
}
```
### Quickstart (transformers)
```python
import torch
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoProcessor
MODEL = "hanji-dev/hanji-parse-4b"
model = AutoModelForImageTextToText.from_pretrained(
MODEL, dtype=torch.bfloat16, device_map="auto"
)
processor = AutoProcessor.from_pretrained(MODEL)
image = preprocess(Image.open("page.png")) # see preprocessing above
PROMPT = "..." # the exact prompt above
messages = [{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": PROMPT},
],
}]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True,
return_dict=True, return_tensors="pt",
).to(model.device)
out = model.generate(**inputs, max_new_tokens=8192, do_sample=False)
print(processor.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
```
### Serving (SGLang)
N-gram speculative decoding gives a large decode speedup on this output
format (the repeated JSON keys draft extremely well):
```bash
python -m sglang.launch_server \
--model-path hanji-dev/hanji-parse-4b \
--attention-backend fa3 \
--mm-attention-backend fa3 \
--mem-fraction-static 0.85 \
--chunked-prefill-size 8192 \
--speculative-algorithm NGRAM \
--speculative-num-draft-tokens 16 \
--speculative-ngram-max-bfs-breadth 10 \
--enable-deterministic-inference \
--context-length 16384
```
A ready-to-run server that implements the full preprocessing + prompt contract
(and a schema-extraction API around it) is available at
https://github.com/youlearn-ai/hanji.
## License
Apache-2.0. Fine-tuned from
[`Qwen/Qwen3-VL-4B-Instruct`](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct)
(Apache-2.0, © Alibaba Cloud / the Qwen team).
|