Skip to content

Commit e80370b

Browse files
authored
Merge pull request #70 from artiz/claude/docs-merge-dclx-input
Claude/docs merge dclx input
2 parents 6ffc8d7 + 60120f5 commit e80370b

108 files changed

Lines changed: 19724 additions & 655 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ __pycache__/
3737
crates/docling-py/target/
3838
crates/docling-py/.venv/
3939
crates/docling-py/python/docling_rs/*.so
40+
# docling's .dclx loader extracts each archive it reads next to the file
41+
tests/data/**/*_artifacts/
42+
tests/data/**/groundtruth_dclx/tests/

COMPARING.md

Lines changed: 0 additions & 352 deletions
This file was deleted.

MIGRATION.md

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ phased plan is kept at the end as history.)
2525
|---|---|
2626
| **What** | A Rust port of docling's converter, backends, and discriminative PDF/ASR pipelines; same `convert → DoclingDocument → export_to_markdown()/json()` shape, single static binary, no Python/torch at runtime |
2727
| **Conformance** | Declarative formats byte-for-byte vs *live* PyPI docling (most 100%, see §2); `.dclx` DocLang output ≈94% mean vs docling's own `.dclx`, OOXML all byte-exact (§2); PDF ML path 5/14 fixtures byte-exact, rest close; every optimization is gated on this not regressing |
28-
| **Performance** | PDF ML pipeline **4.3× faster warm / 4.7× end-to-end** than Python docling at 2.3–2.6× less peak RAM (INT8 + SIMD, conformance-validated); declarative formats 20–60× warm, ~60× less RAM; details + methodology in [`PDF_PERFORMANCE.md`](./PDF_PERFORMANCE.md) |
28+
| **Performance** | PDF ML pipeline **4.3× faster warm / 4.7× end-to-end** than Python docling at 2.3–2.6× less peak RAM (INT8 + SIMD, conformance-validated); declarative formats 20–60× warm, ~60× less RAM; details + methodology in [`PDF_CONFORMANCE.md`](./PDF_CONFORMANCE.md) |
2929
| **Models** | docling's own checkpoints (layout heron, TableFormer, PP-OCRv3, Whisper tiny), format-converted to ONNX by `scripts/export_*.py` — no retraining; INT8 variants are calibrated post-training quantizations (`scripts/install/quantize_models.py`) |
3030
| **Tracking upstream** | See [§9](#9-keeping-up-with-upstream-docling): conformance is measured against the *latest published* docling on demand, so an upstream release that changes output surfaces as a concrete per-fixture diff |
31-
| **Not ported (by design)** | VLM pipelines, enrichment models, DocTags/DocLang *input* backends (§5); inline formatting is baked into text rather than structured fields (§4) |
31+
| **Not ported (by design)** | VLM pipelines and enrichment models (§5); inline formatting is baked into text rather than structured fields (§4) |
3232

3333
---
3434

@@ -96,6 +96,7 @@ PyPI; run via `scripts/conformance/conformance.sh <fmt>`), not the committed gro
9696
| USPTO | `uspto.rs` | **1/5 exact (2/5 whitespace-normalized)** on the sources live docling converts — it errors on the other 5 (those are validated byte-exact via `.dclx`), and its APS-text *Markdown* export is empty where ours emits the text dump (the `.dclx` matches exactly — §5) |
9797
| XBRL | `xbrl.rs` | arelle-free core (dei facts → title, `*TextBlock` → HTML); *vs committed groundtruth* 0/2 (30 / 346 diff lines) — live docling needs arelle, which the conformance venv doesn't ship |
9898
| JSON-docling | `docling_json.rs` (serde_json) | reads docling's native JSON; ~51/145 round-trip exact |
99+
| DocLang (`.dclg`/`.dclx`) | `doclang.rs` (roxmltree) | **15/15 exact** vs live docling reading the same archives back (`tests/data/doclang`); the inverse of the `.dclx` output serializer, incl. docling's round-trip losses (list-item formatting, hyperlink targets) |
99100
| LaTeX | `latex.rs` (scanner) | simple `.tex` ≈ live (0/2 exact, but within 2 / 9 diff lines); multi-file arxiv out of scope |
100101
| MHTML (.mhtml/.mht) | `mhtml.rs` (mail-parser) → HTML backend | **docling.rs extension — no docling backend to compare against**; embedded images resolved by `Content-Location`/`cid:` |
101102

@@ -282,12 +283,18 @@ deliberate scope boundary or a cosmetic, single-fixture polish gap.
282283
classification, formula understanding, code understanding). Model-bound; out of
283284
scope for the discriminative port. (**Audio/ASR is now done** — see §2; the
284285
only container gap is AVI, which symphonia cannot demux.)
285-
- **XML DocLang / DocTags *input* backend** — DocLang is supported as an
286-
**output** format (§3), but reading `.dclx`/DocTags *back in* is not: no such
287-
sources in the corpus to verify against, and not in the requested scope.
288286

289287
**Now migrated (previously listed here):**
290288

289+
- **XML DocLang input backend.** Reading `.dclg`/`.dclg.xml` (bare DocLang XML)
290+
and `.dclx` archives back into a `DoclingDocument` — the corpus gap closed
291+
itself once `--to dclx` shipped: docling's own `.dclx` groundtruth archives
292+
are the sources, and docling 2.112 reads them natively (`InputFormat.DCLX`),
293+
so the backend is scored live like every other format — **15/15 exact**,
294+
reproducing docling's own round-trip semantics (whitespace collapse vs
295+
verbatim CDATA/`<content>`, span text re-expansion, the formatting docling
296+
drops on list items and hyperlink targets).
297+
291298
- **DOCX grouped/anchored drawings and floating text frames.** Blip-less
292299
DrawingML shapes yield docling's one-rendered-picture-per-paragraph as a
293300
placeholder (docling rasterizes them through LibreOffice; the Markdown
@@ -355,12 +362,11 @@ deliberate scope boundary or a cosmetic, single-fixture polish gap.
355362
- **Snapshot harness**`scripts/conformance/pdf_conformance.sh` regenerates and diffs the
356363
PDF/image/METS baseline (needs pdfium + the ONNX models; **91/91 exact**).
357364
- **Conformance**`scripts/conformance/conformance.sh <fmt>` scores a format against the
358-
latest published docling (installed from PyPI; see
359-
[`COMPARING.md`](./COMPARING.md)).
365+
latest published docling (installed from PyPI; how-to in §9).
360366
- **Differential / perf**`scripts/conformance/compare.sh`, `scripts/test/performance.sh`.
361367
The PDF pipeline's profiling data, the INT8/SIMD optimization results
362368
(4.3× warm vs Python docling on the ML pipeline), and the remaining
363-
performance backlog live in [`PDF_PERFORMANCE.md`](./PDF_PERFORMANCE.md).
369+
performance backlog live in [`PDF_CONFORMANCE.md`](./PDF_CONFORMANCE.md).
364370

365371
CI (`.github/workflows/ci.yml`) gates every pull request and master push on
366372
`cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings` and
@@ -416,6 +422,175 @@ process instead of a guess:
416422
4. **New formats/features** follow the same recipe the existing 20 formats
417423
did: a backend module + fixtures + conformance scoring, tracked in §2.
418424

425+
### Running the comparison yourself
426+
427+
The yardstick is **Markdown output**: both projects expose the same operation —
428+
`convert(file).document.export_to_markdown()` — so diffing the two Markdown
429+
strings is a direct, apples-to-apples comparison. Two axes: **correctness**
430+
(A, B) and **performance** (C). Current numbers live in §2; this section is
431+
how to reproduce them.
432+
433+
#### Local docling setup
434+
435+
The comparison scripts install the **latest published** `docling` from PyPI into
436+
an isolated `docling.rs/.venv-compare` (via `uv`) on first run:
437+
438+
```bash
439+
scripts/conformance/setup-docling.sh # optional; the other scripts call this automatically
440+
```
441+
442+
Published docling 2.x bundles every format backend plus the full PDF pipeline
443+
(torch + models), so the first install pulls a few hundred MB. For the
444+
declarative formats the Python side still calls the format backend directly (see
445+
`scripts/conformance/docling_convert.py`) rather than `DocumentConverter`, so it avoids
446+
paying the `torch` import cost on every run — the same conversion work, kept
447+
apples-to-apples with what `docling.rs` does.
448+
449+
### A. Scoring against docling across a corpus
450+
451+
This repo ships a regression corpus under `tests/data/<format>/`:
452+
453+
```text
454+
tests/data/html/sources/example_01.html # input
455+
tests/data/html/groundtruth/example_01.html.md # older committed reference
456+
```
457+
458+
`conformance.sh` scores the Rust port against the **latest published docling**
459+
(installed from PyPI on first run — see `_common.sh`), per format:
460+
461+
```bash
462+
scripts/conformance/conformance.sh html
463+
scripts/conformance/conformance.sh docx
464+
```
465+
466+
It prints a per-fixture diff-line count and a summary:
467+
468+
```text
469+
FIXTURE DIFF-LINES
470+
example_01.html 5
471+
example_02.html EXACT
472+
...
473+
Exact (strict): 10 / 32
474+
Whitespace-normalized matches: 12 / 32
475+
```
476+
477+
The second metric ignores spacing-only differences (collapsing runs of
478+
whitespace, trimming line ends) — useful when our output is the more faithful
479+
one, e.g. dropping docling's spurious double space in a fraction. A row that
480+
matches only after normalization is flagged `N (ws-ok)`.
481+
482+
> The reference is always the installed docling. The committed groundtruth `.md`
483+
> is used only as a fallback for sources docling can't convert — it predates
484+
> docling-core's current serializer (e.g. its compact `| - |` tables), so it is
485+
> not the source of truth.
486+
487+
### B. Live, head-to-head on any file
488+
489+
To compare on a file that isn't in the corpus — or to confirm the groundtruth
490+
hasn't drifted — run both implementations and diff:
491+
492+
```bash
493+
scripts/conformance/compare.sh tests/data/html/sources/example_03.html
494+
scripts/conformance/compare.sh /path/to/your/own.html
495+
```
496+
497+
`compare.sh` runs the local Python docling backend and the Rust CLI on the same
498+
file, normalizes trailing newlines, and shows a unified diff (or `✅ IDENTICAL`).
499+
The local docling install is set up automatically on first run (see above).
500+
501+
Do it by hand if you prefer:
502+
503+
```bash
504+
# Python (using the local install in .venv-compare)
505+
.venv-compare/bin/python scripts/conformance/docling_convert.py in.html > py.md
506+
507+
# Rust
508+
cargo run -p docling-cli -- in.html > rs.md
509+
510+
diff -u py.md rs.md
511+
```
512+
513+
### C. Performance (time, CPU, memory)
514+
515+
`scripts/test/performance.sh` measures the processing cost of each engine on one
516+
file — wall-clock time, CPU utilization, and peak resident memory — using GNU
517+
`/usr/bin/time`. The Rust side is built in `--release`; the Python side runs the
518+
installed docling (declarative backends, no `torch` import).
519+
520+
```bash
521+
scripts/test/performance.sh tests/data/html/sources/wiki_duck.html 10 # 10 runs
522+
```
523+
524+
```text
525+
================ end-to-end (whole process) ================
526+
ENGINE RUNS TIME-min TIME-avg CPU PEAK-MEM
527+
docling (python) 6 1.39s 1.41s 363% 125.5 MB
528+
docling.rs (rust) 6 0.00755s 0.00755s 100% 4.8 MB
529+
530+
wall-time speedup (avg): 186.8x faster (rust)
531+
peak-memory ratio: 26.4x less (rust)
532+
533+
================ conversion only (startup excluded) ========
534+
python (warm, in-process): 0.4736s/doc, peak 134.6 MB
535+
rust (whole process incl. startup): 0.00755s/doc — startup is negligible
536+
warm-conversion speedup: 62.7x faster (rust)
537+
```
538+
539+
**Reading the numbers fairly.** The end-to-end Python time includes interpreter
540+
startup plus importing docling/beautifulsoup4/numpy (~0.3–0.6s), which dominates
541+
on small inputs — a real cost for one-shot CLI use, but not representative of a
542+
long-running service. The script therefore also reports a **warm** number:
543+
Python imports once, then converts in a loop, isolating the actual parse work.
544+
Rust's process startup is ~1 ms, so its end-to-end figure already *is* its warm
545+
figure. Use larger inputs (e.g. `wiki_duck.html`) to see steady-state behavior;
546+
tiny files mostly measure Python's startup.
547+
548+
### Worked example
549+
550+
`tests/data/html/sources/example_01.html` → Python (left) vs Rust (right):
551+
552+
```diff
553+
# Introduction
554+
555+
This is the first paragraph of the introduction.
556+
557+
## Background
558+
559+
Some background information here.
560+
561+
Example image
562+
563+
<!-- image -->
564+
565+
- First item in unordered list
566+
- Second item in unordered list
567+
568+
1. First item in ordered list
569+
2. Second item in ordered list
570+
-
571+
- 42. First item in ordered list with start
572+
- 43. Second item in ordered list with start
573+
+ 3. First item in ordered list with start
574+
+ 4. Second item in ordered list with start
575+
```
576+
577+
Headings, paragraphs, the image placeholder, unordered list, and the first
578+
ordered list are byte-identical. The only difference is the `<ol start="42">`
579+
case — see the divergence table below.
580+
581+
### How to read the numbers
582+
583+
`conformance.sh` counts **diff lines** (`diff` `<`/`>` markers): one changed line
584+
shows as `2`. It reports two summary counts — **Exact (strict)** byte-for-byte and
585+
**Whitespace-normalized matches** (spacing-only diffs ignored; a fixture that
586+
matches only after normalization is flagged `N (ws-ok)`). The point isn't the
587+
absolute score — it's the trend as gaps in the table get closed, and catching
588+
regressions when a change makes a previously-matching fixture diverge.
589+
590+
For CI, gate on the summary (e.g. fail if the exact-match count drops): it
591+
compares against the docling version actually installed, so it won't flag
592+
differences that are really just a stale committed corpus.
593+
419594
What this cannot absorb automatically: upstream features that need new model
420595
*architectures* (VLM pipeline, enrichment models — out of scope per §5) and
421596
places where the document models intentionally differ (§4). Those are

0 commit comments

Comments
 (0)