@staff0rd/assist 0.81.0 → 0.82.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.
@@ -24,7 +24,10 @@ class AudioCapture:
24
24
  self._queue.put(indata[:, 0].copy())
25
25
 
26
26
  def start(self) -> None:
27
- log("audio_start", f"device={self._device}, rate={SAMPLE_RATE}, block={BLOCK_SIZE}")
27
+ log(
28
+ "audio_start",
29
+ f"device={self._device}, rate={SAMPLE_RATE}, block={BLOCK_SIZE}",
30
+ )
28
31
  self._stream = sd.InputStream(
29
32
  samplerate=SAMPLE_RATE,
30
33
  channels=1,
@@ -60,12 +60,15 @@ class VoiceDaemon:
60
60
  self._running = True
61
61
  self._state = IDLE
62
62
  self._audio_buffer: list[np.ndarray] = []
63
+ self._submit_word = os.environ.get("VOICE_SUBMIT_WORD", "").strip().lower()
63
64
 
64
65
  log("daemon_init", "Initializing models...")
65
66
  self._mic = AudioCapture()
66
67
  self._vad = SileroVAD()
67
68
  self._smart_turn = SmartTurn()
68
69
  self._stt = ParakeetSTT()
70
+ if self._submit_word:
71
+ log("daemon_init", f"Submit word: '{self._submit_word}'")
69
72
  log("daemon_ready")
70
73
 
71
74
  # Incremental typing state
@@ -93,25 +96,51 @@ class VoiceDaemon:
93
96
 
94
97
  if self._state == ACTIVATED:
95
98
  # Already activated — everything is the command, no wake word needed
96
- if text.strip() != self._typed_text:
99
+ partial = self._hide_submit_word(text.strip())
100
+ if partial and partial != self._typed_text:
97
101
  if self._typed_text:
98
- self._update_typed_text(text.strip())
102
+ self._update_typed_text(partial)
99
103
  else:
100
- keyboard.type_text(text.strip())
101
- self._typed_text = text.strip()
104
+ keyboard.type_text(partial)
105
+ self._typed_text = partial
102
106
  elif not self._wake_detected:
103
107
  found, command = check_wake_word(text)
104
108
  if found and command:
105
- self._wake_detected = True
106
- log("wake_word_detected", command)
107
- if DEBUG:
108
- print(f" Wake word! Typing: {command}", file=sys.stderr)
109
- keyboard.type_text(command)
110
- self._typed_text = command
109
+ partial = self._hide_submit_word(command)
110
+ if partial:
111
+ self._wake_detected = True
112
+ log("wake_word_detected", partial)
113
+ if DEBUG:
114
+ print(f" Wake word! Typing: {partial}", file=sys.stderr)
115
+ keyboard.type_text(partial)
116
+ self._typed_text = partial
111
117
  else:
112
118
  found, command = check_wake_word(text)
113
- if found and command and command != self._typed_text:
114
- self._update_typed_text(command)
119
+ if found and command:
120
+ partial = self._hide_submit_word(command)
121
+ if partial and partial != self._typed_text:
122
+ self._update_typed_text(partial)
123
+
124
+ def _strip_submit_word(self, text: str) -> tuple[bool, str]:
125
+ """Check if text ends with the submit word.
126
+
127
+ Returns (should_submit, stripped_text).
128
+ If no submit word is configured, always returns (True, text).
129
+ """
130
+ if not self._submit_word:
131
+ return True, text
132
+ words = text.rsplit(None, 1)
133
+ if len(words) >= 1 and words[-1].lower().rstrip(".,!?") == self._submit_word:
134
+ stripped = text[: text.lower().rfind(words[-1].lower())].rstrip()
135
+ return True, stripped
136
+ return False, text
137
+
138
+ def _hide_submit_word(self, text: str) -> str:
139
+ """Strip trailing submit word from partial text so it's never typed."""
140
+ if not self._submit_word:
141
+ return text
142
+ _, stripped = self._strip_submit_word(text)
143
+ return stripped
115
144
 
116
145
  def _update_typed_text(self, new_text: str) -> None:
117
146
  """Diff old typed text vs new, backspace + type the difference."""
@@ -179,15 +208,30 @@ class VoiceDaemon:
179
208
  # Activated mode — full text is the command
180
209
  command = text.strip()
181
210
  if command:
182
- if command != self._typed_text:
183
- if self._typed_text:
184
- self._update_typed_text(command)
211
+ should_submit, stripped = self._strip_submit_word(command)
212
+ if stripped:
213
+ if stripped != self._typed_text:
214
+ if self._typed_text:
215
+ self._update_typed_text(stripped)
216
+ else:
217
+ keyboard.type_text(stripped)
218
+ if should_submit:
219
+ log("dispatch_enter", stripped)
220
+ if DEBUG:
221
+ print(f" Final: {stripped} [Enter]", file=sys.stderr)
222
+ keyboard.press_enter()
185
223
  else:
186
- keyboard.type_text(command)
187
- log("dispatch_enter", command)
188
- if DEBUG:
189
- print(f" Final: {command} [Enter]", file=sys.stderr)
190
- keyboard.press_enter()
224
+ log("dispatch_typed", stripped)
225
+ if DEBUG:
226
+ print(f" Final: {stripped} (no submit)", file=sys.stderr)
227
+ elif should_submit:
228
+ # Submit word only — erase it and press enter
229
+ if self._typed_text:
230
+ keyboard.backspace(len(self._typed_text))
231
+ log("dispatch_enter", "(submit word only)")
232
+ if DEBUG:
233
+ print(" Submit word only [Enter]", file=sys.stderr)
234
+ keyboard.press_enter()
191
235
  else:
192
236
  if self._typed_text:
193
237
  keyboard.backspace(len(self._typed_text))
@@ -199,12 +243,27 @@ class VoiceDaemon:
199
243
  # Correct final text and submit
200
244
  found, command = check_wake_word(text)
201
245
  if found and command:
202
- if command != self._typed_text:
203
- self._update_typed_text(command)
204
- log("dispatch_enter", command)
205
- if DEBUG:
206
- print(f" Final: {command} [Enter]", file=sys.stderr)
207
- keyboard.press_enter()
246
+ should_submit, stripped = self._strip_submit_word(command)
247
+ if stripped:
248
+ if stripped != self._typed_text:
249
+ self._update_typed_text(stripped)
250
+ if should_submit:
251
+ log("dispatch_enter", stripped)
252
+ if DEBUG:
253
+ print(f" Final: {stripped} [Enter]", file=sys.stderr)
254
+ keyboard.press_enter()
255
+ else:
256
+ log("dispatch_typed", stripped)
257
+ if DEBUG:
258
+ print(f" Final: {stripped} (no submit)", file=sys.stderr)
259
+ elif should_submit:
260
+ # Submit word only — erase it and press enter
261
+ if self._typed_text:
262
+ keyboard.backspace(len(self._typed_text))
263
+ log("dispatch_enter", "(submit word only)")
264
+ if DEBUG:
265
+ print(" Submit word only [Enter]", file=sys.stderr)
266
+ keyboard.press_enter()
208
267
  elif self._typed_text:
209
268
  # Wake word but no command — clear what we typed
210
269
  keyboard.backspace(len(self._typed_text))
@@ -213,16 +272,30 @@ class VoiceDaemon:
213
272
  # Check final transcription for wake word
214
273
  found, command = check_wake_word(text)
215
274
  if found and command:
216
- log("wake_word_detected", command)
217
- if DEBUG:
218
- print(f" Wake word! Final: {command} [Enter]", file=sys.stderr)
219
- keyboard.type_text(command)
220
- keyboard.press_enter()
221
- elif found:
275
+ should_submit, stripped = self._strip_submit_word(command)
276
+ if stripped:
277
+ log("wake_word_detected", stripped)
278
+ if DEBUG:
279
+ label = "[Enter]" if should_submit else "(no submit)"
280
+ print(
281
+ f" Wake word! Final: {stripped} {label}", file=sys.stderr
282
+ )
283
+ keyboard.type_text(stripped)
284
+ if should_submit:
285
+ keyboard.press_enter()
286
+ else:
287
+ # Submit word only — just press enter
288
+ log("dispatch_enter", "(submit word only)")
289
+ if DEBUG:
290
+ print(" Wake word + submit word only [Enter]", file=sys.stderr)
291
+ keyboard.press_enter()
292
+ if found and not command:
222
293
  # Wake word only — enter ACTIVATED state for next utterance
223
294
  log("wake_word_only", "Listening for command...")
224
295
  if DEBUG:
225
- print(" Wake word heard — listening for command...", file=sys.stderr)
296
+ print(
297
+ " Wake word heard — listening for command...", file=sys.stderr
298
+ )
226
299
  self._audio_buffer.clear()
227
300
  self._vad.reset()
228
301
  self._wake_detected = False
@@ -231,7 +304,7 @@ class VoiceDaemon:
231
304
  self._activated_at = time.monotonic()
232
305
  self._state = ACTIVATED
233
306
  return # don't reset to IDLE
234
- else:
307
+ elif not found:
235
308
  log("no_wake_word", text)
236
309
  if DEBUG:
237
310
  print(f" No wake word: {text}", file=sys.stderr)
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.81.0",
9
+ version: "0.82.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -137,6 +137,7 @@ var assistConfigSchema = z.strictObject({
137
137
  cwd: z.string().optional(),
138
138
  modelsDir: z.string().optional(),
139
139
  lockDir: z.string().optional(),
140
+ submitWord: z.string().optional(),
140
141
  models: z.strictObject({
141
142
  vad: z.string().optional(),
142
143
  smartTurn: z.string().optional(),
@@ -5369,7 +5370,8 @@ var ENV_MAP = {
5369
5370
  VOICE_MODELS_DIR: (v) => v.modelsDir,
5370
5371
  VOICE_MODEL_VAD: (v) => v.models?.vad,
5371
5372
  VOICE_MODEL_SMART_TURN: (v) => v.models?.smartTurn,
5372
- VOICE_MODEL_STT: (v) => v.models?.stt
5373
+ VOICE_MODEL_STT: (v) => v.models?.stt,
5374
+ VOICE_SUBMIT_WORD: (v) => v.submitWord
5373
5375
  };
5374
5376
  function buildDaemonEnv(options2) {
5375
5377
  const config = loadConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {