feat(convert): add page-level progress callback to DocumentConverter - #3566
Open
assinscreedFC wants to merge 1 commit into
Open
feat(convert): add page-level progress callback to DocumentConverter#3566assinscreedFC wants to merge 1 commit into
assinscreedFC wants to merge 1 commit into
Conversation
Contributor
|
✅ DCO Check Passed Thanks @assinscreedFC, all your commits are properly signed off. 🎉 |
Contributor
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
|
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: |
Codecov Report❌ Patch coverage is
📢 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
force-pushed
the
feat/page-progress-callback
branch
from
June 25, 2026 13:13
57d24b4 to
3eef8dd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3493.
Adds an optional
progress_callbacktoDocumentConverterthat 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:
page_started,page_completed,document_completed. Finer stages (OCR/model) can be a follow-up.BaseModel, discriminated union, mirroring the style ofdatamodel/service/callbacks.py(names kept distinct to avoid confusing in-process events with the HTTP-webhook ones).DocumentConverter(progress_callback=...).Usage
Design notes
pipeline.execute(...)→_build_document(...)rather than stored on the pipeline instance. Pipelines are cached and shared (initialized_pipelines) and re-entered concurrently whendoc_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_completedis emitted once per document inBasePipeline.executeinside thefinallyblock, so a terminal event is delivered even whenraises_on_error=Truere-raises. This also gives non-paginated pipelines (SimplePipeline,AsrPipeline) a sensible single event.page_started/page_completedare emitted inPaginatedPipeline(Legacy, VLM) and the threadedStandardPdfPipeline. Pages started but never completed on adocument_timeoutreceive a terminal failure event, so everypage_startedis balanced by apage_completed.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_startedandpage_completedis guaranteed for the threaded pipeline;document_completedis always emitted last for a given document. This is documented onProgressCallbackandDocumentConverter.__init__.Tests
tests/test_progress_callback.pycovers:StandardPdfPipeline(threaded) andLegacyStandardPdfPipeline(parametrized): onepage_started+ onepage_completedper page, single finaldocument_completed;SimplePipeline): onlydocument_completed, no page events;page_rangesubset:total_pagesand page numbers follow the requested range;ruff check + format clean. No new mypy errors in the touched files.