docling-project / docling-project/docling
`docling-tools models download rapidocr` not actually being used by default `DocumentConverter`
- Dominant language
- Python
- Stars
- 66.4k
- Forks
- 4.8k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 84
Description
### Bug
With Python 3.11, `docling==2.57.0` and `rapidocr==3.4.2` the below GitHub Actions:
```yaml
- id: cache-models
uses: actions/cache@v4
with:
path: |
~/.cache/docling
...
- name: Pre-download Docling models
if: steps.cache-models.outputs.cache-hit != 'true'
run: docling-tools models download rapidocr
- run: pytest -n auto tests
```
When unit tests construct `DocumentConverter`, we hit `RuntimeError: storage has wrong byte size: expected %ld got %ld04` (full stack below) within `rapidocr.RapidOCR` construction.
Full stack trace
```none
___________________________ test_parse_pdf_to_pages ____________________________
[gw0] linux -- Python 3.11.14 /home/runner/work/paper-qa/paper-qa/.venv/bin/python
@pytest.mark.asyncio
async def test_parse_pdf_to_pages() -> None:
assert isinstance(parse_pdf_to_pages, PDFParserFn)
filepath = STUB_DATA_DIR / "pasa.pdf"
> parsed_text = parse_pdf_to_pages(filepath)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
packages/paper-qa-docling/tests/test_paperqa_docling.py:26:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
packages/paper-qa-docling/src/paperqa_docling/reader.py:77: in parse_pdf_to_pages
result = converter.convert(path)
^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/pydantic/_internal/_validate_call.py:39: in wrapper_function
return wrapper(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/pydantic/_internal/_validate_call.py:136: in __call__
res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/docling/document_converter.py:245: in convert
return next(all_res)
^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/docling/document_converter.py:268: in convert_all
for conv_res in conv_res_iter:
.venv/lib/python3.11/site-packages/docling/document_converter.py:340: in _convert
for item in map(
.venv/lib/python3.11/site-packages/docling/document_converter.py:387: in _process_document
conv_res = self._execute_pipeline(in_doc, raises_on_error=raises_on_error)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/docling/document_converter.py:408: in _execute_pipeline
pipeline = self._get_pipeline(in_doc.format)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/docling/document_converter.py:370: in _get_pipeline
self.initialized_pipelines[cache_key] = pipeline_class(
.venv/lib/python3.11/site-packages/docling/pipeline/standard_pdf_pipeline.py:49: in __init__
ocr_model = self.get_ocr_model(artifacts_path=self.artifacts_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/docling/pipeline/standard_pdf_pipeline.py:119: in get_ocr_model
return factory.create_instance(
.venv/lib/python3.11/site-packages/docling/models/factories/base_factory.py:57: in create_instance
return _cls(options=options, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/docling/models/auto_ocr_model.py:103: in __init__
self._engine = RapidOcrModel(
.venv/lib/python3.11/site-packages/docling/models/rapid_ocr_model.py:198: in __init__
self.reader = RapidOCR(
.venv/lib/python3.11/site-packages/rapidocr/main.py:43: in __init__
self._initialize(cfg)
.venv/lib/python3.11/site-packages/rapidocr/main.py:73: in _initialize
self.text_rec = TextRecognizer(cfg.Rec)
^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/rapidocr/ch_ppocr_rec/main.py:37: in __init__
self.session = get_engine(cfg.engine_type)(cfg)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/rapidocr/inference_engine/torch.py:25: in __init__
self.predictor = self._build_and_load_model(arch_config, model_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/rapidocr/inference_engine/torch.py:69: in _build_and_load_model
state_dict = torch.load(model_path, map_location="cpu", weights_only=False)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/torch/serialization.py:1554: in load
return _legacy_load(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
f = <_io.BufferedReader name='/home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_rec_infer.pth'>
map_location = 'cpu'
pickle_module =
pickle_load_args = {'encoding': 'utf-8'}
legacy_load = .legacy_load at 0x7f810c0c2f20>
persistent_load = .persistent_load at 0x7f810c0c3100>
f_should_read_directly = True, magic_number = 119547037146038801333356
protocol_version = 1001
_sys_info = {'little_endian': True, 'protocol_version': 1001, 'type_sizes': {'int': 4, 'long': 4, 'short': 2}}
unpickler = .UnpicklerWrapper object at 0x7f810e9df4d0>
def _legacy_load(f, map_location, pickle_module, **pickle_load_args):
deserialized_objects: dict[int, Any] = {}
restore_location = _get_restore_location(map_location)
class UnpicklerWrapper(pickle_module.Unpickler): # type: ignore[name-defined]
def find_class(self, mod_name, name):
if type(name) is str and "Storage" in name:
try:
return StorageType(name)
except KeyError:
pass
return super().find_class(mod_name, name)
def _check_container_source(container_type, source_file, original_source):
try:
current_source = "".join(get_source_lines_and_file(container_type)[0])
except Exception: # saving the source is optional, so we can ignore any errors
warnings.warn(
"Couldn't retrieve source code for container of "
"type " + container_type.__name__ + ". It won't be checked "
"for correctness upon loading."
)
return
if original_source != current_source:
if container_type.dump_patches:
file_name = container_type.__name__ + ".patch"
diff = difflib.unified_diff(
current_source.split("\n"),
original_source.split("\n"),
source_file,
source_file,
lineterm="",
)
lines = "\n".join(diff)
try:
with open(file_name, "a+") as f:
file_size = f.seek(0, 2)
f.seek(0)
if file_size == 0:
f.write(lines)
elif file_size != len(lines) or f.read() != lines:
raise OSError
msg = (
"Saved a reverse patch to " + file_name + ". "
"Run `patch -p0 < " + file_name + "` to revert your "
"changes."
)
except OSError:
msg = (
"Tried to save a patch, but couldn't create a "
"writable file " + file_name + ". Make sure it "
"doesn't exist and your working directory is "
"writable."
)
else:
msg = (
"you can retrieve the original source code by "
"accessing the object's source attribute or set "
"`torch.nn.Module.dump_patches = True` and use the "
"patch tool to revert the changes."
)
msg = f"source code of class '{torch.typename(container_type)}' has changed. {msg}"
warnings.warn(msg, SourceChangeWarning)
def legacy_load(f):
deserialized_objects: dict[int, Any] = {}
def persistent_load(saved_id):
if isinstance(saved_id, tuple):
# Ignore containers that don't have any sources saved
if all(saved_id[1:]):
_check_container_source(*saved_id)
return saved_id[0]
return deserialized_objects[int(saved_id)]
with (
closing(
tarfile.open(fileobj=f, mode="r:", format=tarfile.PAX_FORMAT)
) as tar,
mkdtemp() as tmpdir,
):
if pickle_module is _weights_only_unpickler:
raise RuntimeError(
"Cannot use ``weights_only=True`` with files saved in the "
"legacy .tar format. " + UNSAFE_MESSAGE
)
tar.extract("storages", path=tmpdir)
with open(os.path.join(tmpdir, "storages"), "rb", 0) as f:
num_storages = pickle_module.load(f, **pickle_load_args)
for _ in range(num_storages):
args = pickle_module.load(f, **pickle_load_args)
key, location, storage_type = args
dtype = storage_type._dtype
obj = cast(Storage, torch.UntypedStorage)._new_with_file(
f, torch._utils._element_size(dtype)
)
obj = restore_location(obj, location)
# TODO: Once we decide to break serialization FC, we can
# stop wrapping with TypedStorage
deserialized_objects[key] = torch.storage.TypedStorage(
wrap_storage=obj, dtype=dtype, _internal=True
)
storage_views = pickle_module.load(f, **pickle_load_args)
for target_cdata, root_cdata, offset, numel in storage_views:
root = deserialized_objects[root_cdata]
element_size = torch._utils._element_size(root.dtype)
offset_bytes = offset * element_size
# TODO: Once we decide to break serialization FC, we can
# stop wrapping with TypedStorage
deserialized_objects[target_cdata] = torch.storage.TypedStorage(
wrap_storage=root._untyped_storage[
offset_bytes : offset_bytes + numel * element_size
],
dtype=root.dtype,
_internal=True,
)
tar.extract("tensors", path=tmpdir)
with open(os.path.join(tmpdir, "tensors"), "rb", 0) as f:
num_tensors = pickle_module.load(f, **pickle_load_args)
for _ in range(num_tensors):
args = pickle_module.load(f, **pickle_load_args)
key, storage_id, _original_tensor_type = args
storage = deserialized_objects[storage_id]
(ndim,) = struct.unpack(" typed_storage._untyped_storage._set_from_file(
f,
offset,
f_should_read_directly,
torch._utils._element_size(typed_storage.dtype),
)
E RuntimeError: storage has wrong byte size: expected %ld got %ld04
.venv/lib/python3.11/site-packages/torch/serialization.py:1821: RuntimeError
----------------------------- Captured stderr call -----------------------------
[INFO] 2025-10-18 03:52:58,258 [RapidOCR] base.py:22: Using engine_name: torch
[INFO] 2025-10-18 03:52:58,262 [RapidOCR] download_file.py:68: Initiating download: https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/torch/PP-OCRv4/det/ch_PP-OCRv4_det_infer.pth
[INFO] 2025-10-18 03:53:00,346 [RapidOCR] download_file.py:82: Download size: 13.83MB
[INFO] 2025-10-18 03:53:01,104 [RapidOCR] download_file.py:95: Successfully saved to: /home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_det_infer.pth
[INFO] 2025-10-18 03:53:01,106 [RapidOCR] torch.py:54: Using /home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_det_infer.pth
[INFO] 2025-10-18 03:53:01,705 [RapidOCR] base.py:22: Using engine_name: torch
[INFO] 2025-10-18 03:53:01,706 [RapidOCR] download_file.py:68: Initiating download: https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/torch/PP-OCRv4/cls/ch_ptocr_mobile_v2.0_cls_infer.pth
[INFO] 2025-10-18 03:53:02,639 [RapidOCR] download_file.py:82: Download size: 0.56MB
[INFO] 2025-10-18 03:53:02,712 [RapidOCR] download_file.py:95: Successfully saved to: /home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_ptocr_mobile_v2.0_cls_infer.pth
[INFO] 2025-10-18 03:53:02,714 [RapidOCR] torch.py:54: Using /home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_ptocr_mobile_v2.0_cls_infer.pth
[INFO] 2025-10-18 03:53:02,807 [RapidOCR] base.py:22: Using engine_name: torch
[INFO] 2025-10-18 03:53:02,807 [RapidOCR] download_file.py:68: Initiating download: https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/torch/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer.pth
[INFO] 2025-10-18 03:53:04,032 [RapidOCR] download_file.py:82: Download size: 25.67MB
[INFO] 2025-10-18 03:53:05,536 [RapidOCR] download_file.py:95: Successfully saved to: /home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_rec_infer.pth
[INFO] 2025-10-18 03:53:05,538 [RapidOCR] torch.py:54: Using /home/runner/work/paper-qa/paper-qa/.venv/lib/python3.11/site-packages/rapidocr/models/ch_PP-OCRv4_rec_infer.pth
```
This failure reveals an issue:
1. `docling-tools models download rapidocr` downloads to Docling's standard cache location in `~/.cache/docling`
2. At runtime with `DocumentConverter`, the `rapidocr.RapidOCR` construction is not using `~/.cache/docling`. Instead a fresh model is downloaded to site packages' `rapidocr/models/`
From reading around, it seems one can customize the pipeline options [`artifacts_path`](https://github.com/docling-project/docling/blob/v2.57.0/docling/datamodel/pipeline_options.py#L280) or set the [`DOCLING_ARTIFACTS_PATH` env var](https://github.com/docling-project/docling/blob/v2.57.0/docling/datamodel/settings.py#L54-L56) to change the RapidOCR cache location.
Imo it's unexpected that Docling's default behavior here is to not use Docling's own cache folder.
Thus I am making this half bug report half request: have the `artifacts_path` be defaulted for synergy within Docling.
### Steps to reproduce
See general section of this issue.
### Docling version
```none
2025-10-20 10:36:48,374 - INFO - Loading plugin 'docling_defaults'
2025-10-20 10:36:48,382 - INFO - Registered ocr engines: ['auto', 'easyocr', 'ocrmac', 'rapidocr', 'tesserocr', 'tesseract']
Docling version: 2.57.0
Docling Core version: 2.49.0
Docling IBM Models version: 3.9.1
Docling Parse version: 4.5.1
```
### Python version
Python 3.11.14
Contributor guide
Assessment
This issue has not been assessed yet.