MagicNoThief/handy-dictation-editing
Handy dictation-editing corpus 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 at once, because they are not separable in speech: drop filler words, repair punctuation and capitalisation, and — the hard one — when the speaker changes their mind mid-sentence, delete the wording they abandoned and keep only what they settled on. Built… See the full description on the dataset page: https://huggingface.co/datasets/MagicNoThief/handy-dictation-editing.
Handy dictation-editing corpus
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 at once, because they are not separable in speech: drop filler words, repair punctuation and capitalisation, and — the hard one — when the speaker changes their mind mid-sentence, delete the wording they abandoned and keep only what they settled on.
Built for the on-device enhancement layer in Handier, a fork of Handy, where a sub-500M model has to do this in under a second on a laptop with no GPU. It is not specific to that app: it is a plain instruction-tuning corpus.
Format
Two views of the same rows, generated from one list so they cannot disagree.
*`_alpaca.jsonl` — for fine-tuning a base** model, which has no chat template of its own:
{
"instruction": "Rewrite the dictated transcript as the speaker meant to write it. Remove filler words and hesitations, repair punctuation and capitalisation, and when the speaker corrects themselves keep only the wording they settled on. Reply with the edited text and nothing else.",
"input": "book it for four people make that six people",
"output": "Book it for six people."
}*`.jsonl** — the same examples as an OpenAI messages` array, for an instruct model that already has a chat template:
{
"messages": [
{
"role": "user",
"content": "book it for four people make that six people"
},
{ "role": "assistant", "content": "Book it for six people." }
]
}The chat view carries no system turn. That is deliberate: the behaviour is meant to live in the weights, and prepending a ~450-token instruction to a ~30-token example is most of the sequence for no gain. Measured on an earlier run, giving the fine-tune the instruction at inference was actively worse — 54/68 against 57/68 — because it started copying the prompt's own rules into its output.
The instruction is constant
Every Alpaca row carries the same instruction. This is a property of the intended use, not an oversight: the host that serves the model sends exactly this string, and a fine-tune given an instruction a few words off the one it learned degrades quietly. If you want instruction diversity, paraphrase it yourself — but then serve a matching distribution.
Serve it as one turn
Whatever you train, prompt it with the whole Alpaca string in a single turn. Splitting it — instruction into a chat "system" slot, input into a "user" slot — looks equivalent and is not. Measured on a 350M fine-tune of this corpus:
This bites specifically because a converted base-model GGUF often still carries a chat template, which then wraps the split prompt into a shape the model never saw in training. One assembled turn is safe either way.
Files
Context length
Measured by rendering every row as the full Alpaca sequence — preamble, instruction, input and the response being learned — and tokenising with LFM2's tokenizer:
512 truncates nothing, by two tokens. That is not much margin, and truncation cuts from the right — which is where the target lives, so a clipped row teaches the model to stop mid-sentence. 384 clips 146 rows. If your trainer pads dynamically, 640 costs almost nothing and removes the question; use 512 only if you are sure your renderer matches the one above.
(The chat view is shorter — max 426 — because it has no preamble.)
Composition
89,996 training rows. 15.6% come from recordings of real speech; the rest are generated from templates targeting specific failure modes.
syn_negative at 19% is load-bearing. A corpus of nothing but corrections teaches a model that something must always be deleted, and it starts eating sentences that were fine. The five smallest generators exist because a previous fine-tune got exactly those patterns wrong.
The held-out split is held out by source
handy_eval*.jsonl is not a random slice of the training file. A slice would still share the source sentences and template instances that produced it, and would measure memorisation. Instead:
- real utterances come from DisfluencySpeech's
testsplit, which the training build never reads; - synthetic rows use a different RNG seed, with every training input excluded by hand;
- the builder asserts zero input overlap and fails the build if that ever breaks.
How it was built
scripts/enhance-train/build_dataset.py in Handier:
python build_dataset.py --out ./corpus --n 90000 \
--system-mode none --editor-only --format both
python validate_dataset.py ./corpus # gate before spending GPU timevalidate_dataset.py checks JSON validity, role patterns, that no target still contains a filler or a stutter, that targets are not malformed, and that no eval-suite input has leaked in.
Limitations
- English only.
- Punctuation style is British-ish and opinionated — sentence case, serial commas absent, full stops added. A model trained on this will impose it.
- Short utterances dominate. The median row is a single sentence; long-form dictation is ~10% of the corpus.
- The synthetic majority is template-shaped. It covers the failure modes it was aimed at, and generalises less well outside them.
- Real-speech transcripts carry their annotators' choices, including some targets that are arguably worse than what a good model would write.
- No verification rows. An earlier version of this corpus trained a second "did the meaning change?" task. Measured end to end on 400 live edits it caught 0 of 10 bad edits and rejected 1 good one, so the whole budget went to editing.
Sources and licensing
Released under CC-BY-4.0, the most restrictive of the inbound licences. Attribution to the three datasets above is required.
Check before you publish: disfl_qa is derived from SQuAD, which is CC-BY-SA-4.0. Its own Hugging Face card declares CC-BY-4.0, and this corpus follows that declaration — but if you need certainty about whether a share-alike obligation reaches this derivative, confirm it rather than relying on this note.Citation
@misc{handy_dictation_editing,
title = {Handy dictation-editing corpus},
author = {Handier contributors},
year = {2026},
note = {Built with scripts/enhance-train/build_dataset.py},
url = {https://github.com/MagicNoThief/Handier-desktop}
}