@staff0rd/assist 0.83.1 → 0.84.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.
- package/dist/commands/voice/python/voice_daemon.py +119 -102
- package/dist/index.js +46 -36
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Voice daemon entry point — main loop and signal handling."""
|
|
2
2
|
|
|
3
|
+
import ctypes
|
|
4
|
+
import json
|
|
3
5
|
import os
|
|
4
6
|
import signal
|
|
7
|
+
import subprocess
|
|
5
8
|
import sys
|
|
6
9
|
import time
|
|
7
10
|
|
|
@@ -16,6 +19,73 @@ from wake_word import check_wake_word
|
|
|
16
19
|
|
|
17
20
|
import keyboard
|
|
18
21
|
|
|
22
|
+
|
|
23
|
+
def _load_voice_config() -> dict:
|
|
24
|
+
"""Load voice config by calling ``assist config get voice``.
|
|
25
|
+
|
|
26
|
+
Sets environment variables so other Python modules (audio_capture,
|
|
27
|
+
vad, smart_turn, stt, wake_word) can read them as before.
|
|
28
|
+
Returns the parsed config dict.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
["assist", "config", "get", "voice"],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
text=True,
|
|
35
|
+
check=True,
|
|
36
|
+
shell=True,
|
|
37
|
+
)
|
|
38
|
+
config = json.loads(result.stdout)
|
|
39
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError) as exc:
|
|
40
|
+
log("config_error", str(exc), level="error")
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
env_map: dict[str, str | None] = {
|
|
44
|
+
"VOICE_WAKE_WORDS": ",".join(config.get("wakeWords", [])) or None,
|
|
45
|
+
"VOICE_MIC": config.get("mic"),
|
|
46
|
+
"VOICE_MODELS_DIR": os.path.expanduser(v)
|
|
47
|
+
if (v := config.get("modelsDir"))
|
|
48
|
+
else None,
|
|
49
|
+
"VOICE_MODEL_VAD": (config.get("models") or {}).get("vad"),
|
|
50
|
+
"VOICE_MODEL_SMART_TURN": (config.get("models") or {}).get("smartTurn"),
|
|
51
|
+
}
|
|
52
|
+
for key, value in env_map.items():
|
|
53
|
+
if value:
|
|
54
|
+
os.environ[key] = value
|
|
55
|
+
|
|
56
|
+
log("config_loaded", json.dumps(config))
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_foreground_window_info() -> str:
|
|
61
|
+
"""Return the title and process name of the currently focused window."""
|
|
62
|
+
user32 = ctypes.windll.user32
|
|
63
|
+
kernel32 = ctypes.windll.kernel32
|
|
64
|
+
hwnd = user32.GetForegroundWindow()
|
|
65
|
+
|
|
66
|
+
# Window title
|
|
67
|
+
buf = ctypes.create_unicode_buffer(256)
|
|
68
|
+
user32.GetWindowTextW(hwnd, buf, 256)
|
|
69
|
+
title = buf.value
|
|
70
|
+
|
|
71
|
+
# Process name from window handle
|
|
72
|
+
pid = ctypes.c_ulong()
|
|
73
|
+
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
74
|
+
process_name = ""
|
|
75
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
76
|
+
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid.value)
|
|
77
|
+
if handle:
|
|
78
|
+
exe_buf = ctypes.create_unicode_buffer(260)
|
|
79
|
+
size = ctypes.c_ulong(260)
|
|
80
|
+
if kernel32.QueryFullProcessImageNameW(handle, 0, exe_buf, ctypes.byref(size)):
|
|
81
|
+
process_name = exe_buf.value.rsplit("\\", 1)[-1]
|
|
82
|
+
kernel32.CloseHandle(handle)
|
|
83
|
+
|
|
84
|
+
if process_name:
|
|
85
|
+
return f"{process_name}: {title}"
|
|
86
|
+
return title
|
|
87
|
+
|
|
88
|
+
|
|
19
89
|
# States
|
|
20
90
|
IDLE = "idle"
|
|
21
91
|
LISTENING = "listening"
|
|
@@ -36,23 +106,9 @@ STOP_CHUNKS = (STOP_MS * 16000) // (BLOCK_SIZE * 1000) # ~31 chunks
|
|
|
36
106
|
ACTIVATED_TIMEOUT = 10.0
|
|
37
107
|
|
|
38
108
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"""Print a live VAD meter to stderr when debug mode is on."""
|
|
43
|
-
rms = float(np.sqrt(np.mean(chunk**2)))
|
|
44
|
-
peak = float(np.max(np.abs(chunk)))
|
|
45
|
-
width = 40
|
|
46
|
-
filled = int(prob * width)
|
|
47
|
-
bar = "█" * filled + "░" * (width - filled)
|
|
48
|
-
marker = ">" if prob > threshold else " "
|
|
49
|
-
print(
|
|
50
|
-
f"\r {marker} VAD {prob:.2f} [{bar}] "
|
|
51
|
-
f"rms={rms:.4f} peak={peak:.4f} {state:10s}",
|
|
52
|
-
end="",
|
|
53
|
-
file=sys.stderr,
|
|
54
|
-
flush=True,
|
|
55
|
-
)
|
|
109
|
+
def _print_state(state: str) -> None:
|
|
110
|
+
"""Print the current daemon state to stderr when debug mode is on."""
|
|
111
|
+
print(f"\r {state:10s}", end="", file=sys.stderr, flush=True)
|
|
56
112
|
|
|
57
113
|
|
|
58
114
|
class VoiceDaemon:
|
|
@@ -60,15 +116,17 @@ class VoiceDaemon:
|
|
|
60
116
|
self._running = True
|
|
61
117
|
self._state = IDLE
|
|
62
118
|
self._audio_buffer: list[np.ndarray] = []
|
|
63
|
-
|
|
119
|
+
|
|
120
|
+
config = _load_voice_config()
|
|
121
|
+
self._submit_windows: set[str] = set(config.get("submitWindows") or [])
|
|
64
122
|
|
|
65
123
|
log("daemon_init", "Initializing models...")
|
|
66
124
|
self._mic = AudioCapture()
|
|
67
125
|
self._vad = SileroVAD()
|
|
68
126
|
self._smart_turn = SmartTurn()
|
|
69
127
|
self._stt = ParakeetSTT()
|
|
70
|
-
if self.
|
|
71
|
-
log("daemon_init", f"Submit
|
|
128
|
+
if self._submit_windows:
|
|
129
|
+
log("daemon_init", f"Submit windows: {self._submit_windows}")
|
|
72
130
|
log("daemon_ready")
|
|
73
131
|
|
|
74
132
|
# Incremental typing state
|
|
@@ -96,7 +154,7 @@ class VoiceDaemon:
|
|
|
96
154
|
|
|
97
155
|
if self._state == ACTIVATED:
|
|
98
156
|
# Already activated — everything is the command, no wake word needed
|
|
99
|
-
partial =
|
|
157
|
+
partial = text.strip()
|
|
100
158
|
if partial and partial != self._typed_text:
|
|
101
159
|
if self._typed_text:
|
|
102
160
|
self._update_typed_text(partial)
|
|
@@ -106,41 +164,25 @@ class VoiceDaemon:
|
|
|
106
164
|
elif not self._wake_detected:
|
|
107
165
|
found, command = check_wake_word(text)
|
|
108
166
|
if found and command:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
keyboard.type_text(partial)
|
|
116
|
-
self._typed_text = partial
|
|
167
|
+
self._wake_detected = True
|
|
168
|
+
log("wake_word_detected", command)
|
|
169
|
+
if DEBUG:
|
|
170
|
+
print(f" Wake word! Typing: {command}", file=sys.stderr)
|
|
171
|
+
keyboard.type_text(command)
|
|
172
|
+
self._typed_text = command
|
|
117
173
|
else:
|
|
118
174
|
found, command = check_wake_word(text)
|
|
119
175
|
if found and command:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
self._update_typed_text(partial)
|
|
176
|
+
if command != self._typed_text:
|
|
177
|
+
self._update_typed_text(command)
|
|
123
178
|
|
|
124
|
-
def
|
|
125
|
-
"""Check if
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"""
|
|
130
|
-
|
|
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
|
|
179
|
+
def _should_submit(self) -> bool:
|
|
180
|
+
"""Check if the foreground window matches the submit allowlist."""
|
|
181
|
+
if not self._submit_windows:
|
|
182
|
+
return True
|
|
183
|
+
info = _get_foreground_window_info()
|
|
184
|
+
process_name = info.split(":")[0].strip() if ":" in info else ""
|
|
185
|
+
return process_name in self._submit_windows
|
|
144
186
|
|
|
145
187
|
def _update_typed_text(self, new_text: str) -> None:
|
|
146
188
|
"""Diff old typed text vs new, backspace + type the difference."""
|
|
@@ -212,26 +254,18 @@ class VoiceDaemon:
|
|
|
212
254
|
log("smart_turn_incomplete", "Continuing to listen...")
|
|
213
255
|
return False
|
|
214
256
|
|
|
215
|
-
def _dispatch_result(self,
|
|
257
|
+
def _dispatch_result(self, text: str) -> None:
|
|
216
258
|
"""Log and optionally submit a recognized command."""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if DEBUG:
|
|
221
|
-
print(f" Final: {stripped} [Enter]", file=sys.stderr)
|
|
222
|
-
keyboard.press_enter()
|
|
223
|
-
else:
|
|
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)")
|
|
259
|
+
should_submit = self._should_submit()
|
|
260
|
+
if should_submit:
|
|
261
|
+
log("dispatch_enter", text)
|
|
232
262
|
if DEBUG:
|
|
233
|
-
print("
|
|
263
|
+
print(f" Final: {text} [Enter]", file=sys.stderr)
|
|
234
264
|
keyboard.press_enter()
|
|
265
|
+
else:
|
|
266
|
+
log("dispatch_typed", text)
|
|
267
|
+
if DEBUG:
|
|
268
|
+
print(f" Final: {text} (no submit)", file=sys.stderr)
|
|
235
269
|
|
|
236
270
|
def _finalize_utterance(self) -> None:
|
|
237
271
|
"""End of turn: final STT, correct typed text, press Enter."""
|
|
@@ -252,14 +286,12 @@ class VoiceDaemon:
|
|
|
252
286
|
# Activated mode — full text is the command
|
|
253
287
|
command = text.strip()
|
|
254
288
|
if command:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
keyboard.type_text(stripped)
|
|
262
|
-
self._dispatch_result(should_submit, stripped)
|
|
289
|
+
if command != self._typed_text:
|
|
290
|
+
if self._typed_text:
|
|
291
|
+
self._update_typed_text(command)
|
|
292
|
+
else:
|
|
293
|
+
keyboard.type_text(command)
|
|
294
|
+
self._dispatch_result(command)
|
|
263
295
|
else:
|
|
264
296
|
if self._typed_text:
|
|
265
297
|
keyboard.backspace(len(self._typed_text))
|
|
@@ -271,11 +303,9 @@ class VoiceDaemon:
|
|
|
271
303
|
# Correct final text and submit
|
|
272
304
|
found, command = check_wake_word(text)
|
|
273
305
|
if found and command:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
self._update_typed_text(stripped)
|
|
278
|
-
self._dispatch_result(should_submit, stripped)
|
|
306
|
+
if command != self._typed_text:
|
|
307
|
+
self._update_typed_text(command)
|
|
308
|
+
self._dispatch_result(command)
|
|
279
309
|
elif found:
|
|
280
310
|
# Wake word found but no command text after it
|
|
281
311
|
if self._typed_text:
|
|
@@ -285,29 +315,16 @@ class VoiceDaemon:
|
|
|
285
315
|
# Final transcription lost the wake word (e.g. audio clipping
|
|
286
316
|
# turned "computer" into "uter"); fall back to the command
|
|
287
317
|
# captured during streaming
|
|
288
|
-
|
|
289
|
-
self._dispatch_result(should_submit, stripped or self._typed_text)
|
|
318
|
+
self._dispatch_result(self._typed_text)
|
|
290
319
|
else:
|
|
291
320
|
# Check final transcription for wake word
|
|
292
321
|
found, command = check_wake_word(text)
|
|
293
322
|
if found and command:
|
|
294
|
-
|
|
295
|
-
if
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
print(
|
|
300
|
-
f" Wake word! Final: {stripped} {label}", file=sys.stderr
|
|
301
|
-
)
|
|
302
|
-
keyboard.type_text(stripped)
|
|
303
|
-
if should_submit:
|
|
304
|
-
keyboard.press_enter()
|
|
305
|
-
else:
|
|
306
|
-
# Submit word only — just press enter
|
|
307
|
-
log("dispatch_enter", "(submit word only)")
|
|
308
|
-
if DEBUG:
|
|
309
|
-
print(" Wake word + submit word only [Enter]", file=sys.stderr)
|
|
310
|
-
keyboard.press_enter()
|
|
323
|
+
log("wake_word_detected", command)
|
|
324
|
+
if DEBUG:
|
|
325
|
+
print(f" Wake word! Final: {command}", file=sys.stderr)
|
|
326
|
+
keyboard.type_text(command)
|
|
327
|
+
self._dispatch_result(command)
|
|
311
328
|
if found and not command:
|
|
312
329
|
# Wake word only — enter ACTIVATED state for next utterance
|
|
313
330
|
log("wake_word_only", "Listening for command...")
|
|
@@ -367,7 +384,7 @@ class VoiceDaemon:
|
|
|
367
384
|
prob = self._vad.process(chunk)
|
|
368
385
|
|
|
369
386
|
if DEBUG:
|
|
370
|
-
|
|
387
|
+
_print_state(self._state)
|
|
371
388
|
|
|
372
389
|
if self._state == IDLE:
|
|
373
390
|
if prob > self._vad.threshold:
|
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.84.0",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -84,6 +84,7 @@ var runConfigSchema = z.strictObject({
|
|
|
84
84
|
name: z.string(),
|
|
85
85
|
command: z.string(),
|
|
86
86
|
args: z.array(z.string()).optional(),
|
|
87
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
87
88
|
filter: z.string().optional()
|
|
88
89
|
});
|
|
89
90
|
var transcriptConfigSchema = z.strictObject({
|
|
@@ -91,6 +92,8 @@ var transcriptConfigSchema = z.strictObject({
|
|
|
91
92
|
transcriptsDir: z.string(),
|
|
92
93
|
summaryDir: z.string()
|
|
93
94
|
});
|
|
95
|
+
var DEFAULT_WAKE_WORDS = ["computer"];
|
|
96
|
+
var DEFAULT_MODELS_DIR = "~/.assist/voice/models";
|
|
94
97
|
var assistConfigSchema = z.strictObject({
|
|
95
98
|
commit: z.strictObject({
|
|
96
99
|
conventional: z.boolean().default(false),
|
|
@@ -126,18 +129,21 @@ var assistConfigSchema = z.strictObject({
|
|
|
126
129
|
run: z.array(runConfigSchema).optional(),
|
|
127
130
|
transcript: transcriptConfigSchema.optional(),
|
|
128
131
|
voice: z.strictObject({
|
|
129
|
-
wakeWords: z.array(z.string()).default(
|
|
132
|
+
wakeWords: z.array(z.string()).default(DEFAULT_WAKE_WORDS),
|
|
130
133
|
mic: z.string().optional(),
|
|
131
134
|
cwd: z.string().optional(),
|
|
132
|
-
modelsDir: z.string().
|
|
135
|
+
modelsDir: z.string().default(DEFAULT_MODELS_DIR),
|
|
133
136
|
lockDir: z.string().optional(),
|
|
134
|
-
|
|
137
|
+
submitWindows: z.array(z.string()).optional(),
|
|
135
138
|
models: z.strictObject({
|
|
136
139
|
vad: z.string().optional(),
|
|
137
|
-
smartTurn: z.string().optional()
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
smartTurn: z.string().optional()
|
|
141
|
+
}).default({})
|
|
142
|
+
}).default({
|
|
143
|
+
wakeWords: DEFAULT_WAKE_WORDS,
|
|
144
|
+
modelsDir: DEFAULT_MODELS_DIR,
|
|
145
|
+
models: {}
|
|
146
|
+
})
|
|
141
147
|
});
|
|
142
148
|
|
|
143
149
|
// src/shared/loadConfig.ts
|
|
@@ -1403,6 +1409,7 @@ function getRunEntries() {
|
|
|
1403
1409
|
return run3.filter((r) => r.name.startsWith("verify:")).map((r) => ({
|
|
1404
1410
|
name: r.name,
|
|
1405
1411
|
fullCommand: buildFullCommand(r.command, r.args),
|
|
1412
|
+
env: r.env,
|
|
1406
1413
|
filter: r.filter
|
|
1407
1414
|
}));
|
|
1408
1415
|
}
|
|
@@ -1502,12 +1509,26 @@ function filterByChangedFiles(entries) {
|
|
|
1502
1509
|
|
|
1503
1510
|
// src/commands/verify/run/spawnCommand.ts
|
|
1504
1511
|
import { spawn } from "child_process";
|
|
1512
|
+
|
|
1513
|
+
// src/shared/expandEnv.ts
|
|
1514
|
+
import { homedir as homedir2 } from "os";
|
|
1515
|
+
function expandTilde(value) {
|
|
1516
|
+
return value.startsWith("~/") ? homedir2() + value.slice(1) : value;
|
|
1517
|
+
}
|
|
1518
|
+
function expandEnv(env) {
|
|
1519
|
+
return Object.fromEntries(
|
|
1520
|
+
Object.entries(env).map(([k, v]) => [k, expandTilde(v)])
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/commands/verify/run/spawnCommand.ts
|
|
1505
1525
|
var isClaudeCode = !!process.env.CLAUDECODE;
|
|
1506
|
-
function spawnCommand(fullCommand, cwd) {
|
|
1526
|
+
function spawnCommand(fullCommand, cwd, env) {
|
|
1507
1527
|
return spawn(fullCommand, [], {
|
|
1508
1528
|
stdio: isClaudeCode ? "pipe" : "inherit",
|
|
1509
1529
|
shell: true,
|
|
1510
|
-
cwd: cwd ?? process.cwd()
|
|
1530
|
+
cwd: cwd ?? process.cwd(),
|
|
1531
|
+
env: env ? { ...process.env, ...expandEnv(env) } : void 0
|
|
1511
1532
|
});
|
|
1512
1533
|
}
|
|
1513
1534
|
function collectOutput(child) {
|
|
@@ -1526,7 +1547,7 @@ function flushIfFailed(exitCode, chunks) {
|
|
|
1526
1547
|
// src/commands/verify/run/index.ts
|
|
1527
1548
|
function runEntry(entry, onComplete) {
|
|
1528
1549
|
return new Promise((resolve3) => {
|
|
1529
|
-
const child = spawnCommand(entry.fullCommand, entry.cwd);
|
|
1550
|
+
const child = spawnCommand(entry.fullCommand, entry.cwd, entry.env);
|
|
1530
1551
|
const chunks = collectOutput(child);
|
|
1531
1552
|
child.on("close", (code) => {
|
|
1532
1553
|
const exitCode = code ?? 1;
|
|
@@ -2952,9 +2973,9 @@ import chalk37 from "chalk";
|
|
|
2952
2973
|
|
|
2953
2974
|
// src/commands/devlog/loadDevlogEntries.ts
|
|
2954
2975
|
import { readdirSync, readFileSync as readFileSync13 } from "fs";
|
|
2955
|
-
import { homedir as
|
|
2976
|
+
import { homedir as homedir3 } from "os";
|
|
2956
2977
|
import { join as join11 } from "path";
|
|
2957
|
-
var DEVLOG_DIR = join11(
|
|
2978
|
+
var DEVLOG_DIR = join11(homedir3(), "git/blog/src/content/devlog");
|
|
2958
2979
|
function loadDevlogEntries(repoName) {
|
|
2959
2980
|
const entries = /* @__PURE__ */ new Map();
|
|
2960
2981
|
try {
|
|
@@ -5306,11 +5327,11 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
5306
5327
|
import { join as join24 } from "path";
|
|
5307
5328
|
|
|
5308
5329
|
// src/commands/voice/shared.ts
|
|
5309
|
-
import { homedir as
|
|
5330
|
+
import { homedir as homedir4 } from "os";
|
|
5310
5331
|
import { dirname as dirname17, join as join23 } from "path";
|
|
5311
5332
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5312
5333
|
var __dirname5 = dirname17(fileURLToPath4(import.meta.url));
|
|
5313
|
-
var VOICE_DIR = join23(
|
|
5334
|
+
var VOICE_DIR = join23(homedir4(), ".assist", "voice");
|
|
5314
5335
|
var voicePaths = {
|
|
5315
5336
|
dir: VOICE_DIR,
|
|
5316
5337
|
pid: join23(VOICE_DIR, "voice.pid"),
|
|
@@ -5441,26 +5462,8 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync19 } from "fs";
|
|
|
5441
5462
|
import { join as join27 } from "path";
|
|
5442
5463
|
|
|
5443
5464
|
// src/commands/voice/buildDaemonEnv.ts
|
|
5444
|
-
var ENV_MAP = {
|
|
5445
|
-
VOICE_WAKE_WORDS: (v) => v.wakeWords?.join(","),
|
|
5446
|
-
VOICE_MIC: (v) => v.mic,
|
|
5447
|
-
VOICE_CWD: (v) => v.cwd,
|
|
5448
|
-
VOICE_MODELS_DIR: (v) => v.modelsDir,
|
|
5449
|
-
VOICE_MODEL_VAD: (v) => v.models?.vad,
|
|
5450
|
-
VOICE_MODEL_SMART_TURN: (v) => v.models?.smartTurn,
|
|
5451
|
-
VOICE_MODEL_STT: (v) => v.models?.stt,
|
|
5452
|
-
VOICE_SUBMIT_WORD: (v) => v.submitWord
|
|
5453
|
-
};
|
|
5454
5465
|
function buildDaemonEnv(options2) {
|
|
5455
|
-
const config = loadConfig();
|
|
5456
5466
|
const env = { ...process.env };
|
|
5457
|
-
const voice = config.voice;
|
|
5458
|
-
if (voice) {
|
|
5459
|
-
for (const [key, getter] of Object.entries(ENV_MAP)) {
|
|
5460
|
-
const value = getter(voice);
|
|
5461
|
-
if (value) env[key] = value;
|
|
5462
|
-
}
|
|
5463
|
-
}
|
|
5464
5467
|
env.VOICE_LOG_FILE = voicePaths.log;
|
|
5465
5468
|
if (options2?.debug) env.VOICE_DEBUG = "1";
|
|
5466
5469
|
return env;
|
|
@@ -5903,8 +5906,12 @@ function onSpawnError(err) {
|
|
|
5903
5906
|
console.error(`Failed to execute command: ${err.message}`);
|
|
5904
5907
|
process.exit(1);
|
|
5905
5908
|
}
|
|
5906
|
-
function spawnCommand2(fullCommand) {
|
|
5907
|
-
const child = spawn5(fullCommand, [], {
|
|
5909
|
+
function spawnCommand2(fullCommand, env) {
|
|
5910
|
+
const child = spawn5(fullCommand, [], {
|
|
5911
|
+
stdio: "inherit",
|
|
5912
|
+
shell: true,
|
|
5913
|
+
env: env ? { ...process.env, ...expandEnv(env) } : void 0
|
|
5914
|
+
});
|
|
5908
5915
|
child.on("close", (code) => process.exit(code ?? 0));
|
|
5909
5916
|
child.on("error", onSpawnError);
|
|
5910
5917
|
}
|
|
@@ -5917,7 +5924,10 @@ function listRunConfigs() {
|
|
|
5917
5924
|
}
|
|
5918
5925
|
function run2(name, args) {
|
|
5919
5926
|
const runConfig = findRunConfig(name);
|
|
5920
|
-
spawnCommand2(
|
|
5927
|
+
spawnCommand2(
|
|
5928
|
+
buildCommand(runConfig.command, runConfig.args ?? [], args),
|
|
5929
|
+
runConfig.env
|
|
5930
|
+
);
|
|
5921
5931
|
}
|
|
5922
5932
|
|
|
5923
5933
|
// src/commands/statusLine.ts
|