Pipeline Usage Guide

A pipeline passes a document-tree Page between detection, layout, recognition and optional correction. See Data Structures for the data model and Pipeline and Stack for method signatures.

Quick start

from manuscript import Pipeline

pipeline = Pipeline()
result = pipeline.predict("document.jpg")
print(result["page"].get_text())

Choosing stages

Supply initialized processors to replace the defaults. Set an optional stage to None to disable it; detection cannot be disabled.

from manuscript.detectors import EAST
from manuscript.layouts import SimpleSorting
from manuscript.recognizers import TRBA
from manuscript.correctors import CharLM

pipeline = Pipeline(
    detector=EAST(weights="east_50_g1"),
    layout=SimpleSorting(use_columns=False),
    recognizer=TRBA(weights="trba_lite_g2", recognition_levels=["word"]),
    corrector=CharLM(),
)

For detection only, use Pipeline(layout=None, recognizer=None). For detection and recognition, use Pipeline(layout=None). Device and model parameters are configured on each processor rather than on Pipeline.

layout_after selects the layout slot: detector (default), recognizer or corrector. The slot executes even if its anchor stage is disabled.

pipeline = Pipeline(layout_after="recognizer", corrector=None)

Combining processors

Stack

Stack runs processors in list order. Detectors each receive the original image, and their regions are appended to a common Page. A layout can then infer containment. Duplicate regions are retained.

from manuscript import Stack
from manuscript.layouts import ReadingSorting

# block_detector and line_detector are initialized detector instances.
pipeline = Pipeline(
    detector=Stack([block_detector, line_detector]),
    layout=ReadingSorting(mode="containment", containment_overlap=0.5),
    recognizer=None,
)

Page processors receive the preceding Page. Later processors may overwrite text; built-in recognizers skip regions already containing text. Sequential behavior therefore depends on the participating processors.

Recognizer voting

Use an ensemble when recognizers should independently read the same regions:

from manuscript.recognizers import MajorityVotingRecognizer, MedoidRecognizer

# model_a, model_b and model_c are initialized recognizers.
recognizer = MajorityVotingRecognizer([model_a, model_b, model_c])
pipeline = Pipeline(recognizer=recognizer)

medoid = MedoidRecognizer([model_a, model_b, model_c], parallel=False)

Majority voting selects the most frequent normalized string; medoid selects an existing answer by summed character edit distance. Both compare text after NFC normalization, lowercasing and trimming outer whitespace. The returned text keeps the most frequent case-sensitive spelling among answers matching the winning normalized string; spelling ties prefer the first model. For example, Москва, Москва, москва produce Москва. With return_raw=True, winner contains this spelling, while normalized_winner identifies its key in votes (and distance_sums for medoid).

Detector consensus

Use ConsensusWBFDetector to retain detections supported by multiple models:

from manuscript.detectors import ConsensusWBFDetector

detector = ConsensusWBFDetector(
    detectors=[detector_a, detector_b, detector_c],
    weights=None,  # Equal coordinate weights; alternatively [1, 2, 1].
    cluster_iou=0.5,
    consensus_fraction=0.5,
    minimum_votes=2,
)
pipeline = Pipeline(detector=detector)
raw = detector.predict("page.jpg", return_raw=True)
votes = [item["votes"] for item in raw["detections"]]

Required votes are max(minimum_votes, ceil(len(detectors) * consensus_fraction)). Empty answers count toward the total number of participants; inference errors propagate. Each participant contributes at most one vote per cluster. Regions are matched separately by (type, label); configure matching semantic labels on participants. Polygons become axis-aligned enclosing boxes. Participant Pages must already use original-image coordinates and dimensions, as built-in detectors do. No extra resize or NMS is performed by the ensemble.

Participants run in supplied order. Greedy one-to-one matching uses representatives frozen before each participant; equal IoUs prefer higher object and cluster indices. The algorithm is order-dependent. The representative and final rectangle use arithmetic means by default, or weighted means with one finite positive weight per participant. Weights affect geometry and matching, never vote thresholds. Confidence scores are ignored, and fused detection_confidence is None.

The output is a flat Page; geometric descendants of participant Pages are treated as detections. Raw detections include votes and detector_indices referencing the supplied order. Fusion does not produce masks or model tensors. predict_batch(images) returns independent results in image order, including an empty list for an empty batch. It calls single-image prediction sequentially. Pickle preserves parameters and nested participants when those participants support pickling; no JSON configuration protocol for arbitrary models is assumed.

Model memory

Use memory_policy="sequential" to release each model’s sessions after use and reload them when needed. The default, "keep", retains loaded sessions.

recognizer = MedoidRecognizer([model_a, model_b, model_c])
pipeline = Pipeline(recognizer=recognizer, memory_policy="sequential")

The policy applies inside nested Stacks and ensembles. It can also be set on an individual Stack, MajorityVotingRecognizer or MedoidRecognizer. Sequential mode releases previously loaded participant sessions before starting and cleans up the active model even if prediction fails. close() releases all participants explicitly; the composition can be used again afterwards.

This mode requires parallel=False throughout the controlled composition. Custom processors must implement an idempotent close() and support another prediction after closing; stateless processors can use a no-op. Participants must not be used concurrently by other pipelines or direct calls without external synchronization. Calls on the same sequential composition are serialized.

Image data, voting results, dictionaries and runtime allocations can still occupy memory. Session release does not guarantee an immediate drop in process RSS or GPU memory reported by the driver. Repeated session initialization trades inference speed for lower model residency.

TrOCR CUDA decoding

TrOCR(device="cuda", cuda_decoding="auto") keeps encoder states, logits and KV caches on the GPU when supported. It requires dynamic-batch KV-cache graphs, CUDA-enabled PyTorch, CUDAExecutionProvider active in all three ONNX sessions, I/O binding and ONNX Runtime DLPack export support. Installing [gpu,trocr-cuda] does not guarantee DLPack support in the runtime wheel.

cuda_decoding="auto" warns once and uses NumPy decoding when CUDA bridge requirements are unmet. "required" raises instead; "off" disables device-resident decoding. CPU, CoreML and Web use NumPy in auto mode. Errors during active CUDA inference propagate. Floating-point scores may differ slightly between host and CUDA decoding.

Recognition crops and batch memory

With region_preparer="bbox", use region_preparer_options to set padding_x, padding_y, inset_x and inset_y as nonnegative fractions of the original bbox size per side. For example, {"inset_y": 0.1} removes 10% from both top and bottom before orientation compensation. Insets must be below 0.5. Padding and inset on the same axis cannot be combined, nor can relative margins and nonzero pixel pad. These options preserve Page geometry and are rejected by other crop presets.

PPOCRRec supports sort_by_width=True to group similar crop widths and restore result order afterwards. It is opt-in because right padding may affect CTC predictions. max_batch_pixels limits padded batch input pixels (batch * height * width, excluding channels), without resizing crops. A single input, including fixed ONNX batch padding, must fit or prediction raises ValueError. This is not a total runtime memory limit; batch_size still limits dynamic batches. Recognition crops are prepared in batches to reduce peak image memory.

SimpleSorting now infers columns by default (use_columns=True). Set SimpleSorting(use_columns=False) to retain grouping without column inference.

Custom stages

Custom components implement a compatible predict method; inheritance from Manuscript base classes is optional. The detector receives an image; other stages receive a Page. Each stage returns a valid Page; the detector may also return a dictionary containing it under page.

See Example Notebooks for integration examples.

Recognition and correction levels

The document tree stores text in word, line and block regions. Recognizers and correctors process selected levels of this tree: recognition_levels selects regions to recognize, and correction_levels selects text to correct. Both accept word, line and block. For example, to select words and lines for recognition and words for correction:

from manuscript import Pipeline
from manuscript.recognizers import TRBA
from manuscript.correctors import CharLM

pipeline = Pipeline(
    recognizer=TRBA(
        weights="trba_lite_g2", recognition_levels=["word", "line"],
    ),
    corrector=CharLM(correction_levels=["word"]),
)

The selected regions must already exist in the Page. When both a line and its words are selected, the words take precedence. Choose recognition levels that match the model’s training task. See Recognizers and Correctors for details.

Results, visualization and timing

predict() returns {"page": page}. With vis=True, it returns (result, preview), where preview is a PIL image:

result, preview = pipeline.predict("document.jpg", vis=True, profile=True)
print(result["page"].get_text())
preview.save("preview.png")
print(pipeline.last_timings)

profile=True prints stage times; last_timings provides them in seconds. Intermediate Pages are available through last_detection_page, last_layout_page, last_recognition_page and last_correction_page. See Pipeline and Stack for details.