@staff0rd/assist 0.87.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,9 @@ dependencies = [
13
13
 
14
14
  [project.optional-dependencies]
15
15
  dev = [
16
+ "radon>=6.0",
16
17
  "ruff>=0.8",
18
+ "xenon>=0.9",
17
19
  ]
18
20
 
19
21
  [[tool.uv.index]]
@@ -252,7 +252,9 @@ dependencies = [
252
252
 
253
253
  [package.optional-dependencies]
254
254
  dev = [
255
+ { name = "radon" },
255
256
  { name = "ruff" },
257
+ { name = "xenon" },
256
258
  ]
257
259
 
258
260
  [package.metadata]
@@ -260,10 +262,12 @@ requires-dist = [
260
262
  { name = "nemo-toolkit", extras = ["asr"], specifier = ">=1.22" },
261
263
  { name = "numpy", specifier = ">=1.24" },
262
264
  { name = "onnxruntime", specifier = ">=1.17" },
265
+ { name = "radon", marker = "extra == 'dev'", specifier = ">=6.0" },
263
266
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" },
264
267
  { name = "silero-vad", specifier = ">=5.1" },
265
268
  { name = "sounddevice", specifier = ">=0.4" },
266
269
  { name = "torch", specifier = ">=2.0", index = "https://download.pytorch.org/whl/cu124" },
270
+ { name = "xenon", marker = "extra == 'dev'", specifier = ">=0.9" },
267
271
  ]
268
272
  provides-extras = ["dev"]
269
273
 
@@ -2146,6 +2150,18 @@ wheels = [
2146
2150
  { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
2147
2151
  ]
2148
2152
 
2153
+ [[package]]
2154
+ name = "mando"
2155
+ version = "0.7.1"
2156
+ source = { registry = "https://pypi.org/simple" }
2157
+ dependencies = [
2158
+ { name = "six" },
2159
+ ]
2160
+ sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" }
2161
+ wheels = [
2162
+ { url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" },
2163
+ ]
2164
+
2149
2165
  [[package]]
2150
2166
  name = "markdown"
2151
2167
  version = "3.10.2"
@@ -3120,7 +3136,7 @@ name = "nvidia-cudnn-cu12"
3120
3136
  version = "9.1.0.70"
3121
3137
  source = { registry = "https://pypi.org/simple" }
3122
3138
  dependencies = [
3123
- { name = "nvidia-cublas-cu12" },
3139
+ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" },
3124
3140
  ]
3125
3141
  wheels = [
3126
3142
  { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" },
@@ -3131,7 +3147,7 @@ name = "nvidia-cufft-cu12"
3131
3147
  version = "11.2.1.3"
3132
3148
  source = { registry = "https://pypi.org/simple" }
3133
3149
  dependencies = [
3134
- { name = "nvidia-nvjitlink-cu12" },
3150
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" },
3135
3151
  ]
3136
3152
  wheels = [
3137
3153
  { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" },
@@ -3150,9 +3166,9 @@ name = "nvidia-cusolver-cu12"
3150
3166
  version = "11.6.1.9"
3151
3167
  source = { registry = "https://pypi.org/simple" }
3152
3168
  dependencies = [
3153
- { name = "nvidia-cublas-cu12" },
3154
- { name = "nvidia-cusparse-cu12" },
3155
- { name = "nvidia-nvjitlink-cu12" },
3169
+ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" },
3170
+ { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" },
3171
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" },
3156
3172
  ]
3157
3173
  wheels = [
3158
3174
  { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" },
@@ -3163,7 +3179,7 @@ name = "nvidia-cusparse-cu12"
3163
3179
  version = "12.3.1.170"
3164
3180
  source = { registry = "https://pypi.org/simple" }
3165
3181
  dependencies = [
3166
- { name = "nvidia-nvjitlink-cu12" },
3182
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" },
3167
3183
  ]
3168
3184
  wheels = [
3169
3185
  { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" },
@@ -4241,6 +4257,19 @@ wheels = [
4241
4257
  { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" },
4242
4258
  ]
4243
4259
 
4260
+ [[package]]
4261
+ name = "radon"
4262
+ version = "6.0.1"
4263
+ source = { registry = "https://pypi.org/simple" }
4264
+ dependencies = [
4265
+ { name = "colorama" },
4266
+ { name = "mando" },
4267
+ ]
4268
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" }
4269
+ wheels = [
4270
+ { url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" },
4271
+ ]
4272
+
4244
4273
  [[package]]
4245
4274
  name = "rapidfuzz"
4246
4275
  version = "3.14.3"
@@ -5652,6 +5681,20 @@ wheels = [
5652
5681
  { url = "https://files.pythonhosted.org/packages/c4/da/5a086bf4c22a41995312db104ec2ffeee2cf6accca9faaee5315c790377d/wrapt-2.1.1-py3-none-any.whl", hash = "sha256:3b0f4629eb954394a3d7c7a1c8cca25f0b07cefe6aa8545e862e9778152de5b7", size = 43886, upload-time = "2026-02-03T02:11:45.048Z" },
5653
5682
  ]
5654
5683
 
5684
+ [[package]]
5685
+ name = "xenon"
5686
+ version = "0.9.3"
5687
+ source = { registry = "https://pypi.org/simple" }
5688
+ dependencies = [
5689
+ { name = "pyyaml" },
5690
+ { name = "radon" },
5691
+ { name = "requests" },
5692
+ ]
5693
+ sdist = { url = "https://files.pythonhosted.org/packages/c4/7c/2b341eaeec69d514b635ea18481885a956d196a74322a4b0942ef0c31691/xenon-0.9.3.tar.gz", hash = "sha256:4a7538d8ba08aa5d79055fb3e0b2393c0bd6d7d16a4ab0fcdef02ef1f10a43fa", size = 9883, upload-time = "2024-10-21T10:27:53.722Z" }
5694
+ wheels = [
5695
+ { url = "https://files.pythonhosted.org/packages/6f/5d/29ff8665b129cafd147d90b86e92babee32e116e3c84447107da3e77f8fb/xenon-0.9.3-py2.py3-none-any.whl", hash = "sha256:6e2c2c251cc5e9d01fe984e623499b13b2140fcbf74d6c03a613fa43a9347097", size = 8966, upload-time = "2024-10-21T10:27:51.121Z" },
5696
+ ]
5697
+
5655
5698
  [[package]]
5656
5699
  name = "xxhash"
5657
5700
  version = "3.6.0"
@@ -335,70 +335,80 @@ class VoiceDaemon:
335
335
  text = self._stt.transcribe(audio)
336
336
 
337
337
  if self._state == ACTIVATED:
338
- # Activated mode — full text is the command
339
- command = text.strip()
340
- if command:
341
- if command != self._typed_text:
342
- if self._typed_text:
343
- self._update_typed_text(command)
344
- else:
345
- keyboard.type_text(command)
346
- self._dispatch_result(command)
347
- else:
348
- if self._typed_text:
349
- keyboard.backspace(len(self._typed_text))
350
- log("dispatch_cancelled", "Empty command in activated mode")
338
+ self._finalize_activated_command(text)
351
339
  self._reset_listening()
352
340
  return
353
341
 
354
342
  if self._wake_detected:
355
- # Correct final text and submit
356
- found, command = check_wake_word(text)
357
- if found and command:
358
- if command != self._typed_text:
359
- self._update_typed_text(command)
360
- self._dispatch_result(command)
361
- elif found:
362
- # Wake word found but no command text after it
363
- if self._typed_text:
364
- keyboard.backspace(len(self._typed_text))
365
- log("dispatch_cancelled", "No command after wake word")
366
- elif self._typed_text:
367
- # Final transcription lost the wake word (e.g. audio clipping
368
- # turned "computer" into "uter"); fall back to the command
369
- # captured during streaming
370
- self._dispatch_result(self._typed_text)
371
- else:
372
- # Check final transcription for wake word
373
- found, command = check_wake_word(text)
374
- if found and command:
375
- log("wake_word_detected", command)
376
- if DEBUG:
377
- print(f" Wake word! Final: {command}", file=sys.stderr)
378
- keyboard.type_text(command)
379
- self._dispatch_result(command)
380
- if found and not command:
381
- # Wake word only — enter ACTIVATED state for next utterance
382
- log("wake_word_only", "Listening for command...")
383
- if DEBUG:
384
- print(
385
- " Wake word heard — listening for command...", file=sys.stderr
386
- )
387
- self._audio_buffer.clear()
388
- self._vad.reset()
389
- self._wake_detected = False
390
- self._typed_text = ""
391
- self._last_partial_at = 0
392
- self._activated_at = time.monotonic()
393
- self._state = ACTIVATED
394
- return # don't reset to IDLE
395
- elif not found:
396
- log("no_wake_word", text)
397
- if DEBUG:
398
- print(f" No wake word: {text}", file=sys.stderr)
343
+ self._finalize_streamed_wake_word(text)
344
+ elif not self._finalize_check_final_text(text):
345
+ return # entered ACTIVATED state, don't reset to IDLE
399
346
 
400
347
  self._reset_listening()
401
348
 
349
+ def _finalize_activated_command(self, text: str) -> None:
350
+ """Finalize utterance in ACTIVATED state (full text is the command)."""
351
+ command = text.strip()
352
+ if command:
353
+ if command != self._typed_text:
354
+ if self._typed_text:
355
+ self._update_typed_text(command)
356
+ else:
357
+ keyboard.type_text(command)
358
+ self._dispatch_result(command)
359
+ else:
360
+ if self._typed_text:
361
+ keyboard.backspace(len(self._typed_text))
362
+ log("dispatch_cancelled", "Empty command in activated mode")
363
+
364
+ def _finalize_streamed_wake_word(self, text: str) -> None:
365
+ """Finalize when wake word was detected during streaming."""
366
+ found, command = check_wake_word(text)
367
+ if found and command:
368
+ if command != self._typed_text:
369
+ self._update_typed_text(command)
370
+ self._dispatch_result(command)
371
+ elif found:
372
+ if self._typed_text:
373
+ keyboard.backspace(len(self._typed_text))
374
+ log("dispatch_cancelled", "No command after wake word")
375
+ elif self._typed_text:
376
+ # Final transcription lost the wake word (e.g. audio clipping
377
+ # turned "computer" into "uter"); fall back to the command
378
+ # captured during streaming
379
+ self._dispatch_result(self._typed_text)
380
+
381
+ def _finalize_check_final_text(self, text: str) -> bool:
382
+ """Check final transcription for wake word.
383
+
384
+ Returns True if caller should reset to IDLE, False if entering ACTIVATED.
385
+ """
386
+ found, command = check_wake_word(text)
387
+ if found and command:
388
+ log("wake_word_detected", command)
389
+ if DEBUG:
390
+ print(f" Wake word! Final: {command}", file=sys.stderr)
391
+ keyboard.type_text(command)
392
+ self._dispatch_result(command)
393
+ if found and not command:
394
+ # Wake word only — enter ACTIVATED state for next utterance
395
+ log("wake_word_only", "Listening for command...")
396
+ if DEBUG:
397
+ print(" Wake word heard — listening for command...", file=sys.stderr)
398
+ self._audio_buffer.clear()
399
+ self._vad.reset()
400
+ self._wake_detected = False
401
+ self._typed_text = ""
402
+ self._last_partial_at = 0
403
+ self._activated_at = time.monotonic()
404
+ self._state = ACTIVATED
405
+ return False
406
+ elif not found:
407
+ log("no_wake_word", text)
408
+ if DEBUG:
409
+ print(f" No wake word: {text}", file=sys.stderr)
410
+ return True
411
+
402
412
  def _reset_listening(self) -> None:
403
413
  self._audio_buffer.clear()
404
414
  self._vad.reset()
@@ -408,6 +418,21 @@ class VoiceDaemon:
408
418
  self._activated_at = 0.0
409
419
  self._state = IDLE
410
420
 
421
+ def _check_activated_timeout(self) -> bool:
422
+ """If in ACTIVATED state with no audio buffered, check for timeout.
423
+
424
+ Returns True if timed out and state was reset.
425
+ """
426
+ if self._state != ACTIVATED or self._audio_buffer:
427
+ return False
428
+ if time.monotonic() - self._activated_at <= ACTIVATED_TIMEOUT:
429
+ return False
430
+ log("activated_timeout", "No command received")
431
+ if DEBUG:
432
+ print("\n Activation timed out", file=sys.stderr)
433
+ self._reset_listening()
434
+ return True
435
+
411
436
  def run(self) -> None:
412
437
  signal.signal(signal.SIGTERM, self._handle_signal)
413
438
  signal.signal(signal.SIGINT, self._handle_signal)
@@ -425,12 +450,7 @@ class VoiceDaemon:
425
450
  while self._running:
426
451
  chunk = self._mic.read(timeout=0.5)
427
452
  if chunk is None:
428
- if self._state == ACTIVATED and not self._audio_buffer:
429
- if time.monotonic() - self._activated_at > ACTIVATED_TIMEOUT:
430
- log("activated_timeout", "No command received")
431
- if DEBUG:
432
- print("\n Activation timed out", file=sys.stderr)
433
- self._reset_listening()
453
+ self._check_activated_timeout()
434
454
  continue
435
455
 
436
456
  prob = self._vad.process(chunk)
@@ -448,14 +468,8 @@ class VoiceDaemon:
448
468
  self._last_partial_at = 0
449
469
 
450
470
  elif self._state == ACTIVATED:
451
- # Check timeout (only before speech starts)
452
- if not self._audio_buffer:
453
- if time.monotonic() - self._activated_at > ACTIVATED_TIMEOUT:
454
- log("activated_timeout", "No command received")
455
- if DEBUG:
456
- print("\n Activation timed out", file=sys.stderr)
457
- self._reset_listening()
458
- continue
471
+ if self._check_activated_timeout():
472
+ continue
459
473
 
460
474
  if prob > self._vad.threshold and not self._audio_buffer:
461
475
  log("speech_start", "command after activation")
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.87.0",
9
+ version: "0.88.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.87.0",
3
+ "version": "0.88.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {