@goodandready/dsh-voice 0.9.17 → 0.9.18
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/lib/client.js +406 -366
- package/lib/host-routes.js +186 -0
- package/lib/index.js +73 -281
- package/lib/local-daemon.js +78 -24
- package/lib/updater.js +15 -7
- package/package.json +1 -1
package/lib/local-daemon.js
CHANGED
|
@@ -6,6 +6,7 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
6
6
|
let child = null
|
|
7
7
|
let whisperError = null
|
|
8
8
|
let startingWhisper = false
|
|
9
|
+
let disposed = false
|
|
9
10
|
|
|
10
11
|
async function whisperAlive() {
|
|
11
12
|
const rawUrl = String(live().whisperUrl || '').trim()
|
|
@@ -20,7 +21,7 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async function startWhisper() {
|
|
23
|
-
if (startingWhisper) return false
|
|
24
|
+
if (startingWhisper || disposed) return false
|
|
24
25
|
startingWhisper = true
|
|
25
26
|
try {
|
|
26
27
|
const cfg = live()
|
|
@@ -36,15 +37,21 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
36
37
|
}
|
|
37
38
|
const command = `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
|
|
38
39
|
+ ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${isAutoLang(cfg.dictation.language) ? 'auto' : cfg.dictation.language}`
|
|
39
|
-
const specOpts = { command, timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
40
|
+
const specOpts = { command, onExpiry: 'none', timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
40
41
|
const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
|
|
41
42
|
|
|
42
43
|
if (typeof ctx.shell?.execute === 'function') {
|
|
43
44
|
child = await ctx.shell.execute(spec)
|
|
44
45
|
} else if (typeof ctx.shell?.start === 'function') {
|
|
45
46
|
child = await ctx.shell.start(spec)
|
|
46
|
-
}
|
|
47
|
-
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (disposed) {
|
|
50
|
+
if (child && typeof child.kill === 'function') {
|
|
51
|
+
try { child.kill() } catch { /* ignore */ }
|
|
52
|
+
child = null
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
if (child && typeof child.on === 'function') {
|
|
@@ -59,7 +66,14 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
59
66
|
}
|
|
60
67
|
if (child && child.done && typeof child.done.then === 'function') {
|
|
61
68
|
child.done.then((res) => {
|
|
62
|
-
if (
|
|
69
|
+
if (child.status === 'killed') {
|
|
70
|
+
whisperError = 'whisper server process was killed'
|
|
71
|
+
} else if (child.status === 'completed') {
|
|
72
|
+
const code = child.result?.exitCode ?? (res && res.code)
|
|
73
|
+
if (code !== 0 && code !== null && code !== undefined) {
|
|
74
|
+
whisperError = `whisper server process exited with code ${code}`
|
|
75
|
+
}
|
|
76
|
+
} else if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
|
|
63
77
|
whisperError = `whisper server process exited with code ${res.code}`
|
|
64
78
|
}
|
|
65
79
|
}).catch((err) => {
|
|
@@ -69,18 +83,31 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
69
83
|
|
|
70
84
|
// Wait for it to become healthy (up to 30s for cold model loading on Metal/CPU).
|
|
71
85
|
for (let i = 0; i < 60; i++) {
|
|
86
|
+
if (disposed) return false
|
|
72
87
|
await new Promise((r) => setTimeout(r, 500))
|
|
88
|
+
if (disposed) return false
|
|
73
89
|
if (await whisperAlive()) {
|
|
74
90
|
whisperError = null
|
|
75
91
|
return true
|
|
76
92
|
}
|
|
77
|
-
if (child
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
if (child) {
|
|
94
|
+
if (child.status === 'killed') {
|
|
95
|
+
whisperError = 'whisper server process was killed'
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
if (child.status === 'completed') {
|
|
99
|
+
const code = child.result?.exitCode ?? child.exitCode ?? 0
|
|
100
|
+
whisperError = `whisper server process exited prematurely with code ${code}`
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
if (child.status === 'exited' || child.status === 'failed') {
|
|
104
|
+
whisperError = `whisper server process exited prematurely with status ${child.status}`
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
if (child.exitCode !== null && child.exitCode !== undefined) {
|
|
108
|
+
whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
113
|
whisperError = 'whisper server process started but failed health check on port 8001'
|
|
@@ -110,7 +137,7 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
async function startSensevoice() {
|
|
113
|
-
if (startingSensevoice) return false
|
|
140
|
+
if (startingSensevoice || disposed) return false
|
|
114
141
|
startingSensevoice = true
|
|
115
142
|
try {
|
|
116
143
|
const cfg = live()
|
|
@@ -128,15 +155,21 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
128
155
|
try { port = new URL(cfg.sensevoiceUrl).port || '6006' } catch { /* invalid URL */ }
|
|
129
156
|
const cmdArgs = buildSensevoiceArgs(cfg, port)
|
|
130
157
|
const commandStr = `${JSON.stringify(cfg.sensevoiceBin)}${cmdArgs}`
|
|
131
|
-
const specOpts = { command: commandStr, timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
158
|
+
const specOpts = { command: commandStr, onExpiry: 'none', timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
132
159
|
const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
|
|
133
160
|
|
|
134
161
|
if (typeof ctx.shell?.execute === 'function') {
|
|
135
162
|
sensevoiceChild = await ctx.shell.execute(spec)
|
|
136
163
|
} else if (typeof ctx.shell?.start === 'function') {
|
|
137
164
|
sensevoiceChild = await ctx.shell.start(spec)
|
|
138
|
-
}
|
|
139
|
-
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (disposed) {
|
|
168
|
+
if (sensevoiceChild && typeof sensevoiceChild.kill === 'function') {
|
|
169
|
+
try { sensevoiceChild.kill() } catch { /* ignore */ }
|
|
170
|
+
sensevoiceChild = null
|
|
171
|
+
}
|
|
172
|
+
return false
|
|
140
173
|
}
|
|
141
174
|
|
|
142
175
|
if (sensevoiceChild && typeof sensevoiceChild.on === 'function') {
|
|
@@ -151,7 +184,14 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
151
184
|
}
|
|
152
185
|
if (sensevoiceChild && sensevoiceChild.done && typeof sensevoiceChild.done.then === 'function') {
|
|
153
186
|
sensevoiceChild.done.then((res) => {
|
|
154
|
-
if (
|
|
187
|
+
if (sensevoiceChild.status === 'killed') {
|
|
188
|
+
sensevoiceError = 'sensevoice process was killed'
|
|
189
|
+
} else if (sensevoiceChild.status === 'completed') {
|
|
190
|
+
const code = sensevoiceChild.result?.exitCode ?? (res && res.code)
|
|
191
|
+
if (code !== 0 && code !== null && code !== undefined) {
|
|
192
|
+
sensevoiceError = `sensevoice process exited with code ${code}`
|
|
193
|
+
}
|
|
194
|
+
} else if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
|
|
155
195
|
sensevoiceError = `sensevoice process exited with code ${res.code}`
|
|
156
196
|
}
|
|
157
197
|
}).catch((err) => {
|
|
@@ -161,18 +201,31 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
161
201
|
|
|
162
202
|
// Wait for it to become healthy (up to 30s).
|
|
163
203
|
for (let i = 0; i < 60; i++) {
|
|
204
|
+
if (disposed) return false
|
|
164
205
|
await new Promise((r) => setTimeout(r, 500))
|
|
206
|
+
if (disposed) return false
|
|
165
207
|
if (await sensevoiceAlive()) {
|
|
166
208
|
sensevoiceError = null
|
|
167
209
|
return true
|
|
168
210
|
}
|
|
169
|
-
if (sensevoiceChild
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
if (sensevoiceChild) {
|
|
212
|
+
if (sensevoiceChild.status === 'killed') {
|
|
213
|
+
sensevoiceError = 'sensevoice process was killed'
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
if (sensevoiceChild.status === 'completed') {
|
|
217
|
+
const code = sensevoiceChild.result?.exitCode ?? sensevoiceChild.exitCode ?? 0
|
|
218
|
+
sensevoiceError = `sensevoice process exited prematurely with code ${code}`
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
if (sensevoiceChild.status === 'exited' || sensevoiceChild.status === 'failed') {
|
|
222
|
+
sensevoiceError = `sensevoice process exited prematurely with status ${sensevoiceChild.status}`
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
if (sensevoiceChild.exitCode !== null && sensevoiceChild.exitCode !== undefined) {
|
|
226
|
+
sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
176
229
|
}
|
|
177
230
|
}
|
|
178
231
|
sensevoiceError = 'sensevoice process started but failed health check'
|
|
@@ -195,6 +248,7 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
195
248
|
}
|
|
196
249
|
|
|
197
250
|
function dispose() {
|
|
251
|
+
disposed = true
|
|
198
252
|
if (child && typeof child.kill === 'function') {
|
|
199
253
|
try { child.kill() } catch { /* ignore */ }
|
|
200
254
|
child = null
|
package/lib/updater.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Implements canonical DSH plugin-updater specification.
|
|
3
3
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync, unlinkSync, renameSync } from 'node:fs';
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
@@ -203,15 +203,23 @@ export function checkProfileLock(profileDir) {
|
|
|
203
203
|
return { locked: true, pid };
|
|
204
204
|
}
|
|
205
205
|
// Verify before unlinking: ensure the file content still holds the dead pid we read,
|
|
206
|
-
//
|
|
206
|
+
// using atomic rename coordinator so a successor's live lock is never deleted (#205).
|
|
207
207
|
try {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
const staleName = `${lockPath}.stale.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
209
|
+
renameSync(lockPath, staleName);
|
|
210
|
+
const renamedPid = readLockPid(staleName);
|
|
211
|
+
if (renamedPid === pid) {
|
|
212
|
+
try { unlinkSync(staleName); } catch {}
|
|
213
|
+
return { locked: false, cleanedStale: true };
|
|
211
214
|
}
|
|
212
|
-
|
|
213
|
-
return { locked:
|
|
215
|
+
try { renameSync(staleName, lockPath); } catch {}
|
|
216
|
+
return { locked: true, pid: renamedPid };
|
|
214
217
|
} catch {
|
|
218
|
+
if (!existsSync(lockPath)) return { locked: false };
|
|
219
|
+
const currentPid = readLockPid(lockPath);
|
|
220
|
+
if (currentPid !== null && isProcessAlive(currentPid)) {
|
|
221
|
+
return { locked: true, pid: currentPid };
|
|
222
|
+
}
|
|
215
223
|
return { locked: false };
|
|
216
224
|
}
|
|
217
225
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.18",
|
|
4
4
|
"description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|