Skip to content

IO — Document Conversion

Scrivai's IO module converts PDF, DOC, and DOCX files to Markdown, and renders Markdown back to DOCX. The PDF conversion pipeline supports multiple OCR backends with concurrent processing, automatic fault tolerance, and batch operations.

Architecture Overview

                        ┌─────────────────────┐
                        │   to_markdown(path)  │  ← zero-config shorthand
                        └─────────┬───────────┘
                                  │
                        ┌─────────▼───────────┐
                        │  MarkdownConverter   │  ← full-featured entry point
                        │  (ConverterConfig)   │
                        └─────────┬───────────┘
                                  │
                  ┌───────────────┼───────────────┐
                  │               │               │
            ┌─────▼─────┐  ┌─────▼─────┐  ┌──────▼──────┐
            │ .doc/.docx │  │   .pdf    │  │   .pdf      │
            │ markitdown │  │ API mode  │  │  GPU mode   │
            │  (local)   │  │           │  │  (MinerU)   │
            └───────────┘  └─────┬─────┘  └─────────────┘
                                 │
                    ┌────────────┼────────────┐
                    │            │            │
              ┌─────▼────┐ ┌────▼────┐ ┌─────▼────┐
              │MonkeyOCR │ │GLM-OCR  │ │ MinerU   │
              │(Docker)  │ │(Cloud)  │ │(HTTP API)│
              └──────────┘ └─────────┘ └──────────┘

to_markdown() vs MarkdownConverter

to_markdown() MarkdownConverter
Use case Quick one-off conversion Production pipelines
Configuration Zero-config (reads env vars) Full ConverterConfig control
Multi-backend Default backends from env Custom backend priority, dispatch strategy
Batch processing Single file only convert_batch() with callbacks
Lifecycle Creates & destroys converter per call Reuse across multiple calls
# Simple: zero-config, reads SCRIVAI_GLM_API_KEY / SCRIVAI_OCR_BASE_URL from env
from scrivai import to_markdown
md = to_markdown("report.pdf")

# Advanced: full control
from scrivai import MarkdownConverter
with MarkdownConverter(
    mode="api",
    monkey_endpoints=["http://gpu6:7866", "http://gpu7:7867"],
    glm_api_keys=["key1", "key2"],
) as conv:
    md = conv.convert("report.pdf")

Two Modes

API Mode (Default)

Uses remote OCR services (MonkeyOCR and/or GLM-OCR). PDFs are split into chunks and dispatched concurrently across backend groups.

with MarkdownConverter(mode="api", monkey_endpoints=["http://localhost:7866"]) as conv:
    md = conv.convert("report.pdf")

GPU Mode

Uses a local MinerU instance via HTTP API. Suitable when you have GPU hardware available and want to avoid external API dependencies.

with MarkdownConverter(mode="gpu") as conv:
    md = conv.convert("report.pdf")

MinerU auto-starts a mineru-router process if no URL is provided. First run downloads models (~2-3 GB).

OCR Backends

MonkeyOCR (Self-Hosted Docker)

A self-hosted OCR service running in Docker with GPU acceleration. Each instance binds to one GPU.

  • Concurrency: 4 concurrent requests per instance (default)
  • Input: PDF files uploaded via HTTP multipart
  • Output: ZIP containing Markdown file
  • Rate limit: Optional upload throttle via upload_rate (bytes/sec)
with MarkdownConverter(
    monkey_endpoints=["http://gpu6:7866", "http://gpu7:7867"],
    upload_rate=512000,  # 500 KB/s upload throttle
) as conv:
    md = conv.convert("report.pdf")

GLM-OCR (ZhipuAI Cloud)

ZhipuAI's cloud OCR API. Requires an API key from open.bigmodel.cn.

  • Concurrency: 3 concurrent requests per key (default)
  • Input: Base64-encoded PDF (≤ 100 pages, ≤ 50 MB per chunk)
  • Output: Markdown text (supports both legacy and ChatCompletion response formats)
  • Chunking: Large PDFs are automatically split into chunks before sending
with MarkdownConverter(
    glm_api_keys=["key1"],
) as conv:
    md = conv.convert("report.pdf")

MinerU (Local GPU Pipeline)

Local OCR pipeline powered by MinerU. Uses intelligent auto-routing: text-based pages are extracted directly, scanned pages go through OCR. Includes table recognition, layout analysis, and formula detection.

  • Concurrency: 2 files per group (default)
  • Groups: Multiple MinerU instances via mineru_urls
  • Auto-start: If no URL is provided, starts a local mineru-router automatically
with MarkdownConverter(
    mode="gpu",
    mineru_urls=["http://gpu1:8000", "http://gpu2:8000"],
    gpu_groups=2,
) as conv:
    md = conv.convert("report.pdf")

ConverterConfig Reference

ConverterConfig is a dataclass that controls all converter behavior. All fields have sensible defaults.

API Mode Fields

Field Type Default Description
mode "api" | "gpu" "api" Processing mode
dispatch "overflow" | "fallback" "overflow" Multi-backend dispatch strategy
api_backends list[str] ["monkey", "glm"] Backend priority order
concurrency_per_group dict[str, int] {"monkey": 4, "glm": 3} Per-backend per-group concurrency
max_failures dict[str, int] {} Per-backend max failures before fallback (required for non-terminal backends in fallback mode)
chunk_pages int 50 Pages per PDF chunk
overlap_pages int 2 Overlapping pages between adjacent chunks
chunk_strategy "file_first" | "interleave" "file_first" Batch chunk dispatch strategy

GPU Mode Fields

Field Type Default Description
gpu_backend str "mineru" GPU backend name
gpu_concurrency_per_group int 2 Concurrent files per GPU group
gpu_groups int 1 Number of GPU groups

Common Fields

Field Type Default Description
html_table "keep" | "to_markdown" "keep" HTML table post-processing
timeout int 300 Per-request HTTP timeout (seconds)
retries int 3 Max retry count per request
retry_base_delay float 10.0 First retry delay (seconds)
retry_multiplier float 2.0 Exponential backoff multiplier

Failure Handling Fields

Field Type Default Description
failure_threshold int 2 Consecutive failures per group before cooldown
failure_cooldown_max float 300.0 Maximum cooldown duration (seconds)

Credential Fields

Field Type Default Description
glm_api_keys list[str] [] GLM API keys (one per group); falls back to SCRIVAI_GLM_API_KEY env
monkey_endpoints list[str] [] MonkeyOCR endpoint URLs; falls back to SCRIVAI_OCR_BASE_URL env
upload_rate int \| None None MonkeyOCR upload throttle (bytes/sec)
mineru_urls list[str] [] MinerU instance URLs

Multi-Group Concurrency

Each backend can have multiple groups — one group per credential (API key or endpoint). Groups are load-balanced with round-robin dispatch, and each group has independent concurrency semaphores and failure tracking.

# 2 MonkeyOCR instances + 3 GLM keys = 5 groups total
# MonkeyOCR: 4 concurrent/group × 2 groups = 8 slots
# GLM: 3 concurrent/group × 3 groups = 9 slots
# Total: 17 concurrent chunk requests
with MarkdownConverter(
    monkey_endpoints=["http://gpu6:7866", "http://gpu7:7867"],
    glm_api_keys=["key1", "key2", "key3"],
) as conv:
    results = conv.convert_batch(pdf_files)

Dispatch Strategies

When multiple backends are configured, the dispatch strategy controls how chunks are assigned.

Overflow Strategy (Default)

Fills the primary backend first. When all primary groups are busy or in cooldown, spills to the next backend in api_backends order.

Chunk arrives → try monkey[0] → monkey[1] → glm[0] → glm[1] → wait → retry

Best for: maximizing throughput when primary backend is preferred but capacity is limited.

MarkdownConverter(
    dispatch="overflow",
    api_backends=["monkey", "glm"],
    monkey_endpoints=["http://gpu6:7866"],
    glm_api_keys=["key1"],
)

Fallback Strategy

Always tries the primary backend first. After max_failures consecutive failures on a backend, degrades to the next. Each chunk gets a fresh failure counter.

Chunk arrives → try monkey (up to max_failures) → degrade to glm → try glm

Best for: preferring one backend with automatic degradation on sustained errors.

MarkdownConverter(
    dispatch="fallback",
    api_backends=["monkey", "glm"],
    max_failures={"monkey": 3},  # required for non-terminal backends
    monkey_endpoints=["http://gpu6:7866"],
    glm_api_keys=["key1"],
)

PDF Chunking & Merge

Large PDFs are split into overlapping chunks for parallel OCR processing.

How It Works

  1. Split: PDF is divided into chunks of chunk_pages pages with overlap_pages overlap between adjacent chunks
  2. Dispatch: Chunks are dispatched concurrently to backend groups
  3. Merge: Results are reassembled with three-tier deduplication:
  4. Tier 1 — Difflib matching (~90%): Finds the longest matching block in the overlap region and cuts at the midpoint
  5. Tier 2 — Ratio estimation (~9%): When difflib fails, estimates the cut point by overlap ratio and snaps to the nearest paragraph boundary
  6. Tier 3 — Direct concatenation (<1%): Defensive fallback when no overlap is detected

Configuration

MarkdownConverter(
    chunk_pages=50,      # pages per chunk (default)
    overlap_pages=2,     # overlap between chunks (default)
)

Small PDFs (≤ chunk_pages) degrade to a single chunk with zero overhead.

Fault Tolerance

The converter implements three layers of fault tolerance:

Layer 1 — Per-Request Retry

Each backend call retries up to retries times with exponential backoff (retry_base_delay × retry_multiplier^n). Rate-limit responses (HTTP 429) trigger immediate cooldown instead of counting as a retry.

Layer 2 — Per-Group Cooldown

After failure_threshold consecutive failures on a single group, that group enters exponential backoff cooldown (up to failure_cooldown_max seconds). Other groups in the same backend remain available.

Layer 3 — Backend Dispatch

  • Overflow mode: When all groups of a backend are in cooldown, chunks automatically spill to the next backend. If all backends are dead for longer than failure_cooldown_max, an OcrBackendError is raised.
  • Fallback mode: After max_failures on a backend, the chunk degrades to the next backend in priority order.

Batch Processing

convert_batch() processes multiple files with two strategies:

file_first (Default)

Files are processed sequentially; chunks within each file are dispatched in parallel. Failed files don't block subsequent files.

with MarkdownConverter(monkey_endpoints=["http://gpu6:7866"]) as conv:
    results = conv.convert_batch(["a.pdf", "b.pdf", "c.pdf"])
    # results: {"a.pdf": "...", "b.pdf": "...", "c.pdf": "..."}

interleave

All files' chunks go into a single dispatch pool, maximizing backend utilization across files.

with MarkdownConverter(
    chunk_strategy="interleave",
    monkey_endpoints=["http://gpu6:7866"],
) as conv:
    results = conv.convert_batch(pdf_files)

Completion Callback

Track per-file progress with on_complete:

def on_file_done(path: str, result: str | Exception) -> None:
    if isinstance(result, Exception):
        print(f"FAILED: {path} — {result}")
    else:
        print(f"OK: {path} ({len(result)} chars)")

with MarkdownConverter(monkey_endpoints=["http://gpu6:7866"]) as conv:
    results = conv.convert_batch(pdf_files, on_complete=on_file_done)

When on_complete is provided, failed files are silently omitted from the result dict. Without it, any failure raises OcrBackendError.

Error Handling

The IO module exports two typed exceptions:

from scrivai.io import OcrBackendError, RateLimitError

try:
    md = conv.convert("report.pdf")
except RateLimitError as e:
    print(f"Rate limited by {e.backend} (HTTP {e.status_code})")
except OcrBackendError as e:
    print(f"Backend error: {e.backend}[{e.group}] — HTTP {e.status_code}")
  • OcrBackendError — Base class for OCR backend errors. Attributes: status_code, backend, group.
  • RateLimitError — HTTP 429 rate-limit error (subclass of OcrBackendError).

HTML Table Post-Processing

OCR backends often return tables as HTML. The html_table config controls post-processing:

  • "keep" (default): Preserve raw HTML tables in the Markdown output
  • "to_markdown": Convert simple HTML tables to Markdown format; tables with rowspan/colspan are kept as HTML (Markdown cannot represent merged cells)
MarkdownConverter(html_table="to_markdown")  # requires markdownify package

Word File Conversion

Word files bypass the OCR pipeline entirely:

Format Pipeline
.docx markitdown (local, instant)
.doc LibreOffice headless → .docx → markitdown
.pdf OCR backend (API or GPU mode)

Environment Variables

For simple setups, configure backends via environment variables instead of ConverterConfig:

Variable Description
SCRIVAI_GLM_API_KEY GLM-OCR API key (single key; for multiple keys use ConverterConfig.glm_api_keys)
SCRIVAI_OCR_BASE_URL MonkeyOCR endpoint URL (single endpoint; for multiple use ConverterConfig.monkey_endpoints)
SCRIVAI_MINERU_URL MinerU router URL (leave empty to auto-start local router)

Environment variables are read at ConverterConfig.__post_init__() time. Explicit config fields always take precedence.

CLI

# Basic conversion (API mode, default)
scrivai-cli io convert --input report.pdf --output report.md

# GPU mode (local MinerU)
scrivai-cli io convert --input report.pdf --output report.md --mode gpu

The CLI uses default ConverterConfig with env-based credentials. For advanced configuration (multi-backend, dispatch strategy, batch), use the Python API.