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.
- autosxtract-0.1.0/.github/workflows/ci.yml +107 -0
- autosxtract-0.1.0/.gitignore +41 -0
- autosxtract-0.1.0/.pre-commit-config.yaml +47 -0
- autosxtract-0.1.0/CLAUDE.md +268 -0
- autosxtract-0.1.0/LICENSE +21 -0
- autosxtract-0.1.0/Makefile +17 -0
- autosxtract-0.1.0/PKG-INFO +761 -0
- autosxtract-0.1.0/README.md +695 -0
- autosxtract-0.1.0/autosxtract/__init__.py +90 -0
- autosxtract-0.1.0/autosxtract/_version.py +1 -0
- autosxtract-0.1.0/autosxtract/cascade.py +436 -0
- autosxtract-0.1.0/autosxtract/cli.py +173 -0
- autosxtract-0.1.0/autosxtract/config.py +364 -0
- autosxtract-0.1.0/autosxtract/engines/__init__.py +39 -0
- autosxtract-0.1.0/autosxtract/engines/base.py +407 -0
- autosxtract-0.1.0/autosxtract/engines/models.py +147 -0
- autosxtract-0.1.0/autosxtract/engines/onnx.py +73 -0
- autosxtract-0.1.0/autosxtract/engines/paddle.py +420 -0
- autosxtract-0.1.0/autosxtract/engines/signature.py +192 -0
- autosxtract-0.1.0/autosxtract/engines/tesseract.py +260 -0
- autosxtract-0.1.0/autosxtract/engines/vision.py +243 -0
- autosxtract-0.1.0/autosxtract/exceptions.py +33 -0
- autosxtract-0.1.0/autosxtract/formats.py +302 -0
- autosxtract-0.1.0/autosxtract/image.py +63 -0
- autosxtract-0.1.0/autosxtract/pdf/__init__.py +29 -0
- autosxtract-0.1.0/autosxtract/pdf/_mupdf.py +42 -0
- autosxtract-0.1.0/autosxtract/pdf/coverage.py +82 -0
- autosxtract-0.1.0/autosxtract/pdf/ink.py +122 -0
- autosxtract-0.1.0/autosxtract/pdf/lock.py +50 -0
- autosxtract-0.1.0/autosxtract/pdf/orientation.py +78 -0
- autosxtract-0.1.0/autosxtract/pdf/pages.py +111 -0
- autosxtract-0.1.0/autosxtract/pdf/profile.py +63 -0
- autosxtract-0.1.0/autosxtract/pdf/render.py +95 -0
- autosxtract-0.1.0/autosxtract/platform.py +75 -0
- autosxtract-0.1.0/autosxtract/quality/__init__.py +52 -0
- autosxtract-0.1.0/autosxtract/quality/anchors.py +95 -0
- autosxtract-0.1.0/autosxtract/quality/consensus.py +130 -0
- autosxtract-0.1.0/autosxtract/quality/gate.py +78 -0
- autosxtract-0.1.0/autosxtract/quality/lexicon.py +153 -0
- autosxtract-0.1.0/autosxtract/quality/lines.py +450 -0
- autosxtract-0.1.0/autosxtract/quality/markers.py +51 -0
- autosxtract-0.1.0/autosxtract/quality/metrics.py +150 -0
- autosxtract-0.1.0/autosxtract/quality/prose.py +263 -0
- autosxtract-0.1.0/autosxtract/quality/rejection.py +151 -0
- autosxtract-0.1.0/autosxtract/quality/response.py +89 -0
- autosxtract-0.1.0/autosxtract/quality/routing.py +141 -0
- autosxtract-0.1.0/autosxtract/quality/scoring.py +177 -0
- autosxtract-0.1.0/autosxtract/quality/screening.py +197 -0
- autosxtract-0.1.0/autosxtract/quality/selection.py +38 -0
- autosxtract-0.1.0/autosxtract/quality/stamp.py +87 -0
- autosxtract-0.1.0/autosxtract/quality/vetoes.py +135 -0
- autosxtract-0.1.0/autosxtract/resources.py +113 -0
- autosxtract-0.1.0/autosxtract/steps/__init__.py +35 -0
- autosxtract-0.1.0/autosxtract/steps/base.py +160 -0
- autosxtract-0.1.0/autosxtract/steps/docling_json.py +151 -0
- autosxtract-0.1.0/autosxtract/steps/docling_local.py +204 -0
- autosxtract-0.1.0/autosxtract/steps/layers.py +244 -0
- autosxtract-0.1.0/autosxtract/steps/native.py +117 -0
- autosxtract-0.1.0/autosxtract/steps/ocr.py +184 -0
- autosxtract-0.1.0/autosxtract/steps/remote.py +436 -0
- autosxtract-0.1.0/autosxtract/steps/screening.py +59 -0
- autosxtract-0.1.0/autosxtract/steps/unwrap.py +82 -0
- autosxtract-0.1.0/autosxtract/types.py +206 -0
- autosxtract-0.1.0/pyproject.toml +141 -0
- autosxtract-0.1.0/scripts/compare_engines.py +102 -0
- autosxtract-0.1.0/scripts/privacy_check.py +268 -0
- autosxtract-0.1.0/tests/conftest.py +121 -0
- autosxtract-0.1.0/tests/test_anchors.py +49 -0
- autosxtract-0.1.0/tests/test_cascade.py +429 -0
- autosxtract-0.1.0/tests/test_config.py +52 -0
- autosxtract-0.1.0/tests/test_consensus.py +59 -0
- autosxtract-0.1.0/tests/test_docling_json.py +88 -0
- autosxtract-0.1.0/tests/test_documentation.py +108 -0
- autosxtract-0.1.0/tests/test_engines.py +153 -0
- autosxtract-0.1.0/tests/test_formats.py +85 -0
- autosxtract-0.1.0/tests/test_gate.py +57 -0
- autosxtract-0.1.0/tests/test_image.py +46 -0
- autosxtract-0.1.0/tests/test_integration.py +49 -0
- autosxtract-0.1.0/tests/test_lexicon.py +43 -0
- autosxtract-0.1.0/tests/test_lines.py +201 -0
- autosxtract-0.1.0/tests/test_packaging.py +88 -0
- autosxtract-0.1.0/tests/test_paddle.py +143 -0
- autosxtract-0.1.0/tests/test_platform.py +43 -0
- autosxtract-0.1.0/tests/test_prose.py +83 -0
- autosxtract-0.1.0/tests/test_rejection.py +76 -0
- autosxtract-0.1.0/tests/test_remote.py +273 -0
- autosxtract-0.1.0/tests/test_resources.py +158 -0
- autosxtract-0.1.0/tests/test_routing.py +73 -0
- autosxtract-0.1.0/tests/test_scoring.py +47 -0
- autosxtract-0.1.0/tests/test_screening.py +69 -0
- autosxtract-0.1.0/tests/test_selection.py +45 -0
- autosxtract-0.1.0/tests/test_signature.py +44 -0
- autosxtract-0.1.0/tests/test_stamp.py +45 -0
- 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
|