@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 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 { span.insert(text); return true } catch (_) {}
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 { span.insertText(text); return true } catch (_) {}
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 { span(text); return true } catch (_) {}
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 { actions.insertText(text); return true } catch (_) {}
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
- actions.setDraft(after)
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 actions = voice.inputActions
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 voice.inputActions?.submit === 'function') {
1563
- voice.inputActions.submit()
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 actions = voice.inputActions
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
@@ -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
- const code = child.result?.exitCode ?? (res && res.code)
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 (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
77
- whisperError = `whisper server process exited with code ${res.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
- const code = child.result?.exitCode ?? child.exitCode ?? 0
100
- whisperError = `whisper server process exited prematurely with code ${code}`
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
- const code = sensevoiceChild.result?.exitCode ?? (res && res.code)
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 (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
195
- sensevoiceError = `sensevoice process exited with code ${res.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
- const modelDir = cfg?.sensevoiceModel ? path.dirname(cfg.sensevoiceModel) : getDefaultModelDir()
236
- const tokensPath = findTokensFile(modelDir)
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
- const normProv = rawProv === 'rknpu' ? 'rknn' : rawProv
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(cfg?.sensevoiceModel || '')}`
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, renameSync } from 'node:fs';
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
- // using atomic rename coordinator so a successor's live lock is never deleted (#205).
207
+ // without renaming the canonical file away so live successors are never hidden (#205).
207
208
  try {
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 {}
209
+ const currentPid = readLockPid(lockPath);
210
+ if (currentPid === pid) {
211
+ unlinkSync(lockPath);
213
212
  return { locked: false, cleanedStale: true };
214
213
  }
215
- try { renameSync(staleName, lockPath); } catch {}
216
- return { locked: true, pid: renamedPid };
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
- return { locked: true, pid: null };
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.18",
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",