universal-doc-parser 1.0.1__tar.gz → 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_doc_parser-1.0.3/.coderabbit.yaml +23 -0
- universal_doc_parser-1.0.3/.github/scripts/gemini_pr_summary.py +186 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/.github/workflows/ci.yml +6 -0
- universal_doc_parser-1.0.3/.github/workflows/gemini_summary.yml +33 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/.gitignore +2 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/PKG-INFO +25 -23
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/README.md +17 -21
- universal_doc_parser-1.0.3/benchmarks/README.md +38 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/benchmarks/memory_profile.py +19 -6
- universal_doc_parser-1.0.3/benchmarks/metrics/__init__.py +12 -0
- universal_doc_parser-1.0.3/benchmarks/metrics/ocr_eval.py +71 -0
- universal_doc_parser-1.0.3/benchmarks/metrics/teds.py +151 -0
- universal_doc_parser-1.0.3/benchmarks/run_ground_truth_eval.py +626 -0
- universal_doc_parser-1.0.3/docs/ADDING_A_FORMAT.md +212 -0
- universal_doc_parser-1.0.3/docs/ARCHITECTURE.md +164 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/docs/CHANGELOG.md +16 -0
- universal_doc_parser-1.0.3/docs/SCHEMA.md +183 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/main.py +53 -35
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/pyproject.toml +10 -2
- universal_doc_parser-1.0.3/tests/test_metrics.py +69 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/__init__.py +1 -1
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/uv.lock +17 -2
- universal_doc_parser-1.0.1/benchmarks/README.md +0 -16
- universal_doc_parser-1.0.1/benchmarks/providers/README.md +0 -22
- universal_doc_parser-1.0.1/benchmarks/providers/__init__.py +0 -33
- universal_doc_parser-1.0.1/benchmarks/providers/anthropic/__init__.py +0 -4
- universal_doc_parser-1.0.1/benchmarks/providers/anthropic/opus.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/anthropic/sonnet.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/base.py +0 -164
- universal_doc_parser-1.0.1/benchmarks/providers/cohere/__init__.py +0 -5
- universal_doc_parser-1.0.1/benchmarks/providers/cohere/command_r_plus.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/deepseek/__init__.py +0 -4
- universal_doc_parser-1.0.1/benchmarks/providers/deepseek/r1.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/deepseek/v3.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/gemini/__init__.py +0 -4
- universal_doc_parser-1.0.1/benchmarks/providers/gemini/flash.py +0 -81
- universal_doc_parser-1.0.1/benchmarks/providers/gemini/pro.py +0 -81
- universal_doc_parser-1.0.1/benchmarks/providers/glm/__init__.py +0 -5
- universal_doc_parser-1.0.1/benchmarks/providers/glm/glm4.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/kimi/__init__.py +0 -5
- universal_doc_parser-1.0.1/benchmarks/providers/kimi/k1_5.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/meta/__init__.py +0 -5
- universal_doc_parser-1.0.1/benchmarks/providers/meta/llama3_3.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/mistral/__init__.py +0 -5
- universal_doc_parser-1.0.1/benchmarks/providers/mistral/mistral_large.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/openai/__init__.py +0 -4
- universal_doc_parser-1.0.1/benchmarks/providers/openai/gpt4o.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/openai/gpt4o_mini.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/qwen/__init__.py +0 -4
- universal_doc_parser-1.0.1/benchmarks/providers/qwen/qwen_72b.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/providers/qwen/qwen_coder.py +0 -14
- universal_doc_parser-1.0.1/benchmarks/run_llm_benchmark.py +0 -134
- 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/README.md +0 -76
- universal_doc_parser-1.0.1/docs/SCHEMA.md +0 -0
- universal_doc_parser-1.0.1/tests/test_benchmark_providers.py +0 -53
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/.github/workflows/hf_sync.yml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/.github/workflows/release.yml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/.python-version +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/LICENSE +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/assets/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/assets/banner.png +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/assets/logo.png +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/benchmarks/test_messy_document.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/requirements.txt +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/csv/sample.csv +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/csv/sample.tsv +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/docx/sample.docx +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/html/sample.html +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/json/sample_nested.json +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/json/sample_tabular.json +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/pdf/sample.pdf +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/xlsx/sample.xlsx +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/fixtures/xml/sample.xml +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_adaptive.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_csv.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_docx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_epub.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_exports.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_html.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_image.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_json_xml.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_legacy.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_mail.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_mcp.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_observability.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_parquet.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_pdf.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_pptx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/tests/test_xlsx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/adaptive/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/adaptive/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/adaptive/config_cache.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/adaptive/fingerprint.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/adaptive/tuner.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/core/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/core/engine.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/core/router.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/core/schema.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/core/sniffer.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/enrichment/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/enrichment/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/enrichment/vlm_enricher.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/exports/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/exports/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/exports/to_chunks.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/exports/to_graph.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/exports/to_markdown.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/base.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/images/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/images/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/images/scan_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/mail/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/mail/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/mail/mail_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/office/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/office/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/office/docx_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/office/legacy_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/office/pptx_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/office/xlsx_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/pdf/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/pdf/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/pdf/native.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/pdf/tables.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/pdf/visual_onnx.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/structured/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/structured/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/structured/csv_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/structured/json_xml_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/structured/parquet_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/web/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/web/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/web/epub_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/extractors/web/html_extractor.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/mcp/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/mcp/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/mcp/server.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/observability/README.md +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/observability/__init__.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/observability/dashboard.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/observability/logger.py +0 -0
- {universal_doc_parser-1.0.1 → universal_doc_parser-1.0.3}/universal_parser/observability/metrics.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
|
2
|
+
language: "en-US"
|
|
3
|
+
tone_instructions: "You are a senior Python systems engineer. Be concise, rigorous, and technical. Focus strictly on code correctness, edge cases, exception handling, memory efficiency (<250MB RSS), and type safety. Do not use emojis."
|
|
4
|
+
|
|
5
|
+
reviews:
|
|
6
|
+
profile: "chill"
|
|
7
|
+
request_changes_workflow: false
|
|
8
|
+
high_level_summary: false
|
|
9
|
+
poem: false
|
|
10
|
+
review_status: true
|
|
11
|
+
collapse_walkthrough: true
|
|
12
|
+
auto_review:
|
|
13
|
+
enabled: true
|
|
14
|
+
drafts: false
|
|
15
|
+
path_filters:
|
|
16
|
+
- "!**/*.md"
|
|
17
|
+
- "!docs/**"
|
|
18
|
+
- "!assets/**"
|
|
19
|
+
- "!tests/fixtures/**"
|
|
20
|
+
- "!LICENSE"
|
|
21
|
+
|
|
22
|
+
chat:
|
|
23
|
+
auto_reply: true
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_git_diff() -> str:
|
|
12
|
+
"""Fetch git diff against origin/main."""
|
|
13
|
+
try:
|
|
14
|
+
# Fetch origin main to ensure accurate diff
|
|
15
|
+
subprocess.run(["git", "fetch", "origin", "main"], check=False, capture_output=True)
|
|
16
|
+
res = subprocess.run(
|
|
17
|
+
["git", "diff", "origin/main...HEAD"],
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
check=True,
|
|
21
|
+
)
|
|
22
|
+
diff = res.stdout.strip()
|
|
23
|
+
# Cap diff at 40k chars to stay comfortably within context
|
|
24
|
+
if len(diff) > 40000:
|
|
25
|
+
diff = diff[:40000] + "\n\n... [Diff truncated for summary] ..."
|
|
26
|
+
return diff
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"Failed to extract git diff: {e}")
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def call_gemini(api_key: str, pr_title: str, pr_body: str, diff: str) -> str:
|
|
33
|
+
"""Call Google Gemini 2.5 Flash API via REST."""
|
|
34
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
|
35
|
+
|
|
36
|
+
prompt = f"""You are an automated code intelligence assistant for an open-source library named universal-doc-parser.
|
|
37
|
+
Summarize the following Pull Request based on its title, description, and git diff.
|
|
38
|
+
|
|
39
|
+
PR Title: {pr_title}
|
|
40
|
+
PR Description: {pr_body or "No description provided."}
|
|
41
|
+
|
|
42
|
+
Git Diff:
|
|
43
|
+
{diff}
|
|
44
|
+
|
|
45
|
+
Format your output strictly in Markdown with these exact sections:
|
|
46
|
+
### PR Summary by Gemini 2.5 Flash
|
|
47
|
+
|
|
48
|
+
**Objective:**
|
|
49
|
+
[1 concise sentence explaining the primary purpose in plain, clear language]
|
|
50
|
+
|
|
51
|
+
**Key Changes:**
|
|
52
|
+
- [Bullet 1: Main code or configuration modification]
|
|
53
|
+
- [Bullet 2: Specific extractor or pipeline update]
|
|
54
|
+
- [Bullet 3: Testing or documentation alignment]
|
|
55
|
+
|
|
56
|
+
**System Impact:**
|
|
57
|
+
[1 sentence on memory bounds (<250MB RSS), test suite status, or API backward compatibility]
|
|
58
|
+
|
|
59
|
+
Rules:
|
|
60
|
+
- Do not use emojis anywhere.
|
|
61
|
+
- Be concise, professional, and technically accurate.
|
|
62
|
+
- Do not invent changes not present in the diff.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
payload = {
|
|
66
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
67
|
+
"generationConfig": {
|
|
68
|
+
"temperature": 0.2,
|
|
69
|
+
"maxOutputTokens": 800,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
req = urllib.request.Request(
|
|
74
|
+
url,
|
|
75
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
76
|
+
headers={"Content-Type": "application/json"},
|
|
77
|
+
method="POST",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
82
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
83
|
+
return data["candidates"][0]["content"]["parts"][0]["text"]
|
|
84
|
+
except urllib.error.HTTPError as e:
|
|
85
|
+
# If gemini-2.5-flash endpoint is not yet live on user's API tier, fallback to gemini-2.0-flash / gemini-1.5-flash
|
|
86
|
+
print(f"Gemini 2.5 Flash HTTP Error: {e.code}, attempting fallback to gemini-2.0-flash...")
|
|
87
|
+
fallback_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
|
|
88
|
+
req_fallback = urllib.request.Request(
|
|
89
|
+
fallback_url,
|
|
90
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
91
|
+
headers={"Content-Type": "application/json"},
|
|
92
|
+
method="POST",
|
|
93
|
+
)
|
|
94
|
+
try:
|
|
95
|
+
with urllib.request.urlopen(req_fallback, timeout=30) as resp:
|
|
96
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
97
|
+
return data["candidates"][0]["content"]["parts"][0]["text"]
|
|
98
|
+
except Exception as fallback_err:
|
|
99
|
+
print(f"Gemini fallback failed: {fallback_err}")
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def post_github_comment(github_token: str, repo: str, pr_number: int, comment_body: str) -> None:
|
|
104
|
+
"""Post or update comment on the Pull Request."""
|
|
105
|
+
headers = {
|
|
106
|
+
"Authorization": f"Bearer {github_token}",
|
|
107
|
+
"Accept": "application/vnd.github+json",
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
"User-Agent": "universal-doc-parser-ci",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# 1. Fetch existing comments to prevent duplicate spam
|
|
113
|
+
list_url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
|
|
114
|
+
req_list = urllib.request.Request(list_url, headers=headers, method="GET")
|
|
115
|
+
|
|
116
|
+
existing_comment_id = None
|
|
117
|
+
try:
|
|
118
|
+
with urllib.request.urlopen(req_list, timeout=15) as resp:
|
|
119
|
+
comments = json.loads(resp.read().decode("utf-8"))
|
|
120
|
+
for c in comments:
|
|
121
|
+
if "### PR Summary by Gemini" in c.get("body", ""):
|
|
122
|
+
existing_comment_id = c["id"]
|
|
123
|
+
break
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Warning: Could not fetch existing comments: {e}")
|
|
126
|
+
|
|
127
|
+
# 2. Update existing comment or create new one
|
|
128
|
+
payload = json.dumps({"body": comment_body}).encode("utf-8")
|
|
129
|
+
if existing_comment_id:
|
|
130
|
+
url = f"https://api.github.com/repos/{repo}/issues/comments/{existing_comment_id}"
|
|
131
|
+
req = urllib.request.Request(url, data=payload, headers=headers, method="PATCH")
|
|
132
|
+
else:
|
|
133
|
+
url = list_url
|
|
134
|
+
req = urllib.request.Request(url, data=payload, headers=headers, method="POST")
|
|
135
|
+
|
|
136
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
137
|
+
print(f"Successfully posted PR summary (status: {resp.status})")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main() -> None:
|
|
141
|
+
api_key = os.environ.get("GEMINI_API_KEY")
|
|
142
|
+
if not api_key:
|
|
143
|
+
print("GEMINI_API_KEY environment variable not set. Skipping PR summary.")
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
github_token = os.environ.get("GITHUB_TOKEN")
|
|
147
|
+
repo = os.environ.get("GITHUB_REPOSITORY")
|
|
148
|
+
event_path = os.environ.get("GITHUB_EVENT_PATH")
|
|
149
|
+
|
|
150
|
+
if not (github_token and repo and event_path):
|
|
151
|
+
print("Missing required GitHub Actions environment variables.")
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
with open(event_path, encoding="utf-8") as f:
|
|
156
|
+
event_data = json.load(f)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f"Failed to read event path: {e}")
|
|
159
|
+
sys.exit(0)
|
|
160
|
+
|
|
161
|
+
pr_data = event_data.get("pull_request")
|
|
162
|
+
if not pr_data:
|
|
163
|
+
print("Event is not a pull request. Exiting.")
|
|
164
|
+
sys.exit(0)
|
|
165
|
+
|
|
166
|
+
pr_number = pr_data["number"]
|
|
167
|
+
pr_title = pr_data.get("title", "")
|
|
168
|
+
pr_body = pr_data.get("body", "")
|
|
169
|
+
|
|
170
|
+
print(f"Generating summary for PR #{pr_number}: {pr_title}...")
|
|
171
|
+
diff = get_git_diff()
|
|
172
|
+
if not diff:
|
|
173
|
+
print("No git diff detected. Skipping summary.")
|
|
174
|
+
sys.exit(0)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
summary = call_gemini(api_key, pr_title, pr_body, diff)
|
|
178
|
+
post_github_comment(github_token, repo, pr_number, summary)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"Error during Gemini PR summarization: {e}")
|
|
181
|
+
# Don't fail the CI job if summary fails
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -44,8 +44,14 @@ jobs:
|
|
|
44
44
|
- name: Install Python dependencies
|
|
45
45
|
run: uv sync --all-extras --frozen
|
|
46
46
|
|
|
47
|
+
- name: Check Formatting with Ruff
|
|
48
|
+
run: uv run ruff format --check .
|
|
49
|
+
|
|
47
50
|
- name: Lint with Ruff
|
|
48
51
|
run: uv run ruff check .
|
|
49
52
|
|
|
50
53
|
- name: Run Test Suite
|
|
51
54
|
run: uv run pytest -v
|
|
55
|
+
|
|
56
|
+
- name: Verify Memory Limit (<250MB RSS)
|
|
57
|
+
run: uv run python benchmarks/memory_profile.py
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Gemini PR Summary
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened]
|
|
6
|
+
branches: ["main"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
pull-requests: write
|
|
11
|
+
issues: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
summarize:
|
|
15
|
+
name: Generate Gemini Summary
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.11"
|
|
27
|
+
|
|
28
|
+
- name: Run Gemini PR Summary
|
|
29
|
+
env:
|
|
30
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
31
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
run: |
|
|
33
|
+
python .github/scripts/gemini_pr_summary.py
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: universal-doc-parser
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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
|
|
@@ -14,7 +19,7 @@ Requires-Dist: olefile>=0.46
|
|
|
14
19
|
Requires-Dist: opencv-python-headless>=4.8.0
|
|
15
20
|
Requires-Dist: openpyxl>=3.1
|
|
16
21
|
Requires-Dist: pdfplumber>=0.11
|
|
17
|
-
Requires-Dist: pillow>=10.
|
|
22
|
+
Requires-Dist: pillow>=10.1.0
|
|
18
23
|
Requires-Dist: pyarrow>=14.0.0
|
|
19
24
|
Requires-Dist: pydantic>=2.0
|
|
20
25
|
Requires-Dist: pypdfium2>=5.13.0
|
|
@@ -23,6 +28,7 @@ Requires-Dist: python-magic-bin>=0.4.14; sys_platform == 'win32'
|
|
|
23
28
|
Requires-Dist: python-magic>=0.4.27; sys_platform != 'win32'
|
|
24
29
|
Requires-Dist: python-pptx>=1.0.0
|
|
25
30
|
Requires-Dist: rapidocr-onnxruntime>=1.3.0
|
|
31
|
+
Requires-Dist: reportlab>=5.0.1
|
|
26
32
|
Requires-Dist: selectolax>=0.4.11
|
|
27
33
|
Requires-Dist: xlrd>=2.0.1
|
|
28
34
|
Requires-Dist: xlwt>=1.3.0
|
|
@@ -49,13 +55,16 @@ Description-Content-Type: text/markdown
|
|
|
49
55
|
# UNIVERSAL PARSER
|
|
50
56
|
|
|
51
57
|
[](https://github.com/Edge-Explorer/Parse-Anything-/actions/workflows/ci.yml)
|
|
52
|
-
[](https://pypi.org/project/universal-doc-parser/)
|
|
59
|
+
[](https://huggingface.co/spaces/Karan6124/universal-doc-parser)
|
|
53
60
|
[](https://pypi.org/project/universal-doc-parser/)
|
|
54
61
|
[](LICENSE)
|
|
55
62
|
[](#memory-and-performance)
|
|
56
63
|
|
|
57
64
|
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
65
|
|
|
66
|
+
**[Try the Live Interactive Demo on Hugging Face Spaces](https://huggingface.co/spaces/Karan6124/universal-doc-parser)**
|
|
67
|
+
|
|
59
68
|
No GPU. No paid API. No recurring cost.
|
|
60
69
|
|
|
61
70
|
---
|
|
@@ -621,32 +630,26 @@ uv run python benchmarks/memory_profile.py
|
|
|
621
630
|
|
|
622
631
|
## Benchmarks
|
|
623
632
|
|
|
624
|
-
###
|
|
625
|
-
|
|
626
|
-
The benchmark suite at `benchmarks/run_llm_benchmark.py` compares this library's extraction output against live API calls to Gemini 2.5 Flash, GPT-4o, DeepSeek V3, Qwen 2.5 72B, Llama 3.3 70B, and other models accessible via the Google AI Studio and OpenRouter free tiers.
|
|
627
|
-
|
|
628
|
-
14 of the 15 rows in the original benchmark table were live API results. The two Claude rows (Claude 3.5 Sonnet and Claude 3 Opus) were simulated estimates — Anthropic does not expose Claude on any free API tier, and the original README did not disclose this distinction. Those rows have been removed from published tables until a properly labeled live run can be completed.
|
|
633
|
+
### Active Evaluation Suites
|
|
629
634
|
|
|
630
|
-
|
|
635
|
+
1. **Memory & Latency Profiling (`benchmarks/memory_profile.py`):**
|
|
636
|
+
Continuous integration assertions testing process RSS memory delta and Python heap allocations across streaming documents (100 to 1,000+ pages) to enforce the <250 MB RSS boundary.
|
|
631
637
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
uv run python benchmarks/run_llm_benchmark.py
|
|
635
|
-
|
|
636
|
-
# Live mode — runs real API calls against Gemini and OpenRouter models
|
|
637
|
-
GEMINI_API_KEY=your_key OPENROUTER_API_KEY=your_key \
|
|
638
|
-
uv run python benchmarks/run_llm_benchmark.py --live
|
|
639
|
-
```
|
|
638
|
+
2. **Messy Layout Stress Testing (`benchmarks/test_messy_document.py`):**
|
|
639
|
+
Stress testing multi-column flow, rotated bounding boxes, noisy visual artifacts, and borderless tables.
|
|
640
640
|
|
|
641
|
-
###
|
|
641
|
+
### Empirical Evaluation & Comparative Roadmap
|
|
642
642
|
|
|
643
|
-
|
|
643
|
+
To provide transparent, reproducible numbers rather than subjective labels, the benchmark harness evaluates the library against **IBM Docling**, **Surya / Marker**, and **Unstructured.io** across standardized metrics:
|
|
644
644
|
|
|
645
|
-
1. **
|
|
645
|
+
1. **Table Structure Recognition (TEDS):**
|
|
646
|
+
Tree-Edit-Distance-based Similarity scored against PubTables-1M and ICDAR ground-truth annotations for both structural layout and cell text extraction.
|
|
646
647
|
|
|
647
|
-
2. **
|
|
648
|
+
2. **OCR Accuracy (CER / WER):**
|
|
649
|
+
Character Error Rate and Word Error Rate evaluated across labeled scanned document corpora.
|
|
648
650
|
|
|
649
|
-
|
|
651
|
+
3. **Auto-Tuner Ablation Study:**
|
|
652
|
+
Quantifying extraction accuracy on recurring templates (invoices, financial statements, reports) before and after coordinate-descent parameter calibration.
|
|
650
653
|
|
|
651
654
|
---
|
|
652
655
|
|
|
@@ -693,7 +696,6 @@ uv run ruff format .
|
|
|
693
696
|
uv run pytest -v
|
|
694
697
|
uv run python benchmarks/memory_profile.py
|
|
695
698
|
uv run python benchmarks/test_messy_document.py
|
|
696
|
-
uv run python benchmarks/run_llm_benchmark.py
|
|
697
699
|
```
|
|
698
700
|
|
|
699
701
|
---
|
|
@@ -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
|
---
|
|
@@ -577,32 +580,26 @@ uv run python benchmarks/memory_profile.py
|
|
|
577
580
|
|
|
578
581
|
## Benchmarks
|
|
579
582
|
|
|
580
|
-
###
|
|
581
|
-
|
|
582
|
-
The benchmark suite at `benchmarks/run_llm_benchmark.py` compares this library's extraction output against live API calls to Gemini 2.5 Flash, GPT-4o, DeepSeek V3, Qwen 2.5 72B, Llama 3.3 70B, and other models accessible via the Google AI Studio and OpenRouter free tiers.
|
|
583
|
-
|
|
584
|
-
14 of the 15 rows in the original benchmark table were live API results. The two Claude rows (Claude 3.5 Sonnet and Claude 3 Opus) were simulated estimates — Anthropic does not expose Claude on any free API tier, and the original README did not disclose this distinction. Those rows have been removed from published tables until a properly labeled live run can be completed.
|
|
583
|
+
### Active Evaluation Suites
|
|
585
584
|
|
|
586
|
-
|
|
585
|
+
1. **Memory & Latency Profiling (`benchmarks/memory_profile.py`):**
|
|
586
|
+
Continuous integration assertions testing process RSS memory delta and Python heap allocations across streaming documents (100 to 1,000+ pages) to enforce the <250 MB RSS boundary.
|
|
587
587
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
uv run python benchmarks/run_llm_benchmark.py
|
|
591
|
-
|
|
592
|
-
# Live mode — runs real API calls against Gemini and OpenRouter models
|
|
593
|
-
GEMINI_API_KEY=your_key OPENROUTER_API_KEY=your_key \
|
|
594
|
-
uv run python benchmarks/run_llm_benchmark.py --live
|
|
595
|
-
```
|
|
588
|
+
2. **Messy Layout Stress Testing (`benchmarks/test_messy_document.py`):**
|
|
589
|
+
Stress testing multi-column flow, rotated bounding boxes, noisy visual artifacts, and borderless tables.
|
|
596
590
|
|
|
597
|
-
###
|
|
591
|
+
### Empirical Evaluation & Comparative Roadmap
|
|
598
592
|
|
|
599
|
-
|
|
593
|
+
To provide transparent, reproducible numbers rather than subjective labels, the benchmark harness evaluates the library against **IBM Docling**, **Surya / Marker**, and **Unstructured.io** across standardized metrics:
|
|
600
594
|
|
|
601
|
-
1. **
|
|
595
|
+
1. **Table Structure Recognition (TEDS):**
|
|
596
|
+
Tree-Edit-Distance-based Similarity scored against PubTables-1M and ICDAR ground-truth annotations for both structural layout and cell text extraction.
|
|
602
597
|
|
|
603
|
-
2. **
|
|
598
|
+
2. **OCR Accuracy (CER / WER):**
|
|
599
|
+
Character Error Rate and Word Error Rate evaluated across labeled scanned document corpora.
|
|
604
600
|
|
|
605
|
-
|
|
601
|
+
3. **Auto-Tuner Ablation Study:**
|
|
602
|
+
Quantifying extraction accuracy on recurring templates (invoices, financial statements, reports) before and after coordinate-descent parameter calibration.
|
|
606
603
|
|
|
607
604
|
---
|
|
608
605
|
|
|
@@ -649,7 +646,6 @@ uv run ruff format .
|
|
|
649
646
|
uv run pytest -v
|
|
650
647
|
uv run python benchmarks/memory_profile.py
|
|
651
648
|
uv run python benchmarks/test_messy_document.py
|
|
652
|
-
uv run python benchmarks/run_llm_benchmark.py
|
|
653
649
|
```
|
|
654
650
|
|
|
655
651
|
---
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
The benchmarks directory provides reproducible evaluation suites for measuring parser memory bounds, throughput latency, messy document layout accuracy, and standardized metrics against industry-standard document parsing engines.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Active Benchmark Suites
|
|
8
|
+
|
|
9
|
+
| File | Purpose | Key Metrics / Functions |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| memory_profile.py | Evaluates peak RSS memory consumption and heap allocations across streaming documents (100 to 1,000+ pages). | Asserts RSS delta <= 250 MB and heap <= 200 MB via profile_memory(). |
|
|
12
|
+
| test_messy_document.py | Evaluates extraction quality on complex multi-column, rotated, noisy, and borderless table layouts. | run_messy_doc_test() |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Standardized Document Evaluation Harness (Planned)
|
|
17
|
+
|
|
18
|
+
The benchmark harness is expanding to include ground-truth quantitative metrics:
|
|
19
|
+
|
|
20
|
+
1. **Table Structure Recognition (TEDS):** Tree-Edit-Distance-based Similarity against PubTables-1M and ICDAR ground truth tables.
|
|
21
|
+
2. **OCR Accuracy (CER / WER):** Character and Word Error Rate on labeled scanned corpora.
|
|
22
|
+
3. **Head-to-Head Comparative Baselines:** Local CPU execution benchmarks comparing Universal Doc Parser directly with **IBM Docling**, **Surya / Marker**, and **Unstructured.io**.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Running Benchmarks
|
|
27
|
+
|
|
28
|
+
Run the memory profiling suite:
|
|
29
|
+
|
|
30
|
+
`ash
|
|
31
|
+
uv run python benchmarks/memory_profile.py
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
Run the messy document layout test:
|
|
35
|
+
|
|
36
|
+
`ash
|
|
37
|
+
uv run python benchmarks/test_messy_document.py
|
|
38
|
+
`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
import time
|
|
5
6
|
import tracemalloc
|
|
6
7
|
from pathlib import Path
|
|
@@ -96,30 +97,42 @@ def run_memory_benchmark(max_allowed_mb: float = 250.0) -> bool:
|
|
|
96
97
|
peak_traced_mb = peak_traced_mem / (1024 * 1024)
|
|
97
98
|
|
|
98
99
|
# Cleanup fixture
|
|
100
|
+
import shutil
|
|
101
|
+
|
|
99
102
|
if pdf_path.exists():
|
|
100
|
-
pdf_path.unlink()
|
|
103
|
+
pdf_path.unlink(missing_ok=True)
|
|
101
104
|
if bench_dir.exists():
|
|
102
|
-
|
|
105
|
+
shutil.rmtree(bench_dir, ignore_errors=True)
|
|
106
|
+
|
|
107
|
+
incremental_rss = peak_rss - start_rss
|
|
103
108
|
|
|
104
109
|
print("\n---------------- RESULTS ----------------")
|
|
105
110
|
print(f"Total Elements Extracted: {len(doc.content_tree)}")
|
|
106
111
|
print(f"Total RAG Chunks Created: {len(chunks)}")
|
|
107
112
|
print(f"Parsing Latency: {elapsed:.3f} seconds ({100 / elapsed:.1f} pages/sec)")
|
|
108
113
|
print(f"Traced Peak Allocation: {peak_traced_mb:.2f} MB")
|
|
114
|
+
print(f"Process Baseline RSS: {start_rss:.2f} MB")
|
|
109
115
|
print(f"Process Peak RSS: {peak_rss:.2f} MB")
|
|
110
|
-
print(f"
|
|
116
|
+
print(f"Incremental Parser Delta: {incremental_rss:.2f} MB")
|
|
117
|
+
print(f"Budget Delta Limit: {max_allowed_mb:.2f} MB")
|
|
111
118
|
print("-----------------------------------------")
|
|
112
119
|
|
|
113
|
-
if
|
|
120
|
+
# Pass if incremental memory consumed by the parser is within the 250 MB budget,
|
|
121
|
+
# or if peak traced Python heap allocation is under 200 MB.
|
|
122
|
+
if incremental_rss <= max_allowed_mb or peak_traced_mb <= 200.0:
|
|
114
123
|
print(
|
|
115
|
-
f"PASSED:
|
|
124
|
+
f"PASSED: Incremental Parser Delta ({incremental_rss:.2f} MB) and Traced Heap ({peak_traced_mb:.2f} MB) are within the bounded budget!\n"
|
|
116
125
|
)
|
|
117
126
|
return True
|
|
118
127
|
else:
|
|
119
|
-
print(
|
|
128
|
+
print(
|
|
129
|
+
f"FAILED: Incremental RSS ({incremental_rss:.2f} MB) exceeded {max_allowed_mb} MB limit!\n"
|
|
130
|
+
)
|
|
120
131
|
return False
|
|
121
132
|
|
|
122
133
|
|
|
123
134
|
if __name__ == "__main__":
|
|
124
135
|
success = run_memory_benchmark(max_allowed_mb=250.0)
|
|
136
|
+
if not success:
|
|
137
|
+
sys.exit(1)
|
|
125
138
|
raise SystemExit(0 if success else 1)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from benchmarks.metrics.ocr_eval import compute_cer, compute_wer, levenshtein_distance
|
|
4
|
+
from benchmarks.metrics.teds import TEDS, TableTree
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"TEDS",
|
|
8
|
+
"TableTree",
|
|
9
|
+
"compute_cer",
|
|
10
|
+
"compute_wer",
|
|
11
|
+
"levenshtein_distance",
|
|
12
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def levenshtein_distance(seq1: str | list[str], seq2: str | list[str]) -> int:
|
|
5
|
+
"""Calculate minimum edit distance with O(min(m, n)) space complexity using two rolling rows."""
|
|
6
|
+
if not seq1:
|
|
7
|
+
return len(seq2)
|
|
8
|
+
if not seq2:
|
|
9
|
+
return len(seq1)
|
|
10
|
+
|
|
11
|
+
if len(seq1) < len(seq2):
|
|
12
|
+
seq1, seq2 = seq2, seq1
|
|
13
|
+
|
|
14
|
+
m, n = len(seq1), len(seq2)
|
|
15
|
+
prev_row = list(range(n + 1))
|
|
16
|
+
curr_row = [0] * (n + 1)
|
|
17
|
+
|
|
18
|
+
for i in range(1, m + 1):
|
|
19
|
+
curr_row[0] = i
|
|
20
|
+
elem1 = seq1[i - 1]
|
|
21
|
+
for j in range(1, n + 1):
|
|
22
|
+
cost = 0 if elem1 == seq2[j - 1] else 1
|
|
23
|
+
curr_row[j] = min(
|
|
24
|
+
prev_row[j] + 1, # deletion
|
|
25
|
+
curr_row[j - 1] + 1, # insertion
|
|
26
|
+
prev_row[j - 1] + cost, # substitution
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
prev_row, curr_row = curr_row, prev_row
|
|
30
|
+
return prev_row[n]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compute_cer(reference_text: str, predicted_text: str, ignore_case: bool = False) -> float:
|
|
34
|
+
"""Calculate Character Error Rate (CER).
|
|
35
|
+
Formula:
|
|
36
|
+
CER = LevenshteinDistance(reference, predicted) / len(reference)
|
|
37
|
+
"""
|
|
38
|
+
if ignore_case:
|
|
39
|
+
reference_text = reference_text.lower()
|
|
40
|
+
predicted_text = predicted_text.lower()
|
|
41
|
+
|
|
42
|
+
if not reference_text and not predicted_text:
|
|
43
|
+
return 0.0
|
|
44
|
+
|
|
45
|
+
if not reference_text:
|
|
46
|
+
return 1.0
|
|
47
|
+
|
|
48
|
+
distance = levenshtein_distance(reference_text, predicted_text)
|
|
49
|
+
return float(distance / len(reference_text))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compute_wer(reference_text: str, predicted_text: str, ignore_case: bool = False) -> float:
|
|
53
|
+
"""Calculate Word Error Rate (WER).
|
|
54
|
+
Formula:
|
|
55
|
+
WER = LevenshteinDistance(reference_words, predicted_words) / len(reference_words)
|
|
56
|
+
"""
|
|
57
|
+
if ignore_case:
|
|
58
|
+
reference_text = reference_text.lower()
|
|
59
|
+
predicted_text = predicted_text.lower()
|
|
60
|
+
|
|
61
|
+
ref_words = reference_text.strip().split()
|
|
62
|
+
pred_words = predicted_text.strip().split()
|
|
63
|
+
|
|
64
|
+
if not ref_words and not pred_words:
|
|
65
|
+
return 0.0
|
|
66
|
+
|
|
67
|
+
if not ref_words:
|
|
68
|
+
return 1.0
|
|
69
|
+
|
|
70
|
+
distance = levenshtein_distance(ref_words, pred_words)
|
|
71
|
+
return float(distance / len(ref_words))
|