Update audio_engine.py issue: 48

Behavioral changes:

Each wake word has its own timer.

Rapid repeats of the same phrase within the refractory window still get rejected (which is desirable).

A misfire of “Hey Jarvis” no longer locks out “Okay Nabu” immediately afterward.

If a user really wants to disable the gating, they can set:

"wake_word": {
  "refractory_seconds": 0
}


and this code will treat it as “no refractory” (max(..., 0.0) above).
This commit is contained in:
imonlinux
2025-12-06 19:32:29 -08:00
committed by GitHub
parent 186f33f1a1
commit 8d135dc51b
+20 -10
View File
@@ -5,7 +5,7 @@ import logging
import threading import threading
import time import time
import warnings import warnings
from typing import List, Optional, Union from typing import Dict, List, Optional, Union
import numpy as np import numpy as np
import soundcard as sc import soundcard as sc
@@ -56,7 +56,9 @@ class AudioEngine:
micro_inputs: List[np.ndarray] = [] micro_inputs: List[np.ndarray] = []
oww_inputs: List[np.ndarray] = [] oww_inputs: List[np.ndarray] = []
has_oww = False has_oww = False
last_active: Optional[float] = None
# Track last activation time per wake word (by id or phrase)
last_active_by_id: Dict[str, float] = {}
try: try:
while True: while True:
@@ -75,7 +77,7 @@ class AudioEngine:
oww_features.reset() oww_features.reset()
# Allow immediate activation (don't enforce refractory here) # Allow immediate activation (don't enforce refractory here)
last_active = None last_active_by_id.clear()
# Flush hardware buffer # Flush hardware buffer
try: try:
@@ -173,16 +175,24 @@ class AudioEngine:
wake_phrase = getattr( wake_phrase = getattr(
wake_word, "wake_word", "wake word" wake_word, "wake_word", "wake word"
) )
wake_id = getattr(wake_word, "id", None) wake_id_attr = getattr(wake_word, "id", None)
# Use id if available, otherwise phrase string as key
id_key = wake_id_attr or wake_phrase
_LOGGER.debug( _LOGGER.debug(
"Wake word ACTIVATED: %s (id=%s)", "Wake word ACTIVATED: %s (id=%s)",
wake_phrase, wake_phrase,
wake_id, wake_id_attr,
) )
now = time.monotonic() now = time.monotonic()
last_active = last_active_by_id.get(id_key)
# If refractory_seconds <= 0, disable gating
refractory = max(self.state.refractory_seconds, 0.0)
if (last_active is None) or ( if (last_active is None) or (
(now - last_active) (now - last_active) > refractory
> self.state.refractory_seconds
): ):
_LOGGER.debug( _LOGGER.debug(
"Wake word ACCEPTED (phrase=%s); " "Wake word ACCEPTED (phrase=%s); "
@@ -191,19 +201,19 @@ class AudioEngine:
0.0 0.0
if last_active is None if last_active is None
else now - last_active, else now - last_active,
self.state.refractory_seconds, refractory,
) )
last_active_by_id[id_key] = now
self.state.loop.call_soon_threadsafe( self.state.loop.call_soon_threadsafe(
self.state.satellite.wakeup, wake_word self.state.satellite.wakeup, wake_word
) )
last_active = now
else: else:
_LOGGER.debug( _LOGGER.debug(
"Wake word REJECTED by refractory period " "Wake word REJECTED by refractory period "
"(phrase=%s, Δt=%.2f < %.2f)", "(phrase=%s, Δt=%.2f < %.2f)",
wake_phrase, wake_phrase,
now - last_active, now - last_active,
self.state.refractory_seconds, refractory,
) )
# Stop word detection # Stop word detection