Building RAG pipelines, the step that kept letting me down was the very first one: turning a PDF into text. Two runs of the same file could give slightly different output, tables collapsed into garbled lines, and Chinese PDFs were mostly a coin flip. When your chunks change between runs, so do your embeddings and your eval numbers — and you stop trusting the index.

So I wrote pdfmuse: a PDF/DOCX parser with one goal — be a precise, boring pre-layer for RAG. Same file in, same output out, every time. No probabilistic models in the core path.

This post is partly about how it works, and partly about the humbling bug I only found by dogfooding it on real data. That second part is the one worth your time.

The design

One Rust core, three bindings (Python / Node / WASM) that are byte-identical. They all serialize through one path; a CI gate fails if their output diverges by a single byte. Prototype in Python, ship in Node/WASM, get the exact same chunks.