Instructions to use botnick/Netra-2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use botnick/Netra-2B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="botnick/Netra-2B") 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("botnick/Netra-2B") model = AutoModelForMultimodalLM.from_pretrained("botnick/Netra-2B", 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]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use botnick/Netra-2B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "botnick/Netra-2B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "botnick/Netra-2B", "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/botnick/Netra-2B
- SGLang
How to use botnick/Netra-2B 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 "botnick/Netra-2B" \ --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": "botnick/Netra-2B", "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 "botnick/Netra-2B" \ --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": "botnick/Netra-2B", "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 botnick/Netra-2B with Docker Model Runner:
docker model run hf.co/botnick/Netra-2B
- Netra-2B v0.2
- Quick start
- Results
- สรุปภาษาไทย (Thai Executive Summary)
- Intended use & limits
- Details
- Modes and output formats
- Loop guard
- vLLM (offline batch)
- Key mapping with your own keys
- Release gate
- Netra-Bench-Real
- ThaiOCRBench
- Hold-out sets (never used for selection)
- Thai receipt key mapping (thairocr)
- Accounting KIE on synthetic documents
- Trust gate
- Training
- Known limitations
- Licence and attribution
- Citation
- Quick start
Netra-2B v0.2
Status: released — v0.2 (2026-10-26), replacing v0.1 (2026-10-22). v0.2 is training round 13. Use it with the loop guard; the release gate is passed only with the guard on. Against v0.1, both with the guard, v0.2 extracts much more from real documents (0.709 vs 0.546) and reads full pages slightly worse (0.693 vs 0.706, within noise). It does not beat v0.1 on every measure; see Release gate. v0.1 remains in this repository's commit history. The trust gate checks every field, cross-reads two independent OCR passes, and checks every party name against a character-level reader (Trust gate).
Netra (เนตร) reads Thai business and accounting documents. Give it a photo or scan of a page and it returns the text as Markdown, or the accounting fields as one JSON object. It is built for developers who process Thai tax invoices, receipts, transfer slips, 50 ทวิ withholding-tax certificates and billing notes. Netra is a 2B vision-language model, fine-tuned from typhoon-ai/typhoon-ocr1.5-2b, and released under Apache-2.0.
Highlights
- Real documents, with the loop guard (as shipped): reading 0.693 vs 0.666 for the base, extraction 0.709 vs 0.406 (Netra-Bench-Real, 147 real Thai finance documents). v0.1 with the guard: 0.706 / 0.546.
- ThaiOCRBench, with the loop guard: lower character error rate than the base on all three page tasks (ThaiOCRBench); the Document-parsing margin (0.2173 vs 0.2179) is a tie in practice.
- Hold-out sets: 5 wins, 2 ties and no loss against the base on 802 rows never used to choose a model (hold-out).
- Real receipts: 0.638 of 105 fields right on 8 real receipts (test only, not released) vs 0.495 for v0.1.
- Ahead of the larger 3B sibling on real documents: 0.680 / 0.713 vs 0.669 / 0.474 for typhoon-ocr1.5-3b-qat (reading / extraction, no loop guard, same benchmark and settings).
- Trust gate: 0 wrong answers accepted on three synthetic test sets (387 documents); it accepts only 32 of them (3–11% per set) and sends the rest to a person (Trust gate).
- Receipt key mapping (NECTEC thairocr) was measured on v0.1 only (thairocr).
Quick start
Define the two prompts, then run one page. Tested with transformers 5.17.0.
OCR_PROMPT and KIE_PROMPT (copy these exactly)
OCR_PROMPT = """Extract all text from the image.
Instructions:
- Only return the clean Markdown.
- Do not include any explanation or extra text.
- You must include all information on the page.
Formatting Rules:
- Tables: Render tables using <table>...</table> in clean HTML format.
- Equations: Render equations using LaTeX syntax with inline ($...$) and block ($$...$$).
- Images/Charts/Diagrams: Wrap any clearly defined visual areas (e.g. charts, diagrams, pictures) in:
<figure>
Describe the image's main elements (people, objects, text), note any contextual clues (place, event, culture), mention visible text and its meaning, provide deeper analysis when relevant (especially for financial charts, graphs, or documents), comment on style or architecture if relevant, then give a concise overall summary. Describe in Thai.
</figure>
- Page Numbers: Wrap page numbers in <page_number>...</page_number> (e.g., <page_number>14</page_number>).
- Checkboxes: Use ☐ for unchecked and ☑ for checked boxes."""
KIE_PROMPT = """Extract the accounting data from this Thai document image.
Return only one JSON object, no explanation, with these keys (null when the document has no such field):
doc_type, doc_title, doc_no, book_no, date, time, due_date, seller, buyer, payer, payee, items, subtotal, discount,
vat_base, vat_rate, vat, vat_included, wht_rate, wht, total, net_paid, amount_in_words, currency, ref_doc_no, ref_date, payment, extra.
Parties are {name, tax_id, branch, address, phone}; items are [{desc, qty, unit, unit_price, amount}].
Dates YYYY-MM-DD (convert พ.ศ. to ค.ศ.). Tax id = 13 digits, no dashes. Branch = 5 digits, head office "00000".
Write all digits as Arabic numerals. Copy names exactly as printed. Money printed blank or "-" is 0. Numbers must be JSON numbers."""
import torch
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoProcessor
from loop_guard import degenerate # loop_guard.py from this repo
model_id = "botnick/Netra-2B"
model = AutoModelForImageTextToText.from_pretrained(model_id, dtype=torch.bfloat16, device_map="auto")
processor = AutoProcessor.from_pretrained(model_id)
def load(path, max_side=1400):
img = Image.open(path).convert("RGB")
if max(img.size) > max_side:
k = max_side / max(img.size)
img = img.resize((round(img.width * k), round(img.height * k)), Image.LANCZOS)
return img
def run(img, prompt, max_new_tokens, guard=False):
messages = [{"role": "user", "content": [{"type": "image", "image": img}, {"type": "text", "text": prompt}]}]
inputs = processor.apply_chat_template(messages, tokenize=True, add_generation_prompt=True,
return_dict=True, return_tensors="pt").to(model.device)
def gen(**kw):
out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False, **kw)
return processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]
text = gen(repetition_penalty=1.05)
if guard and degenerate(text): # loop guard: retry once with stronger anti-repetition
text = gen(repetition_penalty=1.25, no_repeat_ngram_size=16)
return text
page = load("invoice.png")
markdown = run(page, OCR_PROMPT, 4096, guard=True) # page reading
kie_raw = run(page, KIE_PROMPT, 2048) # accounting JSON
Recommended settings: greedy decoding, repetition_penalty=1.05, long side of the image at most 1400 px,
max_new_tokens 4096 for reading and 2048 for extraction, and the loop guard on reading output. The shipped
generation_config.json keeps the base's sampling defaults (do_sample=true, temperature=0.7), so pass
do_sample=False and repetition_penalty=1.05 explicitly, as above.
For batch inference, see vLLM (offline batch). To ask for your own keys, see key mapping.
Results
This chart shows v0.1 (2026-10-22); it is kept because its thairocr row was measured on v0.1 only. v0.2 numbers are in the tables under Details.
v0.1 improves on its base on every benchmark in the chart, and v0.2 raises real-document extraction further (0.575 → 0.713 without the guard). The largest gains are in structured output: asked for specific keys, it returns valid JSON with the right keys far more often. The real-document numbers come from a small set (34 extraction documents), and the synthetic KIE comparison is not a fair head-to-head because the base never saw that schema. Sources and caveats for each number are in Details.
สรุปภาษาไทย (Thai Executive Summary)
- ภาพรวมโมเดล: เนตร (Netra-2B) เวอร์ชัน 0.2 (26 ต.ค. 2569) พัฒนาต่อยอดจาก Typhoon OCR 1.5 2B โดย fine-tune ด้วยเทคนิค LoRA สำหรับเอกสารทางบัญชีและธุรกิจไทยโดยเฉพาะ (เช่น ใบกำกับภาษีเต็มรูป, ใบกำกับภาษีอย่างย่อ, ใบลดหนี้/ใบเพิ่มหนี้, หนังสือรับรองการหักภาษี ณ ที่จ่าย ตามมาตรา 50 ทวิ, สลิปโอนเงิน และใบวางบิล) ตลอดจนเอกสารทางราชการ เป็นผลจากการเทรนรอบที่ 13 แล้วผสานน้ำหนักโมเดล (merge) กลับเข้าสู่โมเดลฐานเต็มรูปแบบ (1.0)
- รูปแบบผลลัพธ์: แปลงเนื้อหาบนหน้าเอกสารเป็น Markdown (แสดงโครงสร้างตารางด้วย HTML) และสกัดข้อมูลสำคัญทางบัญชี (Key Information Extraction: KIE) ให้อยู่ในรูป JSON ตามโครงสร้างข้อมูล (schema) 28 คีย์มาตรฐานเดียว
- ประสิทธิภาพบนเอกสารจริง (Netra-Bench-Real, 147 ฉบับ, เปิด loop guard): อ่านเอกสาร 0.693 (โมเดลฐาน 0.666, v0.1 0.706) และสกัดข้อมูล 0.709 (โมเดลฐาน 0.406, v0.1 0.546) ถ้าไม่เปิด guard: 0.680 / 0.713 ซึ่งดีกว่าโมเดลขนาดใหญ่กว่าอย่าง typhoon-ocr1.5-3b-qat (0.669 / 0.474)
- ใบเสร็จจริง: ถูก 0.638 จาก 105 ช่องข้อมูลในใบเสร็จจริง 8 ใบ (ใช้ทดสอบเท่านั้น ไม่เผยแพร่) เทียบกับ v0.1 ที่ 0.495
- ชุดทดสอบแบบ Hold-out (802 รายการ): ไม่แพ้โมเดลฐานในชุดใดเลย ชนะ 5 ชุด เสมอ 2 ชุด
- ข้อกำหนดสำคัญในการประมวลผล (Loop Guard): ต้องเปิดระบบป้องกันข้อความวนซ้ำ (loop guard) จึงจะผ่านเกณฑ์ปล่อย คือดีกว่าโมเดลฐานใน ThaiOCRBench ครบทั้ง 3 ภารกิจ (หมวดเอกสารทั่วไปชนะเฉียดฉิว CER 0.2173 เทียบ 0.2179 ถือว่าเสมอ) หากไม่เปิด หมวดเอกสารทั่วไปจะแย่กว่าโมเดลฐาน (CER 0.242 เทียบ 0.218)
- เทียบกับ v0.1: ความสามารถในการสกัดข้อมูลดีขึ้นอย่างมาก แต่การอ่านเนื้อหาทั้งหน้ามีประสิทธิภาพลดลงเล็กน้อย (0.693 เทียบกับ 0.706 เมื่อเปิดใช้งาน Guard ทั้งคู่) และผล ThaiOCRBench ทั้ง 3 หมวดยังแย่กว่า v0.1 (ค่าความผิดพลาด CER สูงกว่า) โดยผลการจับคู่ใบเสร็จ (thairocr) ยังคงอ้างอิงตามค่าของ v0.1 เท่านั้น
- ระบบตรวจยืนยัน (Trust Gate): ระบบไม่รับคำตอบที่ผิดเลยแม้แต่ใบเดียวจากชุดทดสอบสังเคราะห์ 3 ชุด (รวม 387 ใบ) โดยระบบรับรองโดยอัตโนมัติ 32 ใบ (3–11% ต่อชุด) และใบที่เหลือจะถูกส่งให้เจ้าหน้าที่ตรวจสอบ ทั้งนี้ ชื่อคู่ค้าทุกชื่อจะต้องตรงกับผลการอ่านทีละตัวอักษรจากตัวอ่านลำดับที่สาม ซึ่งปิดการใช้งานพจนานุกรมไว้
- ข้อควรระวังในการนำไปใช้: ข้อมูลทางบัญชีที่ใช้เทรนเป็นชุดข้อมูลสังเคราะห์ (synthetic data) ทั้งหมด จึงห้ามนำผลลัพธ์ไปบันทึกบัญชีโดยอัตโนมัติ เว้นแต่จะผ่านการตรวจสอบยืนยัน (Trust Gate) เทียบกับผลอ่าน OCR หรือมีผู้ตรวจทาน และไม่อนุญาตให้นำไปประมวลผลบัตรประจำตัวประชาชนหรือเอกสารส่วนบุคคล
Intended use & limits
Use it for:
- Transcribing Thai (and Thai/English) business documents to Markdown for search, archiving and downstream parsing.
- Extracting accounting fields from Thai tax invoices, abbreviated tax invoices, credit/debit notes, 50 ทวิ withholding-tax certificates, bank transfer slips and billing notes into JSON.
- The extraction stage of a pipeline in which every JSON answer passes a deterministic check and failing documents go to a person.
Know its limits:
- The accounting training data is synthetic. No real Thai tax invoice, receipt, slip, 50 ทวิ or billing note was used for training. Expect lower accuracy on real layouts than the synthetic numbers suggest.
- Do not book entries automatically unless each answer passes the trust gate with an OCR cross-read, or a person reviews it. The model can produce plausible but wrong tax IDs, dates and names.
- Real phone photos of receipts are still error-prone. The model may recompute VAT on top of a total that already includes it, misread the total, or fill payment fields (bank, account) the receipt does not print.
- No ID cards or personal documents (ID cards, passports, house registrations, medical records). The model was not trained or evaluated on them, and processing them raises PDPA obligations this card does not address.
- Not for general visual question answering or for languages other than Thai and English.
The full list is in Known limitations.
Details
Modes and output formats
Netra-2B is typhoon-ai/typhoon-ocr1.5-2b (Qwen3-VL-2B architecture) with a LoRA adapter trained on Thai accounting and government documents (training round 11, continued for one epoch in round 13), merged into the base at full strength. One set of weights does three jobs:
| Mode | Input | Output |
|---|---|---|
| Page reading | page image + OCR_PROMPT (the Typhoon OCR prompt) |
Markdown, tables as one-line HTML, <figure> descriptions |
| Accounting extraction (KIE) | page image + KIE_PROMPT |
one JSON object, fixed 28-key schema |
| Key mapping | page image + a prompt naming the keys you want | JSON with exactly the asked keys |
Reading output:
| Element | Format |
|---|---|
| Text | Markdown; title as # …; reading order left column, then right column, top to bottom |
| Tables | HTML on one line: <table><tr><td>…</td></tr></table>; no <th>, no <thead>/<tbody>; colspan/rowspan kept |
| Pictures, logos, stamps, charts | <figure>…</figure> with a Thai description |
| Page numbers | <page_number>…</page_number> |
| Checkboxes | ☐ / ☑ |
| Equations | LaTeX, $…$ and $$…$$ |
| Digits in reading mode | as printed (Thai digits stay Thai digits; a dashed tax ID stays dashed) |
KIE JSON schema
Every object has all 28 keys. A field the document does not have is null.
| Key | Type | Notes |
|---|---|---|
doc_type |
string | tax_invoice, abbreviated_tax_invoice, credit_note, debit_note, wht_certificate, transfer_slip, billing_note; also invoice, quotation, receipt, payment_voucher |
doc_title |
string | printed title, plus the English subtitle when printed |
doc_no |
string | document number; for a transfer slip, the transaction reference |
book_no |
string | เล่มที่ (receipt, 50 ทวิ) |
date, due_date |
YYYY-MM-DD |
Gregorian, converted from พ.ศ. |
time |
HH:MM[:SS] |
POS and transfer slips |
seller, buyer, payer, payee |
party | {name, tax_id, branch, address, phone}; tax_id 13 digits, no dashes; branch 5 digits, head office "00000", null when none is printed; name without the branch suffix |
items |
list | {desc, qty, unit, unit_price, amount}; billing note: {doc_no, date, due_date, desc, amount}; [] when none |
subtotal, discount, vat_base, vat, wht, total, net_paid |
number | discount 0 when none; total includes VAT, before WHT; net_paid = total − WHT |
vat_rate, wht_rate |
number | percent; wht_rate null when rates are mixed |
vat_included |
bool | line prices already include VAT |
amount_in_words |
string | จำนวนเงินตัวอักษรภาษาไทยโดยไม่ต้องใส่วงเล็บ (50 ทวิ: จำนวนภาษีที่หักและนำส่งไว้) |
currency |
string | "THB" |
ref_doc_no, ref_date |
string, date | original tax invoice referenced by a credit/debit note |
payment |
object | method and details (cash / transfer / cheque / card / QR / PromptPay fields) |
extra |
object | ข้อมูลเฉพาะตามประเภทเอกสาร เช่น 50 ทวิ: form, income[], funds; ใบลดหนี้/ใบเพิ่มหนี้: adjustment, difference |
Normalisation: all digits Arabic. Numbers are JSON numbers (int when whole, else at most 2 decimals). Money printed
blank or - is 0. Masked account numbers and names on slips stay masked as printed.
Shape of an answer (fictitious values):
{"doc_type": "tax_invoice", "doc_title": "ใบกำกับภาษี/ใบเสร็จรับเงิน TAX INVOICE/RECEIPT", "doc_no": "INV-0001",
"book_no": null, "date": "2026-01-15", "time": null, "due_date": "2026-02-14",
"seller": {"name": "บริษัท ตัวอย่าง จำกัด", "tax_id": "0000000000001", "branch": "00000", "address": "…", "phone": null},
"buyer": {"name": "ห้างหุ้นส่วนจำกัด ทดสอบ", "tax_id": "0000000000001", "branch": "00000", "address": "…", "phone": null},
"payer": null, "payee": null,
"items": [{"desc": "ค่าบริการ", "qty": 1, "unit": null, "unit_price": 1000, "amount": 1000}],
"subtotal": 1000, "discount": 0, "vat_base": 1000, "vat_rate": 7, "vat": 70, "vat_included": false,
"wht_rate": null, "wht": null, "total": 1070, "net_paid": null,
"amount_in_words": "หนึ่งพันเจ็ดสิบบาทถ้วน", "currency": "THB", "ref_doc_no": null, "ref_date": null,
"payment": null, "extra": {"po_no": null, "salesperson": null, "copy": null, "note": null}}
Loop guard
On a small share of pages the model repeats one line or phrase, often until the token limit. Without the guard,
degenerate() flags 7 of the 120 v0.2 ThaiOCRBench outputs (v0.1: 9). The guard detects the loop and generates once more
with stronger anti-repetition. degenerate() ships in loop_guard.py and needs only the standard
library:
from loop_guard import degenerate # True if a non-trivial line repeats ≥ 8× or a 15–80 char span repeats ≥ 6× in a row
text = generate(page, do_sample=False, repetition_penalty=1.05)
if degenerate(text):
text = generate(page, do_sample=False, repetition_penalty=1.25, no_repeat_ngram_size=16)
What was measured:
- On ThaiOCRBench the guard was measured with transformers
generate(see ThaiOCRBench). There the first pass usedrepetition_penalty=1.1, not the 1.05 recommended here. It regenerated 8 of 120 v0.2 outputs. - On Netra-Bench-Real it was measured with vLLM, whose
SamplingParamshas nono_repeat_ngram_size: an output is regenerated once withrepetition_penalty=1.25whendegenerate()flags it or it hits the token limit (12 of 147 v0.2 outputs). The transformers retry above has not been measured with vLLM; to use it, re-run the flagged pages through transformers. - The retry also breaks legitimately repeated table cells. On ThaiOCRBench tables it costs v0.2 0.016 CER compared with no guard (0.302 vs 0.286; still better than the base, 0.308).
vLLM (offline batch)
This is the configuration used for Netra-Bench-Real and the hold-out sets below (vLLM 0.31.0; loop guard only where a row says so).
from transformers import AutoProcessor
from vllm import LLM, SamplingParams
model_id = "botnick/Netra-2B"
processor = AutoProcessor.from_pretrained(model_id)
llm = LLM(model=model_id, max_model_len=8192, max_num_seqs=32, seed=0,
limit_mm_per_prompt={"image": {"count": 1, "width": 1400, "height": 1400}, "video": 0})
def chat(prompt):
return processor.apply_chat_template(
[{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": prompt}]}],
tokenize=False, add_generation_prompt=True)
read = SamplingParams(temperature=0.0, max_tokens=4096, repetition_penalty=1.05)
extract = SamplingParams(temperature=0.0, max_tokens=2048, repetition_penalty=1.05)
pages = [load(p) for p in ["a.png", "b.png"]] # load() from the quick start
reqs = [{"prompt": chat(OCR_PROMPT), "multi_modal_data": {"image": im}} for im in pages] + \
[{"prompt": chat(KIE_PROMPT), "multi_modal_data": {"image": im}} for im in pages]
outs = llm.generate(reqs, [read] * len(pages) + [extract] * len(pages))
An OpenAI-compatible server (vllm serve botnick/Netra-2B --max-model-len 8192) accepts the same prompts. Send the
image as an image_url content part and set temperature=0, repetition_penalty=1.05. The server path has not been
benchmarked for this card.
Key mapping with your own keys
Name the keys you want. The answer uses exactly those keys, in that order, with null when a key is absent. The
prompt decides whether values are copied verbatim or normalised. An example in the style of the training prompts:
ดึงข้อมูลต่อไปนี้จากเอกสาร ตอบเป็น JSON อย่างเดียว:
เลขที่ใบกำกับภาษี
วันที่ออกเอกสาร
ยอดรวมทั้งสิ้น
Seller Tax ID
(ตัวเลขเป็นตัวเลข, วันที่แบบ YYYY-MM-DD, ไม่มีให้ใส่ null)
Ask for "values exactly as printed" instead and the answer keeps the page's own strings (Thai digits, commas and dashes).
Release gate
The release rule was written down before the round-5 results were read: the model must lose to the base on no hold-out set and no ThaiOCRBench task, and since 2026-10-22 the trust gate must accept no wrong answer on any synthetic test set. v0.2 passes only with the loop guard. Without it, ThaiOCRBench Document parsing is worse than the base (CER 0.242 vs 0.218). With it, Document parsing is 0.2173 vs 0.2179: a pass by the rule, not evidence of a real gain at n = 40. The guard is therefore part of the recommended inference.
The rule compares with the base, not with the previous release. v0.2 does not beat v0.1 everywhere:
- Netra-Bench-Real reading with the guard is 0.013 lower (paired difference −0.0132, standard error 0.0140).
- ThaiOCRBench CER with the guard is higher than v0.1's on all three tasks (Full-page 0.343 vs 0.340, Document parsing 0.217 vs 0.172, Table 0.302 vs 0.293).
- Hold-out FinTabNet is 0.804 vs 0.814.
It replaces v0.1 because extraction gains are large: Netra-Bench-Real extraction with the guard +0.163 (standard error 0.066), and 0.638 vs 0.495 of 105 fields on 8 real receipts (test only, not released). The replacement was a maintainer decision, not an automatic one.
Two candidates were tested against the rule. Round 13 at merge strength 0.85 was tested first and failed (Document parsing 0.2227 vs 0.2179 with the guard). Strength 1.0 was then tested on the same 120 pages and passed. Choosing between two strengths on the gate set makes that pass slightly optimistic.
Round 13 was first blocked (2026-10-24) by the trust gate, which accepted wrong shop names spelled one mark off. Rounds 14 and 15 added training data for that error and were not released (see Rounds). Instead the gate gained a character-level third reader on party names (2026-10-26), with which round 13 accepts no wrong answer. That check was added after the false accepts on these test sets had been seen, so the gate's 0 is measured on sets that are no longer fully unseen for the gate.
Netra-Bench-Real
147 real Thai documents selected from the ThaiOCRBench test split (typhoon-ai/ThaiOCRBench, CC BY-SA 4.0, human-annotated). Selection: category in Finance, Retail, Food & Beverage, Utilities or Insurance, and the question or answer contains a money/tax keyword (ใบเสร็จ, ใบกำกับ, receipt, invoice, ภาษี, VAT, ยอด, บาท, ชำระ, สลิป, โอน).
- Test only. No ThaiOCRBench image appears in any training source (checked by image path and ID against all 23 training files of rounds 11 and 13, the two rounds v0.2 was trained in; content-level near-duplicates were not checked).
- Category mix: Finance 59, Food & Beverage 29, Retail 24, Insurance 21, Utilities 14.
- Reading (113 docs; Full-page OCR, Document parsing, Table parsing, Text recognition, Handwritten,
Fine-grained): the model gets
OCR_PROMPT. Score = 1 − min(1, Levenshtein / reference length) after removing<figure>blocks and tags, markdown symbols and extra whitespace on both sides. Table parsing scores only the<table>…</table>span. Higher is better. - Extraction (34 docs; Key information extraction, Key information mapping): the model gets the benchmark's own question. Score = share of reference values found verbatim (whitespace-normalised) in the output. Higher is better.
- Engine: vLLM 0.31.0, greedy,
repetition_penalty=1.05, settings as in the quick start. No loop guard unless the row says "+ loop guard" (the vLLM retry described in Loop guard). Same 147 document IDs for every model.
| Model | Merge strength | Reading (113) | Extraction (34) |
|---|---|---|---|
| typhoon-ocr1.5-2b (base) | — | 0.6659 | 0.4058 |
| typhoon-ocr1.5-3b-qat (larger sibling, reference) | — | 0.6691 | 0.4738 |
| round 3 (rejected) | 1.0 | 0.5729 | 0.4881 |
| round 4 | 1.0 | 0.6415 | 0.506 |
| round 4 (previous candidate, not shipped) | 0.78 | 0.6663 | 0.483 |
| round 5 | 0.70 | 0.6819 | 0.5345 |
| round 5 | 0.78 | 0.6841 | 0.5422 |
| round 5 | 0.85 | 0.6798 | 0.533 |
| round 5 — Netra-2B v0.1 (previous release) | 1.0 | 0.7091 | 0.575 |
| round 5 — v0.1 + loop guard | 1.0 | 0.7061 | 0.5456 |
| round 6 (not released) | 0.85 | 0.6945 | 0.5324 |
| round 7 (not released) | 0.85 | 0.6751 | 0.5763 |
| round 8 (not released) | 0.85 | 0.6795 | 0.5652 |
| round 9 (not released) | 1.0 | 0.659 | 0.5562 |
| round 9 (not released) | 0.78 | 0.69 | 0.5303 |
| round 10 (not released) | 1.0 | 0.6782 | 0.5823 |
| round 11 (not released) | 0.85 | 0.6762 | 0.5811 |
| round 12 (not released) | 1.0 | 0.6653 | 0.5639 |
| round 13 (not released) | 0.85 | 0.6737 | 0.6741 |
| round 13 + loop guard (failed the gate) | 0.85 | 0.7030 | 0.6800 |
| round 13 — Netra-2B v0.2 (released) | 1.0 | 0.6802 | 0.7126 |
| round 13 — v0.2 + loop guard (recommended) | 1.0 | 0.6929 | 0.7089 |
Other models, same 147 documents, harness and prompts (reference only):
| Model | Reading (113) | Extraction (34) | Note |
|---|---|---|---|
| datalab-to/chandra-ocr-2 | 0.6998 | 0.0282 | page reader; does not answer extraction questions as JSON, so its extraction score is not a fair comparison |
| Qwen/Qwen3-VL-8B-Instruct | 0.5412 | 0.6577 | 4× larger general model; best extraction measured here |
| google/gemma-4-12B-it (w4a16 quantised) | 0.3828 | 0.6663 | quantised build; extraction strong, reading weak |
| Pathumma-llm-vision 3.0.0 | 0.5474 | 0.3626 |
v0.2 has the best extraction score measured here (0.713, ahead of the 8B and 12B general models). Without the guard its reading (0.680) is below v0.1 (0.709) and chandra-ocr-2 (0.700); with the guard it is 0.693.
How to read this table:
- This benchmark is the selection set: rounds and merge strengths were compared on it, so the numbers of the chosen models are somewhat optimistic. The hold-out sets and ThaiOCRBench (below) are the independent checks.
- Paired difference, v0.2 − base, computed per document, no guard: reading +0.0142 (standard error 0.0225), extraction +0.3068 (standard error 0.0822). The reading difference is within noise; the extraction gain is about 3.7 standard errors. Per-model standard error of the mean is about 0.03 for reading and 0.07 for extraction.
- Paired difference, v0.2 − v0.1, both with the guard: reading −0.0132 (standard error 0.0140), extraction +0.1633 (standard error 0.0663).
- Per document, v0.2 vs base (no guard): 63 better, 50 worse, 34 equal. v0.1 vs base: 62 better, 54 worse, 31 equal.
- Outputs that hit the 4,096-token limit, no guard: v0.2 9 of 147, v0.1 7.
- The typhoon-ocr1.5-3b-qat row was run with the same harness, prompts and vLLM settings.
- Round 1–3 reports quote HF-
generatenumbers for the base (0.657 / 0.434). Compare numbers within one engine only.
Per task (mean score, same runs):
| Task | n | Base | v0.1 | v0.1 + guard | v0.2 | v0.2 + guard |
|---|---|---|---|---|---|---|
| Table parsing | 42 | 0.650 | 0.716 | 0.698 | 0.665 | 0.675 |
| Document parsing | 39 | 0.728 | 0.756 | 0.767 | 0.736 | 0.766 |
| Key information extraction | 17 | 0.516 | 0.424 | 0.424 | 0.752 | 0.741 |
| Key information mapping | 17 | 0.296 | 0.726 | 0.667 | 0.673 | 0.677 |
| Text recognition | 14 | 0.582 | 0.588 | 0.588 | 0.587 | 0.587 |
| Full-page OCR | 10 | 0.823 | 0.896 | 0.892 | 0.899 | 0.888 |
| Handwritten content extraction | 6 | 0.531 | 0.563 | 0.564 | 0.500 | 0.500 |
| Fine-grained text recognition | 2 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 |
Tasks with n ≤ 10 move by large steps per document; do not read trends into them. Key information extraction (free-form questions about menus, receipts and similar) rose from below the base in v0.1 (0.424 vs 0.516) to 0.752. Key information mapping (pair given items with given values) is below v0.1 without the guard (0.673 vs 0.726). Table parsing is below v0.1 (0.665 vs 0.716 without the guard). Handwriting (n = 6) is below the base.
Throughput (vLLM 0.31.0, one RTX 5090, gpu_memory_utilization=0.4, max_num_seqs=32): 147 documents generated in
59.6 s by v0.2 (v0.1: 70.9 s at 0.6). Not a controlled latency benchmark.
ThaiOCRBench
40 documents per task from the ThaiOCRBench test split (the same 120 documents for every row). Metric: CER after
tag/whitespace normalisation (table: table span only); lower is better. 22 of these 120 documents are also in
Netra-Bench-Real. Engine: transformers generate (Unsloth 2026.10.7, transformers 5.17.0), greedy,
repetition_penalty=1.1, max_new_tokens=4096; loop-guard retry with repetition_penalty=1.25,
no_repeat_ngram_size=16. Any higher mean CER than the base counts as a loss under the release gate.
| Task (n = 40) | Base | v0.1 + loop guard | v0.2, no guard | v0.2 + loop guard (recommended) | Pass? (v0.2 + guard) |
|---|---|---|---|---|---|
| Full-page OCR CER | 0.3586 | 0.3401 | 0.3550 | 0.3427 | yes (better) |
| Document parsing CER | 0.2179 | 0.1722 | 0.2422 | 0.2173 | yes (by 0.0006: a tie in practice) |
| Table parsing CER | 0.3076 | 0.2929 | 0.2861 | 0.3017 | yes (better) |
| Outputs with whole-output CER ≥ 0.9 (of 120) | 21 | 19 | 22 | 20 | — |
- Without the guard, v0.2 fails the rule (Document parsing 0.242 vs 0.218).
degenerate()flags 7 of its 120 outputs; the guard regenerated 8. - With the guard, v0.2 is better than the base on all three tasks but worse than v0.1 with the guard on all three.
- Round 13 at merge strength 0.85 failed the rule with the guard (Document parsing 0.2227). Strength 1.0 was tested afterwards on the same 120 pages, so its narrow pass is a second look at the same set (see Release gate).
- v0.1 (for reference): without the guard it failed on Full-page OCR (0.386 vs 0.359); 6 pages scored CER ≈ 1.0
(4 repetition loops, 2 diagram-heavy pages transcribed inside one
<figure>, which the metric strips).
Hold-out sets (never used for selection)
802 rows from seven sets that were never used to choose a model. An image-path leak check of every training file of rounds 11
and 13 (119,742 trainable images after the evaluation images are removed) against all 802 images found 0 overlap. Scoring is the same as
Netra-Bench-Real reading/extraction per row (vLLM, repetition_penalty=1.05, no loop guard). Difference = v0.2 − base
per row, with a paired bootstrap 95% CI (2,000 resamples). "win" and "loss" are used only when the CI excludes 0.
| Set | n | Base | v0.1 | v0.2 | Δ (v0.2 − base) | 95% CI | Verdict |
|---|---|---|---|---|---|---|---|
| Thai government documents (real) | 34 | 0.570 | 0.580 | 0.603 | +0.033 | [−0.041, +0.108] | tie |
| FinTabNet tables (real) | 150 | 0.727 | 0.814 | 0.804 | +0.076 | [+0.052, +0.102] | win |
| PubTabNet tables (real) | 150 | 0.572 | 0.787 | 0.813 | +0.241 | [+0.203, +0.283] | win |
| DocAtlas Thai pages (real) | 7 | 0.631 | 0.526 | 0.597 | −0.034 | [−0.101, +0.012] | tie |
| WildReceipt key mapping (real) | 61 | 0.347 | 0.728 | 0.787 | +0.440 | [+0.335, +0.548] | win |
| Key mapping (synthetic + CORD/FATURA) | 300 | 0.272 | 0.959 | 0.964 | +0.692 | [+0.656, +0.730] | win |
| General Thai pages (rendered) | 100 | 0.976 | 0.980 | 0.980 | +0.005 | [+0.003, +0.006] | win |
- No set is a loss. v0.2 is above v0.1 on five sets and below it on FinTabNet (0.804 vs 0.814); the v0.2 − v0.1 differences were not bootstrapped.
- Training used other pages of the same Thai government collection and of FinTabNet, PubTabNet, WildReceipt and the key-mapping generator. Those hold-out rows are therefore in-distribution (different images; the government pages come from different source PDFs). DocAtlas (n = 7) is too small to call.
Thai receipt key mapping (thairocr)
Measured on v0.1 only; not re-run for v0.2. An extra, report-only check, not used to choose or release the model. It uses 1,091 receipt images from the test split of NECTEC thairocr (photorealistic rendered receipts, not scans of real ones), never used in round-5 training. Each prompt names the keys to return (key mapping). vLLM, no loop guard. Key accuracy = correct keys / asked keys, pooled over all rows. Row score = the same per row, averaged (95% CI). No thairocr image is included in this repository.
| Model | Key accuracy | Row score [95% CI] | JSON valid | Every key right | Hit token limit |
|---|---|---|---|---|---|
| typhoon-ocr1.5-2b (base) | 0.104 | 0.105 [0.094, 0.117] | 0.416 | 0.002 | 412 of 1,091 |
| Netra-2B v0.1 | 0.528 | 0.525 [0.508, 0.542] | 0.973 | 0.075 | 1 of 1,091 |
Accounting KIE on synthetic documents
120 held-out documents from our synthetic set (synth2/val, first 120 KIE rows). The v0.1 rows: transfer slip 24, tax invoice 21,
abbreviated tax invoice 21, credit/debit note 19, 50 ทวิ certificate 18, billing note 17. The validation file gained
more document types before v0.2 was scored, so v0.2's 120 documents also include receipts, delivery notes, statements,
utility bills, purchase orders and menus; the rows are not a paired comparison. Prompt KIE_PROMPT;
transformers generate, greedy, max_new_tokens=2048, no repetition penalty, no loop guard.
Metrics:
- Field accuracy over 15 scored fields: doc_type, doc_no, date, seller/buyer name, tax_id and branch, subtotal, discount, vat, wht, total, net_paid, ref_doc_no. Strings are whitespace-normalised; numbers must match within 0.005.
- Item F1 on (desc, amount).
- JSON-valid rate.
- Doc perfect = every scored field and every item right.
Caveat: the base model never saw this schema. Its score measures schema compliance, not reading ability, so this is not a fair head-to-head. The fine-tuned rows are in-distribution (same generator as the training data).
| Model | JSON valid | Field accuracy | Item F1 | Doc perfect |
|---|---|---|---|---|
| typhoon-ocr1.5-2b (base) | 0.75 | 0.0368 | 0.3634 | 0.0 |
| round 2 (rejected for release) | 0.975 | 0.8962 | 0.9662 | 0.483 |
| round 4 @0.78 (previous candidate) | 0.942 | 0.8149 | 0.9482 | 0.25 |
| Netra-2B v0.1 (round 5) | 1.0 | 0.9082 | 0.9208 | 0.542 |
| Netra-2B v0.2 (round 13) | 1.0 | 0.9536 | 0.9731 | 0.692 |
Phone-photo invoices (synth3/val, 117 documents, rebuilt before round 13; not comparable with v0.1): v0.2 field
accuracy 0.9857, item F1 0.9853, doc perfect 0.838. Photo invoices with look-alike characters in the names
(synth4/val, 150 documents, added 2026-10-25): v0.2 0.957 / 0.9785 / 0.600.
v0.1 field accuracy by type: tax invoice 0.971, abbreviated tax invoice 0.960, credit/debit note 0.934, transfer slip 0.933, billing note 0.918, 50 ทวิ certificate 0.623. v0.2, same six types: 0.962, 0.967, 0.976, 1.000, 0.937, 0.952.
Trust gate
The trust gate auto-accepts a KIE answer only when every deterministic check passes. Otherwise it names the fields a
person must check. Checks: required and forbidden fields per doc_type, item sums, the VAT chain, WHT = total ×
rate, net = total − WHT, tax-ID checksum, Thai baht words, branch suffix rules, and (when an OCR pass is supplied)
that dates, names, tax IDs and branches in the JSON are printed in the OCR text.
First replay, on v0.1's KIE outputs (120 synthetic documents; v0.2 is measured in the production replay below):
| Mode | Accepted | Correct among accepted | Wrong but accepted | Perfect docs rejected | Share of perfect docs accepted |
|---|---|---|---|---|---|
| Checks without OCR cross-read | 87 / 120 (72.5%) | 64.4% | 31 | 9 | 86.2% |
| With OCR cross-read, gold OCR text | 49 / 120 (40.8%) | 95.9% | 2 | 18 | 72.3% |
- Without the cross-read the gate is not sufficient. About 1 in 3 accepted answers is wrong (31 of 87), mostly a wrong month in a date or a one-letter slip in a name. Arithmetic and checksum checks cannot see these.
- Caveat (first replay): the cross-read used the synthetic ground-truth text in place of a real OCR pass. That is an upper bound:
a real OCR pass (for example this model in reading mode) makes its own errors, some shared with the KIE answer.
The cross-read with a real OCR pass has not been measured. The 2 wrong answers it accepted were 50 ทวิ certificates
with a wrong
subtotal.
Production replay (2026-10-22)
The gate was then extended and replayed the way it runs in practice, on Netra's own OCR pass instead of the ground truth (first on v0.1 outputs; v0.2 in v0.2 production gate):
- Every field scored. A wrong answer counts if any field is wrong, not only the 15 core fields.
- Field placement checks: item descriptions must be whole table cells with their amount on the same row; party names must not be cut short; a branch must sit in its own party's block; payment fields (bank, account, PromptPay, reference) and item quantities and prices must be printed; labelled extras (copy mark, cashier, credit days, due date) must not be dropped.
- Second reader: every OCR check must also pass against an independent reader (typhoon-ocr1.5-3b-qat). Netra's own OCR shares its misreadings: 9 of 10 wrong answers accepted in the first own-OCR replay were the same one-letter Thai name slip in both its OCR and its JSON.
- Token confidence: a free-text value (name, item, address, bank, extras) is sent to a person when any of its tokens has probability below 0.85. The misread names had 0.45–0.84.
| Set | Pipeline | Accepted | Wrong but accepted |
|---|---|---|---|
| synthetic accounting, 120 docs | own OCR + confidence | 18 (15.0%) | 1 (copy mark dropped) |
| synthetic accounting, 120 docs | + second reader | 16 (13.3%) | 1 |
| phone-photo invoices, 117 docs | own OCR + confidence | 17 (14.5%) | 2 (card mask, salesperson dropped) |
| phone-photo invoices, 117 docs | + second reader | 8 (6.8%) | 0 |
- Correct documents rejected by the checks themselves: 0 of 7,200 ground-truth documents.
- The gate is close to zero wrong answers, but it accepts only 7–15% of documents; the rest go to a person. Raising that share needs a more accurate model, not a looser gate.
- 36 adversarial mutations of correct answers (invented fields, shifted rows, cut names) are all rejected; they are regression tests.
v0.2 production gate (2026-10-26)
Pipeline: Netra's own OCR pass, a second reader (typhoon-ocr1.5-3b-qat), token confidence (threshold 0.85, fixed in
2026-10-22 on v0.1 outputs, not refitted), and a character-level third reader on party names: Tesseract
(tha+eng, page mode 6, system and frequency dictionaries off). Every seller, buyer, payer and payee name in the JSON
must appear, whitespace-insensitive, in the Tesseract text. Netra and the second reader both turn a name printed one
mark off into the dictionary word (กู้เงิณ read as กู้เงิน); a reader without a dictionary does not. A document whose
OCR text, second-reader text or confidence pass is missing is rejected (fail closed).
Round 13 (v0.2) outputs, stage by stage (accepted / wrong but accepted):
| Set | + second reader | + token confidence | + character-level names (shipped) |
|---|---|---|---|
synthetic accounting (synth2/val), 120 docs |
32 / 0 | 16 / 0 | 4 / 0 (3.3%) |
phone-photo invoices (synth3/val), 117 docs |
55 / 1 | 48 / 0 | 12 / 0 (10.3%) |
photo invoices with look-alike names (synth4/val), 150 docs |
60 / 14 | 49 / 11 | 16 / 0 (10.7%) |
- All 14 wrong answers accepted on
synth4/valbefore the third reader were a name with one look-alike mark. - The confidence pass runs in vLLM and the scored KIE answers in transformers; where the two outputs differ (20, 5 and 6 documents), the document is rejected.
- The cost is a low accept rate. The third reader rejects most correct answers too (it misses many names on photos), so only 3–11% of documents are auto-accepted and the share of fully correct documents accepted falls to 4.8%, 12.2% and 17.8%. Everything else goes to a person.
- Caveat: the third-reader check was added after the 14 wrong answers on
synth4/valhad been read. It has no fitted threshold (exact name containment), but these three sets are no longer fully unseen for the gate. - Real fuel-receipt photo (test only): the gate rejects v0.2's answer (13 of 20 fields right). The added checks can only reject more, so this was not re-run.
Training
Rounds
All rounds: LoRA on typhoon-ocr1.5-2b, scored on test sets never used in training.
| Round | Start | Data (samples) | lr, epochs | Real reading / extraction | Verdict |
|---|---|---|---|---|---|
| r1 | base | 6,072 (synthetic forms + Thai-Synth-Receipts) | 1e-4, 2 | 0.624 / 0.394 (HF; base 0.657 / 0.434) | Not released: full-page OCR CER 13.8% → 26.3% (n = 10), stopped emitting <figure> |
| r2 | base | 42,074 | 5e-5, 1 | 0.606 / 0.273 (HF) | Rejected: answered in its own schema instead of the asked keys; template hallucination; early stop |
| r3 | r2 adapter | 25,778 (+ 8,000 key-mapping rows) | 5e-5, 1 | 0.590 / 0.446 (HF); 0.5729 / 0.4881 (vLLM) | Rejected: reading below base; tables regressed |
| r4 | base (fresh LoRA) | 37,593 (+ table normalisation, self-distillation, real tables) | 5e-5, 1 | 0.6415 / 0.506 (vLLM) at strength 1.0; 0.6663 / 0.483 at 0.78 | Previous candidate (0.78); replaced: hold-out loss on FinTabNet −0.050 |
| r5 — v0.1 (released 2026-10-22) | base (fresh LoRA) | 38,630 | 5e-5, 2 | 0.7091 / 0.575 (vLLM; base 0.6659 / 0.4058) at strength 1.0 | Released as v0.1: no hold-out loss; ThaiOCRBench better on 3/3 with the loop guard (Full-page worse without it) |
| r6 | r5 adapter | 53,196 (+ phone-photo tax invoices, + 10,000 thairocr key-mapping rows) | 5e-5, 1 | 0.6945 / 0.5324 at 0.85 | Not released: below v0.1 on both |
| r7 | r5 adapter | 44,931 (r6 without thairocr; 2,070 crawled government pages replace 300) | lower lr, 1 | 0.6751 / 0.5763 at 0.85 | Not released: reading below v0.1 |
| r8 | base (fresh LoRA) | 44,931 (r7 data) | 5e-5, 2 | 0.6385 / 0.5385 at 1.0; 0.6795 / 0.5652 at 0.85 | Not released: hold-out ties v0.1 on all 7 sets, real reading lower |
| r9 | base (fresh LoRA) | 51,074 (r5 mix + phone-photo invoices + 14,684 free-form extraction questions) | 5e-5, 2 | 0.659 / 0.5562 at 1.0; 0.69 / 0.5303 at 0.78; 0.6916 / 0.5549 at 1.0 with the loop guard | Not released: reading below v0.1 |
| r10 | base (fresh LoRA) | 51,708 (r5 mix + photo invoices + free-form extraction) | 5e-5, 2 | 0.6782 / 0.5823 at 1.0 | Not released: reading below v0.1 |
| r11 | base (fresh LoRA) | 74,774 (r10 + 8,000 photo invoices + receipt-printer rows) | 5e-5, 2 | 0.6397 / 0.5784 at 1.0; 0.676 / 0.5805 at 0.85; 0.709 / 0.5732 at 0.85 with the loop guard | Not released: unguarded reading below v0.1; key mapping fell 0.726 → 0.553. Its adapter is the start of r12 and r13 |
| r12 | r11 adapter | 38,630 (r5 mix replay) | 2e-5, 1 | 0.6646 / 0.5642 at 1.0 | Not released: no gain over r11 |
| r13 @0.85 | r11 adapter | 46,606 (r5 mix + 5,978 matching questions + 1,977 free-form) | 2e-5, 1 | 0.6737 / 0.6741; 0.703 / 0.68 with the loop guard | Not released: ThaiOCRBench Document parsing CER 0.2227 vs base 0.2179 with the guard |
| r13 — v0.2 (released 2026-10-26) | r11 adapter | 46,606 (same) | 2e-5, 1 | 0.6802 / 0.7126 at 1.0; 0.6929 / 0.7089 with the loop guard | Released as v0.2: no hold-out loss; ThaiOCRBench better than the base on 3/3 with the loop guard (Document parsing by 0.0006); trust gate 0 wrong accepted once the character-level name check was added |
| r14 | r13 adapter | 63,964 (r13 mix + 5,679 photo invoices with look-alike name swaps + 3,000 photo-invoice replay) | 2e-5, 1 | 0.6604 / 0.6565; 0.6879 / 0.6736 with the loop guard | Not released: trust gate (second reader) accepted 9 wrong answers; extraction below r13 |
| r15 | r13 adapter | r14 mix + 6,500 documents of the same name swaps over 13 document types | 2e-5, 1 | 0.6535 / 0.5922; 0.6719 / 0.6027 with the loop guard | Not released: trust gate (second reader) accepted 11 wrong answers; extraction below r13. Best ThaiOCRBench so far with the guard (0.193 / 0.335 / 0.264) |
Rounds 10–13 (2026-10-23/24): the free-form extraction questions raise extraction on real documents (0.575 → 0.713 for r13), but every round since r10 reads full pages about 0.03 below v0.1 without the loop guard, mostly from repetition on long tables. Round 11 also lost key mapping: it copied values as printed instead of choosing from the values given in the question. Round 13 added about 6,000 matching questions, built from gold tables so the answer is one of the offered values, and recovered most of it (0.553 → 0.673; v0.1 0.726). Round 13 at strength 0.85 failed the release gate on ThaiOCRBench Document parsing; at full strength it passed and became v0.2 (see Release gate). On one real phone photo of a fuel receipt (test only), v0.2 read 13 of 20 fields right; it still misread the total and the amount in words and filled payment fields the receipt does not print, and the trust gate rejected the answer.
Rounds 14–15 (2026-10-25/26): round 13's trust gate accepted shop names one look-alike mark off (รุ่งรุจิ for printed
รุ่งรุจี). Two rounds of generated invoices with such swaps in the names did not remove the error (wrong accepts on
synth4/val: 14 for round 13, 8 for round 14, 10 for round 15) and lowered real-document extraction (0.709 → 0.674 →
0.603 with the guard). The fix moved to the gate instead (v0.2 production gate), and
round 13, the most accurate on real documents, became v0.2.
Round-8 root cause: the loss is concentrated in long tables. Round 8 runs into the 4,096-token limit (a repetition loop) on 13 of the 147 pages against 7 for v0.1, and a looped page scores near 0. Rounds 7 and 8 share the larger crawled government set, so round 9 returns to the round-5 mix and adds only the phone-photo invoices, to test that cause in isolation. The comparison is being re-run with the loop guard on, as Netra is used in practice.
Round-9 result: the free-form extraction questions it added worked on their own task ("Key information extraction" 0.424 → 0.497), but key mapping fell (0.726 → 0.616) and loops rose again (13 pages hit the token limit against 7 for v0.1), so reading dropped. On a real fuel receipt (test only, not in any training set) round 9 read 19 of 20 fields right against 11 for v0.1 and no longer invented bank details, but its JSON was invalid (it joined two strings with +) and the total was the pre-VAT amount; the trust gate rejected it. v0.1 stays the release.
Round-5 changes, from the round-4 root-cause analysis:
- Dot-leader runs (". . . .", up to thousands of dots in 860 of 2,850 FinTabNet targets) collapsed to
...in every OCR target. Round 4 had learned to emit them until the token limit. <figure>pages no longer upsampled. Round 4 used ×4, which pushed figure pages to about a quarter of OCR targets and made the model describe figures at length while dropping page text.- 4,427 more self-distilled pages (base model outputs, quality-filtered), on top of the 3,959 from round 4. Sources with real gold labels (government pages, FinTabNet, PubTabNet) keep their own labels.
- 300 real Thai government pages added (see below).
- Thai-Synth-Receipts: one OCR target per image (68 contradicting duplicates dropped).
- 2 epochs instead of 1.
v0.2 training: two stages
v0.2 is one LoRA adapter trained in two stages: round 11 from a fresh adapter, then round 13 continuing from it.
| Stage | Data (samples) | lr, epochs | Steps | Mean train loss / final eval loss | Time (1 × RTX 5090) |
|---|---|---|---|---|---|
| r11 | 74,774 | 5e-5, 2 | 18,694 | 0.0238 / 0.1736 | not cleanly measured: a host shutdown stopped it at step 18,483, and it was resumed from the epoch-1 checkpoint |
| r13 | 46,606 | 2e-5, 1 | 5,826 | 0.0721 / 0.1697 | 5,878 s |
Rows added on top of the round-5 mix below (counts as loaded at train start):
| Source | r11 rows | r13 rows | Task | Images | Licence |
|---|---|---|---|---|---|
| Synthetic accounting documents (synth2), raised cap | 4,987 (r5: 2,495) | 2,495 | reading + KIE | synthetic | in-house |
| Phone-photo versions of synthetic tax invoices (synth3) | 8,000 | — | reading + KIE | synthetic | in-house |
| Receipt-printer (POS thermal) synthetic receipts, clean and photo | 5,806 | — | reading + KIE | synthetic | in-house |
| Free-form extraction questions, answers cut from gold page text (synthetic sets, Thai-Synth-Receipts) | 2,962 | 1,977 | free-form KIE | as source | as source |
| Free-form questions answered by Qwen3-VL-8B-Instruct, kept only when every value is printed in the gold text | 620 | — | free-form KIE | as source (incl. 105 Thai government pages, see ATTRIBUTION.md) |
as source; teacher Apache-2.0 |
| Matching questions (pair given items with given values) from gold tables | — | 5,978 | key mapping | as source | as source |
The Qwen-answered rows are the only rows whose answers came from another model; their images are from Thai-Synth- Receipts, the synthetic sets, SmartWhatt, DocAtlas and Thai government pages. The 105 government pages come from 21 PDFs of 20 agencies; 104 of them are from the wider round-7 crawl rather than the 300-page round-5 set. They passed the same filter that drops pages with personal ID numbers or home addresses.
Round-5 data mix
The round-5 mix is replayed inside both v0.2 stages. Rows as loaded at train start: after per-source caps, after removal of all evaluation and hold-out images, and after replacing a page's own label with the base model's output where the page was self-distilled. A row that has both a reading and a KIE label becomes two samples: 38,630 training samples, 200 validation.
| Source | Rows | Task | Images | Licence |
|---|---|---|---|---|
| In-house synthetic forms (invoice, quotation, receipt, payment voucher) | 1,476 | reading | synthetic | in-house |
| In-house synthetic Thai accounting documents (6 types) | 2,439 | reading + KIE | synthetic | in-house |
| KIE labels for the synthetic forms | 1,383 | KIE | synthetic | in-house |
| Thai-Synth-Receipts | 3,000 | reading | synthetic | MIT |
| Thai Wikipedia text rendered as articles/reports/posters | 8 own label (+1,394 pages in self-distillation) | reading | synthetic render | CC BY-SA 4.0 (text) |
| SmartWhatt thai_doc_parsing (Thai government publications) | 39 own label (+1,201 pages in self-distillation) | reading | real | ODC / CC BY / CC BY-SA (per document) |
| Thai government documents (procurement notices, budget reports, agency financial statements, blank official forms; 13 agencies) | 300 | reading | real | not copyrighted (Copyright Act B.E. 2537, section 7) |
| iapp Thai handwriting | 2,101 | reading | real | Apache-2.0 |
| CORD v2 (receipts) | 760 | partial KIE (annotated keys only) | real | CC BY 4.0 |
| FATURA2 (invoices) | 1,481 | partial KIE (annotated keys only) | template-generated | CC BY 4.0 |
| Key-mapping rows built from the synthetic sets, CORD and FATURA | 8,000 | key mapping | as source | as source |
| Self-distillation (base outputs on train-split images of the sources in this table) | 8,386 | reading | as source | base output (Apache-2.0); images as source |
| FinTabNet.c (table crops → HTML) | 2,850 | tables | real | CDLA-Permissive-2.0 |
| PubTabNet (table crops → HTML) | 2,850 | tables | real | CDLA-Permissive-1.0 |
| DocAtlas Instruct, Thai rows | 31 own label (+537 pages in self-distillation) | reading | real | CC BY 4.0 |
| WildReceipt | 1,168 | key mapping | real | Apache-2.0 |
Every filled-in Thai accounting document in training (tax invoices, receipts, 50 ทวิ, slips, billing notes) is synthetic. The real Thai pages are government publications and Thai handwriting. The government pages (new in round 5) were issued by ministries, departments, state agencies and local administrations, which section 7 of the Thai Copyright Act B.E. 2537 excludes from copyright. Pages showing private individuals' ID numbers or home addresses were filtered out. No customer documents were used.
Hyperparameters
| Setting | Value |
|---|---|
| Method | LoRA on vision and language layers, attention + MLP |
| Rank / alpha / dropout | 16 / 16 / 0 |
| Trainable parameters | 23,724,032 |
| Schedule | cosine, warmup ratio 0.05 |
| Batch | 2 × 4 gradient accumulation = 8 |
| Optimiser | AdamW 8-bit, weight decay 0.01 |
| Precision | bf16 |
| Image size | long side ≤ 1400 px |
<figure> upsampling |
none |
| Seed | 3407 |
| Learning rate, epochs, losses, time | per stage: see v0.2 training (round 5 / v0.1: 5e-5, 2 epochs, 9,658 steps, 0.0953 / 0.1692, 17,550 s) |
| Software | Unsloth 2026.10.7, transformers 5.17.0, TRL 1.15.0, PEFT 0.21.2 |
| Merge | LoRA merged into the base at full strength (1.0), bf16. Strengths 0.85 / 0.78 were evaluated; 0.85 failed the release gate |
Known limitations
From the per-round root-cause analyses. Items marked not re-measured have not been checked on v0.2.
- Page reading is slightly below v0.1. On Netra-Bench-Real, v0.2 reads 0.013 lower than v0.1 with the guard (0.693 vs 0.706) and 0.029 lower without it (0.680 vs 0.709); ThaiOCRBench CER is higher than v0.1's on all three tasks. If you only need page reading, v0.1 (in this repository's history) reads slightly better.
- Repetition loops. Without the loop guard a few pages loop until the token limit (ThaiOCRBench: 7 of 120 v0.2
outputs flagged; Netra-Bench-Real: 9 of 147 hit the limit, v0.1 7). This is why Document parsing fails the release
rule without the guard. The guard is measured with
transformers only; with vLLM, flagged pages must be re-run (see Loop guard). Diagram-heavy pages can
still be transcribed entirely inside one
<figure>block. - Template hallucination. Round 2 transcribed a restaurant menu with an invented shop address and
TAX ID:line. Cause: most accounting training targets are synthetic documents with shared header layouts. A plausible fabricated tax ID is the most damaging error for accounting use. Not re-measured; a hallucination probe set is planned. Always cross-check extracted tax IDs against an independent OCR pass. - The trust gate needs an OCR cross-read. Without it (measured on v0.1), the gate accepted 72.5% of v0.1's synthetic KIE answers, and only 64.4% of those were fully correct (wrong months in dates, one-letter name slips). Use the gate only in cross-read mode with a real OCR pass, the second reader and the character-level name check (see Trust gate); it then auto-accepts only 3–11% of documents.
- Early stop on long pages. Round 2 returned 30–190 characters where the reference was 600–2,000. Short synthetic targets dominate the length distribution. Rounds 4–5 added distilled long pages; output/reference length ratio not re-measured.
- Tables. With the loop guard, ThaiOCRBench table CER is 0.302 (base 0.308, no guard 0.286, v0.1 with the guard
0.293). Netra-Bench-Real table parsing is below v0.1 (0.665 vs 0.716 without the guard). The retry's
no_repeat_ngram_size=16also breaks legitimately repeated cells. - Accounting data is synthetic. No real Thai tax invoice, receipt, slip, 50 ทวิ or billing note was used for training. Synthetic layouts are low-entropy (round 1 reached train loss 0.0177), so accuracy on unseen real layouts is lower than the synthetic KIE numbers suggest. In one real phone photo of a VAT-inclusive tax invoice, an earlier round (r4) read every key field correctly in reading mode. In KIE mode it returned invalid JSON, recomputed VAT on top of the printed VAT-inclusive total and invented payment fields. On 8 real receipts (test only, not released) v0.2 gets 0.638 of 105 fields right (v0.1 0.495): better, but far from bookable without review. On one real fuel-receipt photo it still misreads the total and fills payment fields that are not printed. 50 ทวิ certificates were the weakest synthetic type in v0.1 (field accuracy 0.623; v0.2 0.952).
- Small real test set. 113 reading and 34 extraction documents; standard error ≈ 0.03 on reading means and ≈ 0.07 on extraction means. Differences under ~0.05 (reading) are not distinguishable from noise. v0.2's extraction gain over the base (+0.307, standard error 0.082) is clear; its reading difference from the base and from v0.1 is not.
- Rounds and merge strength chosen on test sets. Rounds were compared on Netra-Bench-Real, and the 0.85 vs 1.0 choice was settled on the ThaiOCRBench gate set (see Release gate). The hold-out sets are the independent check.
- Schema imposition / generic extraction. Round 2 answered free-form questions in its own 28-key schema. Key-mapping data fixed most of it (mapping 0.296 → 0.726 in v0.1). v0.2 answers free-form questions far better (Key information extraction 0.752 vs 0.424 for v0.1 and 0.516 for the base) but pairs given items with given values less well than v0.1 (Key information mapping 0.673 vs 0.726).
- Handwriting (n = 6) scores below the base (0.500 vs 0.531).
- Fine-grained text recognition scores 0.000 for every model on the 2 documents in the set.
- Dates. KIE converts พ.ศ. to ค.ศ.; two-digit พ.ศ. years are read as 25yy. Synthetic training dates span 2025–2027.
Licence and attribution
The model weights are released under Apache-2.0, the licence of the base model. Training data carries the terms below. Attribution is required for CC BY, ODC and CDLA sources.
| Data | Licence | Attribution |
|---|---|---|
| typhoon-ai/typhoon-ocr1.5-2b (base) | Apache-2.0 | Nonesung et al., "Typhoon OCR: Open Vision-Language Model For Thai Document Extraction", arXiv:2601.14722 (2026) |
| Thai-Synth-Receipts | MIT | DuckerMaster/Thai-Synth-Receipts on Hugging Face |
| iapp Thai handwriting | Apache-2.0 | iapp/thai_handwriting_dataset on Hugging Face |
| SmartWhatt thai_doc_parsing | Open Data Common / CC BY / CC BY-SA, per document | SmartWhatt/thai_doc_parsing; per-document title, publisher, licence and URL in ATTRIBUTION.md |
| Thai government documents (rounds 5 and 11) | Not copyrighted: Copyright Act B.E. 2537, section 7 | Issuing agencies listed in ATTRIBUTION.md |
| Qwen/Qwen3-VL-8B-Instruct (teacher for 620 free-form answers, round 11) | Apache-2.0 | Qwen Team, Qwen3-VL; Qwen/Qwen3-VL-8B-Instruct on Hugging Face |
| Thai Wikipedia | CC BY-SA 4.0 | Thai Wikipedia contributors; article URLs recorded per training page |
| CORD v2 | CC BY 4.0 | Park et al., "CORD: A Consolidated Receipt Dataset for Post-OCR Parsing", NeurIPS 2019 Document Intelligence Workshop; naver-clova-ix/cord-v2 |
| FATURA2 | CC BY 4.0 | Limam, Dhiaf, Kessentini, "FATURA: A Multi-Layout Invoice Image Dataset for Document Analysis and Understanding", arXiv:2311.11856 (2023); Zenodo record 10371464 |
| FinTabNet.c | CDLA-Permissive-2.0 (original FinTabNet: CDLA-Permissive-1.0) | Smock, Pesala, Abraham, "Aligning benchmark datasets for table structure recognition", ICDAR 2023, arXiv:2303.00716; IBM FinTabNet, Zheng et al., WACV 2021 |
| PubTabNet | CDLA-Permissive-1.0 | Zhong, ShafieiBavani, Jimeno Yepes, "Image-based table recognition: data, model, and evaluation", ECCV 2020, arXiv:1911.10683; images from the PubMed Central Open Access Subset |
| DocAtlas Instruct (Thai rows) | CC BY 4.0 | Heakl et al., "DocAtlas: Multilingual Document Understanding Across 80+ Languages", arXiv:2605.12623 (2026) |
| WildReceipt | Apache-2.0 | Sun et al., "Spatial Dual-Modality Graph Reasoning for Key Information Extraction", arXiv:2103.14470 (2021); OpenMMLab MMOCR |
| ThaiOCRBench (evaluation only) | CC BY-SA 4.0 | Nonesung et al., "ThaiOCRBench: A Task-Diverse Benchmark for Vision-Language Understanding in Thai", arXiv:2511.04479 (2025) |
Modifications to third-party data: table crops and HTML targets rebuilt from annotations (FinTabNet.c, PubTabNet); markdown normalised to the output conventions above (DocAtlas); key-mapping prompts generated from box labels (WildReceipt); CORD/FATURA labels converted to the KIE schema with unannotated keys removed from the target.
Licence decision for this release: the weights are Apache-2.0. CC BY-SA material (Thai Wikipedia text rendered to pages; 36 page images from 15 CC BY-SA SmartWhatt documents) was a training input only. No training data, page image or label is redistributed with the model, and no ThaiOCRBench image is included in this repository.
Citation
@misc{netra2026,
title = {Netra-2B: a Thai accounting-document reader fine-tuned from Typhoon OCR 1.5},
author = {botnick},
year = {2026},
note = {Version 0.2},
url = {https://huggingface.co/botnick/Netra-2B}
}
Please also cite the base model:
@misc{nonesung2026typhoonocropenvisionlanguage,
title = {Typhoon OCR: Open Vision-Language Model For Thai Document Extraction},
author = {Surapon Nonesung and Natapong Nitarach and Teetouch Jaknamon and Pittawat Taveekitworachai and Kunat Pipatanakul},
year = {2026},
eprint = {2601.14722},
archivePrefix = {arXiv},
primaryClass = {cs.CL},
url = {https://arxiv.org/abs/2601.14722}
}
- Downloads last month
- 32




