CoolFace
Modelpublic

MagicNoThief/handy-editor-lfm2.5-350m

sourceHugging Faceotherupdated 18d agoView on Hugging Face
1likes219downloads
Model Card

Handy Editor 350M

A 350M model that turns a raw dictated transcript into the text the speaker meant to write.

in : um so the meeting is uh moved to friday no wait thursday at three
out: The meeting is Thursday at three.

Three jobs that in speech are one job: drop filler words, repair punctuation and capitalisation, and — the part models of this size usually get wrong — when the speaker changes their mind mid-sentence, delete the wording they abandoned and keep only what they settled on.

It runs in ~100 ms in 229 MB and scores 68/68 on a self-correction suite — the same score a 4B general-purpose model needs 350 ms and 1.67 GB to reach. 7× smaller and ~3× faster at no cost in accuracy: this exists to run locally, on a laptop with no GPU, without adding a pause you can feel before your text appears.

Fine-tuned from `LiquidAI/LFM2.5-350M` for Handier's on-device enhancement layer.

Prompt format

Chat format, with the transcript as the user turn and no instruction. The task is in the weights; adding the instruction back measured worse (57/68 → 54/68 on an earlier checkpoint) because the model started copying the prompt's own rules into its output.

python
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "MagicNoThief/handy-editor-lfm2.5-350m"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id)

messages = [{"role": "user", "content": "um so the meeting is uh moved to friday no wait thursday at three"}]
inputs = tok.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt")
# Editing is near-deterministic. Sampling turns a working transcript into a
# creative one, which is the one failure users cannot forgive.
out = model.generate(inputs, max_new_tokens=256, do_sample=False)
print(tok.decode(out[0][inputs.shape[1]:], skip_special_tokens=True))

With llama.cpp, send an empty system message

Not an absent one — the two are not the same thing here, and the difference is worth 7 points.

llama.cpp does not evaluate the GGUF's jinja template; it matches it to a built-in family and renders that. Its chatml renderer emits the system block even when the content is empty, which the jinja template does not:

text
<|im_start|>system
<|im_end|>
<|im_start|>user
um so the meeting is uh moved to friday no wait thursday at three<|im_end|>
<|im_start|>assistant
System messageSuite
Empty string (renders the block above)66/68
Omitted entirely59/68

In the 59/68 run the model began answering SAME and CHANGED to editing requests — it had stopped recognising the shape of its own input.

With transformers this does not arise: apply_chat_template uses the jinja source, which drops an empty system block, so the plain user-only message list in the snippet above is already correct.

Evaluation

Two independent measurements. Both matter, and a model can pass one while failing the other.

MetricQ4_K_MQ8_0F16
Self-correction suite (68 cases)68/6868/6868/68
cut (46 cases that must delete)46/4646/4646/46
keep (22 cases that must not)22/2222/2222/22
Held-out exact match (2,152 rows, editor-only set)97.4%97.7%97.7%
Held-out mean word-F10.9990.9990.999
Median latency (RTX 3060, Vulkan)~100 ms~100 ms~120 ms
Size229 MB379 MB711 MB

Generation is greedy, so the scores are reproducible rather than a good sample. The latencies are not: across repeated runs the same build measured 94-126 ms depending on what else the machine was doing. Treat them as an order of magnitude and measure on your own hardware if it matters.

`keep` is the number to watch. A model that deletes eagerly scores well on a single total while destroying sentences that were already correct, and that is the failure a user actually notices. 22/22 means it never touched a sentence that did not need touching.

The held-out set is held out by source, not by slicing the training file: real utterances come from a split the training build never reads, synthetic rows use a different seed with every training input excluded by hand, and the builder asserts zero overlap. The suite is 68 hand-written cases, independent of both.

Reproduce with `scripts/enhance-eval/`:

bash
python bench.py --model handy-editor-350m-Q4_K_M.gguf --no-switch --label mine
python eval_heldout.py --model handy-editor-350m-Q4_K_M.gguf \
    --eval handy_eval.jsonl --label mine

Why this checkpoint

Four fine-tunes were compared on the same held-out set:

RunBaseCorpus viewSuiteHeld-out exact
this modelLFM2.5-350Mchat68/6897.4%
earlierLFM2.5-350Mchat66/6889.9%
3,500 stepsLFM2.5-350M-BaseAlpaca61/6865.4%
5,000 stepsLFM2.5-350M-BaseAlpaca60/6844.2% (600-row sample)

The two runs from the base checkpoint got worse between step 3,500 and 5,000, failing by producing the right edit and then continuing ("The conference is in Austin. My mistake is in Vienna. That's not right. Austin. …") until the host's length guard rejected the whole thing. Their prompt format was separately verified correct, so that is a training result rather than a data-pipeline one.

Two variables move at once here — base checkpoint and corpus view — so this table says which artefact to use, not which of the two mattered.

Which file do I want?

You haveTakeWhy
< 6 GB RAM, or a busy machineQ4_K_M (229 MB)97.4%, and 150 MB cheaper
headroom to spareQ8_0 (379 MB)Lossless — scores identically to F16
plans to requantiseF16 (711 MB)Nothing else; see below

Both run at the same speed (~100 ms), so this is purely a memory decision.

The interesting result is that Q8_0 and F16 score identically — 2103/2152 each, not approximately but exactly. Q80 therefore costs nothing in quality against the full-precision weights, and F16 buys only disk. Q4KM is the only one carrying measurable quantisation loss, and it is 6 rows in 2,152: real, but far too small to notice in use. Prefer Q80 if the memory is free, Q4KM if it is not, and do not agonise over it.

Q2_K_L is published nowhere, and you should not make one. It is the reason this section exists:

SuiteHeld-out exact
Q4KM68/68 (100%)97.4%
Q2KL62/68 (91%)47.9%

On the 68-case suite Q2K_L looks merely a little degraded — 91%, a number plenty of people would ship on. On the held-out set it gets \_half the edits wrong. A 68-case suite is structurally unable to see that, which is why any quantisation you make must be run through both evaluations before you trust it. Do not infer quality from the suite alone.

Using it in Handier

Nothing to set up: it is Handier's default editor. Settings → Advanced → Local Enhancement turns the layer on and fetches it, and Settings → Models → Enhancement Models offers both builds — Q8_0 as the default, Q4_K_M for machines counting megabytes. The empty system turn is applied automatically, so there is no prompt setting to get wrong.

Running a GGUF you built yourself is still Your Own Model → Choose a GGUF file…; leave How to prompt this model on Fine-tuned for editing, which is what sends the empty system turn instead of Handier's instruction prompt.

Limitations

  • English only.
  • Opinionated punctuation. Sentence case, full stops added, serial commas absent. It will impose that style on your dictation.
  • It deletes on purpose. Cutting retracted wording is the feature, so its mistakes look like missing words rather than garbled ones. Handy keeps the raw transcript in history for exactly this reason; any host should do the same.
  • Not a general instruction-following model. One task, one format. It will not do anything else usefully, and it has no chat ability worth the name.
  • Short utterances dominate its training. Long-form dictation is ~10% of the corpus, and the two suite failures are both long-range retractions.
  • It cannot judge its own edits. Asked whether an edit preserved meaning it gives a confident, meaningless answer: measured over 400 live edits, that pass caught 0 of 10 bad edits and rejected 1 good one. Do not build a verification step on it.

Training

Base`LiquidAI/LFM2.5-350M`
Data`handy-dictation-editing`, 89,996 rows (15.6% real speech)
Formatchat (messages), no system turn
Context length512 (truncates nothing; max row is 426 tokens)

The corpus is 19% examples that need no edit. That share is load-bearing: trained only on corrections, a model learns that something must always be deleted, and starts eating sentences that were fine.

Licence

Inherits the base model's licence: LFM Open License v1.0 (lfm1.0). The terms are the base model's copy, which license_link points at directly: `LiquidAI/LFM2.5-350M/LICENSE`.

Training data is CC-BY-4.0 and requires attribution to `disfl_qa`, `nyralabs/disfluency_speech_english` and `amaai-lab/DisfluencySpeech`.