The Bug: Catbot Can't Hear Its Own Name
Anna Villarreal's desktop pet, Catbot, misheard its name constantly. "Catbot" became "kappa", "catfight", "cat bought", even "Lawrence". Every attempt to wake Catbot triggered a debate about its name. The root cause wasn't hardware or audio quality—it was the speech recognition model's grammar.
Why Vosk Can't Recognize "Catbot"
Catbot uses Vosk, a speech recognition toolkit built on Kaldi. Kaldi decodes audio against a finite state transducer called HCLG, composed of four layers: H, C, L, and G. The G layer is the grammar/language model. It assigns a cost to every possible word sequence, and the decoder picks the sequence with the lowest total cost (acoustic + language costs).
"Catbot" is not a real word. It doesn't exist in the model's vocabulary. So when you say "Catbot", the decoder looks for the closest match—often "kappa". This is a vocabulary trap: there's no path through G that emits "Catbot".
The Fix: Dynamic Graph Model + Grammar Constraint
The default model, vosk-model-en-us-0.22, ships a pre-composed decoding graph. It ignores runtime grammar because the graph is frozen into a single file. Anna discovered that vosk-model-en-us-0.22-lgraph supports dynamic grammar—it keeps the language model (G) as a separate file, allowing you to inject phrases at runtime.
She implemented a dual recognizer: a free recognizer for conversation, and a second, grammar-constrained recognizer that only listens for wake words and commands. The grammar recognizer can't hear anything else, so "kappa" no longer appears as a candidate.
Code: Building a Grammar-Constrained Recognizer
Here's the key part of Anna's implementation:
# catbot_voice.py
VOSK_DYNAMIC = ROOT / "models" / "vosk-model-en-us-0.22-lgraph"
# Wake word forms: "cat bot", "cat pot", etc.
WAKE_NAME_FORMS = ["cat bot", "cat pot", "cat bought", "cat bottom", "cat but",
"catfight", "combat", "cabot", "kat bot", "cap pot"]
What can follow his name
GRAMMAR_TAILS = ["", "engage", "engaged", "wake up", "wake", "are you listening", "are you there", "you there", "go to sleep", "sleep", "stop", "stop listening", "be quiet", "quiet", "shut up", "hush", "open terminal", "close terminal", "approve", "skip"]
Phrases that stand alone
GRAMMAR_BARE = ["open terminal", "close terminal"]
def _build_grammar(model) -> str | None: """JSON phrase list for the constrained recognizer, filtered to words the model actually knows. Returns None if nothing usable survived.""" names = list(WAKE_NAME_FORMS) # ... add learned variants from catbot_wake_variants.json phrases = {f"{lead}{name} {tail}".strip() for lead in ("", "hey ") for name in names for tail in GRAMMAR_TAILS} phrases.update(GRAMMAR_BARE)
# Filter out phrases with words the model doesn't know
kept, dropped = [], set()
for phrase in sorted(phrases):
missing = [w for w in phrase.split() if not in_vocab(w)]
if missing:
dropped.update(missing)
continue
kept.append(phrase)
# ...
return json.dumps(kept + ["[unk]"]) # [unk] is the escape hatch
The grammar includes "[unk]" as an escape hatch, so the decoder can say "that wasn't any of these" instead of forcing conversation into a command.
## Why Dual Recognizers?
A grammar-constrained recognizer alone would break conversation. It can only hear the listed phrases. The free recognizer handles all other speech. The grammar recognizer supplements it, catching wake words and commands reliably.
## The Multimodal Alignment Insight
Anna draws a parallel between speech recognition and Helen Keller's story. The acoustics model (Vosk) perceives raw audio but lacks semantic context. The language model processes text but can't hear. An alignment interface (your code) bridges the two. This is multimodal alignment.
## Hardware Matters Too
Anna initially thought the issue was her webcam mic picking up background noise. Switching to a close-range omni-directional headset mic improved accuracy significantly. But the fundamental bug remained until she addressed the grammar.
## The Takeaway
If you're building voice interfaces with Vosk or similar tools, check whether your model supports runtime grammar. Pre-composed graphs won't accept custom words. Use a dynamic-graph model and constrain the grammar for wake words and commands. And remember: the name you choose for your AI must be in the model's vocabulary, or you'll end up with a pet that argues about its own name.


