Skip to content

feat(convert): add page-level progress callback to DocumentConverter - #3566

Open
assinscreedFC wants to merge 1 commit into
docling-project:mainfrom
assinscreedFC:feat/page-progress-callback
Open

feat(convert): add page-level progress callback to DocumentConverter#3566
assinscreedFC wants to merge 1 commit into
docling-project:mainfrom
assinscreedFC:feat/page-progress-callback

Conversation

@assinscreedFC

Copy link
Copy Markdown

Closes #3493.

Adds an optional progress_callback to DocumentConverter that receives page-level conversion progress events, so callers can drive a progress bar or stream updates without polling.

This follows the design agreed with @ibrahimGoumrane in the issue:

  • Scope (v1): page-level events only — page_started, page_completed, document_completed. Finer stages (OCR/model) can be a follow-up.
  • Event type: Pydantic BaseModel, discriminated union, mirroring the style of datamodel/service/callbacks.py (names kept distinct to avoid confusing in-process events with the HTTP-webhook ones).
  • API: constructor-level callback only, DocumentConverter(progress_callback=...).
  • Thread safety: documented (see below); no callback serialization.

Usage

from docling.document_converter import DocumentConverter
from docling.datamodel.progress import (
    PageStartedProgress, PageCompletedProgress, DocumentCompletedProgress,
)

def on_progress(event):
    if isinstance(event, PageCompletedProgress):
        print(f"page {event.page_no}/{event.total_pages} done (ok={event.success})")
    elif isinstance(event, DocumentCompletedProgress):
        print(f"document done: {event.num_pages} pages, {event.status}")

converter = DocumentConverter(progress_callback=on_progress)
converter.convert("document.pdf")

Design notes

  • The callback is passed as a parameter through pipeline.execute(...)_build_document(...) rather than stored on the pipeline instance. Pipelines are cached and shared (initialized_pipelines) and re-entered concurrently when doc_batch_concurrency > 1, so storing the callback on the instance would be a data race. Passing it on the call stack keeps emission thread-safe with no shared mutable state.
  • document_completed is emitted once per document in BasePipeline.execute inside the finally block, so a terminal event is delivered even when raises_on_error=True re-raises. This also gives non-paginated pipelines (SimplePipeline, AsrPipeline) a sensible single event.
  • page_started/page_completed are emitted in PaginatedPipeline (Legacy, VLM) and the threaded StandardPdfPipeline. Pages started but never completed on a document_timeout receive a terminal failure event, so every page_started is balanced by a page_completed.
  • Callback exceptions are logged and swallowed so a misbehaving callback can never break a conversion.

Thread-safety / ordering

The callback must be thread-safe: the default PDF pipeline processes pages on a background producer thread, so the callback is invoked from more than one thread even for a single document. No per-page ordering between page_started and page_completed is guaranteed for the threaded pipeline; document_completed is always emitted last for a given document. This is documented on ProgressCallback and DocumentConverter.__init__.

Tests

tests/test_progress_callback.py covers:

  • paginated PDF — both StandardPdfPipeline (threaded) and LegacyStandardPdfPipeline (parametrized): one page_started + one page_completed per page, single final document_completed;
  • non-paginated (Markdown / SimplePipeline): only document_completed, no page events;
  • page_range subset: total_pages and page numbers follow the requested range;
  • no-callback no-op;
  • exception isolation on both the page-event (PDF) and document-event (Markdown) paths.

ruff check + format clean. No new mypy errors in the touched files.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

DCO Check Passed

Thanks @assinscreedFC, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@dosubot

dosubot Bot commented Jun 8, 2026

Copy link
Copy Markdown

Related Knowledge

1 document with suggested updates is ready for review.

Docling

What are the detailed pipeline options and processing behaviors for PDF, DOCX, PPTX, and XLSX files in the Python SDK?
View Suggested Changes
@@ -458,24 +458,45 @@
 
 - **`allowed_formats`**: List of allowed input formats. By default, any format supported by Docling is allowed.
 - **`format_options`**: Dictionary of format-specific options (e.g., `PdfPipelineOptions`, `AsrPipelineOptions`). See format-specific sections above for details.
-- **`progress_callback`**: Optional callback function that receives structured progress events during conversion, including:
-    - **Document start/complete events** (`DocumentProgressEvent`): Emitted when a document begins or finishes processing. Includes document name and page count (if available).
-    - **Pipeline phase transitions** (`PhaseProgressEvent`): Emitted when entering or completing a phase (BUILD, ASSEMBLE, ENRICH).
-    - **Individual page completions** (`PageProgressEvent`): Emitted when each page finishes processing. Includes current page number and total page count.
-
-When no callback is provided (the default), no progress events are emitted and there is zero overhead.
+- **`progress_callback`**: Optional callable invoked with page-level progress events (`PageStartedProgress`, `PageCompletedProgress`, `DocumentCompletedProgress`) during conversion. The callback must be thread-safe: the default PDF pipeline processes pages on a background thread, so it is invoked from multiple threads even for a single document, and concurrent conversions (`settings.perf.doc_batch_concurrency > 1`) add further concurrency. Exceptions raised by the callback are logged and suppressed so they cannot interrupt conversion.
+
+The progress events use a discriminated union:
+
+1. **`PageStartedProgress`** - emitted when a page enters the conversion pipeline
+   - `kind: Literal[ConversionProgressKind.PAGE_STARTED]`
+   - `page_no: int` (1-based page number)
+   - `total_pages: int` (number of pages being processed)
+
+2. **`PageCompletedProgress`** - emitted when a page finishes processing
+   - `kind: Literal[ConversionProgressKind.PAGE_COMPLETED]`
+   - `page_no: int` (1-based page number)
+   - `total_pages: int`
+   - `success: bool` (default True)
+
+3. **`DocumentCompletedProgress`** - emitted once after a whole document finishes converting
+   - `kind: Literal[ConversionProgressKind.DOCUMENT_COMPLETED]`
+   - `num_pages: int`
+   - `status: ConversionStatus`
+
+For the threaded PDF pipeline no per-page ordering is guaranteed between `page_started` and `page_completed` events. The terminal `document_completed` event is always emitted last for a given document.
 
 **Usage Example**:
 
 ```python
-from docling.datamodel.progress_event import ProgressEvent
+from docling.datamodel.progress import ConversionProgressEvent, ConversionProgressKind
 from docling.document_converter import DocumentConverter
 
-def on_progress(event: ProgressEvent):
-    print(event.event_type, event.document_name)
-
-converter = DocumentConverter(progress_callback=on_progress)
-result = converter.convert(source="https://arxiv.org/pdf/2408.09869")
+def progress_callback(event: ConversionProgressEvent):
+    if event.kind == ConversionProgressKind.PAGE_STARTED:
+        print(f"Started page {event.page_no} of {event.total_pages}")
+    elif event.kind == ConversionProgressKind.PAGE_COMPLETED:
+        status = "successfully" if event.success else "with errors"
+        print(f"Completed page {event.page_no} of {event.total_pages} {status}")
+    elif event.kind == ConversionProgressKind.DOCUMENT_COMPLETED:
+        print(f"Document conversion completed with {event.num_pages} pages, status: {event.status}")
+
+converter = DocumentConverter(progress_callback=progress_callback)
+result = converter.convert("document.pdf")
 ```
 
 **CLI Support**: The CLI also supports progress tracking via the `--progress` flag:

[Accept] [Edit] [Decline]

How did I do? Any feedback?  Join Discord

ibrahimGoumrane

This comment was marked as off-topic.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/pipeline/base_pipeline.py 95.83% 1 Missing ⚠️
docling/pipeline/standard_pdf_pipeline.py 90.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Add an optional `progress_callback` to `DocumentConverter` that receives
page-level conversion progress events, so callers can render progress bars
or stream updates without polling (closes docling-project#3493).

A new `docling/datamodel/progress.py` defines a discriminated-union event
model mirroring the style of `datamodel/service/callbacks.py`:
`PageStartedProgress`, `PageCompletedProgress`, and `DocumentCompletedProgress`.

The callback is threaded through `pipeline.execute(...)` and `_build_document`
as a parameter rather than stored on the pipeline, because pipelines are
cached and shared across concurrent conversions; this avoids shared mutable
state and keeps emission thread-safe by construction.

Emission points:
- `document_completed` once per document in `BasePipeline.execute` (in the
  `finally` block, so a terminal event is delivered even when
  `raises_on_error=True` re-raises) — covers paginated and non-paginated
  (SimplePipeline, AsrPipeline) pipelines.
- `page_started` / `page_completed` in `PaginatedPipeline` (Legacy, VLM) and
  in the threaded `StandardPdfPipeline`. Pages started but never completed on
  a `document_timeout` get a terminal failure event so every `page_started`
  is balanced by a `page_completed`.

Callback exceptions are logged and suppressed so a misbehaving callback can
never break a conversion. The callback must be thread-safe: the default PDF
pipeline runs pages on a background thread, so it is invoked from multiple
threads even for a single document.

Tests cover the threaded and legacy paginated pipelines, the non-paginated
path, page-range subsets, the no-callback no-op, and exception isolation on
both the page-event and document-event paths.

Signed-off-by: ANIS <119749586+assinscreedFC@users.noreply.github.com>
@assinscreedFC
assinscreedFC force-pushed the feat/page-progress-callback branch from 57d24b4 to 3eef8dd Compare June 25, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Add per-page progress callback support to DocumentConverter

2 participants