Skip to main content

How to run OpenThai 2.0 OCR Text and OCR Markdown on Windows, Mac and Linux

· 11 min read
Kobkrit Viriyayudhakorn
CEO @ iApp Technology

Today we released two Thai OCR models as free downloads: OpenThai 2.0 OCR Text and OpenThai 2.0 OCR Markdown. This guide shows how to run each of them on your own computer, on Windows, macOS or Linux, from installing Python to turning a PDF into text or Markdown. Your documents stay on your machine.

Which model to use​

OpenThai 2.0 OCR TextOpenThai 2.0 OCR Markdown
What it writesThe page's text in reading order, with a box and a confidence for every lineMarkdown, with tables as HTML (rowspan and colspan), and a box and a class for every block
Download size4 MB2.5 GB
Runs onAny CPU; no GPU neededAn NVIDIA GPU (fastest), a Mac with Apple silicon, or a CPU
SpeedAbout 0.5 s a page on a Mac with M3 Max1.6 s a page on one NVIDIA H100; 15 to 27 s a page on a Mac with M3 Max
LicenceApache-2.0OpenMDW-1.1

Use OCR Text when you need the words: search, copying text out of scans, or feeding a chatbot. Use OCR Markdown when the layout matters, above all tables, whose figures must stay in their rows and columns.

The Mac figures were measured for this guide on a MacBook Pro with M3 Max, on a Thai government letter and a table page; the H100 figure is from the model card.

Without writing code: Chinda Bot​

Chinda Bot, our free desktop assistant, already has OCR Text built in. Attach a PDF with the paperclip, or drag it onto the chat, and every page is read on your computer; click the file's name to see and copy the text that was read. Chinda Bot runs on a Mac with Apple silicon, on Windows PCs (Intel or AMD, and Windows on Arm) and on Linux arm64.

Chinda Bot reads text lines only. For tables kept as tables, use OCR Markdown, below.

Before you start: Python​

Both models run from Python 3.10 or later. We tested this guide with Python 3.13 on macOS and Python 3.12 on Ubuntu 24.04.

Download Python 3.13 from python.org and run the installer. On its first screen, tick Add python.exe to PATH. Then open Command Prompt (press the Windows key and type cmd). Every Windows command below is for Command Prompt.

OpenThai 2.0 OCR Text, on any computer​

1. Make a folder and a Python environment​

mkdir thai-ocr
cd thai-ocr
py -m venv .venv
.venv\Scripts\activate

The environment keeps these packages apart from the rest of your computer. Activate it again (the last line) whenever you open a new window.

2. Install the packages and download the model​

The same two commands on every system:

pip install onnxruntime opencv-python-headless pyclipper numpy pillow pypdfium2 huggingface_hub
hf download iapp/OpenThai2.0-OCR-Text --local-dir OpenThai2.0-OCR-Text

The model is two files of 1.8 MB and 2.4 MB, with its reading code, in the folder OpenThai2.0-OCR-Text.

Windows on Arm (RTX Spark and other Arm PCs): two of the packages, opencv-python-headless and pyclipper, do not yet publish versions for Windows on Arm, so the pip install line fails there. Either install the x64 version of Python from python.org, which Windows runs under emulation, more slowly; or use Chinda Bot, which runs OCR Text natively on Windows on Arm.

3. Save the script​

Save this as ocr_text.py in the thai-ocr folder:

ocr_text.py
"""Read Thai and English text from pictures and PDFs with OpenThai 2.0 OCR Text.

Usage: python ocr_text.py document.pdf [more files ...]
Prints the text and saves it next to each file as <file>.txt.
"""
import sys
import time
from pathlib import Path

MODEL_DIR = Path(__file__).resolve().parent / "OpenThai2.0-OCR-Text"
sys.path.insert(0, str(MODEL_DIR))
from runtime.line_engine import LineOCR # noqa: E402


def pages(path):
"""Each page of a PDF as a picture (drawn at 200 dpi), or the picture itself."""
if path.suffix.lower() == ".pdf":
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument(str(path))
for i in range(len(pdf)):
yield pdf[i].render(scale=200 / 72).to_pil()
else:
yield str(path)


ocr = LineOCR(str(MODEL_DIR), threads=2)
for name in sys.argv[1:]:
path = Path(name)
texts = []
for number, page in enumerate(pages(path), start=1):
start = time.perf_counter()
result = ocr.read(page)
seconds = time.perf_counter() - start
print(f"{path.name} page {number}: {len(result['lines'])} lines in {seconds:.2f} s", file=sys.stderr)
texts.append(result["text"])
text = "\n\n".join(texts)
path.with_name(path.name + ".txt").write_text(text, encoding="utf-8")
print(text)

4. Read a document​

python ocr_text.py document.pdf

It prints the text and saves it as document.pdf.txt. It reads PDFs page by page, and pictures (PNG, JPG and the other common formats) as they are; give it several files at once if you like. Each page takes about half a second on a recent computer.

Two notes:

  • Scanned PDFs and PDFs with text both work, because every page is drawn as a picture and read.
  • For thousands of pages, change threads=2 to threads=1 and run one copy of the script per CPU core, each on its own share of the files. In one process, more threads stop helping after about two cores. The model card measures about 950 pages a minute on 8 server cores.

OpenThai 2.0 OCR Markdown​

This model is 2.5 GB, so it is best on a GPU. We run it on Linux with NVIDIA H100 GPUs, and we tested this guide on a Mac with M3 Max. Pick your system:

With an NVIDIA GPU: use WSL 2, which runs Ubuntu inside Windows and gives it the GPU through your normal NVIDIA driver. In PowerShell opened as administrator:

wsl --install -d Ubuntu-24.04

Restart when asked, open Ubuntu from the Start menu, and follow the Linux steps on this page inside it. We have not yet tested OCR Markdown on Windows ourselves.

Without an NVIDIA GPU: use the OCR Markdown API below, or Chinda Bot for plain text.

Save this as ocr_markdown.py, in the folder that holds OpenThai2.0-OCR-Markdown:

ocr_markdown.py
"""Read pictures and PDFs into Markdown with OpenThai 2.0 OCR Markdown.

python ocr_markdown.py document.pdf
runs the model on this computer: an NVIDIA GPU, a Mac's GPU, or the CPU
python ocr_markdown.py document.pdf --server http://localhost:8000/v1
sends the pages to a vLLM server instead (the fastest way)

Saves the Markdown next to each file as <file>.md. Tables come out as HTML.
"""
import argparse
import sys
import time
from pathlib import Path

from PIL import Image
from nemotron_parse.inference import DocumentParser, GenerationError, GenerationSettings, TransformersBackend

MODEL_DIR = Path(__file__).resolve().parent / "OpenThai2.0-OCR-Markdown"


def pages(path):
"""Each page of a PDF as a picture (drawn at 200 dpi), or the picture itself."""
if path.suffix.lower() == ".pdf":
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument(str(path))
for i in range(len(pdf)):
yield pdf[i].render(scale=200 / 72).to_pil()
else:
yield Image.open(path)


def local_parser():
import torch
from transformers import AutoModel, AutoProcessor

if torch.cuda.is_available():
device, dtype = "cuda", torch.bfloat16
elif torch.backends.mps.is_available():
device, dtype = "mps", torch.float32
else:
device, dtype = "cpu", torch.float32
model = AutoModel.from_pretrained(MODEL_DIR, trust_remote_code=True, dtype=dtype).to(device).eval()
processor = AutoProcessor.from_pretrained(MODEL_DIR, trust_remote_code=True)

class Backend(TransformersBackend):
# a vLLM server reads the prompt after a new line; send the same tokens here
def __call__(self, image, *, prompt, **kwargs):
return super().__call__(image, prompt="\n" + prompt, **kwargs)

print(f"Running the model on {device}", file=sys.stderr)
# read greedily (temperature 0); with sampling, a Mac's GPU garbled pages in our test
return DocumentParser(backend=Backend(model, processor), fullpage=GenerationSettings(temperature=0))


args = argparse.ArgumentParser()
args.add_argument("files", nargs="+")
args.add_argument("--server", help="a vLLM server, for example http://localhost:8000/v1")
args = args.parse_args()

parser = DocumentParser(base_url=args.server, model="nemotron-parse") if args.server else local_parser()
with parser:
for name in args.files:
path = Path(name)
parts = []
for number, page in enumerate(pages(path), start=1):
start = time.perf_counter()
try:
parts.append(parser.parse(page, mode="single")["markdown"])
except GenerationError:
parts.append(f"<!-- page {number} could not be read completely -->")
print(f"{path.name} page {number}: {time.perf_counter() - start:.1f} s", file=sys.stderr)
markdown = "\n\n".join(parts)
path.with_name(path.name + ".md").write_text(markdown, encoding="utf-8")
print(markdown)

It writes document.pdf.md, with each table as an HTML <table>; Markdown viewers and most language models read those as they are. If a page is ever cut off at the length limit, the script notes it in the file and goes on with the next page.

No GPU? Use the API​

Both models also run as hosted APIs, with nothing to install:

If a Thai document does not read well, write to us at sale@iapp.co.th with a sample that holds no personal data; those pages help us improve the next version.