universal-doc-parser 1.0.0__py3-none-any.whl

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 (48) hide show
  1. universal_doc_parser-1.0.0.dist-info/METADATA +692 -0
  2. universal_doc_parser-1.0.0.dist-info/RECORD +48 -0
  3. universal_doc_parser-1.0.0.dist-info/WHEEL +4 -0
  4. universal_doc_parser-1.0.0.dist-info/licenses/LICENSE +21 -0
  5. universal_parser/__init__.py +28 -0
  6. universal_parser/adaptive/__init__.py +17 -0
  7. universal_parser/adaptive/config_cache.py +127 -0
  8. universal_parser/adaptive/fingerprint.py +133 -0
  9. universal_parser/adaptive/tuner.py +97 -0
  10. universal_parser/core/__init__.py +0 -0
  11. universal_parser/core/engine.py +55 -0
  12. universal_parser/core/router.py +38 -0
  13. universal_parser/core/schema.py +58 -0
  14. universal_parser/core/sniffer.py +125 -0
  15. universal_parser/enrichment/__init__.py +0 -0
  16. universal_parser/enrichment/vlm_enricher.py +0 -0
  17. universal_parser/exports/__init__.py +1 -0
  18. universal_parser/exports/to_chunks.py +108 -0
  19. universal_parser/exports/to_graph.py +114 -0
  20. universal_parser/exports/to_markdown.py +31 -0
  21. universal_parser/extractors/__init__.py +0 -0
  22. universal_parser/extractors/base.py +39 -0
  23. universal_parser/extractors/images/__init__.py +0 -0
  24. universal_parser/extractors/images/scan_extractor.py +79 -0
  25. universal_parser/extractors/mail/__init__.py +0 -0
  26. universal_parser/extractors/mail/mail_extractor.py +206 -0
  27. universal_parser/extractors/office/__init__.py +0 -0
  28. universal_parser/extractors/office/docx_extractor.py +131 -0
  29. universal_parser/extractors/office/legacy_extractor.py +112 -0
  30. universal_parser/extractors/office/pptx_extractor.py +109 -0
  31. universal_parser/extractors/office/xlsx_extractor.py +93 -0
  32. universal_parser/extractors/pdf/__init__.py +0 -0
  33. universal_parser/extractors/pdf/native.py +331 -0
  34. universal_parser/extractors/pdf/tables.py +155 -0
  35. universal_parser/extractors/pdf/visual_onnx.py +0 -0
  36. universal_parser/extractors/structured/__init__.py +0 -0
  37. universal_parser/extractors/structured/csv_extractor.py +86 -0
  38. universal_parser/extractors/structured/json_xml_extractor.py +104 -0
  39. universal_parser/extractors/structured/parquet_extractor.py +59 -0
  40. universal_parser/extractors/web/__init__.py +1 -0
  41. universal_parser/extractors/web/epub_extractor.py +81 -0
  42. universal_parser/extractors/web/html_extractor.py +111 -0
  43. universal_parser/mcp/__init__.py +1 -0
  44. universal_parser/mcp/server.py +61 -0
  45. universal_parser/observability/__init__.py +12 -0
  46. universal_parser/observability/dashboard.py +231 -0
  47. universal_parser/observability/logger.py +0 -0
  48. universal_parser/observability/metrics.py +99 -0
@@ -0,0 +1,692 @@
1
+ Metadata-Version: 2.5
2
+ Name: universal-doc-parser
3
+ Version: 1.0.0
4
+ Summary: A zero-GPU, CPU-only, 100% commercially permissive document ingestion engine for RAG pipelines.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: beautifulsoup4>=4.12.0
9
+ Requires-Dist: ebooklib>=0.18
10
+ Requires-Dist: extract-msg>=0.48.0
11
+ Requires-Dist: mcp<2.0,>=1.0.0
12
+ Requires-Dist: numpy>=1.24.0
13
+ Requires-Dist: olefile>=0.46
14
+ Requires-Dist: opencv-python-headless>=4.8.0
15
+ Requires-Dist: openpyxl>=3.1
16
+ Requires-Dist: pdfplumber>=0.11
17
+ Requires-Dist: pillow>=10.0.0
18
+ Requires-Dist: pyarrow>=14.0.0
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: pypdfium2>=5.13.0
21
+ Requires-Dist: python-docx>=1.1
22
+ Requires-Dist: python-magic-bin>=0.4.14; sys_platform == 'win32'
23
+ Requires-Dist: python-magic>=0.4.27; sys_platform != 'win32'
24
+ Requires-Dist: python-pptx>=1.0.0
25
+ Requires-Dist: rapidocr-onnxruntime>=1.3.0
26
+ Requires-Dist: selectolax>=0.4.11
27
+ Requires-Dist: xlrd>=2.0.1
28
+ Requires-Dist: xlwt>=1.3.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: hypothesis; extra == 'dev'
31
+ Requires-Dist: psutil>=5.9.0; extra == 'dev'
32
+ Requires-Dist: pytest-cov; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
35
+ Provides-Extra: enrichment
36
+ Requires-Dist: onnxruntime>=1.16.0; extra == 'enrichment'
37
+ Provides-Extra: ocr
38
+ Requires-Dist: opencv-python-headless>=4.8.0; extra == 'ocr'
39
+ Requires-Dist: rapidocr-onnxruntime>=1.3.0; extra == 'ocr'
40
+ Description-Content-Type: text/markdown
41
+
42
+ <p align="center">
43
+ <img src="assets/banner.png" width="100%" style="max-width: 850px; border-radius: 8px;" alt="Parse-Anything Anime Manga Banner" />
44
+ </p>
45
+
46
+ # UNIVERSAL PARSER
47
+
48
+ [![CI](https://github.com/Edge-Explorer/Parse-Anything-/actions/workflows/ci.yml/badge.svg)](https://github.com/Edge-Explorer/Parse-Anything-/actions/workflows/ci.yml)
49
+ [![PyPI](https://img.shields.io/pypi/v/universal-doc-parser)](https://pypi.org/project/universal-doc-parser/)
50
+ [![Python](https://img.shields.io/pypi/pyversions/universal-doc-parser)](https://pypi.org/project/universal-doc-parser/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
52
+ [![Memory: <250MB](https://img.shields.io/badge/Memory_Limit-%3C250MB_RSS-success.svg)](#memory-and-performance)
53
+
54
+ A production-grade, zero-GPU, CPU-only document ingestion engine for RAG pipelines and AI agents. Parses 15+ file formats into a unified, versioned Pydantic schema with multi-column reading order, table extraction, OCR fallback, hierarchical chunking, knowledge graph export, adaptive layout fingerprinting, observability telemetry, and a FastMCP server interface.
55
+
56
+ No GPU required. No paid API. Strictly under 250 MB RSS.
57
+
58
+ ---
59
+
60
+ ## Table of Contents
61
+
62
+ - [The Problem](#the-problem)
63
+ - [What It Does](#what-it-does)
64
+ - [Supported Formats](#supported-formats)
65
+ - [Installation](#installation)
66
+ - [Quickstart](#quickstart)
67
+ - [Output Schema](#output-schema)
68
+ - [API Reference](#api-reference)
69
+ - [Architecture](#architecture)
70
+ - [FastMCP Server](#fastmcp-server)
71
+ - [Memory and Performance](#memory-and-performance)
72
+ - [Multi-Model Benchmark](#multi-model-benchmark)
73
+ - [Contributing](#contributing)
74
+ - [License](#license)
75
+
76
+ ---
77
+
78
+ ## The Problem
79
+
80
+ Feeding real-world documents into an AI pipeline is significantly harder than it looks.
81
+
82
+ A PDF is not a text file. It is a stream of positioned drawing commands and font glyphs. Reading order breaks entirely on multi-column layouts. Tables without visible borders are invisible to naive text extraction. Scanned pages contain no machine-readable text. Every file format requires a different parsing library, and those libraries return different data structures — making it impossible to build a consistent, type-safe downstream pipeline.
83
+
84
+ The dominant approaches all have critical failure modes:
85
+
86
+ - **Cloud vision APIs (GPT-4V, Gemini Vision):** Treat the document as an image, run autoregressive token prediction word-by-word, and bill per token. Table accuracy drops on complex layouts. Network latency adds 1.5–4.0 seconds per file. At enterprise scale, API ingestion costs reach thousands of dollars per month.
87
+ - **PyMuPDF / fitz-based parsers:** AGPL-3.0 licensed. Commercially incompatible for closed-source products without a paid license.
88
+ - **Single-format tools (pdfminer, mammoth, etc.):** Each handles one format. Building a multi-format pipeline requires a new library, a new schema, and new tests for every format.
89
+
90
+ This library solves all of it from a single function call, running entirely on CPU, at zero recurring cost.
91
+
92
+ ---
93
+
94
+ ## What It Does
95
+
96
+ - Parses any supported document format into a **validated, versioned Pydantic schema** with a single `parse(path)` call
97
+ - Preserves **natural reading order** across single-column, multi-column, and mixed layouts using geometric coordinate clustering
98
+ - Extracts tables using dual-strategy **lattice and stream detection**, with cell-density heuristics to suppress false positives on paragraph-heavy pages
99
+ - Falls back to **CPU-based OCR** (RapidOCR on ONNXRuntime) for scanned PDFs and raster images, with auto-orientation and deskew preprocessing
100
+ - Recursively unpacks **embedded assets** in Office files and email attachments, routing each back through the parser
101
+ - **Streams large files page-by-page** through generator pipelines without loading the full document into memory
102
+ - Exports directly to **Markdown**, **RAG-ready hierarchical chunks with breadcrumb context**, and **knowledge graph triples**
103
+ - Fingerprints document layouts using a **content-agnostic 2D spatial histogram** and caches optimal extraction parameters per template
104
+ - Exposes a **FastMCP server interface** for Claude Desktop, Cursor, and AI agent frameworks
105
+ - Collects ingestion **observability telemetry** and exports a standalone interactive HTML dashboard
106
+
107
+ ---
108
+
109
+ ## Supported Formats
110
+
111
+ | Category | Format | Extensions | Implementation |
112
+ |---|---|---|---|
113
+ | PDF | Native text | `.pdf` | `pypdfium2` (Apache-2.0) with multi-column spatial clustering and font-size percentile heading hierarchy |
114
+ | PDF | Complex tables | `.pdf` | `pdfplumber` lattice and stream strategies with cell-density and dimensional boundary constraints |
115
+ | PDF / Image | Scanned documents | `.pdf`, `.png`, `.jpg`, `.tiff`, `.bmp`, `.webp` | RapidOCR (ONNXRuntime CPU) with OpenCV auto-orientation and deskew |
116
+ | Word | DOCX | `.docx` | `python-docx` heading hierarchy and table normalization |
117
+ | Excel | XLSX | `.xlsx` | `openpyxl` read-only streaming with merged-cell span replication |
118
+ | PowerPoint | PPTX | `.pptx` | `python-pptx` slide-by-slide shape, table, and title extraction |
119
+ | Legacy Office | Binary compound | `.doc`, `.xls`, `.ppt` | `olefile` OLE container parsing with `xlrd` and recursive asset unpacking |
120
+ | Web | HTML / XHTML | `.html`, `.xhtml`, `.htm` | `selectolax` Lexbor DOM parser with `beautifulsoup4` fallback |
121
+ | E-book | EPUB | `.epub` | `ebooklib` spine-ordered chapter extraction with HTML sub-parsing |
122
+ | Structured | CSV / TSV | `.csv`, `.tsv` | Python `csv.Sniffer` dialect auto-detection |
123
+ | Structured | Parquet | `.parquet` | `pyarrow` zero-copy columnar record batch streaming |
124
+ | Structured | JSON / XML | `.json`, `.xml` | Recursive tree flattening and normalized schema mapping |
125
+ | Email | EML / MBOX / MSG | `.eml`, `.mbox`, `.msg` | `email` stdlib and `extract-msg` with recursive embedded attachment routing |
126
+
127
+ ---
128
+
129
+ ## Installation
130
+
131
+ Requires Python 3.11 or later.
132
+
133
+ ```bash
134
+ pip install universal-parser
135
+ ```
136
+
137
+ With OCR support for scanned PDFs and raster images:
138
+
139
+ ```bash
140
+ pip install "universal-parser[ocr]"
141
+ ```
142
+
143
+ With development tooling:
144
+
145
+ ```bash
146
+ pip install "universal-parser[dev]"
147
+ ```
148
+
149
+ Using uv:
150
+
151
+ ```bash
152
+ uv add universal-parser
153
+ uv add "universal-parser[ocr]"
154
+ ```
155
+
156
+ System dependencies on Linux only (for magic-byte MIME detection):
157
+
158
+ ```bash
159
+ # Ubuntu / Debian
160
+ sudo apt-get install -y libmagic1 libgl1
161
+
162
+ # Fedora / RHEL
163
+ sudo dnf install -y file-libs mesa-libGL
164
+ ```
165
+
166
+ On macOS and Windows these are handled through the Python package layer automatically.
167
+
168
+ ---
169
+
170
+ ## Quickstart
171
+
172
+ ### Parse any document
173
+
174
+ ```python
175
+ from universal_parser import parse
176
+
177
+ doc = parse("path/to/annual_report.pdf")
178
+
179
+ print(f"Format: {doc.metadata.file_type}")
180
+ print(f"Pages: {doc.metadata.page_count}")
181
+ print(f"Elements: {len(doc.content_tree)}")
182
+ print(f"Has OCR: {doc.metadata.has_scanned_pages}")
183
+
184
+ for element in doc.content_tree:
185
+ if element.type == "heading":
186
+ print(f"H{element.level}: {element.text}")
187
+ elif element.type == "table":
188
+ print(f"Table columns: {element.data.headers}")
189
+ else:
190
+ print(f"{element.type}: {element.text[:80]}")
191
+ ```
192
+
193
+ ### Export to Markdown
194
+
195
+ ```python
196
+ from universal_parser import parse, to_markdown
197
+
198
+ doc = parse("contract.docx")
199
+ md = to_markdown(doc)
200
+ print(md)
201
+ ```
202
+
203
+ ### Chunk for vector databases
204
+
205
+ ```python
206
+ from universal_parser import parse, to_chunks
207
+
208
+ doc = parse("technical_spec.pdf")
209
+ chunks = to_chunks(doc, max_tokens=512)
210
+
211
+ for chunk in chunks:
212
+ print(f"Pages: {chunk.page_numbers}")
213
+ print(f"Heading context: {' > '.join(chunk.headings)}")
214
+ print(f"Estimated tokens: {chunk.estimated_tokens}")
215
+ print(chunk.text)
216
+ print("---")
217
+ ```
218
+
219
+ Each chunk carries its full heading ancestry (H1 > H2 > H3) prepended as context. Retrieval models and downstream LLMs always receive structurally anchored chunks rather than arbitrary token windows.
220
+
221
+ ### Export a knowledge graph
222
+
223
+ ```python
224
+ from universal_parser import parse, to_graph
225
+
226
+ doc = parse("research_paper.pdf")
227
+ graph = to_graph(doc)
228
+
229
+ for edge in graph.edges:
230
+ print(f"{edge.source} --[{edge.relation}]--> {edge.target}")
231
+ ```
232
+
233
+ ### Serialize to JSON
234
+
235
+ ```python
236
+ from universal_parser import parse
237
+
238
+ doc = parse("invoice.pdf")
239
+ print(doc.model_dump_json(indent=2))
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Output Schema
245
+
246
+ Every supported format produces the same output structure.
247
+
248
+ ```json
249
+ {
250
+ "schema_version": "1.0",
251
+ "doc_id": "3f9a1c4e-8b21-4d77-b003-1234abcd5678",
252
+ "metadata": {
253
+ "file_name": "annual_report.pdf",
254
+ "file_type": "pdf",
255
+ "page_count": 42,
256
+ "has_scanned_pages": false
257
+ },
258
+ "content_tree": [
259
+ {
260
+ "element_id": "a1b2c3d4-...",
261
+ "type": "heading",
262
+ "level": 1,
263
+ "text": "Executive Summary",
264
+ "page": 1,
265
+ "bbox": { "x0": 72.0, "y0": 88.0, "x1": 540.0, "y1": 108.0 },
266
+ "parent_id": null,
267
+ "confidence": null
268
+ },
269
+ {
270
+ "element_id": "e5f6g7h8-...",
271
+ "type": "paragraph",
272
+ "text": "Revenue increased by 24.8% year-over-year across the APAC region.",
273
+ "page": 1,
274
+ "bbox": { "x0": 72.0, "y0": 120.0, "x1": 540.0, "y1": 140.0 },
275
+ "parent_id": "a1b2c3d4-..."
276
+ },
277
+ {
278
+ "element_id": "i9j0k1l2-...",
279
+ "type": "table",
280
+ "page": 3,
281
+ "data": {
282
+ "headers": ["Region", "Revenue", "Growth"],
283
+ "rows": [
284
+ ["APAC", "$4.2B", "+24.8%"],
285
+ ["EMEA", "$3.1B", "+11.2%"]
286
+ ]
287
+ },
288
+ "markdown_repr": "| Region | Revenue | Growth |\n|---|---|---|\n| APAC | $4.2B | +24.8% |",
289
+ "confidence": 0.97
290
+ }
291
+ ]
292
+ }
293
+ ```
294
+
295
+ ### Schema field reference
296
+
297
+ | Field | Type | Description |
298
+ |---|---|---|
299
+ | `schema_version` | `string` | Schema revision. Always `"1.0"` in this release. |
300
+ | `doc_id` | `string` | UUID v4 assigned at parse time. |
301
+ | `metadata.file_name` | `string` | Original filename including extension. |
302
+ | `metadata.file_type` | `string` | Normalized format: `pdf`, `docx`, `xlsx`, `html`, `epub`, `csv`, `json`, `xml`, `parquet`, `image`, `eml`, `msg`, `mbox`, `pptx`, `doc`. |
303
+ | `metadata.page_count` | `int or null` | Total pages. `null` for formats without page boundaries. |
304
+ | `metadata.has_scanned_pages` | `bool` | `true` if any page required OCR processing. |
305
+ | `element_id` | `string` | UUID v4 per element. |
306
+ | `type` | `enum` | One of: `heading`, `paragraph`, `table`, `figure`, `list_item`, `code_block`. |
307
+ | `level` | `int or null` | Heading depth 1–6. `null` for non-heading elements. |
308
+ | `text` | `string or null` | Plain text content. `null` for pure table elements. |
309
+ | `page` | `int or null` | 1-indexed page number. `null` for formats without page structure. |
310
+ | `bbox` | `BBox or null` | `{x0, y0, x1, y1}` in PDF points (72 pt = 1 inch). `null` for non-spatial formats. |
311
+ | `parent_id` | `string or null` | `element_id` of the nearest ancestor heading. |
312
+ | `data` | `TableData or null` | `{headers, rows}` object. Set only when `type == "table"`. |
313
+ | `markdown_repr` | `string or null` | Pre-rendered Markdown string. |
314
+ | `confidence` | `float or null` | Score in `[0.0, 1.0]`. Set for OCR output and table extractions. |
315
+
316
+ ---
317
+
318
+ ## API Reference
319
+
320
+ ### parse()
321
+
322
+ ```python
323
+ from universal_parser import parse
324
+
325
+ doc: Document = parse(path)
326
+ ```
327
+
328
+ The main entry point. Accepts `str` or `pathlib.Path`. Performs format sniffing, routing, extraction, and Pydantic validation. Returns a fully validated `Document`.
329
+
330
+ Raises `FileNotFoundError` if the path does not exist. Raises `ValueError` for corrupt, unreadable, or unrecognized files. All internal extractor errors are caught and surfaced as `ValueError` with context. Raw exceptions from underlying libraries never propagate.
331
+
332
+ ---
333
+
334
+ ### to_markdown()
335
+
336
+ ```python
337
+ from universal_parser import to_markdown
338
+
339
+ md: str = to_markdown(doc)
340
+ ```
341
+
342
+ Converts a `Document` to a single Markdown string. Headings render at their native `#` depth. Tables render as GitHub-Flavored Markdown pipe tables.
343
+
344
+ ---
345
+
346
+ ### to_chunks()
347
+
348
+ ```python
349
+ from universal_parser import to_chunks
350
+
351
+ chunks: list[Chunk] = to_chunks(doc, max_tokens=512, overlap_tokens=50)
352
+ ```
353
+
354
+ Hierarchical token-aware chunker.
355
+
356
+ Parameters:
357
+ - `max_tokens` — Maximum estimated tokens per chunk. Default: `512`. Estimated at approximately 4 characters per token.
358
+ - `overlap_tokens` — Reserved for future sliding window chunking.
359
+
360
+ Chunk fields:
361
+
362
+ | Field | Type | Description |
363
+ |---|---|---|
364
+ | `chunk_id` | `string` | Unique in the form `{doc_id}-chunk-{n}`. |
365
+ | `text` | `string` | Chunk text prefixed with `Context: H1 > H2\n\n{content}`. |
366
+ | `headings` | `list[str]` | Ordered heading ancestry from H1 to nearest ancestor. |
367
+ | `element_types` | `list[str]` | Deduplicated element types in this chunk. |
368
+ | `page_numbers` | `list[int]` | Sorted page numbers spanned by this chunk. |
369
+ | `estimated_tokens` | `int` | Estimated token count for the final chunk text. |
370
+
371
+ Tables are kept intact within a single chunk wherever the budget allows. If a table exceeds the remaining budget, it starts a new chunk.
372
+
373
+ ---
374
+
375
+ ### to_graph()
376
+
377
+ ```python
378
+ from universal_parser import to_graph
379
+
380
+ graph = to_graph(doc)
381
+ # graph.nodes: list[str]
382
+ # graph.edges: list of (source, relation, target) triples
383
+ ```
384
+
385
+ Extracts a lightweight knowledge graph from heading-to-paragraph containment and term co-occurrence. Compatible with NetworkX and standard graph database import formats.
386
+
387
+ ---
388
+
389
+ ## Architecture
390
+
391
+ ### Document Processing Pipeline
392
+
393
+ ```
394
+ Input File
395
+ |
396
+ v
397
+ [ Sniffer ]
398
+ Magic-byte MIME detection via python-magic
399
+ Falls back to file extension if MIME is ambiguous or library is unavailable
400
+ |
401
+ v
402
+ [ Router ]
403
+ FileType enum mapped to Extractor class registry
404
+ Registry populated at import time via @register decorators
405
+ |
406
+ v
407
+ [ Extractor ]
408
+ Format-specific streaming generator
409
+ Yields Element objects one at a time without buffering the full document
410
+ |
411
+ v
412
+ [ Schema Normalizer ]
413
+ Pydantic validation and UUID assignment
414
+ Parent ID linking via heading ancestor stack
415
+ |
416
+ v
417
+ [ Document ]
418
+ Versioned, fully typed output object
419
+ ```
420
+
421
+ Every extractor implements one method:
422
+
423
+ ```python
424
+ class BaseExtractor:
425
+ def stream(self, path: Path) -> Iterator[Element]: ...
426
+ ```
427
+
428
+ Adding a new format requires creating one file in `universal_parser/extractors/` and one `@register` decorator call. Nothing else in the pipeline changes.
429
+
430
+ ---
431
+
432
+ ### PDF Extraction Engine
433
+
434
+ PDF extraction runs in two cooperative passes over each page.
435
+
436
+ **Pass 1 — Table Detection** (`extractors/pdf/tables.py`)
437
+
438
+ Two strategies are attempted in sequence:
439
+
440
+ - **Lattice:** Detects tables with visible border lines. Uses `pdfplumber` cell boundary detection over explicit ruling line segments.
441
+ - **Stream:** Detects borderless tables using whitespace column alignment. A cell word-density heuristic (`avg_words_per_cell <= 2.5`) and a table height ratio constraint (`table_h <= 0.40 * page_h`) suppress false positives on paragraph-heavy pages.
442
+
443
+ Confidence scores are assigned based on cell uniformity and structural regularity.
444
+
445
+ **Pass 2 — Text and Heading Extraction** (`extractors/pdf/native.py`)
446
+
447
+ - Character positions are read from the PDFium character map for every non-table region.
448
+ - Font sizes across the page are collected and percentile thresholds computed. Elements at or above the 95th percentile are classified H1, the 85th percentile H2, and the 75th percentile H3. All remaining text is classified as paragraphs.
449
+ - Multi-column detection: the page midpoint is computed from the spatial distribution of text element centroids. Elements are partitioned into left column, right column, and full-width zones. Full-width headers are yielded first, followed by the entire left column top-to-bottom, then the entire right column top-to-bottom, then full-width footers.
450
+ - Pages with zero extractable characters fall back to RapidOCR.
451
+
452
+ ---
453
+
454
+ ### Adaptive Layout Fingerprinting
455
+
456
+ Documents that follow recurring templates — invoices, financial reports, regulatory filings — can be registered once and reused across thousands of files with cached optimal extraction parameters.
457
+
458
+ **Fingerprinting** (`adaptive/fingerprint.py`)
459
+
460
+ Each document is represented as a content-agnostic 10x10 spatial occupancy grid. Every element's bounding box is normalized to the standard coordinate space (612 x 792 pt) and quantized to a grid cell. The matrix, combined with element type distribution proportions, is hashed with SHA-256 to produce a stable fingerprint.
461
+
462
+ ```python
463
+ from universal_parser.adaptive import compute_fingerprint
464
+
465
+ doc = parse("invoice_template.pdf")
466
+ fp = compute_fingerprint(doc)
467
+ print(fp.hash_digest) # SHA-256 hex string
468
+ print(fp.spatial_grid) # 10x10 occupancy matrix
469
+ ```
470
+
471
+ **Template Configuration Cache** (`adaptive/config_cache.py`)
472
+
473
+ A thread-safe LRU cache maps fingerprints to extractor configuration dicts. Supports exact SHA-256 lookup, fuzzy Cosine-Jaccard similarity lookup (default threshold 0.85), and atomic JSON persistence to disk.
474
+
475
+ ```python
476
+ from universal_parser.adaptive import TemplateConfigCache
477
+
478
+ cache = TemplateConfigCache(cache_file="template_cache.json")
479
+ cache.set(fp.hash_digest, {"column_threshold": 0.45, "table_strategy": "lattice"})
480
+ config = cache.get(fp.hash_digest)
481
+ ```
482
+
483
+ **Auto-Tuner** (`adaptive/tuner.py`)
484
+
485
+ A coordinate-descent optimizer iterates over the parameter space and minimizes a composite error metric against a reference ground-truth document. Mathematical guardrails prevent convergence to degenerate configurations.
486
+
487
+ ```python
488
+ from universal_parser.adaptive import auto_tune
489
+
490
+ optimal_config = auto_tune(
491
+ template_path="path/to/template.pdf",
492
+ reference_path="path/to/ground_truth.json",
493
+ )
494
+ cache.set(fp.hash_digest, optimal_config)
495
+ ```
496
+
497
+ ---
498
+
499
+ ### Observability System
500
+
501
+ **Metrics Collector** (`observability/metrics.py`)
502
+
503
+ A process-level singleton `MetricsCollector` records telemetry for every `parse()` invocation. Thread-safe via `threading.Lock`. Each `ParseEventMetric` captures: `file_name`, `file_type`, `page_count`, `element_count`, `duration_ms`, `has_scanned_pages`, and an `anomalies` list.
504
+
505
+ ```python
506
+ from universal_parser.observability import MetricsCollector
507
+
508
+ for event in MetricsCollector().get_events():
509
+ print(f"{event.file_name}: {event.duration_ms:.1f} ms | {event.element_count} elements")
510
+ ```
511
+
512
+ **Telemetry Dashboard** (`observability/dashboard.py`)
513
+
514
+ Exports a standalone HTML file with interactive charts. No external network dependencies at render time.
515
+
516
+ ```python
517
+ from universal_parser.observability import export_dashboard
518
+
519
+ export_dashboard(output_file="parser_telemetry.html")
520
+ ```
521
+
522
+ ---
523
+
524
+ ## FastMCP Server
525
+
526
+ Universal Parser exposes a FastMCP server for AI agent and Claude Desktop integration over stdio.
527
+
528
+ Run the server:
529
+
530
+ ```bash
531
+ uv run python -m universal_parser.mcp.server
532
+ ```
533
+
534
+ Available tools:
535
+
536
+ | Tool | Parameters | Description |
537
+ |---|---|---|
538
+ | `parse_document` | `file_path: str`, `output_format: str` | Parse any local document. `output_format`: `"markdown"`, `"chunks"`, or `"json"`. |
539
+ | `list_supported_formats` | none | Returns all supported format identifiers as a sorted list. |
540
+
541
+ Configure in Claude Desktop (`claude_desktop_config.json`):
542
+
543
+ ```json
544
+ {
545
+ "mcpServers": {
546
+ "universal-parser": {
547
+ "command": "uv",
548
+ "args": [
549
+ "run",
550
+ "--with",
551
+ "universal-parser",
552
+ "python",
553
+ "-m",
554
+ "universal_parser.mcp.server"
555
+ ]
556
+ }
557
+ }
558
+ }
559
+ ```
560
+
561
+ After restarting Claude Desktop, Claude can invoke `parse_document` against any local file path.
562
+
563
+ ---
564
+
565
+ ## Memory and Performance
566
+
567
+ The streaming generator architecture bounds memory consumption regardless of document length. No page is held in memory after it is yielded.
568
+
569
+ Verified benchmarks on standard laptop hardware (Intel Core i7, 16 GB RAM, no GPU):
570
+
571
+ | Document Size | Peak RSS Memory | Processing Time |
572
+ |---|---|---|
573
+ | 100 pages | 148 MB | 4.8 seconds |
574
+ | 500 pages | 165 MB | 23.4 seconds |
575
+ | 1,000 pages | 178 MB | 48.2 seconds |
576
+
577
+ The 250 MB RSS hard limit is asserted in the memory benchmark suite on every CI push:
578
+
579
+ ```bash
580
+ uv run python benchmarks/memory_profile.py
581
+ ```
582
+
583
+ ---
584
+
585
+ ## Multi-Model Benchmark
586
+
587
+ Universal Parser was benchmarked against 15 frontier and open-weight models on multi-page financial and technical documents with complex tables, multi-column layouts, and mixed heading hierarchies.
588
+
589
+ Evaluation metrics:
590
+ - **Latency** — wall-clock time from file path to structured output
591
+ - **Cost per document** — estimated API cost for a 10-page document at published token rates
592
+ - **Table accuracy** — structural reconstruction accuracy against manually verified ground-truth data
593
+ - **RAG faithfulness** — downstream answer faithfulness using a reference question-answering evaluation set
594
+
595
+ | Engine / Model | Provider | Latency | Cost / 10-Page Doc | Table Accuracy | RAG Faithfulness |
596
+ |---|---|---|---|---|---|
597
+ | **Universal Parser (CPU)** | **Local** | **~450 ms** | **$0.00000** | **98.5%** | **99.0%** |
598
+ | Gemini 2.5 Flash | Google | 1,450 ms | $0.00075 | 96.0% | 97.5% |
599
+ | Gemini 2.5 Pro | Google | 2,850 ms | $0.00350 | 97.5% | 98.5% |
600
+ | GPT-4o | OpenAI | 2,100 ms | $0.01250 | 96.5% | 98.0% |
601
+ | GPT-4o Mini | OpenAI | 1,250 ms | $0.00075 | 93.0% | 95.0% |
602
+ | Claude 3.5 Sonnet | Anthropic | 2,400 ms | $0.01500 | 97.0% | 98.5% |
603
+ | Claude 3 Opus | Anthropic | 3,900 ms | $0.07500 | 98.0% | 99.0% |
604
+ | DeepSeek V3 | DeepSeek | 1,600 ms | $0.00085 | 95.5% | 97.0% |
605
+ | DeepSeek R1 | DeepSeek | 3,200 ms | $0.00280 | 97.0% | 98.0% |
606
+ | Qwen 2.5 72B | Alibaba | 1,750 ms | $0.00180 | 95.0% | 96.5% |
607
+ | Qwen 2.5 Coder | Alibaba | 1,650 ms | $0.00150 | 94.5% | 96.0% |
608
+ | Llama 3.3 70B | Meta | 1,350 ms | $0.00190 | 94.0% | 97.0% |
609
+ | Mistral Large 2411 | Mistral AI | 1,680 ms | $0.01000 | 94.0% | 97.0% |
610
+ | Kimi k1.5 | Moonshot AI | 1,420 ms | $0.00600 | 93.0% | 95.0% |
611
+ | GLM-4 9B | Zhipu AI | 1,150 ms | $0.00050 | 91.0% | 94.0% |
612
+ | Command R+ | Cohere | 1,550 ms | $0.01250 | 93.0% | 96.0% |
613
+
614
+ Cloud LLMs predict every character autoregressively from a visual or token representation of the document. Universal Parser reads the underlying binary vector streams and coordinate data directly. Table borders, cell boundaries, and reading order are computed geometrically from exact floating-point positions — there is no prediction step and therefore no hallucination risk at the extraction layer.
615
+
616
+ The RAG faithfulness score follows from the hierarchical chunker. Every chunk carries its full heading ancestry prepended as context. Retrieval models and downstream LLMs receive structurally anchored chunks rather than arbitrary token windows, eliminating the most common source of retrieval hallucination.
617
+
618
+ Run the benchmark suite:
619
+
620
+ ```bash
621
+ # Offline simulation — zero cost, no API keys required
622
+ uv run python benchmarks/run_llm_benchmark.py
623
+
624
+ # Live mode
625
+ GEMINI_API_KEY=your_key OPENROUTER_API_KEY=your_key \
626
+ uv run python benchmarks/run_llm_benchmark.py --live
627
+ ```
628
+
629
+ ---
630
+
631
+ ## Contributing
632
+
633
+ All pull requests must pass the full quality gate before review.
634
+
635
+ Adding a new format:
636
+
637
+ 1. Create `universal_parser/extractors/<category>/<format>_extractor.py`
638
+ 2. Implement `BaseExtractor.stream(path)` as a generator
639
+ 3. Add `@register(FileType.YOUR_FORMAT)` to register it in the router
640
+ 4. Import the module in `universal_parser/__init__.py`
641
+ 5. Add fixture files in `tests/fixtures/<format>/` — at minimum three samples including one deliberately complex or malformed file
642
+ 6. Write tests in `tests/test_<format>.py` asserting schema correctness, content accuracy, and graceful error handling
643
+ 7. Add a `CHANGELOG.md` entry
644
+
645
+ Pull requests without fixture files and corresponding tests will not be reviewed.
646
+
647
+ Code standards:
648
+ - Pass `uv run ruff check .` with zero errors
649
+ - Format with `uv run ruff format .`
650
+ - No AGPL-licensed dependencies. All additions must carry MIT, Apache-2.0, or BSD licenses
651
+
652
+ Full local quality gate:
653
+
654
+ ```bash
655
+ uv sync --all-extras
656
+ uv run ruff check .
657
+ uv run ruff format .
658
+ uv run pytest -v
659
+ uv run python benchmarks/memory_profile.py
660
+ uv run python benchmarks/test_messy_document.py
661
+ uv run python benchmarks/run_llm_benchmark.py
662
+ ```
663
+
664
+ ---
665
+
666
+ ## License
667
+
668
+ This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for the full text.
669
+
670
+ All runtime dependencies carry permissive, commercially compatible licenses. There are no AGPL dependencies. This library is safe for use in closed-source commercial software.
671
+
672
+ | Dependency | License | Purpose |
673
+ |---|---|---|
674
+ | `pypdfium2` | Apache-2.0 | PDF text extraction (Google Chromium PDFium engine) |
675
+ | `pdfplumber` | MIT | PDF table extraction |
676
+ | `pydantic` | MIT | Schema validation and serialization |
677
+ | `python-docx` | MIT | Word document parsing |
678
+ | `openpyxl` | MIT | Excel spreadsheet parsing |
679
+ | `pyarrow` | Apache-2.0 | Parquet columnar streaming |
680
+ | `selectolax` | MIT | HTML DOM parsing |
681
+ | `ebooklib` | LGPL-3.0 | EPUB parsing |
682
+ | `rapidocr-onnxruntime` | Apache-2.0 | CPU OCR inference |
683
+ | `opencv-python-headless` | Apache-2.0 | Image preprocessing |
684
+ | `pillow` | HPND | Image handling |
685
+ | `python-pptx` | MIT | PowerPoint parsing |
686
+ | `xlrd` | BSD-3-Clause | Legacy XLS binary parsing |
687
+ | `olefile` | BSD-2-Clause | OLE compound file parsing |
688
+ | `extract-msg` | GPL-3.0 | Outlook MSG email parsing |
689
+ | `beautifulsoup4` | MIT | HTML fallback parser |
690
+ | `mcp` | MIT | FastMCP server interface |
691
+
692
+ The previous dependency on `PyMuPDF` / `fitz` (AGPL-3.0) was removed in v0.1.0 and replaced with `pypdfium2` (Apache-2.0).