TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Korean Document OCR Comparison 2: Five Obstacles During Installation and Code That Used eval() on Model Outputs

Status: Actual measurement records (October 28 to October 30, 2026). Symptoms and commands are taken from measurement logs and execution scripts. Instances where exact error messages could not be copied because they were not left in the logs are noted as such. All three PRs were open (unmerged) as of October 30, 2026, and results are not definitive.

Table of Contents

Part 1showed only the results. Afterward, there were quite a few issues encountered during installation. I write down the symptoms, causes, and fixes so that others walking the same path will wander less. The environment was a Linux server equipped with a single 24GB GPU, and measurements were conducted inside disposable GPU-enabled containers.

1. At a Glance

Obstacle Symptom Fix
Model Download Hugging Face download stops at 2.88GB HF_HUB_DISABLE_XET=1 Disable Xet and download
Two Frameworks Adding Paddle to PyTorch image breaks torch (NCCL conflict) Put Paddle in a separate virtual environment, and Unlimited-OCR dependencies in a separate folder
OpenCV and libGL ImportError: libGL.so.1 Install libgl1·libglib2.0-0 on the system
headless OpenCV triggers PaddleX dependency check error, swapping breaks cv2 folder Delete all four versions and use only one --force-reinstall --no-deps
Long Measurement Remote connection drops, destroying the entire container measurement Split batches and log results line by line per section
Scoring CER inflated to 16% due to coordinate markers Add coordinate regex (Part 1, Section 3)
Security Remote code measures using model output eval() save_results=False , submitted ast.literal_eval PR to upstream

2. Model Download Stopped at 2.88GB

When downloading weights for Unlimited-OCR-2 for the first time, the download did not progress past 2.88GB. It stopped advancing in progress without any errors. The Hugging Face Hub delivers large files using a new storage method called Xet (which splits files into chunks to send and receive without duplication), and huggingface_hub uses that path if hf_xet is installed.

Setting the environment variable HF_HUB_DISABLE_XET=1 reverts it to the legacy HTTP download method. After doing so, downloading the two weight files finished in 1 minute and 50 seconds, and loading the model took 114.7 seconds. I did not separately reproduce why it stopped (network path, hf_xet version, server-side conditions) (unconfirmed). We added this variable by default to the script that spins up the measurement containers.

As an aside, PaddleOCR models were also downloaded from Hugging Face. Setting the source with PaddleX's PADDLE_PDX_MODEL_SOURCE=huggingface and placing the cache folder (PADDLE_PDX_CACHE_HOME) on a persistent volume prevents re-downloading every time a new container is launched.

3. Putting Two Frameworks in One Image

Initially, I tried to run all three engines in one place by adding PaddlePaddle to a single PyTorch-based container image. Two issues arose.

The NCCL version required by the Paddle wheel was not available on PyPI. paddlepaddle-gpu==3.4.0 requires NVIDIA's collective communication library NCCL as nvidia-nccl-cu12==2.28.3 , but the default index only had up to 2.27.3, causing installation to fail. This can be resolved by checking the index for CUDA 12.9 maintained by Paddle first and then appending PyPI.

pip install paddlepaddle-gpu==3.4.0 \
  -i https://www.paddlepaddle.org.cn/packages/stable/cu129/ \
  --extra-index-url https://pypi.org/simple

Putting them in the same environment broke torch. PyTorch and Paddle each bring along bundled nvidia-* CUDA libraries built for themselves. Adding Paddle to the same site-packages caused the NCCL versions from both sides to conflict, and torch broke at the import stage (neither which version won nor the exact error message remained in the logs). Since there is no need to use both frameworks together in a single process, splitting the environment was the answer.

python -m venv --without-pip /models/ocr/venv-paddle
P=/models/ocr/venv-paddle/bin/python
pip --python $P install paddlepaddle-gpu==3.4.0 -i …/cu129/ --extra-index-url https://pypi.org/simple
pip --python $P install "paddleocr[doc-parser]==3.7.0"

--without-pip Creating an empty virtual environment with and installing into it using the outer pip's --python saves time on reinstalling pip for each virtual environment. The Unlimited-OCR-2 side also did not touch the image's packages. The Transformers 4.52.1 tested in the model card and its matching tokenizers·huggingface-hub were placed in a separate folder using pip install --target and invoked only via PYTHONPATH . This approach leaves the torch provided by the image intact while adding only the versions required by the model code.

4. OpenCV, libGL, and the Broken cv2 Folder

I set up the Paddle environment and import cv2 and stopped right there.

ImportError: libGL.so.1: cannot open shared object file: No such file or directory

The OpenCV pulled in by PaddleOCR (opencv-contrib-python) includes screen output features, causing it to look for system libGL . That library does not exist in the server container image.

A common remedy is to switch to the headless edition (opencv-contrib-python-headless) with the screen features removed. Here I hit two more roadblocks.

  1. PaddleX's dependency check inspects the distribution name. PaddleX verifies whether the required packages are installed by checking the name of the installed distribution rather than whether it import . The headless edition provides the same cv2 but has a different name, so it concluded that "required packages are missing" and stopped.
  2. While swapping between the two editions, the cv2 folder became corrupted. All four OpenCV editions (opencv-python, -headless, -contrib, -contrib-headless) write files to the same site-packages/cv2/ folder. Deleting one also removes the files written by another edition, and installing a new edition on top of it leaves a half-empty folder. pip does not prevent this.

The order I settled on in the end is as follows: keep the original edition as is, and fill in the missing system libraries.

V=$(pip --python $P show opencv-contrib-python | sed -n 's/^Version: //p')
pip --python $P uninstall -y opencv-contrib-python opencv-contrib-python-headless \
  opencv-python opencv-python-headless
pip --python $P install --force-reinstall --no-deps opencv-contrib-python==$V
apt-get install -y libgl1 libglib2.0-0
$P -c "import cv2; print(cv2.__version__)"

Deleting all four editions and reinstalling just one edition via --force-reinstall --no-deps is the only way to clean up the folder. I used the exact version number chosen initially by PaddleOCR. If you want to avoid running apt-get every time you spin up a new container, it is better to pre-build an image containing libgl1 .

5. Two Long Measurement Runs Interrupted

The measurement run executing both PaddleOCR engines simultaneously was terminated along with the measurement container due to a dropped remote connection. The end of the log only showed a TLS error and "timed out waiting for condition." The 40-page batch of Unlimited-OCR-2 was also interrupted mid-output as the session restarted (which is why Part 1 only contains pages 10 and 20).

The only reason we were able to recover both times was because the measurement script saved its results every time a single page finished .

After that, I passed batches (general body text, faded copies, tables, long documents) as arguments to run them separately, maintaining separate logs for each batch. In an environment prone to connection drops, avoiding multi-hour single execution runs is the cheapest insurance. Additionally, the first page of every run was discarded as a warm-up. The first page of Unlimited-OCR-2 took 43.9 seconds, which was longer than the subsequent pages (around 30 seconds).

6. A Scoring Pitfall

As mentioned in Section 3 of Part 1, if you do not remove the block coordinates text[71, 96, 900, 160] from the Unlimited-OCR-2 output, the general body CER comes out to 16.3%, whereas removing them brings it to 0.80%. When the format of the engine output changes, the scorer silently produces incorrect numbers. Before attaching a new engine, you should visually inspect a few pages of raw output and print the normalized text side-by-side with the ground truth before trusting the scores.

7. Security Note: Code That Used eval() on Model Outputs

Unlimited-OCR-2 imports model code downloaded from the repository using trust_remote_code=True . While reading through that code, I found this line in the result saving path (modeling_unlimitedocr.py, revision 5632329a).

cor_list = eval(ref_text[2])
모델 출력이 파이썬 코드가 되는 자리 원격 코드에서 save_results=True 일 때만 타는 길 ① 문서 이미지 누구나 만들 수 있는 입력 ② 출력에 <|det|> 표식이 있으면 예: <|det|>text[71, 96, 900, 160]<|/det|> ③ 정규식이 [ … ] 를 잘라 괄호 안이 ] 만 아니면 무엇이든 통과 ④ eval("[ … ]") 숫자가 아니라 식이면 그 식을 실행 우리가 쓴 길 save_results=False 출력 글자만 받아 채점 업스트림에 낸 수정 ast.literal_eval 숫자·목록만 읽고 식은 거절
Figure: Where the model output becomes Python code

infer(..., save_results=True) ), when called with <|det|>이름[…]<|/det|> , the remote code uses a regular expression to search the model output for markers in the form of eval() , and passes them to ] in order to read the text inside the brackets as coordinates. The regular expression passes anything as long as it does not contain inside the brackets. And the model output istext that varies depending on the input document

. Since anyone can create a document, in principle, text within the document could pass through the model output and become a path executed as Python code.

For our measurements, we called save_results=False and only retrieved the output text. If you need to draw figures or crop and save bounding boxes, the coordinates can be read using ast.literal_eval . Since it accepts only literals like numbers, lists, and strings and rejects expressions via ValueError , the results are identical for normal coordinates.

Since this function originated from DeepSeek-OCR family example code, identical lines exist in multiple repositories. I submitted PRs with the same fix to three places. In each PR, I documented that the original and fixed functions were run on the same inputs to confirm that normal coordinate results remain identical, while inputs containing expressions are safely rejected into the existing error path instead of being executed. I also included a disclosure that the fix was prepared with AI assistance.

Repository PR Status (2026-10-30)
deepseek-ai/DeepSeek-OCR-2 (vLLM example script) #61 Open
openvinotoolkit/openvino_notebooks (deepseek-ocr, deepseek-vl2 notebooks) #3820 Open
basetenlabs/truss-examples (deepseek-ocr example) #594 Open

trust_remote_code=True We also list the practices we follow when using models as . We pin the revision (commit hash) so the received code does not change, read the code of that pinned version at least once, and turn off unused logging and visualization paths. The benchmark container using the GPU was launched without cluster API tokens (disabling automatic service account token mounting).

8. What to do from the start if we were to do it again

  1. Add HF_HUB_DISABLE_XET=1 and model cache volume to the benchmark container launch script by default.
  2. Separate environments for each framework. On top of the PyTorch image, Paddle uses a separate virtual environment, and dependencies for remote code models go to the --target folder.
  3. libgl1·libglib2.0-0 Use either image. For the OpenCV version, stick to the one chosen by PaddleOCR; if it needs to be changed, delete all four versions and start over.
  4. The benchmark script leaves an output file and a single line of JSON per page, runs in split batches, and discards the first page as a warmup.
  5. Before grading, read the raw output text and compare the normalized characters side-by-side with the ground truth.
  6. trust_remote_code For models, pin the revision, and first look for eval·exec·pickle·subprocess in that version's code.

A word from TT. The text read by an OCR model comes out just as the document instructed. Therefore, model outputs should also be treated like a note handed to you by a customer. Nobody executes what is written on a note blindly, right?

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment