Ultralytics 8.4.212, released on 13 November 2026, adds OCR for more writing systems. Its OCR solution can now read Hindi and other Devanagari text, Korean, Cyrillic, Arabic, Thai, Greek, Tamil and Telugu by plugging in PaddleOCR’s PP-OCRv5 language models. These run on ONNX Runtime, so you don’t need to install PaddlePaddle or PaddleOCR. This tutorial shows how to run OCR in a few lines of Python, choose the right recognizer for your script, use the structured results and avoid the most common mistake: reading text in the wrong script with the default model.
What’s new in 8.4.212
From the release notes:
- OCR supports more languages: pick a PP-OCRv5 model to recognize Korean, Cyrillic, Arabic, Devanagari, Thai, Greek, Tamil or Telugu text.
- The models run through ONNX Runtime, without requiring PaddlePaddle or PaddleOCR.
- OCR does not detect the language automatically. You choose the model for the text you expect.
- Hebrew is not included in the new models.
The release also includes training fixes (no more distributed-training hangs on cancel), better ONNX Runtime matching for CUDA 12 PyTorch installs and safer handling of segmentation-mask filenames.
How the OCR pipeline works
The OCR guide describes a two-stage pipeline:
- A YOLO26-Text detector finds every word in the image. It’s an oriented-box (OBB) model, so it handles rotated text.
- The page rotation is estimated from the word boxes, and each word is cropped upright.
- A recognizer reads each word. The default is PaddleOCR’s PP-OCRv6, run on ONNX Runtime.
- Words are grouped into lines and returned in reading order, top to bottom and left to right.
Because detection and recognition are separate, you can swap the recognizer without touching detection. That’s exactly how the new language support works.
Install
pip install -U ultralytics opencv-python
The first OCR run downloads the YOLO26x-Text detector and the recognizer weights, and installs ONNX Runtime (the GPU build if CUDA is available). The docs note that no OCR packages are added to the base Ultralytics dependencies.
Your first OCR script
import cv2
from ultralytics import solutions
ocr = solutions.OCR() # YOLO26x-Text at imgsz=1280 with PP-OCRv6
im0 = cv2.imread("receipt.jpg")
assert im0 is not None, "Error reading image file"
result = ocr(im0)
cv2.imwrite("receipt_ocr.jpg", result.plot_im) # annotated image
for word in result.texts:
print(word["line"], word["text"], round(word["confidence"], 3))
You can also run it from the command line:
yolo solutions ocr source="path/to/video.mp4"
Note that OCR treats each frame on its own. It doesn’t track text across video frames.
Reading Hindi, Korean, Arabic and other scripts
This is the key thing to understand. The default PP-OCRv6 recognizer can only output characters from its built-in 18,708-character set, which covers Chinese, Japanese and Latin-script languages (with partial Vietnamese and Greek). The guide warns that text in another script comes back as lookalike characters. For example, Cyrillic ТЕКСТ reads as TEKCT, often with high confidence. High confidence doesn’t mean correct.
To read a script outside that set, pass a PP-OCRv5 language model name as recognizer:
# Hindi, Marathi or Nepali signboards
ocr_hi = solutions.OCR(recognizer="devanagari_PP-OCRv5_mobile_rec")
# Korean
ocr_ko = solutions.OCR(recognizer="korean_PP-OCRv5_mobile_rec")
# Arabic, Persian, Urdu
ocr_ar = solutions.OCR(recognizer="arabic_PP-OCRv5_mobile_rec")
result = ocr_hi(cv2.imread("shop_sign.jpg"))
print(" ".join(w["text"] for w in result.texts))
The full list of recognizer names from the docs:
recognizer |
Script / languages |
|---|---|
korean_PP-OCRv5_mobile_rec |
Korean (Hangul) and Latin letters |
cyrillic_PP-OCRv5_mobile_rec |
Russian, Ukrainian, Bulgarian, Serbian, Kazakh, Mongolian |
eslav_PP-OCRv5_mobile_rec |
Russian, Ukrainian, Belarusian |
arabic_PP-OCRv5_mobile_rec |
Arabic, Persian, Urdu, Uyghur |
devanagari_PP-OCRv5_mobile_rec |
Hindi, Marathi, Nepali |
th_PP-OCRv5_mobile_rec |
Thai |
el_PP-OCRv5_mobile_rec |
Greek, including accents and final ς |
ta_PP-OCRv5_mobile_rec |
Tamil |
te_PP-OCRv5_mobile_rec |
Telugu |
latin_PP-OCRv5_mobile_rec |
Latin scripts, smaller and faster than PP-OCRv6 |
en_PP-OCRv5_mobile_rec |
English, smaller and faster than PP-OCRv6 |
The detector finds words in any script, so only the recognizer changes.
Handling mixed-language documents
Since OCR won’t switch models for you, a simple approach for documents that may be in one of a few scripts is to run each candidate recognizer and keep the one with the highest average confidence. Treat it as a heuristic and check it on your own data, because a wrong-script model can still report confident lookalike results.
import statistics
CANDIDATES = {
"latin": None, # default PP-OCRv6
"hindi": "devanagari_PP-OCRv5_mobile_rec",
"tamil": "ta_PP-OCRv5_mobile_rec",
}
readers = {
name: solutions.OCR(recognizer=rec) if rec else solutions.OCR()
for name, rec in CANDIDATES.items()
}
def best_read(image):
scores = {}
for name, reader in readers.items():
res = reader(image)
confs = [w["confidence"] for w in res.texts]
scores[name] = (statistics.mean(confs) if confs else 0.0, res)
name, (score, res) = max(scores.items(), key=lambda kv: kv[1][0])
return name, score, res
lang, score, res = best_read(cv2.imread("form.jpg"))
print(lang, round(score, 3))
If you know the region your images come from, such as Tamil Nadu or Maharashtra, just pick the matching recognizer. That’s faster and more reliable.
Working with the results
Each call returns a SolutionResults object. result.plot_im is the annotated image and result.texts is a list of dictionaries in reading order:
| Key | Meaning |
|---|---|
text |
Recognized text |
confidence |
Recognition confidence |
detection_confidence |
Detector confidence |
detection_index |
Original detection index before sorting |
line |
Line number; words on one line share it |
polygon |
Corner points in original-image coordinates |
Here’s how to rebuild lines of text:
from itertools import groupby
lines = [
" ".join(w["text"] for w in words)
for _, words in groupby(result.texts, key=lambda w: w["line"])
]
print("\n".join(lines))
Coordinates always refer to the original image, so you can draw boxes or crop regions without converting them.
Choosing the detector size
The YOLO26-Text family comes in n, s, m, l and x sizes. The default is yolo26x-text at imgsz=1280. The docs say yolo26l-text is within 0.5 points on their benchmark, so it’s a good trade-off on smaller GPUs:
ocr = solutions.OCR(model="yolo26l-text.pt")
On NVIDIA GPUs you can also use recognizer="tensorrt", which builds an FP16 TensorRT engine on first run for the default PP-OCRv6 recognizer.
Bring your own recognizer
For scripts that no PP-OCRv5 model covers, such as Hebrew, pass a callable. It receives one BGR crop at a time and must return a (text, confidence) tuple:
def recognize(crop):
text, conf = my_model.read(crop) # your own model
return text, conf
ocr = solutions.OCR(recognizer=recognize)
Licensing note
Ultralytics itself is AGPL-3.0. The PP-OCRv5 and PP-OCRv6 weights are Apache-2.0 and download on first use. Review the model cards and redistribution terms before you ship them in a product.
Key takeaways
- Ultralytics 8.4.212 adds OCR for Devanagari (Hindi), Korean, Cyrillic, Arabic, Thai, Greek, Tamil and Telugu through PP-OCRv5 models on ONNX Runtime.
- Pass
recognizer="..._PP-OCRv5_mobile_rec". The default model can’t read these scripts and returns confident lookalike characters. - Results come back in reading order with line numbers and polygons, ready to post-process.
- No PaddlePaddle install is needed.
Sources: Ultralytics v8.4.212 release notes and the Ultralytics Scene Text OCR guide.
