Skip to content

Commit 0314b75

Browse files
committed
Merge feat/pdf-markdown: PDF→Markdown endpoint + geometric structure mode
- POST /ocr/pdf?markdown=1 (& ?as_pages=1) on GPU and CPU routes - pdf_job.h split into header + pdf_job.cpp - PDFium char-flow text-layer extraction (surrogates, control chars) - markdown exporter plain-text fallback for unrecognized formula/table regions - geometric mode: exact text layer + tables→HTML / formulas→LaTeX, never OCRs (image-only pages return empty prose; use mode=auto/ocr to OCR them)
2 parents b4f92af + c88c1ca commit 0314b75

11 files changed

Lines changed: 1385 additions & 844 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ site/
6060

6161
# TRT/model scratch cache
6262
model.cache
63+
# Local scratch outputs (demo/markdown test results) — never tracked
64+
/tmp/

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ if(USE_CPU_ONLY)
417417
src/server/cpu_main.cpp
418418
src/routes/common_routes.cpp
419419
src/routes/pdf_routes.cpp
420+
src/pipeline/pdf_job.cpp
420421
${PROTO_GEN_CC}
421422
${GRPC_GEN_CC}
422423
)
@@ -685,6 +686,7 @@ else()
685686
src/routes/common_routes.cpp
686687
src/routes/image_routes.cpp
687688
src/routes/pdf_routes.cpp
689+
src/pipeline/pdf_job.cpp
688690
${PROTO_GEN_CC}
689691
${GRPC_GEN_CC}
690692
)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Then opt in per request (combine freely; `tables`/`formulas` auto-enable layout)
109109
```bash
110110
curl -X POST "http://localhost:8000/ocr/raw?layout=1&tables=1&formulas=1" \
111111
--data-binary @paper.png -H "Content-Type: image/png"
112-
# PDF: POST /ocr/pdf · Markdown export: POST /ocr/markdown · gRPC: port 50051
112+
# PDF: POST /ocr/pdf · PDF → Markdown: POST /ocr/pdf?markdown=1 · page → Markdown: POST /ocr/markdown · gRPC: port 50051
113113
```
114114

115115
`GET /capabilities` reports which stages a running server has loaded.
@@ -268,7 +268,7 @@ One binary serves HTTP and gRPC from a shared GPU pipeline pool.
268268
| `POST /ocr` | OCR base64 image in JSON |
269269
| `POST /ocr/pixels` | Zero-decode raw pixel buffer |
270270
| `POST /ocr/batch` | Batch of images |
271-
| `POST /ocr/pdf` | PDF → text (optional page images & auto-rotate) |
271+
| `POST /ocr/pdf` | PDF → text (optional page images & auto-rotate); `?markdown=1` → whole PDF as Markdown |
272272
| `POST /ocr/markdown` | Page → faithful Markdown (GPU build; requires layout) |
273273
| `POST /infer` | OCR + layout / reading-order / blocks in one structured response |
274274
| `GET /capabilities` | Runtime feature & route discovery |

docs/api/http.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,14 @@ embedded text is trustworthy.
291291
- multipart with field name `file` or `pdf`.
292292
- **`mode` query** (default `ocr`):
293293
- `ocr` — always render + OCR every page.
294-
- `geometric` — use the PDF text layer for content; render only when
295-
`layout=1` or as a safety net.
294+
- `geometric` — prose comes **only** from the PDF text layer; the page is
295+
**never OCR'd**. A page with no usable text layer (image-only / scanned)
296+
returns empty prose — use `auto` or `ocr` for those. The page is still
297+
rendered for layout and table/formula recognition when requested (those
298+
are vision-recognized for born-digital pages too), so `?tables=1`/
299+
`?formulas=1` work in geometric mode and keep the exact text-layer prose.
296300
- `auto` — text layer when trusted (`text_layer_quality == "trusted"`),
297-
OCR otherwise.
301+
OCR otherwise. This is the mode that recovers prose on image-only pages.
298302
- `auto_verified` — GPU only. Runs OCR, then cross-checks every
299303
detection against the PDF text layer; replaces matches with the
300304
native string (`source: "pdf"`). On CPU this aliases to `auto`.
@@ -304,6 +308,40 @@ embedded text is trustworthy.
304308
`MAX_PDF_PAGES` defaults to `2000`. Exceeding returns
305309
`400 PDF_TOO_LARGE` with the limit echoed back in the message.
306310

311+
### PDF → Markdown (`?markdown=1`)
312+
313+
One call converts the whole PDF to the same faithful Markdown as
314+
`POST /ocr/markdown`, using the parallel page pipeline — no client-side
315+
page splitting. Available on both the GPU and CPU builds:
316+
317+
```bash
318+
curl -X POST 'http://localhost:8000/ocr/pdf?markdown=1' \
319+
--data-binary @paper.pdf -H 'Content-Type: application/pdf'
320+
```
321+
322+
- Returns `text/markdown`: pages concatenated in order, each prefixed with an
323+
invisible `<!-- page N -->` marker (safe to render, easy to split on).
324+
- `&as_pages=1` returns JSON instead — `{"pages":[{"page_index":0,
325+
"markdown":"…"}, …]}` — for chunked/RAG consumers; per-page
326+
`text_degraded` / `table_degraded` / `formula_degraded` flags appear when set.
327+
- Implies `layout=1&reading_order=1` (requires the layout model —
328+
`400 LAYOUT_DISABLED` otherwise). In the default `ocr` mode, tables and
329+
formulas are recognized whenever their backends are loaded (→ HTML / LaTeX);
330+
pass `tables=0` / `formulas=0` to opt out.
331+
- **Mode choice.** `mode=ocr` (default) renders and OCRs every page — best for
332+
scanned PDFs. `mode=geometric` reads a born-digital PDF's embedded text layer
333+
directly for the prose (exact text, faster, no OCR errors) while **still**
334+
recognizing tables → HTML and formulas → LaTeX on the rendered image — so a
335+
born-digital paper exports exact text *and* structured math/tables. `auto`
336+
picks per page: text layer when trustworthy, OCR otherwise.
337+
- Figure/chart crops are embedded as base64 `data:` URIs with their OCR'd text
338+
as alt text; tables come back as HTML, formulas as `$…$` / `$$…$$` LaTeX.
339+
- `dpi`, `mode` and `autorotate` work as usual. `text=0` and `images=` are
340+
rejected (`400 INVALID_PARAMETER`) — markdown needs the text, and the figure
341+
crops are already embedded.
342+
- Per-stage degradation is aggregated in the `X-OCR-Degraded` response header
343+
with page numbers (e.g. `table(p3,p7)`), same contract as `/ocr/markdown`.
344+
307345
### Inline page-image export
308346

309347
`?images=inline` adds each rendered page back to the response as a

include/turbo_ocr/pipeline/ocr_pipeline.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,20 @@ class OcrPipeline : public IOcrPipeline {
209209
[[nodiscard]] OcrPipelineResult run_layout_only(const cv::Mat &img,
210210
cudaStream_t stream);
211211

212+
// Layout + table/formula recognition WITHOUT detection/recognition. Used by
213+
// /ocr/pdf mode=geometric/auto when the client asked for structure: the page
214+
// text comes from the PDFium text layer (passed in as `text_results`, already
215+
// in this image's pixel space), so det/rec is skipped, but tables (→ HTML) and
216+
// formulas (→ LaTeX) still need the rendered image and the layout regions.
217+
// Uploads the image once, runs layout, then the CUA router over the supplied
218+
// text boxes — the same dispatch_router_ the OCR path uses. Returns the text
219+
// results verbatim plus populated `.layout` / `.tables` / `.formulas`. A no-op
220+
// (returns just the text) when layout isn't loaded.
221+
[[nodiscard]] OcrPipelineResult run_layout_and_structure(
222+
const cv::Mat &img, cudaStream_t stream,
223+
std::vector<OCRResultItem> text_results, bool want_tables,
224+
bool want_formulas, const routing::RequestRouting &routing = {});
225+
212226
// Ensure the GPU upload buffer can hold an image of the given size.
213227
// Returns {d_img_buf_, d_img_pitch_} after grow-only reallocation.
214228
// Useful for callers that want to decode directly into the pipeline's GPU buffer.

0 commit comments

Comments
 (0)