Shoddy · Fettler

Screening: the models

The second tier. A second program, model checkpoints you choose and fetch yourself, and the licences that come with them.

Note None of this is needed for the first tier. identifiers runs inside fettle with nothing installed — switching it on is one line. This page is the other three categories: clinical, legal and scientific, each of which needs a model.

Install burler, the model host

burler is a second program, in its own archive. It is the trade's word for the person who picks the faults out of finished cloth before it leaves the mill, which is this job exactly. It hosts the named-entity models; fettle talks to it over a pipe and they share no code.

It is separate because of what the models need. Running them needs ONNX Runtime, which ships native binaries for each operating system. fettle is built from a short list of packages that admits none of those, so that it can stay a single file you copy onto a machine. Folding the models in would make every download pay for a feature most trees never use.

Unpack it into the same directory as fettle. That is the only place it is looked for. It is deliberately not searched for on PATH: a program that decides what may leave your tree should not be one an environment variable can substitute.
Windows · PowerShell
$dir = "$env:LOCALAPPDATA\Programs\fettle"   # wherever fettle.exe already is
Expand-Archive burler-X.Y.Z-win-x64.zip -DestinationPath $dir -Force
Get-ChildItem $dir -Recurse | Unblock-File
Get-ChildItem $dir\burler.exe                # it should be beside fettle.exe
macOS and Linux
tar -xzf burler-X.Y.Z-osx-arm64.tar.gz -C ~/.local/bin
xattr -d com.apple.quarantine ~/.local/bin/burler   # macOS only
ls -l ~/.local/bin/burler                           # beside fettle

Archives are published per platform, the same four as fettle: win-x64, linux-x64, osx-x64 and osx-arm64. Take the one matching the fettle you already installed — the two are started as one process pair and must be built for the same machine.

Nothing happens yet. burler with no models does nothing at all, and Fettler will not even start it until a models directory is named in step 6. Installing it early is harmless.

Choose the models

Nothing here ships a model, and nothing here downloads one for you. Quantised, they come to a few hundred megabytes against a fettle download measured in single-digit ones — and there are three of them at most, because identifiers never takes a model. More to the point, several of the useful checkpoints carry terms that would forbid this project redistributing them. So you fetch them, you read their licence, and you decide.

What kind of model this wants

A named-entity model: one that labels each word of a sentence. It is handed a piece of text and answers which words are a person, a date, an identifier, and so on. burler reads those labels, works out where each entity starts and stops, and reports how many it found.

A base checkpoint is not one of these. This is the trap worth knowing before you start downloading. BioBERT and SciBERT are bases — models that understand clinical or scientific language but have not been taught to label anything. What you need is a fine-tune: a base that somebody has further trained on a labelling task, such as clinical de-identification. Look for a model card that names a task, not only a vocabulary.

Anything in the BERT family works, as long as it is a token classifier and exports to ONNX. Nothing here is tied to one checkpoint.

Where to start looking, per category

CategoryFamilyWhat to look for
clinicalBioBERT, ClinicalBERTA de-identification fine-tune. The i2b2 and n2c2 shared tasks are the usual training data, and models trained on them are the ones that label patient names, dates and record numbers — none of which any pattern here finds.
scientificSciBERTAgain a fine-tune, not the base. Entity-recognition models over scientific text.
legalA permissively licensed general model, or LegalBERTContract-clause models trained on CUAD are the readiest. LegalBERT is stronger and has a licence condition — see the table below.

There is no row for identifiers, and there cannot be. It is the pattern tier; it takes no model and no directory. If you want person and place names caught, that is a model under one of the three categories above — not an addition to this one.

Read the licence at the source, every time

This is a gate, not a footnote. Some clinical weights carry data-use terms that forbid passing them on, and a model directory you share with a colleague is passing them on. Two rules make the rest simple:

These were checked against the live model cards and source repositories on 20 August 2026. Check again before you rely on them: terms change, and this table is a starting point rather than legal advice.

CheckpointWhat was foundWhat that means for you
BioBERTApache-2.0, in the licence file of dmis-lab/biobert on GitHub. The model-host mirrors carry no tag.Usable. Record the GitHub repository as provenance, not the mirror you downloaded from.
SciBERTApache-2.0, on allenai/scibert. Same untagged-mirror situation.Usable, same provenance rule.
ClinicalBERT, and the i2b2 de-identification fine-tunesTagged MIT by the groups that held the underlying data-use agreements.Usable on that grant. Record the revision as well as the source, because the grant is theirs and attaches to what they published.
LegalBERTCC-BY-SA-4.0 — a share-alike licence.Usable as your own add-on. Do not use it as a base to fine-tune from, because share-alike carries into whatever you build on it.
Anything with no licence anywhereDo not adopt it. However good the scores.

Prepare them

burler runs models in the ONNX format, which is a portable way of saving a trained model so that programs other than the one that trained it can run it. Most published checkpoints are not in that format yet, so you convert them once.

Export to ONNX

Hugging Face's optimum tool does this in one command. The task matters: token-classification is the name for a model that labels each word.

pip install optimum[onnxruntime]
optimum-cli export onnx --model ORG/CHECKPOINT --task token-classification out/

Quantising afterwards is optional and worth it. Quantisation stores the model's numbers less precisely — typically 8 bits instead of 32. It cuts the file to roughly a quarter of its size and makes it several times faster, at a small cost in accuracy. optimum can do this too. The figures quoted on this page are for quantised models; unquantised, expect roughly four times that.

Lay the files out

One directory per category, named for the category, under one parent:

screening-models/
  clinical/
    model.onnx
    vocab.txt
    labels.json
    manifest.json
  legal/
    model.onnx
    vocab.txt
    labels.json
    manifest.json

Only the categories you screen need a directory. A category you never name in any tree is never looked for. Models load one at a time, when first wanted, so a tree screening only clinical loads one model rather than three. identifiers never needs a directory at all.

FileWhat it is
model.onnxThe exported model.
vocab.txtThe vocabulary that came with the checkpoint — the list of word pieces it was trained on. Take the one from the same checkpoint; a mismatched vocabulary produces confident nonsense.
labels.jsonA JSON array of label names, one per class, in order. The position in the array is the class number. An object mapping numbers to names is refused, because a gap in the numbering would need a rule and a gap means a broken export.
manifest.jsonWhat the model says about itself. Required — see below.

A directory missing any of the first three is refused by name, so you find out which file is absent rather than that something did not work.

Write the manifest

Four fields, all required, all non-empty strings:

{
  "checkpoint": "dmis-lab/biobert-base-cased-v1.1",
  "revision":   "a1b2c3d",
  "licence":    "Apache-2.0",
  "provenance": "https://github.com/dmis-lab/biobert"
}

Name the directory

Put it in the personal overlay, not the checked-in file. Where several hundred megabytes of models sit is a fact about a disk, not about a project, and .fettler.local.json is gitignored:

// .fettler.local.json  - beside .fettler.json, gitignored
{
  "models": "C:/models/screening"
}

It is spelled like every other declared path: forward slashes, and relative paths resolve against the file they are written in. If the check-in file also names one, the overlay wins.

With the directory named and a model in it, roots stops reporting the tier as inactive and names what is doing the judging instead:

$ fettle roots
records       /work/records
              can: list read
              screen: identifiers clinical
                clinical: obi/deid_roberta_i2b2 @ a1b2c3d

declared by /work/shoddy/.fettler.json

The checkpoint and the revision, read from the manifest. A category whose directory holds no model says no model installed - reads here will refuse instead, which is the state worth catching before a read does. The model is never hidden behind the category word: wherever the tiers are listed, the thing doing the judging is named.

Naming this directory is the act that makes the models load-bearing, and it changes what a failure means. Before, the patterns are the whole screen and a clean payload is served — nobody has claimed a model is installed, so there is no second tier to have failed. After, a screened category with no model refuses, because somebody has now said they expect it to be there.

That is why the directory's absence is not checked when the configuration is read. A configuration opened on a machine without the models is not malformed, and refusing the whole boundary over it would take the tree away rather than the screen. The refusal arrives at the disclosure instead, where it can name the category that wanted the model.

Restart the assistant. Then read something. The first screened read starts burler and loads a model, which takes seconds; every read after that is tens of milliseconds.

Optional extras

More than one model in a category

Give the category a directory of subdirectories instead of a model:

screening-models/
  legal/
    cuad-clauses/       model.onnx, vocab.txt, labels.json, manifest.json
    legalbert-ner/      model.onnx, vocab.txt, labels.json, manifest.json

Their findings add together. Any detection by any of them refuses, so a second model can only ever make the screen stricter. That is what lets you run a permissive default alongside a stronger add-on rather than choosing between them.

burler decides which shape a category is by looking for model.onnx directly inside it. Found there, that is the one model. Not found, every subdirectory holding one is loaded.

A shared models directory

Nothing stops "models" naming a network share, and for a team it saves everyone the download. Know what you are trusting when you do: