autosxtract 0.1.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.
Files changed (94) hide show
  1. autosxtract-0.1.0/.github/workflows/ci.yml +107 -0
  2. autosxtract-0.1.0/.gitignore +41 -0
  3. autosxtract-0.1.0/.pre-commit-config.yaml +47 -0
  4. autosxtract-0.1.0/CLAUDE.md +268 -0
  5. autosxtract-0.1.0/LICENSE +21 -0
  6. autosxtract-0.1.0/Makefile +17 -0
  7. autosxtract-0.1.0/PKG-INFO +761 -0
  8. autosxtract-0.1.0/README.md +695 -0
  9. autosxtract-0.1.0/autosxtract/__init__.py +90 -0
  10. autosxtract-0.1.0/autosxtract/_version.py +1 -0
  11. autosxtract-0.1.0/autosxtract/cascade.py +436 -0
  12. autosxtract-0.1.0/autosxtract/cli.py +173 -0
  13. autosxtract-0.1.0/autosxtract/config.py +364 -0
  14. autosxtract-0.1.0/autosxtract/engines/__init__.py +39 -0
  15. autosxtract-0.1.0/autosxtract/engines/base.py +407 -0
  16. autosxtract-0.1.0/autosxtract/engines/models.py +147 -0
  17. autosxtract-0.1.0/autosxtract/engines/onnx.py +73 -0
  18. autosxtract-0.1.0/autosxtract/engines/paddle.py +420 -0
  19. autosxtract-0.1.0/autosxtract/engines/signature.py +192 -0
  20. autosxtract-0.1.0/autosxtract/engines/tesseract.py +260 -0
  21. autosxtract-0.1.0/autosxtract/engines/vision.py +243 -0
  22. autosxtract-0.1.0/autosxtract/exceptions.py +33 -0
  23. autosxtract-0.1.0/autosxtract/formats.py +302 -0
  24. autosxtract-0.1.0/autosxtract/image.py +63 -0
  25. autosxtract-0.1.0/autosxtract/pdf/__init__.py +29 -0
  26. autosxtract-0.1.0/autosxtract/pdf/_mupdf.py +42 -0
  27. autosxtract-0.1.0/autosxtract/pdf/coverage.py +82 -0
  28. autosxtract-0.1.0/autosxtract/pdf/ink.py +122 -0
  29. autosxtract-0.1.0/autosxtract/pdf/lock.py +50 -0
  30. autosxtract-0.1.0/autosxtract/pdf/orientation.py +78 -0
  31. autosxtract-0.1.0/autosxtract/pdf/pages.py +111 -0
  32. autosxtract-0.1.0/autosxtract/pdf/profile.py +63 -0
  33. autosxtract-0.1.0/autosxtract/pdf/render.py +95 -0
  34. autosxtract-0.1.0/autosxtract/platform.py +75 -0
  35. autosxtract-0.1.0/autosxtract/quality/__init__.py +52 -0
  36. autosxtract-0.1.0/autosxtract/quality/anchors.py +95 -0
  37. autosxtract-0.1.0/autosxtract/quality/consensus.py +130 -0
  38. autosxtract-0.1.0/autosxtract/quality/gate.py +78 -0
  39. autosxtract-0.1.0/autosxtract/quality/lexicon.py +153 -0
  40. autosxtract-0.1.0/autosxtract/quality/lines.py +450 -0
  41. autosxtract-0.1.0/autosxtract/quality/markers.py +51 -0
  42. autosxtract-0.1.0/autosxtract/quality/metrics.py +150 -0
  43. autosxtract-0.1.0/autosxtract/quality/prose.py +263 -0
  44. autosxtract-0.1.0/autosxtract/quality/rejection.py +151 -0
  45. autosxtract-0.1.0/autosxtract/quality/response.py +89 -0
  46. autosxtract-0.1.0/autosxtract/quality/routing.py +141 -0
  47. autosxtract-0.1.0/autosxtract/quality/scoring.py +177 -0
  48. autosxtract-0.1.0/autosxtract/quality/screening.py +197 -0
  49. autosxtract-0.1.0/autosxtract/quality/selection.py +38 -0
  50. autosxtract-0.1.0/autosxtract/quality/stamp.py +87 -0
  51. autosxtract-0.1.0/autosxtract/quality/vetoes.py +135 -0
  52. autosxtract-0.1.0/autosxtract/resources.py +113 -0
  53. autosxtract-0.1.0/autosxtract/steps/__init__.py +35 -0
  54. autosxtract-0.1.0/autosxtract/steps/base.py +160 -0
  55. autosxtract-0.1.0/autosxtract/steps/docling_json.py +151 -0
  56. autosxtract-0.1.0/autosxtract/steps/docling_local.py +204 -0
  57. autosxtract-0.1.0/autosxtract/steps/layers.py +244 -0
  58. autosxtract-0.1.0/autosxtract/steps/native.py +117 -0
  59. autosxtract-0.1.0/autosxtract/steps/ocr.py +184 -0
  60. autosxtract-0.1.0/autosxtract/steps/remote.py +436 -0
  61. autosxtract-0.1.0/autosxtract/steps/screening.py +59 -0
  62. autosxtract-0.1.0/autosxtract/steps/unwrap.py +82 -0
  63. autosxtract-0.1.0/autosxtract/types.py +206 -0
  64. autosxtract-0.1.0/pyproject.toml +141 -0
  65. autosxtract-0.1.0/scripts/compare_engines.py +102 -0
  66. autosxtract-0.1.0/scripts/privacy_check.py +268 -0
  67. autosxtract-0.1.0/tests/conftest.py +121 -0
  68. autosxtract-0.1.0/tests/test_anchors.py +49 -0
  69. autosxtract-0.1.0/tests/test_cascade.py +429 -0
  70. autosxtract-0.1.0/tests/test_config.py +52 -0
  71. autosxtract-0.1.0/tests/test_consensus.py +59 -0
  72. autosxtract-0.1.0/tests/test_docling_json.py +88 -0
  73. autosxtract-0.1.0/tests/test_documentation.py +108 -0
  74. autosxtract-0.1.0/tests/test_engines.py +153 -0
  75. autosxtract-0.1.0/tests/test_formats.py +85 -0
  76. autosxtract-0.1.0/tests/test_gate.py +57 -0
  77. autosxtract-0.1.0/tests/test_image.py +46 -0
  78. autosxtract-0.1.0/tests/test_integration.py +49 -0
  79. autosxtract-0.1.0/tests/test_lexicon.py +43 -0
  80. autosxtract-0.1.0/tests/test_lines.py +201 -0
  81. autosxtract-0.1.0/tests/test_packaging.py +88 -0
  82. autosxtract-0.1.0/tests/test_paddle.py +143 -0
  83. autosxtract-0.1.0/tests/test_platform.py +43 -0
  84. autosxtract-0.1.0/tests/test_prose.py +83 -0
  85. autosxtract-0.1.0/tests/test_rejection.py +76 -0
  86. autosxtract-0.1.0/tests/test_remote.py +273 -0
  87. autosxtract-0.1.0/tests/test_resources.py +158 -0
  88. autosxtract-0.1.0/tests/test_routing.py +73 -0
  89. autosxtract-0.1.0/tests/test_scoring.py +47 -0
  90. autosxtract-0.1.0/tests/test_screening.py +69 -0
  91. autosxtract-0.1.0/tests/test_selection.py +45 -0
  92. autosxtract-0.1.0/tests/test_signature.py +44 -0
  93. autosxtract-0.1.0/tests/test_stamp.py +45 -0
  94. autosxtract-0.1.0/tests/test_vetoes.py +72 -0
@@ -0,0 +1,107 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ quality:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ cache: pip
23
+
24
+ - name: Install
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install -e ".[dev]"
28
+
29
+ - name: Lint
30
+ run: |
31
+ ruff check .
32
+ ruff format --check .
33
+
34
+ - name: Types
35
+ run: mypy autosxtract
36
+ continue-on-error: true
37
+
38
+ # No OCR extra: the suite has to pass on the bare core, because that is
39
+ # how many people will install it. The tests that need an engine mark
40
+ # themselves as skipped.
41
+ - name: Tests (core)
42
+ run: pytest --cov=autosxtract --cov-report=term-missing
43
+
44
+ # Runs on EVERY push, not only on a release: it is cheap, and it is the
45
+ # only automatic defence against an accidental `git add -A` in a directory
46
+ # that sits next to the real documents the library extracts.
47
+ - name: Privacy scan
48
+ run: python scripts/privacy_check.py .
49
+
50
+ # Step 2 off Apple hardware, with the real engine. Kept separate because it
51
+ # downloads ONNX Runtime and the PP-OCRv6 weights — expensive to run across
52
+ # the whole matrix.
53
+ with-ocr:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: actions/setup-python@v5
58
+ with:
59
+ python-version: "3.11"
60
+ cache: pip
61
+ - name: Install with the engine
62
+ run: |
63
+ python -m pip install --upgrade pip
64
+ pip install -e ".[dev,paddle]"
65
+ - name: Integration tests
66
+ run: pytest -m "not apple" -q
67
+
68
+ # Apple's cascade can only be exercised on a macOS runner.
69
+ apple:
70
+ runs-on: macos-latest
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: actions/setup-python@v5
74
+ with:
75
+ python-version: "3.11"
76
+ cache: pip
77
+ - name: Install with Vision
78
+ run: |
79
+ python -m pip install --upgrade pip
80
+ pip install -e ".[dev,apple]"
81
+ - name: Diagnose
82
+ run: python -m autosxtract.cli diagnose
83
+ - name: Tests
84
+ run: pytest -q
85
+
86
+ packaging:
87
+ runs-on: ubuntu-latest
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ - uses: actions/setup-python@v5
91
+ with:
92
+ python-version: "3.11"
93
+
94
+ - name: Build the wheel
95
+ run: |
96
+ python -m pip install --upgrade pip build
97
+ python -m build
98
+
99
+ # Importing in a clean environment catches the commonest packaging
100
+ # error: a subpackage forgotten in the wheel, which only shows outside the
101
+ # repository.
102
+ - name: Install in a clean environment and import
103
+ run: |
104
+ python -m venv /tmp/clean
105
+ /tmp/clean/bin/pip install dist/*.whl
106
+ /tmp/clean/bin/python -c "import autosxtract; print(autosxtract.__version__)"
107
+ /tmp/clean/bin/autosxtract diagnose
@@ -0,0 +1,41 @@
1
+ # ── dados e artefatos privados ───────────────────────────────────────────
2
+ # Nenhum documento real entra no repositório. Os fixtures da suíte são
3
+ # gerados na hora pelo PyMuPDF, com texto inventado.
4
+ data/
5
+ private/
6
+ acervo/
7
+ pdfs/
8
+ saidas/
9
+ *.pdf
10
+ *.csv
11
+ *.jsonl
12
+
13
+ # ── pesos de modelo (baixados, nunca versionados) ────────────────────────
14
+ *.onnx
15
+ modelos/
16
+
17
+ .env
18
+ .env.*
19
+ credentials*
20
+ secrets*
21
+
22
+ # ── exceções: fixtures sintéticos ────────────────────────────────────────
23
+ !tests/fixtures/
24
+ !tests/fixtures/**
25
+
26
+ # ── python ───────────────────────────────────────────────────────────────
27
+ __pycache__/
28
+ *.pyc
29
+ *.pyo
30
+ .venv/
31
+ venv/
32
+ *.egg-info/
33
+ build/
34
+ dist/
35
+ .pytest_cache/
36
+ .mypy_cache/
37
+ .ruff_cache/
38
+ .coverage
39
+ htmlcov/
40
+
41
+ .DS_Store
@@ -0,0 +1,47 @@
1
+ # Install with: pre-commit install
2
+ #
3
+ # Order matters: the privacy scan runs FIRST, because it is the only one that
4
+ # prevents a leak — the others only prevent a red CI. A commit blocked on
5
+ # formatting costs thirty seconds; a real document published has no undo.
6
+ repos:
7
+ - repo: local
8
+ hooks:
9
+ - id: privacy-check
10
+ name: privacy scan (staged)
11
+ entry: python scripts/privacy_check.py . --staged
12
+ language: system
13
+ pass_filenames: false
14
+ always_run: true
15
+
16
+ - id: ruff
17
+ name: ruff check
18
+ entry: ruff check --fix
19
+ language: system
20
+ types: [python]
21
+
22
+ # Without this hook, `ruff check` passes and CI fails on `ruff format
23
+ # --check`. They are different checks: the first looks at lint rules, the
24
+ # second at formatting.
25
+ - id: ruff-format
26
+ name: ruff format
27
+ entry: ruff format
28
+ language: system
29
+ types: [python]
30
+
31
+ - repo: https://github.com/pre-commit/pre-commit-hooks
32
+ rev: v5.0.0
33
+ hooks:
34
+ # Blocks any file above 1 MB. It is the net that catches the archive PDF
35
+ # that slipped past .gitignore — right extension, wrong directory.
36
+ - id: check-added-large-files
37
+ args: ["--maxkb=1024"]
38
+ - id: check-merge-conflict
39
+ - id: check-toml
40
+ - id: check-yaml
41
+ - id: end-of-file-fixer
42
+ - id: trailing-whitespace
43
+ # Stops a private file entering through a symlink pointing outside the
44
+ # repository.
45
+ - id: check-symlinks
46
+ - id: destroyed-symlinks
47
+ - id: detect-private-key
@@ -0,0 +1,268 @@
1
+ # AutosXtract — project rules
2
+
3
+ This file is for whoever touches the code (person or agent). It records the
4
+ decisions that are **not** obvious from the code and that have already cost
5
+ something once.
6
+
7
+ ## 1. The default cascade does no networking, and networking is never accidental
8
+
9
+ `Cascade()` assembles local steps only. No engine and no gate opens a socket.
10
+ There are exactly two exceptions, and both are explicit:
11
+
12
+ - `engines/models.py` downloads the PP-OCRv6 weights **once**, and extraction
13
+ works without it (falling back to rapidocr's embedded model).
14
+ - `steps/remote.py` brings `DoclingStep` and `VLMStep`, which **require `url` in
15
+ the constructor**. They are not in the default cascade, there is no discovery
16
+ through an environment variable, no built-in default and no fallback to a
17
+ known endpoint.
18
+
19
+ `steps/docling_local.py` is the useful counterexample: the same engine as
20
+ `DoclingStep`, with no networking at all. It stays out of the default cascade
21
+ for a **different** reason — it weighs ~2 GB of models and ~4 s per document —
22
+ and that is why it lives outside `remote.py`. Confusing "expensive" with
23
+ "remote" would make this paragraph's invariant meaningless.
24
+
25
+ This is not a preference: the previous version of this pipeline reached the OCR
26
+ engine on a Mac over a reverse SSH tunnel, and that worker going down
27
+ **silently degraded the text** — 488 documents re-extracted down the worse path,
28
+ 19.5 min instead of 4.9, 28,239 characters lost, and nobody noticed until
29
+ someone checked. A remote step nobody declared must not exist.
30
+
31
+ The corollary that holds the rest together: **`Config` has not a single host,
32
+ port, URL or credential field.** All networking lives in the constructor of a
33
+ step somebody wrote by hand. `tests/test_config.py::test_no_field_points_at_a_network`
34
+ and `tests/test_remote.py` are the guard rails.
35
+
36
+ ## 2. One acceptance criterion
37
+
38
+ `quality/gate.py::evaluate` decides, for every step, whether the text suffices.
39
+ Whoever decides the current step solved it and whoever decides the next one is
40
+ worth paying for ask the same question, with the same code.
41
+
42
+ Two competing notions of "adequate extraction" in one pipeline was the defect
43
+ this function exists to avoid repeating: the step approved itself by one
44
+ criterion and the cascade refused it by another.
45
+
46
+ ## 3. A missing engine is never an exception
47
+
48
+ `available()` returns `(False, reason)`, the step goes inert and the cascade
49
+ moves on. **The absence of a tool is not evidence about the document** —
50
+ treating "I have no OCR" as "the page is empty" switches the pipeline off in
51
+ silence.
52
+
53
+ Corollary: degrading without breaking is right; degrading without warning is
54
+ not. Every reason goes to the provenance and to `autosxtract diagnose`.
55
+
56
+ ## 4. Two different gates, and the difference is what happens to the refused
57
+
58
+ **Acceptance gate** (`quality/gate.py`): "is this text good enough to stop the
59
+ cascade?". Something refused here **stays in the contest** — it may be the best
60
+ reading there is.
61
+
62
+ **Replacement gate** (`quality/rejection.py`): runs only after an `expensive`
63
+ step and asks "is it better than what I already had, and did it lose nothing?".
64
+ Something refused here is **discarded**. Letting it compete would cancel the
65
+ gate, because volume is usually on the wrong side: the corrupted text is
66
+ precisely the longest one.
67
+
68
+ Confusing the two is the easy mistake. The first compares against a threshold;
69
+ the second compares against a concrete text and has already concluded the new
70
+ one is worse.
71
+
72
+ ## 5. Refused text still competes
73
+
74
+ `StepResult` separates the **verdict** (does the cascade stop?) from the
75
+ **candidate** (does the text enter the contest?). A refused step may have
76
+ produced the best reading there is. Discarding it left 682 documents with zero
77
+ characters while the PDF had a text layer.
78
+
79
+ ## 6. PyMuPDF is serialised, and that is not negotiable
80
+
81
+ It **crashes the process** with several threads: a segfault in
82
+ `page_get_textpage`, captured with `faulthandler` and reproduced with 489 PDFs
83
+ across 12 threads. `try/except` does not protect you — a segmentation fault is
84
+ not a Python exception.
85
+
86
+ Every access goes through `pdf/lock.py`. The measured cost is ~4% (37.2 s with 4
87
+ threads against 38.6 s with 24). The useful parallelism is **per document**.
88
+
89
+ The same goes for `get_image_rects` / `get_image_info`: `pdf/coverage.py` uses a
90
+ single `get_text("dict")` traversal precisely because the per-image version
91
+ segfaulted under concurrency.
92
+
93
+ ## 7. Engine confidence does not arbitrate quality
94
+
95
+ Measured on 60 documents audited by four reviewers: engine confidence does not
96
+ separate a good reading from an unsafe one — there was an unsafe document at
97
+ confidence 100. It enters only as a floor against degenerate output, never as a
98
+ criterion.
99
+
100
+ ## 8. Measured numbers go in the comment
101
+
102
+ Every threshold in `config.py` carries the measurement that fixed it. Changing a
103
+ number without measuring is the mistake this project tries to make difficult.
104
+
105
+ And the methodological lesson, which is worth more than any specific number: **an
106
+ isolated measurement has already lied here.** Turning off Vision's language
107
+ correction improved anchors across 60 documents (+4) and worsened them across
108
+ the whole cascade (−227), because the worse text failed the gate and fell to
109
+ worse engines. What decides is the cascade's behaviour, not one engine's output.
110
+ Use `scripts/compare_engines.py`, which measures both.
111
+
112
+ ## 9. No real document in the repository
113
+
114
+ The fixtures are generated on the fly by PyMuPDF, with invented text
115
+ (`tests/conftest.py`). `.gitignore` blocks `*.pdf` at the root. A failing test
116
+ must point at the code, not at one specific archive file.
117
+
118
+ That applies to **identifiers inside comments and docstrings** too. Documenting a
119
+ measurement with the case number it was made on looks harmless and is not:
120
+ `scripts/privacy_check.py` caught a real case number that had made its way into
121
+ the examples here, with a valid check digit. The examples use numbers with an
122
+ **invalid** check digit on purpose — that way the scanner stays quiet and nobody
123
+ has to decide case by case whether a number exists.
124
+
125
+ The scanner runs in `pre-commit` (the first hook, before the style ones) and on
126
+ every CI push. It validates tax IDs, company IDs and case numbers by their check
127
+ digit: precision matters more than raw recall, because a noisy scanner gets
128
+ switched off.
129
+
130
+ Watch out for one trap: `pre-commit` stashes unstaged changes before running. It
131
+ inspects **what will be committed**, not what is in your working tree — fixing a
132
+ file without `git add` leaves the hook looking at the old version.
133
+
134
+ ## 10. Layers
135
+
136
+ ```
137
+ pdf/ knows only the file — does not know what an engine is
138
+ quality/ knows only text — does no I/O
139
+ engines/ reads pixels — does not know what a cascade is
140
+ steps/ composes the two — does not know which engine is behind it
141
+ cascade.py orchestrates
142
+ ```
143
+
144
+ If an import crosses the arrow backwards, the design has broken. It was that
145
+ boundary that let the OCR step be **a single one**, generic, for Vision and
146
+ PP-OCRv6 alike.
147
+
148
+ ## 11. The platform decides twice, and both are necessary
149
+
150
+ install a PEP 508 marker in `pyproject.toml`, evaluated by pip
151
+ runtime `platform.py` plus the registry in `engines/base.py`
152
+
153
+ The first makes `pip install autosxtract` bring Vision on macOS and PP-OCRv6
154
+ elsewhere, with no extra. The second is what stops the library from blowing up
155
+ when the marker did not match — an image built for another platform,
156
+ `--no-deps`, a lockfile for another `sys_platform`, an incomplete pyobjc.
157
+
158
+ A temptation to resist: deleting the second layer because "the first already
159
+ guarantees it". It does not guarantee it — it guarantees the common case.
160
+ `tests/test_packaging.py` pins the first; `tests/test_engines.py` pins the second.
161
+
162
+ ## 12. English in the code, Portuguese in the patterns
163
+
164
+ Names, docstrings and messages are in English, so the library is usable outside
165
+ Brazil. The **regexes and word lists stay in Portuguese**, because they describe
166
+ the corpus: the conformity stamp, the enclitic pronouns, the abbreviations, the
167
+ identity-card markers, the legal vocabulary of the lexicon.
168
+
169
+ That is the adaptation seam. Anyone porting the library to another language
170
+ swaps `quality/stamp.py`, `quality/lexicon.py`, `quality/prose.py` and
171
+ `quality/screening.py` — the code around them does not change.
172
+
173
+ ## 13. What runs BEFORE the expensive step
174
+
175
+ `quality/vetoes.py` holds the five vetoes, and the order is by rising cost:
176
+ pixel statistics at 40 DPI (ms), then a real local OCR (~1 s), then comparing
177
+ text already read (free).
178
+
179
+ Two warnings that have already cost time:
180
+
181
+ - **The first two are only valid together with "extracted no text".** On their
182
+ own they would discard an old photocopy on dark paper, which is continuous
183
+ tone and carries thousands of legitimate characters (measured: 0.99 / 0.99 /
184
+ 0.83 mid-tone with 1,001, 2,612 and 632 characters).
185
+ - **The witness has to be of another architecture.** A second engine of the same
186
+ family is not independent evidence, and the agreement veto stops meaning what
187
+ it says. That is why `veto_engine` points at Tesseract and not at a second
188
+ PP-OCR.
189
+
190
+ `local_reading=None` means "I don't know" and skips the last three vetoes. It
191
+ never becomes "there is no text".
192
+
193
+ ## 14. Parallelism is decided by the machine, not by the code
194
+
195
+ The three fields accept `None` = "decide here", and that is the default. An
196
+ explicit number is obeyed; what it is not, is a promise.
197
+
198
+ threads 72 cores 2 cores
199
+ 1 1.36 pg/s 1.44 pg/s
200
+ 2 1.74 1.68 <- plateau
201
+ 4 1.99 1.58
202
+ 8 2.18 1.54 <- worse than 2 threads
203
+ 16 2.27 1.71
204
+
205
+ Three things that have bitten and are pinned in code:
206
+
207
+ - **`os.cpu_count()` lies inside a container.** It reports the host, not the
208
+ quota. `resources.cores()` crosses affinity, cgroup v1/v2 and `cpu_count` and
209
+ keeps the smallest — none of the three alone covers `taskset` AND `--cpus`.
210
+ - **The product multiplies silently.** `documents × pages` reaches 32 pages in
211
+ flight from values that look modest. The aggregate cap cuts the PAGES, never
212
+ the documents: cutting documents raises total time predictably, cutting pages
213
+ costs almost nothing.
214
+ - **The engine has the last word.** Whoever configures the cascade does not know
215
+ whether a hardware queue sits behind it. `OCREngine.scales_with_threads =
216
+ False` makes the engine use 1 thread, and `OCRStep` records the effective
217
+ value in the provenance when it differs from the requested one — clamping
218
+ silently would be the same antipattern as section 1.
219
+
220
+ Resolution is a **method**, not a field computed in the constructor: the machine
221
+ that resolves may not be the one that serialised the configuration.
222
+
223
+ ## 15. The engine contract has two levels, and the detailed one is optional
224
+
225
+ `transcribe_page` returns `(text, confidence)`; `read_page` returns it line by
226
+ line, with polygon and score. The second is **optional** — `None` is the honest
227
+ answer from an engine without geometry, and the cascade falls back to the first.
228
+
229
+ Except that without it there are no containment layers, and they are the
230
+ pipeline's cheapest measured gain (entity recall 0.902 → 0.921, and latency
231
+ FALLS). When writing a new engine, implement the detailed contract if the
232
+ backend exposes geometry.
233
+
234
+ `Transcription.pages` is only filled when **every** page answered in detail. A
235
+ partial list would make the layers operate on a different document from the one
236
+ transcribed, and the page index would stop lining up.
237
+
238
+ ## 16. Never reuse the main OCR instance to recognise a crop
239
+
240
+ Calling the main `rapidocr` with `use_det=False` turns detection off
241
+ **permanently on that object**. Measured: the next whole-page read returned 1
242
+ line where it had returned 56, and the document came out with 1 character
243
+ instead of 3,900.
244
+
245
+ The defect is of the worst kind — silent, order-dependent, and only visible from
246
+ the second page of the batch, because the first still uses a clean object. It
247
+ passed a whole test suite without showing; it only appeared on measuring
248
+ document by document.
249
+
250
+ `PaddleEngine._recognizer()` keeps a **separate** instance for Layer 2.
251
+ `tests/test_paddle.py` pins that.
252
+
253
+ The general corollary: a third-party library with per-instance state is a shared
254
+ resource. If one path changes configuration, that path needs its own object.
255
+
256
+ ## 17. A visual detector does not decide on its own
257
+
258
+ The signature detector (YOLO) was measured on a real archive: 19% of pages with
259
+ a detection, most of them **false positives** on seals, stamps, logos, QR codes
260
+ and coats of arms. A model that is good on a public benchmark can be useless in
261
+ your domain, and the way to find out is to run it on your archive, not to read
262
+ the mAP.
263
+
264
+ The answer was neither to throw the detector away nor to trust it: it was to
265
+ **cross it with the text**. A box only counts if some overlapping line is
266
+ illegible, and it is discarded if any overlapping line is stamp text. That is the
267
+ pattern to repeat whenever a visual signal enters the pipeline — alone it errs,
268
+ crossed with what is already known it helps.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AutosXtract
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ PY := .venv/bin/python
2
+
3
+ .PHONY: test lint fmt typecheck diagnose privacy hooks all
4
+
5
+ test: ; $(PY) -m pytest
6
+ # BOTH commands, as in CI: `ruff check` does not verify formatting, and running
7
+ # only it leaves CI red with the local tree green.
8
+ lint: ; $(PY) -m ruff check . && $(PY) -m ruff format --check .
9
+ fmt: ; $(PY) -m ruff format .
10
+ typecheck: ; $(PY) -m mypy autosxtract
11
+ # Prints what THIS machine can run. Worth more than any log when an extraction
12
+ # result surprises you.
13
+ diagnose: ; $(PY) -m autosxtract.cli diagnose
14
+ # The only check that prevents a leak rather than merely a red CI.
15
+ privacy: ; $(PY) scripts/privacy_check.py .
16
+ hooks: ; $(PY) -m pre_commit install
17
+ all: lint typecheck test privacy