Skip to main content

docmd

Convert PDFs, DOCX, and PPTX to clean, structure-preserving Markdown — from the CLI or a Python library.

Built for feeding documents into LLM and RAG pipelines, where clean Markdown beats raw text extraction.

(Published on PyPI as docmd-cli since docmd was already taken by an unrelated package — the import (import docmd) and CLI command (docmd convert ...) are unaffected.)

pip install docmd-cli
docmd convert report.pdf
from docmd import convert

markdown = convert("report.pdf")
print(markdown)

Why docmd

Great open-source document-to-Markdown converters already exist (docmd is built on Marker). The problem isn't quality — it's that running them yourself means a Python environment, several GB of RAM, ideally a GPU, and a non-trivial setup process before you convert your first file.

docmd wraps that engine with sane defaults and adds real post-processing on top (table cleanup, heading normalization) so the output is closer to what a RAG pipeline actually wants, not just raw model output.

A hosted API (POST a file, get Markdown back, no local setup) is planned — see ARCHITECTURE.md. It is not live yet; this repo is the open-source core, usable standalone today.

What it handles

Input Output
PDF (text-based) Markdown with preserved headings, lists, tables
PDF (scanned) Markdown via OCR — bundled by Marker, free, but needs one extra native binary
DOCX Markdown with formatting preserved (pip install docmd-cli[full])
PPTX Markdown, one section per slide (pip install docmd-cli[full])

Quickstart

CLI

pip install docmd-cli
docmd convert my-file.pdf -o output.md

Python

from docmd import convert

# From a file path
markdown = convert("my-file.pdf")

# From bytes
with open("my-file.pdf", "rb") as f:
    markdown = convert(f.read(), filename="my-file.pdf")

Installing DOCX/PPTX support

The base install (pip install docmd-cli) covers PDF only and stays lean. DOCX and PPTX need Marker's own additional dependencies:

pip install "docmd-cli[full]"

DOCX/PPTX conversion also needs weasyprint's native Pango/GObject/Cairo libraries, which pip cannot install for you:

# macOS
brew install pango

# Debian/Ubuntu (24.04 and older)
sudo apt-get install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info

# Debian trixie (13) and newer: libgdk-pixbuf2.0-0 was renamed
sudo apt-get install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 libffi-dev shared-mime-info

PDF conversion (the base install) does not need this.

OCR and equations need one native binary

Found by testing docmd against a real scanned document, not documented anywhere upstream at the time of writing: Marker's current OCR and equation-recognition model is a vision-language model served through either vllm (GPU/Linux-oriented) or llama.cpp's llama-server binary - there is no plain-CPU/transformers fallback. pip install docmd-cli cannot provide either one, since neither ships as a normal Python wheel.

This only matters for scanned PDFs (no embedded text layer) and PDFs with equations - a normal text-layer PDF never touches this code path, and everything else in this README works with just pip install.

# macOS / Linux with Homebrew
brew install llama.cpp

# No Homebrew: download a prebuilt binary directly, no package manager needed
# (pick the archive matching your OS/arch from the releases page)
curl -LO https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-<version>-bin-macos-arm64.tar.gz
tar xzf llama-<version>-bin-macos-arm64.tar.gz
export LLAMA_CPP_BINARY=$PWD/llama-<version>/llama-server

Without it, converting a scanned PDF or one with equations raises a MissingSystemDependencyError with these same instructions - not a raw stack trace. First real OCR run also downloads the model's GGUF weights from Hugging Face (a few GB), separate from the PyTorch weights Marker already downloaded.

OCR output is not guaranteed reproducible run to run - confirmed directly: the same scanned file through the same code path twice produced different text. This is architectural (concurrent batched inference under the hood), not a docmd bug and not fixable by a config flag alone - see CONTRACT.md for the full investigation. Doesn't affect plain text-layer PDFs, only pages that actually go through OCR or equation recognition.

Image handling

Images default to a text placeholder (*[... omitted]*) - no binary data, nothing to resolve, safe for RAG chunking:

markdown = convert("report.pdf")  # image_mode="placeholder" by default

To keep real image links instead, use image_mode="alt-text" and pass output_dir so the image files actually get saved somewhere the links can resolve to:

from docmd import convert_document
from docmd.config import ConvertConfig

result = convert_document(
    "report.pdf",
    config=ConvertConfig(image_mode="alt-text"),
    output_dir="output/",
)

From the CLI, --image-dir defaults to the output file's directory when -o is given:

docmd convert report.pdf -o output/report.md --image-mode alt-text

image_mode="skip" drops images entirely - no placeholder, no files.

Provenance

Every conversion carries tracking info in result.provenance - useful for "this converted differently yesterday" debugging:

from docmd import convert_document

result = convert_document("report.pdf")
print(result.provenance)
# {'docmd_version': '0.1.3', 'backend': 'marker', 'backend_version': '2.0.0',
#  'ocr_used': False, 'conversion_duration_ms': 489}

Same shape regardless of which backend actually ran - convert() (the plain string-returning function) doesn't expose this; use convert_document() for it.

RAG-ready chunks

For search/RAG use cases that need more than one Markdown string, opt into per-block chunks with page, section, content type, and bounding-box metadata:

from docmd import convert_document
from docmd.config import ConvertConfig

result = convert_document("report.pdf", config=ConvertConfig(include_chunks=True))
for chunk in result.chunks:
    print(chunk.page, chunk.content_type, chunk.section, chunk.text[:50])

Or from the CLI: docmd convert report.pdf --format rag -o chunks.json.

One chunk per structural block the backend identified (a paragraph, a table, a heading, an image, ...) by default. content_type is one of a small, stable set (text, heading, table, image, list), independent of Marker's own internal block-type names. section is a breadcrumb of the nearest heading at each level above the chunk (e.g. "Chapter 3 > 3.1 Introduction"). Chunk text is the block's own raw text, extracted before docmd's Markdown post-processing runs - a table chunk's text is the backend's flattened cell text, not a cleaned Markdown table. chunks is None unless include_chunks=True - the default convert_document() call is unaffected.

Raw blocks are often too small (a bare heading, a one-sentence paragraph) or too large (a big table) for good embeddings. Set chunk_max_tokens to merge adjacent text/heading chunks - same page, same section - up to roughly that many tokens (a cheap ~4-chars/token estimate, not a real tokenizer):

config = ConvertConfig(include_chunks=True, chunk_max_tokens=400)

Tables, images, and lists are never merged into surrounding text, and merging never crosses a page or section boundary. From the CLI: docmd convert report.pdf --format rag --chunk-max-tokens 400 -o chunks.json.

How it works

docmd wraps Marker with sane defaults and a clean output format, then runs its own post-processing pass (docmd/postprocess/) to fix table structure and normalize heading levels — see ARCHITECTURE.md for why this is the actual differentiation, not just a thin wrapper, and CONTRACT.md for exactly what's guaranteed about the output (and what isn't, yet) — independent of Marker or any future backend.

License

The docmd wrapper code is MIT — see LICENSE.

docmd depends on Marker, whose code and model weights carry separate licenses — the weights license has a revenue/funding threshold that matters if you deploy commercially at scale. See docs/licensing.md for the precise terms; don't rely on this sentence alone.

Roadmap

  • PDF, DOCX, PPTX → Markdown
  • Table cleanup / heading normalization post-processing
  • Hosted API
  • OCR quality tuning for scanned documents
  • Batch conversion endpoint
  • HTML output option

Contributing

Issues and PRs welcome. If you're hitting a conversion quality issue, please include a sample file (or a minimal reproduction) — it makes fixes much faster.


If docmd saves you the trouble of setting up your own PDF-parsing pipeline, consider starring the repo — it's how other people find it.

Metadata

Release files for docmd-cli 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for docmd-cli 0.1.5
File Size Uploaded
docmd_cli-0.1.5.tar.gz 90.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for docmd-cli 0.1.5
File Interpreter ABI Platform
docmd_cli-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 117.7 kB

Release files / docmd_cli-0.1.5.tar.gz

Download URL docmd_cli-0.1.5.tar.gz
Size 90.1 kB
Tags Source
SHA-256 checksum
How to use checksums
017d9532ed7d94f5b5d65ada6ebe57fd331e49c656ccf38a85de64d9c7932cdf
BLAKE2b-256 checksum
How to use checksums
276d65922913af8244689e781005f33bfc4b880ad9748e26726754e889d887a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / docmd_cli-0.1.5-py3-none-any.whl

Download URL docmd_cli-0.1.5-py3-none-any.whl
Size 27.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ecccb4a1d92f51e49b8196c29220b64e27de242a94f400c88b5069f23f0ef37
BLAKE2b-256 checksum
How to use checksums
37344523ca91a611e2aa0713db0c3ce246e225fe29ae477010a1e28632791be0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.15

2 release files

0.1.12

2 release files

0.1.6

2 release files

This release

0.1.5 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page