Mondegreen — v0.1.0
用語集を、お願いではなく制約にする。 A private glossary, compiled into a hard phonetic constraint.
Whisper does not know your colleagues' names, your product names or your team's jargon — and 10,000 of them do not fit in a 244-token prompt. Mondegreen corrects them afterwards, locally, as a span replacement that is structurally unable to leave your glossary.
- Code: https://github.com/NagaYu/mondegreen
- Space: https://huggingface.co/spaces/NagaYu/mondegreen
- Dataset: https://huggingface.co/datasets/NagaYu/mondegreen-asr-errors
What is in this repo
gate.json — the calibrated conservative gate (3 KB, ships with the package)lora/ — LoRA adapter for the candidate re-ranker (Qwen2.5-0.5B base)
Read this before assuming what the LM does
The hard constraint is not learned and not in these weights. The legal
replacement set for a span is computed by mondegreen.index.PhoneticIndex as a
finite list, under bounds evaluated before any model runs:
| bound | what it stops |
|---|---|
| normalised phonetic distance ≤ τ (0.28) | unrelated words |
| absolute distance ≤ 0.25 + 0.20·√mora | long terms reached via many cheap edits |
| mora-count difference ≤ 34% | invented syllables |
| common dictionary words need near-exact homophony | 「稼働率」→「加藤率」 |
| containment guard | 「新藤さん」→「新藤」 (deleting an honorific) |
The LoRA adapter only re-ranks candidates already inside that set. It cannot
add to it, cannot introduce a term that is not in your glossary, and cannot
rewrite grammar. On synthetic glossaries only ~1% of spans have more than one
legal candidate, so most of the time it is not consulted at all — which is
precisely why 4-bit quantisation is safe here, and is asserted by
tests/test_quantization.py (removing the LM entirely costs < 2 recall points).
gate.json is the calibrated conservative gate — a logistic regression over 18
interpretable span features (AUC 0.985, ECE 0.053,
threshold 0.82). It is 3 KB of JSON and it is the
component whose job is to say no.
Results
400 held-out sentences, 10,000-term glossary, evaluation glossary strictly disjoint from training by surface and by reading:
| condition | CER | WER | term recall | damage rate |
|---|---|---|---|---|
| (A) raw Whisper | 0.2842 | 0.2541 | 25.3% | 0.00000 |
(B) Whisper initial_prompt |
0.2796 | 0.2509 | 27.2% | 0.00009 |
| (C) cloud LLM post-processing | 0.0893 | 0.0944 | 83.0% | 0.00657 |
| (D) Mondegreen | 0.1105 | 0.1217 | 66.3% | 0.00009 |
| (E) Mondegreen, quantised | 0.1105 | 0.1217 | 66.3% | 0.00009 |
(C) wins on term recall (83.0% vs 66.3%) and does 73× the damage (0.00657 vs 0.00009), needs the transcript to leave the machine, and is therefore unusable on the confidential audio that motivates this project. That trade is the finding, not a footnote.
| throughput | 464 characters/second (10,000-term glossary) |
| 1 hour of transcription | 45 seconds |
| peak memory | 196 MB |
| machine | Apple M2, 16.0 GB |
| network | none |
Provenance. These numbers are
simulated: condition (D) is always the real system, but the error generator and baselines (B)/(C) are explicit models whose parameters are printed in the results file. They are not measured Whisper numbers. Seebenchmarks/README.mdin the repo for how to replace them with measurements.
Use
pip install git+https://github.com/NagaYu/mondegreen
from mondegreen import ConstrainedCorrector, load_glossary
corrector = ConstrainedCorrector(load_glossary("terms.csv"))
print(corrector.correct("進藤さんが両氏誤り訂正の話をしました。").text)
# 新藤さんが量子誤り訂正の話をしました。
With the quantised re-ranker:
from mondegreen.runtime import build_reranker
corrector = ConstrainedCorrector(
load_glossary("terms.csv"),
lm=build_reranker("mondegreen-Q4_K_M.gguf"),
)
CLI, with the evidence for every edit:
mondegreen fix transcript.txt --glossary terms.csv
mondegreen explain transcript.txt --glossary terms.csv
Training data
Synthetic. Glossaries are generated by mondegreen.harvest.GlossaryBuilder;
carrier sentences by SentenceFactory; errors by the phonetic corruption model in
mondegreen.simulate, which perturbs the reading using the same confusion
classes the distance function discounts and re-renders it as a homophone kanji
spelling. No real audio, no real person's name, and no LLM grading anywhere.
Limitations
- Japanese only. The mora table, the confusion costs and the POS rules are all Japanese-specific.
- Without
fugashi/pyopenjtalkthe bundled 4,030-kanji fallback table is used. It has no part-of-speech information, so the common-word protection cannot fire and the damage rate rises. Installmondegreen[g2p]. - The n-gram candidate accelerator is not exact (99.67% recall vs exhaustive at 10,000 terms). Misses can only cause a missed correction, never an illegal one — the bound is re-verified on every scored candidate.
- Evaluation is on synthetic glossaries and simulated ASR errors.
⚠️ Intended use
A glossary can contain personal names. Use this only on data under your own control. Do not run it over other people's meeting records, or with a glossary of names you have no relationship to. Mondegreen handles text only, never audio, and makes no network calls.
Citation
@software{mondegreen,
title = {Mondegreen: private glossaries as hard phonetic constraints for local ASR correction},
author = {NagaYu},
year = {2026},
url = {https://github.com/NagaYu/mondegreen},
license = {Apache-2.0}
}