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
Hanji Parse 4B
Hanji Parse 4B is a document-parsing vision-language model fine-tuned from
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
[{"bbox_2d": [x1, y1, x2, y2], "text_content": "..."}]
bbox_2dis[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.
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.
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_jsonin vLLM) with this schema:
{
"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)
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):
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
(Apache-2.0, © Alibaba Cloud / the Qwen team).
- Downloads last month
- 23