parsearena 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. parsearena-0.1.3/.gitignore +14 -0
  2. parsearena-0.1.3/CHANGELOG.md +52 -0
  3. parsearena-0.1.3/LICENSE +21 -0
  4. parsearena-0.1.3/PKG-INFO +267 -0
  5. parsearena-0.1.3/README.es.md +243 -0
  6. parsearena-0.1.3/README.md +237 -0
  7. parsearena-0.1.3/pyproject.toml +64 -0
  8. parsearena-0.1.3/src/parsearena/__init__.py +3 -0
  9. parsearena-0.1.3/src/parsearena/cli.py +288 -0
  10. parsearena-0.1.3/src/parsearena/demo/pdfs/NOTICE.md +36 -0
  11. parsearena-0.1.3/src/parsearena/demo/pdfs/census-income-2023.pdf +0 -0
  12. parsearena-0.1.3/src/parsearena/demo/pdfs/dpo-arxiv-2305.18290.pdf +0 -0
  13. parsearena-0.1.3/src/parsearena/demo/pdfs/naca-report-1135-scan.pdf +0 -0
  14. parsearena-0.1.3/src/parsearena/demo/questions.yaml +104 -0
  15. parsearena-0.1.3/src/parsearena/demo/recorded.json +304 -0
  16. parsearena-0.1.3/src/parsearena/errors.py +14 -0
  17. parsearena-0.1.3/src/parsearena/evaluate.py +130 -0
  18. parsearena-0.1.3/src/parsearena/llm.py +154 -0
  19. parsearena-0.1.3/src/parsearena/parsers.py +197 -0
  20. parsearena-0.1.3/src/parsearena/pipeline.py +276 -0
  21. parsearena-0.1.3/src/parsearena/questions.py +125 -0
  22. parsearena-0.1.3/src/parsearena/render.py +40 -0
  23. parsearena-0.1.3/src/parsearena/report.py +57 -0
  24. parsearena-0.1.3/src/parsearena/report_template.html +450 -0
  25. parsearena-0.1.3/src/parsearena/retrieval.py +197 -0
  26. parsearena-0.1.3/src/parsearena/runners/_common.py +148 -0
  27. parsearena-0.1.3/src/parsearena/runners/run_docling.py +25 -0
  28. parsearena-0.1.3/src/parsearena/runners/run_marker.py +54 -0
  29. parsearena-0.1.3/src/parsearena/runners/run_mineru.py +59 -0
  30. parsearena-0.1.3/src/parsearena/runners/run_pypdf.py +22 -0
  31. parsearena-0.1.3/src/parsearena/runners/run_unstructured.py +38 -0
  32. parsearena-0.1.3/src/parsearena/text.py +90 -0
  33. parsearena-0.1.3/tests/conftest.py +32 -0
  34. parsearena-0.1.3/tests/fixtures/run_fake.py +40 -0
  35. parsearena-0.1.3/tests/test_cli.py +79 -0
  36. parsearena-0.1.3/tests/test_evaluate.py +69 -0
  37. parsearena-0.1.3/tests/test_llm.py +128 -0
  38. parsearena-0.1.3/tests/test_pipeline.py +156 -0
  39. parsearena-0.1.3/tests/test_questions.py +94 -0
  40. parsearena-0.1.3/tests/test_retrieval.py +65 -0
  41. parsearena-0.1.3/tests/test_text.py +48 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
10
+ parsearena-out/
11
+ parsearena-demo/
12
+ runs/
13
+ _site/
14
+ .DS_Store
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## [0.1.3] - 2026-11-03
4
+
5
+ ### Fixed
6
+ - Parser helper processes no longer outlive a run. Marker 2.x starts Surya inference servers
7
+ (layout, OCR error, llama.cpp) in their own sessions, and they kept running after `parsearena run`
8
+ finished. Runners now call Marker's `shutdown_models()` and stop every descendant process on exit,
9
+ and `run` stops the runner's whole process tree on timeouts and crashes.
10
+
11
+ ## [0.1.2] - 2026-11-03
12
+
13
+ ### Added
14
+ - `src/parsearena/demo/pdfs/NOTICE.md` with title, authors, source, license and changes for each
15
+ bundled demo PDF, including the full CC BY 4.0 attribution for the DPO paper. It ships in the wheel.
16
+ - README section "Third-party documents" (EN/ES) pointing to it.
17
+
18
+ ### Changed
19
+ - The default Mistral model for `--gen-questions` is now `ministral-8b-latest` (was `mistral-small-latest`).
20
+ - README notes the live `--gen-questions` tests with `ministral-8b-latest`.
21
+
22
+ ## [0.1.1] - 2026-11-03
23
+
24
+ ### Fixed
25
+ - Hybrid retrieval: BM25 chunks with a score of 0 no longer take part in the fusion with a position
26
+ taken from their place in the corpus, and tied scores now share a rank. Recall@5 of the demo is
27
+ unchanged; some MRR values move by 0.02 to 0.03.
28
+ - `--gen-questions`: a timeout is reported as a timeout, not as an unreachable server
29
+ (`PARSEARENA_LLM_TIMEOUT` sets the limit).
30
+
31
+ ### Changed
32
+ - Embeddings are cached on disk by model and text hash: a second `parsearena demo` takes about 2 s
33
+ instead of about 40 s on a busy machine.
34
+ - The first download of the embedding model prints one line instead of Hugging Face progress bars
35
+ and warnings.
36
+ - README and viewer explain why hybrid can score below BM25 and dense, and give the real first-run
37
+ time of the quickstart.
38
+
39
+ ## [0.1.0] - 2026-11-03
40
+
41
+ First release.
42
+
43
+ - `parsearena run`: Docling 2.146.0, Marker 2.1.0, MinerU 4.1.6, Unstructured 0.27.36 and a pypdf 6.21.4
44
+ baseline, each in its own uv environment, run as a subprocess with pinned versions.
45
+ - Per-page outputs, time per page (model loading reported apart as warm-up) and failures.
46
+ - One chunker for every output; retrieval with BM25, local fastembed embeddings and their fusion:
47
+ answer survives parsing, recall@k and MRR.
48
+ - Questions from a YAML file or drafted with an LLM (`--gen-questions`, Ollama, OpenRouter, OpenAI or
49
+ Mistral) with a `--max-cost` cap.
50
+ - Static HTML viewer: page image next to any two parser outputs, metrics, timings and failures,
51
+ English and Spanish, light and dark.
52
+ - `parsearena demo`: recorded outputs on three public PDFs, retrieval computed live.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Diego Pérez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.5
2
+ Name: parsearena
3
+ Version: 0.1.3
4
+ Summary: Run your PDFs through Docling, Marker, MinerU and Unstructured. Compare the output side by side and see what each one does to retrieval.
5
+ Project-URL: Homepage, https://diegoperezg7.github.io/parsearena/
6
+ Project-URL: Repository, https://github.com/diegoperezg7/parsearena
7
+ Project-URL: Issues, https://github.com/diegoperezg7/parsearena/issues
8
+ Author: Diego Pérez
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: benchmark,docling,marker,mineru,parsing,pdf,rag,retrieval,unstructured
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Text Processing
20
+ Requires-Python: <3.15,>=3.11
21
+ Requires-Dist: fastembed==0.9.1
22
+ Requires-Dist: numpy==2.3.5
23
+ Requires-Dist: openai==3.42.1
24
+ Requires-Dist: pillow==13.0.0
25
+ Requires-Dist: pypdfium2==5.14.0
26
+ Requires-Dist: pyyaml==6.0.3
27
+ Requires-Dist: rich==15.0.0
28
+ Requires-Dist: typer==0.27.3
29
+ Description-Content-Type: text/markdown
30
+
31
+ <p align="center">
32
+ <img src="docs/assets/banner.svg" alt="parsearena" width="640">
33
+ </p>
34
+
35
+ <p align="center">
36
+ <a href="https://github.com/diegoperezg7/parsearena/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/diegoperezg7/parsearena/actions/workflows/ci.yml/badge.svg"></a>
37
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-0064d2"></a>
38
+ <a href="https://github.com/diegoperezg7/parsearena/releases"><img alt="Release" src="https://img.shields.io/github/v/release/diegoperezg7/parsearena?color=0064d2"></a>
39
+ <a href="https://github.com/diegoperezg7/parsearena/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/diegoperezg7/parsearena?color=0064d2"></a>
40
+ </p>
41
+
42
+ <p align="center"><b>Run your PDFs through Docling, Marker, MinerU and Unstructured. Compare the output side by side and see what each one does to retrieval.</b></p>
43
+
44
+ <p align="center"><a href="README.es.md">Leer en español</a> · <a href="https://diegoperezg7.github.io/parsearena/">Example viewer</a></p>
45
+
46
+ <p align="center">
47
+ <img src="docs/assets/demo.gif" alt="parsearena demo in the terminal" width="760">
48
+ </p>
49
+
50
+ <p align="center">
51
+ <a href="https://diegoperezg7.github.io/parsearena/"><img src="docs/assets/viewer-light.png" alt="The parsearena viewer: page image next to two parser outputs" width="860"></a>
52
+ </p>
53
+
54
+ ## Why
55
+
56
+ Public parser benchmarks score parsers on their own fixed corpus:
57
+ [ParseBench](https://github.com/run-llama/ParseBench) (625 stars, by LlamaIndex) and
58
+ [OmniDocBench](https://github.com/opendatalab/OmniDocBench) (2,146 stars). That tells you which parser
59
+ wins on their documents. It does not tell you what happens to *your* invoices, papers or scanned
60
+ reports. Harnesses that run several parsers on your own PDFs exist only as 0-star repositories
61
+ (for example [ticketvipers/pdf-parser-comparison](https://github.com/ticketvipers/pdf-parser-comparison)
62
+ and [xvalovic/document-parser-benchmark](https://github.com/xvalovic/document-parser-benchmark)).
63
+
64
+ parsearena works on your documents. It shows each parser's output next to the original page, and it
65
+ measures the thing you actually care about in a RAG pipeline: after parsing and chunking, can the
66
+ retriever still find the answer?
67
+
68
+ ## Quickstart
69
+
70
+ The demo replays outputs recorded from a real run and computes retrieval live. No keys, no GPU,
71
+ no Docker, no parser downloads.
72
+
73
+ ```bash
74
+ uvx --from git+https://github.com/diegoperezg7/parsearena@v0.1.3 parsearena demo
75
+ ```
76
+
77
+ or from a clone:
78
+
79
+ ```bash
80
+ git clone https://github.com/diegoperezg7/parsearena && cd parsearena && uv run parsearena demo
81
+ ```
82
+
83
+ It writes `parsearena-demo/index.html` and opens it. The first run takes about 75 s (dependencies and a
84
+ 65 MB embedding model); later runs take about 2 s because computed embeddings are cached. `uvx parsearena` is coming to PyPI.
85
+
86
+ Then run it on your own PDFs:
87
+
88
+ ```bash
89
+ parsearena run docs/*.pdf --questions questions.yaml
90
+ parsearena run docs/*.pdf --gen-questions # questions drafted by a local Ollama model
91
+ parsearena run docs/*.pdf --parsers docling,pypdf # any subset
92
+ ```
93
+
94
+ ## Example output
95
+
96
+ Recorded on 3 Nov 2026 on an Apple M4 with 16 GB of RAM (macOS 27.2), three public PDFs (8 pages), 24 hand-written questions,
97
+ recall@5 with hybrid retrieval (BM25 + `BAAI/bge-small-en-v1.5`, fused with reciprocal rank):
98
+
99
+ | Parser | Answer survives | Recall@5 BM25 | Recall@5 dense | Recall@5 hybrid | MRR hybrid | s / page |
100
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
101
+ | Docling 2.146.0 | 88% | 67% | 67% | 63% | 0.55 | 11.8 |
102
+ | Marker 2.1.0 | **96%** | **79%** | **67%** | **75%** | **0.63** | 21.0 |
103
+ | MinerU 4.1.6 | **96%** | 63% | 58% | 58% | 0.52 | 7.6 |
104
+ | Unstructured 0.27.36 | 92% | 67% | 54% | 54% | 0.52 | 6.2 |
105
+ | pypdf 6.21.4 (baseline) | 75% | 58% | 46% | 54% | 0.40 | 0.02 |
106
+
107
+ What the numbers say on these pages:
108
+
109
+ - **The scanned page is where the baseline breaks.** pypdf has no text to read on the NACA page and
110
+ loses all 6 of its answers. The four parsers OCR it; Marker and MinerU keep all 6.
111
+ - **Charts and fonts.** On the Census chart page pypdf returns glyph names instead of digits
112
+ (`/dollar.tab/eight.tab/zero.tab...`), so the chart values only survive through the table page.
113
+ - **Tables survive, but not always in a retrievable shape.** MinerU keeps every number of Table A-1,
114
+ yet it merges several row labels into one HTML cell. With the same chunker, the label and its value
115
+ end up apart: the chunk with the numbers shares no word with the question, so BM25 never retrieves
116
+ it, dense retrieval ranks it 7th to 16th, and the fused list puts it 31st to 37th. The other parsers
117
+ mostly keep those questions in the top 10.
118
+ - **Page furniture.** Docling and Marker drop the first-page footer, so "NeurIPS 2023" is gone from
119
+ their output. Unstructured merges the DPO results table with the body text around it and loses its
120
+ 0.36 cell.
121
+ - **Speed.** pypdf parses a page in 0.02 s. The model-based parsers take 6 to 21 s per page on this
122
+ machine, plus 20 to 100 s to load their models.
123
+
124
+ Eight pages and 24 questions are a smoke test, not a ranking: run it on your own documents.
125
+
126
+ Open the [example viewer](https://diegoperezg7.github.io/parsearena/) to check each of these against
127
+ the page image.
128
+
129
+ ## How it works
130
+
131
+ ```mermaid
132
+ flowchart LR
133
+ P[Your PDFs] --> R1
134
+ subgraph envs[One uv environment per parser]
135
+ R1[Docling] ~~~ R2[Marker] ~~~ R3[MinerU] ~~~ R4[Unstructured] ~~~ R5[pypdf]
136
+ end
137
+ envs -->|markdown per page, seconds, errors| O[outputs.json]
138
+ O --> C[Same chunker for all]
139
+ Q[questions.yaml or --gen-questions] --> E
140
+ C --> E[BM25 + fastembed + fusion]
141
+ E --> M[answer survives, recall@k, MRR]
142
+ M --> V[index.html viewer]
143
+ P -->|pypdfium2| V
144
+ ```
145
+
146
+ 1. **Isolated parsers.** Each parser is installed with pinned versions into its own virtual
147
+ environment under `~/.cache/parsearena/envs` and driven through a small runner script in a
148
+ subprocess. Their torch versions never meet, their licenses stay separate, and parsearena never
149
+ bundles any of them. Every parser runs with its default settings.
150
+ 2. **Timing.** Each runner first converts a two-page warm-up PDF (one text page, one image-only page)
151
+ so that model loading is reported apart. The seconds per page in the table are parsing only.
152
+ Device is `auto`: CUDA or Apple MPS when torch sees them, CPU otherwise (`--device cpu` to force).
153
+ 3. **One chunker.** Every output goes through the same paragraph packer (1,200 characters by default,
154
+ padding spaces and markdown table rules removed), so differences come from the parser.
155
+ 4. **Retrieval.** For each question, chunks from *all* documents are ranked with BM25, with local
156
+ embeddings ([fastembed](https://github.com/qdrant/fastembed), ONNX, no torch) and with their
157
+ reciprocal-rank fusion. Embeddings are cached by model and text under `~/.cache/parsearena/models`. A chunk is relevant if it belongs to the question's document and contains
158
+ the answer string.
159
+ Hybrid can score below both of its parts: fusion rewards chunks that both methods rank well, so a
160
+ chunk that only one of them finds (a table cell with none of the query words, say) drops in the
161
+ fused list. BM25 chunks with a score of 0 are treated as not retrieved, and tied scores share a rank.
162
+ 5. **Metrics.** *Answer survives*: the answer string is still in the parser output after
163
+ normalization (case, whitespace, markdown and HTML markup). *Recall@k*: a relevant chunk is in the
164
+ top k. *MRR*: reciprocal rank of the first relevant chunk, cut at 10.
165
+
166
+ ## Configuration
167
+
168
+ | Option | Default | What it does |
169
+ | --- | --- | --- |
170
+ | `--parsers` | `docling,marker,mineru,unstructured,pypdf` | Parsers to run |
171
+ | `--questions FILE` | | YAML question set (`parsearena questions-example` prints the format) |
172
+ | `--gen-questions` | off | Draft questions with an LLM from the PDF text layer; keeps only answers found verbatim |
173
+ | `--provider` / `--model` | `ollama` / `qwen3:4b` | `ollama`, `openrouter`, `openai` or `mistral` |
174
+ | `--max-cost` | `1.0` | USD cap for LLM calls, checked before every call. Local models cost 0 |
175
+ | `--k` | `5` | k for recall@k |
176
+ | `--device` | `auto` | `auto`, `cpu`, `mps` or `cuda` |
177
+ | `--chunk-chars` | `1200` | Chunk size |
178
+ | `--embed-model` / `--no-dense` | `BAAI/bge-small-en-v1.5` | fastembed model, or BM25 only |
179
+ | `--timeout` | `3600` | Seconds per parser |
180
+ | `--out` | `parsearena-out` | Output directory (viewer, `outputs.json`, `results.json`, logs) |
181
+
182
+ Environment variables: `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, `MISTRAL_API_KEY` (only for
183
+ `--gen-questions` with that provider), `OLLAMA_HOST`, `PARSEARENA_HOME` (where environments live),
184
+ `LLAMA_CPP_BINARY`, `PARSEARENA_LLM_TIMEOUT` (seconds per LLM call, default 300). See [.env.example](.env.example). `parsearena parsers` lists pinned versions and
185
+ what is installed.
186
+
187
+ `--gen-questions` was tested live on 3 Nov 2026 with `--provider mistral --model ministral-8b-latest`
188
+ on two demo PDFs: 9 questions kept out of 10 asked, all sensible and with answers found verbatim in the
189
+ text, for about $0.001. Review the drafted file before trusting it: one answer was just `β`, too short
190
+ to be a useful retrieval target. `--gen-questions` was also tested live with mistral/ministral-8b-latest on census-income-2023.pdf:
191
+ 4 questions, all grounded, $0.0006. `ministral-8b-latest` is the default Mistral model.
192
+
193
+ Questions file:
194
+
195
+ ```yaml
196
+ questions:
197
+ - doc: report.pdf
198
+ q: What was the median household income in 2023?
199
+ answer: "80,610"
200
+ - doc: paper.pdf
201
+ q: Which preference model makes the partition function cancel?
202
+ answers: ["Bradley-Terry", "Bradley Terry"]
203
+ ```
204
+
205
+ ## What a real run costs
206
+
207
+ Be ready for this the first time:
208
+
209
+ - **Downloads.** Each parser environment is 1.1 to 1.4 GB, and the first parse downloads its models
210
+ from Hugging Face. On the machine below, the four parser environments plus models took about
211
+ 9 (5.2 GB of environments and 3.5 GB of models) of disk.
212
+ - **System tools.** Unstructured's `hi_res` strategy needs `poppler` and `tesseract`. Marker 2.x runs
213
+ its OCR model through llama.cpp when there is no NVIDIA GPU: install it (`brew install llama.cpp`)
214
+ or point `LLAMA_CPP_BINARY` at a `llama-server` binary. parsearena warns before running a parser
215
+ whose tools are missing.
216
+ - **Time.** Measured on an Apple M4 with 16 GB of RAM (macOS 27.2), with the models already downloaded:
217
+
218
+ | Parser | Device | Model loading (s) | Seconds per page | Slowest document |
219
+ | --- | --- | ---: | ---: | --- |
220
+ | Docling | MPS | 38 | 11.8 | scanned page, 77 s |
221
+ | Marker | MPS + llama.cpp (Metal) | 99 | 21.0 | scanned page, 107 s |
222
+ | MinerU | MPS | 20 | 7.6 | DPO paper (equations), 43 s for 4 pages |
223
+ | Unstructured | CPU (ONNX) | 22 | 6.2 | Census, 21 s for 3 pages |
224
+ | pypdf | CPU | 0.1 | 0.02 | |
225
+
226
+ The whole `run` over the 8 demo pages took about 10 minutes, one parser at a time.
227
+
228
+ ## Parsers and their licenses
229
+
230
+ parsearena is MIT. It installs the parsers on your machine on demand and never redistributes them;
231
+ their licenses apply to your use of them.
232
+
233
+ | Parser | Version | License |
234
+ | --- | --- | --- |
235
+ | [Docling](https://github.com/docling-project/docling) | 2.146.0 | MIT |
236
+ | [Marker](https://github.com/datalab-to/marker) | 2.1.0 | Code Apache-2.0. Model weights under a modified OpenRAIL-M: free for research, personal use and startups under 5M USD |
237
+ | [MinerU](https://github.com/opendatalab/MinerU) | 4.1.6 | Apache-2.0 with extra terms: a commercial license above 100M monthly active users or 20M USD monthly revenue, and attribution for online services |
238
+ | [Unstructured](https://github.com/Unstructured-IO/unstructured) | 0.27.36 (`hi_res`) | Apache-2.0 |
239
+ | [pypdf](https://github.com/py-pdf/pypdf) (baseline) | 6.21.4 | BSD-3-Clause |
240
+
241
+ MinerU runs at its `medium` effort (local layout, OCR, table and formula models, no vision-language
242
+ model) with its telemetry disabled (`MINERU_TELEMETRY_DISABLED=1`). parsearena itself has no telemetry.
243
+
244
+ ## Third-party documents
245
+
246
+ | File | Pages used | License |
247
+ | --- | --- | --- |
248
+ | [U.S. Census Bureau, *Income in the United States: 2023* (P60-282)](https://www.census.gov/library/publications/2024/demo/p60-282.html) | 1, 2 (chart), 15 (Table A-1) | Public domain (U.S. government work) |
249
+ | [Rafailov et al., *Direct Preference Optimization* (arXiv:2305.18290v3)](https://arxiv.org/abs/2305.18290) | 1, 4, 5 (equations), 9 (table) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
250
+ | [NACA Report 1135, *Equations, Tables, and Charts for Compressible Flow* (1953)](https://ntrs.nasa.gov/citations/19930091059) | p. 613, rasterized to an image-only page | Public domain (U.S. government work) |
251
+
252
+ The pages were extracted unchanged, except the NACA page, which was re-rendered as an image so that it
253
+ has no text layer, like a real scan. These excerpts are not covered by parsearena's MIT license. Full
254
+ titles, authors, sources, licenses and the CC BY attribution are in
255
+ [`src/parsearena/demo/pdfs/NOTICE.md`](src/parsearena/demo/pdfs/NOTICE.md), which ships in the wheel. The question set is in
256
+ [`src/parsearena/demo/questions.yaml`](src/parsearena/demo/questions.yaml).
257
+
258
+ ## Roadmap
259
+
260
+ - Cloud parsers: LlamaParse, Mistral OCR, Azure Document Intelligence
261
+ - Vision-language model parsers (and MinerU's VLM efforts)
262
+ - Table structure metrics (TEDS)
263
+ - docx and pptx input
264
+
265
+ ## License
266
+
267
+ [MIT](LICENSE). Contributions welcome, see [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,243 @@
1
+ <p align="center">
2
+ <img src="docs/assets/banner.svg" alt="parsearena" width="640">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/diegoperezg7/parsearena/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/diegoperezg7/parsearena/actions/workflows/ci.yml/badge.svg"></a>
7
+ <a href="LICENSE"><img alt="Licencia: MIT" src="https://img.shields.io/badge/license-MIT-0064d2"></a>
8
+ <a href="https://github.com/diegoperezg7/parsearena/releases"><img alt="Release" src="https://img.shields.io/github/v/release/diegoperezg7/parsearena?color=0064d2"></a>
9
+ <a href="https://github.com/diegoperezg7/parsearena/stargazers"><img alt="Estrellas" src="https://img.shields.io/github/stars/diegoperezg7/parsearena?color=0064d2"></a>
10
+ </p>
11
+
12
+ <p align="center"><b>Pasa tus PDF por Docling, Marker, MinerU y Unstructured. Compara las salidas lado a lado y mira qué le hace cada uno a la recuperación.</b></p>
13
+
14
+ <p align="center"><a href="README.md">Read in English</a> · <a href="https://diegoperezg7.github.io/parsearena/">Visor de ejemplo</a></p>
15
+
16
+ <p align="center">
17
+ <img src="docs/assets/demo.gif" alt="Demo de parsearena en la terminal" width="760">
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://diegoperezg7.github.io/parsearena/"><img src="docs/assets/viewer-light.png" alt="El visor de parsearena: la página junto a dos salidas de parsers" width="860"></a>
22
+ </p>
23
+
24
+ ## Por qué
25
+
26
+ Los benchmarks públicos de parsers puntúan sobre su propio corpus fijo:
27
+ [ParseBench](https://github.com/run-llama/ParseBench) (625 estrellas, de LlamaIndex) y
28
+ [OmniDocBench](https://github.com/opendatalab/OmniDocBench) (2.146 estrellas). Te dicen qué parser gana
29
+ con sus documentos, no qué pasa con *tus* facturas, papers o informes escaneados. Los arneses que
30
+ pasan varios parsers por tus propios PDF solo existen como repos con 0 estrellas (por ejemplo
31
+ [ticketvipers/pdf-parser-comparison](https://github.com/ticketvipers/pdf-parser-comparison) y
32
+ [xvalovic/document-parser-benchmark](https://github.com/xvalovic/document-parser-benchmark)).
33
+
34
+ parsearena trabaja con tus documentos. Pone la salida de cada parser junto a la página original y
35
+ mide lo que de verdad importa en un RAG: después de parsear y trocear, ¿el recuperador sigue
36
+ encontrando la respuesta?
37
+
38
+ ## Inicio rápido
39
+
40
+ La demo reproduce salidas grabadas de una ejecución real y calcula la recuperación en vivo. Sin
41
+ claves, sin GPU, sin Docker y sin descargar parsers.
42
+
43
+ ```bash
44
+ uvx --from git+https://github.com/diegoperezg7/parsearena@v0.1.3 parsearena demo
45
+ ```
46
+
47
+ o desde un clon:
48
+
49
+ ```bash
50
+ git clone https://github.com/diegoperezg7/parsearena && cd parsearena && uv run parsearena demo
51
+ ```
52
+
53
+ Escribe `parsearena-demo/index.html` y lo abre. La primera vez tarda unos 75 s (dependencias y un
54
+ modelo de embeddings de 65 MB); después, unos 2 s, porque los embeddings ya calculados se guardan en caché. `uvx parsearena` llegará con la publicación en PyPI.
55
+
56
+ Después, con tus PDF:
57
+
58
+ ```bash
59
+ parsearena run docs/*.pdf --questions preguntas.yaml
60
+ parsearena run docs/*.pdf --gen-questions # preguntas redactadas por un modelo local de Ollama
61
+ parsearena run docs/*.pdf --parsers docling,pypdf # cualquier subconjunto
62
+ ```
63
+
64
+ ## Resultado de ejemplo
65
+
66
+ Grabado el 3 nov 2026 en un Apple M4 con 16 GB de RAM (macOS 27.2), tres PDF públicos (8 páginas), 24 preguntas escritas a mano,
67
+ recall@5 con recuperación híbrida (BM25 + `BAAI/bge-small-en-v1.5`, fusionados por rango recíproco):
68
+
69
+ | Parser | Respuesta sobrevive | Recall@5 BM25 | Recall@5 densa | Recall@5 híbrida | MRR híbrida | s / página |
70
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
71
+ | Docling 2.146.0 | 88% | 67% | 67% | 63% | 0,55 | 11,8 |
72
+ | Marker 2.1.0 | **96%** | **79%** | **67%** | **75%** | **0,63** | 21,0 |
73
+ | MinerU 4.1.6 | **96%** | 63% | 58% | 58% | 0,52 | 7,6 |
74
+ | Unstructured 0.27.36 | 92% | 67% | 54% | 54% | 0,52 | 6,2 |
75
+ | pypdf 6,21.4 (línea base) | 75% | 58% | 46% | 54% | 0,40 | 0,02 |
76
+
77
+ Lo que dicen las cifras en estas páginas:
78
+
79
+ - **La página escaneada es donde la línea base se rompe.** pypdf no tiene texto que leer en la
80
+ página de NACA y pierde sus 6 respuestas. Los cuatro parsers hacen OCR; Marker y MinerU conservan
81
+ las 6.
82
+ - **Gráficos y fuentes.** En la página del gráfico del Census, pypdf devuelve nombres de glifos en vez
83
+ de dígitos (`/dollar.tab/eight.tab/zero.tab...`), así que esos valores solo sobreviven por la tabla.
84
+ - **Las tablas sobreviven, pero no siempre de forma recuperable.** MinerU conserva todos los números de
85
+ la tabla A-1, pero junta varias etiquetas de fila en una sola celda HTML. Con el mismo troceado, la
86
+ etiqueta y su valor quedan separados: el fragmento con los números no comparte ninguna palabra con
87
+ la pregunta, así que BM25 no lo recupera, la búsqueda densa lo pone entre el puesto 7 y el 16, y la
88
+ fusión lo deja entre el 31 y el 37. Los demás parsers dejan esas preguntas casi siempre entre los 10
89
+ primeros.
90
+ - **Pies de página.** Docling y Marker quitan el pie de la primera página, así que "NeurIPS 2023"
91
+ desaparece de su salida. Unstructured mezcla la tabla de resultados de DPO con el texto de alrededor
92
+ y pierde la celda 0.36.
93
+ - **Velocidad.** pypdf parsea una página en 0,02 s. Los parsers con modelos tardan entre 6 y 21 s por
94
+ página en esta máquina, más 20 a 100 s de carga de modelos.
95
+
96
+ Ocho páginas y 24 preguntas son una prueba de humo, no una clasificación: pásalo por tus documentos.
97
+
98
+ Abre el [visor de ejemplo](https://diegoperezg7.github.io/parsearena/) para comprobar cada punto contra
99
+ la imagen de la página.
100
+
101
+ ## Cómo funciona
102
+
103
+ ```mermaid
104
+ flowchart LR
105
+ P[Tus PDF] --> R1
106
+ subgraph envs[Un entorno uv por parser]
107
+ R1[Docling] ~~~ R2[Marker] ~~~ R3[MinerU] ~~~ R4[Unstructured] ~~~ R5[pypdf]
108
+ end
109
+ envs -->|markdown por página, segundos, errores| O[outputs.json]
110
+ O --> C[El mismo troceado para todos]
111
+ Q[questions.yaml o --gen-questions] --> E
112
+ C --> E[BM25 + fastembed + fusión]
113
+ E --> M[respuesta sobrevive, recall@k, MRR]
114
+ M --> V[visor index.html]
115
+ P -->|pypdfium2| V
116
+ ```
117
+
118
+ 1. **Parsers aislados.** Cada parser se instala con versiones fijadas en su propio entorno virtual
119
+ (`~/.cache/parsearena/envs`) y se ejecuta con un pequeño runner en un subproceso. Sus versiones de
120
+ torch no chocan, sus licencias no se mezclan y parsearena no empaqueta ninguno. Todos corren con su
121
+ configuración por defecto.
122
+ 2. **Tiempos.** Cada runner convierte primero un PDF de calentamiento de dos páginas (una con texto y
123
+ otra solo imagen) para que la carga de modelos se cuente aparte. Los segundos por página son solo
124
+ de parseo. El dispositivo es `auto`: CUDA o MPS de Apple si torch los ve, CPU si no
125
+ (`--device cpu` para forzarlo).
126
+ 3. **Un solo troceado.** Todas las salidas pasan por el mismo empaquetador de párrafos (1.200
127
+ caracteres por defecto, sin espacios de relleno ni reglas de tablas markdown), así que las
128
+ diferencias vienen del parser.
129
+ 4. **Recuperación.** Para cada pregunta se ordenan los fragmentos de *todos* los documentos con BM25,
130
+ con embeddings locales ([fastembed](https://github.com/qdrant/fastembed), ONNX, sin torch) y con su
131
+ fusión por rango recíproco. Los embeddings se guardan en caché por modelo y texto en `~/.cache/parsearena/models`. Un fragmento es relevante si es del documento de la pregunta y contiene
132
+ la respuesta.
133
+ La híbrida puede quedar por debajo de sus dos partes: la fusión premia los fragmentos que ambos
134
+ métodos ponen arriba, así que uno que solo encuentra un método (por ejemplo, una celda de tabla sin
135
+ ninguna palabra de la pregunta) baja en la lista fusionada. Los fragmentos con BM25 igual a 0 cuentan
136
+ como no recuperados, y las puntuaciones empatadas comparten posición.
137
+ 5. **Métricas.** *Respuesta sobrevive*: el texto de la respuesta sigue en la salida tras normalizar
138
+ (mayúsculas, espacios, marcado markdown y HTML). *Recall@k*: hay un fragmento relevante entre los k
139
+ primeros. *MRR*: rango recíproco del primer fragmento relevante, con corte en 10.
140
+
141
+ ## Configuración
142
+
143
+ | Opción | Por defecto | Qué hace |
144
+ | --- | --- | --- |
145
+ | `--parsers` | `docling,marker,mineru,unstructured,pypdf` | Parsers a ejecutar |
146
+ | `--questions FICHERO` | | Preguntas en YAML (`parsearena questions-example` muestra el formato) |
147
+ | `--gen-questions` | no | Redacta preguntas con un LLM a partir del texto del PDF; solo guarda respuestas literales |
148
+ | `--provider` / `--model` | `ollama` / `qwen3:4b` | `ollama`, `openrouter`, `openai` o `mistral` |
149
+ | `--max-cost` | `1.0` | Tope en USD para llamadas a LLM, comprobado antes de cada llamada. Los modelos locales cuestan 0 |
150
+ | `--k` | `5` | k de recall@k |
151
+ | `--device` | `auto` | `auto`, `cpu`, `mps` o `cuda` |
152
+ | `--chunk-chars` | `1200` | Tamaño de fragmento |
153
+ | `--embed-model` / `--no-dense` | `BAAI/bge-small-en-v1.5` | Modelo de fastembed, o solo BM25 |
154
+ | `--timeout` | `3600` | Segundos por parser |
155
+ | `--out` | `parsearena-out` | Carpeta de salida (visor, `outputs.json`, `results.json`, logs) |
156
+
157
+ Variables de entorno: `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, `MISTRAL_API_KEY` (solo para
158
+ `--gen-questions` con ese proveedor), `OLLAMA_HOST`, `PARSEARENA_HOME` (dónde viven los entornos),
159
+ `LLAMA_CPP_BINARY` y `PARSEARENA_LLM_TIMEOUT` (segundos por llamada al LLM, 300 por defecto). Mira [.env.example](.env.example). `parsearena parsers` muestra versiones y qué
160
+ está instalado.
161
+
162
+ `--gen-questions` se probó en vivo el 3 nov 2026 con `--provider mistral --model ministral-8b-latest`
163
+ sobre dos PDF de la demo: 9 preguntas guardadas de 10 pedidas, todas con sentido y con la respuesta
164
+ literal en el texto, por unos 0,001 USD. Revisa el fichero antes de fiarte: una respuesta era solo `β`,
165
+ demasiado corta para ser un buen objetivo de recuperación. `--gen-questions` también se probó en vivo con mistral/ministral-8b-latest sobre census-income-2023.pdf:
166
+ 4 preguntas, todas con la respuesta en el texto, 0,0006 USD. `ministral-8b-latest` es el modelo de Mistral por defecto.
167
+
168
+ Fichero de preguntas:
169
+
170
+ ```yaml
171
+ questions:
172
+ - doc: informe.pdf
173
+ q: What was the median household income in 2023?
174
+ answer: "80,610"
175
+ - doc: paper.pdf
176
+ q: Which preference model makes the partition function cancel?
177
+ answers: ["Bradley-Terry", "Bradley Terry"]
178
+ ```
179
+
180
+ ## Lo que cuesta una ejecución real
181
+
182
+ La primera vez, cuenta con esto:
183
+
184
+ - **Descargas.** Cada entorno de parser ocupa entre 1,1 y 1,4 GB, y el primer parseo descarga sus
185
+ modelos de Hugging Face. En la máquina de abajo, los cuatro entornos más los modelos ocuparon unos
186
+ 9 (5,2 GB de entornos y 3,5 GB de modelos).
187
+ - **Herramientas del sistema.** La estrategia `hi_res` de Unstructured necesita `poppler` y
188
+ `tesseract`. Marker 2.x ejecuta su modelo de OCR con llama.cpp cuando no hay GPU NVIDIA: instálalo
189
+ (`brew install llama.cpp`) o apunta `LLAMA_CPP_BINARY` a un binario `llama-server`. parsearena avisa
190
+ antes de ejecutar un parser al que le faltan herramientas.
191
+ - **Tiempo.** Medido en un Apple M4 con 16 GB de RAM (macOS 27.2), con los modelos ya descargados:
192
+
193
+ | Parser | Dispositivo | Carga de modelos (s) | Segundos por página | Documento más lento |
194
+ | --- | --- | ---: | ---: | --- |
195
+ | Docling | MPS | 38 | 11,8 | página escaneada, 77 s |
196
+ | Marker | MPS + llama.cpp (Metal) | 99 | 21,0 | página escaneada, 107 s |
197
+ | MinerU | MPS | 20 | 7,6 | paper de DPO (ecuaciones), 43 s para 4 páginas |
198
+ | Unstructured | CPU (ONNX) | 22 | 6,2 | Census, 21 s para 3 páginas |
199
+ | pypdf | CPU | 0,1 | 0,02 | |
200
+
201
+ El `run` completo sobre las 8 páginas de la demo tardó unos 10 minutos, un parser cada vez.
202
+
203
+ ## Parsers y sus licencias
204
+
205
+ parsearena es MIT. Instala los parsers en tu máquina cuando los usas y nunca los redistribuye; sus
206
+ licencias se aplican a tu uso.
207
+
208
+ | Parser | Versión | Licencia |
209
+ | --- | --- | --- |
210
+ | [Docling](https://github.com/docling-project/docling) | 2.146.0 | MIT |
211
+ | [Marker](https://github.com/datalab-to/marker) | 2.1.0 | Código Apache-2.0. Pesos con OpenRAIL-M modificada: gratis para investigación, uso personal y startups de menos de 5 M USD |
212
+ | [MinerU](https://github.com/opendatalab/MinerU) | 4.1.6 | Apache-2.0 con términos extra: licencia comercial por encima de 100 M de usuarios activos al mes o 20 M USD de ingresos mensuales, y atribución en servicios online |
213
+ | [Unstructured](https://github.com/Unstructured-IO/unstructured) | 0.27.36 (`hi_res`) | Apache-2.0 |
214
+ | [pypdf](https://github.com/py-pdf/pypdf) (línea base) | 6.21.4 | BSD-3-Clause |
215
+
216
+ MinerU corre con su esfuerzo `medium` (modelos locales de layout, OCR, tablas y fórmulas, sin modelo de
217
+ visión y lenguaje) y con su telemetría desactivada (`MINERU_TELEMETRY_DISABLED=1`). parsearena no tiene
218
+ telemetría.
219
+
220
+ ## Documentos de terceros
221
+
222
+ | Fichero | Páginas | Licencia |
223
+ | --- | --- | --- |
224
+ | [U.S. Census Bureau, *Income in the United States: 2023* (P60-282)](https://www.census.gov/library/publications/2024/demo/p60-282.html) | 1, 2 (gráfico), 15 (tabla A-1) | Dominio público (obra del gobierno de EE. UU.) |
225
+ | [Rafailov et al., *Direct Preference Optimization* (arXiv:2305.18290v3)](https://arxiv.org/abs/2305.18290) | 1, 4, 5 (ecuaciones), 9 (tabla) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
226
+ | [NACA Report 1135, *Equations, Tables, and Charts for Compressible Flow* (1953)](https://ntrs.nasa.gov/citations/19930091059) | p. 613, rasterizada a página solo imagen | Dominio público (obra del gobierno de EE. UU.) |
227
+
228
+ Las páginas se extrajeron sin cambios, salvo la de NACA, que se volvió a renderizar como imagen para
229
+ que no tenga capa de texto, como un escaneo real. Estos extractos no están bajo la licencia MIT de
230
+ parsearena. Títulos, autores, fuentes, licencias y la atribución CC BY completos están en
231
+ [`src/parsearena/demo/pdfs/NOTICE.md`](src/parsearena/demo/pdfs/NOTICE.md), que va dentro del wheel. Las preguntas están en
232
+ [`src/parsearena/demo/questions.yaml`](src/parsearena/demo/questions.yaml).
233
+
234
+ ## Hoja de ruta
235
+
236
+ - Parsers en la nube: LlamaParse, Mistral OCR, Azure Document Intelligence
237
+ - Parsers con modelos de visión y lenguaje (y los esfuerzos VLM de MinerU)
238
+ - Métricas de estructura de tablas (TEDS)
239
+ - Entrada docx y pptx
240
+
241
+ ## Licencia
242
+
243
+ [MIT](LICENSE). Las contribuciones son bienvenidas, mira [CONTRIBUTING.md](CONTRIBUTING.md).