restruct-cv 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.
- restruct_cv-0.1.0/LICENSE +21 -0
- restruct_cv-0.1.0/PKG-INFO +226 -0
- restruct_cv-0.1.0/README.md +198 -0
- restruct_cv-0.1.0/pyproject.toml +64 -0
- restruct_cv-0.1.0/pyproject.toml.orig +54 -0
- restruct_cv-0.1.0/src/restruct/__init__.py +56 -0
- restruct_cv-0.1.0/src/restruct/cli.py +521 -0
- restruct_cv-0.1.0/src/restruct/configs/__init__.py +5 -0
- restruct_cv-0.1.0/src/restruct/configs/embedding_references.py +290 -0
- restruct_cv-0.1.0/src/restruct/configs/settings.py +175 -0
- restruct_cv-0.1.0/src/restruct/debug/__init__.py +1 -0
- restruct_cv-0.1.0/src/restruct/debug/artifacts.py +134 -0
- restruct_cv-0.1.0/src/restruct/debug/canvas.py +141 -0
- restruct_cv-0.1.0/src/restruct/debug/colors.py +172 -0
- restruct_cv-0.1.0/src/restruct/debug/reconstruct.py +663 -0
- restruct_cv-0.1.0/src/restruct/debug/render.py +323 -0
- restruct_cv-0.1.0/src/restruct/debug/stages.py +294 -0
- restruct_cv-0.1.0/src/restruct/document/__init__.py +17 -0
- restruct_cv-0.1.0/src/restruct/document/physical.py +293 -0
- restruct_cv-0.1.0/src/restruct/document/stats.py +532 -0
- restruct_cv-0.1.0/src/restruct/document/types.py +121 -0
- restruct_cv-0.1.0/src/restruct/errors.py +128 -0
- restruct_cv-0.1.0/src/restruct/geometry.py +151 -0
- restruct_cv-0.1.0/src/restruct/ingestion/__init__.py +1 -0
- restruct_cv-0.1.0/src/restruct/ingestion/docx.py +558 -0
- restruct_cv-0.1.0/src/restruct/ingestion/native.py +256 -0
- restruct_cv-0.1.0/src/restruct/ingestion/ocr.py +248 -0
- restruct_cv-0.1.0/src/restruct/layout/__init__.py +1 -0
- restruct_cv-0.1.0/src/restruct/layout/blocks.py +154 -0
- restruct_cv-0.1.0/src/restruct/layout/lines.py +75 -0
- restruct_cv-0.1.0/src/restruct/layout/rows.py +95 -0
- restruct_cv-0.1.0/src/restruct/layout/unsupported.py +237 -0
- restruct_cv-0.1.0/src/restruct/layout/words.py +229 -0
- restruct_cv-0.1.0/src/restruct/model.py +674 -0
- restruct_cv-0.1.0/src/restruct/parsers/__init__.py +1 -0
- restruct_cv-0.1.0/src/restruct/parsers/education.py +340 -0
- restruct_cv-0.1.0/src/restruct/parsers/experience.py +474 -0
- restruct_cv-0.1.0/src/restruct/parsers/grouped.py +580 -0
- restruct_cv-0.1.0/src/restruct/parsers/header.py +513 -0
- restruct_cv-0.1.0/src/restruct/parsers/skills.py +266 -0
- restruct_cv-0.1.0/src/restruct/parsers/urls.py +194 -0
- restruct_cv-0.1.0/src/restruct/patterns/__init__.py +1 -0
- restruct_cv-0.1.0/src/restruct/patterns/bullets.py +14 -0
- restruct_cv-0.1.0/src/restruct/patterns/contacts.py +23 -0
- restruct_cv-0.1.0/src/restruct/patterns/dates.py +59 -0
- restruct_cv-0.1.0/src/restruct/patterns/education.py +32 -0
- restruct_cv-0.1.0/src/restruct/patterns/languages.py +81 -0
- restruct_cv-0.1.0/src/restruct/patterns/layout.py +31 -0
- restruct_cv-0.1.0/src/restruct/patterns/organizations.py +19 -0
- restruct_cv-0.1.0/src/restruct/patterns/personal.py +69 -0
- restruct_cv-0.1.0/src/restruct/patterns/separators.py +47 -0
- restruct_cv-0.1.0/src/restruct/pipeline.py +247 -0
- restruct_cv-0.1.0/src/restruct/schema.py +257 -0
- restruct_cv-0.1.0/src/restruct/stages.py +28 -0
- restruct_cv-0.1.0/src/restruct/structure/__init__.py +1 -0
- restruct_cv-0.1.0/src/restruct/structure/compound.py +375 -0
- restruct_cv-0.1.0/src/restruct/structure/headings.py +195 -0
- restruct_cv-0.1.0/src/restruct/structure/keyvalue.py +96 -0
- restruct_cv-0.1.0/src/restruct/structure/metadata.py +66 -0
- restruct_cv-0.1.0/src/restruct/structure/resolver.py +218 -0
- restruct_cv-0.1.0/src/restruct/structure/sections.py +189 -0
- restruct_cv-0.1.0/src/restruct/structure/separators.py +201 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nyein Chan Soe
|
|
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,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: restruct-cv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON
|
|
5
|
+
Keywords: resume,cv,parser,ats,applicant-tracking,ocr,pdf,docx
|
|
6
|
+
Author: Nyein Chan Soe
|
|
7
|
+
Author-email: Nyein Chan Soe <contact@nyeinchansoe.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Text Processing
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
18
|
+
Requires-Dist: pillow>=11.0
|
|
19
|
+
Requires-Dist: python-docx>=1.1
|
|
20
|
+
Requires-Dist: pymupdf>=1.28.2
|
|
21
|
+
Requires-Dist: sentence-transformers>=6.0.1
|
|
22
|
+
Requires-Dist: transformers>=4.51.3,<5.14.0
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Project-URL: Homepage, https://github.com/Nye1nChanSoe/restruct-cv
|
|
25
|
+
Project-URL: Repository, https://github.com/Nye1nChanSoe/restruct-cv
|
|
26
|
+
Project-URL: Issues, https://github.com/Nye1nChanSoe/restruct-cv/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Restruct
|
|
30
|
+
|
|
31
|
+
Restruct turns messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON.
|
|
32
|
+
It combines OCR, layout geometry, NER, semantic similarity, and deterministic rules so every result
|
|
33
|
+
can be traced back to the thing on the page that produced it.
|
|
34
|
+
|
|
35
|
+
Built for applicant tracking systems, candidate search, analytics pipelines, talent platforms, and
|
|
36
|
+
other systems that need trustworthy resume data.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync # from a checkout
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
or, from a built wheel:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv build
|
|
50
|
+
pip install dist/restruct_cv-*.whl
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The distribution is `restruct-cv`; the import name and the command it installs are both
|
|
54
|
+
`restruct`. The unqualified name was already taken on PyPI by an unrelated project.
|
|
55
|
+
|
|
56
|
+
### Model weights
|
|
57
|
+
|
|
58
|
+
All inference weights are **local-only** and never fetched at run time. Two directories are
|
|
59
|
+
required:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
models/
|
|
63
|
+
all-MiniLM-L6-v2/
|
|
64
|
+
distilbert-NER/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
They are looked for, in order, in `models/` beside the checkout (when running from one), `models/`
|
|
68
|
+
under the current working directory, and `~/.restruct/models`. Set
|
|
69
|
+
`RESTRUCT_MODELS_DIRECTORY=/path/to/models` to name the directory outright, which is the usual
|
|
70
|
+
answer for an installed copy; when it is set nothing else is consulted. A run with no weights
|
|
71
|
+
exits `20` and names every place it looked.
|
|
72
|
+
|
|
73
|
+
### Tesseract
|
|
74
|
+
|
|
75
|
+
Tesseract is a system dependency, needed **only** for scanned pages — a PDF with its own text
|
|
76
|
+
layer and a DOCX never ask for it.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
brew install tesseract # macOS
|
|
80
|
+
apt-get install tesseract-ocr # Debian/Ubuntu
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It is found on `PATH`, or where the installers put it (Program Files on Windows, either Homebrew
|
|
84
|
+
prefix on macOS). PyMuPDF renders an OCR page at 300 DPI and `tesseract` is invoked directly,
|
|
85
|
+
without a shell, using the English language data, LSTM engine mode 1 and page segmentation mode 3.
|
|
86
|
+
Rendered page images are deleted after each document.
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv run restruct <resume.pdf> -o out.json # one resume; quiet, writes nothing else
|
|
92
|
+
uv run restruct <resume.docx> -o . # a directory: writes <resume>.json into it
|
|
93
|
+
uv run restruct <resume.pdf> -o out.json --debug # + stage 4-5 artifacts in out/
|
|
94
|
+
uv run restruct <resume.pdf> -o out.json --stages 1-3 # + those stages (implies --debug)
|
|
95
|
+
uv run restruct <resume.pdf> -o out.json --reconstruct # + the result drawn back as a page
|
|
96
|
+
uv run restruct examples/11/resume.json --reconstruct # draw a result already extracted
|
|
97
|
+
|
|
98
|
+
uv run restruct # batch over resumes-synthetic/
|
|
99
|
+
uv run restruct --truths # batch over resumes-truths/ (local, gitignored)
|
|
100
|
+
uv run restruct --unsupported # batch over resumes-unsupported/
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--stages` selects **debug artifacts, never whether a pass runs**: every pass feeds the next, so a
|
|
104
|
+
flag that skipped one would quietly produce a different resume.
|
|
105
|
+
|
|
106
|
+
Exit codes are an API, grouped by decade — `1x` input, `2x` environment, `3x` extraction, `4x`
|
|
107
|
+
output, with `2` left to argparse. `errors.py` names every failure and `cli.py` is the only module
|
|
108
|
+
that maps one to a code, so a caller embedding restruct as a library catches an exception by type
|
|
109
|
+
instead of losing its process.
|
|
110
|
+
|
|
111
|
+
## Output
|
|
112
|
+
|
|
113
|
+
`resume.json` is lean and metadata-free: no bounding boxes, fonts, geometry, model names,
|
|
114
|
+
confidences or detection methods. Its first key is `schema_version`, and `resume.schema.json` in
|
|
115
|
+
the repository root is the published contract every fixture is validated against.
|
|
116
|
+
|
|
117
|
+
Sixteen destinations are always present, always in the same order. A section is `null` when the
|
|
118
|
+
resume has none, `[]` when it exists but yielded no entries; a key is never absent.
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"schema_version": "1.0",
|
|
123
|
+
"header_profile": { "name": "…", "emails": ["…"], "phones": ["…"] },
|
|
124
|
+
"summary": null,
|
|
125
|
+
"experience": [{ "job_titles": ["…"], "companies": ["…"], "bullets": ["…"] }],
|
|
126
|
+
"education": [],
|
|
127
|
+
"…": "…",
|
|
128
|
+
"others": []
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
All the evidence behind those values — boxes, fonts, confidences, which method decided what —
|
|
133
|
+
lives in a separate track under `raw/`, and the overlays draw it:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
results/<name>/
|
|
137
|
+
resume.json the lean output
|
|
138
|
+
raw/*.json the evidence: boxes, fonts, confidences, methods
|
|
139
|
+
debug/page-N.png the combined overlay, one per source page
|
|
140
|
+
debug/pass-*/ passes 1-4, gitignored
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The pass-1 dump is named after the reader that produced it — `pymupdf.json` for a PDF, `docx.json`
|
|
144
|
+
for a DOCX, since the two read different things and only one of them has coordinates. The
|
|
145
|
+
single-file form writes it beside the other evidence in `raw/`; the batch writes it to
|
|
146
|
+
`debug/<name>.raw-<reader>.json`.
|
|
147
|
+
|
|
148
|
+
A model-backed box is drawn more heavily than a deterministic one, so a reader can tell whether a
|
|
149
|
+
box is something the document said or something a model concluded.
|
|
150
|
+
|
|
151
|
+
### Reading the result back
|
|
152
|
+
|
|
153
|
+
`--reconstruct` draws `resume.json` back out as a page — `reconstruction.pdf` and one PNG per
|
|
154
|
+
page — so the result can be proof-read by eye.
|
|
155
|
+
|
|
156
|
+
Drawing a result on its own writes those files flat beside the JSON, named after it
|
|
157
|
+
(`resume-reconstruction.pdf`, `resume-page-1.png`); `-o` names a directory to put them in
|
|
158
|
+
instead, under their plain names.
|
|
159
|
+
|
|
160
|
+
It answers a different question from the overlays. An overlay draws on top of the document, so it
|
|
161
|
+
shows whether a box landed on the right words; the document keeps making sense regardless of what
|
|
162
|
+
was understood. A reconstruction throws the page away and draws only what was understood, which is
|
|
163
|
+
what makes a bullet filed under education or a date read as a job title visible at a glance.
|
|
164
|
+
|
|
165
|
+
It is deliberately not a facsimile — imitating the original layout would hide the errors it exists
|
|
166
|
+
to reveal. Absent and empty fields are skipped, so what is on the page is what was extracted, and
|
|
167
|
+
anything the renderer cannot place is drawn in red under UNPLACED rather than dropped. Given a
|
|
168
|
+
`resume.json` as the input it draws that and runs nothing else, loading no models.
|
|
169
|
+
|
|
170
|
+
### Worked examples
|
|
171
|
+
|
|
172
|
+
`results/` is regenerated by every run and is not committed. Three resumes are, under
|
|
173
|
+
`examples/` — one per ingestion track, because what each track makes available is different:
|
|
174
|
+
|
|
175
|
+
| Example | Source | What it shows |
|
|
176
|
+
| --- | --- | --- |
|
|
177
|
+
| [`examples/7.anomaly/`](examples/7.anomaly/) | native PDF, 3 pages | The full evidence track. `debug/page-N.png` draws every section box on the page it came from, model-backed boxes drawn more heavily than deterministic ones. `raw/layout-warnings.json` is written even when empty, so an absent finding is distinguishable from an absent check. |
|
|
178
|
+
| [`examples/9.ocr/`](examples/9.ocr/) | scanned PDF | The same shapes from a page with no text layer. OCR is rebuilt into the line geometry the native path produces, so nothing downstream has an OCR case — which is visible here as an overlay that looks like any other. |
|
|
179
|
+
| [`examples/11/`](examples/11/) | DOCX | No overlays at all. A DOCX has no geometry and must never pretend to, so there is nothing to draw on — the structure comes from what the document states outright: styles, list markers, table cells. |
|
|
180
|
+
|
|
181
|
+
Each holds the `resume.json`, the `raw/*.json` evidence behind it, and the overlays where there
|
|
182
|
+
are any. They are copied out of `results/` by `tools/refresh_examples.py` rather than curated by
|
|
183
|
+
hand, so they are what the pipeline currently produces and not a snapshot of what it once did.
|
|
184
|
+
Their inputs are in `resumes-synthetic/`, and every one is synthetic.
|
|
185
|
+
|
|
186
|
+
## Architecture
|
|
187
|
+
|
|
188
|
+
Five ordered passes over one shared in-memory document.
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
ingestion/ physical extraction — native PDF text, per-page OCR fallback, DOCX
|
|
192
|
+
document/ shared types and document-wide statistics
|
|
193
|
+
layout/ row clustering, paragraph/bullet accumulation, unsupported-layout detection
|
|
194
|
+
structure/ heading detection, routing, compound headings, precedence resolver, separators
|
|
195
|
+
parsers/ one module per section shape (header, experience, education, skills, grouped, urls)
|
|
196
|
+
model.py DistilBERT NER and MiniLM adapters, loaded on first use
|
|
197
|
+
patterns/ deterministic regex evidence, grouped by what it describes
|
|
198
|
+
debug/ artifacts (JSON) and overlays (Pillow), one canvas, one colour registry
|
|
199
|
+
schema.py the lean, versioned output (contract: resume.schema.json)
|
|
200
|
+
errors.py the failure taxonomy; only cli.py turns one into an exit code
|
|
201
|
+
pipeline.py orchestration only — the only module that knows the stage order
|
|
202
|
+
cli.py argparse and filesystem layout only
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Extraction runs in a fixed precedence — deterministic, context-sensitive deterministic, NER,
|
|
206
|
+
MiniLM, geometry, then `other` — and `structure/resolver.py` **enforces** that order rather than
|
|
207
|
+
documenting it: a stage that runs after a weaker one has already claimed a span raises.
|
|
208
|
+
|
|
209
|
+
v1 targets single-column resumes. Layouts whose reading order cannot be recovered (column gutters,
|
|
210
|
+
vertical text, overlapping boxes, nested tables) are **recorded, never repaired**, in
|
|
211
|
+
`debug/layout-warnings.json`.
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
uv run pytest # full suite
|
|
217
|
+
uv run python -m tests.scorecard # per-field precision/recall/F1
|
|
218
|
+
uv run restruct && uv run python tools/refresh_examples.py
|
|
219
|
+
git status --short examples/ # clean == byte-identical
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the test and scorecard workflow, and `CLAUDE.md` for the
|
|
223
|
+
design decisions behind each module.
|
|
224
|
+
|
|
225
|
+
Please do not submit **real** resumes or their labels; `resumes-truths/` is gitignored for exactly
|
|
226
|
+
that reason. Fixtures in `resumes-synthetic/` are synthetic and safe to commit.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Restruct
|
|
2
|
+
|
|
3
|
+
Restruct turns messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON.
|
|
4
|
+
It combines OCR, layout geometry, NER, semantic similarity, and deterministic rules so every result
|
|
5
|
+
can be traced back to the thing on the page that produced it.
|
|
6
|
+
|
|
7
|
+
Built for applicant tracking systems, candidate search, analytics pipelines, talent platforms, and
|
|
8
|
+
other systems that need trustworthy resume data.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync # from a checkout
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
or, from a built wheel:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv build
|
|
22
|
+
pip install dist/restruct_cv-*.whl
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The distribution is `restruct-cv`; the import name and the command it installs are both
|
|
26
|
+
`restruct`. The unqualified name was already taken on PyPI by an unrelated project.
|
|
27
|
+
|
|
28
|
+
### Model weights
|
|
29
|
+
|
|
30
|
+
All inference weights are **local-only** and never fetched at run time. Two directories are
|
|
31
|
+
required:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
models/
|
|
35
|
+
all-MiniLM-L6-v2/
|
|
36
|
+
distilbert-NER/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
They are looked for, in order, in `models/` beside the checkout (when running from one), `models/`
|
|
40
|
+
under the current working directory, and `~/.restruct/models`. Set
|
|
41
|
+
`RESTRUCT_MODELS_DIRECTORY=/path/to/models` to name the directory outright, which is the usual
|
|
42
|
+
answer for an installed copy; when it is set nothing else is consulted. A run with no weights
|
|
43
|
+
exits `20` and names every place it looked.
|
|
44
|
+
|
|
45
|
+
### Tesseract
|
|
46
|
+
|
|
47
|
+
Tesseract is a system dependency, needed **only** for scanned pages — a PDF with its own text
|
|
48
|
+
layer and a DOCX never ask for it.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
brew install tesseract # macOS
|
|
52
|
+
apt-get install tesseract-ocr # Debian/Ubuntu
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It is found on `PATH`, or where the installers put it (Program Files on Windows, either Homebrew
|
|
56
|
+
prefix on macOS). PyMuPDF renders an OCR page at 300 DPI and `tesseract` is invoked directly,
|
|
57
|
+
without a shell, using the English language data, LSTM engine mode 1 and page segmentation mode 3.
|
|
58
|
+
Rendered page images are deleted after each document.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv run restruct <resume.pdf> -o out.json # one resume; quiet, writes nothing else
|
|
64
|
+
uv run restruct <resume.docx> -o . # a directory: writes <resume>.json into it
|
|
65
|
+
uv run restruct <resume.pdf> -o out.json --debug # + stage 4-5 artifacts in out/
|
|
66
|
+
uv run restruct <resume.pdf> -o out.json --stages 1-3 # + those stages (implies --debug)
|
|
67
|
+
uv run restruct <resume.pdf> -o out.json --reconstruct # + the result drawn back as a page
|
|
68
|
+
uv run restruct examples/11/resume.json --reconstruct # draw a result already extracted
|
|
69
|
+
|
|
70
|
+
uv run restruct # batch over resumes-synthetic/
|
|
71
|
+
uv run restruct --truths # batch over resumes-truths/ (local, gitignored)
|
|
72
|
+
uv run restruct --unsupported # batch over resumes-unsupported/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--stages` selects **debug artifacts, never whether a pass runs**: every pass feeds the next, so a
|
|
76
|
+
flag that skipped one would quietly produce a different resume.
|
|
77
|
+
|
|
78
|
+
Exit codes are an API, grouped by decade — `1x` input, `2x` environment, `3x` extraction, `4x`
|
|
79
|
+
output, with `2` left to argparse. `errors.py` names every failure and `cli.py` is the only module
|
|
80
|
+
that maps one to a code, so a caller embedding restruct as a library catches an exception by type
|
|
81
|
+
instead of losing its process.
|
|
82
|
+
|
|
83
|
+
## Output
|
|
84
|
+
|
|
85
|
+
`resume.json` is lean and metadata-free: no bounding boxes, fonts, geometry, model names,
|
|
86
|
+
confidences or detection methods. Its first key is `schema_version`, and `resume.schema.json` in
|
|
87
|
+
the repository root is the published contract every fixture is validated against.
|
|
88
|
+
|
|
89
|
+
Sixteen destinations are always present, always in the same order. A section is `null` when the
|
|
90
|
+
resume has none, `[]` when it exists but yielded no entries; a key is never absent.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"schema_version": "1.0",
|
|
95
|
+
"header_profile": { "name": "…", "emails": ["…"], "phones": ["…"] },
|
|
96
|
+
"summary": null,
|
|
97
|
+
"experience": [{ "job_titles": ["…"], "companies": ["…"], "bullets": ["…"] }],
|
|
98
|
+
"education": [],
|
|
99
|
+
"…": "…",
|
|
100
|
+
"others": []
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
All the evidence behind those values — boxes, fonts, confidences, which method decided what —
|
|
105
|
+
lives in a separate track under `raw/`, and the overlays draw it:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
results/<name>/
|
|
109
|
+
resume.json the lean output
|
|
110
|
+
raw/*.json the evidence: boxes, fonts, confidences, methods
|
|
111
|
+
debug/page-N.png the combined overlay, one per source page
|
|
112
|
+
debug/pass-*/ passes 1-4, gitignored
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The pass-1 dump is named after the reader that produced it — `pymupdf.json` for a PDF, `docx.json`
|
|
116
|
+
for a DOCX, since the two read different things and only one of them has coordinates. The
|
|
117
|
+
single-file form writes it beside the other evidence in `raw/`; the batch writes it to
|
|
118
|
+
`debug/<name>.raw-<reader>.json`.
|
|
119
|
+
|
|
120
|
+
A model-backed box is drawn more heavily than a deterministic one, so a reader can tell whether a
|
|
121
|
+
box is something the document said or something a model concluded.
|
|
122
|
+
|
|
123
|
+
### Reading the result back
|
|
124
|
+
|
|
125
|
+
`--reconstruct` draws `resume.json` back out as a page — `reconstruction.pdf` and one PNG per
|
|
126
|
+
page — so the result can be proof-read by eye.
|
|
127
|
+
|
|
128
|
+
Drawing a result on its own writes those files flat beside the JSON, named after it
|
|
129
|
+
(`resume-reconstruction.pdf`, `resume-page-1.png`); `-o` names a directory to put them in
|
|
130
|
+
instead, under their plain names.
|
|
131
|
+
|
|
132
|
+
It answers a different question from the overlays. An overlay draws on top of the document, so it
|
|
133
|
+
shows whether a box landed on the right words; the document keeps making sense regardless of what
|
|
134
|
+
was understood. A reconstruction throws the page away and draws only what was understood, which is
|
|
135
|
+
what makes a bullet filed under education or a date read as a job title visible at a glance.
|
|
136
|
+
|
|
137
|
+
It is deliberately not a facsimile — imitating the original layout would hide the errors it exists
|
|
138
|
+
to reveal. Absent and empty fields are skipped, so what is on the page is what was extracted, and
|
|
139
|
+
anything the renderer cannot place is drawn in red under UNPLACED rather than dropped. Given a
|
|
140
|
+
`resume.json` as the input it draws that and runs nothing else, loading no models.
|
|
141
|
+
|
|
142
|
+
### Worked examples
|
|
143
|
+
|
|
144
|
+
`results/` is regenerated by every run and is not committed. Three resumes are, under
|
|
145
|
+
`examples/` — one per ingestion track, because what each track makes available is different:
|
|
146
|
+
|
|
147
|
+
| Example | Source | What it shows |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| [`examples/7.anomaly/`](examples/7.anomaly/) | native PDF, 3 pages | The full evidence track. `debug/page-N.png` draws every section box on the page it came from, model-backed boxes drawn more heavily than deterministic ones. `raw/layout-warnings.json` is written even when empty, so an absent finding is distinguishable from an absent check. |
|
|
150
|
+
| [`examples/9.ocr/`](examples/9.ocr/) | scanned PDF | The same shapes from a page with no text layer. OCR is rebuilt into the line geometry the native path produces, so nothing downstream has an OCR case — which is visible here as an overlay that looks like any other. |
|
|
151
|
+
| [`examples/11/`](examples/11/) | DOCX | No overlays at all. A DOCX has no geometry and must never pretend to, so there is nothing to draw on — the structure comes from what the document states outright: styles, list markers, table cells. |
|
|
152
|
+
|
|
153
|
+
Each holds the `resume.json`, the `raw/*.json` evidence behind it, and the overlays where there
|
|
154
|
+
are any. They are copied out of `results/` by `tools/refresh_examples.py` rather than curated by
|
|
155
|
+
hand, so they are what the pipeline currently produces and not a snapshot of what it once did.
|
|
156
|
+
Their inputs are in `resumes-synthetic/`, and every one is synthetic.
|
|
157
|
+
|
|
158
|
+
## Architecture
|
|
159
|
+
|
|
160
|
+
Five ordered passes over one shared in-memory document.
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
ingestion/ physical extraction — native PDF text, per-page OCR fallback, DOCX
|
|
164
|
+
document/ shared types and document-wide statistics
|
|
165
|
+
layout/ row clustering, paragraph/bullet accumulation, unsupported-layout detection
|
|
166
|
+
structure/ heading detection, routing, compound headings, precedence resolver, separators
|
|
167
|
+
parsers/ one module per section shape (header, experience, education, skills, grouped, urls)
|
|
168
|
+
model.py DistilBERT NER and MiniLM adapters, loaded on first use
|
|
169
|
+
patterns/ deterministic regex evidence, grouped by what it describes
|
|
170
|
+
debug/ artifacts (JSON) and overlays (Pillow), one canvas, one colour registry
|
|
171
|
+
schema.py the lean, versioned output (contract: resume.schema.json)
|
|
172
|
+
errors.py the failure taxonomy; only cli.py turns one into an exit code
|
|
173
|
+
pipeline.py orchestration only — the only module that knows the stage order
|
|
174
|
+
cli.py argparse and filesystem layout only
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Extraction runs in a fixed precedence — deterministic, context-sensitive deterministic, NER,
|
|
178
|
+
MiniLM, geometry, then `other` — and `structure/resolver.py` **enforces** that order rather than
|
|
179
|
+
documenting it: a stage that runs after a weaker one has already claimed a span raises.
|
|
180
|
+
|
|
181
|
+
v1 targets single-column resumes. Layouts whose reading order cannot be recovered (column gutters,
|
|
182
|
+
vertical text, overlapping boxes, nested tables) are **recorded, never repaired**, in
|
|
183
|
+
`debug/layout-warnings.json`.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
uv run pytest # full suite
|
|
189
|
+
uv run python -m tests.scorecard # per-field precision/recall/F1
|
|
190
|
+
uv run restruct && uv run python tools/refresh_examples.py
|
|
191
|
+
git status --short examples/ # clean == byte-identical
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the test and scorecard workflow, and `CLAUDE.md` for the
|
|
195
|
+
design decisions behind each module.
|
|
196
|
+
|
|
197
|
+
Please do not submit **real** resumes or their labels; `resumes-truths/` is gitignored for exactly
|
|
198
|
+
that reason. Fixtures in `resumes-synthetic/` are synthetic and safe to commit.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "restruct-cv"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Turn messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
keywords = [
|
|
8
|
+
"resume",
|
|
9
|
+
"cv",
|
|
10
|
+
"parser",
|
|
11
|
+
"ats",
|
|
12
|
+
"applicant-tracking",
|
|
13
|
+
"ocr",
|
|
14
|
+
"pdf",
|
|
15
|
+
"docx",
|
|
16
|
+
]
|
|
17
|
+
license = "MIT"
|
|
18
|
+
license-files = ["LICENSE"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Text Processing",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pillow>=11.0",
|
|
31
|
+
"python-docx>=1.1",
|
|
32
|
+
"pymupdf>=1.28.2",
|
|
33
|
+
"sentence-transformers>=6.0.1",
|
|
34
|
+
"transformers>=4.51.3,<5.14.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[project.authors]]
|
|
38
|
+
name = "Nyein Chan Soe"
|
|
39
|
+
email = "contact@nyeinchansoe.com"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/Nye1nChanSoe/restruct-cv"
|
|
43
|
+
Repository = "https://github.com/Nye1nChanSoe/restruct-cv"
|
|
44
|
+
Issues = "https://github.com/Nye1nChanSoe/restruct-cv/issues"
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
restruct = "restruct.cli:main"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=8.0",
|
|
52
|
+
"jsonschema>=4.20",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[build-system]
|
|
56
|
+
requires = ["uv_build>=0.12.7,<0.13.0"]
|
|
57
|
+
build-backend = "uv_build"
|
|
58
|
+
|
|
59
|
+
[tool.uv.build-backend]
|
|
60
|
+
module-name = "restruct"
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
addopts = "-q"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "restruct-cv"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Turn messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Nyein Chan Soe", email = "contact@nyeinchansoe.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
keywords = ["resume", "cv", "parser", "ats", "applicant-tracking", "ocr", "pdf", "docx"]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Text Processing",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pillow>=11.0",
|
|
25
|
+
"python-docx>=1.1",
|
|
26
|
+
"pymupdf>=1.28.2",
|
|
27
|
+
"sentence-transformers>=6.0.1",
|
|
28
|
+
"transformers>=4.51.3,<5.14.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Nye1nChanSoe/restruct-cv"
|
|
33
|
+
Repository = "https://github.com/Nye1nChanSoe/restruct-cv"
|
|
34
|
+
Issues = "https://github.com/Nye1nChanSoe/restruct-cv/issues"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0",
|
|
39
|
+
"jsonschema>=4.20",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
restruct = "restruct.cli:main"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["uv_build>=0.12.7,<0.13.0"]
|
|
47
|
+
build-backend = "uv_build"
|
|
48
|
+
|
|
49
|
+
[tool.uv.build-backend]
|
|
50
|
+
module-name = "restruct"
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
addopts = "-q"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Restruct: explainable resume extraction from PDF and scanned documents.
|
|
2
|
+
|
|
3
|
+
This module is the public Python API. It re-exports the pipeline entry points
|
|
4
|
+
and the shared document types; everything else lives in the stage packages:
|
|
5
|
+
|
|
6
|
+
ingestion/ physical extraction, native and OCR
|
|
7
|
+
document/ shared types and document representation
|
|
8
|
+
layout/ rows, paragraphs and bullet reconstruction
|
|
9
|
+
structure/ headings, key-value pairs and section routing
|
|
10
|
+
parsers/ one module per section shape
|
|
11
|
+
models/ NER and embedding adapters
|
|
12
|
+
patterns/ deterministic evidence
|
|
13
|
+
debug/ artifacts and overlay rendering
|
|
14
|
+
schema/ the versioned clean output
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from restruct.document.types import (
|
|
18
|
+
DetectedHeading,
|
|
19
|
+
ExtractedLine,
|
|
20
|
+
HeaderEntityMatch,
|
|
21
|
+
)
|
|
22
|
+
from restruct.schema import build_v1_resume, write_v1_resume
|
|
23
|
+
|
|
24
|
+
# ``main`` and ``extract_resume`` reach the model libraries, which cost about
|
|
25
|
+
# four seconds to import. Re-exporting them eagerly made every entry into this
|
|
26
|
+
# package pay that -- including `restruct --help` and a run that fails
|
|
27
|
+
# validation before a model is ever consulted. PEP 562 keeps the public names
|
|
28
|
+
# where they were and defers the cost to the first use.
|
|
29
|
+
_LAZY_EXPORTS = {
|
|
30
|
+
"main": ("restruct.cli", "main"),
|
|
31
|
+
"extract_resume": ("restruct.pipeline", "extract_resume"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __getattr__(name: str):
|
|
36
|
+
target = _LAZY_EXPORTS.get(name)
|
|
37
|
+
if target is None:
|
|
38
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
39
|
+
import importlib
|
|
40
|
+
|
|
41
|
+
module_name, attribute = target
|
|
42
|
+
return getattr(importlib.import_module(module_name), attribute)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __dir__() -> list[str]:
|
|
46
|
+
return sorted(__all__)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"DetectedHeading",
|
|
50
|
+
"ExtractedLine",
|
|
51
|
+
"HeaderEntityMatch",
|
|
52
|
+
"build_v1_resume",
|
|
53
|
+
"extract_resume",
|
|
54
|
+
"main",
|
|
55
|
+
"write_v1_resume",
|
|
56
|
+
]
|