User dictation dictionary — STT replacement words (Diapason-style).
Stored as JSON (default ~/.diapason/dictation_dictionary.json).
Not a filler lexicon — fillers live in dictate_polish.py.
Functions:
apply_dictionary
apply_dictionary(
text: str,
entries: Optional[list[DictionaryEntry]] = None,
*,
path: str | Path | None = None,
bump_usage: bool = True,
) -> str
Replace STT mistakes using case-insensitive, word-boundary matches.
Source code in src/diapason/speech/dictation_dictionary.py
| def apply_dictionary(
text: str,
entries: Optional[list[DictionaryEntry]] = None,
*,
path: str | Path | None = None,
bump_usage: bool = True,
) -> str:
"""Replace STT mistakes using case-insensitive, word-boundary matches."""
raw = text or ""
if not raw.strip():
return raw
items = entries if entries is not None else load_dictionary(path)
if not items:
return raw
# Longer keys first so "open ai" wins over "ai"
# Map key_lower → entry id for usage bumps
key_to_id: dict[str, str] = {}
pairs: list[tuple[str, str]] = []
for entry in sorted(items, key=lambda e: (-e.usage_count, -len(e.word))):
for key in _replacement_keys(entry):
pairs.append((key, entry.word))
key_to_id[key.lower()] = entry.id
pairs.sort(key=lambda kv: len(kv[0]), reverse=True)
result = raw
bumped: set[str] = set()
for key, word in pairs:
# Allow multi-word keys with flexible whitespace
parts = re.split(r"\s+", key.strip())
if not parts:
continue
pattern = r"\b" + r"\s+".join(re.escape(p) for p in parts) + r"\b"
new_result, n = re.subn(pattern, word, result, flags=re.IGNORECASE)
if n and bump_usage:
eid = key_to_id.get(key.lower())
if eid:
bumped.add(eid)
result = new_result
if bumped:
by_id = {e.id: e for e in items}
changed = False
for eid in bumped:
e = by_id.get(eid)
if e is None:
continue
e.usage_count = int(e.usage_count or 0) + 1
changed = True
if changed:
try:
save_dictionary(items, path)
except OSError:
logger.debug("could not persist dictionary usage", exc_info=True)
return result
|
learn_from_correction
learn_from_correction(
original: str,
corrected: str,
*,
path: str | Path | None = None,
locale: str = "",
) -> list[DictionaryEntry]
Infer dictionary entries from an STT→user-corrected pair.
Uses difflib opcodes to find short replace spans (1–3 tokens).
Skips filler-only and tiny tokens. Upserts into the dictionary file.
Source code in src/diapason/speech/dictation_dictionary.py
| def learn_from_correction(
original: str,
corrected: str,
*,
path: str | Path | None = None,
locale: str = "",
) -> list[DictionaryEntry]:
"""Infer dictionary entries from an STT→user-corrected pair.
Uses difflib opcodes to find short replace spans (1–3 tokens).
Skips filler-only and tiny tokens. Upserts into the dictionary file.
"""
import difflib
before = (original or "").strip()
after = (corrected or "").strip()
if not before or not after or before == after:
return []
a = before.split()
b = after.split()
if not a or not b:
return []
learned: list[DictionaryEntry] = []
matcher = difflib.SequenceMatcher(a=a, b=b, autojunk=False)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag != "replace":
continue
if (i2 - i1) > 3 or (j2 - j1) > 3:
continue
from_phrase = " ".join(a[i1:i2]).strip()
to_phrase = " ".join(b[j1:j2]).strip()
if not from_phrase or not to_phrase:
continue
if from_phrase.lower() == to_phrase.lower():
continue
if not _learnable_phrase(from_phrase) or not _learnable_phrase(to_phrase):
continue
items = load_dictionary(path)
existing = None
for e in items:
if e.word.lower() == to_phrase.lower():
existing = e
break
if existing is None:
entry = DictionaryEntry(
word=to_phrase,
original_word=from_phrase,
replacements=[from_phrase],
locale=locale,
usage_count=1,
context="auto-learn",
)
else:
reps = list(existing.replacements or [])
if from_phrase not in reps and from_phrase.lower() != existing.word.lower():
reps.append(from_phrase)
if not existing.original_word:
existing.original_word = from_phrase
existing.replacements = reps
existing.usage_count = int(existing.usage_count or 0) + 1
if not existing.context:
existing.context = "auto-learn"
entry = existing
learned.append(upsert_entry(entry, path=path))
return learned
|
transcription_hints
transcription_hints(
entries: Optional[list[DictionaryEntry]] = None,
*,
path: str | Path | None = None,
limit: int = 40,
) -> list[str]
Canonical words for STT boosting prompts.
Source code in src/diapason/speech/dictation_dictionary.py
| def transcription_hints(
entries: Optional[list[DictionaryEntry]] = None,
*,
path: str | Path | None = None,
limit: int = 40,
) -> list[str]:
"""Canonical words for STT boosting prompts."""
items = entries if entries is not None else load_dictionary(path)
ranked = sorted(items, key=lambda e: -e.usage_count)
words: list[str] = []
seen: set[str] = set()
for e in ranked:
w = e.word.strip()
if not w or w.lower() in seen:
continue
# Defensive: a hint is a word, and this list is fed straight to the
# recogniser as a prompt. An oversized entry — from a dictionary
# poisoned before the learn-time caps existed — must not be sent.
if len(w) > _MAX_LEARN_CHARS:
continue
seen.add(w.lower())
words.append(w)
if len(words) >= limit:
break
return words
|