A stack of scanned pages is rarely a single document. Mailroom batches, loan packages, and archived files arrive as one merged PDF that actually holds an invoice, then a contract, then an ID card — concatenated in order. Before you can route, index, or extract anything, you have to find where each sub-document starts. That’s page-stream segmentation, or document splitting.
This sample shows how to split a merged document into its constituent sub-documents using Nutrient Python SDK. Splitting predicts, for every page, whether it starts a new sub-document, and returns the resulting page ranges. It runs locally.
Download sampleHow Nutrient helps
Nutrient Python SDK runs the full page-stream segmentation pipeline behind a single method call. The SDK handles:
- Rendering every page to a bitmap at the resolution the image model expects
- Deriving each page’s text from the extraction pipeline
- Scoring each page for whether it begins a new sub-document, fusing the visual and text signals
- Grouping consecutive pages into sub-document page ranges
- Serializing the result to JSON
The result is the list of detected sub-documents — each a contiguous, 1-based, inclusive page range with the confidence that a new document starts there.
Why two signals
A new document often announces itself visually — a fresh letterhead, a different layout, a form’s first page — and just as often in its text — a new salutation, a heading, an invoice number. Scoring both the page image and the page’s text and fusing them is more robust than either alone, especially for scanned batches where layout and wording shift together at a boundary.
The boundary threshold
Each page gets a calibrated boundary confidence between 0 and 1. A page starts a new sub-document when its confidence meets boundary_threshold (the first page is always a boundary). Raise the threshold for fewer, larger segments when the model must be more certain to cut; lower it to split more eagerly. The default (0.5) is a balanced starting point.
Document splitting requires the document split feature in your license.
Complete implementation
Import the classes used in the sample:
from nutrient_sdk import Document, Vision, NutrientExceptionLoading the document
Open the merged document in a context manager(opens in a new tab) so resources are cleaned up after processing:
def main(): try: with Document.open("merged_documents.pdf") as document:Configuring the split
Tune the behavior on the document’s split settings. This example keeps the default threshold and asks for the per-page confidences so you can inspect the decision:
split = document.settings.document_split_settings
# A page starts a new sub-document when its calibrated confidence meets this (0..1). # Higher = fewer, larger segments. Default 0.5. split.boundary_threshold = 0.5
# Diagnostic: also return the calibrated boundary confidence for every page. split.include_page_confidences = TrueSplitting the document
Create a vision instance bound to the document with Vision.set(document), then call split():
vision = Vision.set(document) result_json = vision.split()Write the JSON result to a file for downstream routing:
with open("output.json", "w") as f: f.write(result_json) except NutrientException as e: print(f"Error: {e}")
if __name__ == "__main__": main()Understanding the output
split() returns JSON with a top-level split object:
segments— The detected sub-documents, in reading order. Each entry carries:startPage— First page of the sub-document (1-based, inclusive).endPage— Last page of the sub-document (1-based, inclusive).pageCount— Number of pages in the sub-document.boundaryConfidence— Calibrated confidence (0.0 to 1.0) that a new sub-document starts atstartPage. The first segment (page 1) is a forced boundary and reports 1.
pageConfidences— When you setinclude_page_confidences, the calibrated boundary confidence for every page in order (index 0 is page 1), so you can re-threshold offline without re-running the model.
To turn the segments into separate files, use the page ranges with your PDF page-extraction workflow — each startPage–endPage range is one sub-document.
Choosing the threshold
If the splitter is over-segmenting (cutting a single document into pieces), raise boundary_threshold toward 1. If it’s merging distinct documents, lower it toward 0. Turning on include_page_confidences shows exactly how close each page was to the cut, which makes tuning the threshold straightforward.
Error handling
Vision API raises VisionException (a NutrientException) when splitting fails.
Common failure scenarios include:
- The document can’t be read due to path or permission issues
- A page produces no renderable image
- A splitting model is missing or inaccessible, or the feature isn’t licensed
- The document exceeds the splitter’s maximum page count (split the stream into sub-ranges and process each)
In production code:
- Catch
NutrientException. - Return a clear error message.
- Log failure details for debugging.
Conclusion
The workflow for document splitting is:
- Open the merged document using a context manager(opens in a new tab) for automatic resource cleanup.
- Tune
boundary_threshold(and optionallyinclude_page_confidences) on the split settings. - Create a vision instance with
Vision.set(). - Call
split()to segment the document and export the page ranges as JSON. - Write the JSON to a file, then extract each
startPage–endPagerange as its own document. - Handle
NutrientExceptionfor robust error recovery.
For related document workflows, refer to the Python SDK guides.
Download this ready-to-use sample package to explore document splitting.