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.
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/PKG-INFO +10 -2
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/README.md +4 -1
- universal_doc_parser-1.0.2/docs/ADDING_A_FORMAT.md +212 -0
- universal_doc_parser-1.0.2/docs/ARCHITECTURE.md +164 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/docs/CHANGELOG.md +16 -0
- universal_doc_parser-1.0.2/docs/SCHEMA.md +183 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/main.py +53 -35
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/pyproject.toml +8 -1
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/__init__.py +1 -1
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/uv.lock +1 -1
- universal_doc_parser-1.0.1/docs/ADDING_A_FORMAT.md +0 -0
- universal_doc_parser-1.0.1/docs/ARCHITECTURE.md +0 -0
- universal_doc_parser-1.0.1/docs/SCHEMA.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.github/workflows/ci.yml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.github/workflows/hf_sync.yml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.github/workflows/release.yml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.gitignore +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/.python-version +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/LICENSE +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/assets/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/assets/banner.png +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/assets/logo.png +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/memory_profile.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/anthropic/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/anthropic/opus.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/anthropic/sonnet.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/base.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/cohere/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/cohere/command_r_plus.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/deepseek/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/deepseek/r1.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/deepseek/v3.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/gemini/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/gemini/flash.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/gemini/pro.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/glm/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/glm/glm4.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/kimi/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/kimi/k1_5.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/meta/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/meta/llama3_3.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/mistral/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/mistral/mistral_large.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/openai/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/openai/gpt4o.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/openai/gpt4o_mini.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/qwen/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/qwen/qwen_72b.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/providers/qwen/qwen_coder.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/run_llm_benchmark.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/benchmarks/test_messy_document.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/docs/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/requirements.txt +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/csv/sample.csv +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/csv/sample.tsv +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/docx/sample.docx +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/html/sample.html +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/json/sample_nested.json +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/json/sample_tabular.json +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/pdf/sample.pdf +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/xlsx/sample.xlsx +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/fixtures/xml/sample.xml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_adaptive.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_benchmark_providers.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_csv.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_docx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_epub.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_exports.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_html.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_image.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_json_xml.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_legacy.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_mail.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_mcp.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_observability.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_parquet.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_pdf.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_pptx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/tests/test_xlsx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/config_cache.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/fingerprint.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/adaptive/tuner.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/engine.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/router.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/schema.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/core/sniffer.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/enrichment/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/enrichment/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/enrichment/vlm_enricher.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/to_chunks.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/to_graph.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/exports/to_markdown.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/base.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/images/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/images/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/images/scan_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/mail/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/mail/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/mail/mail_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/docx_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/legacy_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/pptx_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/office/xlsx_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/native.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/tables.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/pdf/visual_onnx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/csv_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/json_xml_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/structured/parquet_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/epub_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/extractors/web/html_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/mcp/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/mcp/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/mcp/server.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/dashboard.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.2}/universal_parser/observability/logger.py +0 -0
- {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.
|
|
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
|
[](https://github.com/Edge-Explorer/Parse-Anything-/actions/workflows/ci.yml)
|
|
52
|
-
[](https://pypi.org/project/universal-doc-parser/)
|
|
58
|
+
[](https://huggingface.co/spaces/Karan6124/universal-doc-parser)
|
|
53
59
|
[](https://pypi.org/project/universal-doc-parser/)
|
|
54
60
|
[](LICENSE)
|
|
55
61
|
[](#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
|
[](https://github.com/Edge-Explorer/Parse-Anything-/actions/workflows/ci.yml)
|
|
8
|
-
[](https://pypi.org/project/universal-doc-parser/)
|
|
9
|
+
[](https://huggingface.co/spaces/Karan6124/universal-doc-parser)
|
|
9
10
|
[](https://pypi.org/project/universal-doc-parser/)
|
|
10
11
|
[](LICENSE)
|
|
11
12
|
[](#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`. |
|