Metadata-Version: 2.4
Name: restruct-cv
Version: 0.1.0
Summary: Turn messy PDF, DOCX, and scanned resumes into consistent, explainable, versioned JSON
Keywords: resume,cv,parser,ats,applicant-tracking,ocr,pdf,docx
Author: Nyein Chan Soe
Author-email: Nyein Chan Soe <contact@nyeinchansoe.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Text Processing
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Dist: pillow>=11.0
Requires-Dist: python-docx>=1.1
Requires-Dist: pymupdf>=1.28.2
Requires-Dist: sentence-transformers>=6.0.1
Requires-Dist: transformers>=4.51.3,<5.14.0
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/Nye1nChanSoe/restruct-cv
Project-URL: Repository, https://github.com/Nye1nChanSoe/restruct-cv
Project-URL: Issues, https://github.com/Nye1nChanSoe/restruct-cv/issues
Description-Content-Type: text/markdown

# 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

```bash
uv sync                      # from a checkout
```

or, from a built wheel:

```bash
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:

```text
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.

```bash
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

```bash
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.

```json
{
  "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:

```text
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/`](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/`](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/`](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.

```text
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

```bash
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](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.
