tinydoc 0.1.1__tar.gz → 0.2.0__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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include MANIFEST.in
3
+ recursive-include tinydoc/schemas *.json
tinydoc-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinydoc
3
+ Version: 0.2.0
4
+ Summary: Local-first grounded document extraction SDK — schema, evidence, confidence. Free Ollama/Qwen engine by default; no API key.
5
+ Home-page: https://github.com/eulogik/TinyDoc-VLM
6
+ Author: eulogik
7
+ Author-email: hello@eulogik.com
8
+ Project-URL: Model status, https://github.com/eulogik/TinyDoc-VLM/blob/main/docs/v3_plan.md
9
+ Project-URL: HuggingFace Space, https://huggingface.co/spaces/eulogik/TinyDoc-VLM
10
+ Project-URL: Bug Tracker, https://github.com/eulogik/TinyDoc-VLM/issues
11
+ Project-URL: Documentation, https://github.com/eulogik/TinyDoc-VLM#readme
12
+ Project-URL: Benchmarks, https://github.com/eulogik/TinyDoc-VLM/blob/main/docs/BENCHMARKS.md
13
+ Project-URL: Website, https://eulogik.github.io/TinyDoc-VLM/
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Intended Audience :: Developers
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: pydantic>=2.8.0
23
+ Requires-Dist: pillow>=10.0.0
24
+ Requires-Dist: jsonschema>=4.20.0
25
+ Provides-Extra: ocr
26
+ Requires-Dist: pytesseract>=0.3.10; extra == "ocr"
27
+ Provides-Extra: ocr-pp
28
+ Requires-Dist: rapidocr-onnxruntime>=1.3.0; extra == "ocr-pp"
29
+ Provides-Extra: smolvlm2
30
+ Requires-Dist: torch>=2.2.0; extra == "smolvlm2"
31
+ Requires-Dist: transformers>=4.48.0; extra == "smolvlm2"
32
+ Requires-Dist: numpy>=1.26.0; extra == "smolvlm2"
33
+ Requires-Dist: sentencepiece>=0.2.0; extra == "smolvlm2"
34
+ Provides-Extra: onnx
35
+ Requires-Dist: onnxruntime>=1.19.0; extra == "onnx"
36
+ Requires-Dist: optimum>=1.22.0; extra == "onnx"
37
+ Provides-Extra: all
38
+ Requires-Dist: pytesseract>=0.3.10; extra == "all"
39
+ Requires-Dist: rapidocr-onnxruntime>=1.3.0; extra == "all"
40
+ Requires-Dist: torch>=2.2.0; extra == "all"
41
+ Requires-Dist: transformers>=4.48.0; extra == "all"
42
+ Requires-Dist: numpy>=1.26.0; extra == "all"
43
+ Requires-Dist: sentencepiece>=0.2.0; extra == "all"
44
+ Dynamic: author
45
+ Dynamic: author-email
46
+ Dynamic: classifier
47
+ Dynamic: description
48
+ Dynamic: description-content-type
49
+ Dynamic: home-page
50
+ Dynamic: project-url
51
+ Dynamic: provides-extra
52
+ Dynamic: requires-dist
53
+ Dynamic: requires-python
54
+ Dynamic: summary
55
+
56
+ # TinyDoc 0.2.0 — local-first grounded document extraction
57
+
58
+ Extract receipt/invoice fields as **schema-validated JSON** with per-field
59
+ **evidence** (quote + OCR bbox) and **confidence**. Runs locally with Ollama;
60
+ no API key, no cloud, no telemetry.
61
+
62
+ > **Status (2026-11-06):** the SDK and evaluation harness are measured. The
63
+ > first-party 256M model is retired (OCRBench 0.0%) and the SmolVLM2-2.2B SFT
64
+ > track is closed (no gain over base). The working product is the engine +
65
+ > trust layer, not the old checkpoint. Full analysis:
66
+ > [docs/v3_plan.md](https://github.com/eulogik/TinyDoc-VLM/blob/main/docs/v3_plan.md).
67
+
68
+ ## Measured results (SROIE public-train-derived clean-100, n=100)
69
+
70
+ | Engine | Field F1 | Address | Schema | Median latency | Errors |
71
+ |---|---:|---:|---:|---:|---:|
72
+ | **Qwen2.5-VL-3B** (`ollama:qwen2.5vl:3b`) | **0.9475** | 0.86 | 1.00 | 14.1 s | 0 |
73
+ | **Qwen3-VL-2B-Instruct** (`ollama:qwen3-vl:2b-instruct`) | **0.9200** | 0.84 | 1.00 | 7.0 s | 0 |
74
+ | Hybrid v2 first-party (`hybrid_2p2b`) | 0.603 | 0.37 | 0.93 | ~8 s | — |
75
+ | PP-OCR + heuristics | 0.376 | 0.03 | — | — | — |
76
+ | Tesseract + regex | 0.227 | — | 0.60 | — | — |
77
+
78
+ > **Caveat:** these are **not external benchmark numbers**. The 100-doc set is a
79
+ > duplicate-filtered split of the public SROIE **train** pool; Qwen may have seen
80
+ > it during pretraining. The original 100-doc set is 60% leaked. Use the numbers
81
+ > for same-document engine comparisons, not as a generalization claim. The next
82
+ > eval adds CORD/FUNSD/invoices and a private wild set.
83
+
84
+ ## Install
85
+
86
+ From PyPI (use this once 0.2.0 is published):
87
+
88
+ ```bash
89
+ pip install tinydoc
90
+ ```
91
+
92
+ From the repo:
93
+
94
+ ```bash
95
+ pip install ./sdk
96
+ # editable for development:
97
+ pip install -e ./sdk
98
+ ```
99
+
100
+ If `pip install tinydoc` resolves to **0.1.2**, that is the legacy 256M API; use
101
+ the source install above until 0.2.0 is uploaded. Core deps: `pydantic`,
102
+ `pillow`, `jsonschema`. Optional extras:
103
+
104
+ ```bash
105
+ pip install './sdk[ocr]' # pytesseract for evidence + ocr_regex
106
+ pip install './sdk[ocr-pp]' # RapidOCR ONNX evidence
107
+ pip install './sdk[smolvlm2]' # optional local HF SmolVLM2 engine
108
+ ```
109
+
110
+ For the default engine, install [Ollama](https://ollama.com) and pull a base:
111
+
112
+ ```bash
113
+ ollama pull qwen2.5vl:3b # quality default
114
+ ollama pull qwen3-vl:2b-instruct # fast tier (use -instruct, not the thinking tag)
115
+ ```
116
+
117
+ System `tesseract` is needed for OCR-span evidence (`brew install tesseract` on macOS).
118
+
119
+ ## Quick start
120
+
121
+ ```python
122
+ from tinydoc import ReceiptPipeline
123
+
124
+ # Quality default: Qwen2.5-VL-3B
125
+ pipe = ReceiptPipeline("ollama:qwen2.5vl:3b")
126
+
127
+ # Fast tier: Qwen3-VL-2B-Instruct, ~2x faster
128
+ # pipe = ReceiptPipeline("ollama:qwen3-vl:2b-instruct")
129
+
130
+ result = pipe.extract("receipt.jpg")
131
+ print(result.fields) # {"company": ..., "date": ..., "address": ..., "total": ...}
132
+ print(result.schema_valid) # True/False (JSON Schema)
133
+ print(result.confidence) # 0..1 aggregate
134
+ for field in result.field_results:
135
+ print(field.name, field.value, field.confidence, field.evidence)
136
+ ```
137
+
138
+ CLI:
139
+
140
+ ```bash
141
+ tinydoc extract ./receipts/ --engine ollama:qwen2.5vl:3b \
142
+ --out results.jsonl --overlay overlays/
143
+ ```
144
+
145
+ Overlay PNGs draw OCR-span evidence boxes and field labels (`tinydoc.draw_overlay`).
146
+
147
+ ## Engines
148
+
149
+ | Engine id | What it does |
150
+ |---|---|
151
+ | `ollama` / `ollama:qwen2.5vl:3b` | Quality default; free, local |
152
+ | `ollama:qwen3-vl:2b-instruct` | Fast tier; 1.9 GB Q4, ~2x faster |
153
+ | `auto` | Ollama if reachable; OCR floor if the schema would fail |
154
+ | `ocr_regex` | Offline floor (Tesseract + rules) |
155
+ | `mlx_2p2b` | First-party v2 edge engine (MLX SmolVLM2-2.2B 4-bit) |
156
+ | `hybrid_2p2b` | v2 product path: MLX fields + frozen F3 OCR address |
157
+
158
+ **Do not** set Ollama `format: "json"` — constrained decoding truncates long
159
+ addresses and can drop `total`. The SDK uses unconstrained prompts, JSON salvage,
160
+ and prompt-v1.
161
+
162
+ ### Environment
163
+
164
+ - `TINYDOC_OLLAMA_TIMEOUT` (default 180 s) — raise to 600 for slow local runs.
165
+ - `TINYDOC_OLLAMA_NUM_CTX` (default 8192) — Ollama context window. The implicit
166
+ 4096 rejected 2481×3508 scans with HTTP 400; keep 8192 for full pages.
167
+
168
+ ## Evidence and honesty
169
+
170
+ - Evidence kind is always `ocr_span` (RapidOCR word boxes), **not** VLM-predicted
171
+ boxes. Treat it as provenance, not as model grounding. A 100-box probe measured
172
+ VLM bbox hit@0.5 of **0.20** (2B) and **0.04** (3B), below the 0.70 product gate,
173
+ so OCR spans remain the shipped path.
174
+ - The schema is packaged (`tinydoc/schemas/sroie_receipt.schema.json`); validation
175
+ uses `jsonschema`.
176
+ - Confidence is a heuristic, not a probability. Evidence-off runs are constant
177
+ (0.4025; field ECE 0.52–0.55). The default evidence-on path measures field ECE
178
+ **0.115–0.135** raw and **0.005–0.014** after document-grouped isotonic OOF;
179
+ document-level confidence is **not** calibrated (ECE 0.09–0.10). Use field-level
180
+ scores for review routing only.
181
+ - Every published number must recompute from `evaluation/phase0/results/`:
182
+ `python3 evaluation/phase0/recompute_scores.py` must exit 0.
183
+ - The 256M model card and the v2 decision are kept for reproducibility; neither is
184
+ a current performance claim.
185
+
186
+ ## Layout
187
+
188
+ ```
189
+ sdk/tinydoc/
190
+ pipeline.py # engines, schema, sanitize, confidence, ReceiptPipeline
191
+ evidence.py # OCR-span evidence
192
+ overlay.py # draw bboxes + labels
193
+ cli.py # tinydoc extract
194
+ schemas/ # JSON Schema per doc type
195
+ ```
196
+
197
+ ## License
198
+
199
+ Apache 2.0
@@ -0,0 +1,144 @@
1
+ # TinyDoc 0.2.0 — local-first grounded document extraction
2
+
3
+ Extract receipt/invoice fields as **schema-validated JSON** with per-field
4
+ **evidence** (quote + OCR bbox) and **confidence**. Runs locally with Ollama;
5
+ no API key, no cloud, no telemetry.
6
+
7
+ > **Status (2026-11-06):** the SDK and evaluation harness are measured. The
8
+ > first-party 256M model is retired (OCRBench 0.0%) and the SmolVLM2-2.2B SFT
9
+ > track is closed (no gain over base). The working product is the engine +
10
+ > trust layer, not the old checkpoint. Full analysis:
11
+ > [docs/v3_plan.md](https://github.com/eulogik/TinyDoc-VLM/blob/main/docs/v3_plan.md).
12
+
13
+ ## Measured results (SROIE public-train-derived clean-100, n=100)
14
+
15
+ | Engine | Field F1 | Address | Schema | Median latency | Errors |
16
+ |---|---:|---:|---:|---:|---:|
17
+ | **Qwen2.5-VL-3B** (`ollama:qwen2.5vl:3b`) | **0.9475** | 0.86 | 1.00 | 14.1 s | 0 |
18
+ | **Qwen3-VL-2B-Instruct** (`ollama:qwen3-vl:2b-instruct`) | **0.9200** | 0.84 | 1.00 | 7.0 s | 0 |
19
+ | Hybrid v2 first-party (`hybrid_2p2b`) | 0.603 | 0.37 | 0.93 | ~8 s | — |
20
+ | PP-OCR + heuristics | 0.376 | 0.03 | — | — | — |
21
+ | Tesseract + regex | 0.227 | — | 0.60 | — | — |
22
+
23
+ > **Caveat:** these are **not external benchmark numbers**. The 100-doc set is a
24
+ > duplicate-filtered split of the public SROIE **train** pool; Qwen may have seen
25
+ > it during pretraining. The original 100-doc set is 60% leaked. Use the numbers
26
+ > for same-document engine comparisons, not as a generalization claim. The next
27
+ > eval adds CORD/FUNSD/invoices and a private wild set.
28
+
29
+ ## Install
30
+
31
+ From PyPI (use this once 0.2.0 is published):
32
+
33
+ ```bash
34
+ pip install tinydoc
35
+ ```
36
+
37
+ From the repo:
38
+
39
+ ```bash
40
+ pip install ./sdk
41
+ # editable for development:
42
+ pip install -e ./sdk
43
+ ```
44
+
45
+ If `pip install tinydoc` resolves to **0.1.2**, that is the legacy 256M API; use
46
+ the source install above until 0.2.0 is uploaded. Core deps: `pydantic`,
47
+ `pillow`, `jsonschema`. Optional extras:
48
+
49
+ ```bash
50
+ pip install './sdk[ocr]' # pytesseract for evidence + ocr_regex
51
+ pip install './sdk[ocr-pp]' # RapidOCR ONNX evidence
52
+ pip install './sdk[smolvlm2]' # optional local HF SmolVLM2 engine
53
+ ```
54
+
55
+ For the default engine, install [Ollama](https://ollama.com) and pull a base:
56
+
57
+ ```bash
58
+ ollama pull qwen2.5vl:3b # quality default
59
+ ollama pull qwen3-vl:2b-instruct # fast tier (use -instruct, not the thinking tag)
60
+ ```
61
+
62
+ System `tesseract` is needed for OCR-span evidence (`brew install tesseract` on macOS).
63
+
64
+ ## Quick start
65
+
66
+ ```python
67
+ from tinydoc import ReceiptPipeline
68
+
69
+ # Quality default: Qwen2.5-VL-3B
70
+ pipe = ReceiptPipeline("ollama:qwen2.5vl:3b")
71
+
72
+ # Fast tier: Qwen3-VL-2B-Instruct, ~2x faster
73
+ # pipe = ReceiptPipeline("ollama:qwen3-vl:2b-instruct")
74
+
75
+ result = pipe.extract("receipt.jpg")
76
+ print(result.fields) # {"company": ..., "date": ..., "address": ..., "total": ...}
77
+ print(result.schema_valid) # True/False (JSON Schema)
78
+ print(result.confidence) # 0..1 aggregate
79
+ for field in result.field_results:
80
+ print(field.name, field.value, field.confidence, field.evidence)
81
+ ```
82
+
83
+ CLI:
84
+
85
+ ```bash
86
+ tinydoc extract ./receipts/ --engine ollama:qwen2.5vl:3b \
87
+ --out results.jsonl --overlay overlays/
88
+ ```
89
+
90
+ Overlay PNGs draw OCR-span evidence boxes and field labels (`tinydoc.draw_overlay`).
91
+
92
+ ## Engines
93
+
94
+ | Engine id | What it does |
95
+ |---|---|
96
+ | `ollama` / `ollama:qwen2.5vl:3b` | Quality default; free, local |
97
+ | `ollama:qwen3-vl:2b-instruct` | Fast tier; 1.9 GB Q4, ~2x faster |
98
+ | `auto` | Ollama if reachable; OCR floor if the schema would fail |
99
+ | `ocr_regex` | Offline floor (Tesseract + rules) |
100
+ | `mlx_2p2b` | First-party v2 edge engine (MLX SmolVLM2-2.2B 4-bit) |
101
+ | `hybrid_2p2b` | v2 product path: MLX fields + frozen F3 OCR address |
102
+
103
+ **Do not** set Ollama `format: "json"` — constrained decoding truncates long
104
+ addresses and can drop `total`. The SDK uses unconstrained prompts, JSON salvage,
105
+ and prompt-v1.
106
+
107
+ ### Environment
108
+
109
+ - `TINYDOC_OLLAMA_TIMEOUT` (default 180 s) — raise to 600 for slow local runs.
110
+ - `TINYDOC_OLLAMA_NUM_CTX` (default 8192) — Ollama context window. The implicit
111
+ 4096 rejected 2481×3508 scans with HTTP 400; keep 8192 for full pages.
112
+
113
+ ## Evidence and honesty
114
+
115
+ - Evidence kind is always `ocr_span` (RapidOCR word boxes), **not** VLM-predicted
116
+ boxes. Treat it as provenance, not as model grounding. A 100-box probe measured
117
+ VLM bbox hit@0.5 of **0.20** (2B) and **0.04** (3B), below the 0.70 product gate,
118
+ so OCR spans remain the shipped path.
119
+ - The schema is packaged (`tinydoc/schemas/sroie_receipt.schema.json`); validation
120
+ uses `jsonschema`.
121
+ - Confidence is a heuristic, not a probability. Evidence-off runs are constant
122
+ (0.4025; field ECE 0.52–0.55). The default evidence-on path measures field ECE
123
+ **0.115–0.135** raw and **0.005–0.014** after document-grouped isotonic OOF;
124
+ document-level confidence is **not** calibrated (ECE 0.09–0.10). Use field-level
125
+ scores for review routing only.
126
+ - Every published number must recompute from `evaluation/phase0/results/`:
127
+ `python3 evaluation/phase0/recompute_scores.py` must exit 0.
128
+ - The 256M model card and the v2 decision are kept for reproducibility; neither is
129
+ a current performance claim.
130
+
131
+ ## Layout
132
+
133
+ ```
134
+ sdk/tinydoc/
135
+ pipeline.py # engines, schema, sanitize, confidence, ReceiptPipeline
136
+ evidence.py # OCR-span evidence
137
+ overlay.py # draw bboxes + labels
138
+ cli.py # tinydoc extract
139
+ schemas/ # JSON Schema per doc type
140
+ ```
141
+
142
+ ## License
143
+
144
+ Apache 2.0
tinydoc-0.2.0/setup.py ADDED
@@ -0,0 +1,68 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import setup, find_packages
4
+
5
+ setup(
6
+ name="tinydoc",
7
+ version="0.2.0",
8
+ packages=find_packages(),
9
+ include_package_data=True,
10
+ package_data={
11
+ "tinydoc": ["schemas/*.json"],
12
+ },
13
+ install_requires=[
14
+ "pydantic>=2.8.0",
15
+ "pillow>=10.0.0",
16
+ "jsonschema>=4.20.0",
17
+ ],
18
+ extras_require={
19
+ "ocr": ["pytesseract>=0.3.10"],
20
+ "ocr-pp": ["rapidocr-onnxruntime>=1.3.0"],
21
+ "smolvlm2": [
22
+ "torch>=2.2.0",
23
+ "transformers>=4.48.0",
24
+ "numpy>=1.26.0",
25
+ "sentencepiece>=0.2.0",
26
+ ],
27
+ "onnx": ["onnxruntime>=1.19.0", "optimum>=1.22.0"],
28
+ "all": [
29
+ "pytesseract>=0.3.10",
30
+ "rapidocr-onnxruntime>=1.3.0",
31
+ "torch>=2.2.0",
32
+ "transformers>=4.48.0",
33
+ "numpy>=1.26.0",
34
+ "sentencepiece>=0.2.0",
35
+ ],
36
+ },
37
+ entry_points={
38
+ "console_scripts": [
39
+ "tinydoc=tinydoc.cli:main",
40
+ ],
41
+ },
42
+ author="eulogik",
43
+ author_email="hello@eulogik.com",
44
+ description=(
45
+ "Local-first grounded document extraction SDK — schema, evidence, "
46
+ "confidence. Free Ollama/Qwen engine by default; no API key."
47
+ ),
48
+ long_description=(Path(__file__).parent / "README.md").read_text(encoding="utf-8"),
49
+ long_description_content_type="text/markdown",
50
+ url="https://github.com/eulogik/TinyDoc-VLM",
51
+ project_urls={
52
+ "Model status": "https://github.com/eulogik/TinyDoc-VLM/blob/main/docs/v3_plan.md",
53
+ "HuggingFace Space": "https://huggingface.co/spaces/eulogik/TinyDoc-VLM",
54
+ "Bug Tracker": "https://github.com/eulogik/TinyDoc-VLM/issues",
55
+ "Documentation": "https://github.com/eulogik/TinyDoc-VLM#readme",
56
+ "Benchmarks": "https://github.com/eulogik/TinyDoc-VLM/blob/main/docs/BENCHMARKS.md",
57
+ "Website": "https://eulogik.github.io/TinyDoc-VLM/",
58
+ },
59
+ classifiers=[
60
+ "Development Status :: 4 - Beta",
61
+ "Programming Language :: Python :: 3",
62
+ "License :: OSI Approved :: Apache Software License",
63
+ "Operating System :: OS Independent",
64
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
65
+ "Intended Audience :: Developers",
66
+ ],
67
+ python_requires=">=3.9",
68
+ )
@@ -0,0 +1,28 @@
1
+ from .models import ExtractionResult, QAResult, TableResult
2
+ from .pipeline import ReceiptPipeline, DocumentResult, FieldResult, build_engine
3
+ from .evidence import Evidence, attach_evidence
4
+ from .overlay import draw_overlay
5
+
6
+ __all__ = [
7
+ "ExtractionResult",
8
+ "QAResult",
9
+ "TableResult",
10
+ "ReceiptPipeline",
11
+ "DocumentResult",
12
+ "FieldResult",
13
+ "build_engine",
14
+ "Evidence",
15
+ "attach_evidence",
16
+ "draw_overlay",
17
+ # legacy name kept for import compatibility
18
+ "TinyDocExtractor",
19
+ ]
20
+
21
+
22
+ def __getattr__(name: str):
23
+ # Lazy legacy extractor (256M path) so pipeline imports do not load torch models
24
+ if name == "TinyDocExtractor":
25
+ from .extractor import TinyDocExtractor
26
+
27
+ return TinyDocExtractor
28
+ raise AttributeError(name)
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """TinyDoc Phase-1 CLI: folder → schema-valid JSON + evidence + confidence.
3
+
4
+ Usage:
5
+ python -m tinydoc.cli extract <folder_or_image> [--engine auto|ollama|ocr_regex|smolvlm2]
6
+ [--out results.jsonl] [--limit N] [--no-evidence]
7
+
8
+ Writes JSONL (one doc per line) and prints a summary table.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def main(argv=None) -> int:
20
+ # allow running from repo without install
21
+ sdk_root = Path(__file__).absolute().parent.parent
22
+ if str(sdk_root) not in sys.path:
23
+ sys.path.insert(0, str(sdk_root))
24
+
25
+ from tinydoc.pipeline import ReceiptPipeline # noqa: E402
26
+
27
+ ap = argparse.ArgumentParser(prog="tinydoc", description=__doc__)
28
+ sub = ap.add_subparsers(dest="cmd", required=True)
29
+
30
+ ex = sub.add_parser("extract", help="Extract fields from an image or folder")
31
+ ex.add_argument("path", help="Image file or directory")
32
+ ex.add_argument("--engine", default="auto")
33
+ ex.add_argument("--out", default=None, help="Write JSONL here")
34
+ ex.add_argument("--limit", type=int, default=None)
35
+ ex.add_argument("--no-evidence", action="store_true")
36
+ ex.add_argument("--model", default=None, help="Engine-specific model id")
37
+ ex.add_argument(
38
+ "--overlay",
39
+ default=None,
40
+ metavar="DIR",
41
+ help="Write evidence-overlay PNGs into DIR (red/blue boxes + labels)",
42
+ )
43
+
44
+ args = ap.parse_args(argv)
45
+
46
+ p = Path(args.path)
47
+ if not p.exists():
48
+ print(f"path not found: {p}", file=sys.stderr)
49
+ return 2
50
+ kwargs = {}
51
+ if args.model:
52
+ kwargs["model"] = args.model
53
+ pipe = ReceiptPipeline(args.engine, **kwargs)
54
+ with_ev = not args.no_evidence
55
+
56
+ if p.is_dir():
57
+ results = pipe.extract_folder(p, limit=args.limit, with_evidence=with_ev)
58
+ else:
59
+ results = [pipe.extract(p, with_evidence=with_ev)]
60
+
61
+ out_path = Path(args.out) if args.out else None
62
+ if out_path:
63
+ out_path.parent.mkdir(parents=True, exist_ok=True)
64
+ with out_path.open("w") as f:
65
+ for r in results:
66
+ f.write(json.dumps(r.to_dict()) + "\n")
67
+
68
+ if args.overlay:
69
+ from tinydoc.overlay import draw_overlay
70
+
71
+ odir = Path(args.overlay)
72
+ odir.mkdir(parents=True, exist_ok=True)
73
+ for r in results:
74
+ name = Path(r.image_path).stem + "_overlay.png"
75
+ try:
76
+ draw_overlay(r.image_path, r.field_results, odir / name)
77
+ except Exception as e:
78
+ print(f" overlay failed for {r.image_path}: {e}")
79
+ print(f"wrote overlays → {odir}")
80
+
81
+ n = len(results)
82
+ if n == 0:
83
+ print("no images found")
84
+ return 1
85
+ schema_rate = sum(r.schema_valid for r in results) / n
86
+ mean_conf = sum(r.confidence for r in results) / n
87
+ ev_rate = sum(r.evidence_coverage for r in results) / n
88
+ mean_lat = sum(r.latency_ms for r in results) / n
89
+ print(
90
+ f"engine={pipe.engine.name} n={n} "
91
+ f"schema={schema_rate:.3f} conf={mean_conf:.3f} "
92
+ f"evidence={ev_rate:.3f} latency_ms={mean_lat:.0f}"
93
+ )
94
+ for r in results[:5]:
95
+ print(
96
+ f" {Path(r.image_path).name}: schema={r.schema_valid} "
97
+ f"conf={r.confidence:.2f} fields={r.fields}"
98
+ )
99
+ if out_path:
100
+ print(f"wrote {out_path}")
101
+ return 0
102
+
103
+
104
+ if __name__ == "__main__":
105
+ raise SystemExit(main())