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.
Files changed (62) hide show
  1. restruct_cv-0.1.0/LICENSE +21 -0
  2. restruct_cv-0.1.0/PKG-INFO +226 -0
  3. restruct_cv-0.1.0/README.md +198 -0
  4. restruct_cv-0.1.0/pyproject.toml +64 -0
  5. restruct_cv-0.1.0/pyproject.toml.orig +54 -0
  6. restruct_cv-0.1.0/src/restruct/__init__.py +56 -0
  7. restruct_cv-0.1.0/src/restruct/cli.py +521 -0
  8. restruct_cv-0.1.0/src/restruct/configs/__init__.py +5 -0
  9. restruct_cv-0.1.0/src/restruct/configs/embedding_references.py +290 -0
  10. restruct_cv-0.1.0/src/restruct/configs/settings.py +175 -0
  11. restruct_cv-0.1.0/src/restruct/debug/__init__.py +1 -0
  12. restruct_cv-0.1.0/src/restruct/debug/artifacts.py +134 -0
  13. restruct_cv-0.1.0/src/restruct/debug/canvas.py +141 -0
  14. restruct_cv-0.1.0/src/restruct/debug/colors.py +172 -0
  15. restruct_cv-0.1.0/src/restruct/debug/reconstruct.py +663 -0
  16. restruct_cv-0.1.0/src/restruct/debug/render.py +323 -0
  17. restruct_cv-0.1.0/src/restruct/debug/stages.py +294 -0
  18. restruct_cv-0.1.0/src/restruct/document/__init__.py +17 -0
  19. restruct_cv-0.1.0/src/restruct/document/physical.py +293 -0
  20. restruct_cv-0.1.0/src/restruct/document/stats.py +532 -0
  21. restruct_cv-0.1.0/src/restruct/document/types.py +121 -0
  22. restruct_cv-0.1.0/src/restruct/errors.py +128 -0
  23. restruct_cv-0.1.0/src/restruct/geometry.py +151 -0
  24. restruct_cv-0.1.0/src/restruct/ingestion/__init__.py +1 -0
  25. restruct_cv-0.1.0/src/restruct/ingestion/docx.py +558 -0
  26. restruct_cv-0.1.0/src/restruct/ingestion/native.py +256 -0
  27. restruct_cv-0.1.0/src/restruct/ingestion/ocr.py +248 -0
  28. restruct_cv-0.1.0/src/restruct/layout/__init__.py +1 -0
  29. restruct_cv-0.1.0/src/restruct/layout/blocks.py +154 -0
  30. restruct_cv-0.1.0/src/restruct/layout/lines.py +75 -0
  31. restruct_cv-0.1.0/src/restruct/layout/rows.py +95 -0
  32. restruct_cv-0.1.0/src/restruct/layout/unsupported.py +237 -0
  33. restruct_cv-0.1.0/src/restruct/layout/words.py +229 -0
  34. restruct_cv-0.1.0/src/restruct/model.py +674 -0
  35. restruct_cv-0.1.0/src/restruct/parsers/__init__.py +1 -0
  36. restruct_cv-0.1.0/src/restruct/parsers/education.py +340 -0
  37. restruct_cv-0.1.0/src/restruct/parsers/experience.py +474 -0
  38. restruct_cv-0.1.0/src/restruct/parsers/grouped.py +580 -0
  39. restruct_cv-0.1.0/src/restruct/parsers/header.py +513 -0
  40. restruct_cv-0.1.0/src/restruct/parsers/skills.py +266 -0
  41. restruct_cv-0.1.0/src/restruct/parsers/urls.py +194 -0
  42. restruct_cv-0.1.0/src/restruct/patterns/__init__.py +1 -0
  43. restruct_cv-0.1.0/src/restruct/patterns/bullets.py +14 -0
  44. restruct_cv-0.1.0/src/restruct/patterns/contacts.py +23 -0
  45. restruct_cv-0.1.0/src/restruct/patterns/dates.py +59 -0
  46. restruct_cv-0.1.0/src/restruct/patterns/education.py +32 -0
  47. restruct_cv-0.1.0/src/restruct/patterns/languages.py +81 -0
  48. restruct_cv-0.1.0/src/restruct/patterns/layout.py +31 -0
  49. restruct_cv-0.1.0/src/restruct/patterns/organizations.py +19 -0
  50. restruct_cv-0.1.0/src/restruct/patterns/personal.py +69 -0
  51. restruct_cv-0.1.0/src/restruct/patterns/separators.py +47 -0
  52. restruct_cv-0.1.0/src/restruct/pipeline.py +247 -0
  53. restruct_cv-0.1.0/src/restruct/schema.py +257 -0
  54. restruct_cv-0.1.0/src/restruct/stages.py +28 -0
  55. restruct_cv-0.1.0/src/restruct/structure/__init__.py +1 -0
  56. restruct_cv-0.1.0/src/restruct/structure/compound.py +375 -0
  57. restruct_cv-0.1.0/src/restruct/structure/headings.py +195 -0
  58. restruct_cv-0.1.0/src/restruct/structure/keyvalue.py +96 -0
  59. restruct_cv-0.1.0/src/restruct/structure/metadata.py +66 -0
  60. restruct_cv-0.1.0/src/restruct/structure/resolver.py +218 -0
  61. restruct_cv-0.1.0/src/restruct/structure/sections.py +189 -0
  62. 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
+ ]