RecognitionPredictor deterministically outputs Cyrillic/Armenian homoglyphs for specific short German text lines (llamacpp backend)
Environment
- surya-ocr 0.21.2
- Inference backend: llamacpp (auto-selected, no NVIDIA GPU present), llama-server b9752, ROCm build (AMD Ryzen AI Max+ 395 / gfx1151)
- Model: datalab-to/surya-ocr-2-gguf (default checkpoint)
- Python 3.14, Linux x86_64 (Ubuntu 26.04)
- TORCH_DEVICE=cpu (layout/detection model only; recognition goes through the llama.cpp backend)
What happened
Running layout detection + recognition on a synthetic test image
containing German business-letter text, most lines are transcribed
correctly (including umlauts and §), but two specific short lines are
consistently transcribed using the wrong script (Cyrillic/Armenian
characters instead of Latin), including Cyrillic homoglyphs that are
visually near-identical to the intended Latin letters:
- Input:
Max MustermannOutput:Мах Мустерманн(Cyrillic Ð/а substituted for Latin M/a, plus extra characters) - Input:
Sehr geehrte Damen und Herren,Output:Տեիր ցееիրեе Дамен սոд Неրлеդ,,(mixed Armenian/Cyrillic gibberish)
Other lines in the same document/run were transcribed correctly, e.g.:
hiermit kündige ich den Vertrag Nr. 12345 gemäß § 622 BGB fristgerecht zum nächstmöglichen Termin.→ transcribed exactly correctly, including§and umlauts.
Reproducibility
This is fully deterministic, not random noise:
- Ran the identical script 3 times with the same layout/line order: identical (character-for-character) garbled output all 3 times.
- Reordered the lines in the source image (moved the two affected lines to different positions/different neighboring blocks) and reran: the exact same two lines produced the exact same garbled output again, confirming the failure is tied to the text content, not its position in the layout or in the document.
- Ruled out a font-rendering artifact: initial testing used a math font (KaTeX) that garbled umlauts, but switching to DejaVu Sans (a standard sans-serif font with correct German glyph coverage) did not change this specific failure — most lines then read perfectly, but these same two lines still came out in Cyrillic/Armenian.
Expected behavior
For a page that is clearly German/Latin-script throughout, isolated lines should not switch to an entirely different (and for this use case, essentially random) writing system — particularly concerning because some of the substituted characters are homoglyphs indistinguishable from Latin letters at a glance (e.g. Cyrillic Х/а vs Latin X/a), which is very easy to miss in a proofreading/review pass on a real document.
Why this matters
We're evaluating Surya for OCR on German legal/business documents where a corrupted name (e.g. in a contract or court filing) silently rendered in visually-identical Cyrillic characters would be worse than an obviously wrong OCR result, since it's much harder for a human reviewer to catch.
Repro script
import os
os.environ["TORCH_DEVICE"] = "cpu"
# LLAMA_CPP_BINARY must point at a llama-server build for your platform;
# any recent llama.cpp build with the mtmd/vision support Surya needs
# should reproduce this (this was tested against a ROCm build, b9752).
# os.environ["LLAMA_CPP_BINARY"] = "/path/to/llama-server"
from PIL import Image, ImageDraw, ImageFont
TEST_TEXT = [
"Kündigungsschreiben",
"",
"Sehr geehrte Damen und Herren,",
"",
"hiermit kündige ich den Vertrag Nr. 12345 gemäß § 622 BGB",
"fristgerecht zum nächstmöglichen Termin.",
"",
"Mit freundlichen Grüßen",
"Max Mustermann",
]
img = Image.new("RGB", (1000, 700), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28
)
y = 40
for line in TEST_TEXT:
draw.text((60, y), line, fill="black", font=font)
y += 50
img.save("/tmp/surya_test.png")
from surya.fast_layout import FastLayoutPredictor
from surya.recognition import RecognitionPredictor
image = Image.open("/tmp/surya_test.png")
layout_result = FastLayoutPredictor()([image])[0]
ocr_result = RecognitionPredictor()([image], [layout_result])[0]
for block in ocr_result.blocks:
print(f"[{block.label}] {block.html!r}")
# Expected: all lines transcribed in German/Latin script.
# Actual: "Sehr geehrte Damen und Herren," and "Max Mustermann" come
# back in Armenian/Cyrillic script, identically across repeated runs
# and independent of their position in TEST_TEXT.Source: datalab-to/surya