Three jobs behind one button
"Convert a file to Markdown" is three jobs wearing one label. Getting the text out of its container is the first. Deciding which lines are headings, tables and code is the second. Keeping structured data machine-readable is the third. A converter tends to be strong at one and merely acceptable at the next two, and the format decides which — a spreadsheet stores its grid, a PDF stores coordinates, and the same button has very different work to do.
One command, one file at a time, the same registry whether you use the site, the API or a shell:
# one file, Markdown on stdout
python -m app.cli convert paper.pdf --stdout > paper.md
# a folder, recursively: one .md per source plus report.md
python -m app.cli convert ~/documents/q4 -o ~/markdown/q4
# an archive of already-OCR-ed scans
python -m app.cli convert invoices.zip -o out --no-clobber
Token counts are o200k_base, the tokenizer behind GPT-4o and later. Another model's tokenizer lands within 5–15%, not an order of magnitude. Times are wall clock on one core, and everything under 0.1 s is dominated by process start.
Twelve files, one table
| File | Size | Time | Words | Tokens | Structure that survived |
|---|---|---|---|---|---|
paper.pdf, 15 pages | 2.2 MB | 3.50 s | 5,868 | 9,939 | none: 0 headings, 0 tables, 0 links |
report.docx | 36 KB | 0.83 s | 156 | 239 | 3 headings, 1 table with an empty header row |
profile.pptx, 7 slides | 118 KB | 0.69 s | 259 | 565 | slide comments, 7 empty "Notes" headings |
market.xlsx | 16 KB | 0.74 s | 2,102 | 4,127 | 3 sheet headings, 48 data rows |
moby.epub | 795 KB | 1.07 s | 216,432 | 312,480 | 147 headings, 143 links, 17 code fences |
api-docs.html | 33 KB | 0.71 s | 2,630 | 4,160 | 16 headings, 42 table rows, 9 fences, 16 links |
airports.csv, 86,289 rows | 8.4 MB | 0.33 s | 2,369,953 | 4,543,028 | one table, no headings (a CSV has none) |
repo.json | 6.5 KB | 0.03 s | 384 | 1,779 | 5 headings, one per top-level key |
hn.xml | 27.6 KB | 0.04 s | 2,634 | 5,618 | one table, 20 items, 7 columns |
main.py | 31 KB | 0.03 s | 2,788 | 7,794 | one python fence |
notes.md | 4.3 KB | 0.03 s | 654 | 1,138 | passthrough: 8 headings, 5 fences |
runbook.rtf | 1.2 KB | 0.01 s | 164 | 207 | title, 3 headings, bullets, bold and italic kept |
Word and slides: headings survive, titles do not
The report kept its three section headings as # lines, its bullets as * items, its bold and italic runs, and its four-row table as a pipe table. Two things did not survive. The document Title style arrived as an ordinary paragraph — "Q4 Platform Review" with no # in front of it — and the table came out with an empty header row:
| | | |
| --- | --- | --- |
| **Pipeline** | **Pages** | **Cost / 1k pages** |
| Batch upload | 12,400 | $0.84 |
A reader loses nothing: the bold row is right there. A script that parses the header line gets three empty strings, because the file stores no header row — the row was made bold, not marked as a header. If column names matter downstream, add them after conversion rather than trusting the first line.
The deck is the opposite case: 120 KB of design produced 259 words and 565 tokens. Slide boundaries are kept as <!-- Slide number: N --> comments, slide titles arrive as plain lines rather than headings, and every slide whose speaker notes are empty contributes an empty ### Notes: line — seven of them here. That is a fair summary of a deck, which is mostly layout, colour and a few bullets.
Spreadsheets and data: the most Markdown per byte
The spreadsheet is the only container where Markdown is close to lossless. Sixteen kilobytes of workbook became 2,102 words, 4,127 tokens and 51 table lines, with one ## heading per worksheet. Nothing was inferred: the grid was already there. Merged cells are the known weak spot of every spreadsheet exporter, including this one — check a financial sheet by eye before you trust a pipeline built on it.
The CSV is the same story at a different scale: 8.4 MB and 86,289 rows became 11 MB of pipe table in 0.33 seconds, 4,543,028 tokens, zero headings. There are no headings to recover because the file has none; the first row becomes the header, which is correct and also the only interpretation available. That pair is the useful lesson: bytes say nothing about the token bill — a 118 KB deck is 565 tokens, an 8.4 MB CSV is 4,543,028. Plan context on tokens, never on file size.
JSON, XML and config: structure in, a path out
The JSON response converted well — 5 headings, one per top-level key, and 1,779 tokens for 6.5 KB. The RSS feed is no longer the exception: 27,608 bytes of Hacker News XML became 20,479 bytes of Markdown in one pipe table — 20 items, seven columns (title, description, pubDate, link, dc:creator, comments, guid), 5,618 tokens instead of 8,128 — with the feed's own <channel> metadata kept as a bullet list above it. HTML inside <description> is stripped to plain text rather than escaped into a cell, an attribute-bearing element such as <guid isPermaLink="false"> arrives as {"@isPermaLink": "false", "#text": "…"} instead of being dropped, and no byte of the feed is left inside a fence.
RTF stopped being a fence too: a 1,166-byte runbook with a title, three section headings and three bullets came back as ## and ### headings, a bullet list, and *batch* and **done** exactly where the italics and the bold were — 164 words, 331 source tokens down to 207 in Markdown. A plist gains the most, because it is a key/value tree by construction: the 1,204-byte Info.plist of a desktop app arrived as key/value bullets, nested dictionaries as ##–#### headings, and an array of two document types as a two-row table — 438 tokens down to 162, a little more than a third.
Three limits are worth knowing. A monospaced run in RTF is still plain text, so C:\data and failed.log lose the inline-code look. RTF has no heading concept — a short bold paragraph is read as a heading, and its level comes from the font size — so one bold sentence is enough to invent a section. And inside a plist table an array stays JSON (["md", "markdown"]) rather than becoming a list.
All four also begin with a single # line holding the source path rather than a title from the document. On the site that path is the uploaded filename, so it is short; from a shell it is the full path. Worth knowing before you concatenate a folder of conversions into one file: those lines are noise, and they are the one thing on this page that could be done better.
PDF: the format that fails quietly
The fifteen-page paper is 2,215,244 bytes and came out as 39,663 characters — 5,868 words, 9,939 tokens, 1,074 lines, and zero headings, zero tables, zero links. The first 37 lines are the arXiv stamp, rotated on the page and flattened into 56 lines of one character each: 62 tokens of noise before the first real sentence. After that the prose is clean and 14 form feeds mark where pages break, which is the only structural signal the file offers.
This is not a defect to file. A PDF stores glyph positions, not structure: a heading is only a bigger font at those coordinates. Anything claiming otherwise is guessing, and guessing is how a table becomes eleven paragraphs. For a searchable PDF, take flat text and chunk on the form feeds; the PDF guide covers the repair work in detail.
Archives, and what the converter refuses
A ZIP of three files came back as four Markdown files — one per member plus a manifest that lists every source, its output name and its size, in the order it was processed. Nested archives are not unpacked, __MACOSX and .DS_Store are skipped, and each member keeps its own converter.
Two things it declines to do, both loudly. A scanned PDF — pages as images, no text layer — returns HTTP 422 with the reason and no bytes at all, rather than inventing text or dumping the file. A binary with no text in it exits 1 with "binary file, no extractable text". Neither is a fallback pretending to be a result.
It also does not chunk. Every endpoint and the CLI return whole Markdown; splitting for embeddings belongs where the model's limits live. The chunking guide measures what a bad split costs on real documents.
Choosing the route
| What you have | What to expect | Why |
|---|---|---|
| Report, deck, spreadsheet | headings and tables, 3–51 of them | Office files store structure, so it is copied, not guessed |
| EPUB book | chapters as headings, links kept | 147 headings and 143 links in Moby Dick; the front matter comes along too |
| Web page | article text plus the site chrome | 16 headings, 42 table rows, and 35 tokens of navigation |
| flat text, no headings | the file has coordinates; chunk on page breaks | |
| CSV or JSON | a table or a key tree, both faithful | no interpretation needed, and no drift between runs |
| A scan | HTTP 422 with a reason | run OCR first, then convert |