Files
voculinux/docs/CUSTOM_DICTIONARY_DESIGN.md
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

126 lines
6.8 KiB
Markdown

# Custom Dictionary Support Design
## Scope
Custom dictionary support has two separate jobs:
1. **Custom terms** bias local Whisper, whisper.cpp, and Faster Whisper recognition
toward names and jargon.
2. **Transcript corrections** deterministically replace a known misheard phrase
after transcription.
They appear together on the **Custom Dictionary** Settings page, but are not
interchangeable. A correction replacement does not implicitly become a prompt
term. That coupling would make recognition bias depend on correction order and
would make the external terms contract surprising.
## File contracts
Both files live in Voculinux's XDG-aware configuration directory:
`$XDG_CONFIG_HOME/voculinux` (or `~/.config/voculinux` when unset).
| File | Contract | Consumer |
| --- | --- | --- |
| `dictionary.txt` | UTF-8 (BOM accepted), one term per line. Blank lines and lines beginning with `#` are ignored. Terms are de-duplicated case-insensitively while retaining the first spelling. | Voculinux and an external accessibility scanner. |
| `custom-dictionary-corrections.json` | UTF-8 JSON object: `{"version": 1, "corrections": [{"heard": "super base", "replacement": "Supabase"}]}`. | Voculinux only. |
The terms file remains deliberately simple and scanner-friendly. Corrections
need two unambiguous fields, a version, and exact replacement casing, so JSON is
used rather than overloading the line file with an ambiguous delimiter grammar.
The UI writes either file by atomic replacement; a failed UI save preserves the
old file and reports an error.
The application reads both files for each completed transcript. An externally
edited file therefore takes effect on the next dictation segment without an app
restart. Missing, unreadable, malformed, or unsupported correction files are
ignored safely; dictation continues without their entries.
The terms file may contain any number of lines, but Voculinux limits a single
prompt to the first 200 valid terms and 2,000 characters. This bounds decoding
work while leaving the scanner's complete line-file contract intact. The Custom
Dictionary page keeps both an add/remove editor and a validated file chooser
for the terms file.
## Processing and engine behavior
For every completed audio segment, Voculinux applies this order:
1. The selected engine transcribes audio.
2. Live phrase corrections run against the raw transcript.
3. Voice-command interpretation runs on that corrected text.
4. Remaining text and actions are dispatched.
Corrections run before commands so a correction can prevent a command-like
misrecognition from being acted on. This also means users must not create a
replacement that deliberately creates a voice command unless that is intended.
Corrections match literal phrases case-insensitively, only where the adjacent
characters are not Unicode word characters (or common combining marks). Longer
phrases win over shorter overlaps; equal-length entries retain JSON order.
Replacement text is inserted exactly as written. Inputs are normalized to NFC
for matching, so composed/decomposed accents are handled consistently.
| Engine | Custom terms | Corrections |
| --- | --- | --- |
| OpenAI Whisper | `initial_prompt` | Yes |
| whisper.cpp | Advanced initial prompt followed by custom terms; explicit empty prompt clears reused native state | Yes |
| Faster Whisper | `initial_prompt` | Yes |
| VOSK | No prompt-bias API | Yes |
| Parakeet | No prompt-bias API | Yes |
| Remote API | No prompt is added to current request formats | Yes |
`--dictionary-file PATH` preserves the #767 session-only behavior: `PATH` is
used as the custom terms file, it enables terms for that session, and Settings
does not modify the saved terms file or enablement. Corrections remain loaded
from the standard JSON file. The previous experimental #768
`text_injection.custom_dictionary` config list is read only as a fallback until
the JSON corrections file exists; it is never silently deleted or migrated.
The #767 persisted configuration keys are retained unchanged:
`dictionary.enabled`, `dictionary.file_path` (default
`~/.config/voculinux/dictionary.txt`), and `dictionary.max_words`. Existing
Peony settings therefore continue to select the scanner's `dictionary.txt`
file. New configured paths are accepted only when they can be expanded and are
either absent (so the UI can create them) or readable regular files; a failed
save leaves the prior setting in place. Invalid configured and `--dictionary-file`
paths are safely ignored and shown as unavailable rather than raising.
## Alternatives rejected
- **One mixed line file:** cannot express arbitrary phrases and replacements
without reserving/escaping delimiters, which weakens the scanner contract.
- **Corrections in `config.json`:** makes a scanner compete with application
settings writes and offers no durable external-file schema.
- **Post-command correction:** allows command-like transcription errors to be
acted on before they can be fixed.
- **Automatically adding correction replacements to prompts:** rejected because
replacements are not automatically included in the recognition prompt;
corrections work post-transcription on every engine.
## Human test checklist
1. Add `Voculinux`, `PyGObject`, and a non-ASCII term in **Custom terms**;
confirm `dictionary.txt` is UTF-8, one line per term, and an external
edit changes the next Whisper, whisper.cpp, and Faster Whisper dictation segment.
2. Add `super base` → `Supabase`; test lowercase, uppercase, punctuation, an
overlapping short correction, `C++`, and non-ASCII text. Confirm replacement
casing is exact and embedded text (for example `supersize`) is not replaced.
3. With voice commands enabled, add a correction that changes a command-like
misrecognition into ordinary text. Confirm it is injected rather than acted
on. Also verify an intentionally command-like replacement is avoided.
4. Edit each file externally while Voculinux is running. Test an invalid JSON
correction file and invalid UTF-8 terms file; confirm dictation continues and
the Settings status/save feedback is clear.
5. Test the UI with keyboard only: Add buttons via Enter, empty validation,
update/remove feedback, narrow window, long entries, and high-contrast theme.
6. Test all engines: Whisper, whisper.cpp, and Faster Whisper should use terms and
corrections; VOSK, Parakeet, and remote API should use corrections only, with
the VOSK terms warning.
7. Start with `--dictionary-file /path/to/terms.txt`; confirm the session uses
that file, Settings disables terms editing, saved settings remain unchanged,
and corrections still apply.
8. Start with an unresolved path such as `--dictionary-file ~missing-user/terms.txt`
and configure an unreadable path through the file chooser. Confirm Settings
reports the path safely, dictation continues without terms, and the prior
saved `dictionary.file_path` remains intact.