universal-doc-parser 1.0.1__tar.gz → 1.0.2__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 (139) hide show
  1. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/PKG-INFO +10 -2
  2. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/README.md +4 -1
  3. universal_doc_parser-1.0.2/docs/ADDING_A_FORMAT.md +212 -0
  4. universal_doc_parser-1.0.2/docs/ARCHITECTURE.md +164 -0
  5. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/docs/CHANGELOG.md +16 -0
  6. universal_doc_parser-1.0.2/docs/SCHEMA.md +183 -0
  7. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/main.py +53 -35
  8. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/pyproject.toml +8 -1
  9. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/__init__.py +1 -1
  10. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/uv.lock +1 -1
  11. universal_doc_parser-1.0.1/docs/ADDING_A_FORMAT.md +0 -0
  12. universal_doc_parser-1.0.1/docs/ARCHITECTURE.md +0 -0
  13. universal_doc_parser-1.0.1/docs/SCHEMA.md +0 -0
  14. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.github/workflows/ci.yml +0 -0
  15. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.github/workflows/hf_sync.yml +0 -0
  16. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.github/workflows/release.yml +0 -0
  17. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.gitignore +0 -0
  18. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.python-version +0 -0
  19. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/LICENSE +0 -0
  20. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/assets/README.md +0 -0
  21. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/assets/banner.png +0 -0
  22. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/assets/logo.png +0 -0
  23. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/README.md +0 -0
  24. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/memory_profile.py +0 -0
  25. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/README.md +0 -0
  26. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/__init__.py +0 -0
  27. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/anthropic/__init__.py +0 -0
  28. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/anthropic/opus.py +0 -0
  29. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/anthropic/sonnet.py +0 -0
  30. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/base.py +0 -0
  31. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/cohere/__init__.py +0 -0
  32. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/cohere/command_r_plus.py +0 -0
  33. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/deepseek/__init__.py +0 -0
  34. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/deepseek/r1.py +0 -0
  35. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/deepseek/v3.py +0 -0
  36. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/gemini/__init__.py +0 -0
  37. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/gemini/flash.py +0 -0
  38. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/gemini/pro.py +0 -0
  39. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/glm/__init__.py +0 -0
  40. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/glm/glm4.py +0 -0
  41. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/kimi/__init__.py +0 -0
  42. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/kimi/k1_5.py +0 -0
  43. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/meta/__init__.py +0 -0
  44. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/meta/llama3_3.py +0 -0
  45. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/mistral/__init__.py +0 -0
  46. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/mistral/mistral_large.py +0 -0
  47. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/openai/__init__.py +0 -0
  48. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/openai/gpt4o.py +0 -0
  49. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/openai/gpt4o_mini.py +0 -0
  50. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/qwen/__init__.py +0 -0
  51. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/qwen/qwen_72b.py +0 -0
  52. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/qwen/qwen_coder.py +0 -0
  53. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/run_llm_benchmark.py +0 -0
  54. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/test_messy_document.py +0 -0
  55. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/docs/README.md +0 -0
  56. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/requirements.txt +0 -0
  57. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/README.md +0 -0
  58. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/__init__.py +0 -0
  59. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/csv/sample.csv +0 -0
  60. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/csv/sample.tsv +0 -0
  61. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/docx/sample.docx +0 -0
  62. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/html/sample.html +0 -0
  63. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/json/sample_nested.json +0 -0
  64. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/json/sample_tabular.json +0 -0
  65. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/pdf/sample.pdf +0 -0
  66. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/xlsx/sample.xlsx +0 -0
  67. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/xml/sample.xml +0 -0
  68. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_adaptive.py +0 -0
  69. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_benchmark_providers.py +0 -0
  70. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_csv.py +0 -0
  71. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_docx.py +0 -0
  72. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_epub.py +0 -0
  73. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_exports.py +0 -0
  74. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_html.py +0 -0
  75. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_image.py +0 -0
  76. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_json_xml.py +0 -0
  77. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_legacy.py +0 -0
  78. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_mail.py +0 -0
  79. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_mcp.py +0 -0
  80. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_observability.py +0 -0
  81. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_parquet.py +0 -0
  82. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_pdf.py +0 -0
  83. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_pptx.py +0 -0
  84. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_xlsx.py +0 -0
  85. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/README.md +0 -0
  86. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/__init__.py +0 -0
  87. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/config_cache.py +0 -0
  88. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/fingerprint.py +0 -0
  89. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/tuner.py +0 -0
  90. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/__init__.py +0 -0
  91. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/engine.py +0 -0
  92. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/router.py +0 -0
  93. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/schema.py +0 -0
  94. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/sniffer.py +0 -0
  95. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/enrichment/README.md +0 -0
  96. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/enrichment/__init__.py +0 -0
  97. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/enrichment/vlm_enricher.py +0 -0
  98. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/README.md +0 -0
  99. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/__init__.py +0 -0
  100. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/to_chunks.py +0 -0
  101. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/to_graph.py +0 -0
  102. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/to_markdown.py +0 -0
  103. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/README.md +0 -0
  104. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/__init__.py +0 -0
  105. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/base.py +0 -0
  106. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/images/README.md +0 -0
  107. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/images/__init__.py +0 -0
  108. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/images/scan_extractor.py +0 -0
  109. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/mail/README.md +0 -0
  110. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/mail/__init__.py +0 -0
  111. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/mail/mail_extractor.py +0 -0
  112. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/README.md +0 -0
  113. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/__init__.py +0 -0
  114. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/docx_extractor.py +0 -0
  115. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/legacy_extractor.py +0 -0
  116. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/pptx_extractor.py +0 -0
  117. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/xlsx_extractor.py +0 -0
  118. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/README.md +0 -0
  119. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/__init__.py +0 -0
  120. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/native.py +0 -0
  121. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/tables.py +0 -0
  122. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/visual_onnx.py +0 -0
  123. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/README.md +0 -0
  124. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/__init__.py +0 -0
  125. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/csv_extractor.py +0 -0
  126. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/json_xml_extractor.py +0 -0
  127. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/parquet_extractor.py +0 -0
  128. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/README.md +0 -0
  129. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/__init__.py +0 -0
  130. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/epub_extractor.py +0 -0
  131. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/html_extractor.py +0 -0
  132. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/mcp/README.md +0 -0
  133. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/mcp/__init__.py +0 -0
  134. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/mcp/server.py +0 -0
  135. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/README.md +0 -0
  136. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/__init__.py +0 -0
  137. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/dashboard.py +0 -0
  138. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/logger.py +0 -0
  139. {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/metrics.py +0 -0
@@ -1,7 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: universal-doc-parser
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: A zero-GPU, CPU-only document ingestion engine for RAG pipelines and AI agents.
5
+ Project-URL: Homepage, https://github.com/Edge-Explorer/Parse-Anything-
6
+ Project-URL: Live Demo, https://huggingface.co/spaces/Karan6124/universal-doc-parser
7
+ Project-URL: Repository, https://github.com/Edge-Explorer/Parse-Anything-
8
+ Project-URL: Documentation, https://github.com/Edge-Explorer/Parse-Anything-#readme
9
+ Project-URL: Issues, https://github.com/Edge-Explorer/Parse-Anything-/issues
5
10
  License: MIT
6
11
  License-File: LICENSE
7
12
  Requires-Python: >=3.11
@@ -49,13 +54,16 @@ Description-Content-Type: text/markdown
49
54
  # UNIVERSAL PARSER
50
55
 
51
56
  [![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)
52
- [![PyPI](https://img.shields.io/badge/PyPI-v1.0.1-blue.svg)](https://pypi.org/project/universal-doc-parser/)
57
+ [![PyPI](https://img.shields.io/badge/PyPI-v1.0.2-blue.svg)](https://pypi.org/project/universal-doc-parser/)
58
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Live%20Demo-yellow.svg)](https://huggingface.co/spaces/Karan6124/universal-doc-parser)
53
59
  [![Python](https://img.shields.io/badge/Python-3.11%20%7C%203.12-blue.svg)](https://pypi.org/project/universal-doc-parser/)
54
60
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
55
61
  [![Memory: <250MB](https://img.shields.io/badge/Memory_Limit-%3C250MB_RSS-success.svg)](#memory-and-performance)
56
62
 
57
63
  A CPU-only document ingestion engine for RAG pipelines and AI agents. Parses 15+ file formats into a unified Pydantic schema with hierarchical chunking, adaptive layout fingerprinting, and a FastMCP server interface.
58
64
 
65
+ **[Try the Live Interactive Demo on Hugging Face Spaces](https://huggingface.co/spaces/Karan6124/universal-doc-parser)**
66
+
59
67
  No GPU. No paid API. No recurring cost.
60
68
 
61
69
  ---
@@ -5,13 +5,16 @@
5
5
  # UNIVERSAL PARSER
6
6
 
7
7
  [![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)
8
- [![PyPI](https://img.shields.io/badge/PyPI-v1.0.1-blue.svg)](https://pypi.org/project/universal-doc-parser/)
8
+ [![PyPI](https://img.shields.io/badge/PyPI-v1.0.2-blue.svg)](https://pypi.org/project/universal-doc-parser/)
9
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Live%20Demo-yellow.svg)](https://huggingface.co/spaces/Karan6124/universal-doc-parser)
9
10
  [![Python](https://img.shields.io/badge/Python-3.11%20%7C%203.12-blue.svg)](https://pypi.org/project/universal-doc-parser/)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
12
  [![Memory: <250MB](https://img.shields.io/badge/Memory_Limit-%3C250MB_RSS-success.svg)](#memory-and-performance)
12
13
 
13
14
  A CPU-only document ingestion engine for RAG pipelines and AI agents. Parses 15+ file formats into a unified Pydantic schema with hierarchical chunking, adaptive layout fingerprinting, and a FastMCP server interface.
14
15
 
16
+ **[Try the Live Interactive Demo on Hugging Face Spaces](https://huggingface.co/spaces/Karan6124/universal-doc-parser)**
17
+
15
18
  No GPU. No paid API. No recurring cost.
16
19
 
17
20
  ---
@@ -0,0 +1,212 @@
1
+ # Adding a New Document Format
2
+
3
+ This guide walks contributors and integrators through the complete process of adding support for a new document format to `universal-doc-parser`.
4
+
5
+ The architecture uses a decoupled Registry and Strategy pattern: adding a new format requires implementing a single extractor class and registering it with a decorator. The core engine, validation layer, and export pipelines require zero modifications.
6
+
7
+ ---
8
+
9
+ ## Prerequisites and Design Constraints
10
+
11
+ Before implementing a new extractor:
12
+
13
+ 1. **Permissive Licensing Only:** Any third-party library introduced must be licensed under MIT, Apache-2.0, or BSD. AGPL-licensed dependencies are strictly prohibited.
14
+ 2. **Streaming Execution:** Extractors must yield `Element` objects incrementally via Python generators (`yield`). Do not buffer entire multi-gigabyte files in RAM.
15
+ 3. **Graceful Failure:** Extractors must never leak raw third-party library exceptions. Catch library-specific errors and raise standard `ValueError` with descriptive diagnostic messages.
16
+ 4. **No GPU Requirement:** All parsing logic must execute on CPU within the bounded memory envelope (<250 MB RSS).
17
+
18
+ ---
19
+
20
+ ## Step-by-Step Implementation Guide
21
+
22
+ ### Step 1: Declare the Format in the Sniffer
23
+
24
+ Open `universal_parser/core/sniffer.py`:
25
+
26
+ 1. Add your format to the `FileType` enum:
27
+ ```python
28
+ class FileType(Enum):
29
+ # ... existing types ...
30
+ MARKDOWN = auto() # Example: adding Markdown support
31
+ ```
32
+
33
+ 2. Add extension mappings to `_EXT_MAP`:
34
+ ```python
35
+ _EXT_MAP: dict[str, FileType] = {
36
+ # ...
37
+ ".md": FileType.MARKDOWN,
38
+ ".markdown": FileType.MARKDOWN,
39
+ }
40
+ ```
41
+
42
+ 3. If the format has standard MIME types, add them to `_MIME_MAP`:
43
+ ```python
44
+ _MIME_MAP: dict[str, FileType] = {
45
+ # ...
46
+ "text/markdown": FileType.MARKDOWN,
47
+ "text/x-markdown": FileType.MARKDOWN,
48
+ }
49
+ ```
50
+
51
+ ---
52
+
53
+ ### Step 2: Implement the Extractor Class
54
+
55
+ Create a new module in the appropriate category subdirectory under `universal_parser/extractors/`:
56
+
57
+ - `universal_parser/extractors/pdf/`
58
+ - `universal_parser/extractors/office/`
59
+ - `universal_parser/extractors/images/`
60
+ - `universal_parser/extractors/structured/`
61
+ - `universal_parser/extractors/web/`
62
+ - `universal_parser/extractors/mail/`
63
+
64
+ Create `universal_parser/extractors/structured/markdown_extractor.py`:
65
+
66
+ ```python
67
+ from __future__ import annotations
68
+
69
+ from collections.abc import Iterator
70
+ from pathlib import Path
71
+ from typing import ClassVar
72
+
73
+ from universal_parser.core.router import register
74
+ from universal_parser.core.schema import BBox, Element
75
+ from universal_parser.core.sniffer import FileType
76
+ from universal_parser.extractors.base import BaseExtractor
77
+
78
+
79
+ @register
80
+ class MarkdownExtractor(BaseExtractor):
81
+ """Extracts structured headings, paragraphs, and code blocks from Markdown files."""
82
+
83
+ supported_types: ClassVar[list[FileType]] = [FileType.MARKDOWN]
84
+
85
+ def stream(self, path: str | Path) -> Iterator[Element]:
86
+ """Stream elements from a Markdown file line-by-line."""
87
+ path = Path(path)
88
+ if not path.is_file():
89
+ raise FileNotFoundError(f"File not found: {path}")
90
+
91
+ try:
92
+ with open(path, encoding="utf-8", errors="replace") as f:
93
+ for line_idx, raw_line in enumerate(f, start=1):
94
+ line = raw_line.strip()
95
+ if not line:
96
+ continue
97
+
98
+ # Heading detection
99
+ if line.startswith("#"):
100
+ level = min(6, len(line) - len(line.lstrip("#")))
101
+ text = line.lstrip("#").strip()
102
+ yield Element(
103
+ type="heading",
104
+ level=level,
105
+ text=text,
106
+ page=1,
107
+ )
108
+ else:
109
+ yield Element(
110
+ type="paragraph",
111
+ text=line,
112
+ page=1,
113
+ )
114
+ except Exception as err:
115
+ raise ValueError(f"Failed to parse Markdown file {path.name}: {err}") from err
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Step 3: Register the Extractor in Package Initialization
121
+
122
+ Open `universal_parser/__init__.py` and add the import to ensure the `@register` decorator executes when the package loads:
123
+
124
+ ```python
125
+ import universal_parser.extractors.structured.markdown_extractor # noqa: F401
126
+ ```
127
+
128
+ ---
129
+
130
+ ### Step 4: Add Test Fixtures
131
+
132
+ Every format requires at least three real test fixture files in `tests/fixtures/<format>/`:
133
+
134
+ 1. `sample.<ext>`: Standard clean document with common features (headings, text, lists).
135
+ 2. `complex.<ext>`: Complex document with nested sections, large tables, or mixed encoding.
136
+ 3. `malformed.<ext>`: Deliberately broken or empty file to verify graceful error handling.
137
+
138
+ Create the directory:
139
+ ```bash
140
+ tests/fixtures/markdown/
141
+ ├── sample.md
142
+ ├── complex.md
143
+ └── malformed.md
144
+ ```
145
+
146
+ ---
147
+
148
+ ### Step 5: Write Unit and Integration Tests
149
+
150
+ Create `tests/test_markdown.py`:
151
+
152
+ ```python
153
+ from pathlib import Path
154
+ import pytest
155
+ from universal_parser import parse, to_chunks, to_markdown
156
+ from universal_parser.core.sniffer import FileType, sniff
157
+
158
+ FIXTURES = Path(__file__).parent / "fixtures" / "markdown"
159
+
160
+
161
+ def test_markdown_sniffing():
162
+ assert sniff(FIXTURES / "sample.md") == FileType.MARKDOWN
163
+
164
+
165
+ def test_markdown_parsing():
166
+ doc = parse(FIXTURES / "sample.md")
167
+ assert doc.metadata.file_type == "markdown"
168
+ assert len(doc.content_tree) > 0
169
+
170
+ headings = [el for el in doc.content_tree if el.type == "heading"]
171
+ assert len(headings) >= 1
172
+ assert headings[0].level == 1
173
+
174
+
175
+ def test_markdown_chunking():
176
+ doc = parse(FIXTURES / "sample.md")
177
+ chunks = to_chunks(doc, max_tokens=256)
178
+ assert len(chunks) > 0
179
+ assert all(c.text for c in chunks)
180
+
181
+
182
+ def test_malformed_markdown_handling():
183
+ # Verify the parser handles empty or corrupt files without crashing
184
+ doc = parse(FIXTURES / "malformed.md")
185
+ assert doc is not None
186
+ ```
187
+
188
+ ---
189
+
190
+ ### Step 6: Verify Code Quality Gates
191
+
192
+ Run the local quality checks:
193
+
194
+ ```bash
195
+ # 1. Lint and format checks
196
+ uv run ruff check .
197
+ uv run ruff format .
198
+
199
+ # 2. Run full test suite
200
+ uv run pytest tests/test_markdown.py -v
201
+ uv run pytest -v
202
+
203
+ # 3. Check memory limits
204
+ uv run python benchmarks/memory_profile.py
205
+ ```
206
+
207
+ ---
208
+
209
+ ### Step 7: Update Documentation and Changelog
210
+
211
+ 1. Add your format row to the Supported Formats table in `README.md`.
212
+ 2. Document the new format under the `[Unreleased]` or current release section in `docs/CHANGELOG.md`.
@@ -0,0 +1,164 @@
1
+ # Architecture and System Design
2
+
3
+ This document provides a comprehensive technical overview of `universal-doc-parser`. It details the internal data structures, algorithms, subsystem boundaries, and design rationales governing the document ingestion engine.
4
+
5
+ ---
6
+
7
+ ## 1. High-Level System Architecture
8
+
9
+ The parser follows a multi-stage, generator-driven pipeline designed for deterministic memory consumption (<250 MB RSS) and zero cloud API dependency.
10
+
11
+ ```
12
+ +-----------------------------------------------------------------------------------+
13
+ | Input Document |
14
+ +-----------------------------------------------------------------------------------+
15
+ |
16
+ v
17
+ +-----------------------------------------------------------------------------------+
18
+ | Stage 1: Sniffer (MIME & Byte Detection) |
19
+ | - Magic byte inspection via python-magic (optional OS fallback) |
20
+ | - Extension-based disambiguation and normalization |
21
+ +-----------------------------------------------------------------------------------+
22
+ |
23
+ v
24
+ +-----------------------------------------------------------------------------------+
25
+ | Stage 2: Router & Extractor Registry |
26
+ | - Maps FileType enum to registered BaseExtractor subclass |
27
+ | - Dynamic registry populated via @register decorators |
28
+ +-----------------------------------------------------------------------------------+
29
+ |
30
+ v
31
+ +-----------------------------------------------------------------------------------+
32
+ | Stage 3: Streaming Extractor Execution |
33
+ | - Yields Element generator stream page-by-page / record-by-record |
34
+ | - PDF: 2-Pass Table Extraction -> Font Percentile Headings -> Column Order |
35
+ | - Office / Compound: Recursive embedded asset unpacker |
36
+ | - Scanned / Image: RapidOCR ONNX CPU inference with deskewing |
37
+ +-----------------------------------------------------------------------------------+
38
+ |
39
+ v
40
+ +-----------------------------------------------------------------------------------+
41
+ | Stage 4: Normalizer & Tree Assembler |
42
+ | - UUID v4 assignment for document and elements |
43
+ | - Heading ancestry stack: links child paragraphs/tables to nearest parent_id |
44
+ | - Pydantic v2 strict schema validation |
45
+ +-----------------------------------------------------------------------------------+
46
+ |
47
+ v
48
+ +-----------------------------------------------------------------------------------+
49
+ | Stage 5: Downstream Export Layer |
50
+ | - to_markdown(): Pre-rendered GFM Markdown |
51
+ | - to_chunks(): Hierarchical token-budgeted chunks with breadcrumb ancestry |
52
+ | - to_graph(): Knowledge graph nodes and edges for GraphRAG |
53
+ | - FastMCP Server: Stdio tool interface for AI agents |
54
+ +-----------------------------------------------------------------------------------+
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 2. Core Ingestion Pipeline
60
+
61
+ ### 2.1 Sniffer (`universal_parser/core/sniffer.py`)
62
+ File identification relies on a multi-tier sniffing strategy:
63
+ 1. **Magic Bytes:** Reads the initial 2048 bytes of the file stream using `python-magic` to inspect file signatures (e.g., `%PDF-`, `PK\x03\x04`, `\xD0\xCF\x11\xE0`).
64
+ 2. **Ambiguity Resolution:** For plain text containers (where MIME is `text/plain` or `text/csv`), the file extension is evaluated against `_EXT_MAP` to disambiguate CSV, TSV, JSON, and XML.
65
+ 3. **Graceful Fallback:** If `python-magic` or `libmagic` is unavailable at the OS level, sniffing safely defaults to extension mapping without raising exceptions. Unrecognized formats return `FileType.UNKNOWN`.
66
+
67
+ ### 2.2 Router (`universal_parser/core/router.py`)
68
+ The router maintains an in-memory mapping of `FileType -> type[BaseExtractor]`. Extractors register themselves using the `@register` decorator upon module import. This decouples individual parser implementations from the core dispatch loop.
69
+
70
+ ### 2.3 Streaming Generator Pipeline (`universal_parser/core/engine.py`)
71
+ Every extractor implements `BaseExtractor.stream(path: Path) -> Iterator[Element]`. Memory consumption remains bounded because elements are yielded lazily as individual pages or records are processed, rather than assembling the entire document in memory.
72
+
73
+ ---
74
+
75
+ ## 3. PDF Extraction Subsystem
76
+
77
+ PDF documents represent the highest layout complexity. The PDF engine in `universal_parser/extractors/pdf/` uses a cooperative multi-pass strategy:
78
+
79
+ ```
80
+ PDF Page Stream
81
+ |
82
+ +---> Pass 1: Table Extraction (PDFTableExtractor)
83
+ | |-- Lattice Strategy: Explicit horizontal/vertical vector lines
84
+ | |-- Stream Strategy: Whitespace column clustering with density guardrails
85
+ | +-> Computes Table Bounding Boxes
86
+ |
87
+ +---> Pass 2: Native Text Extraction (pypdfium2 / pdfplumber)
88
+ | |-- Filters out words falling inside Table Bounding Boxes
89
+ | |-- Computes font size percentiles (H1: >=95th, H2: >=85th, H3: >=75th)
90
+ | |-- Column Band Clustering: partitions into Left, Right, Full-Width bands
91
+ | +-> Orders elements in natural reading sequence
92
+ |
93
+ +---> Scanned Fallback (if 0 text characters found)
94
+ +-> RapidOCR ONNX inference on CPU at 200 DPI with deskew preprocessing
95
+ ```
96
+
97
+ ### 3.1 Two-Pass Table Extraction (`tables.py`)
98
+ 1. **Lattice Strategy:** Identifies bordered tables by analyzing intersecting vector ruling lines using explicit coordinate snap and join tolerances (`snap_tolerance=3`, `join_tolerance=3`). Confidence is scored at `0.95+`.
99
+ 2. **Stream Strategy:** Identifies borderless tables using whitespace gaps and text alignment. False positives (such as multi-column paragraphs) are suppressed using two mathematical constraints:
100
+ - **Word Density Constraint:** Average words per non-empty cell must be $\le 2.5$. If cells contain paragraph sentences, the candidate table is rejected.
101
+ - **Height Ratio Constraint:** The table bounding box must not exceed 40% of the total page height ($table\_h \le 0.40 \times page\_h$).
102
+
103
+ ### 3.2 Font-Size Percentile Hierarchy (`native.py`)
104
+ Rather than relying on brittle hardcoded point sizes (e.g. `size > 14`), the parser samples the font size distribution across the document and calculates empirical percentiles:
105
+ - **H1 (Top-level Title):** $\ge 95\text{th percentile}$ (guaranteed $\ge \text{median} + 3.0\text{pt}$)
106
+ - **H2 (Section Header):** $\ge 85\text{th percentile}$ (guaranteed $\ge \text{median} + 1.5\text{pt}$)
107
+ - **H3 (Subsection Header):** $\ge 75\text{th percentile}$ (guaranteed $\ge \text{median} + 0.5\text{pt}$)
108
+ - **Paragraphs / Body Text:** All remaining text below the 75th percentile.
109
+
110
+ ### 3.3 Multi-Column Reading Order Reconstruction (`native.py`)
111
+ Elements are partitioned spatially relative to the page horizontal midpoint ($X_{\text{mid}} = \frac{\text{page\_width}}{2}$):
112
+ 1. **Full-Width Header Zone:** Spans with bounding box width $> 0.70 \times \text{page\_width}$ appearing near the top are yielded first.
113
+ 2. **Left Column:** Elements where $x_1 \le X_{\text{mid}} + 15\text{pt}$, sorted top-to-bottom ($y_0$).
114
+ 3. **Right Column:** Elements where $x_0 \ge X_{\text{mid}} - 15\text{pt}$, sorted top-to-bottom ($y_0$).
115
+ 4. **Full-Width Footer Zone:** Full-width elements at the bottom of the page.
116
+
117
+ ---
118
+
119
+ ## 4. Adaptive Layout Fingerprinting Subsystem (`universal_parser/adaptive/`)
120
+
121
+ In recurring document workflows (invoices, tax forms, financial 10-K filings), identical templates are ingested repeatedly. The adaptive subsystem enables self-tuning parameter caching for recurring document structures.
122
+
123
+ ```
124
+ Document Bounding Boxes
125
+ |
126
+ v
127
+ [ Spatial Grid Quantization ] -> 10x10 Content-Agnostic Occupancy Matrix
128
+ |
129
+ v
130
+ [ SHA-256 Fingerprint Hash ] -> Hex Digest + Spatial Histogram
131
+ |
132
+ v
133
+ [ TemplateConfigCache ] -> Exact Match OR Fuzzy Cosine-Jaccard Lookup (Threshold >= 0.85)
134
+ |
135
+ v
136
+ [ Auto-Tuned Extractor Config ] -> Injects optimal column/table thresholds into extractor
137
+ ```
138
+
139
+ 1. **Fingerprinter (`fingerprint.py`):** Normalizes all element coordinates to a reference 612x792 pt coordinate space and quantizes them into a 10x10 binary spatial occupancy grid. Combines the spatial grid with element type distribution proportions and hashes the payload with SHA-256.
140
+ 2. **Configuration Cache (`config_cache.py`):** Thread-safe LRU cache with atomic JSON disk persistence. Supports exact SHA-256 lookup and fuzzy Cosine-Jaccard similarity matching for documents with minor layout shifts.
141
+ 3. **Auto-Tuner (`tuner.py`):** Uses coordinate descent optimization against reference ground-truth data to automatically discover optimal extraction parameters (such as column split boundaries and table tolerances) without manual configuration.
142
+
143
+ ---
144
+
145
+ ## 5. Downstream RAG & Export Subsystem
146
+
147
+ ### 5.1 Hierarchical Chunking (`universal_parser/exports/to_chunks.py`)
148
+ Standard token-window chunking slices text arbitrarily, splitting paragraphs and losing title context. The hierarchical chunker:
149
+ - Accumulates elements while respecting a configurable token budget (`max_tokens`, default 512).
150
+ - Prepends full heading ancestry to every chunk: `Context: Executive Summary > Financial Results\n\n{text}`.
151
+ - Keeps tables intact within a single chunk whenever budget permits.
152
+
153
+ ### 5.2 Knowledge Graph Generator (`universal_parser/exports/to_graph.py`)
154
+ Generates GraphRAG-compatible nodes and edges:
155
+ - **Document Root Node:** Links metadata properties.
156
+ - **Section Nodes:** Represents headings with hierarchical `CONTAINS_SECTION` edges.
157
+ - **Content Nodes:** Paragraphs and tables linked to parent sections via `CONTAINS` edges and sequenced via `FOLLOWS` edges.
158
+
159
+ ---
160
+
161
+ ## 6. Observability and FastMCP Integration
162
+
163
+ 1. **Telemetry Collector (`universal_parser/observability/metrics.py`):** Thread-safe singleton capturing parsing latency, element distributions, scanned page occurrences, and structural anomalies per invocation. Exports standalone interactive HTML dashboards.
164
+ 2. **FastMCP Server (`universal_parser/mcp/server.py`):** Exposes `parse_document` and `list_supported_formats` tools over stdio for Claude Desktop, Cursor, and agent frameworks.
@@ -4,6 +4,22 @@ All notable changes to this project are documented in this file. Entries are org
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.0.2]
8
+
9
+ ### Release Context
10
+
11
+ Documentation and metadata release adding live Hugging Face Spaces integration links, project URLs, and comprehensive architecture specifications across the documentation suite.
12
+
13
+ ### Added
14
+
15
+ - Added live Hugging Face Space interactive demo badge and links to `README.md` and `pyproject.toml` (`[project.urls]`).
16
+ - Created complete `docs/ARCHITECTURE.md` specification detailing the two-pass extraction engine, font-size percentile hierarchy, and adaptive fingerprinting.
17
+ - Created complete `docs/SCHEMA.md` field-level reference covering all Pydantic models, type constraints, and null semantics.
18
+ - Created complete `docs/ADDING_A_FORMAT.md` step-by-step contributor implementation guide for extending format coverage.
19
+ - Added UI payload safeguards and thread ceilings in `main.py` to ensure smooth browser rendering on multi-page reports.
20
+
21
+ ---
22
+
7
23
  ## [1.0.1]
8
24
 
9
25
  ### Release Context
@@ -0,0 +1,183 @@
1
+ # Output Schema Specification
2
+
3
+ This document provides the canonical field-level specification for all output data models produced by `universal-doc-parser`. All models are implemented using Pydantic v2 (`universal_parser/core/schema.py`).
4
+
5
+ ---
6
+
7
+ ## 1. Top-Level Models
8
+
9
+ ### 1.1 `Document`
10
+
11
+ The root object returned by `parse(path)`.
12
+
13
+ | Field | Type | Required | Description |
14
+ |---|---|---|---|
15
+ | `schema_version` | `str` | Yes | Schema specification version. Always `"1.0"` in this release. |
16
+ | `doc_id` | `str` | Yes | Unique UUID v4 identifier generated at parse time. |
17
+ | `metadata` | `DocumentMetadata` | Yes | Document-level metadata (filename, detected format, page count). |
18
+ | `content_tree` | `list[Element]` | Yes | Ordered sequence of extracted document elements in natural reading order. |
19
+
20
+ ```json
21
+ {
22
+ "schema_version": "1.0",
23
+ "doc_id": "c8cdb60a-fc20-4e79-bb30-37218c8dba8a",
24
+ "metadata": {
25
+ "file_name": "annual_report.pdf",
26
+ "file_type": "pdf",
27
+ "page_count": 42,
28
+ "has_scanned_pages": false
29
+ },
30
+ "content_tree": [ ... ]
31
+ }
32
+ ```
33
+
34
+ ---
35
+
36
+ ### 1.2 `DocumentMetadata`
37
+
38
+ Captures file container properties and parsing telemetry.
39
+
40
+ | Field | Type | Required | Description |
41
+ |---|---|---|---|
42
+ | `file_name` | `str` | Yes | Original filename including file extension. |
43
+ | `file_type` | `str` | Yes | Normalized format identifier: `pdf`, `docx`, `xlsx`, `pptx`, `html`, `epub`, `csv`, `tsv`, `parquet`, `json`, `xml`, `eml`, `msg`, `mbox`, `image`, `doc`, `xls`, `ppt`. |
44
+ | `page_count` | `int or None` | No | Total pages in paginated documents. `null` for non-paginated formats (e.g. CSV, Parquet, JSON). |
45
+ | `has_scanned_pages` | `bool` | Yes | `true` if any page in the document required CPU OCR fallback processing. |
46
+
47
+ ---
48
+
49
+ ## 2. Element Models
50
+
51
+ ### 2.1 `Element`
52
+
53
+ The atomic unit of extracted content within `content_tree`.
54
+
55
+ | Field | Type | Required | Null Semantics / Invariants |
56
+ |---|---|---|---|
57
+ | `element_id` | `str` | Yes | Unique UUID v4 assigned to this specific element. |
58
+ | `type` | `str` | Yes | One of: `"heading"`, `"paragraph"`, `"table"`, `"figure"`, `"list_item"`, `"code_block"`. |
59
+ | `level` | `int or None` | No | Heading hierarchy depth ($1 \le \text{level} \le 6$). `null` for all non-heading elements. |
60
+ | `text` | `str or None` | No | Plain text content. `null` for pure table elements. |
61
+ | `page` | `int or None` | No | 1-indexed page number where the element appears. `null` for non-paginated files. |
62
+ | `bbox` | `BBox or None` | No | Spatial coordinates in standard PDF points (72 pt = 1 inch). `null` for non-spatial files. |
63
+ | `parent_id` | `str or None` | No | `element_id` of the nearest ancestor heading above this element. `null` for root-level items. |
64
+ | `data` | `TableData or None` | No | Structured grid data (`headers`, `rows`). Populated **only** when `type == "table"`. |
65
+ | `markdown_repr` | `str or None` | No | Pre-rendered GitHub-Flavored Markdown representation of the element. |
66
+ | `confidence` | `float or None` | No | Extraction confidence score in $[0.0, 1.0]$. Populated for OCR text and tables; `null` for digital text. |
67
+ | `vlm_description` | `str or None` | No | Optional visual description for images/figures. Reserved for multimodal extensions. |
68
+
69
+ ---
70
+
71
+ ### 2.2 `BBox` (Bounding Box)
72
+
73
+ Spatial coordinates for layout-aware document elements.
74
+
75
+ | Field | Type | Required | Description |
76
+ |---|---|---|---|
77
+ | `x0` | `float` | Yes | Left horizontal coordinate in PDF points from top-left origin. |
78
+ | `y0` | `float` | Yes | Top vertical coordinate in PDF points from top-left origin. |
79
+ | `x1` | `float` | Yes | Right horizontal coordinate in PDF points from top-left origin. |
80
+ | `y1` | `float` | Yes | Bottom vertical coordinate in PDF points from top-left origin. |
81
+
82
+ ---
83
+
84
+ ### 2.3 `TableData`
85
+
86
+ Structured tabular representation.
87
+
88
+ | Field | Type | Required | Description |
89
+ |---|---|---|---|
90
+ | `headers` | `list[str]` | Yes | Ordered list of table column header strings. |
91
+ | `rows` | `list[list[str]]` | Yes | 2D list of row cell values normalized to string representations. |
92
+
93
+ ```json
94
+ {
95
+ "headers": ["Quarter", "Revenue (USD)", "Operating Margin"],
96
+ "rows": [
97
+ ["Q1", "$1.2B", "24.5%"],
98
+ ["Q2", "$1.4B", "26.1%"]
99
+ ]
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 3. Element Type Examples
106
+
107
+ ### 3.1 Heading Element (H1)
108
+ ```json
109
+ {
110
+ "element_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
111
+ "type": "heading",
112
+ "level": 1,
113
+ "text": "1. Executive Summary",
114
+ "page": 1,
115
+ "bbox": { "x0": 72.0, "y0": 100.0, "x1": 540.0, "y1": 124.0 },
116
+ "parent_id": null,
117
+ "data": null,
118
+ "markdown_repr": "# 1. Executive Summary",
119
+ "confidence": null,
120
+ "vlm_description": null
121
+ }
122
+ ```
123
+
124
+ ### 3.2 Paragraph Element (Child of H1)
125
+ ```json
126
+ {
127
+ "element_id": "f2e3d4c5-b6a7-4890-1234-567890abcdef",
128
+ "type": "paragraph",
129
+ "level": null,
130
+ "text": "Operating cash flow reached record highs across all European business units.",
131
+ "page": 1,
132
+ "bbox": { "x0": 72.0, "y0": 135.0, "x1": 540.0, "y1": 160.0 },
133
+ "parent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
134
+ "data": null,
135
+ "markdown_repr": "Operating cash flow reached record highs across all European business units.",
136
+ "confidence": null,
137
+ "vlm_description": null
138
+ }
139
+ ```
140
+
141
+ ### 3.3 Table Element
142
+ ```json
143
+ {
144
+ "element_id": "99887766-5544-3322-1100-aabbccddeeff",
145
+ "type": "table",
146
+ "level": null,
147
+ "text": null,
148
+ "page": 2,
149
+ "bbox": { "x0": 72.0, "y0": 200.0, "x1": 540.0, "y1": 350.0 },
150
+ "parent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
151
+ "data": {
152
+ "headers": ["Region", "YoY Growth"],
153
+ "rows": [["APAC", "+18%"], ["EMEA", "+12%"]]
154
+ },
155
+ "markdown_repr": "| Region | YoY Growth |\n|---|---|\n| APAC | +18% |\n| EMEA | +12% |",
156
+ "confidence": 0.98,
157
+ "vlm_description": null
158
+ }
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 4. Export Models
164
+
165
+ ### 4.1 `Chunk` (Produced by `to_chunks()`)
166
+
167
+ | Field | Type | Description |
168
+ |---|---|---|
169
+ | `chunk_id` | `str` | Unique chunk identifier formatted as `{doc_id}-chunk-{index}`. |
170
+ | `text` | `str` | Full chunk text prepended with heading ancestry breadcrumbs: `Context: H1 > H2\n\n{text}`. |
171
+ | `headings` | `list[str]` | Ordered list of ancestor heading titles from root to nearest section. |
172
+ | `element_types` | `list[str]` | Deduplicated list of element types contained within this chunk. |
173
+ | `page_numbers` | `list[int]` | Sorted list of page numbers spanned by the chunk. |
174
+ | `estimated_tokens` | `int` | Approximate token count (estimated at ~4 characters per token). |
175
+
176
+ ---
177
+
178
+ ### 4.2 `KnowledgeGraph` (Produced by `to_graph()`)
179
+
180
+ | Field | Type | Description |
181
+ |---|---|---|
182
+ | `nodes` | `list[GraphNode]` | Graph vertices representing the document root, sections, and content elements. |
183
+ | `edges` | `list[GraphEdge]` | Directed relationship edges: `CONTAINS_SECTION`, `CONTAINS`, `FOLLOWS`. |