Grammar-constrained wake word recognition with Vosk
I raised the recognition ceiling by upgrading Vosk from small to full and Whisper from base to small. The full Vosk model loaded in 16.05 seconds and added 5.07 GB resident memory. Whisper base and small both passed the self-test identically on clean Kokoro TTS, so the test proved non-regression rather than a gain.
The language model was fighting the name
Live recognition with the full model scored roughly 1 out of 10 attempts. The partials showed it was not an acoustic problem. The language model preferred ordinary English over the proper noun, rendering attempts as "pay for me", "the have", and "hey format". A grammar removes the competition. The decoder may emit only listed phrases or [unk].
Only a dynamic-graph model can take a runtime grammar. The 1.8 GB full model has a static graph and cannot be constrained. I switched the default to lgraph, which loads in 0.83 seconds at +352 MB resident. That is 19 times faster to load and 14 times less memory than the full model. I kept the open-vocabulary shape-matching path as the fallback for full, so selecting the full tier via the new wake_model setting is a real A/B rather than silently broken.
Grammar generation and audition
Grammar generation became style-aware. From a name, six styles are crossed with every expected spelling: bare, hey, okay, good morning, good evening, are you there. That produces 24 phrases per finalist. I added python foreman.py audition <name> to drive the real detector on a candidate, count wakes, and attribute each one to the entry that caught it. A clean hit on the name is distinguishable from a rescue by a variant spelling.
Matching is complete-entry tail-anchored, never "any non-[unk] result fires". A grammar constrains vocabulary but not length, so partials are prefixes of legal phrases. The literal rule would fire on a bare "hey" or "good morning". I do not strip [unk] before matching. Leaving it in stops a mid-sentence phrase from waking. "The site had a foreman on it" returns [unk] hey foreman [unk], and stripping would leave a clean phrase that wakes mid-sentence.
The bare-name style and the cost of the name itself
The bare-name style plus the name "foreman" produced 4 out of 6 false wakes on ordinary trade sentences. It fired on "the foreman said the job was done", "we are forming a new crew tomorrow", and "what is the format of that file". With the name "odin" the same style measured 1 out of 6. This is not a defect in the style. The style correctly reported that the wake name is a common noun in the user's own working vocabulary.
The rename to Odin
I renamed the assistant to Odin. The backronym is Operations, Data, Intelligence, Networks. Foreman stays the product and repo name. The rename was written through the settings layer, so voice and style round-tripped untouched.
The live 21-wake audition was carried by the variant "oden" 17 times and "oh den" 4 times, never by the canonical spelling and never by "o d n". I withheld the canonical spelling from the bare style only, recorded per name in _BARE_CANONICAL_BLOCKED, because the recognizer renders "on it" mid-sentence as "odin". Greeted styles keep the canonical spelling. The exclusion is a per-name table, not a blanket rule on the bare style. Apollo resolved canonically every time in the same audition, so a general rule would have deleted its only bare wake path.
I pulled the "o d n" variant on its finished ledger: 0 genuine wakes across the live audition and retest, 1 false wake in the retest. I expected 0 out of 11 after pulling it. I got 1 out of 11. The collision moved from "o d n" to "oh den". Pruning spellings does not converge. The false wake is reassigned, not removed. I measured every subset. "oh den" plus "oden" scored 1 out of 11, "oden" only scored 1 out of 11 on a different sentence, "oh den" only scored 1 out of 11, and deleting the bare style scored 0 out of 11. A constrained decoder must put every sound on a legal phrase or on [unk], so deleting the phrase catching near-miss audio reassigns it to the next-nearest one.
One false wake in eleven is the price of having a bare style at all on this set. The only lever that reaches 0 out of 11 is deleting the bare-name style row from _WAKE_STYLES, and that costs saying just the name.