You need to agree to share your Hugging Face information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

Netra · เนตร logo, an eye whose lower lid is a line of text. Reads Thai accounting documents — text and key fields, in a 2B model. v0.1, 2B, Thai / English, Apache-2.0. Beside it, one synthetic tax invoice: printed ใบกำกับภาษี becomes "doc_type": "tax_invoice", 29 ก.ย. 2570 becomes "date": "2027-09-29", 0-7552-78249-83-1 becomes "tax_id": "0755278249831", 4,183.70 becomes "total": 4183.7

Netra-2B v0.1

Status: released — v0.1 (2026-10-22). Use it with the loop guard; the release gate is passed only with the guard on. Update 2026-10-22: rounds 6, 7 and 8 were trained and tested against v0.1; none beat it, so v0.1 stays the release (Rounds). The trust gate now checks every field and cross-reads two independent OCR passes (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: reading 0.709 vs 0.666 for the base, extraction 0.575 vs 0.406 (Netra-Bench-Real, 147 real Thai finance documents).
  • Receipt key mapping: key accuracy 52.8% vs 10.4% for the base, valid JSON 97.3% vs 41.6% (NECTEC thairocr, 1,091 receipts).
  • Accounting extraction: field accuracy 90.8% and valid JSON 100% on 120 held-out synthetic documents (accounting KIE).
  • Ahead of the larger 3B sibling on real documents: 0.709 / 0.575 vs 0.669 / 0.474 for typhoon-ocr1.5-3b-qat (reading / extraction, same benchmark and settings).

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

Bar chart, Netra-2B v0.1 vs the base typhoon-ocr1.5-2b, higher is better. Netra-Bench-Real: page reading 0.666 to 0.709, information extraction 0.406 to 0.575. NECTEC thairocr receipt key mapping: key accuracy 0.104 to 0.528, valid JSON 0.416 to 0.973. Synthetic accounting KIE: field accuracy 0.037 to 0.908, valid JSON 0.750 to 1.000, every field right 0.000 to 0.542.

Netra improves on its base on every benchmark in the chart. 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 gains are smaller and measured on 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) พัฒนาต่อยอดจาก Typhoon OCR 1.5 2B โดย fine-tune ด้วยเทคนิค LoRA สำหรับเอกสารทางบัญชีและธุรกิจไทยโดยเฉพาะ (เช่น ใบกำกับภาษีเต็มรูป, ใบกำกับภาษีอย่างย่อ, ใบลดหนี้/ใบเพิ่มหนี้, หนังสือรับรองการหักภาษี ณ ที่จ่าย ตามมาตรา 50 ทวิ, สลิปโอนเงิน และใบวางบิล) ตลอดจนเอกสารทางราชการ พร้อมผสานน้ำหนักโมเดล (merge) กลับเข้าสู่โมเดลฐานอย่างเต็มรูปแบบ (1.0 full strength)

  • รูปแบบผลลัพธ์: แปลงเนื้อหาบนหน้าเอกสารเป็น Markdown (แสดงโครงสร้างตารางด้วย HTML) และสกัดข้อมูลสำคัญทางบัญชี (Key Information Extraction: KIE) ให้อยู่ในรูป JSON ตามโครงสร้างข้อมูล (schema) 28 คีย์มาตรฐานเดียว

  • ประสิทธิภาพบนเอกสารจริง (Netra-Bench-Real): ประเมินผลกับเอกสารการเงินจริง 147 ฉบับ (ชุดทดสอบที่ไม่เคยใช้ในการเทรน): ความแม่นยำในการอ่านเอกสารอยู่ที่ 0.7091 (โมเดลฐาน 0.6659) และความแม่นยำในการสกัดข้อมูลอยู่ที่ 0.575 (โมเดลฐาน 0.4058) ซึ่งมีประสิทธิภาพเหนือกว่าโมเดลขนาดใหญ่กว่าอย่าง typhoon-ocr1.5-3b-qat (0.6691 / 0.4738)

  • ชุดทดสอบแบบ Hold-out (802 รายการ): ไม่ด้อยกว่าโมเดลฐานในทุกชุดข้อมูล โดยมีคะแนนชนะ 5 ชุด และเสมอ 2 ชุด (ไม่มีชุดใดแพ้)

  • ข้อกำหนดสำคัญในการประมวลผล (Loop Guard): สำหรับงานแปลงเอกสารทั้งหน้า (Full-page OCR) จำเป็นต้องเปิดใช้งานระบบป้องกันข้อความวนซ้ำ (loop guard) จึงจะทำผลงานได้เหนือกว่าโมเดลฐานครบทั้ง 3 ภารกิจ หากไม่เปิดใช้ งานอ่านทั้งหน้าจะผิดพลาดมากกว่าโมเดลฐาน (CER 0.386 เทียบกับ 0.359)

  • ข้อควรระวังในการนำไปใช้: ข้อมูลทางบัญชีที่ใช้เทรนเป็นชุดข้อมูลสังเคราะห์ (synthetic data) ทั้งหมด จึงห้ามนำผลลัพธ์ไปบันทึกบัญชีโดยอัตโนมัติ เว้นแต่จะผ่านการตรวจสอบยืนยัน (Trust Gate) เทียบกับผลอ่าน OCR หรือมีผู้ตรวจทาน และไม่อนุญาตให้นำไปประมวลผลบัตรประจำตัวประชาชนหรือเอกสารส่วนบุคคล

  • อัปเดต 22 ต.ค. 2569: เทรนรอบ 6, 7, 8 แล้ว ไม่มีรอบไหนชนะ v0.1 จึงยังใช้ v0.1 · ตัวตรวจ (Trust Gate) ตรวจครบทุกช่อง และเทียบกับผลอ่านจากโมเดลอ่านตัวที่สองที่เป็นอิสระ: ผิดแต่ผ่าน 0–2 ฉบับจาก 117–120 ฉบับ แต่ผ่านอัตโนมัติได้เพียง 7–15% ที่เหลือส่งคนตรวจ


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 VAT-inclusive receipts are still error-prone. The model may recompute VAT on top of a total that already includes it.
  • 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 (training round 5) trained on Thai accounting and government documents, 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 9 of the 120 round-5 ThaiOCRBench outputs. 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:

  • The guard was measured on reading output with transformers generate only (see ThaiOCRBench). There the first pass used repetition_penalty=1.1, not the 1.05 recommended here.
  • It was not measured on extraction output or with vLLM. vLLM's SamplingParams has no no_repeat_ngram_size, so with vLLM, re-run the pages that degenerate() flags through transformers with the retry settings above.
  • The retry also breaks legitimately repeated table cells. On ThaiOCRBench tables it costs 0.013 CER compared with no guard (still better than the base).

vLLM (offline batch)

This is the configuration used for Netra-Bench-Real and the hold-out sets below (vLLM 0.31.0, no loop guard).

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. v0.1 passes only with the loop guard. Without it, ThaiOCRBench Full-page OCR is worse than the base (CER 0.386 vs 0.359). The guard is therefore part of the recommended inference.

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 against all 17 round-5 training files; 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. 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 (released) 1.0 0.7091 0.575
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

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

Netra v0.1 has the best reading score of every model measured. On extraction the larger general models (8B, 12B) are ahead; closing that gap is the current work.

How to read this table:

  • This benchmark is the selection set. The rule, fixed before the round-5 results were read: among the merge strengths whose reading score is at least the base's, take the best extraction. Full strength won on both columns. Because v0.1 was selected on this set, its numbers here are somewhat optimistic. The hold-out sets and ThaiOCRBench (below) are the independent checks.
  • Paired difference, v0.1 − base, computed per document: reading +0.0432 (standard error 0.0205), extraction +0.1692 (standard error 0.0957). The reading gain is about two standard errors; the extraction gain is within noise at n = 34. Per-model standard error of the mean is about 0.03 for reading and 0.07 for extraction.
  • Per document, v0.1 vs base: 62 better, 54 worse, 31 equal.
  • The typhoon-ocr1.5-3b-qat row was run with the same harness, prompts and vLLM settings.
  • Round 1–3 reports quote HF-generate numbers for the base (0.657 / 0.434). Compare numbers within one engine only.

Per task (mean score, same runs):

Task n Base r4 @0.78 (previous) r5 (v0.1)
Table parsing 42 0.650 0.656 0.716
Document parsing 39 0.728 0.699 0.756
Key information extraction 17 0.516 0.486 0.424
Key information mapping 17 0.296 0.480 0.726
Text recognition 14 0.582 0.583 0.588
Full-page OCR 10 0.823 0.895 0.896
Handwritten content extraction 6 0.531 0.566 0.563
Fine-grained text recognition 2 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) is below the base (0.424 vs 0.516).

Throughput (vLLM 0.31.0, one RTX 5090, gpu_memory_utilization=0.6, max_num_seqs=32): 147 documents generated in 70.9 s. 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.

Grouped bar chart of ThaiOCRBench character error rate, lower is better, 40 documents per task. Full-page OCR: base 0.359, v0.1 without guard 0.386, v0.1 with loop guard 0.340. Document parsing: 0.218, 0.193, 0.172. Table parsing: 0.308, 0.280, 0.293.

Task (n = 40) Base r4 @0.78 (previous) r5, no guard r5 + loop guard (recommended) Pass? (r5 + guard)
Full-page OCR CER 0.359 0.345 0.386 0.340 yes (better)
Document parsing CER 0.218 0.204 0.193 0.172 yes (better)
Table parsing CER 0.308 0.270 0.280 0.293 yes (better)
Outputs with whole-output CER ≥ 0.9 (of 120) 21 22 23 19 —
  • Without the guard, round 5 fails the rule (Full-page OCR 0.386 vs 0.359). On Full-page OCR, 34 of 40 pages tie the base. 6 pages score CER ≈ 1.0: 4 repetition loops until the token limit and 2 diagram-heavy pages transcribed entirely inside one <figure> (which the metric strips). The guard fixes loops, not the <figure> case.
  • The guard was added after the no-guard result was seen, then re-tested on the same 120 pages under the same rule. The Full-page margin over the base (−0.019) is small at n = 40. The guard regenerated 10 of 120 outputs.

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 the round-5 training mix 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.1 − base per row, with a paired bootstrap 95% CI (2,000 resamples). "win" and "loss" are used only when the CI excludes 0.

Dot-and-interval chart of the paired difference v0.1 minus base per hold-out set with 95% confidence intervals. Key mapping +0.688, WildReceipt +0.381, PubTabNet +0.215, FinTabNet +0.086, General Thai pages +0.004 are wins; Thai government documents +0.011 and DocAtlas -0.104 are ties. No set is a loss.

Set n Base v0.1 Δ 95% CI Verdict
Thai government documents (real) 34 0.570 0.580 +0.011 [−0.056, +0.074] tie
FinTabNet tables (real) 150 0.727 0.814 +0.086 [+0.066, +0.108] win
PubTabNet tables (real) 150 0.572 0.787 +0.215 [+0.178, +0.257] win
DocAtlas Thai pages (real) 7 0.631 0.526 −0.104 [−0.283, +0.024] tie
WildReceipt key mapping (real) 61 0.347 0.728 +0.381 [+0.273, +0.493] win
Key mapping (synthetic + CORD/FATURA) 300 0.272 0.959 +0.688 [+0.650, +0.727] win
General Thai pages (rendered) 100 0.976 0.980 +0.004 [+0.002, +0.005] win
  • No set is a loss. The round-4 FinTabNet loss (−0.050, dot-leader loops) became a win after round 5 collapsed dot-leader runs in the training targets.
  • Round 5 trained on 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 for this round (different images; the government pages come from different source PDFs). DocAtlas (n = 7) has a negative point estimate; it is too small to call.

Thai receipt key mapping (thairocr)

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, fixed seed): transfer slip 24, tax invoice 21, abbreviated tax invoice 21, credit/debit note 19, 50 ทวิ certificate 18, billing note 17. 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

A synthetic tax invoice (fictitious companies) from the held-out set, next to Netra-2B v0.1's JSON output for it: doc_type tax_invoice, doc_no TX7009-0832, date 2027-09-29 converted from the printed Buddhist-era date, seller and buyer names, tax IDs and branches, subtotal 3910, VAT 7% 273.7, total 4183.7. Every scored field is correct.

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.

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.

Replay on v0.1's KIE outputs above (120 synthetic documents):

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:

  • 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.

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) base (fresh LoRA) 38,630 5e-5, 2 0.7091 / 0.575 (vLLM; base 0.6659 / 0.4058) at strength 1.0 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

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-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.

Round-5 data mix

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
Learning rate 5e-5, cosine, warmup ratio 0.05
Epochs / steps 2 / 9,658
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
Mean train loss / final eval loss 0.0953 / 0.1692
Hardware / time 1 × RTX 5090, 17,550 s (part of it shared with another GPU job)
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 / 0.70 were evaluated and not selected

Known limitations

From the per-round root-cause analyses. Items marked not re-measured have not been checked on v0.1.

  • Repetition loops. Without the loop guard a few pages loop until the token limit (ThaiOCRBench: 9 of 120 outputs flagged). This is why Full-page OCR 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, 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. That mode has so far been measured only with gold text (an upper bound).
  • 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.293 (base 0.308, no guard 0.280). The retry's no_repeat_ngram_size=16 also 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. This card has no measured result for v0.1 on real photos of accounting documents. 50 ทวิ certificates are the weakest synthetic type (field accuracy 0.623).
  • 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, and the extraction gain over the base is not statistically significant.
  • Merge strength chosen on the test set. See Netra-Bench-Real. The hold-out sets and ThaiOCRBench are the independent checks.
  • 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). Generic non-accounting extraction (food menus, a crypto-exchange receipt) is still below the base (Key information extraction 0.424 vs 0.516) and lower than round 4 (0.486).
  • 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 (round 5) Not copyrighted: Copyright Act B.E. 2537, section 7 Issuing agencies listed in ATTRIBUTION.md
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.1},
  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
13
Safetensors
Model size
2B params
Tensor type
BF16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for botnick/Netra-2B

Finetuned
(1)
this model

Datasets used to train botnick/Netra-2B

Papers for botnick/Netra-2B