Adding a weather tool, wake word detection, and reminders to Foreman
I shipped three major features for Foreman across four sessions on July 30th: external weather data, hands-free wake word listening, and a reminders system.
Weather via Open-Meteo
I added Foreman's first external data tool using Open-Meteo, a free weather API that requires no signup or key. The tool is read-only and SAFE-classified. This sets the pattern for all future external tools: one named tool, one fixed source, no general web access.
Weather answers now shape to the question. "Is it going to rain today?" gets a yes/no verdict plus numbers, while "what's the weather?" returns a general two-sentence summary. The router picks the shape via a focus enum, but every number and verdict comes straight from the tool dict through a fixed template. The model cannot invent a temperature.
Rain verdicts use fixed thresholds: under 20% chance means no rain expected, 20-60% is maybe, over 60% is likely. The forecast covers today only. "Will it rain tomorrow?" routes to the rain focus but answers for today, documented as a known limit.
Knowing the current date and time
Emma now always knows the current date and time through prompt injection, not a tool. Every routing call carries a fresh "Current date and time: ..." line at the front of the system prompt, plus a directive to answer date and time questions directly from it. The datetime is built fresh on every call and never cached, so two calls a minute apart carry different timestamps.
I fixed TTS reading clock times digit-by-digit. "9:07 AM" was spoken as "nine zero seven." A normalization step now runs inside the say() function before synthesis, so Kokoro says "nine oh seven." The fix covers every current and future spoken time without callers knowing. Printed output stays untouched.
Wake word listening with Vosk
I built hands-free wake word listening as an alternative to the push-to-talk hotkey. The hotkey mode is fully retained.
I used Vosk keyphrase spotting instead of openWakeWord because openWakeWord's stock models are licensed CC BY-NC-SA 4.0, which is non-commercial. Vosk and its small English model are both Apache 2.0.
The wake phrase is name-derived: "hey" plus the assistant name lowercased. For Emma, it's "hey emma." The phrase is read at start from get_assistant_name(), so renaming the assistant in the panel retunes the wake word on next start.
The detector sits behind a small swappable interface with phrase, reset(), and accept(pcm). A dedicated acoustic wake model can replace Vosk at the single _make_detector() swap point without touching the mic capture or pipeline.
Only arrival changes. The whole corrections, refusals, router, gate, and speak chain is untouched. Gate confirmation and refusals work identically in wake mode.
Live tuning across four rounds
I tested wake mode live across four rounds, fixing defects between each.
Round two added a passive gate, debug mode, and per-mode messaging. I hardened the partial match to fire only on the just-spoken tail, added a frames-discarded counter proving passive audio is dropped, and added wake --debug to print every partial and final.
Round three fixed recognizer deafness. Live debug showed Vosk degrading after long unbroken streams. I now rebuild the recognizer from the loaded model after every endpoint and every 30 seconds. I also switched from a literal-phrase grammar to free-form recognition with shape-matching, because the small model cannot produce "emma." Live partials were "hey mom," "hey member," "a m m," "him," and "hey." The _spotted() function matches these real mishearings.
Round four added a spoken acknowledgment, native-rate capture, and split-phrase assist. Wake mode now speaks a short random ack like "Yes?" or "I'm here" via say(), so the user hears an answer. I confirmed she cannot self-wake because the mic is closed during the ack window. I fixed an audio quality bug where capture was resampling each 100ms block from 44100 to 16000 and concatenating, causing per-block artifacts. It now keeps native-rate blocks and resamples the whole command once, like hotkey mode. A bare "hey" is remembered, and any m-word within 1.5 seconds completes the wake, bridging splits even across a rebuild. Every wake appends its partials to memory/wake_tuning.log.
Round five tightened wake matching after a privacy incident. A speakerphone call woke wake mode six times. I diagnosed the incident from the tuning log. Every capture followed a rule-qualifying wake, so there was no stealth capture. All six false wakes were the same vector: a bare or trailing "hey" standing in for the whole phrase via the old _SINGLE_WAKE rule. The spoken ack fired before every capture, so all six false wakes were audible.
I removed the _SINGLE_WAKE rule, the _EMMA_FRAGMENTS rule, and the loose "hey" plus any-word-starting-with-m catch-all. A wake now requires the opener and an emma-word as the just-spoken last two tokens. The _EMMA_WORDS set tightened to emma, mom, ma, and member. I verified all six false-wake strings now return False and genuine "hey emma," "hey mom," and "hey ma" still fire.
The ack is a hard invariant. It is always audible before capture. The play_ack tone is the guaranteed floor even if TTS fails. A stealth capture is impossible.
Reminders with due times and on-wake brief
I built a reminders feature with due times and an on-start brief. Reminders is the fifth memory store, alongside decisions, corrections, preferences, and memory. Three SAFE router tools handle set_reminder, list_reminders_tool, and complete_reminder_tool.
Reminders are delivered by an on-start spoken brief in run_voice, shared by voice and wake modes. This is not background scheduling. Foreman speaks only while a mode runs. Overdue reminders reappear at each session start until completed. That persistence is the delivery mechanism.
The full reminder is spoken back on set, never terse-shortened. This is the misresolution catch, the same rule as gate confirmations. Ambiguity denies completion.
The router misresolves "Friday afternoon" to Saturday when the injected context says "Thursday, July 30, 2026." This is reproducible. The spoken readback surfaces it by design. Reminders with no time given resolve to today at 9am, landing in the next brief immediately if already past.
Beloit fix
I added "beloit" to KNOWN_NOUNS and the Whisper vocab prompt. Corrections map "bell oit," "below it," and "beloyt" to "beloit." I deliberately did not map "lloyd" to "beloit" because Lloyd is a real name.