Instructions to use lacuna-ai/lacuna-altus with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers.js
How to use lacuna-ai/lacuna-altus with Transformers.js:
// npm i @huggingface/transformers import { pipeline } from '@huggingface/transformers'; // Allocate pipeline const pipe = await pipeline('text-classification', 'lacuna-ai/lacuna-altus');
Lacuna Altus
Mind the gap, before you apply.
Code, training pipeline and the Chrome extension: github.com/cucoleadan/lacuna-ai.
Lacuna Altus scores how a job posting fits a CV, in the browser, on the user's device. It is the large tier (395M parameters) of the Lacuna Chrome extension and the teacher of Lacuna Celer, the fast default.
It is ModernBERT-large fine-tuned with five heads on the LLM-labelled pairs below.
| File | Precision | Size | Note |
|---|---|---|---|
model_fp16.onnx |
fp16 | 790 MB | Identical predictions to PyTorch. Needs a GPU with about 2 GB free |
A 4-bit build (297 MB) was tested and not published: it lost 0.05 verdict F1 on validation, most of what the large model is for.
What it does
Reads a CV and a job posting together (up to 4,096 tokens) and predicts, in one pass:
| Head | Classes | Meaning |
|---|---|---|
verdict |
apply / stretch / skip | Overall fit for this candidate |
location |
yes / unclear / no | Can this person work where the job requires? |
seniority |
below / at / above | Candidate's level vs. the level the posting asks for |
must_met |
no / yes | Are the posting's must-have requirements covered? |
line_met (per job line) |
none / yes / partial / no | For each line of the posting: is it a requirement, and does the CV cover it? |
line_must (per job line) |
probability | Is that requirement a must-have? |
The per-line heads are what make the gaps concrete: the job's must-have lines the CV does not cover.
It is a triage aid for job seekers, used on their own CV. It ranks postings so you read the promising ones first and says where a posting does not fit you. It does not decide anything.
Intended use and limits
- For job seekers only. Do not use it to screen, rank or reject candidates. It was not built or evaluated for that, and hiring decisions about people need human judgement (and, in many places, legal safeguards).
- Labels come from an LLM, not from recruiters. The model learned DeepSeek V4.1 Flash's judgement; it can be no better than that judgement, and has its biases.
- Synthetic CVs. Training CVs were written by an LLM from realistic career paths; real CVs are messier. Normalise your CV the same way (see Input format).
- English only, mostly tech, product, data, design, marketing, sales and operations roles, Europe-heavy locations.
- Weak spots: "too senior for me" postings are often missed; personal taste (industry, company type) is not modelled; US-only remote roles are treated as "not eligible" for people outside the US, which is right for employees but not for contractors.
- Location
noshould not hide a job outright unless the model is confident; the reference extension hides only at P(no) ≥ 0.8 and shows "unclear" otherwise.
Training data
- About 82,000 public job postings from company career sites and job boards (not redistributed).
- 2,217 synthetic CVs, each built from a sampled career path (roles taken from real postings, dates, location, languages, CV style and quality) and written by an LLM.
- 12,699 labelled CV × posting pairs. Per CV: its target job, a similar job, one at another level, one outside the person's work location, plus over-qualified and stretch pairs. Split by CV: 10,171 train / 1,270 validation / 1,258 test.
- Labels: DeepSeek V4.1 Flash read each pair with a fixed rubric (requirements, met / partial / not met, location eligibility, years and level); the verdict is derived from those answers by rules, not asked directly.
- CVs pass through one normalisation (
normalize_cv: block-aware text, ALL-CAPS headings, "- " bullets, contact details removed) in training and at inference.
Input format
[CLS] normalize_cv(cv) [SEP] job [SEP] # truncated longest-first to 4,096 tokens
job = "Title: …\nCompany: …\nLocation: …\nWorkplace: …\n\n" + posting as markdown
ONNX inputs input_ids, attention_mask (int64, [batch, seq]). Outputs: verdict, location, seniority, must_met logits [batch, k]; line_met [batch, seq, 4] and line_must [batch, seq] per token. Average the per-token line outputs over each posting line's tokens (lines of 15+ characters), then softmax / sigmoid. Divide head logits by the temperatures in jobfit.json before the softmax for calibrated probabilities. Labels and temperatures are in jobfit.json.
Evaluation
Macro-F1 on held-out CVs (the test CVs never appear in training, also not in the distillation pairs).
| Model | Params | Verdict | Seniority | Location | Must-haves | Req. lines |
|---|---|---|---|---|---|---|
| Lacuna Altus (this model) | 395M | 0.671 | 0.731 | 0.901 | 0.806 | 0.715 |
| Lacuna Celer (distilled from Altus) | 68M | 0.686 | 0.718 | 0.903 | 0.810 | 0.709 |
| Ettin-68M, same data, no distillation | 68M | 0.654 | 0.667 | 0.891 | 0.804 | 0.686 |
Test split, 1,258 pairs. Honest summary: after distillation, the small model is as good as this one on held-out data (differences are within seed noise; Altus is a single run). Altus is offered for users with a capable GPU who want the original rather than the copy, and it was the best model on validation verdict among all runs at selection time.
Training: 6 epochs, lr 3e-5, 8-bit AdamW, 4,096 tokens, 2× T4 GPUs, about 7 hours.
Model tree for lacuna-ai/lacuna-altus
Base model
answerdotai/ModernBERT-large