@goodandready/dsh-voice 0.9.18 → 0.9.19
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 +46 -19
- package/lib/local-daemon.js +65 -17
- package/lib/sensevoice-installer.js +24 -4
- package/lib/updater.js +21 -10
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -884,11 +884,23 @@ window.__ModuleLoader__.load({
|
|
|
884
884
|
const span = target && target.span
|
|
885
885
|
if (span) {
|
|
886
886
|
if (typeof span.insert === 'function') {
|
|
887
|
-
try {
|
|
887
|
+
try {
|
|
888
|
+
const r = span.insert(text)
|
|
889
|
+
if (r !== false) return true
|
|
890
|
+
return false
|
|
891
|
+
} catch (_) {}
|
|
888
892
|
} else if (typeof span.insertText === 'function') {
|
|
889
|
-
try {
|
|
893
|
+
try {
|
|
894
|
+
const r = span.insertText(text)
|
|
895
|
+
if (r !== false) return true
|
|
896
|
+
return false
|
|
897
|
+
} catch (_) {}
|
|
890
898
|
} else if (typeof span === 'function') {
|
|
891
|
-
try {
|
|
899
|
+
try {
|
|
900
|
+
const r = span(text)
|
|
901
|
+
if (r !== false) return true
|
|
902
|
+
return false
|
|
903
|
+
} catch (_) {}
|
|
892
904
|
}
|
|
893
905
|
}
|
|
894
906
|
const actions = (target && target.actions) || voice.inputActions
|
|
@@ -898,13 +910,23 @@ window.__ModuleLoader__.load({
|
|
|
898
910
|
return false
|
|
899
911
|
}
|
|
900
912
|
if (typeof actions.insertText === 'function') {
|
|
901
|
-
try {
|
|
913
|
+
try {
|
|
914
|
+
const res = actions.insertText(text, span)
|
|
915
|
+
if (res !== false) return true
|
|
916
|
+
return false
|
|
917
|
+
} catch (_) {
|
|
918
|
+
return false
|
|
919
|
+
}
|
|
902
920
|
}
|
|
903
921
|
const draft = input && typeof input.draft === 'string' ? input.draft : ''
|
|
904
922
|
const before = draft
|
|
905
923
|
const sep = (!draft || draft.endsWith('\n') || text.startsWith('\n')) ? '' : ' '
|
|
906
924
|
const after = draft + sep + text
|
|
907
|
-
|
|
925
|
+
try {
|
|
926
|
+
actions.setDraft(after)
|
|
927
|
+
} catch (_) {
|
|
928
|
+
return false
|
|
929
|
+
}
|
|
908
930
|
const limit = Number(voice.settings.historyLimit)
|
|
909
931
|
if (limit > 0) {
|
|
910
932
|
insertHistory.push({ before, added: sep + text, after })
|
|
@@ -1291,7 +1313,7 @@ voice._chainsPromise = null
|
|
|
1291
1313
|
}
|
|
1292
1314
|
}
|
|
1293
1315
|
if (action === 'send' || action === 'clear' || action === 'newline') {
|
|
1294
|
-
executeVoiceAction(action, text)
|
|
1316
|
+
executeVoiceAction(action, text, target)
|
|
1295
1317
|
return
|
|
1296
1318
|
}
|
|
1297
1319
|
if (action === 'cancel') {
|
|
@@ -1301,7 +1323,7 @@ voice._chainsPromise = null
|
|
|
1301
1323
|
const delay = Number(voice.settings.sendDelayMs) || 0
|
|
1302
1324
|
if (text && delay > 0 && !voice.holding) {
|
|
1303
1325
|
appendDraft(text, target)
|
|
1304
|
-
voice.set({ phase: 'pending', pending: { text, undoOnly: true, leftMs: delay } })
|
|
1326
|
+
voice.set({ phase: 'pending', pending: { text, target, undoOnly: true, leftMs: delay } })
|
|
1305
1327
|
return
|
|
1306
1328
|
}
|
|
1307
1329
|
if (text) appendDraft(text, target)
|
|
@@ -1465,7 +1487,7 @@ voice._chainsPromise = null
|
|
|
1465
1487
|
if (!said) { voice.set({ phase: 'idle' }); return }
|
|
1466
1488
|
if (mode === 'message') {
|
|
1467
1489
|
appendDraft(said, target)
|
|
1468
|
-
voice.set({ phase: 'pending', pending: { text: said, leftMs: voice.settings.autoSendMs } })
|
|
1490
|
+
voice.set({ phase: 'pending', pending: { text: said, target, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs } })
|
|
1469
1491
|
} else {
|
|
1470
1492
|
voice.set({ phase: 'idle' })
|
|
1471
1493
|
}
|
|
@@ -1525,14 +1547,14 @@ voice._chainsPromise = null
|
|
|
1525
1547
|
}
|
|
1526
1548
|
}
|
|
1527
1549
|
if (action === 'send' || action === 'clear' || action === 'newline') {
|
|
1528
|
-
executeVoiceAction(action, text)
|
|
1550
|
+
executeVoiceAction(action, text, target)
|
|
1529
1551
|
return
|
|
1530
1552
|
}
|
|
1531
1553
|
if (action === 'cancel') { cancelCurrent(); return }
|
|
1532
1554
|
appendDraft(text, target)
|
|
1533
1555
|
if (voice.lastNote) voice.lastNote.text = text
|
|
1534
1556
|
if (mode === 'message') {
|
|
1535
|
-
voice.set({ phase: 'pending', pending: { text, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs, audioUrl: noteUrl } })
|
|
1557
|
+
voice.set({ phase: 'pending', pending: { text, target, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs, audioUrl: noteUrl } })
|
|
1536
1558
|
} else {
|
|
1537
1559
|
voice.set({ phase: 'idle' })
|
|
1538
1560
|
}
|
|
@@ -1549,26 +1571,29 @@ voice._chainsPromise = null
|
|
|
1549
1571
|
}
|
|
1550
1572
|
})
|
|
1551
1573
|
}
|
|
1552
|
-
function clearDraft() {
|
|
1553
|
-
const
|
|
1574
|
+
function clearDraft(target) {
|
|
1575
|
+
const activeTarget = target || voice._activeTarget
|
|
1576
|
+
const actions = (activeTarget && activeTarget.actions) || voice.inputActions
|
|
1554
1577
|
if (actions && typeof actions.setDraft === 'function') actions.setDraft('')
|
|
1555
1578
|
}
|
|
1556
1579
|
|
|
1557
1580
|
// Voice actions (#29-7): spoken triggers perform composer actions.
|
|
1558
|
-
function executeVoiceAction(action, rawText) {
|
|
1581
|
+
function executeVoiceAction(action, rawText, target) {
|
|
1582
|
+
const activeTarget = target || voice._activeTarget
|
|
1583
|
+
const actions = (activeTarget && activeTarget.actions) || voice.inputActions
|
|
1559
1584
|
if (action === 'send') {
|
|
1560
1585
|
const text = rawText.replace(/(^|[\s,.!?])(отправь|пошли|send)([\s,.!?]|$)/gi, '').trim()
|
|
1561
|
-
if (text) appendDraft(text)
|
|
1562
|
-
if (typeof
|
|
1563
|
-
|
|
1586
|
+
if (text) appendDraft(text, activeTarget)
|
|
1587
|
+
if (typeof actions?.submit === 'function') {
|
|
1588
|
+
actions.submit()
|
|
1564
1589
|
}
|
|
1565
1590
|
voice.set({ phase: 'idle' })
|
|
1566
1591
|
} else if (action === 'clear') {
|
|
1567
|
-
clearDraft()
|
|
1592
|
+
clearDraft(activeTarget)
|
|
1568
1593
|
voice.set({ phase: 'idle' })
|
|
1569
1594
|
} else if (action === 'newline') {
|
|
1570
1595
|
const payload = (rawText.replace(/(^|[\s,.!?])(с новой строки|новая строка|абзац|new line|paragraph)([\s,.!?]|$)/gi, '').trim())
|
|
1571
|
-
appendDraft(payload)
|
|
1596
|
+
appendDraft(payload, activeTarget)
|
|
1572
1597
|
voice.set({ phase: 'idle' })
|
|
1573
1598
|
}
|
|
1574
1599
|
}
|
|
@@ -1577,7 +1602,8 @@ voice._chainsPromise = null
|
|
|
1577
1602
|
const pending = voice.pending
|
|
1578
1603
|
if (!pending) return
|
|
1579
1604
|
const text = pending.text
|
|
1580
|
-
const
|
|
1605
|
+
const target = pending.target || voice._activeTarget
|
|
1606
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
1581
1607
|
voice.pending = null
|
|
1582
1608
|
voice.set({ phase: 'idle' })
|
|
1583
1609
|
|
|
@@ -3935,6 +3961,7 @@ voice._chainsPromise = null
|
|
|
3935
3961
|
startRecording,
|
|
3936
3962
|
cancelCurrent,
|
|
3937
3963
|
stopCurrent,
|
|
3964
|
+
submitPending,
|
|
3938
3965
|
loadWebGpuWhisper,
|
|
3939
3966
|
}
|
|
3940
3967
|
return module.exports
|
package/lib/local-daemon.js
CHANGED
|
@@ -65,16 +65,30 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
65
65
|
})
|
|
66
66
|
}
|
|
67
67
|
if (child && child.done && typeof child.done.then === 'function') {
|
|
68
|
-
child.done.then((res) => {
|
|
68
|
+
child.done.then(async (res) => {
|
|
69
|
+
let code = res && res.code
|
|
70
|
+
let detail = ''
|
|
71
|
+
if (typeof child.result === 'function') {
|
|
72
|
+
try {
|
|
73
|
+
const r = await child.result()
|
|
74
|
+
if (r) {
|
|
75
|
+
if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
|
|
76
|
+
if (r.stderr) detail = String(r.stderr).trim()
|
|
77
|
+
}
|
|
78
|
+
} catch (_) {}
|
|
79
|
+
} else if (child.result && typeof child.result === 'object') {
|
|
80
|
+
code = child.result.exitCode ?? code
|
|
81
|
+
if (child.result.stderr) detail = String(child.result.stderr).trim()
|
|
82
|
+
}
|
|
69
83
|
if (child.status === 'killed') {
|
|
70
|
-
whisperError = 'whisper server process was killed'
|
|
84
|
+
whisperError = detail ? `whisper server process was killed: ${detail}` : 'whisper server process was killed'
|
|
71
85
|
} else if (child.status === 'completed') {
|
|
72
|
-
|
|
86
|
+
code = code ?? child.exitCode ?? 0
|
|
73
87
|
if (code !== 0 && code !== null && code !== undefined) {
|
|
74
|
-
whisperError = `whisper server process exited with code ${code}`
|
|
88
|
+
whisperError = `whisper server process exited with code ${code}${detail ? `: ${detail}` : ''}`
|
|
75
89
|
}
|
|
76
|
-
} else if (
|
|
77
|
-
whisperError = `whisper server process exited with code ${
|
|
90
|
+
} else if (code !== 0 && code !== null && code !== undefined) {
|
|
91
|
+
whisperError = `whisper server process exited with code ${code}${detail ? `: ${detail}` : ''}`
|
|
78
92
|
}
|
|
79
93
|
}).catch((err) => {
|
|
80
94
|
whisperError = err?.message || String(err)
|
|
@@ -91,21 +105,41 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
91
105
|
return true
|
|
92
106
|
}
|
|
93
107
|
if (child) {
|
|
108
|
+
let detail = ''
|
|
109
|
+
if (typeof child.read === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
const out = child.read()
|
|
112
|
+
if (out && out.stderr) detail = String(out.stderr).trim()
|
|
113
|
+
} catch (_) {}
|
|
114
|
+
}
|
|
94
115
|
if (child.status === 'killed') {
|
|
95
|
-
whisperError = 'whisper server process was killed'
|
|
116
|
+
whisperError = detail ? `whisper server process was killed: ${detail}` : 'whisper server process was killed'
|
|
96
117
|
return false
|
|
97
118
|
}
|
|
98
119
|
if (child.status === 'completed') {
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
let code = child.exitCode
|
|
121
|
+
if (typeof child.result === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
const r = await child.result()
|
|
124
|
+
if (r) {
|
|
125
|
+
if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
|
|
126
|
+
if (r.stderr && !detail) detail = String(r.stderr).trim()
|
|
127
|
+
}
|
|
128
|
+
} catch (_) {}
|
|
129
|
+
} else if (child.result && typeof child.result === 'object') {
|
|
130
|
+
code = child.result.exitCode ?? code
|
|
131
|
+
if (child.result.stderr && !detail) detail = String(child.result.stderr).trim()
|
|
132
|
+
}
|
|
133
|
+
code = code ?? 0
|
|
134
|
+
whisperError = `whisper server process exited prematurely with code ${code}${detail ? `: ${detail}` : ''}`
|
|
101
135
|
return false
|
|
102
136
|
}
|
|
103
137
|
if (child.status === 'exited' || child.status === 'failed') {
|
|
104
|
-
whisperError = `whisper server process exited prematurely with status ${child.status}`
|
|
138
|
+
whisperError = `whisper server process exited prematurely with status ${child.status}${detail ? `: ${detail}` : ''}`
|
|
105
139
|
return false
|
|
106
140
|
}
|
|
107
141
|
if (child.exitCode !== null && child.exitCode !== undefined) {
|
|
108
|
-
whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
|
|
142
|
+
whisperError = `whisper server process exited prematurely with code ${child.exitCode}${detail ? `: ${detail}` : ''}`
|
|
109
143
|
return false
|
|
110
144
|
}
|
|
111
145
|
}
|
|
@@ -183,16 +217,30 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
183
217
|
})
|
|
184
218
|
}
|
|
185
219
|
if (sensevoiceChild && sensevoiceChild.done && typeof sensevoiceChild.done.then === 'function') {
|
|
186
|
-
sensevoiceChild.done.then((res) => {
|
|
220
|
+
sensevoiceChild.done.then(async (res) => {
|
|
221
|
+
let code = res && res.code
|
|
222
|
+
let detail = ''
|
|
223
|
+
if (typeof sensevoiceChild.result === 'function') {
|
|
224
|
+
try {
|
|
225
|
+
const r = await sensevoiceChild.result()
|
|
226
|
+
if (r) {
|
|
227
|
+
if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
|
|
228
|
+
if (r.stderr) detail = String(r.stderr).trim()
|
|
229
|
+
}
|
|
230
|
+
} catch (_) {}
|
|
231
|
+
} else if (sensevoiceChild.result && typeof sensevoiceChild.result === 'object') {
|
|
232
|
+
code = sensevoiceChild.result.exitCode ?? code
|
|
233
|
+
if (sensevoiceChild.result.stderr) detail = String(sensevoiceChild.result.stderr).trim()
|
|
234
|
+
}
|
|
187
235
|
if (sensevoiceChild.status === 'killed') {
|
|
188
|
-
sensevoiceError = 'sensevoice process was killed'
|
|
236
|
+
sensevoiceError = detail ? `sensevoice process was killed: ${detail}` : 'sensevoice process was killed'
|
|
189
237
|
} else if (sensevoiceChild.status === 'completed') {
|
|
190
|
-
|
|
238
|
+
code = code ?? sensevoiceChild.exitCode ?? 0
|
|
191
239
|
if (code !== 0 && code !== null && code !== undefined) {
|
|
192
|
-
sensevoiceError = `sensevoice process exited with code ${code}`
|
|
240
|
+
sensevoiceError = `sensevoice process exited with code ${code}${detail ? `: ${detail}` : ''}`
|
|
193
241
|
}
|
|
194
|
-
} else if (
|
|
195
|
-
sensevoiceError = `sensevoice process exited with code ${
|
|
242
|
+
} else if (code !== 0 && code !== null && code !== undefined) {
|
|
243
|
+
sensevoiceError = `sensevoice process exited with code ${code}${detail ? `: ${detail}` : ''}`
|
|
196
244
|
}
|
|
197
245
|
}).catch((err) => {
|
|
198
246
|
sensevoiceError = err?.message || String(err)
|
|
@@ -232,16 +232,36 @@ export function registerSensevoiceInstaller(ctx, { liveConfig, isAlive, updateCo
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
export function buildSensevoiceArgs(cfg, port) {
|
|
235
|
-
|
|
236
|
-
const
|
|
235
|
+
let modelPath = String(cfg?.sensevoiceModel || '')
|
|
236
|
+
const modelDir = modelPath ? path.dirname(modelPath) : getDefaultModelDir()
|
|
237
|
+
const tokensPath = (cfg?.sensevoiceTokens && existsSync(cfg.sensevoiceTokens))
|
|
238
|
+
? cfg.sensevoiceTokens
|
|
239
|
+
: findTokensFile(modelDir)
|
|
237
240
|
const tokensArg = tokensPath ? ` --tokens=${JSON.stringify(tokensPath)}` : ''
|
|
238
241
|
const rawProv = String(cfg?.sensevoiceProvider || '').trim().toLowerCase()
|
|
239
|
-
|
|
242
|
+
let normProv = rawProv === 'rknpu' ? 'rknn' : rawProv
|
|
243
|
+
|
|
244
|
+
if (normProv === 'rknn') {
|
|
245
|
+
const ext = path.extname(modelPath).toLowerCase()
|
|
246
|
+
if (ext === '.onnx') {
|
|
247
|
+
const rknnCandidate = path.join(modelDir, path.basename(modelPath, ext) + '.rknn')
|
|
248
|
+
const defaultRknn = path.join(modelDir, 'model.rknn')
|
|
249
|
+
if (existsSync(rknnCandidate)) {
|
|
250
|
+
modelPath = rknnCandidate
|
|
251
|
+
} else if (existsSync(defaultRknn)) {
|
|
252
|
+
modelPath = defaultRknn
|
|
253
|
+
} else {
|
|
254
|
+
// Guard: ordinary ONNX model cannot be loaded by sherpa-onnx RKNN runtime (#221)
|
|
255
|
+
normProv = 'cpu'
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
240
260
|
const provider = (normProv && normProv !== 'cpu')
|
|
241
261
|
? ` --provider=${normProv}`
|
|
242
262
|
: ''
|
|
243
263
|
const threads = Number(cfg?.sensevoiceThreads) || 4
|
|
244
|
-
return ` --sense-voice-model=${JSON.stringify(
|
|
264
|
+
return ` --sense-voice-model=${JSON.stringify(modelPath)}`
|
|
245
265
|
+ tokensArg
|
|
246
266
|
+ ` --port=${port} --num-threads=${threads}`
|
|
247
267
|
+ provider
|
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,
|
|
5
|
+
import { existsSync, readFileSync, unlinkSync, statSync } 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';
|
|
@@ -202,18 +202,19 @@ export function checkProfileLock(profileDir) {
|
|
|
202
202
|
if (isProcessAlive(pid)) {
|
|
203
203
|
return { locked: true, pid };
|
|
204
204
|
}
|
|
205
|
+
// Stale lock: PID is not running.
|
|
205
206
|
// Verify before unlinking: ensure the file content still holds the dead pid we read,
|
|
206
|
-
//
|
|
207
|
+
// without renaming the canonical file away so live successors are never hidden (#205).
|
|
207
208
|
try {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (renamedPid === pid) {
|
|
212
|
-
try { unlinkSync(staleName); } catch {}
|
|
209
|
+
const currentPid = readLockPid(lockPath);
|
|
210
|
+
if (currentPid === pid) {
|
|
211
|
+
unlinkSync(lockPath);
|
|
213
212
|
return { locked: false, cleanedStale: true };
|
|
214
213
|
}
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
if (currentPid !== null && isProcessAlive(currentPid)) {
|
|
215
|
+
return { locked: true, pid: currentPid };
|
|
216
|
+
}
|
|
217
|
+
return { locked: false };
|
|
217
218
|
} catch {
|
|
218
219
|
if (!existsSync(lockPath)) return { locked: false };
|
|
219
220
|
const currentPid = readLockPid(lockPath);
|
|
@@ -223,7 +224,17 @@ export function checkProfileLock(profileDir) {
|
|
|
223
224
|
return { locked: false };
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
+
// Lockfile without valid PID: check age (> 10 min)
|
|
228
|
+
try {
|
|
229
|
+
const stat = statSync(lockPath);
|
|
230
|
+
if (Date.now() - stat.mtimeMs > 10 * 60_000) {
|
|
231
|
+
try { unlinkSync(lockPath); } catch {}
|
|
232
|
+
return { locked: false, cleanedStale: true };
|
|
233
|
+
}
|
|
234
|
+
return { locked: true, pid: null };
|
|
235
|
+
} catch {
|
|
236
|
+
return { locked: false };
|
|
237
|
+
}
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
async function installExact(target, packageSpec, options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.19",
|
|
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",
|