spych.wake

  1import threading, time
  2from faster_whisper import WhisperModel
  3from spych.utils import Notify, Recorder, get_clean_audio_buffer
  4
  5
  6class SpychWakeListener(Notify):
  7    def __init__(self, spych_wake_object):
  8        """
  9        Usage:
 10
 11        - Initializes a single wake word listener thread worker
 12
 13        Requires:
 14
 15        - `spych_wake_object`:
 16            - Type: SpychWake
 17            - What: The parent SpychWake instance that owns this listener
 18            - Note: Used to access shared state such as `locked`, `wake_word_map`,
 19              and `device_index`
 20        """
 21        self.spych_wake_object = spych_wake_object
 22        self.locked = False
 23        self.kill = False
 24
 25    def stop(self):
 26        """
 27        Usage:
 28
 29        - Signals this listener to stop at the next available checkpoint
 30        - Note: Does not immediately halt execution; the listener will exit cleanly
 31          after its current operation completes
 32        """
 33        self.kill = True
 34
 35    def should_stop(self):
 36        """
 37        Usage:
 38
 39        - Checks whether this listener should stop processing and exit early
 40        - Resets `kill` and `locked` state if stopping is required
 41
 42        Returns:
 43
 44        - `should_stop`:
 45            - Type: bool
 46            - What: True if the listener should stop, False if it should continue
 47            - Note: Returns True if `self.kill` is set or the parent `SpychWake` is locked
 48        """
 49        if self.kill or self.spych_wake_object.locked:
 50            self.kill = False
 51            self.locked = False
 52            return True
 53        return False
 54
 55    def __call__(self):
 56        """
 57        Usage:
 58
 59        - Executes one full listen-and-detect cycle when this listener is invoked as a thread target
 60        - Records audio, transcribes it, and triggers a wake event if any wake word is detected
 61
 62        Notes:
 63
 64        - Skips execution silently if this listener is already locked (i.e. mid-cycle)
 65        - Checks `should_stop` at each major step to allow early exit without blocking
 66        - Uses `beam_size=2` for fast transcription appropriate for short wake word clips
 67        - The `initial_prompt` biases the model toward all registered wake words to reduce
 68          false negatives
 69        - If multiple wake words are present in a single segment, the first match wins
 70        """
 71        if self.locked:
 72            self.notify(
 73                "Listener is locked, skipping...", notification_type="verbose"
 74            )
 75            return
 76        if self.should_stop():
 77            return
 78        self.locked = True
 79        buffer = self.spych_wake_object.recorder.record(
 80            device_index=self.spych_wake_object.device_index,
 81            duration=self.spych_wake_object.wake_listener_time,
 82        )
 83        if self.should_stop():
 84            return
 85        audio_buffer = get_clean_audio_buffer(buffer)
 86        if self.should_stop():
 87            return
 88        wake_words = list(self.spych_wake_object.wake_word_map.keys())
 89        wake_string = "[" + ", ".join(wake_words) + "]"
 90        segments, _ = self.spych_wake_object.wake_model.transcribe(
 91            audio_buffer,
 92            beam_size=2,
 93            initial_prompt=f"""Here are some wake words: {wake_string}. Only return what you understood was said, but place extra weight on those words if there is a tie.""",
 94        )
 95        for segment in segments:
 96            # Skip segments with high no_speech_prob to reduce false positives on silence/background noise;
 97            # the threshold can be adjusted based on testing and environment
 98            if (
 99                segment.no_speech_prob
100                > self.spych_wake_object.no_speech_threshold
101            ):
102                continue
103            if self.should_stop():
104                return
105            text = segment.text.lower()
106            for wake_word in wake_words:
107                if wake_word in text:
108                    self.spych_wake_object.wake(wake_word)
109                    self.locked = False
110                    self.kill = False
111                    return
112        self.locked = False
113        self.kill = False
114
115
116class SpychWake(Notify):
117    def __init__(
118        self,
119        wake_word_map,
120        terminate_words=None,
121        wake_listener_count=3,
122        wake_listener_time=2,
123        wake_listener_max_processing_time=0.5,
124        device_index=-1,
125        whisper_model="tiny.en",
126        whisper_device="cpu",
127        whisper_compute_type="int8",
128        no_speech_threshold=0.3,
129        on_terminate=None,
130    ):
131        """
132        Usage:
133
134        - Initializes a wake word detection system using overlapping listener threads
135          and faster-whisper for offline transcription
136        - Supports multiple wake words, each mapped to a different callback function
137
138        Requires:
139
140        - `wake_word_map`:
141            - Type: dict[str, callable]
142            - What: A dictionary mapping wake words to their corresponding no-argument
143              callback functions
144            - Note: Keys are stored and matched in lowercase
145            - Example:
146                {
147                    "jarvis": on_jarvis_wake,
148                    "computer": on_computer_wake,
149                }
150
151        Optional:
152
153        - `terminate_words`:
154            - Type: list[str]
155            - What: A list of words that, if detected in the wake listener's transcription,
156              will immediately terminate the entire SpychWake system
157            - Note: Use with caution, as any false positive on a terminate word will stop
158              the wake system until it is manually restarted
159            - default: None (disabled)
160
161        - `wake_listener_count`:
162            - Type: int
163            - What: The number of concurrent listener threads to run
164            - Default: 3
165            - Note: More listeners reduce the chance of missing a wake word between
166              recording windows; at least 3 is recommended for continuous coverage
167
168        - `wake_listener_time`:
169            - Type: int | float
170            - What: The duration in seconds each listener records per cycle
171            - Default: 2
172
173        - `wake_listener_max_processing_time`:
174            - Type: int | float
175            - What: The estimated maximum time in seconds for transcription to complete
176            - Default: 0.5
177            - Note: Used alongside `wake_listener_time` and `wake_listener_count` to
178              calculate the stagger delay between thread launches
179
180        - `device_index`:
181            - Type: int
182            - What: The microphone device index to record from
183            - Default: -1
184            - Note: Use `-1` to select the system default input device
185
186        - `whisper_model`:
187            - Type: str
188            - What: The faster-whisper model name to use for wake word transcription
189            - Default: "tiny.en"
190            - Note: Smaller models (tiny, base) are recommended here for low latency
191
192        - `whisper_device`:
193            - Type: str
194            - What: The device to run the whisper model on
195            - Default: "cpu"
196            - Note: Use "cuda" for GPU acceleration if available
197
198        - `whisper_compute_type`:
199            - Type: str
200            - What: The compute type to use for the whisper model
201            - Default: "int8"
202            - Note: "int8" offers a good balance of speed and accuracy on both CPU and GPU
203
204        - `no_speech_threshold`:
205            - Type: float
206            - What: The threshold for the `no_speech_prob` returned by faster-whisper
207            - Default: 0.3
208            - Note: Segments with a `no_speech_prob` above this threshold will be ignored to reduce false positives from silence or background noise
209
210        - `on_terminate`:
211            - Type: callable
212            - What: A no-argument callback function to execute when a terminate word is detected
213            - Default: None (disabled)
214            - Note: If provided, this callback will be executed before the system is stopped when a terminate word is detected
215        """
216        self.recorder = Recorder()
217        self.wake_word_map = {k.lower(): v for k, v in wake_word_map.items()}
218        # Handle Terminating Words
219        self.terminate_words = (
220            [w.lower() for w in terminate_words] if terminate_words else []
221        )
222        for word in self.terminate_words:
223            if word in self.wake_word_map:
224                raise ValueError(
225                    f"Terminate word '{word}' cannot also be a wake word."
226                )
227            self.wake_word_map[word] = self.stop
228        self.no_speech_threshold = no_speech_threshold
229        self.on_terminate = on_terminate
230        self.wake_listener_count = wake_listener_count
231        self.wake_listener_time = wake_listener_time
232        self.wake_listener_max_processing_time = (
233            wake_listener_max_processing_time
234        )
235        self.device_index = device_index
236        self.locked = False
237        self.kill = False
238        self.wake_model = WhisperModel(
239            whisper_model,
240            device=whisper_device,
241            compute_type=whisper_compute_type,
242        )
243        self.wake_listeners = [
244            SpychWakeListener(self) for _ in range(self.wake_listener_count)
245        ]
246
247    def start(self):
248        """
249        Usage:
250
251        - Starts the wake word detection loop using overlapping listener threads
252        - Blocks until a KeyboardInterrupt is received or `stop()` is called
253
254        Notes:
255
256        - Callbacks are defined in `wake_word_map` at init time rather than passed to `start`
257        - Listener threads are staggered by `(wake_listener_time + wake_listener_max_processing_time)
258          / wake_listener_count` seconds to ensure continuous audio coverage
259        - New threads are only launched when the system is not locked (i.e. not currently
260          processing a wake event)
261        """
262        try:
263            while True:
264                for listener in self.wake_listeners:
265                    if self.kill:
266                        self.kill = False
267                        return
268                    if not self.locked:
269                        threading.Thread(target=listener).start()
270                    time.sleep(
271                        (
272                            self.wake_listener_time
273                            + self.wake_listener_max_processing_time
274                        )
275                        / self.wake_listener_count
276                    )
277        except KeyboardInterrupt:
278            self.stop()
279
280    def stop_listeners(self):
281        """
282        Usage:
283
284        - Signals all listener threads to stop at their next available checkpoint
285        - Note: Does not block; listeners will exit cleanly after their current operation
286        """
287        for listener in self.wake_listeners:
288            listener.stop()
289
290    def stop(self):
291        """
292        Usage:
293
294        - Stops all listener threads and exits the `start` loop
295        - Note: Combines `stop_listeners` with setting the kill flag on the main loop
296        """
297        self.stop_listeners()
298        self.kill = True
299        if self.on_terminate:
300            try:
301                self.on_terminate()
302            except Exception as e:
303                self.notify(
304                    f"Error in on_terminate callback: {e}",
305                    notification_type="exception",
306                )
307
308    def wake(self, wake_word):
309        """
310        Usage:
311
312        - Called internally when a wake word is detected
313        - Stops all listeners, locks the system, executes the mapped callback for the
314          detected wake word, then unlocks
315
316        Requires:
317
318        - `wake_word`:
319            - Type: str
320            - What: The detected wake word, used to look up the correct callback in
321              `wake_word_map`
322
323        Notes:
324
325        - If the system is already locked when `wake` is called, the call is a no-op
326          to prevent concurrent wake executions
327        - Any exception raised by the callback is caught and re-raised as a spych exception
328        - The system is always unlocked in the `finally` block, even if the callback raises
329        """
330        self.stop_listeners()
331        if self.locked:
332            return
333        self.locked = True
334        try:
335            self.wake_word_map[wake_word]()
336        except Exception as e:
337            self.notify(
338                f"Error in on_wake_fn for '{wake_word}': {e}",
339                notification_type="exception",
340            )
341        finally:
342            self.locked = False
class SpychWakeListener(spych.utils.Notify):
  7class SpychWakeListener(Notify):
  8    def __init__(self, spych_wake_object):
  9        """
 10        Usage:
 11
 12        - Initializes a single wake word listener thread worker
 13
 14        Requires:
 15
 16        - `spych_wake_object`:
 17            - Type: SpychWake
 18            - What: The parent SpychWake instance that owns this listener
 19            - Note: Used to access shared state such as `locked`, `wake_word_map`,
 20              and `device_index`
 21        """
 22        self.spych_wake_object = spych_wake_object
 23        self.locked = False
 24        self.kill = False
 25
 26    def stop(self):
 27        """
 28        Usage:
 29
 30        - Signals this listener to stop at the next available checkpoint
 31        - Note: Does not immediately halt execution; the listener will exit cleanly
 32          after its current operation completes
 33        """
 34        self.kill = True
 35
 36    def should_stop(self):
 37        """
 38        Usage:
 39
 40        - Checks whether this listener should stop processing and exit early
 41        - Resets `kill` and `locked` state if stopping is required
 42
 43        Returns:
 44
 45        - `should_stop`:
 46            - Type: bool
 47            - What: True if the listener should stop, False if it should continue
 48            - Note: Returns True if `self.kill` is set or the parent `SpychWake` is locked
 49        """
 50        if self.kill or self.spych_wake_object.locked:
 51            self.kill = False
 52            self.locked = False
 53            return True
 54        return False
 55
 56    def __call__(self):
 57        """
 58        Usage:
 59
 60        - Executes one full listen-and-detect cycle when this listener is invoked as a thread target
 61        - Records audio, transcribes it, and triggers a wake event if any wake word is detected
 62
 63        Notes:
 64
 65        - Skips execution silently if this listener is already locked (i.e. mid-cycle)
 66        - Checks `should_stop` at each major step to allow early exit without blocking
 67        - Uses `beam_size=2` for fast transcription appropriate for short wake word clips
 68        - The `initial_prompt` biases the model toward all registered wake words to reduce
 69          false negatives
 70        - If multiple wake words are present in a single segment, the first match wins
 71        """
 72        if self.locked:
 73            self.notify(
 74                "Listener is locked, skipping...", notification_type="verbose"
 75            )
 76            return
 77        if self.should_stop():
 78            return
 79        self.locked = True
 80        buffer = self.spych_wake_object.recorder.record(
 81            device_index=self.spych_wake_object.device_index,
 82            duration=self.spych_wake_object.wake_listener_time,
 83        )
 84        if self.should_stop():
 85            return
 86        audio_buffer = get_clean_audio_buffer(buffer)
 87        if self.should_stop():
 88            return
 89        wake_words = list(self.spych_wake_object.wake_word_map.keys())
 90        wake_string = "[" + ", ".join(wake_words) + "]"
 91        segments, _ = self.spych_wake_object.wake_model.transcribe(
 92            audio_buffer,
 93            beam_size=2,
 94            initial_prompt=f"""Here are some wake words: {wake_string}. Only return what you understood was said, but place extra weight on those words if there is a tie.""",
 95        )
 96        for segment in segments:
 97            # Skip segments with high no_speech_prob to reduce false positives on silence/background noise;
 98            # the threshold can be adjusted based on testing and environment
 99            if (
100                segment.no_speech_prob
101                > self.spych_wake_object.no_speech_threshold
102            ):
103                continue
104            if self.should_stop():
105                return
106            text = segment.text.lower()
107            for wake_word in wake_words:
108                if wake_word in text:
109                    self.spych_wake_object.wake(wake_word)
110                    self.locked = False
111                    self.kill = False
112                    return
113        self.locked = False
114        self.kill = False
SpychWakeListener(spych_wake_object)
 8    def __init__(self, spych_wake_object):
 9        """
10        Usage:
11
12        - Initializes a single wake word listener thread worker
13
14        Requires:
15
16        - `spych_wake_object`:
17            - Type: SpychWake
18            - What: The parent SpychWake instance that owns this listener
19            - Note: Used to access shared state such as `locked`, `wake_word_map`,
20              and `device_index`
21        """
22        self.spych_wake_object = spych_wake_object
23        self.locked = False
24        self.kill = False

Usage:

  • Initializes a single wake word listener thread worker

Requires:

  • spych_wake_object:
    • Type: SpychWake
    • What: The parent SpychWake instance that owns this listener
    • Note: Used to access shared state such as locked, wake_word_map, and device_index
spych_wake_object
locked
kill
def stop(self):
26    def stop(self):
27        """
28        Usage:
29
30        - Signals this listener to stop at the next available checkpoint
31        - Note: Does not immediately halt execution; the listener will exit cleanly
32          after its current operation completes
33        """
34        self.kill = True

Usage:

  • Signals this listener to stop at the next available checkpoint
  • Note: Does not immediately halt execution; the listener will exit cleanly after its current operation completes
def should_stop(self):
36    def should_stop(self):
37        """
38        Usage:
39
40        - Checks whether this listener should stop processing and exit early
41        - Resets `kill` and `locked` state if stopping is required
42
43        Returns:
44
45        - `should_stop`:
46            - Type: bool
47            - What: True if the listener should stop, False if it should continue
48            - Note: Returns True if `self.kill` is set or the parent `SpychWake` is locked
49        """
50        if self.kill or self.spych_wake_object.locked:
51            self.kill = False
52            self.locked = False
53            return True
54        return False

Usage:

  • Checks whether this listener should stop processing and exit early
  • Resets kill and locked state if stopping is required

Returns:

  • should_stop:
    • Type: bool
    • What: True if the listener should stop, False if it should continue
    • Note: Returns True if self.kill is set or the parent SpychWake is locked
Inherited Members
spych.utils.Notify
notify
class SpychWake(spych.utils.Notify):
117class SpychWake(Notify):
118    def __init__(
119        self,
120        wake_word_map,
121        terminate_words=None,
122        wake_listener_count=3,
123        wake_listener_time=2,
124        wake_listener_max_processing_time=0.5,
125        device_index=-1,
126        whisper_model="tiny.en",
127        whisper_device="cpu",
128        whisper_compute_type="int8",
129        no_speech_threshold=0.3,
130        on_terminate=None,
131    ):
132        """
133        Usage:
134
135        - Initializes a wake word detection system using overlapping listener threads
136          and faster-whisper for offline transcription
137        - Supports multiple wake words, each mapped to a different callback function
138
139        Requires:
140
141        - `wake_word_map`:
142            - Type: dict[str, callable]
143            - What: A dictionary mapping wake words to their corresponding no-argument
144              callback functions
145            - Note: Keys are stored and matched in lowercase
146            - Example:
147                {
148                    "jarvis": on_jarvis_wake,
149                    "computer": on_computer_wake,
150                }
151
152        Optional:
153
154        - `terminate_words`:
155            - Type: list[str]
156            - What: A list of words that, if detected in the wake listener's transcription,
157              will immediately terminate the entire SpychWake system
158            - Note: Use with caution, as any false positive on a terminate word will stop
159              the wake system until it is manually restarted
160            - default: None (disabled)
161
162        - `wake_listener_count`:
163            - Type: int
164            - What: The number of concurrent listener threads to run
165            - Default: 3
166            - Note: More listeners reduce the chance of missing a wake word between
167              recording windows; at least 3 is recommended for continuous coverage
168
169        - `wake_listener_time`:
170            - Type: int | float
171            - What: The duration in seconds each listener records per cycle
172            - Default: 2
173
174        - `wake_listener_max_processing_time`:
175            - Type: int | float
176            - What: The estimated maximum time in seconds for transcription to complete
177            - Default: 0.5
178            - Note: Used alongside `wake_listener_time` and `wake_listener_count` to
179              calculate the stagger delay between thread launches
180
181        - `device_index`:
182            - Type: int
183            - What: The microphone device index to record from
184            - Default: -1
185            - Note: Use `-1` to select the system default input device
186
187        - `whisper_model`:
188            - Type: str
189            - What: The faster-whisper model name to use for wake word transcription
190            - Default: "tiny.en"
191            - Note: Smaller models (tiny, base) are recommended here for low latency
192
193        - `whisper_device`:
194            - Type: str
195            - What: The device to run the whisper model on
196            - Default: "cpu"
197            - Note: Use "cuda" for GPU acceleration if available
198
199        - `whisper_compute_type`:
200            - Type: str
201            - What: The compute type to use for the whisper model
202            - Default: "int8"
203            - Note: "int8" offers a good balance of speed and accuracy on both CPU and GPU
204
205        - `no_speech_threshold`:
206            - Type: float
207            - What: The threshold for the `no_speech_prob` returned by faster-whisper
208            - Default: 0.3
209            - Note: Segments with a `no_speech_prob` above this threshold will be ignored to reduce false positives from silence or background noise
210
211        - `on_terminate`:
212            - Type: callable
213            - What: A no-argument callback function to execute when a terminate word is detected
214            - Default: None (disabled)
215            - Note: If provided, this callback will be executed before the system is stopped when a terminate word is detected
216        """
217        self.recorder = Recorder()
218        self.wake_word_map = {k.lower(): v for k, v in wake_word_map.items()}
219        # Handle Terminating Words
220        self.terminate_words = (
221            [w.lower() for w in terminate_words] if terminate_words else []
222        )
223        for word in self.terminate_words:
224            if word in self.wake_word_map:
225                raise ValueError(
226                    f"Terminate word '{word}' cannot also be a wake word."
227                )
228            self.wake_word_map[word] = self.stop
229        self.no_speech_threshold = no_speech_threshold
230        self.on_terminate = on_terminate
231        self.wake_listener_count = wake_listener_count
232        self.wake_listener_time = wake_listener_time
233        self.wake_listener_max_processing_time = (
234            wake_listener_max_processing_time
235        )
236        self.device_index = device_index
237        self.locked = False
238        self.kill = False
239        self.wake_model = WhisperModel(
240            whisper_model,
241            device=whisper_device,
242            compute_type=whisper_compute_type,
243        )
244        self.wake_listeners = [
245            SpychWakeListener(self) for _ in range(self.wake_listener_count)
246        ]
247
248    def start(self):
249        """
250        Usage:
251
252        - Starts the wake word detection loop using overlapping listener threads
253        - Blocks until a KeyboardInterrupt is received or `stop()` is called
254
255        Notes:
256
257        - Callbacks are defined in `wake_word_map` at init time rather than passed to `start`
258        - Listener threads are staggered by `(wake_listener_time + wake_listener_max_processing_time)
259          / wake_listener_count` seconds to ensure continuous audio coverage
260        - New threads are only launched when the system is not locked (i.e. not currently
261          processing a wake event)
262        """
263        try:
264            while True:
265                for listener in self.wake_listeners:
266                    if self.kill:
267                        self.kill = False
268                        return
269                    if not self.locked:
270                        threading.Thread(target=listener).start()
271                    time.sleep(
272                        (
273                            self.wake_listener_time
274                            + self.wake_listener_max_processing_time
275                        )
276                        / self.wake_listener_count
277                    )
278        except KeyboardInterrupt:
279            self.stop()
280
281    def stop_listeners(self):
282        """
283        Usage:
284
285        - Signals all listener threads to stop at their next available checkpoint
286        - Note: Does not block; listeners will exit cleanly after their current operation
287        """
288        for listener in self.wake_listeners:
289            listener.stop()
290
291    def stop(self):
292        """
293        Usage:
294
295        - Stops all listener threads and exits the `start` loop
296        - Note: Combines `stop_listeners` with setting the kill flag on the main loop
297        """
298        self.stop_listeners()
299        self.kill = True
300        if self.on_terminate:
301            try:
302                self.on_terminate()
303            except Exception as e:
304                self.notify(
305                    f"Error in on_terminate callback: {e}",
306                    notification_type="exception",
307                )
308
309    def wake(self, wake_word):
310        """
311        Usage:
312
313        - Called internally when a wake word is detected
314        - Stops all listeners, locks the system, executes the mapped callback for the
315          detected wake word, then unlocks
316
317        Requires:
318
319        - `wake_word`:
320            - Type: str
321            - What: The detected wake word, used to look up the correct callback in
322              `wake_word_map`
323
324        Notes:
325
326        - If the system is already locked when `wake` is called, the call is a no-op
327          to prevent concurrent wake executions
328        - Any exception raised by the callback is caught and re-raised as a spych exception
329        - The system is always unlocked in the `finally` block, even if the callback raises
330        """
331        self.stop_listeners()
332        if self.locked:
333            return
334        self.locked = True
335        try:
336            self.wake_word_map[wake_word]()
337        except Exception as e:
338            self.notify(
339                f"Error in on_wake_fn for '{wake_word}': {e}",
340                notification_type="exception",
341            )
342        finally:
343            self.locked = False
SpychWake( wake_word_map, terminate_words=None, wake_listener_count=3, wake_listener_time=2, wake_listener_max_processing_time=0.5, device_index=-1, whisper_model='tiny.en', whisper_device='cpu', whisper_compute_type='int8', no_speech_threshold=0.3, on_terminate=None)
118    def __init__(
119        self,
120        wake_word_map,
121        terminate_words=None,
122        wake_listener_count=3,
123        wake_listener_time=2,
124        wake_listener_max_processing_time=0.5,
125        device_index=-1,
126        whisper_model="tiny.en",
127        whisper_device="cpu",
128        whisper_compute_type="int8",
129        no_speech_threshold=0.3,
130        on_terminate=None,
131    ):
132        """
133        Usage:
134
135        - Initializes a wake word detection system using overlapping listener threads
136          and faster-whisper for offline transcription
137        - Supports multiple wake words, each mapped to a different callback function
138
139        Requires:
140
141        - `wake_word_map`:
142            - Type: dict[str, callable]
143            - What: A dictionary mapping wake words to their corresponding no-argument
144              callback functions
145            - Note: Keys are stored and matched in lowercase
146            - Example:
147                {
148                    "jarvis": on_jarvis_wake,
149                    "computer": on_computer_wake,
150                }
151
152        Optional:
153
154        - `terminate_words`:
155            - Type: list[str]
156            - What: A list of words that, if detected in the wake listener's transcription,
157              will immediately terminate the entire SpychWake system
158            - Note: Use with caution, as any false positive on a terminate word will stop
159              the wake system until it is manually restarted
160            - default: None (disabled)
161
162        - `wake_listener_count`:
163            - Type: int
164            - What: The number of concurrent listener threads to run
165            - Default: 3
166            - Note: More listeners reduce the chance of missing a wake word between
167              recording windows; at least 3 is recommended for continuous coverage
168
169        - `wake_listener_time`:
170            - Type: int | float
171            - What: The duration in seconds each listener records per cycle
172            - Default: 2
173
174        - `wake_listener_max_processing_time`:
175            - Type: int | float
176            - What: The estimated maximum time in seconds for transcription to complete
177            - Default: 0.5
178            - Note: Used alongside `wake_listener_time` and `wake_listener_count` to
179              calculate the stagger delay between thread launches
180
181        - `device_index`:
182            - Type: int
183            - What: The microphone device index to record from
184            - Default: -1
185            - Note: Use `-1` to select the system default input device
186
187        - `whisper_model`:
188            - Type: str
189            - What: The faster-whisper model name to use for wake word transcription
190            - Default: "tiny.en"
191            - Note: Smaller models (tiny, base) are recommended here for low latency
192
193        - `whisper_device`:
194            - Type: str
195            - What: The device to run the whisper model on
196            - Default: "cpu"
197            - Note: Use "cuda" for GPU acceleration if available
198
199        - `whisper_compute_type`:
200            - Type: str
201            - What: The compute type to use for the whisper model
202            - Default: "int8"
203            - Note: "int8" offers a good balance of speed and accuracy on both CPU and GPU
204
205        - `no_speech_threshold`:
206            - Type: float
207            - What: The threshold for the `no_speech_prob` returned by faster-whisper
208            - Default: 0.3
209            - Note: Segments with a `no_speech_prob` above this threshold will be ignored to reduce false positives from silence or background noise
210
211        - `on_terminate`:
212            - Type: callable
213            - What: A no-argument callback function to execute when a terminate word is detected
214            - Default: None (disabled)
215            - Note: If provided, this callback will be executed before the system is stopped when a terminate word is detected
216        """
217        self.recorder = Recorder()
218        self.wake_word_map = {k.lower(): v for k, v in wake_word_map.items()}
219        # Handle Terminating Words
220        self.terminate_words = (
221            [w.lower() for w in terminate_words] if terminate_words else []
222        )
223        for word in self.terminate_words:
224            if word in self.wake_word_map:
225                raise ValueError(
226                    f"Terminate word '{word}' cannot also be a wake word."
227                )
228            self.wake_word_map[word] = self.stop
229        self.no_speech_threshold = no_speech_threshold
230        self.on_terminate = on_terminate
231        self.wake_listener_count = wake_listener_count
232        self.wake_listener_time = wake_listener_time
233        self.wake_listener_max_processing_time = (
234            wake_listener_max_processing_time
235        )
236        self.device_index = device_index
237        self.locked = False
238        self.kill = False
239        self.wake_model = WhisperModel(
240            whisper_model,
241            device=whisper_device,
242            compute_type=whisper_compute_type,
243        )
244        self.wake_listeners = [
245            SpychWakeListener(self) for _ in range(self.wake_listener_count)
246        ]

Usage:

  • Initializes a wake word detection system using overlapping listener threads and faster-whisper for offline transcription
  • Supports multiple wake words, each mapped to a different callback function

Requires:

  • wake_word_map:
    • Type: dict[str, callable]
    • What: A dictionary mapping wake words to their corresponding no-argument callback functions
    • Note: Keys are stored and matched in lowercase
    • Example: { "jarvis": on_jarvis_wake, "computer": on_computer_wake, }

Optional:

  • terminate_words:

    • Type: list[str]
    • What: A list of words that, if detected in the wake listener's transcription, will immediately terminate the entire SpychWake system
    • Note: Use with caution, as any false positive on a terminate word will stop the wake system until it is manually restarted
    • default: None (disabled)
  • wake_listener_count:

    • Type: int
    • What: The number of concurrent listener threads to run
    • Default: 3
    • Note: More listeners reduce the chance of missing a wake word between recording windows; at least 3 is recommended for continuous coverage
  • wake_listener_time:

    • Type: int | float
    • What: The duration in seconds each listener records per cycle
    • Default: 2
  • wake_listener_max_processing_time:

    • Type: int | float
    • What: The estimated maximum time in seconds for transcription to complete
    • Default: 0.5
    • Note: Used alongside wake_listener_time and wake_listener_count to calculate the stagger delay between thread launches
  • device_index:

    • Type: int
    • What: The microphone device index to record from
    • Default: -1
    • Note: Use -1 to select the system default input device
  • whisper_model:

    • Type: str
    • What: The faster-whisper model name to use for wake word transcription
    • Default: "tiny.en"
    • Note: Smaller models (tiny, base) are recommended here for low latency
  • whisper_device:

    • Type: str
    • What: The device to run the whisper model on
    • Default: "cpu"
    • Note: Use "cuda" for GPU acceleration if available
  • whisper_compute_type:

    • Type: str
    • What: The compute type to use for the whisper model
    • Default: "int8"
    • Note: "int8" offers a good balance of speed and accuracy on both CPU and GPU
  • no_speech_threshold:

    • Type: float
    • What: The threshold for the no_speech_prob returned by faster-whisper
    • Default: 0.3
    • Note: Segments with a no_speech_prob above this threshold will be ignored to reduce false positives from silence or background noise
  • on_terminate:

    • Type: callable
    • What: A no-argument callback function to execute when a terminate word is detected
    • Default: None (disabled)
    • Note: If provided, this callback will be executed before the system is stopped when a terminate word is detected
recorder
wake_word_map
terminate_words
no_speech_threshold
on_terminate
wake_listener_count
wake_listener_time
wake_listener_max_processing_time
device_index
locked
kill
wake_model
wake_listeners
def start(self):
248    def start(self):
249        """
250        Usage:
251
252        - Starts the wake word detection loop using overlapping listener threads
253        - Blocks until a KeyboardInterrupt is received or `stop()` is called
254
255        Notes:
256
257        - Callbacks are defined in `wake_word_map` at init time rather than passed to `start`
258        - Listener threads are staggered by `(wake_listener_time + wake_listener_max_processing_time)
259          / wake_listener_count` seconds to ensure continuous audio coverage
260        - New threads are only launched when the system is not locked (i.e. not currently
261          processing a wake event)
262        """
263        try:
264            while True:
265                for listener in self.wake_listeners:
266                    if self.kill:
267                        self.kill = False
268                        return
269                    if not self.locked:
270                        threading.Thread(target=listener).start()
271                    time.sleep(
272                        (
273                            self.wake_listener_time
274                            + self.wake_listener_max_processing_time
275                        )
276                        / self.wake_listener_count
277                    )
278        except KeyboardInterrupt:
279            self.stop()

Usage:

  • Starts the wake word detection loop using overlapping listener threads
  • Blocks until a KeyboardInterrupt is received or stop() is called

Notes:

  • Callbacks are defined in wake_word_map at init time rather than passed to start
  • Listener threads are staggered by (wake_listener_time + wake_listener_max_processing_time) / wake_listener_count seconds to ensure continuous audio coverage
  • New threads are only launched when the system is not locked (i.e. not currently processing a wake event)
def stop_listeners(self):
281    def stop_listeners(self):
282        """
283        Usage:
284
285        - Signals all listener threads to stop at their next available checkpoint
286        - Note: Does not block; listeners will exit cleanly after their current operation
287        """
288        for listener in self.wake_listeners:
289            listener.stop()

Usage:

  • Signals all listener threads to stop at their next available checkpoint
  • Note: Does not block; listeners will exit cleanly after their current operation
def stop(self):
291    def stop(self):
292        """
293        Usage:
294
295        - Stops all listener threads and exits the `start` loop
296        - Note: Combines `stop_listeners` with setting the kill flag on the main loop
297        """
298        self.stop_listeners()
299        self.kill = True
300        if self.on_terminate:
301            try:
302                self.on_terminate()
303            except Exception as e:
304                self.notify(
305                    f"Error in on_terminate callback: {e}",
306                    notification_type="exception",
307                )

Usage:

  • Stops all listener threads and exits the start loop
  • Note: Combines stop_listeners with setting the kill flag on the main loop
def wake(self, wake_word):
309    def wake(self, wake_word):
310        """
311        Usage:
312
313        - Called internally when a wake word is detected
314        - Stops all listeners, locks the system, executes the mapped callback for the
315          detected wake word, then unlocks
316
317        Requires:
318
319        - `wake_word`:
320            - Type: str
321            - What: The detected wake word, used to look up the correct callback in
322              `wake_word_map`
323
324        Notes:
325
326        - If the system is already locked when `wake` is called, the call is a no-op
327          to prevent concurrent wake executions
328        - Any exception raised by the callback is caught and re-raised as a spych exception
329        - The system is always unlocked in the `finally` block, even if the callback raises
330        """
331        self.stop_listeners()
332        if self.locked:
333            return
334        self.locked = True
335        try:
336            self.wake_word_map[wake_word]()
337        except Exception as e:
338            self.notify(
339                f"Error in on_wake_fn for '{wake_word}': {e}",
340                notification_type="exception",
341            )
342        finally:
343            self.locked = False

Usage:

  • Called internally when a wake word is detected
  • Stops all listeners, locks the system, executes the mapped callback for the detected wake word, then unlocks

Requires:

  • wake_word:
    • Type: str
    • What: The detected wake word, used to look up the correct callback in wake_word_map

Notes:

  • If the system is already locked when wake is called, the call is a no-op to prevent concurrent wake executions
  • Any exception raised by the callback is caught and re-raised as a spych exception
  • The system is always unlocked in the finally block, even if the callback raises
Inherited Members
spych.utils.Notify
notify