Shoddy · Fettler
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.
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.
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.$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.exetar -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 fettleArchives 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.
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.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.
| Category | Family | What to look for |
|---|---|---|
clinical | BioBERT, ClinicalBERT | A 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. |
scientific | SciBERT | Again a fine-tune, not the base. Entity-recognition models over scientific text. |
legal | A permissively licensed general model, or LegalBERT | Contract-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.
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.
| Checkpoint | What was found | What that means for you |
|---|---|---|
| BioBERT | Apache-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. |
| SciBERT | Apache-2.0, on allenai/scibert. Same untagged-mirror situation. | Usable, same provenance rule. |
| ClinicalBERT, and the i2b2 de-identification fine-tunes | Tagged 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. |
| LegalBERT | CC-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 anywhere | — | Do not adopt it. However good the scores. |
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.
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.
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.jsonOnly 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.
| File | What it is |
|---|---|
model.onnx | The exported model. |
vocab.txt | The 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.json | A 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.json | What 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.
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"
}provenance is where the licence was read, which is
often not where the file was downloaded. That is the whole point of
recording both.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.jsonThe 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.
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.
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.jsonTheir 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.
Nothing stops "models" naming a network share, and for a
team it saves everyone the download. Know what you are trusting
when you do: