@goodandready/dsh-voice 0.8.20 → 0.8.22

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.
@@ -1,5 +1,8 @@
1
1
  // to use browser recognition or record a file.
2
2
  let chainsPromise = null
3
+ if (typeof window !== 'undefined') {
4
+ window.addEventListener('dsh-voice:settings-saved', () => { chainsPromise = null })
5
+ }
3
6
  function modeChain(mode) {
4
7
  if (!chainsPromise) {
5
8
  chainsPromise = fetch('/dsh-voice/status', { cache: 'no-store' })
@@ -274,7 +274,14 @@
274
274
  return () => clearInterval(timer)
275
275
  }, [ready])
276
276
 
277
- React.useEffect(() => { if (ready && draft === null) setDraft(JSON.parse(JSON.stringify(value))) }, [ready, draft, value])
277
+ const deepClone = (obj) => {
278
+ if (typeof structuredClone === 'function') {
279
+ try { return structuredClone(obj) } catch (e) { /* fallback */ }
280
+ }
281
+ try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
282
+ }
283
+
284
+ React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
278
285
 
279
286
  React.useEffect(() => {
280
287
  if (!ready) return
@@ -335,7 +342,7 @@
335
342
  }
336
343
 
337
344
  const setIn = (mode, key, v) => setDraft((d) => {
338
- const next = JSON.parse(JSON.stringify(d || {}))
345
+ const next = deepClone(d || {})
339
346
  next[mode] = next[mode] || {}
340
347
  next[mode][key] = v
341
348
  return next
package/lib/client.js CHANGED
@@ -509,6 +509,9 @@ window.__ModuleLoader__.load({
509
509
  // Fetch the mode chain from the host once. Only needed to decide whether
510
510
  // to use browser recognition or record a file.
511
511
  let chainsPromise = null
512
+ if (typeof window !== 'undefined') {
513
+ window.addEventListener('dsh-voice:settings-saved', () => { chainsPromise = null })
514
+ }
512
515
  function modeChain(mode) {
513
516
  if (!chainsPromise) {
514
517
  chainsPromise = fetch('/dsh-voice/status', { cache: 'no-store' })
@@ -1603,7 +1606,14 @@ window.__ModuleLoader__.load({
1603
1606
  return () => clearInterval(timer)
1604
1607
  }, [ready])
1605
1608
 
1606
- React.useEffect(() => { if (ready && draft === null) setDraft(JSON.parse(JSON.stringify(value))) }, [ready, draft, value])
1609
+ const deepClone = (obj) => {
1610
+ if (typeof structuredClone === 'function') {
1611
+ try { return structuredClone(obj) } catch (e) { /* fallback */ }
1612
+ }
1613
+ try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
1614
+ }
1615
+
1616
+ React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
1607
1617
 
1608
1618
  React.useEffect(() => {
1609
1619
  if (!ready) return
@@ -1664,7 +1674,7 @@ window.__ModuleLoader__.load({
1664
1674
  }
1665
1675
 
1666
1676
  const setIn = (mode, key, v) => setDraft((d) => {
1667
- const next = JSON.parse(JSON.stringify(d || {}))
1677
+ const next = deepClone(d || {})
1668
1678
  next[mode] = next[mode] || {}
1669
1679
  next[mode][key] = v
1670
1680
  return next
package/lib/index.js CHANGED
@@ -213,7 +213,10 @@ export function apply(ctx, baseConfig) {
213
213
  try {
214
214
  const controller = new AbortController()
215
215
  const t = setTimeout(() => controller.abort(), 2000)
216
- const res = await fetch(live().whisperUrl.split('/inference')[0] + '/', { signal: controller.signal })
216
+ const rawUrl = String(live().whisperUrl || '').trim()
217
+ const base = rawUrl.includes('/inference') ? rawUrl.split('/inference')[0] : rawUrl.replace(/\/+$/, '')
218
+ if (!base) return false
219
+ const res = await fetch(base + '/', { signal: controller.signal })
217
220
  clearTimeout(t)
218
221
  return res.ok
219
222
  } catch { return false }
@@ -266,7 +269,8 @@ export function apply(ctx, baseConfig) {
266
269
  if (!cfg.sensevoiceModel) return false
267
270
  if (await sensevoiceAlive()) return true
268
271
  try {
269
- const port = new URL(cfg.sensevoiceUrl).port || '6006'
272
+ let port = '6006'
273
+ try { port = new URL(cfg.sensevoiceUrl).port || '6006' } catch { /* invalid URL */ }
270
274
  const spec = ctx.shell.resolve({
271
275
  command: `${JSON.stringify(cfg.sensevoiceBin)}`
272
276
  + ` --sense-voice-model=${JSON.stringify(cfg.sensevoiceModel)}`
@@ -533,7 +537,7 @@ export function apply(ctx, baseConfig) {
533
537
  + 'Use for voice messages, recordings, interviews.',
534
538
  parameters: {
535
539
  file_path: { type: 'string', required: true, description: 'Absolute path to the audio file (wav, mp3, m4a, ogg, flac, webm).' },
536
- language: { type: 'string', description: `Recognition language code. Default: ${baseConfig.message.language}.` },
540
+ language: { type: 'string', description: `Recognition language code. Default: ${(baseConfig && baseConfig.message && baseConfig.message.language) || 'ru'}.` },
537
541
  },
538
542
  output: {
539
543
  schema: {
package/lib/providers.js CHANGED
@@ -102,6 +102,25 @@ function isAutoLang(lang) {
102
102
  return !lang || lang === 'auto' || String(lang).includes(',')
103
103
  }
104
104
 
105
+ async function readErrorDetail(res, defaultLabel) {
106
+ let detail = `HTTP ${res.status}`
107
+ try {
108
+ const e = await res.json()
109
+ if (e?.error) detail = typeof e.error === 'string' ? e.error : (e.error.message || JSON.stringify(e.error))
110
+ else if (e?.message) detail = e.message
111
+ else if (e?.err_msg) detail = e.err_msg
112
+ } catch { /* not json */ }
113
+ return `${defaultLabel} ${detail}`
114
+ }
115
+
116
+ async function safeJson(res, defaultLabel) {
117
+ try {
118
+ return await res.json()
119
+ } catch {
120
+ throw new Error(`${defaultLabel}: invalid JSON response`)
121
+ }
122
+ }
123
+
105
124
  export function makeProviders(deps, req) {
106
125
  const { resolveKey, fetchImpl, cfg } = deps
107
126
  const { bytes, mime, lang, signal, models } = req
@@ -122,8 +141,11 @@ export function makeProviders(deps, req) {
122
141
  body: bytes,
123
142
  signal,
124
143
  })
125
- if (!res.ok) throw new Error(`Deepgram HTTP ${res.status}`)
126
- const data = await res.json()
144
+ if (!res.ok) throw new Error(await readErrorDetail(res, 'Deepgram'))
145
+ let data
146
+ try { data = await safeJson(res, 'Deepgram') } catch (e) {
147
+ return { ok: false, provider: 'deepgram', reason: e.message }
148
+ }
127
149
  const text = (data?.results?.channels?.[0]?.alternatives?.[0]?.transcript || '').trim()
128
150
  return { ok: text.length > 0, provider: 'deepgram', text, reason: text ? '' : 'empty transcript' }
129
151
  }
@@ -142,8 +164,11 @@ export function makeProviders(deps, req) {
142
164
  body: form,
143
165
  signal,
144
166
  })
145
- if (!res.ok) throw new Error(`Groq HTTP ${res.status}`)
146
- const data = await res.json()
167
+ if (!res.ok) throw new Error(await readErrorDetail(res, 'Groq'))
168
+ let data
169
+ try { data = await safeJson(res, 'Groq') } catch (e) {
170
+ return { ok: false, provider: 'groq', reason: e.message }
171
+ }
147
172
  const text = (data?.text || '').trim()
148
173
  return { ok: text.length > 0, provider: 'groq', text, reason: text ? '' : 'empty transcript' }
149
174
  }
@@ -158,8 +183,11 @@ export function makeProviders(deps, req) {
158
183
  body: bytes,
159
184
  signal,
160
185
  })
161
- if (!res.ok) throw new Error(`HF HTTP ${res.status}`)
162
- const data = await res.json()
186
+ if (!res.ok) throw new Error(await readErrorDetail(res, 'HF'))
187
+ let data
188
+ try { data = await safeJson(res, 'HF') } catch (e) {
189
+ return { ok: false, provider: 'hf', reason: e.message }
190
+ }
163
191
  const text = (data?.text || '').trim()
164
192
  return { ok: text.length > 0, provider: 'hf', text, reason: text ? '' : 'empty transcript' }
165
193
  }
@@ -199,7 +227,10 @@ export function makeProviders(deps, req) {
199
227
  try { const e = await res.json(); if (e?.error) detail = e.error } catch { /* body is not json */ }
200
228
  return { ok: false, provider: 'local-whisper', reason: `local whisper: ${detail}` }
201
229
  }
202
- const data = await res.json()
230
+ let data
231
+ try { data = await safeJson(res, 'local whisper') } catch (e) {
232
+ return { ok: false, provider: 'local-whisper', reason: e.message }
233
+ }
203
234
  const text = (data?.text || '').trim()
204
235
  return { ok: text.length > 0, provider: 'local-whisper', text, reason: text ? '' : 'empty transcript' }
205
236
  }
@@ -286,8 +317,8 @@ export function makeProviders(deps, req) {
286
317
  const res = await fetchImpl(`${base}/audio/transcriptions`, {
287
318
  method: 'POST', headers, body: form, signal,
288
319
  })
289
- if (!res.ok) throw new Error(`${label} HTTP ${res.status}`)
290
- const data = await res.json()
320
+ if (!res.ok) throw new Error(await readErrorDetail(res, label))
321
+ const data = await safeJson(res, label)
291
322
  return (data?.text || '').trim()
292
323
  }
293
324
 
@@ -319,8 +350,8 @@ export function makeProviders(deps, req) {
319
350
  }),
320
351
  signal,
321
352
  })
322
- if (!res.ok) throw new Error(`${label} HTTP ${res.status}`)
323
- const data = await res.json()
353
+ if (!res.ok) throw new Error(await readErrorDetail(res, label))
354
+ const data = await safeJson(res, label)
324
355
  return String(data?.choices?.[0]?.message?.content || '').trim()
325
356
  }
326
357
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
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",