TinyML Arduino Speech Decisions: Separate a High Score from a New Command Event
A speech model repeatedly favors “yes,” but the application emits no new command. Lowering the classifier threshold is premature: the archived Arduino example has a separate stateful decoder between model output and application response.
This article examines that historical RecognizeCommands implementation. The TensorFlow Arduino examples repository is archived, so its behavior is a specific maintenance reference rather than a current product recommendation.
A score, an averaged label and an event are different outputs
The sketch sends each inference output and an audio timestamp to ProcessLatestResults. It then forwards the decoder's label, score and is_new_command flag to the responder.
Do not trigger an action merely because the returned label equals “yes.” The label can remain the strongest category without a new event. Likewise, printing only the raw model output hides the history and suppression decisions that actually gate the response.
The Arduino responder changes its command indication when is_new_command is true, while its activity LED toggles on each call. An activity indication therefore does not establish a newly recognized command.
Check whether the averaging window is mature
The decoder header defaults to a 1000-millisecond averaging window, threshold 200, suppression 1500 milliseconds and minimum count three. These are constructor choices, not universal speech-recognition settings.
The implementation also requires the retained results to span at least one quarter of the averaging window. Three scores delivered at nearly the same time do not satisfy that duration condition. During this immature period, it returns score zero and no new event.
Once mature, it averages the retained int8 category values after adding 128 to each. The threshold comparison uses that integer score, not an independently calibrated probability. Replacing the model's output quantization contract without adapting this decoder can silently change threshold meaning.

Suppression is not a universal cooldown
The event condition requires the averaged top score to be strictly greater than the threshold. For an ordinary previous non-silence event, it additionally requires either a different top label or elapsed time strictly greater than the suppression interval. A changed label can therefore trigger before the same-label interval expires.
There is a specific exception: when the previous label is silence, or the previous time still has its initial sentinel value, the implementation substitutes INT32_MAX for elapsed time. With the default suppression setting, that time branch passes immediately. Mature history and the score threshold still apply; this is not an unconditional command event.
For a controlled test with mature averaging, suppose “yes” triggers at 1000 milliseconds. Another qualifying “yes” at 1300 milliseconds is suppressed. A qualifying change to “no” at that time can trigger. This is a proposed test sequence, not recorded device behavior. Equality at the threshold or same-label time boundary should be tested separately because the comparisons are strict.
If a product needs a global cooldown across all commands, implement that requirement explicitly at the event boundary. Do not assume the parameter name already provides it.
Diagnose arrival rate and history before changing thresholds
The header's result queue stores at most 50 entries. A full queue logs a failed push and returns without inserting the new entry; that helper has no error return for the decoder to propagate. Increasing inference frequency can therefore change which scores enter the average, especially with a long averaging window.
Instrument queue count, oldest retained time, incoming time, averaged top score and event flag. Keep timestamps increasing in the caller. The implementation's backward-time check compares against the oldest retained result, so it should not be treated as a complete check against every out-of-order arrival.
Build a decoder-only test using synthetic score sequences before involving a microphone: immature history, sustained same label, changed label, threshold equality and queue saturation each exercise a different branch. Keep this test separate from classifier accuracy. Once event behavior matches the product requirement, reconnect the real model and evaluate whether its scores produce the desired user-visible decisions.