docling-glm-ocr 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ """A docling OCR plugin for GLM-OCR."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from docling_glm_ocr.model import GlmOcrRemoteModel
6
+ from docling_glm_ocr.options import GlmOcrRemoteOptions
7
+
8
+ __all__ = ["GlmOcrRemoteModel", "GlmOcrRemoteOptions"]
@@ -0,0 +1,159 @@
1
+ """GLM-OCR remote OCR engine for the docling standard pipeline.
2
+
3
+ Sends each page crop to a vLLM-hosted GLM-OCR model and returns the
4
+ recognised text as ``TextCell`` objects that docling merges with its
5
+ standard-pipeline output.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import io
12
+ import logging
13
+ from typing import TYPE_CHECKING
14
+
15
+ import httpx
16
+ from docling.models.base_ocr_model import BaseOcrModel
17
+ from docling.utils.profiling import TimeRecorder
18
+ from docling_core.types.doc import BoundingBox, CoordOrigin
19
+ from docling_core.types.doc.page import BoundingRectangle, TextCell
20
+
21
+ from docling_glm_ocr.options import GlmOcrRemoteOptions
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Iterable
25
+ from pathlib import Path
26
+
27
+ from docling.datamodel.accelerator_options import AcceleratorOptions
28
+ from docling.datamodel.base_models import Page
29
+ from docling.datamodel.document import ConversionResult
30
+ from docling.datamodel.pipeline_options import OcrOptions
31
+ from PIL import Image
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ def _pil_to_base64_uri(image: Image.Image, fmt: str = "PNG") -> str:
37
+ buf = io.BytesIO()
38
+ image.save(buf, format=fmt)
39
+ b64 = base64.b64encode(buf.getvalue()).decode()
40
+ return f"data:image/{fmt.lower()};base64,{b64}"
41
+
42
+
43
+ class GlmOcrRemoteModel(BaseOcrModel):
44
+ """OCR engine that delegates recognition to a remote GLM-OCR vLLM server."""
45
+
46
+ def __init__(
47
+ self,
48
+ *,
49
+ enabled: bool,
50
+ artifacts_path: Path | None,
51
+ options: GlmOcrRemoteOptions,
52
+ accelerator_options: AcceleratorOptions,
53
+ ) -> None:
54
+ super().__init__(
55
+ enabled=enabled,
56
+ artifacts_path=artifacts_path,
57
+ options=options,
58
+ accelerator_options=accelerator_options,
59
+ )
60
+ self.options: GlmOcrRemoteOptions
61
+ self.scale = 3 # 72 dpi → 216 dpi
62
+
63
+ if self.enabled:
64
+ self._client = httpx.Client(timeout=self.options.timeout)
65
+ logger.info(
66
+ "GlmOcrRemoteModel initialised: api_url=%s model=%s",
67
+ self.options.api_url,
68
+ self.options.model_name,
69
+ )
70
+
71
+ def _recognise_crop(self, image: Image.Image) -> str:
72
+ """Send a single cropped image to the remote GLM-OCR endpoint."""
73
+ data_uri = _pil_to_base64_uri(image)
74
+ payload = {
75
+ "model": self.options.model_name,
76
+ "max_tokens": self.options.max_tokens,
77
+ "messages": [
78
+ {
79
+ "role": "user",
80
+ "content": [
81
+ {"type": "text", "text": self.options.prompt},
82
+ {"type": "image_url", "image_url": {"url": data_uri}},
83
+ ],
84
+ }
85
+ ],
86
+ }
87
+ resp = self._client.post(self.options.api_url, json=payload)
88
+ resp.raise_for_status()
89
+ choices = resp.json().get("choices", [])
90
+ if not choices:
91
+ return ""
92
+ return choices[0].get("message", {}).get("content", "")
93
+
94
+ def __call__(
95
+ self,
96
+ conv_res: ConversionResult,
97
+ page_batch: Iterable[Page],
98
+ ) -> Iterable[Page]:
99
+ """Run OCR on each page crop and yield pages with recognised text cells."""
100
+ if not self.enabled:
101
+ yield from page_batch
102
+ return
103
+
104
+ for page in page_batch:
105
+ if page._backend is None or not page._backend.is_valid(): # noqa: SLF001
106
+ yield page
107
+ continue
108
+
109
+ with TimeRecorder(conv_res, "ocr"):
110
+ ocr_rects = self.get_ocr_rects(page)
111
+ all_ocr_cells: list[TextCell] = []
112
+
113
+ for cell_idx, ocr_rect in enumerate(ocr_rects):
114
+ if ocr_rect.area() == 0:
115
+ continue
116
+
117
+ high_res_image = page._backend.get_page_image( # noqa: SLF001
118
+ scale=self.scale,
119
+ cropbox=ocr_rect,
120
+ )
121
+
122
+ try:
123
+ text = self._recognise_crop(high_res_image)
124
+ except httpx.HTTPError:
125
+ logger.exception("GLM-OCR remote call failed for crop %d", cell_idx)
126
+ continue
127
+
128
+ if not text.strip():
129
+ continue
130
+
131
+ all_ocr_cells.append(
132
+ TextCell(
133
+ index=cell_idx,
134
+ text=text,
135
+ orig=text,
136
+ from_ocr=True,
137
+ confidence=1.0,
138
+ rect=BoundingRectangle.from_bounding_box(
139
+ BoundingBox.from_tuple(
140
+ coord=(
141
+ ocr_rect.l,
142
+ ocr_rect.t,
143
+ ocr_rect.r,
144
+ ocr_rect.b,
145
+ ),
146
+ origin=CoordOrigin.TOPLEFT,
147
+ )
148
+ ),
149
+ )
150
+ )
151
+
152
+ self.post_process_cells(all_ocr_cells, page)
153
+
154
+ yield page
155
+
156
+ @classmethod
157
+ def get_options_type(cls) -> type[OcrOptions]:
158
+ """Return the options class for this OCR engine."""
159
+ return GlmOcrRemoteOptions
@@ -0,0 +1,46 @@
1
+ """Configuration model for the GLM-OCR remote OCR engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import ClassVar, Literal
7
+
8
+ from docling.datamodel.pipeline_options import OcrOptions
9
+ from pydantic import ConfigDict, Field
10
+
11
+
12
+ class GlmOcrRemoteOptions(OcrOptions):
13
+ """Options for the remote GLM-OCR OCR engine.
14
+
15
+ The engine sends each page crop to a vLLM-hosted GLM-OCR model via its
16
+ OpenAI-compatible chat completion endpoint and returns the recognised text.
17
+
18
+ Attributes:
19
+ api_url: OpenAI-compatible chat completion URL of the vLLM server.
20
+ Falls back to the ``GLMOCR_REMOTE_OCR_API_URL`` env var, then to
21
+ a localhost default.
22
+ model_name: ``model`` parameter sent in the chat completion request.
23
+ prompt: Text prompt sent alongside each image crop.
24
+ Falls back to the ``GLMOCR_REMOTE_OCR_PROMPT`` env var.
25
+ timeout: HTTP request timeout in seconds per crop.
26
+ max_tokens: Maximum tokens for the chat completion response.
27
+ """
28
+
29
+ kind: ClassVar[Literal["glm-ocr-remote"]] = "glm-ocr-remote"
30
+
31
+ lang: list[str] = Field(default_factory=lambda: ["en"])
32
+ api_url: str = os.environ.get(
33
+ "GLMOCR_REMOTE_OCR_API_URL",
34
+ "http://localhost:8001/v1/chat/completions",
35
+ )
36
+ model_name: str = "zai-org/GLM-OCR"
37
+ prompt: str = os.environ.get(
38
+ "GLMOCR_REMOTE_OCR_PROMPT",
39
+ """Recognize the text in the image and output in Markdown format.
40
+ Preserve the original layout (headings/paragraphs/tables/formulas).
41
+ Do not fabricate content that does not exist in the image.""",
42
+ )
43
+ timeout: float = 120
44
+ max_tokens: int = 16384
45
+
46
+ model_config = ConfigDict(extra="forbid")
@@ -0,0 +1,8 @@
1
+ """Docling plugin entry point registering the GLM-OCR remote OCR engine."""
2
+
3
+ from docling_glm_ocr.model import GlmOcrRemoteModel
4
+
5
+
6
+ def ocr_engines() -> dict[str, list[type]]:
7
+ """Return the OCR engine classes provided by this plugin."""
8
+ return {"ocr_engines": [GlmOcrRemoteModel]}
File without changes
@@ -0,0 +1,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: docling-glm-ocr
3
+ Version: 0.1.0
4
+ Summary: A docling OCR plugin for GLM-OCR
5
+ Project-URL: Homepage, https://github.com/DCC-BS/docling-glm-ocr
6
+ Project-URL: Repository, https://github.com/DCC-BS/docling-glm-ocr
7
+ Project-URL: Issues, https://github.com/DCC-BS/docling-glm-ocr/issues
8
+ Project-URL: Changelog, https://github.com/DCC-BS/docling-glm-ocr/releases
9
+ Author-email: Yanick Schraner <yanick.schraner@bs.ch>, Tobias Bollinger <tobias.bollinger@bs.ch>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.13
19
+ Requires-Dist: docling>=2.73
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pillow>=10.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # docling-glm-ocr
25
+
26
+ A docling OCR plugin that delegates text recognition to a remote
27
+ [GLM-OCR](https://huggingface.co/zai-org/GLM-OCR) model served by vLLM.
28
+
29
+ ---
30
+
31
+ <p align="center">
32
+ <a href="https://github.com/DCC-BS/docling-glm-ocr">GitHub</a>
33
+ &nbsp;|&nbsp;
34
+ <a href="https://pypi.org/project/docling-glm-ocr/">PyPI</a>
35
+ </p>
36
+
37
+ ---
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/docling-glm-ocr.svg)](https://pypi.org/project/docling-glm-ocr/)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/docling-glm-ocr.svg)](https://pypi.org/project/docling-glm-ocr/)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/DCC-BS/docling-glm-ocr/blob/main/LICENSE)
42
+ [![CI](https://github.com/DCC-BS/docling-glm-ocr/actions/workflows/main.yml/badge.svg)](https://github.com/DCC-BS/docling-glm-ocr/actions/workflows/main.yml)
43
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
44
+ [![codecov](https://codecov.io/gh/DCC-BS/docling-glm-ocr/graph/badge.svg?token=LOPIP1UZOC)](https://codecov.io/gh/DCC-BS/docling-glm-ocr)
45
+
46
+ ## Overview
47
+
48
+ `docling-glm-ocr` is a [docling](https://github.com/DS4SD/docling) plugin that
49
+ replaces the built-in OCR stage with a call to a remote
50
+ [GLM-OCR](https://huggingface.co/zai-org/GLM-OCR) model hosted on a
51
+ [vLLM](https://github.com/vllm-project/vllm) server.
52
+
53
+ Each page crop is sent to the vLLM OpenAI-compatible chat completion endpoint
54
+ as a base64-encoded image. The model returns Markdown-formatted text which
55
+ docling merges back into the document structure.
56
+
57
+ The plugin registers itself under the `"glm-ocr-remote"` OCR engine key so it
58
+ can be selected per-request through docling or docling-serve without changing
59
+ application code.
60
+
61
+ ## Requirements
62
+
63
+ - Python 3.13+
64
+ - A running vLLM server hosting `zai-org/GLM-OCR` (or any compatible model)
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ # with uv (recommended)
70
+ uv add docling-glm-ocr
71
+
72
+ # with pip
73
+ pip install docling-glm-ocr
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ ### Python SDK
79
+
80
+ ```python
81
+ from docling.datamodel.base_models import InputFormat
82
+ from docling.datamodel.pipeline_options import PdfPipelineOptions
83
+ from docling.document_converter import DocumentConverter, PdfFormatOption
84
+
85
+ from docling_glm_ocr import GlmOcrRemoteOptions
86
+
87
+ pipeline_options = PdfPipelineOptions(
88
+ allow_external_plugins=True,
89
+ ocr_options=GlmOcrRemoteOptions(
90
+ api_url="http://localhost:8001/v1/chat/completions",
91
+ model_name="zai-org/GLM-OCR",
92
+ ),
93
+ )
94
+
95
+ converter = DocumentConverter(
96
+ format_options={
97
+ InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)
98
+ }
99
+ )
100
+ result = converter.convert("document.pdf")
101
+ print(result.document.export_to_markdown())
102
+ ```
103
+
104
+ ### docling-serve
105
+
106
+ Select the engine per-request via the standard API:
107
+
108
+ ```bash
109
+ curl -X POST http://localhost:5001/v1/convert/source \
110
+ -H 'Content-Type: application/json' \
111
+ -d '{
112
+ "options": {
113
+ "ocr_engine": "glm-ocr-remote"
114
+ },
115
+ "sources": [{"kind": "http", "url": "https://arxiv.org/pdf/2501.17887"}]
116
+ }'
117
+ ```
118
+
119
+ The server must have `DOCLING_SERVE_ALLOW_EXTERNAL_PLUGINS=true` set so the
120
+ plugin is loaded automatically.
121
+
122
+ ## Configuration
123
+
124
+ ### Environment variables
125
+
126
+ | Variable | Description | Default |
127
+ |---|---|---|
128
+ | `GLMOCR_REMOTE_OCR_API_URL` | vLLM chat completion URL | `http://localhost:8001/v1/chat/completions` |
129
+ | `GLMOCR_REMOTE_OCR_PROMPT` | Text prompt sent with each image crop | see below |
130
+
131
+ ### `GlmOcrRemoteOptions`
132
+
133
+ All options can be set programmatically via `GlmOcrRemoteOptions`:
134
+
135
+ | Option | Type | Description | Default |
136
+ |---|---|---|---|
137
+ | `api_url` | `str` | OpenAI-compatible chat completion URL | `GLMOCR_REMOTE_OCR_API_URL` env or `http://localhost:8001/v1/chat/completions` |
138
+ | `model_name` | `str` | Model name sent to vLLM | `zai-org/GLM-OCR` |
139
+ | `prompt` | `str` | Text prompt for each image crop | `GLMOCR_REMOTE_OCR_PROMPT` env or default prompt |
140
+ | `timeout` | `float` | HTTP timeout per crop (seconds) | `120` |
141
+ | `max_tokens` | `int` | Max tokens per completion | `16384` |
142
+ | `lang` | `list[str]` | Language hint (passed to docling) | `["en"]` |
143
+
144
+ Default prompt:
145
+
146
+ ```
147
+ Recognize the text in the image and output in Markdown format.
148
+ Preserve the original layout (headings/paragraphs/tables/formulas).
149
+ Do not fabricate content that does not exist in the image.
150
+ ```
151
+
152
+ ## Architecture
153
+
154
+ ```mermaid
155
+ flowchart LR
156
+ subgraph docling
157
+ Pipeline --> GlmOcrRemoteModel
158
+ end
159
+
160
+ subgraph vLLM
161
+ GLMOCR["zai-org/GLM-OCR"]
162
+ end
163
+
164
+ GlmOcrRemoteModel -- "POST /v1/chat/completions\n(base64 image)" --> GLMOCR
165
+ GLMOCR -- "Markdown text" --> GlmOcrRemoteModel
166
+ ```
167
+
168
+ For each page the model:
169
+ 1. Collects OCR regions from the docling layout analysis
170
+ 2. Renders each region at 3× scale (216 dpi) using the page backend
171
+ 3. Encodes the crop as a base64 PNG data URI
172
+ 4. POSTs a chat completion request to the vLLM endpoint
173
+ 5. Returns the recognised text as `TextCell` objects for docling to merge
174
+
175
+ ## Starting a GLM-OCR vLLM server
176
+
177
+ ```bash
178
+ docker run -d \
179
+ --rm --name ocr-glm \
180
+ --gpus device=0 \
181
+ --ipc=host \
182
+ -p 8001:8000 \
183
+ -v "${HOME}/.cache/huggingface:/root/.cache/huggingface" \
184
+ -e "HF_TOKEN=${HF_TOKEN}" \
185
+ --entrypoint /bin/bash \
186
+ vllm/vllm-openai:latest \
187
+ -c "uv pip install --system --upgrade transformers && \
188
+ exec vllm serve zai-org/GLM-OCR \
189
+ --served-model-name zai-org/GLM-OCR \
190
+ --port 8000 \
191
+ --trust-remote-code"
192
+ ```
193
+
194
+ The plugin will connect to `http://localhost:8001/v1/chat/completions` by default.
195
+
196
+ ## Development
197
+
198
+ ### Setup
199
+
200
+ ```bash
201
+ git clone https://github.com/DCC-BS/docling-glm-ocr.git
202
+ cd docling-glm-ocr
203
+ make install
204
+ ```
205
+
206
+ ### Available commands
207
+
208
+ ```
209
+ make install Install dependencies and pre-commit hooks
210
+ make check Run all quality checks (ruff lint, format, ty type check)
211
+ make test Run tests with coverage report
212
+ make build Build distribution packages
213
+ make publish Publish to PyPI
214
+ ```
215
+
216
+ ### Running tests
217
+
218
+ ```bash
219
+ make test
220
+ ```
221
+
222
+ Tests are in `tests/` and use [pytest](https://pytest.org).
223
+ Coverage reports are generated at `coverage.xml` and printed to the terminal.
224
+
225
+ #### End-to-end tests
226
+
227
+ The e2e tests hit a real vLLM server and are **skipped by default**.
228
+ To run them, set the server URL and use the `e2e` marker:
229
+
230
+ ```bash
231
+ GLMOCR_REMOTE_OCR_API_URL=http://localhost:8001/v1/chat/completions pytest -m e2e
232
+ ```
233
+
234
+ ### Code quality
235
+
236
+ This project uses:
237
+
238
+ - **[ruff](https://github.com/astral-sh/ruff)** – linting and formatting
239
+ - **[ty](https://github.com/astral-sh/ty)** – type checking
240
+ - **[pre-commit](https://pre-commit.com/)** – pre-commit hooks
241
+
242
+ Run all checks:
243
+
244
+ ```bash
245
+ make check
246
+ ```
247
+
248
+ ### Releasing
249
+
250
+ Releases are published to PyPI automatically.
251
+ Update the version in `pyproject.toml`, then trigger the **Publish** workflow from GitHub Actions:
252
+
253
+ ```
254
+ GitHub → Actions → Publish to PyPI → Run workflow
255
+ ```
256
+
257
+ The workflow tags the commit, builds the package, and publishes to PyPI via trusted publishing.
258
+
259
+ ## License
260
+
261
+ [MIT](LICENSE) © Data Competence Center Basel-Stadt
@@ -0,0 +1,10 @@
1
+ docling_glm_ocr/__init__.py,sha256=vu6BnFv3j7UPZX4sPxxNHyYZhh_eZggSzONFNvRAcLM,228
2
+ docling_glm_ocr/model.py,sha256=XlnqbPMOljLKxn9UR6qe5oaYqaQW-r1MUsALcbuyhqg,5470
3
+ docling_glm_ocr/options.py,sha256=wDIQWfUeck3n6t8uILp-e0UMh5IjzFYDTHhxjeHkG-g,1724
4
+ docling_glm_ocr/plugin.py,sha256=QsILIweZ1mr-n3SjdNbKi98sjJFcDbCSWfNAhhW77Ww,288
5
+ docling_glm_ocr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ docling_glm_ocr-0.1.0.dist-info/METADATA,sha256=4rdsiNTZMY8gq68Xi35GR-tann-nPN01b9-Wi7KqR9c,8025
7
+ docling_glm_ocr-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ docling_glm_ocr-0.1.0.dist-info/entry_points.txt,sha256=jLx9L5LwAeV8eolH8xufrZuKbnnnmUIWUxMMdxeiVFM,51
9
+ docling_glm_ocr-0.1.0.dist-info/licenses/LICENSE,sha256=APpdFXym3_5C78pFAfnlLvmP7umPBWYpQ2MKidnoRuw,1091
10
+ docling_glm_ocr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [docling]
2
+ docling_glm_ocr = docling_glm_ocr.plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Data Competence Center Basel-Stadt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.