Skip to content

About

Patched docling-serve image with DCC docling plugins for GLM-OCR and PP-DocLayout-V3 installed and an updated gradio ui

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

Docling-Serve Plugins: PP-DocLayout-V3 + GLM-OCR

CI

A patched docling-serve Docker image that bundles two community plugins:

Available images

Image Based on Architectures Notes
ghcr.io/dcc-bs/dcc-docling-serve docling-serve linux/amd64 Base image, packages from PyPI (torch with CUDA 13)
ghcr.io/dcc-bs/dcc-docling-serve-cpu docling-serve-cpu linux/amd64, linux/arm64 CPU-only, torch from PyTorch CPU index
ghcr.io/dcc-bs/dcc-docling-serve-cu128 docling-serve-cu128 linux/amd64 CUDA 12.8, torch from cu128 index
ghcr.io/dcc-bs/dcc-docling-serve-cu130 docling-serve-cu130 linux/amd64 CUDA 13.0, torch from cu130 index

Each image is tagged with the docling-serve version it is built on (e.g. v1.37.0) and :latest: dcc-docling-serve:v1.37.0 is docling-serve v1.37.0 with our plugins and patches.

Plugins

Plugin PyPI Purpose
docling-glm-ocr pip install docling-glm-ocr Remote OCR via a vLLM-hosted GLM-OCR model
docling-pp-doc-layout pip install docling-pp-doc-layout Local layout detection via PP-DocLayout-V3
docling-pp-ocrv6 pip install docling-pp-ocrv6 Local OCR via PP-OCRv6 (medium), with a box per word

The plugins are selectable per-request through the standard docling-serve API:

  • Layout -- layout_preset: "ppdoclayout-v3" (preset defined in compose.yaml) or layout_custom_config: { "kind": "ppdoclayout-v3" }
  • OCR -- ocr_preset: "glm-ocr-remote" or ocr_preset: "pp-ocrv6"

The image pins the plugin versions (DOCLING_GLM_OCR_VERSION, DOCLING_PP_DOC_LAYOUT_VERSION and DOCLING_PP_OCRV6_VERSION in plugins/Dockerfile.docling-serve) and bakes the PP-DocLayout-V3 and PP-OCRv6 models into it at pinned Hugging Face revisions (PP_DOC_LAYOUT_MODEL_REVISION, PP_OCRV6_DET_REVISION, PP_OCRV6_REC_REVISION), so no pod downloads them and a new upload to a model repo changes nothing until the revision here is moved. Renovate opens a PR when a new plugin version is released; merge it, then run the image build again for the current version. Unpinned, the build's layer cache would keep shipping the old plugins.

The docling-serve web UI at /ui is upstream's, unchanged. It lists both plugins among its OCR and layout choices.

Architecture

flowchart TD
    subgraph doclingServe ["dcc-docling-serve"]
        LP["PP-DocLayout-V3 plugin"]
        OP["GLM-OCR plugin"]
    end

    subgraph vllmServer [vLLM Server]
        GLMOCR["zai-org/GLM-OCR"]
    end

    OP -- "remote OCR" --> GLMOCR
Loading

Quickstart

1) Prerequisites

  • Docker with GPU support (NVIDIA)
  • A HuggingFace token with access to the GLM-OCR model

2) Configure

Copy .env.example to .env and set the required variables:

cp .env.example .env
# edit .env — at minimum set HF_TOKEN
Variable Description Default
HF_TOKEN HuggingFace token for downloading GLM-OCR (required) —
HF_CACHE_DIR Host directory for the HF model cache .hf-cache
VLLM_HOST_PORT Host port for the vLLM server 8001
DOCLING_HOST_PORT Host port for docling-serve 5001
DOCLING_SERVE_TAG Upstream docling-serve image tag latest
DOCLING_SERVE_LOG_LEVEL Log level for docling-serve INFO

3) Start the stack

make docker-up

This starts two services:

Service Purpose
vllm-glm-ocr vLLM server hosting zai-org/GLM-OCR (GPU 1)
docling-serve Docling API + web UI with both plugins (GPU 0)

The web UI is available at http://localhost:5001/ui.

4) Convert a document

Remote GLM-OCR OCR with PP-DocLayout-V3 layout:

curl -X POST http://localhost:5001/v1/convert/source \
  -H 'Content-Type: application/json' \
  -d '{
    "options": {
      "ocr_engine": "glm-ocr-remote",
      "layout_custom_config": { "kind": "ppdoclayout-v3" }
    },
    "sources": [{"kind": "http", "url": "https://arxiv.org/pdf/2501.17887"}]
  }'

Docker Compose

The compose.yaml references the pre-built image from GHCR:

ghcr.io/dcc-bs/dcc-docling-serve:latest
Service Image
vllm-glm-ocr vllm/vllm-openai:v0.30.0
docling-serve ghcr.io/dcc-bs/dcc-docling-serve:latest

Environment variables are documented in .env.example.

make docker-up    # start all services
make docker-down  # stop all services

Building the image locally

To build the patched docling-serve image from source:

make docker-build

This runs docker build against plugins/Dockerfile.docling-serve (default upstream image, latest tag) and tags the result as $DOCLING_IMAGE (default ghcr.io/dcc-bs/dcc-docling-serve:latest).

Running the locally built image

To test the local image without pushing it to GHCR, run docling-serve directly with docker run:

docker run --add-host=host.docker.internal:host-gateway --rm \
  --gpus device=0 \
  -p 5001:5001 \
  -e DOCLING_SERVE_ENABLE_UI=true \
  -e DOCLING_SERVE_ENABLE_REMOTE_SERVICES=true \
  -e DOCLING_SERVE_ALLOW_EXTERNAL_PLUGINS=true \
  -e GLMOCR_REMOTE_OCR_API_URL=http://host.docker.internal:8001/v1/chat/completions \
  ghcr.io/dcc-bs/dcc-docling-serve:latest

The web UI is then available at http://localhost:5001/ui.

Because make docker-build tags the image with the name compose.yaml uses, the normal compose flow picks up the local build:

make docker-build
make docker-up

Plugin configuration

All plugins are fully configurable via environment variables, making them suitable for zero-code deployment in Docker / Compose environments. Explicit GlmOcrRemoteOptions and PPDocLayoutV3Options constructor arguments always take precedence when using the Python SDK directly.

GLM-OCR remote OCR — environment variables

Variable Description Default
GLMOCR_REMOTE_OCR_API_URL vLLM chat completion URL http://localhost:8001/v1/chat/completions
GLMOCR_REMOTE_OCR_MODEL_NAME Model name sent to vLLM zai-org/GLM-OCR
GLMOCR_REMOTE_OCR_PROMPT Text prompt sent with each image crop built-in default
GLMOCR_REMOTE_OCR_TIMEOUT HTTP timeout per crop (seconds) 120
GLMOCR_REMOTE_OCR_MAX_TOKENS Max tokens per completion 16384
GLMOCR_REMOTE_OCR_SCALE Image crop rendering scale 3.0
GLMOCR_REMOTE_OCR_MAX_IMAGE_PIXELS Pixel budget per crop 4500000
GLMOCR_REMOTE_OCR_MAX_CONCURRENT_REQUESTS Max concurrent API requests 10
GLMOCR_REMOTE_OCR_MAX_RETRIES Max retry attempts for HTTP errors 3
GLMOCR_REMOTE_OCR_RETRY_BACKOFF_FACTOR Exponential backoff factor for retries 2.0
GLMOCR_REMOTE_OCR_LANG Comma-separated language hint(s) en
GLMOCR_REMOTE_OCR_API_KEY Bearer token for Authorization header unset (no header sent)

PP-DocLayout-V3 layout — environment variables

Variable Description Default
PP_DOC_LAYOUT_MODEL_NAME HuggingFace model repo ID or local folder our image: the baked-in copy (see below); plugin: PaddlePaddle/PP-DocLayoutV3_safetensors
PP_DOC_LAYOUT_CONFIDENCE_THRESHOLD Minimum detection confidence (0.0–1.0) 0.3
PP_DOC_LAYOUT_BATCH_SIZE Batch size for layout inference 8
PP_DOC_LAYOUT_CREATE_ORPHAN_CLUSTERS Create clusters for orphaned elements (true/false) true
PP_DOC_LAYOUT_KEEP_EMPTY_CLUSTERS Retain empty clusters in results (true/false) false
PP_DOC_LAYOUT_SKIP_CELL_ASSIGNMENT Skip table-cell assignment (true/false) false
PP_DOC_LAYOUT_LIST_DETECTION List-item detection: rules, heron or off (see Lists with PP-DocLayout-V3) rules

Boolean variables accept true, 1, yes (case-insensitive) as truthy; anything else is false.

Our image contains the PP-DocLayout-V3 weights (128 MB, pinned by PP_DOC_LAYOUT_MODEL_REVISION in plugins/Dockerfile.docling-serve) and points PP_DOC_LAYOUT_MODEL_NAME at them. Without them the plugin fetched the model from huggingface.co every time a pipeline was built, and downloaded it on every container start; now the worker runs without internet access. To update the weights, set the new commit of PaddlePaddle/PP-DocLayoutV3_safetensors as PP_DOC_LAYOUT_MODEL_REVISION.

The threshold defaults to 0.3 since plugin 0.2.5 (it was 0.5). docling only OCRs inside layout regions, and PP-DocLayout-V3 (trained on document pages) detects handwriting on phone photos with only 0.3–0.4 confidence. At 0.5 most of the handwritten list on the test photo got no region and was never read. At 0.3 over all 28 test documents: photo handwriting 11 % → 44 % with RapidOCR and 22 % → 89 % with GLM-OCR, headings on the synthetic documents 0.85 → 0.96, born-digital text unchanged. The catch: GLM-OCR also gets low-confidence regions on text-free photos and can invent text there (on one photo page it repeated "Wiesbaden" 483 times); RapidOCR does not. GLM-OCR 0.5.2 discards such repetition loops, but a short invented line can still get through. See the engine dashboard for the full comparison.

SDK option reference

All environment variables above correspond to fields on GlmOcrRemoteOptions and PPDocLayoutV3Options. See the individual plugin READMEs for the full option reference:

RapidOCR on the GPU

The upstream CUDA images ship the CPU-only onnxruntime, so RapidOCR (the default OCR engine, onnxruntime backend) runs on the CPU even on a GPU and dominates the processing time. plugins/install_onnxruntime_gpu.sh fixes this at build time for every image whose torch has CUDA support. CPU images are left unchanged.

  • It replaces onnxruntime with the matching onnxruntime-gpu: PyPI for CUDA 13 (same version as upstream), the onnxruntime CUDA 12 feed for CUDA 12.
  • It registers the CUDA/cuDNN libraries of the nvidia-* wheels with ldconfig, so the CUDA provider loads regardless of whether torch was imported first.
  • The build fails if CUDAExecutionProvider is missing or its libraries do not resolve.

No request option is needed. docling already enables CUDA for RapidOCR when the accelerator device is CUDA, which it is by default when a GPU is visible.

Health-probe log muting

Kubernetes/Compose liveness probes hitting GET /health produce two INFO records per probe (docling_serve.app "Health check requested" and the matching uvicorn.access line). The image ships plugins/mute_health_logs.py, installed into site-packages as dcc_mute_health_logs.py plus a .pth file that imports it at interpreter startup. It attaches logging.Filters to those two loggers — no upstream docling-serve file is modified, so it survives upstream updates. The filters sit on the Logger objects, not on handlers, so docling-serve's own setup_logging() (which only swaps handlers/levels) does not undo them.

Variable Description Default
DCC_MUTE_HEALTH_LOGS Set to 0/false/no to disable muting 1
DCC_MUTE_HEALTH_PATHS Comma-separated request paths to mute in the access log /health

Convert API: the words of each page

DoclingDocument carries regions and their text, not words. Whoever needs to know where a word stands — to highlight a hit, to anchor a citation, to mark a name for redaction — has to parse the file a second time on their own side, and run their own OCR for the pages that have no text layer. docling has measured all of it already and throws it away after assembly.

The image ships plugins/word_boxes.py, installed into site-packages as dcc_word_boxes.py plus a .pth file, like the health-log muting. It adds one request option and one field to the answer:

include_word_boxes request option, boolean, defaults to false
word_boxes answer field, beside md_content and the other contents

word_boxes holds, per page number, docling's own SegmentedPdfPage: word_cells and textline_cells, each cell with its text, the rectangle it was read in (four corners, so text at an angle keeps its angle) and from_ocr. char_cells and the page picture are dropped — they are large, and a word's text and box is what was asked for.

curl -s -X POST localhost:5001/v1/convert/file \
  -F files=@document.pdf -F to_formats=json -F pdf_backend=dlparse_v4 \
  -F include_word_boxes=true |
  python3 -c "import sys, json; pages = json.load(sys.stdin)['document']['word_boxes']; \
    print({n: len(p['word_cells']) for n, p in pages.items()})"

Only a docling-parse backend measures a box per word: with pdf_backend=pypdfium2 the answer carries the words OCR read and none of the ones the text layer draws.

What it changes. The option sets generate_parsed_pages on the pipeline, so the cells survive assembly; ExportableDocument carries them off the conversion result and the export writes them into the answer. Nothing else moves: the option defaults to false, no existing field changes, and a request that does not ask for it is answered exactly as before — the field is then null, like the other contents that were not requested.

With PP-OCRv6 (ocr_preset=pp-ocrv6) the option also turns on the engine's whole_page and return_word_box: every page is read whole, so a line is not cut at a layout box, and each word OCR reads gets a box of its own. A word the text layer draws is not doubled; over invisible text (a scanner's layer) the read words stay. Both options cost time and stay off for requests that do not ask for word boxes. docling-serve keeps one converter per set of options, so the two kinds of request do not share one; with more than two kinds in use, raise DOCLING_SERVE_OPTIONS_CACHE_SIZE (default 2) to avoid reloading models.

Adding a field to a pydantic model is not enough on its own: a model that holds another one keeps a copy of its schema from when it was first built, so the models of those two modules are rebuilt as well. Without that the field validates but is dropped again on the way out.

Variable Description Default
DCC_WORD_BOXES Set to 0/false/no to leave the API as upstream has it 1

If a wrapped docling function is missing (renamed upstream), the patch logs a warning and does nothing; conversions keep working, without the field.

Python SDK usage

The plugins can also be used directly with the docling Python SDK (without docling-serve). See examples/convert_with_plugins.py:

from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption

from docling_glm_ocr import GlmOcrRemoteOptions
from docling_pp_doc_layout.options import PPDocLayoutV3Options

pipeline_options = PdfPipelineOptions(
    allow_external_plugins=True,
    ocr_options=GlmOcrRemoteOptions(api_url="..."),
    layout_options=PPDocLayoutV3Options(),
)

converter = DocumentConverter(format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)})
result = converter.convert("https://arxiv.org/pdf/2501.17887")
print(result.document.export_to_markdown())

vLLM GLM-OCR container

Standalone command to run the GLM-OCR vLLM server:

docker run -d \
  --rm --name ocr-glm \
  --gpus device=1 \
  --ipc=host \
  -p 8001:8000 \
  -v "${HOME}/.cache/huggingface:/root/.cache/huggingface" \
  -e "HF_TOKEN=${HF_TOKEN:-}" \
  -e "LD_LIBRARY_PATH=/lib/x86_64-linux-gnu" \
  ghcr.io/dcc-bs/vllm:v0.16.0-cu130 \
  zai-org/GLM-OCR \
  --served-model-name zai-org/GLM-OCR \
  --port 8000 \
  --trust-remote-code \
  --max-num-batched-tokens 8192

Required: --max-num-batched-tokens 8192

Without this flag, vLLM will reject any high-resolution image with HTTP 400.

In vLLM 0.16.0+ (v1 engine), the encoder cache size is derived from max_num_batched_tokens (default 2048 when chunked prefill is enabled):

encoder_cache_size = max(max_num_batched_tokens, model_max_tokens_per_image)
                   = max(2048, 4800)  ←  4800 is GLM-OCR's model floor
                   = 4800 tokens      ←  too small for real documents

The Glm46VImageProcessor encodes images at approximately 784 pixels per token (patch_size=14 × merge_size=2, squared). A typical A4 page rendered at scale 3× (1785 × 2526 px) produces 5760 tokens; a phone-photo crop at scale 3× can reach 6120 tokens — both exceed the default 4800-token cache and are rejected.

Setting --max-num-batched-tokens 8192 raises the encoder cache to max(8192, 4800) = 8192 tokens, which covers all real-world inputs with comfortable headroom.

Note: --limit-mm-per-prompt does not control the encoder cache size in vLLM 0.16.0. That flag only limits the count of images per request.

Testing

Setup (dev)

make install

Format and lint

make check

Runs ruff format (auto-format) and ruff check --fix (auto-fix lint errors) locally. The CI workflow runs these as read-only checks.

Unit tests

make test

Smoke test (local SDK)

Tests the plugins directly via the Python SDK, without a running docling-serve instance. Requires the plugin repos checked out as siblings of this repo:

./smoke_test.sh
# or with a custom vLLM URL:
GLMOCR_REMOTE_OCR_API_URL=http://host:8001/v1/chat/completions ./smoke_test.sh

End-to-end tests

The e2e tests require a running stack (docling-serve + vLLM). They use the images in data/ to validate conversion through both plugins.

export DOCLING_SERVE_URL=http://localhost:5001
make test

Tests are skipped automatically when DOCLING_SERVE_URL is not set.

Processing-time benchmark

benchmarks/e2e_timing.py compares end-to-end conversion time of the official and our docling-serve images (CU130 and CPU) and plain docling installed from PyPI. The variants run one after another. Each one gets a cold conversion to load its models, then converts every document --repeats times with the options in OPTIONS. Results (raw JSONL plus summary.md with per-stage timings) go to benchmarks/results/.

uv run --script benchmarks/e2e_timing.py --docs ../test-docs
uv run --script benchmarks/e2e_timing.py --docs ../test-docs --variants official-cu130 custom-cu130 --repeats 1

Converter comparison

benchmarks/tool_comparison/ compares docling (upstream, with GPU OCR, and with GPU OCR plus the former shape patch, which docling 2.136 made obsolete) with marker, docTR, markitdown and Jina Reader on the test documents plus four synthetic PDFs with exact ground truth (charts, tables and forms, a scanned page, a designed page). It measures conversion time per document and page, scores the markdown against the ground truth and the PDF text layer, and builds an HTML dashboard with side-by-side diffs next to the original page.

benchmarks/tool_comparison/run_all.sh          # needs Docker, a GPU and jina_key in ../.env
python3 -m http.server -d benchmarks/tool_comparison/results/dashboard 8765

Note that the Jina runner uploads every document to r.jina.ai.

build_dashboard.py reads what to compare from a JSON config: tools and labels, results folder, documents, assessment file and page texts. With no --config it uses converters.config.json, which builds the dashboard above.

OCR engine × layout model

engines.config.json builds a second dashboard with the same page: RapidOCR and GLM-OCR, each with docling's default layout model and with PP-DocLayout-V3, all in the published dcc-docling-serve-cu130:v1.35.0 image (docling-glm-ocr 0.5.2, docling-pp-doc-layout 0.2.5) with otherwise identical options. It also compares these runs with the earlier ones on docling-glm-ocr 0.5.1 and docling-pp-doc-layout 0.2.4. On top of the converter views it shows:

  • an engine × layout grid for each key metric (ground-truth F1, scanned text, text in images, tables, headings, text-layer recall and precision, reference recall, time)
  • a "Previous vs published" tab: pages with text that came out empty, and the documents where the two plugin versions differ (tab title and column names come from pairs_tab and each pair's title and names)
  • a "Words not in reference" tab: output words that are missing from the ground truth or the reference transcription, split into misreadings and words with no close match (useful for GLM-OCR, which is a VLM)

The handwritten note (data/ocr.png) and the phone photo in the test documents have no text layer. They are scored against the transcriptions in benchmarks/tool_comparison/references.json. Put the pros and cons into engines.assessment.json (same format as assessment.json).

Results with the published image, all 28 documents (RTX 4090, vLLM 0.29 for GLM-OCR):

Configuration Time Synthetic F1 Headings Text-layer recall / precision Handwritten note Photo handwriting
RapidOCR + default layout 167 s 0.96 1.00 0.75 / 0.92 31 % 30 %
RapidOCR + PP-DocLayout-V3 199 s 0.96 0.96 0.83 / 0.92 27 % 44 %
GLM-OCR + default layout 190 s 0.94 0.92 0.75 / 0.85 97 % 85 %
GLM-OCR + PP-DocLayout-V3 273 s 0.94 0.96 0.84 / 0.90 96 % 89 %

With docling-glm-ocr 0.5.1 the two GLM-OCR runs took 2'745 s and 2'701 s: the plugin OCR'd every born-digital page as a full page. The text-free photos in the Legislaturplan now come out empty, except for one invented English description of the city photo with PP-DocLayout-V3.

Each configuration is a run_docling.py run whose --tool matches an id in the config (release-rapidocr, release-rapidocr-pp, release-glm, release-glm-pp, and the earlier fixed-rapidocr-pp-t03, fixed-glm, fixed-glm-pp-t03), with --results pointing at one shared folder. Then build and serve:

cd benchmarks/tool_comparison
uv run --script build_dashboard.py --config engines.config.json --results <engine results>
python3 -m http.server -d <engine results>/dashboard 8766

Configurations or documents without results are left out; grid cells show "not run".

Lists with PP-DocLayout-V3

PP-DocLayout-V3 has no list-item class, so before plugin 0.2.6 docling produced no lists with it: bullet points came out as paragraphs starting with "·" (367 list items against Heron's 2'422 on the test documents). The plugin now detects list items after OCR from list markers, enumerator sequences and bullet marks OCR did not read (PP_DOC_LAYOUT_LIST_DETECTION=rules, the default), optionally with docling's Heron model as a second opinion (heron). Details in the plugin README.

benchmarks/list_eval/ holds the benchmark: make_lists.py generates list pages with exact ground truth (born-digital and scanned, with traps such as numbered headings, dates and amounts), run_all.sh <image> converts them and the test documents with Heron and with PP-DocLayout-V3 in each mode, evaluate.py scores them. Results with plugin 0.2.6:

Configuration Born-digital R / P Scanned R / P Traps as list items Bullet lines found Time
Heron (docling default) 0.67 / 0.96 0.97 / 0.95 2 245 / 251 136 s
PP-DocLayout-V3, off 0 0 0 0 / 251 145 s
PP-DocLayout-V3, rules 1.00 / 1.00 0.86 / 1.00 0 231 / 251 145 s
PP-DocLayout-V3, heron 1.00 / 1.00 0.94 / 0.94 1 231 / 251 153 s

13 of the 20 bullet lines PP-DocLayout-V3 misses are the brochure's commission list, which it lays out as a picture (the text is in the JSON output, inside the picture). List detection runs inside docling's layout post-processing, which has no plugin interface yet; add a check to the upgrade list below that lists still appear.

Upgrading docling-serve

Our image changes upstream in a few places (plugins, GPU OCR, the words of each page). Check each one before publishing an image built on a new docling-serve version (replace v1.37.0 with the new tag). Build the image first (step 2) for the checks that use it.

  1. Plugins in the web UI. We ship upstream's UI unchanged. Check that the plugins still show up where the UI reads its choices from: glm-ocr-remote under the OCR presets and ppdoclayout-v3 under the layout presets (added by DOCLING_SERVE_CUSTOM_LAYOUT_PRESETS in compose.yaml):

    docker run -d --name upgrade-check -p 5099:5001 -e DOCLING_SERVE_ENABLE_UI=true \
      -e DOCLING_SERVE_ALLOW_EXTERNAL_PLUGINS=true \
      -e 'DOCLING_SERVE_CUSTOM_LAYOUT_PRESETS={"ppdoclayout-v3": {"kind": "ppdoclayout-v3"}}' \
      dcc-docling-serve-test:cu130
    curl -s localhost:5099/v1/capabilities | python3 -c "import sys, json; s = json.load(sys.stdin)['stages']; \
      print([p['id'] for p in s['ocr']['presets']], [p['id'] for p in s['layout']['presets']])"
    docker rm -f upgrade-check
  2. Build the images locally (CU130 and CPU at least). The build itself fails if onnxruntime-gpu has no CUDA provider or its libraries do not resolve:

    docker build --build-arg DOCLING_SERVE_IMAGE=ghcr.io/docling-project/docling-serve-cu130 \
      --build-arg DOCLING_SERVE_TAG=v1.37.0 -t dcc-docling-serve-test:cu130 \
      -f plugins/Dockerfile.docling-serve plugins/
  3. GPU OCR at runtime. The RapidOCR session must list CUDAExecutionProvider first:

    docker run --rm --gpus device=0 --entrypoint python dcc-docling-serve-test:cu130 -c "
    import glob, onnxruntime as ort
    model = sorted(glob.glob('/opt/app-root/src/.cache/docling/models/RapidOcr/*det*.onnx'))[0]
    print(ort.InferenceSession(model, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']).get_providers())"
  4. Vector shapes do not force OCR. docling 2.136 fixed this upstream (we carried a patch for it until docling-serve 1.37.0), so a rule or a table fill crossing programmatic text no longer sends that text to OCR. Check the fix is still in (True); if it is gone, restore the patch from git history (plugins/ocr_ignore_shapes.py, removed with the move to 1.37.0):

    docker run --rm --entrypoint python dcc-docling-serve-test:cu130 -c \
      "import docling.models.base_ocr_model as m; print(hasattr(m.BaseOcrModel, '_is_rule_like'))"
  5. Words of each page. Check that the option and the field are still there, and that they survive nesting (a stale holder schema drops the field on the way out):

    docker run --rm --entrypoint python dcc-docling-serve-test:cu130 -c "
    from docling.datamodel.service.responses import ConvertDocumentResponse, ExportDocumentResponse
    from docling.datamodel.service.options import ConvertDocumentsOptions
    print('option:', 'include_word_boxes' in ConvertDocumentsOptions.model_fields)
    r = ConvertDocumentResponse(document=ExportDocumentResponse(filename='x'), status='success', processing_time=0.1)
    r.document.word_boxes = {1: None}
    print('answered:', 'word_boxes' in r.model_dump()['document'])"
  6. Speed kept. Compare timings against the previous release; born-digital documents (the CELEX regulation) should not be OCR-bound:

    uv run --script benchmarks/e2e_timing.py --docs ../test-docs --tag v1.37.0 --variants official-cu130 custom-cu130 \
      --image custom-cu130=dcc-docling-serve-test:cu130
  7. Plugins. Run the e2e tests against the stack (GLM-OCR via vLLM, PP-DocLayout-V3) and convert a document in the web UI with both plugins selected. Then check that PP-DocLayout-V3 still produces lists: its list detection hooks into docling's layout post-processing and switches itself off (warning "list detection is off" or "list detection failed" in the logs) if docling changed it. benchmarks/list_eval/run_all.sh dcc-docling-serve-test:cu130 should show the pp-rules numbers from the table above.

  8. Publish. Merge to main, then run the Build docling-serve with layout and OCR plugins workflow with version=v1.37.0:

    gh workflow run cd.yml -f version=v1.37.0

CI/CD

CI (.github/workflows/ci.yml)

Runs on push/PR: lint with ruff.

Docker image (.github/workflows/cd.yml)

Builds and pushes the patched docling-serve images (default, -cpu, -cu128, -cu130) to GHCR. It runs only when dispatched manually (Actions → Build docling-serve with layout and OCR plugins → Run workflow, or gh workflow run cd.yml -f version=v1.37.0); pushing to main never publishes an image. Its one input, version, is a docling-serve release tag: every variant is built on that upstream tag and published under the same tag and :latest. Moving tags such as main or latest are refused.

To ship our own changes (a plugin bump, a patch) without a new upstream release, run it again with the current version; it overwrites that tag with the new build.

The tag is resolved to its digest at the start of the run and the build uses that digest, so all variants of one run share the same base. The digest is shown in the run summary and stored in the image labels:

docker inspect ghcr.io/dcc-bs/dcc-docling-serve:v1.37.0 \
  --format '{{ index .Config.Labels "org.opencontainers.image.base.name" }} {{ index .Config.Labels "org.opencontainers.image.base.digest" }}'

License

MIT

About

Patched docling-serve image with DCC docling plugins for GLM-OCR and PP-DocLayout-V3 installed and an updated gradio ui

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages