⚡Highlights
OCR Pipelines for heterogeneous documents
OCREngine applies one prompt to every page. That breaks down when a single PDF or TIFF interleaves different page types — for example a packet of cognitive-assessment forms where each form needs its own JSON schema. v0.6.0 adds OCR pipelines: you decide, per page, how that page is processed, while the pipeline owns file loading, concurrency, page ordering, error handling, and OCRResult assembly.
OCREngine.ocr_image_async — the atomic building block
Run a single, already-loaded image through an engine's prompt and get back a standalone OCRPage. No preprocessing is applied (the caller owns rotate/resize), so a page can be OCR'd once and reused across calls.
import asyncio
from PIL import Image
from vlm4ocr import VLLMVLMEngine, OCREngine
engine = OCREngine(VLLMVLMEngine(model="Qwen/Qwen3-VL-30B-A3B-Instruct"), output_mode="markdown")
page = asyncio.run(engine.ocr_image_async(Image.open("page.png")))
print(page.text)IndependentPagePipeline — per-page processing, concurrently
Supply a process_page(image, *, messages_logger=None) -> OCRPage function and get a full pipeline whose concurrent_ocr mirrors OCREngine.concurrent_ocr (same arguments, same first-complete-first-out AsyncGenerator[OCRResult]). process_page receives only its own page image, so page independence is guaranteed by construction.
import json, asyncio
from vlm4ocr import OCREngine, IndependentPagePipeline, OCRPage
# One classifier + per-type extractors + a fallback — each engine is an atom (one prompt).
classifier = OCREngine(vlm_engine, output_mode="JSON",
user_prompt='Classify this page. Respond as JSON: {"page_type": "<one_token>"}')
extractors = {"form_a": OCREngine(vlm_engine, output_mode="JSON", user_prompt="<form A schema>"),
"form_b": OCREngine(vlm_engine, output_mode="JSON", user_prompt="<form B schema>")}
default = OCREngine(vlm_engine, output_mode="JSON", user_prompt="<generic schema>")
async def classify_and_extract(image, *, messages_logger=None) -> OCRPage:
label = json.loads((await classifier.ocr_image_async(image, messages_logger=messages_logger)).text)
page_type = (label[0] if isinstance(label, list) else label).get("page_type", "unknown")
page = await extractors.get(page_type, default).ocr_image_async(image, messages_logger=messages_logger)
page.metadata["page_type"] = page_type
return page
pipeline = IndependentPagePipeline(classify_and_extract, output_mode="JSON")
async def run():
async for result in pipeline.concurrent_ocr(pdf_paths, concurrent_batch_size=8):
for page in result.pages:
print(page.metadata["page_type"], page.text)
asyncio.run(run())All routing logic — classifier prompt, schemas, type labels — lives in your code; the pipeline only orchestrates. See the OCR Pipelines guide for the full walkthrough and the independence assumption.
OCRPage.metadata
OCRPage now carries a free-form metadata dict (e.g. a page type assigned by a routing pipeline), preserved on each page of the resulting OCRResult. Dict-style access continues to work.
Changes
- New:
IndependentPagePipelinefor per-page custom processing (routing, classification, multi-pass refinement) with concurrent, files-in /OCRResult-out execution. Exported from the top-levelvlm4ocrnamespace. - New:
OCREngine.ocr_image_async(image, ...)— the atomic, preprocessing-free single-image OCR call, returning a standaloneOCRPage. - New:
OCRPage.metadatafree-form dict, andOCRResult.add_page(..., metadata=...). - New:
OCRResultandOCRPageexported from the top-levelvlm4ocrnamespace. - Internal: loader dispatch consolidated into
vlm4ocr.utils.get_data_loader;SUPPORTED_IMAGE_EXTSnow lives invlm4ocr.utils. - Compat: fully backward compatible —
OCREngineand its existing methods are unchanged.