Lacuna Celer

Mind the gap, before you apply.

Code, training pipeline and the Chrome extension: github.com/cucoleadan/lacuna-ai.

Lacuna Celer scores how a job posting fits a CV, in the browser, on the user's device. It is the fast tier (68M parameters, 76 MB) of the Lacuna Chrome extension; Lacuna Altus is the large one.

It is Ettin-68M fine-tuned with five heads and distilled from Lacuna Altus: trained on the LLM labels plus Altus's predictions on 13,970 extra unlabelled CV × posting pairs.

File Precision Size Note
model_q4f16.onnx 4-bit weights, fp16 76 MB Default. Same argmax as PyTorch on 94-99% of validation pairs per head
model_fp16.onnx fp16 137 MB Identical predictions to PyTorch

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 no should 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 Celer (this model, PyTorch) 68M 0.686 0.718 0.903 0.810 0.709
Lacuna Celer, 3 seeds mean 68M 0.665 0.718 0.899 0.808 0.706
Ettin-68M, same data, no distillation 68M 0.654 0.667 0.891 0.804 0.686
Lacuna Altus (teacher) 395M 0.671 0.731 0.901 0.806 0.715

Test split, 1,258 pairs. Distillation lifts seniority clearly (+0.05, about three times the seed-to-seed spread); on verdict, location and must-haves it is within noise. The student reaches the teacher.

On one real user's own past apply/skip decisions (private), Celer ranked applied postings above skipped ones about as well as Altus, and far better than off-the-shelf similarity (TF-IDF, bge-m3, gte-modernbert-base), which cannot tell where a person may work. That check is a single person, not a benchmark.

Training: 3 epochs, lr 1e-4, AdamW, 4,096 tokens, soft-target weight 0.5, 2× T4 GPUs, about 2 hours.

Use

The Lacuna extension does everything above (CV normalisation, encoding, line averaging, calibration) in the browser with ONNX Runtime Web on WebGPU. A JavaScript port of the CV normaliser and encoder is tested against the Python pipeline (identical token ids and line ranges on 200 validation pairs).

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for lacuna-ai/lacuna-celer

Quantized
(14)
this model