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:

  1. A YOLO26-Text detector finds every word in the image. It’s an oriented-box (OBB) model, so it handles rotated text.
  2. The page rotation is estimated from the word boxes, and each word is cropped upright.
  3. A recognizer reads each word. The default is PaddleOCR’s PP-OCRv6, run on ONNX Runtime.
  4. 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.