Streaming Parsers
ProtoMolt has two parsing front ends. module-parser is a JVM pipeline module that wraps a shaded Apache Tika 4 and extracts text plus typed, per-format protobuf metadata from 100+ file formats. gRParse is a standalone C++ gRPC service that turns PDFs and raster images into a page-streamed protobuf document — OCR and layout detection on ONNX Runtime, nothing touching disk — and coordinates a fleet of smaller collector services around it.
gRParse predates the platform's module runtime and is being integrated as a first-class parsing backend. The two are described separately below because they are separate codebases with separate contracts today.
module-parser: Tika 4 in a pipeline module
module-parser is an ordinary platform module: it implements the same
ModuleProcessor<PipeStream> contract as every other module, dials the
engine's work stream, and pulls units. Inside, parsing is done by Apache Tika 4.x shipped as
the platform's shaded artifact ai.pipestream:tika4-shaded (classes relocated
under ai.pipestream.shaded.tika), covering Office documents, PDF, images
(EXIF/IPTC), email, EPUB, fonts, NetCDF climate data, WARC archives, and more — the 100+
formats Tika supports.
What distinguishes it from a raw Tika call is where the metadata lands. Rather than a flat
string map, each format family gets its own protobuf message — one proto each for PDF,
Office, image, email, media, HTML, EPUB, RTF, font, database, WARC, and climate, plus Dublin
Core and Creative Commons — under
core-services/pipestream-protos/common/proto/ai/pipestream/parsed/data/*. Over
1,000 Tika metadata fields are mapped onto typed fields (catalogued in the module's
TIKA_INTERFACE_MAPPING.md), so a consumer reads a PDF's page count or an email's
envelope from a typed field instead of parsing strings.
The module also extracts structure Tika itself does not expose: PDF bookmark outlines (via
PDFBox), EPUB tables of contents, HTML heading hierarchies, and a full CommonMark AST for
Markdown — a CommonmarkDocument message stored in
parsed_metadata["commonmark"], built with commonmark-java and ten GitHub
Flavored Markdown extensions.
Optional Docling OCR, gated by CEL
For documents Tika reads poorly — scanned PDFs, images — the module can call a Docling sidecar for OCR, layout, and table extraction. The sidecar is not invoked unconditionally: a CEL expression is evaluated per document after the Tika pass, and only matching documents take the expensive path. The shipped default routes every image and every near-empty PDF to OCR:
modules/module-parser/docs/architecture.md §5.2 — the parser's default Docling gate
mimeType.startsWith("image/") || (bodyLength < 200 && mimeType == "application/pdf")
The sidecar endpoint can be swapped live through
BackendEndpointService.UpdateBackendEndpoint without redeploying the module.
The module's configuration schema is self-describing: the published JSON Schema is generated
from the ParserConfig Java record's OpenAPI schema — there is no second copy to
drift — and enhanced at runtime with Tika's actual supported-MIME list as
x-suggestions.
Extraction has a terminal-reject policy: a document with neither body nor title after
extraction is rejected outright (REJECT_REASON_EMPTY_PARSE /
REJECT_REASON_PARSE_ERROR) rather than retried. A document that produced a body
but hit a parse error continues through the pipeline with the (truncated, 500-character)
error stamped into search_metadata.metadata["parse_error"], and degradation is
reported on the response, not only in server logs.
gRParse: diskless PDF/image parsing in C++
gRParse is a C++ (CMake) gRPC service whose contract is
ai.pipestream.parse.v1.ParseService. Its defining property is in the name of
its architecture document: document bytes stream in over gRPC and results stream out as
protobuf, and nothing — not the input, not rendered pages, not OCR intermediates — is ever
written to disk. The streaming RPC,
ParseStreamingService/StreamProcessDocument, accepts a stream of
DocumentChunk messages (up to 500 MiB) and emits one
DocumentStreamEvent.page per page in page-number order, followed by a single
complete event. Each outbound event is allocated in a short-lived
google::protobuf::Arena that lives until the asynchronous write completes.
Recognition runs on the maintained C++ RapidOCR implementation (RapidOcrOnnx) through ONNX
Runtime, on NVIDIA GPUs via CUDA or Intel GPUs/CPUs/NPUs via OpenVINO, selected with
GRPARSE_ORT_EP (cuda by default; openvino,
cpu, or auto). OCR is selective: pages with a full embedded text
layer skip raster OCR entirely, pages with a weak or partial digital layer keep their native
boxes and still run OCR, and a geometry merge drops overlapping duplicates. Per-request
do_ocr / force_ocr options override the heuristic; the
contradictory combination is rejected by name. Pages rasterize at 200 DPI by default, with a
per-request render_scale in multiples of 72 DPI (accepted range [1.0, 8.0]).
Scanned pages fed in sideways or upside down are re-read at the likely rotation and the best
read wins, with the applied turn recorded in the page's typed
PageItem.quality.rotation_degrees.
Beyond text, the page pipeline produces structure: layout detection (the default
heron model predicts seventeen labels; a legacy five-label
picodet model is compiled in as an alternative), reading order by recursive
XY-cut so multi-column pages read column by column, SLANet-plus table structure with cell
spans and header rows (a geometry fallback clusters lines into a grid when the model is
absent), a 26-class figure classifier on picture crops (bar charts, QR codes, signatures,
photographs), and ZXing barcode/QR decoding that needs no model at all. Every OCR line
becomes a TextItem with its page and bounding box in provenance;
running headers and footers land on a separate #/furniture content layer rather
than in the body.
The server fails loudly at startup if a required model is absent or the OCR sessions cannot
initialize on the configured provider, rather than silently running CPU OCR. Backpressure is
explicit: a client that stops reading page events stops returning page credits to the
scheduler, so that document stalls within its configured page window while other documents
continue. Concurrency and queue memory are tuned through a family of
GRPARSE_* knobs (GRPARSE_PAGE_WORKERS,
GRPARSE_RENDER_WORKERS, GRPARSE_INFERENCE_QUEUE,
GRPARSE_PAGE_WINDOW, and others), and a Prometheus endpoint exposes per-stage
busy fractions and a page-latency histogram.
Deterministic chunking
Two further RPCs, ChunkHierarchicalSource and ChunkHybridSource,
parse the source exactly as ConvertSource does and chunk the resulting
document. Determinism is the stated goal: the same input bytes produce the same chunk bytes
on every machine and every run — no tokenizer download, no locale, no defaulted budget — and
every boundary rule carries a version, reported per chunk in rules_digest:
grpc-services/gRParse/README.md — the versioned chunking rule sets
hierarchical walk grparse-hier/1
hybrid grparse-hybrid/1;tok=wordish/1;sent=sentence/1;max_tokens=N;merge_peers=B
tokenizer wordish/1
sentences sentence/1
Chunks report start_offset/end_offset as UTF-8 code-point
positions in the document's concatenated body text when the parse supplied an offset table
for every consumed text item; otherwise both stay unset rather than being guessed.
Collector scatter-gather
gRParse is also the coordinator of a family of standalone parser services, the collectors.
Incoming bytes are routed by format into one or more collectors; every collector's output
is an ai.pipestream.document.v1.Document whose items carry a
CollectorSource tag, and the coordinator merges them additively — item
references renumber, sources never overwrite each other, and choosing a winner among sources
is deliberately left to downstream consumers. A failed collector degrades to an error entry
instead of failing the parse; the parse fails only when every selected collector fails.
Two collectors are compiled in (the CV path itself and a Confluence wiki-storage XHTML handler). The rest are remote services, each enabled by one environment variable — an unset variable means the collector does not exist for that deployment:
grpc-services/gRParse/README.md — the remote collectors and their target variables
GRPARSE_LIBREOFFICE_TARGET office formats (doc/x, xls/x, ppt/x, odf, rtf, csv)
GRPARSE_ASR_TARGET audio and video (whisper.cpp; GRPARSE_ASR_MODEL required)
GRPARSE_EMAIL_TARGET .eml, .msg, message/rfc822
GRPARSE_XML_TARGET .xml, .nxml, .xbrl, archive forms (.dclx, .tar.gz)
GRPARSE_EBCDIC_TARGET mainframe fixed-width; explicit selection only
GRPARSE_EPUB_TARGET .epub (plus GRPARSE_MARKUP_TARGET for the chapters)
GRPARSE_MARKUP_TARGET .md, .html, .adoc, .tex, .vtt, .boxnote, Docling JSON
GRPARSE_LOL_HTML_TARGET targeted CSS-selector extraction; explicit selection only
GRPARSE_FASTWARC_TARGET .warc, .warc.gz, .warc.zst, .warc.lz4
GRPARSE_PDF_TARGET the PDF routing oracle (grpc-pdf-inspector) The wider family also includes grpc-calamine (spreadsheets), grPOIc (Apache POI), and grpc-vlm-convert, which the demo shell dials directly. No code path converts office bytes to PDF in order to parse them: office text and tables come exactly from the LibreOffice core, while the collector's page renders flow through the same layout, figure-classification, and barcode engines as the CV path — so a chart or QR code inside a DOCX is still spotted and decoded.
The PDF collector is a routing oracle rather than another source of pages. When
GRPARSE_PDF_TARGET is configured, an unrouted PDF is streamed to
grpc-pdf-inspector first: a text-based classification takes a fast path (the in-process
CV/ONNX pipeline is skipped entirely), while a scanned or mixed document falls through to
the CV pipeline with recognition restricted to the inspector's
pages_needing_ocr list. If the inspector is unreachable, the parse degrades to
the unrouted CV path with the failure noted — never to a failed parse.
After the merge, a format-agnostic repair pass runs on the finished document: repeated
top/bottom-band lines are relabelled PAGE_HEADER/PAGE_FOOTER and
moved to the furniture tree, hyphenation splits are rejoined, and paragraphs broken across
page or column boundaries are merged with provenance appended. Heading levels follow the
numbering, the first page's opening heading block becomes the TitleItem, and
everything the pass changes is counted in the Prometheus exposition
(grparse_repair_changes_total). GRPARSE_REPAIR=off disables it.
Output formats
gRParse renders the merged document into every format its wire contract declares: TEXT,
MARKDOWN, HTML, HTML_SPLIT_PAGE, JSON, YAML, DOCTAGS, DOCLANG, and VTT. An optional result
Target delivers the same canonical bundle — a manifest.json with
SHA-256 and byte size per member, a deterministically serialized document.pb,
canonical document.json, one export file per requested format, and embedded
page/picture PNGs — either as a ZIP in the response or written to an S3-compatible store
with request-supplied credentials (AWS Signature V4 over libcurl, path style, no SDK). The
bundle is byte-identical across machines for the same input: sorted members, fixed archive
timestamps, one compressor setting.
fastwarc.v1 dialect that is not wire-compatible with the published
pipestreamai/fastwarc-grpc image, so GRPARSE_FASTWARC_TARGET is
deliberately left unset. grPOIc and grpc-calamine are likewise dialed only by the demo
shell — gRParse's merge already ranks poi and calamine claims
below its own, but wiring them in as collectors is an open item. In module-parser, the
shipped Docling client is a purpose-built HTTP/1.1 client, not the
quarkus-docling injection the original integration plan describes; the
architecture document is the accurate source. Markdown link extraction is implemented but
unwired, so discovered_links is empty for Markdown.
Related
- Modules — the demand-pull contract module-parser implements.
- Data processing — what happens to parsed text next: chunking, embedding, semantic graphs, quality scoring.
- AI-friendly output formats — the typed document model and its renderings.
- Mapping & validation — the CEL machinery the Docling gate and routing conditions use.