Conversation
Add extract_layout_blocks / extract_layout_blocks_mem: run the existing Full-mode extract+convert pipeline and return a Firecrawl-compatible blocks payload for citation grounding. The Markdown convert loop already classifies every fragment it emits (heading tier, list item, caption, code, table, image); an opt-in BlockRecorder now serializes those decisions as typed blocks with normalized 0-1 page-space bboxes (top-left origin) and exact [start, end) byte spans into the payload's markdown. Recording is a parallel observation of the same conversion pass: with no recorder the pipeline and its output bytes are unchanged, and all markdown snapshots pass. - Hosted type mapping: heading tier 1 -> title, tiers 2-6 -> section_header (label H2..H6), body -> text, list -> list_item, caption -> caption, code -> code, table -> table, image -> picture. Furniture is stripped before conversion and never invented. - source is always native_text; confidence.layout/ocr stay null (no layout or OCR model produces a real score). - New CLI flag: pdf2md --layout-blocks-json. - Python (extract_layout_blocks(_bytes)) and napi (extractLayoutBlocks) wired like extract_structure_elements; WASM omitted. - Tests: recorder unit tests, synthetic lopdf + fixture integration tests, Python and napi binding tests. Docs for rust-api, python, and the napi README. Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
There was a problem hiding this comment.
All reported issues were addressed across 15 files
Shadow auto-approve: would not auto-approve because issues were found.
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
- Enable image placeholders for the layout-blocks payload so figures surface as picture blocks (the default process_pdf markdown still omits them); document the divergence on every API surface and add fixture coverage (Rust + Python). - Skip standalone page-number removal for classified list-item fragments: a lone fragment is always "isolated", so a legitimate "- 5 -" list item was dropped from the payload even though the document-level pass keeps it. Unit test added. - Add the missing y0 lower-bound check in the synthetic bbox assertion. - test.mjs: decode span slices with a fatal TextDecoder so mid-codepoint spans fail instead of being silently replaced. - napi README: note extractLayoutBlocks is synchronous with no async variant. Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Shadow auto-approve: would not auto-approve because issues were found.
Fix all with cubic | Re-trigger cubic
…mption Two review findings: - `markdownSpan` was emitted as `u32`, so byte offsets into markdown larger than 4 GiB wrapped. It is now `f64` (JS numbers are safe integers well past that), with a regression test at the boundary and a `Number.isSafeInteger` assertion in the napi smoke test. - List items were blanket-exempted from page-number removal, which kept every page-number-shaped fragment. The exemption now applies only when an adjacent list fragment on the same page makes the item non-isolated, matching what the document-level pass would decide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TeuFQUkLJAMt4KTLvSF7hh
|
Addressed the review findings in f758607.
On the list-item folio exemption: the blanket skip was too broad. Page-number removal decides by line isolation, and a single-item fragment is always isolated, so exempting every list item kept fragments the document-level pass would have dropped. The exemption now applies only when an adjacent list fragment on the same page makes the item non-isolated, which is the same conclusion the document-level pass reaches. 1040 lib tests and the napi test pass. |
There was a problem hiding this comment.
0 issues found across 3 files (changes from recent commits).
Shadow auto-approve: would require human review. Adds an opt-in layout-blocks extraction feature with a public API across Rust, Python, Node, and a CLI flag. Needs human review since it defines new API contracts and updates the core conversion loop; the pdf-evals suite hasn't run.
Re-trigger cubic
Summary
Adds
extract_layout_blocks/extract_layout_blocks_mem: run the existing Full-mode extract+convert pipeline and return a Firecrawl-compatible blocks payload for citation grounding. No layout model — the Markdown convert loop already classifies every fragment it emits (heading tier, list item, caption, code, table, image); it just never serialized those decisions. A new opt-inBlockRecorder(src/markdown/blocks.rs) captures them as they happen, so the hypothesis in the issue held and no re-derivation pass was needed.Each block carries:
markdownSpan: exact[start, end)byte offsets into the payload's ownmarkdownstring, recorded as the convert loop builds the document. Spans are ascending, non-overlapping, land on UTF-8 boundaries, and cover ~99% of the markdown on the fixture corpus (only inter-block separators fall outside).bbox: normalized 0–1 page space, top-left origin, from the existingTextItem/ table / image coordinates and the page's CropBox/MediaBox.type: hosted names via heading tier 1 →title, tiers 2–6 →section_header(labelH2..H6), body →text, list →list_item, caption →caption, code →code, table →table, image →picture. Furniture is stripped before conversion as today and is never invented as blocks.source: alwaysnative_text;confidence.layout/confidence.ocrstaynullbecause no layout or OCR model produces a real score.Default output is untouched. Recording is a parallel observation of the same conversion pass: with no recorder the pipeline is byte-identical (
recording_does_not_change_default_markdownunit test), and all existing markdown snapshot tests pass unchanged.Span exactness vs document-level postprocess. The document-wide cleanup pass (hyphenation, space collapsing, folio removal) rewrites bytes, which would invalidate recorded offsets. The payload therefore runs the same cleanup per recorded fragment and reassembles the markdown with the original inter-block separators, keeping spans exact. On 7 of 10 snapshot fixtures the payload markdown is byte-identical to
process_pdf; the remaining three differ by 1–2 lines where document-level hyphenation joins a word across a block/page boundary (e.g.Green-Compstays hyphenated) — inherent to guaranteeing exact spans, and documented on the API.Surface
extract_layout_blocks(path)/extract_layout_blocks_mem(bytes)returningLayoutBlocksResult { markdown, blocks, pdf_type, page_count }. Scanned/image-based PDFs (and garbage-text suppressions) return the classification with empty markdown and no blocks, mirroringprocess_pdf's trust decisions.pdf2md --layout-blocks-jsonflag emitting{"schema_version":1, "pdf_type", "page_count", "markdown", "blocks":[{"type","label","page","bbox","markdownSpan","source","confidence":{"layout":null,"ocr":null}}]}, hand-rolled JSON like the existing--json/--items-jsonoutputs. Defaultpdf2mdoutput is unchanged.extract_layout_blocks/extract_layout_blocks_bytes+LayoutBlock/LayoutBlocksResultclasses,.pyistubs, anddocs/python.md, wired likeextract_structure_elements.extractLayoutBlocks(buffer)withLayoutBlockJs/LayoutBlocksResultJs, README section, andtest.mjscoverage. WASM intentionally omitted (root crate still checks forwasm32-unknown-unknown).Testing
cargo fmt/cargo fmt --all -- --checkclean (root and napi crates).cargo clippy -- -D warningsandcargo clippy --features ocr -- -D warningsclean.cargo testandcargo test --features ocr: all pass (1038/1126 unit + 169 integration), including unchanged markdown snapshots.src/markdown/blocks.rs), 4 integration tests including a synthetic lopdf document (title/paragraph/list geometry and top-left-origin ordering assertions), fixture grounding (span validity, ≥90% coverage, table blocks,H*labels), byte-equality withprocess_pdf_memon the snapshot fixture, and the image-based-PDF empty payload.maturin develop+pytest tests/test_python.py— 90 passed; the single failure (test_process_all_fixtures[encrypted-secret123.pdf]) is pre-existing onmain(that parametrized test runsprocess_pdfwithout a password over every fixture).napi build --platform+node test.mjs— all pass including the newextractLayoutBlocksspan/bbox assertions.cargo check --target wasm32-unknown-unknownpasses.pdf-evalsregression suite (bench.py test) — the sibling repo isn't available in this environment. Since the default pipeline is byte-identical without a recorder and all in-repo snapshots pass, no snapshot churn is expected.Out of scope
ML layout models, default markdown changes, new language bindings (WASM), and the furniture audit report (#459).
Summary by cubic
Adds layout-block extraction across Rust, the CLI, Python, and napi. It runs the existing Full-mode conversion pipeline and returns markdown plus Firecrawl-compatible typed blocks for citation grounding, with exact byte spans and normalized page coordinates; default
process_pdfoutput remains unchanged.New Features
native_textprovenance, and null confidence.pdf2md --layout-blocks-json,extract_layout_blocks/extract_layout_blocks_bytes, and synchronousextractLayoutBlocks(buffer)APIs; WASM remains unsupported.markdownSpanisf64so offsets stay exact past 4 GiB; page-number-shaped list fragments are kept only when adjacent to another list item on the same page.Written for commit f758607. Summary will update on new commits.