@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(
|
|
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
|
-
|
|
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(
|
|
102
|
+
self._update_typed_text(partial)
|
|
99
103
|
else:
|
|
100
|
-
keyboard.type_text(
|
|
101
|
-
self._typed_text =
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
114
|
-
self.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
217
|
-
if
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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();
|