Skip to main content

Restruct

PyPI version Python versions License

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 is restruct. The unqualified name was already taken on PyPI by an unrelated project.
  • --install-models downloads 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

A resume redrawn from the extracted JSON: name, titles and contact line at the top, then SUMMARY and EXPERIENCE sections with job title, employer, location, dates and bullets.

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

The same resume with coloured boxes drawn over it: name, job titles, phone, email and location in the header, then boxes around each section heading, each skill group and each bullet.

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:

A scanned resume page with the same style of extraction overlay drawn on it.


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.
  • null when the resume has none, [] when the section exists but yielded nothing, never absent.
  • resume.schema.json is 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

MIT

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)

Source distribution for restruct-cv 0.2.3
File Size Uploaded
restruct_cv-0.2.3.tar.gz 122.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for restruct-cv 0.2.3
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

This release

0.2.3 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page