@goodandready/dsh-voice 0.9.18 → 0.9.20

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,28 @@ 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) {
916
+ if (target && typeof actions.captureInsertion === 'function') {
917
+ try { target.span = actions.captureInsertion() } catch (_) {}
918
+ }
919
+ return true
920
+ }
921
+ return false
922
+ } catch (_) {
923
+ return false
924
+ }
902
925
  }
903
926
  const draft = input && typeof input.draft === 'string' ? input.draft : ''
904
927
  const before = draft
905
928
  const sep = (!draft || draft.endsWith('\n') || text.startsWith('\n')) ? '' : ' '
906
929
  const after = draft + sep + text
907
- actions.setDraft(after)
930
+ try {
931
+ actions.setDraft(after)
932
+ } catch (_) {
933
+ return false
934
+ }
908
935
  const limit = Number(voice.settings.historyLimit)
909
936
  if (limit > 0) {
910
937
  insertHistory.push({ before, added: sep + text, after })
@@ -914,10 +941,10 @@ window.__ModuleLoader__.load({
914
941
  }
915
942
 
916
943
  function appendDraft(text, target) {
917
- if (voice.disposed || !text) return
944
+ if (voice.disposed || !text) return false
918
945
  let clean = voice.settings.voiceCommands ? applyVoiceCommands(text) : tidyPhrase(text)
919
- if (!clean) return
920
- insertDraftText(clean, target)
946
+ if (!clean) return false
947
+ return insertDraftText(clean, target)
921
948
  }
922
949
 
923
950
  function announceVoice(phase) {
@@ -1291,7 +1318,7 @@ voice._chainsPromise = null
1291
1318
  }
1292
1319
  }
1293
1320
  if (action === 'send' || action === 'clear' || action === 'newline') {
1294
- executeVoiceAction(action, text)
1321
+ executeVoiceAction(action, text, target)
1295
1322
  return
1296
1323
  }
1297
1324
  if (action === 'cancel') {
@@ -1300,8 +1327,12 @@ voice._chainsPromise = null
1300
1327
  }
1301
1328
  const delay = Number(voice.settings.sendDelayMs) || 0
1302
1329
  if (text && delay > 0 && !voice.holding) {
1303
- appendDraft(text, target)
1304
- voice.set({ phase: 'pending', pending: { text, undoOnly: true, leftMs: delay } })
1330
+ const ok = appendDraft(text, target)
1331
+ if (ok !== false) {
1332
+ voice.set({ phase: 'pending', pending: { text, target, undoOnly: true, leftMs: delay } })
1333
+ } else {
1334
+ voice.set({ phase: 'idle' })
1335
+ }
1305
1336
  return
1306
1337
  }
1307
1338
  if (text) appendDraft(text, target)
@@ -1464,8 +1495,12 @@ voice._chainsPromise = null
1464
1495
  voice.browserFinals = null
1465
1496
  if (!said) { voice.set({ phase: 'idle' }); return }
1466
1497
  if (mode === 'message') {
1467
- appendDraft(said, target)
1468
- voice.set({ phase: 'pending', pending: { text: said, leftMs: voice.settings.autoSendMs } })
1498
+ const ok = appendDraft(said, target)
1499
+ if (ok !== false) {
1500
+ voice.set({ phase: 'pending', pending: { text: said, target, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs } })
1501
+ } else {
1502
+ voice.set({ phase: 'idle' })
1503
+ }
1469
1504
  } else {
1470
1505
  voice.set({ phase: 'idle' })
1471
1506
  }
@@ -1525,14 +1560,18 @@ voice._chainsPromise = null
1525
1560
  }
1526
1561
  }
1527
1562
  if (action === 'send' || action === 'clear' || action === 'newline') {
1528
- executeVoiceAction(action, text)
1563
+ executeVoiceAction(action, text, target)
1529
1564
  return
1530
1565
  }
1531
1566
  if (action === 'cancel') { cancelCurrent(); return }
1532
- appendDraft(text, target)
1567
+ const ok = appendDraft(text, target)
1533
1568
  if (voice.lastNote) voice.lastNote.text = text
1534
1569
  if (mode === 'message') {
1535
- voice.set({ phase: 'pending', pending: { text, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs, audioUrl: noteUrl } })
1570
+ if (ok !== false) {
1571
+ voice.set({ phase: 'pending', pending: { text, target, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs, audioUrl: noteUrl } })
1572
+ } else {
1573
+ voice.set({ phase: 'idle' })
1574
+ }
1536
1575
  } else {
1537
1576
  voice.set({ phase: 'idle' })
1538
1577
  }
@@ -1549,26 +1588,29 @@ voice._chainsPromise = null
1549
1588
  }
1550
1589
  })
1551
1590
  }
1552
- function clearDraft() {
1553
- const actions = voice.inputActions
1591
+ function clearDraft(target) {
1592
+ const activeTarget = target || voice._activeTarget
1593
+ const actions = (activeTarget && activeTarget.actions) || voice.inputActions
1554
1594
  if (actions && typeof actions.setDraft === 'function') actions.setDraft('')
1555
1595
  }
1556
1596
 
1557
1597
  // Voice actions (#29-7): spoken triggers perform composer actions.
1558
- function executeVoiceAction(action, rawText) {
1598
+ function executeVoiceAction(action, rawText, target) {
1599
+ const activeTarget = target || voice._activeTarget
1600
+ const actions = (activeTarget && activeTarget.actions) || voice.inputActions
1559
1601
  if (action === 'send') {
1560
1602
  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()
1603
+ if (text) appendDraft(text, activeTarget)
1604
+ if (typeof actions?.submit === 'function') {
1605
+ actions.submit()
1564
1606
  }
1565
1607
  voice.set({ phase: 'idle' })
1566
1608
  } else if (action === 'clear') {
1567
- clearDraft()
1609
+ clearDraft(activeTarget)
1568
1610
  voice.set({ phase: 'idle' })
1569
1611
  } else if (action === 'newline') {
1570
1612
  const payload = (rawText.replace(/(^|[\s,.!?])(с новой строки|новая строка|абзац|new line|paragraph)([\s,.!?]|$)/gi, '').trim())
1571
- appendDraft(payload)
1613
+ appendDraft(payload, activeTarget)
1572
1614
  voice.set({ phase: 'idle' })
1573
1615
  }
1574
1616
  }
@@ -1577,7 +1619,8 @@ voice._chainsPromise = null
1577
1619
  const pending = voice.pending
1578
1620
  if (!pending) return
1579
1621
  const text = pending.text
1580
- const actions = voice.inputActions
1622
+ const target = pending.target || voice._activeTarget
1623
+ const actions = (target && target.actions) || voice.inputActions
1581
1624
  voice.pending = null
1582
1625
  voice.set({ phase: 'idle' })
1583
1626
 
@@ -3935,6 +3978,7 @@ voice._chainsPromise = null
3935
3978
  startRecording,
3936
3979
  cancelCurrent,
3937
3980
  stopCurrent,
3981
+ submitPending,
3938
3982
  loadWebGpuWhisper,
3939
3983
  }
3940
3984
  return module.exports
@@ -65,16 +65,45 @@ 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.readOutput === 'function') {
72
+ try {
73
+ const out = child.readOutput()
74
+ if (out && out.stderr) detail = String(out.stderr).trim()
75
+ else if (out && out.stdout) detail = String(out.stdout).trim()
76
+ } catch (_) {}
77
+ } else if (typeof child.read === 'function') {
78
+ try {
79
+ const out = child.read()
80
+ if (out && out.stderr) detail = String(out.stderr).trim()
81
+ } catch (_) {}
82
+ }
83
+ if (typeof child.result === 'function') {
84
+ try {
85
+ const r = await child.result()
86
+ if (r) {
87
+ if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
88
+ if (r.stderr && !detail) detail = String(r.stderr).trim()
89
+ }
90
+ } catch (err) {
91
+ const msg = err && err.message ? err.message : String(err)
92
+ if (msg && !detail) detail = msg.trim()
93
+ }
94
+ } else if (child.result && typeof child.result === 'object') {
95
+ code = child.result.exitCode ?? code
96
+ if (child.result.stderr && !detail) detail = String(child.result.stderr).trim()
97
+ }
69
98
  if (child.status === 'killed') {
70
- whisperError = 'whisper server process was killed'
99
+ whisperError = detail ? `whisper server process was killed: ${detail}` : 'whisper server process was killed'
71
100
  } else if (child.status === 'completed') {
72
- const code = child.result?.exitCode ?? (res && res.code)
101
+ code = code ?? child.exitCode ?? 0
73
102
  if (code !== 0 && code !== null && code !== undefined) {
74
- whisperError = `whisper server process exited with code ${code}`
103
+ whisperError = `whisper server process exited with code ${code}${detail ? `: ${detail}` : ''}`
75
104
  }
76
- } else if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
77
- whisperError = `whisper server process exited with code ${res.code}`
105
+ } else if (code !== 0 && code !== null && code !== undefined) {
106
+ whisperError = `whisper server process exited with code ${code}${detail ? `: ${detail}` : ''}`
78
107
  }
79
108
  }).catch((err) => {
80
109
  whisperError = err?.message || String(err)
@@ -91,21 +120,50 @@ export function createLocalDaemons({ live, ctx }) {
91
120
  return true
92
121
  }
93
122
  if (child) {
123
+ let detail = ''
124
+ if (typeof child.readOutput === 'function') {
125
+ try {
126
+ const out = child.readOutput()
127
+ if (out && out.stderr) detail = String(out.stderr).trim()
128
+ else if (out && out.stdout) detail = String(out.stdout).trim()
129
+ } catch (_) {}
130
+ } else if (typeof child.read === 'function') {
131
+ try {
132
+ const out = child.read()
133
+ if (out && out.stderr) detail = String(out.stderr).trim()
134
+ } catch (_) {}
135
+ }
136
+ let code = child.exitCode
137
+ if (typeof child.result === 'function') {
138
+ try {
139
+ const r = await child.result()
140
+ if (r) {
141
+ if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
142
+ if (r.stderr && !detail) detail = String(r.stderr).trim()
143
+ }
144
+ } catch (err) {
145
+ const msg = err && err.message ? err.message : String(err)
146
+ if (msg && !detail) detail = msg.trim()
147
+ }
148
+ } else if (child.result && typeof child.result === 'object') {
149
+ code = child.result.exitCode ?? code
150
+ if (child.result.stderr && !detail) detail = String(child.result.stderr).trim()
151
+ }
94
152
  if (child.status === 'killed') {
95
- whisperError = 'whisper server process was killed'
153
+ whisperError = detail ? `whisper server process was killed: ${detail}` : 'whisper server process was killed'
96
154
  return false
97
155
  }
98
156
  if (child.status === 'completed') {
99
- const code = child.result?.exitCode ?? child.exitCode ?? 0
100
- whisperError = `whisper server process exited prematurely with code ${code}`
157
+ code = code ?? 0
158
+ whisperError = `whisper server process exited prematurely with code ${code}${detail ? `: ${detail}` : ''}`
101
159
  return false
102
160
  }
103
161
  if (child.status === 'exited' || child.status === 'failed') {
104
- whisperError = `whisper server process exited prematurely with status ${child.status}`
162
+ whisperError = `whisper server process exited prematurely with status ${child.status}${detail ? `: ${detail}` : ''}`
105
163
  return false
106
164
  }
107
165
  if (child.exitCode !== null && child.exitCode !== undefined) {
108
- whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
166
+ whisperError = `whisper server process exited prematurely with code ${child.exitCode}${detail ? `: ${detail}` : ''}`
109
167
  return false
110
168
  }
111
169
  }
@@ -183,16 +241,30 @@ export function createLocalDaemons({ live, ctx }) {
183
241
  })
184
242
  }
185
243
  if (sensevoiceChild && sensevoiceChild.done && typeof sensevoiceChild.done.then === 'function') {
186
- sensevoiceChild.done.then((res) => {
244
+ sensevoiceChild.done.then(async (res) => {
245
+ let code = res && res.code
246
+ let detail = ''
247
+ if (typeof sensevoiceChild.result === 'function') {
248
+ try {
249
+ const r = await sensevoiceChild.result()
250
+ if (r) {
251
+ if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
252
+ if (r.stderr) detail = String(r.stderr).trim()
253
+ }
254
+ } catch (_) {}
255
+ } else if (sensevoiceChild.result && typeof sensevoiceChild.result === 'object') {
256
+ code = sensevoiceChild.result.exitCode ?? code
257
+ if (sensevoiceChild.result.stderr) detail = String(sensevoiceChild.result.stderr).trim()
258
+ }
187
259
  if (sensevoiceChild.status === 'killed') {
188
- sensevoiceError = 'sensevoice process was killed'
260
+ sensevoiceError = detail ? `sensevoice process was killed: ${detail}` : 'sensevoice process was killed'
189
261
  } else if (sensevoiceChild.status === 'completed') {
190
- const code = sensevoiceChild.result?.exitCode ?? (res && res.code)
262
+ code = code ?? sensevoiceChild.exitCode ?? 0
191
263
  if (code !== 0 && code !== null && code !== undefined) {
192
- sensevoiceError = `sensevoice process exited with code ${code}`
264
+ sensevoiceError = `sensevoice process exited with code ${code}${detail ? `: ${detail}` : ''}`
193
265
  }
194
- } else if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
195
- sensevoiceError = `sensevoice process exited with code ${res.code}`
266
+ } else if (code !== 0 && code !== null && code !== undefined) {
267
+ sensevoiceError = `sensevoice process exited with code ${code}${detail ? `: ${detail}` : ''}`
196
268
  }
197
269
  }).catch((err) => {
198
270
  sensevoiceError = err?.message || String(err)
@@ -209,21 +281,50 @@ export function createLocalDaemons({ live, ctx }) {
209
281
  return true
210
282
  }
211
283
  if (sensevoiceChild) {
284
+ let detail = ''
285
+ if (typeof sensevoiceChild.readOutput === 'function') {
286
+ try {
287
+ const out = sensevoiceChild.readOutput()
288
+ if (out && out.stderr) detail = String(out.stderr).trim()
289
+ else if (out && out.stdout) detail = String(out.stdout).trim()
290
+ } catch (_) {}
291
+ } else if (typeof sensevoiceChild.read === 'function') {
292
+ try {
293
+ const out = sensevoiceChild.read()
294
+ if (out && out.stderr) detail = String(out.stderr).trim()
295
+ } catch (_) {}
296
+ }
297
+ let code = sensevoiceChild.exitCode
298
+ if (typeof sensevoiceChild.result === 'function') {
299
+ try {
300
+ const r = await sensevoiceChild.result()
301
+ if (r) {
302
+ if (r.exitCode !== undefined && r.exitCode !== null) code = r.exitCode
303
+ if (r.stderr && !detail) detail = String(r.stderr).trim()
304
+ }
305
+ } catch (err) {
306
+ const msg = err && err.message ? err.message : String(err)
307
+ if (msg && !detail) detail = msg.trim()
308
+ }
309
+ } else if (sensevoiceChild.result && typeof sensevoiceChild.result === 'object') {
310
+ code = sensevoiceChild.result.exitCode ?? code
311
+ if (sensevoiceChild.result.stderr && !detail) detail = String(sensevoiceChild.result.stderr).trim()
312
+ }
212
313
  if (sensevoiceChild.status === 'killed') {
213
- sensevoiceError = 'sensevoice process was killed'
314
+ sensevoiceError = detail ? `sensevoice process was killed: ${detail}` : 'sensevoice process was killed'
214
315
  return false
215
316
  }
216
317
  if (sensevoiceChild.status === 'completed') {
217
- const code = sensevoiceChild.result?.exitCode ?? sensevoiceChild.exitCode ?? 0
218
- sensevoiceError = `sensevoice process exited prematurely with code ${code}`
318
+ code = code ?? 0
319
+ sensevoiceError = `sensevoice process exited prematurely with code ${code}${detail ? `: ${detail}` : ''}`
219
320
  return false
220
321
  }
221
322
  if (sensevoiceChild.status === 'exited' || sensevoiceChild.status === 'failed') {
222
- sensevoiceError = `sensevoice process exited prematurely with status ${sensevoiceChild.status}`
323
+ sensevoiceError = `sensevoice process exited prematurely with status ${sensevoiceChild.status}${detail ? `: ${detail}` : ''}`
223
324
  return false
224
325
  }
225
326
  if (sensevoiceChild.exitCode !== null && sensevoiceChild.exitCode !== undefined) {
226
- sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
327
+ sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}${detail ? `: ${detail}` : ''}`
227
328
  return false
228
329
  }
229
330
  }
@@ -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,21 @@ export function checkProfileLock(profileDir) {
202
202
  if (isProcessAlive(pid)) {
203
203
  return { locked: true, pid };
204
204
  }
205
- // 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).
205
+ // Stale lock: PID is not running.
206
+ // Verify before unlinking: re-check PID to ensure a concurrent live successor
207
+ // was not written during the check (#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 p1 = readLockPid(lockPath);
210
+ if (p1 !== null && isProcessAlive(p1)) return { locked: true, pid: p1 };
211
+ const p2 = readLockPid(lockPath);
212
+ if (p2 !== null && isProcessAlive(p2)) return { locked: true, pid: p2 };
213
+ if (p1 === pid && p2 === pid) {
214
+ unlinkSync(lockPath);
213
215
  return { locked: false, cleanedStale: true };
214
216
  }
215
- try { renameSync(staleName, lockPath); } catch {}
216
- return { locked: true, pid: renamedPid };
217
+ if (p2 !== null && isProcessAlive(p2)) return { locked: true, pid: p2 };
218
+ if (p1 !== null && isProcessAlive(p1)) return { locked: true, pid: p1 };
219
+ return { locked: false };
217
220
  } catch {
218
221
  if (!existsSync(lockPath)) return { locked: false };
219
222
  const currentPid = readLockPid(lockPath);
@@ -223,7 +226,17 @@ export function checkProfileLock(profileDir) {
223
226
  return { locked: false };
224
227
  }
225
228
  }
226
- return { locked: true, pid: null };
229
+ // Lockfile without valid PID: check age (> 10 min)
230
+ try {
231
+ const stat = statSync(lockPath);
232
+ if (Date.now() - stat.mtimeMs > 10 * 60_000) {
233
+ try { unlinkSync(lockPath); } catch {}
234
+ return { locked: false, cleanedStale: true };
235
+ }
236
+ return { locked: true, pid: null };
237
+ } catch {
238
+ return { locked: false };
239
+ }
227
240
  }
228
241
 
229
242
  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.20",
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",