Skip to main content

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)

Source distribution for restruct-cv 0.1.0
File Size Uploaded
restruct_cv-0.1.0.tar.gz 114.5 kB Details

Built distribution (wheel)

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

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

0.2.3

2 release files

This release

0.1.0 This release

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