Skip to main content

profgen (profile generator)

Project generated with PyScaffold

Convert candidate CVs into standardised Word profiles — without inventing facts.

profgen (the profile generator) turns a candidate CV (PDF/DOCX/TXT) into a standardised Word profile through a verbatim-extract → typed-structure → grounding-check → render → review-report pipeline. The profile is rendered against a template you supply, so any house style — including a private or corporate one — can be applied without the template living in the package.

The one hard rule: no invented facts

Omitting information is acceptable; fabricating a company, tool, date, qualification, institution or project is a defect. Concretely:

  • Anything absent from the source CV is marked "Not stated" (scalars) or left as an empty list — never guessed.
  • A deterministic, LLM-independent grounding check verifies that every extracted hard fact — company, tool, technology, certification, institution and dates — actually appears in the source text. Anything it cannot find is flagged in the review report. Inferred or curated content (the summary, the derived grade, the applied discipline, curated project domains) is exempt from grounding and surfaced in the review report for human sign-off instead.
  • Employers are anonymised. Experience is rendered as Project N | <domain> rather than by company name (the company is still extracted, purely so the grounding check can confirm nothing was invented). By default no client or employer name appears anywhere in the rendered text, and a deterministic anonymisation check lists any it finds in the review report; the config-only [render] include_client_names = true permits them when naming clients is acceptable.
  • No fabricated hard facts. The renderer derives nothing itself: a total years-of-experience figure is never computed from dates and seniority titles are never invented. On the Claude path the skills table's per-skill "Years Experience" column may be estimated from the supporting employment span (balanced/bold only) — but every such estimate is recorded as an inferred value in the review report for sign-off; otherwise the column reads "Not stated".

Each conversion therefore writes two files: the .docx profile and a sibling *.review.md listing missing information and everything to verify before customer submission.

Installation

profgen is published on PyPI (Python 3.11+):

pip install profgen

Or install from source for development:

git clone https://github.com/ksteptoe/profgen
cd profgen
make dev                  # editable install with all dev/docs extras
# or, equivalently:
pip install -e ".[dev]"

Quickstart

# 1. Generate a starter .docx style-donor template (neutral default styles).
profgen make-template templates/profile_template.docx

# 2. Convert a CV offline (no API key, no network) — produces out.docx AND out.review.md.
profgen convert cv.pdf --output out.docx --offline

When --output is omitted the profile is written to <identifier>_profile.docx in the current directory — named from the abstracted identifier (e.g. VV_profile.docx), resolved after structuring so the candidate's real name never leaks into the output filename — with the review report alongside. (The name falls back to the source stem only when no name could be abstracted.)

To convert a whole folder of CVs at once, pass a directory with -R/--recursive:

profgen convert ./cvs -R --offline

This recursively finds every .pdf/.docx/.txt CV beneath the directory, converts each one, and writes each profile (and its *.review.md) next to its own source file, finishing with a Converted N profile(s). summary. Any existing target is backed up to a deterministic <name>.bak (then .bak.1, .bak.2, …) before being overwritten, and generated artefacts and hidden files are skipped. --output cannot be combined with -R (each profile goes next to its source).

To collect every profile in one folder instead, add --output-dir DIR. It redirects the derived <identifier>_profile.docx (and its *.review.md) into DIR — the directory only; the filename stays the PII-safe abstracted identifier — and creates DIR if it does not exist. Unlike --output, --output-dir works with -R: it flattens the whole tree into the one directory, backing up to <name>.bak on an identifier collision. --output-dir and --output cannot be combined.

Add -c/--cost to print the run's estimated Claude API cost once the profile is written — the input/output token counts and the estimate headlined in GBP (£). Anthropic bills in USD, so the figure is computed from the published claude-sonnet-4-6 pricing ($3.00 per million input and $15.00 per million output tokens) and converted USD→GBP at a default rate of 0.79, with the USD basis and the rate shown so it stays verifiable. Override the rate with --gbp-rate FLOAT (e.g. --gbp-rate 0.80) — FX moves, so set it to your own current rate when the figure matters. On --offline it reports that no API call was made, and on a recursive run it adds a per-file line plus a grand total.

Add --pdf (or set [render] pdf = true in a --config file) to also write a sibling PDF next to the .docx profile (e.g. VV_profile.docx → VV_profile.pdf). It works with --output, --output-dir and -R/--recursive. A faithful DOCX→PDF that keeps the branded header, footer, logo and styles needs a real office renderer, so convert drives one of two backends (choose with --pdf-engine {auto,word,libreoffice}; auto is the default):

  • Microsoft Word / Office — on Windows --pdf works out of the box with an installed desktop Word, driven over COM. The required comtypes package is now installed automatically on Windows, so a plain pip install profgen suffices — the profgen[pdf-word] extra is no longer needed there.
  • LibreOffice — the cross-platform alternative (Linux/macOS/Windows). Install it (Debian/Ubuntu sudo apt-get install libreoffice-writer, macOS brew install --cask libreoffice, Windows install LibreOffice) and select it with --pdf-engine libreoffice.

auto prefers Word on Windows and falls back to LibreOffice. If no usable engine is found, convert fails fast with a clear message naming the missing engine before doing any work.

cv-formatter is an identical alias for profgen, and python -m profgen works too. Run profgen convert --help for the full option list.

Bring your own template

The renderer binds content to eleven logical roles — id, title, section_heading, date_heading, project_method, subsection_label, body, bullet, domain_line, legal and table — rather than to fixed style names. By default each role maps to a neutral built-in or starter style (DEFAULT_STYLE_MAP), e.g. id → Profile ID, title → Profile Title, date_heading → Profile Date, body → Normal, bullet → List Bullet, legal → Profile Legal, table → Table Grid. The full role-to-style table is in the user guide.

To apply your own house style, pass your branded document with --template and an inline [style_map] table in your config.toml that points each role at the real paragraph style names in your document:

profgen convert cv.pdf --template my_template.docx --config config.toml
# config.toml — map the logical roles to YOUR template's style names.
[style_map]
title        = "My Heading Style"
date_heading = "My Date Style"
legal        = "My Legal Style"

The map may be partial: any role you omit falls back to its default (so naming only the roles you care about is fine). This is how a private or corporate template can be applied without it ever living in the package. The donor document's header, footer, fonts and logo media are preserved. Scaffold a ready-to-edit starter config with profgen config init. For how to get the full branded look — and the warning convert prints when the donor carries no header, footer or logo — see Branding your profiles in the user guide.

One-step branded profile (make profile)

make profile renders a finished, branded profile from a candidate's CV using your private (gitignored) local/ assets. It is the one-command wrapper around profgen convert.

profgen config init --path local/config.toml              # required — then edit
# drop your branded template at local/template.docx        # required
make profile CV=cv.pdf                                     # Claude path (needs ANTHROPIC_API_KEY)
make profile CV=cv.pdf OFFLINE=1                           # deterministic, network-free path
make profile CV=cv.pdf OUT=out.docx CREATIVITY=bold

What it does

  1. Checks prerequisites — fails early with a clear message if CV= is missing, or if local/template.docx / local/config.toml are absent.
  2. Runs the pipeline — profgen convert <CV> against your branded template and config, writing the .docx profile and its sibling *.review.md.

It expands to:

profgen convert "$CV" \
  --config local/config.toml \
  --template local/template.docx \
  --creativity balanced \
  [--output ...] [--offline]

Inputs (all under the gitignored local/):

Input Role Required?
local/template.docx your branded style-donor (header/footer/logo + named styles) yes
local/config.toml the single config file — inline [style_map], [grade_map], [[discipline]], [job_spec] and the [render] defaults (profgen config init --path local/config.toml) yes

The style map, grade ladder, disciplines and job-spec brief all live as tables inside local/config.toml; each table is optional, so a minimal setup with just the template and a [style_map] works.

Variables

  • CV= — the source CV (.pdf / .docx / .txt). Required.
  • OUT= — output path (default <identifier>_profile.docx in the current directory).
  • OFFLINE=1 — use the deterministic, network-free heuristic instead of Claude.
  • CREATIVITY= — strict / balanced (default) / bold; gates only the inferred fields.

Default vs offline

  • Default — the Claude path; needs ANTHROPIC_API_KEY. Produces the curated, client-grade profile.
  • OFFLINE=1 — no network, fully deterministic; a degraded skeleton (grounded facts only, no curation) that ignores CREATIVITY and the job-spec and cannot infer a discipline (it stays "Not stated"). Useful for a quick structural check without an API key.

The local/ directory and .env are gitignored, so confidential templates, configs and API keys stay out of the repository. Override the asset directory with LOCAL_DIR= (e.g. make profile CV=cv.pdf LOCAL_DIR=clientA) to keep per-client asset sets.

Targeting and identity

Describe your internal grade ladder and the disciplines you staff as tables inside your config.toml: a disciplines = [...] allowed-list, a [grade_map] ladder, optional [[discipline]] enrichment blocks and a [job_spec] brief. Scaffold a ready-to-edit starter into the gitignored local/config.toml and edit it:

profgen config init --path local/config.toml   # then edit
profgen convert cv.pdf --config local/config.toml
  • disciplines — the allowed-list of disciplines profgen may infer from (there is no forced --discipline; see below).
  • [grade_map] — your years-of-experience → grade-title ladder.
  • [[discipline]] — optional per-discipline priority skills and tool groupings that enrich a name in the allowed-list.
  • [job_spec] — a per-run brief (text or file) that augments the discipline defaults; it orders and selects the candidate's grounded strengths, never asserts fit for the role.

Discipline is inferred, not asserted. There is no --discipline flag and no [render].discipline key — both were removed. On the Claude path profgen infers each candidate's discipline from their CV, constrained to your disciplines list (∪ the [[discipline]] block names), choosing the best fit the CV clearly supports and honestly leaving it "Not stated" when nothing fits (a value off the list is kept but flagged for verification). This per-candidate inference removes the batch hazard of stamping one discipline onto every CV. The --offline heuristic cannot infer, so offline profiles carry no discipline on the title line.

The scalar flags --id-mode, --id and --creativity are active and override the matching [render] value. The grade is derived from the candidate's stated years (and stays "Not stated" when no years are given), and the candidate's identity is abstracted — --id-mode initials renders "Valentin Vasilescu" as "VV", --id-mode number gives a stable number, --id forces an explicit value, and --id-mode none keeps the real name. The real name is always retained for grounding but only shown with --id-mode none.

On the Claude path, --creativity {strict,balanced,bold} (default balanced) tunes how much interpretive licence the structuring step takes over inferred fields — hard facts stay strictly grounded at every level — and the [job_spec] brief plus the inferred discipline's [[discipline]] targeting decide which grounded strengths lead the summary and the skills table — ordering and selection only; the profile never asserts that the candidate fits the role, and the review report flags any "strong fit" / "aligned with" wording that slips through. The --offline heuristic client ignores these (it curates nothing). See the user guide for the full details.

One config file for everything (--config)

All per-run inputs — the style map, grade ladder, disciplines, job-spec brief and render defaults — live as tables inside a single config.toml. Point convert at it with --config:

profgen convert cv.pdf --config local/config.toml

Explicit flags still override the file (flag → config value → built-in default), so every flag-driven invocation keeps working unchanged. Scaffold a starter with profgen config init (--user for the per-user location, --minimal for a comment-free copy); profgen config path reports which config a run will use.

Auto-discovery. When you omit --config, profgen looks for a config in the standard locations and uses the first that exists, in order:

  1. the file named by the $PROFGEN_CONFIG environment variable;
  2. ./profgen.toml in the current working directory;
  3. config.toml in the per-user config directory (%APPDATA%\profgen on Windows, ~/.config/profgen on Linux, ~/Library/Application Support/profgen on macOS — resolved with platformdirs).

On a find it reports Using configuration from <path> to stderr, then loads it. So naming your file profgen.toml in your project directory (or config.toml in your user config directory) lets you simply run profgen convert cv.pdf with no --config. An explicit --config FILE always bypasses discovery and wins. Set PROFGEN_CONFIG_DIR to relocate the per-user config directory base. make profile passes local/config.toml explicitly.

The unified file also drives the profile's section order and selection: a [[section]] list chooses which body sections render and in what order (with an optional per-section heading override), and can switch on the off-by-default certifications and languages sections. The title and legal footer stay pinned first and last, and [render].legal_text overrides the closing footer wording. See the user guide for the full schema.

Offline vs real Claude path

The structuring stage has two interchangeable backends behind one interface:

  • Offline (--offline) — the deterministic, network-free HeuristicStructuringClient. Needs no API key, makes no network call, and is what the entire test suite uses. Ideal for plumbing checks and CI.
  • Real Claude (default) — the ClaudeStructuringClient, which calls the Anthropic API and needs ANTHROPIC_API_KEY. This path is deliberately never exercised in CI; it is smoke-tested only behind an explicit opt-in (see examples/smoke_real_path.py).

Example

A runnable, fully-offline example builds a profile from a bundled synthetic CV with no API key:

.venv/bin/python examples/build_example_profile.py

It reads examples/input_cvs/sample_cv.txt, runs the offline pipeline, and writes the profile and its review report into examples/output_profiles/ (gitignored).

Development

make dev      # editable install with all dependencies
make test     # run the fully-offline test suite
make lint     # ruff
make format   # ruff --fix
make docs     # build the Sphinx HTML User Guide
make docs-pdf # build a single PDF of the docs (needs a LaTeX toolchain)

Quality gates: ruff clean, mypy --strict clean (scoped to src/), and pytest green with the network disabled. The Sphinx User Guide (make docs, or docs/userguide.md) is the full pipeline walkthrough and the shipped reference for every option and configuration file.

make docs-pdf produces docs/_build/latex/profgen.pdf. It needs a system LaTeX toolchain on PATH — xelatex, latexmk, and makeindex (install TeX Live or, on Windows, MiKTeX). The toolchain is not pip-installable and is optional: the target fails fast with a clear message if latexmk is missing.

Note

This project has been set up using PyScaffold 4.6 with the ClickStart extension.

Release files for profgen 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 profgen 0.1.0
File Size Uploaded
profgen-0.1.0.tar.gz 404.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for profgen 0.1.0
File Interpreter ABI Platform
profgen-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 536.3 kB

Release files / profgen-0.1.0.tar.gz

Download URL profgen-0.1.0.tar.gz
Size 404.8 kB
Tags Source
SHA-256 checksum
How to use checksums
8e6020f74705550187300a7017935fc22a2c6109b60e7ab0ef4a43634daa3693
BLAKE2b-256 checksum
How to use checksums
2a56ce341dbe530d60e561969e501c613647c5b5af3adbb3e88ee4d4f2725b66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / profgen-0.1.0-py3-none-any.whl

Download URL profgen-0.1.0-py3-none-any.whl
Size 131.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
622152cc50ccb941f103c41a9e907cd1b9997f4ffd5140d2b1873fe60513c35d
BLAKE2b-256 checksum
How to use checksums
2f5f4c8708780cfabfc6f200ae34273b196a5b7d67d945fd8af01f23a68f88ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

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