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
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, anddevice_index
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
killandlockedstate 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.killis set or the parentSpychWakeis locked
Inherited Members
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:
-
- 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)
-
- 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
-
- 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_timeandwake_listener_countto calculate the stagger delay between thread launches
-
- Type: int
- What: The microphone device index to record from
- Default: -1
- Note: Use
-1to 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
-
- Type: float
- What: The threshold for the
no_speech_probreturned by faster-whisper - Default: 0.3
- Note: Segments with a
no_speech_probabove this threshold will be ignored to reduce false positives from silence or background noise
-
- 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
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_mapat init time rather than passed tostart - Listener threads are staggered by
(wake_listener_time + wake_listener_max_processing_time) / wake_listener_countseconds 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
startloop - Note: Combines
stop_listenerswith 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
wakeis 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
finallyblock, even if the callback raises