@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.
- package/lib/client-src/40-recording.js +3 -0
- package/lib/client-src/70-settings.js +9 -2
- package/lib/client.js +12 -2
- package/lib/index.js +7 -3
- package/lib/providers.js +42 -11
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
126
|
-
|
|
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(
|
|
146
|
-
|
|
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(
|
|
162
|
-
|
|
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
|
-
|
|
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(
|
|
290
|
-
const data = await res
|
|
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(
|
|
323
|
-
const data = await res
|
|
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.
|
|
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",
|