Restruct
Turn a resume PDF, DOCX, or a Scan into clean, consistent JSON.
Runs entirely on your machine. No upload, no API key.
Restruct reads a resume the way a person does: it looks at the layout, finds the sections, and pulls out the name, the contact details, the jobs, the dates, the schools, the skills.
The result has the same shape every time, whatever the resume looked like, ready to drop into an applicant tracking system, a search index, or an analytics pipeline.
- PDF, DOCX and scanned pages. One command, one output shape for all three.
- Fully local. The models are files on your disk; nothing leaves the machine.
- Sixteen fixed sections. Always present, always in the same order, never a missing key.
- Two ways to check the result. Draw it back as a page, or see it drawn on the original.
- Careful by default. What it isn't sure about goes to
others, not into the wrong field.
Why Restruct exists
Restruct grew out of my work on Open LinkedOut, a lightweight, local-first job scraping and matching system. Small local models consumed too much RAM and disk space while still hallucinating resume details. Restruct takes a more deterministic and resource-efficient approach, leaving language models to what they do best contextual understanding, not factual extraction.
Install
uv add restruct-cv
uv run restruct --install-models
About those two commands
- The distribution is
restruct-cv; the command it installs isrestruct. The unqualified name was already taken on PyPI by an unrelated project. --install-modelsdownloads the two models Restruct uses: 352 MB, once. They are ordinary local files, verified by checksum as they arrive.- It is the only command that touches the network. Everything after it works offline.
Scanned resumes, the ones that are pictures of pages with no text inside them, also need Tesseract. A normal PDF or DOCX never asks for it.
brew install tesseract # macOS
apt-get install tesseract-ocr # Debian / Ubuntu
Use it
uv run restruct resume.pdf -o .
That writes resume.json next to you, and prints nothing.
-o takes a file or a directory:
| You write | You get |
|---|---|
-o out.json |
exactly that file |
-o . |
resume.json in the current directory |
-o results/ |
resume.json inside results/ |
A directory gets <resume>.json, named after the input, so extracting several resumes into
one place doesn't have each one overwrite the last.
See what was understood
uv run restruct resume.pdf -o . --reconstruct
This draws the extracted result back out as a readable page, reconstruction.pdf plus a PNG
per page, built only from what was extracted, with the original layout thrown away.
That's the whole point:
- A bullet filed under the wrong section is obvious at a glance here, and invisible in a wall of JSON.
- A date read as a job title shows up in the wrong line of the header.
- Anything Restruct could not place is drawn in red under
UNPLACED, rather than quietly dropped.
It is deliberately not a facsimile. Imitating the original layout would hide the very errors it exists to reveal.
You can also draw a result you already have, without re-extracting anything:
uv run restruct resume.json --reconstruct
Check it is ATS-friendly
uv run restruct resume.pdf -o . --ats
Alongside the JSON you get an overlay of every page, showing exactly what a parser could read and where it thought each section began and ended.
- Boxes in the wrong places, or missing, are the layouts machines choke on: side-by-side columns, text inside a graphic, a table nested in a table.
- Each box is labelled with the field it became:
name,job_title,bullet,skill_group. - Heavier boxes are model conclusions. Lighter ones are things the document stated outright, so you can tell a guess from a fact.
Scans go through the same path. OCR is rebuilt into the same geometry a native PDF produces, so a scanned page gets an overlay that looks like any other:
What you get back
resume.json is plain data: no bounding boxes, no model names, no confidence scores.
{
"schema_version": "1.0",
"header_profile": { "name": "…", "emails": ["…"], "phones": ["…"] },
"summary": null,
"experience": [{ "job_titles": ["…"], "companies": ["…"], "bullets": ["…"] }],
"education": [],
"…": "…",
"others": []
}
- Sixteen sections, always present, always in the same order.
nullwhen the resume has none,[]when the section exists but yielded nothing, never absent.resume.schema.jsonis the published contract, and every release is validated against it.- A section Restruct isn't confident about goes to
others, with its original heading kept, rather than being guessed into the wrong place.
Restruct is deliberately careful about what it claims. v1 targets single-column resumes, and a layout whose reading order can't be recovered is recorded as such, never silently repaired into something that reads plausibly and is wrong.
From Python
from restruct import extract_resume
Failures are raised as typed exceptions, so embedding Restruct in a service doesn't cost you your process. The command-line tool turns those into exit codes instead, grouped by decade:
| Code | Meaning |
|---|---|
0 |
success |
1x |
input: not found, unsupported format, unreadable document |
2x |
environment: models missing, Tesseract missing, download failed |
3x |
extraction |
4x |
output |
Examples
Three extracted resumes are committed under examples/, one per kind of input:
| Example | Source | What's in it |
|---|---|---|
7.anomaly/ |
native PDF, 3 pages | JSON and a page overlay per page |
9.ocr/ |
scanned PDF | the same shapes, recovered by OCR |
11/ |
DOCX | JSON only, because a DOCX has no page to draw on |
Worth a look before installing anything.
Contributing
Contributions are welcome. See CONTRIBUTING.md for the setup, the test
suite and the accuracy scorecard, and CLAUDE.md for the design decisions behind each module.
⚠️ Please do not submit real resumes, or labels derived from them. The fixtures in
resumes-synthetic/are synthetic and safe to commit.
License
Metadata
Release files for restruct-cv 0.2.3
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.2.3.tar.gz | 122.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| restruct_cv-0.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 274.7 kB
Release files / restruct_cv-0.2.3.tar.gz
| Download URL | restruct_cv-0.2.3.tar.gz |
|---|---|
| Size | 122.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d03d9297ef98443852144c0e6640d5b625ab0c375a441062be9b1d731bf5211a
|
|
BLAKE2b-256 checksum How to use checksums |
4492178f8b0b337f744905447d334338f3ac238ee5a60aa7a720c42049969096
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","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.2.3-py3-none-any.whl
| Download URL | restruct_cv-0.2.3-py3-none-any.whl |
|---|---|
| Size | 152.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1004d3e6b8ecd8f8152680cac2588d5a739d9424c78ac062639533237f690fee
|
|
BLAKE2b-256 checksum How to use checksums |
e34cfad1470edc3919fb6ba9181713ed39d49fd73e755dc180451f9be6d24aa6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","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}
|