Restruct
Restruct turns messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON. It combines OCR, layout geometry, NER, semantic similarity, and deterministic rules so every result can be traced back to the thing on the page that produced it.
Built for applicant tracking systems, candidate search, analytics pipelines, talent platforms, and other systems that need trustworthy resume data.
Install
uv sync # from a checkout
or, from a built wheel:
uv build
pip install dist/restruct_cv-*.whl
The distribution is restruct-cv; the import name and the command it installs are both
restruct. The unqualified name was already taken on PyPI by an unrelated project.
Model weights
All inference weights are local-only and never fetched at run time. Two directories are required:
models/
all-MiniLM-L6-v2/
distilbert-NER/
They are looked for, in order, in models/ beside the checkout (when running from one), models/
under the current working directory, and ~/.restruct/models. Set
RESTRUCT_MODELS_DIRECTORY=/path/to/models to name the directory outright, which is the usual
answer for an installed copy; when it is set nothing else is consulted. A run with no weights
exits 20 and names every place it looked.
Tesseract
Tesseract is a system dependency, needed only for scanned pages — a PDF with its own text layer and a DOCX never ask for it.
brew install tesseract # macOS
apt-get install tesseract-ocr # Debian/Ubuntu
It is found on PATH, or where the installers put it (Program Files on Windows, either Homebrew
prefix on macOS). PyMuPDF renders an OCR page at 300 DPI and tesseract is invoked directly,
without a shell, using the English language data, LSTM engine mode 1 and page segmentation mode 3.
Rendered page images are deleted after each document.
Usage
uv run restruct <resume.pdf> -o out.json # one resume; quiet, writes nothing else
uv run restruct <resume.docx> -o . # a directory: writes <resume>.json into it
uv run restruct <resume.pdf> -o out.json --debug # + stage 4-5 artifacts in out/
uv run restruct <resume.pdf> -o out.json --stages 1-3 # + those stages (implies --debug)
uv run restruct <resume.pdf> -o out.json --reconstruct # + the result drawn back as a page
uv run restruct examples/11/resume.json --reconstruct # draw a result already extracted
uv run restruct # batch over resumes-synthetic/
uv run restruct --truths # batch over resumes-truths/ (local, gitignored)
uv run restruct --unsupported # batch over resumes-unsupported/
--stages selects debug artifacts, never whether a pass runs: every pass feeds the next, so a
flag that skipped one would quietly produce a different resume.
Exit codes are an API, grouped by decade — 1x input, 2x environment, 3x extraction, 4x
output, with 2 left to argparse. errors.py names every failure and cli.py is the only module
that maps one to a code, so a caller embedding restruct as a library catches an exception by type
instead of losing its process.
Output
resume.json is lean and metadata-free: no bounding boxes, fonts, geometry, model names,
confidences or detection methods. Its first key is schema_version, and resume.schema.json in
the repository root is the published contract every fixture is validated against.
Sixteen destinations are always present, always in the same order. A section is null when the
resume has none, [] when it exists but yielded no entries; a key is never absent.
{
"schema_version": "1.0",
"header_profile": { "name": "…", "emails": ["…"], "phones": ["…"] },
"summary": null,
"experience": [{ "job_titles": ["…"], "companies": ["…"], "bullets": ["…"] }],
"education": [],
"…": "…",
"others": []
}
All the evidence behind those values — boxes, fonts, confidences, which method decided what —
lives in a separate track under raw/, and the overlays draw it:
results/<name>/
resume.json the lean output
raw/*.json the evidence: boxes, fonts, confidences, methods
debug/page-N.png the combined overlay, one per source page
debug/pass-*/ passes 1-4, gitignored
The pass-1 dump is named after the reader that produced it — pymupdf.json for a PDF, docx.json
for a DOCX, since the two read different things and only one of them has coordinates. The
single-file form writes it beside the other evidence in raw/; the batch writes it to
debug/<name>.raw-<reader>.json.
A model-backed box is drawn more heavily than a deterministic one, so a reader can tell whether a box is something the document said or something a model concluded.
Reading the result back
--reconstruct draws resume.json back out as a page — reconstruction.pdf and one PNG per
page — so the result can be proof-read by eye.
Drawing a result on its own writes those files flat beside the JSON, named after it
(resume-reconstruction.pdf, resume-page-1.png); -o names a directory to put them in
instead, under their plain names.
It answers a different question from the overlays. An overlay draws on top of the document, so it shows whether a box landed on the right words; the document keeps making sense regardless of what was understood. A reconstruction throws the page away and draws only what was understood, which is what makes a bullet filed under education or a date read as a job title visible at a glance.
It is deliberately not a facsimile — imitating the original layout would hide the errors it exists
to reveal. Absent and empty fields are skipped, so what is on the page is what was extracted, and
anything the renderer cannot place is drawn in red under UNPLACED rather than dropped. Given a
resume.json as the input it draws that and runs nothing else, loading no models.
Worked examples
results/ is regenerated by every run and is not committed. Three resumes are, under
examples/ — one per ingestion track, because what each track makes available is different:
| Example | Source | What it shows |
|---|---|---|
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. |
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. |
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. |
Each holds the resume.json, the raw/*.json evidence behind it, and the overlays where there
are any. They are copied out of results/ by tools/refresh_examples.py rather than curated by
hand, so they are what the pipeline currently produces and not a snapshot of what it once did.
Their inputs are in resumes-synthetic/, and every one is synthetic.
Architecture
Five ordered passes over one shared in-memory document.
ingestion/ physical extraction — native PDF text, per-page OCR fallback, DOCX
document/ shared types and document-wide statistics
layout/ row clustering, paragraph/bullet accumulation, unsupported-layout detection
structure/ heading detection, routing, compound headings, precedence resolver, separators
parsers/ one module per section shape (header, experience, education, skills, grouped, urls)
model.py DistilBERT NER and MiniLM adapters, loaded on first use
patterns/ deterministic regex evidence, grouped by what it describes
debug/ artifacts (JSON) and overlays (Pillow), one canvas, one colour registry
schema.py the lean, versioned output (contract: resume.schema.json)
errors.py the failure taxonomy; only cli.py turns one into an exit code
pipeline.py orchestration only — the only module that knows the stage order
cli.py argparse and filesystem layout only
Extraction runs in a fixed precedence — deterministic, context-sensitive deterministic, NER,
MiniLM, geometry, then other — and structure/resolver.py enforces that order rather than
documenting it: a stage that runs after a weaker one has already claimed a span raises.
v1 targets single-column resumes. Layouts whose reading order cannot be recovered (column gutters,
vertical text, overlapping boxes, nested tables) are recorded, never repaired, in
debug/layout-warnings.json.
Development
uv run pytest # full suite
uv run python -m tests.scorecard # per-field precision/recall/F1
uv run restruct && uv run python tools/refresh_examples.py
git status --short examples/ # clean == byte-identical
See CONTRIBUTING.md for the test and scorecard workflow, and CLAUDE.md for the
design decisions behind each module.
Please do not submit real resumes or their labels; resumes-truths/ is gitignored for exactly
that reason. Fixtures in resumes-synthetic/ are synthetic and safe to commit.
Metadata
Release files for restruct-cv 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| restruct_cv-0.1.0.tar.gz | 114.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| restruct_cv-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 258.9 kB
Release files / restruct_cv-0.1.0.tar.gz
| Download URL | restruct_cv-0.1.0.tar.gz |
|---|---|
| Size | 114.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1aacf7e7a25fe83bab36ef9adad5de7c6b2df88ed59fa66f40aaf20d372e9138
|
|
BLAKE2b-256 checksum How to use checksums |
78d8bd4e67227fe0b67e4822c9e0d42c80bc136fbf8a9f5f0566443243cc3bed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / restruct_cv-0.1.0-py3-none-any.whl
| Download URL | restruct_cv-0.1.0-py3-none-any.whl |
|---|---|
| Size | 144.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff6da82a344423fbe758d1c965eae4ee2d508dc83be2180e392039bd5b965554
|
|
BLAKE2b-256 checksum How to use checksums |
0ba33df5d6b52cdf873cca2eef86e17affd2964210e82a95dea22113b863bdf9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|