@goodandready/dsh-voice 0.8.36 → 0.8.38
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 +16 -6
- package/lib/config-compat.js +43 -0
- package/lib/index.js +90 -18
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -2166,7 +2166,10 @@ window.__ModuleLoader__.load({
|
|
|
2166
2166
|
// writes are dropped. The server does not share that restriction, and
|
|
2167
2167
|
// dsh-lanmode rebuilds the same mechanics on the same calls. Prefer its
|
|
2168
2168
|
// service when installed, otherwise the kernel one (fine on loopback).
|
|
2169
|
-
const
|
|
2169
|
+
const settingsService = (ctx.get && ctx.get('lanSettings')) || ctx.configForms || ctx.settingsScope
|
|
2170
|
+
const scope = settingsService && typeof settingsService.get === 'function'
|
|
2171
|
+
? settingsService.get(NS)
|
|
2172
|
+
: (settingsService && typeof settingsService.bind === 'function' ? settingsService.bind({ namespace: NS }) : null)
|
|
2170
2173
|
const [snap, setSnap] = React.useState(null)
|
|
2171
2174
|
const [draft, setDraft] = React.useState(null)
|
|
2172
2175
|
const [saved, setSaved] = React.useState(false)
|
|
@@ -2190,11 +2193,11 @@ window.__ModuleLoader__.load({
|
|
|
2190
2193
|
|
|
2191
2194
|
React.useEffect(() => {
|
|
2192
2195
|
let alive = true
|
|
2193
|
-
const render = () => { if (alive) setSnap(scope.getSnapshot()) }
|
|
2196
|
+
const render = () => { if (alive && scope && typeof scope.getSnapshot === 'function') setSnap(scope.getSnapshot()) }
|
|
2194
2197
|
render()
|
|
2195
|
-
const off = scope.subscribe(render)
|
|
2198
|
+
const off = (scope && typeof scope.subscribe === 'function') ? scope.subscribe(render) : () => {}
|
|
2196
2199
|
return () => { alive = false; off() }
|
|
2197
|
-
}, [])
|
|
2200
|
+
}, [scope])
|
|
2198
2201
|
|
|
2199
2202
|
const [devices, setDevices] = React.useState([])
|
|
2200
2203
|
React.useEffect(() => {
|
|
@@ -2214,7 +2217,13 @@ window.__ModuleLoader__.load({
|
|
|
2214
2217
|
const timer = setInterval(() => {
|
|
2215
2218
|
if (tries >= 15) { clearInterval(timer); return }
|
|
2216
2219
|
tries += 1
|
|
2217
|
-
try {
|
|
2220
|
+
try {
|
|
2221
|
+
if (ctx.configForms && typeof ctx.configForms.describe === 'function') {
|
|
2222
|
+
ctx.configForms.describe().load()
|
|
2223
|
+
} else if (ctx.settingsScope && typeof ctx.settingsScope.describe === 'function') {
|
|
2224
|
+
ctx.settingsScope.describe().load()
|
|
2225
|
+
}
|
|
2226
|
+
} catch (e) { /* service not up yet */ }
|
|
2218
2227
|
}, 1000)
|
|
2219
2228
|
return () => clearInterval(timer)
|
|
2220
2229
|
}, [ready])
|
|
@@ -3125,7 +3134,7 @@ window.__ModuleLoader__.load({
|
|
|
3125
3134
|
},
|
|
3126
3135
|
PluginCard,
|
|
3127
3136
|
))
|
|
3128
|
-
// Row seat
|
|
3137
|
+
// Row seat (plugins.row.config): rendered in the plugin row in Plugins table.
|
|
3129
3138
|
ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
|
|
3130
3139
|
{
|
|
3131
3140
|
name: 'plugins.row.config',
|
|
@@ -3135,6 +3144,7 @@ window.__ModuleLoader__.load({
|
|
|
3135
3144
|
},
|
|
3136
3145
|
PluginCard,
|
|
3137
3146
|
))
|
|
3147
|
+
// Legacy seat fallback
|
|
3138
3148
|
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
3139
3149
|
{
|
|
3140
3150
|
name: 'settings.plugin.item',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Decode modern volatile Config references while retaining stable identities for
|
|
2
|
+
// the request hot path. Legacy cores pass ordinary resolved configuration.
|
|
3
|
+
// If ctx is provided, invalidation listeners cache the unwrapped tree so the hot
|
|
4
|
+
// path never recursively walks volatile nodes on unchanged configuration.
|
|
5
|
+
export function createConfigReader(config, ctx) {
|
|
6
|
+
let previous
|
|
7
|
+
let dirty = true
|
|
8
|
+
if (ctx && typeof ctx.on === 'function') {
|
|
9
|
+
const markDirty = () => { dirty = true }
|
|
10
|
+
if (typeof ctx.effect === 'function') {
|
|
11
|
+
ctx.effect(() => ctx.on('settings/document-updated', markDirty))
|
|
12
|
+
ctx.effect(() => ctx.on('loader/volatile-update', markDirty))
|
|
13
|
+
ctx.effect(() => ctx.on('config', markDirty))
|
|
14
|
+
} else {
|
|
15
|
+
ctx.on('settings/document-updated', markDirty)
|
|
16
|
+
ctx.on('loader/volatile-update', markDirty)
|
|
17
|
+
ctx.on('config', markDirty)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const read = (value, held) => {
|
|
22
|
+
if (value && typeof value.get === 'function') value = value.get()
|
|
23
|
+
if (!value || typeof value !== 'object') return value
|
|
24
|
+
const keys = Object.keys(value)
|
|
25
|
+
const next = Array.isArray(value) ? [] : {}
|
|
26
|
+
let same = held && typeof held === 'object' && Array.isArray(value) === Array.isArray(held)
|
|
27
|
+
&& keys.length === Object.keys(held).length
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
Object.defineProperty(next, key, { value: read(value[key], held?.[key]), writable: true, enumerable: true, configurable: true })
|
|
30
|
+
if (!held || !Object.hasOwn(held, key) || !Object.is(next[key], held[key])) same = false
|
|
31
|
+
}
|
|
32
|
+
return same ? held : next
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
if (ctx && !dirty && previous !== undefined) {
|
|
37
|
+
return previous
|
|
38
|
+
}
|
|
39
|
+
previous = read(config, previous)
|
|
40
|
+
dirty = false
|
|
41
|
+
return previous
|
|
42
|
+
}
|
|
43
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
|
28
28
|
import { buildProviderOrder, sessionCommand } from './transcribe-core.js'
|
|
29
29
|
import { registerPluginUpdater } from './updater.js'
|
|
30
30
|
import { registerSensevoiceInstaller } from './sensevoice-installer.js'
|
|
31
|
+
import { createConfigReader } from './config-compat.js'
|
|
31
32
|
|
|
32
33
|
function isAutoLang(lang) {
|
|
33
34
|
return !lang || lang === 'auto' || String(lang).includes(',')
|
|
@@ -65,7 +66,21 @@ const CustomProvider = z.object({
|
|
|
65
66
|
+ 'Empty means the built-in one.'),
|
|
66
67
|
})
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
function makeVolatile(schema) {
|
|
70
|
+
if (!schema || typeof schema.volatile !== 'function') return schema
|
|
71
|
+
if (schema.type === 'object' && schema.dict) {
|
|
72
|
+
const dict = {}
|
|
73
|
+
for (const [k, v] of Object.entries(schema.dict)) {
|
|
74
|
+
dict[k] = makeVolatile(v)
|
|
75
|
+
}
|
|
76
|
+
const res = z.object(dict)
|
|
77
|
+
if (schema.meta) Object.assign(res.meta, schema.meta)
|
|
78
|
+
return res
|
|
79
|
+
}
|
|
80
|
+
return schema.volatile()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const BaseConfig = z.object({
|
|
69
84
|
dictation: z.object({
|
|
70
85
|
chain: z.array(ChainEntry)
|
|
71
86
|
.default([{ provider: 'deepgram', model: '' }, { provider: 'groq', model: '' }, { provider: 'local-whisper', model: '' }])
|
|
@@ -181,6 +196,10 @@ export const Config = z.object({
|
|
|
181
196
|
.description('Target voice dictation and numeric choices to active DSH structured prompt modals.'),
|
|
182
197
|
})
|
|
183
198
|
|
|
199
|
+
export const Config = typeof z.number().volatile === 'function'
|
|
200
|
+
? makeVolatile(BaseConfig)
|
|
201
|
+
: BaseConfig
|
|
202
|
+
|
|
184
203
|
const MIME_BY_EXT = {
|
|
185
204
|
'.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.m4a': 'audio/mp4', '.mp4': 'audio/mp4',
|
|
186
205
|
'.ogg': 'audio/ogg', '.oga': 'audio/ogg', '.flac': 'audio/flac', '.webm': 'audio/webm', '.aac': 'audio/aac',
|
|
@@ -190,19 +209,13 @@ const MIME_BY_EXT = {
|
|
|
190
209
|
export function apply(ctx, baseConfig) {
|
|
191
210
|
let child = null
|
|
192
211
|
|
|
193
|
-
// The settings card edits a namespace named after the plugin.
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
212
|
+
// The settings card edits a namespace named after the plugin.
|
|
213
|
+
// Using createConfigReader decodes volatile nodes on modern DSH cores
|
|
214
|
+
// while caching unwrapped values across requests.
|
|
215
|
+
const readEntryConfig = createConfigReader(baseConfig, ctx)
|
|
197
216
|
let scope = null
|
|
198
|
-
let getConfig =
|
|
199
|
-
const live = () =>
|
|
200
|
-
|
|
201
|
-
ctx.inject(['settings'], (sctx) => {
|
|
202
|
-
scope = sctx.settings.register(NS, Config, { base: baseConfig })
|
|
203
|
-
getConfig = () => scope.get() ?? baseConfig
|
|
204
|
-
sctx.effect(() => () => { getConfig = () => baseConfig; scope = null })
|
|
205
|
-
})
|
|
217
|
+
let getConfig = readEntryConfig
|
|
218
|
+
const live = () => BaseConfig(structuredClone(getConfig() ?? {})) ?? readEntryConfig()
|
|
206
219
|
|
|
207
220
|
async function resolveKey(ref) {
|
|
208
221
|
try {
|
|
@@ -246,12 +259,27 @@ export function apply(ctx, baseConfig) {
|
|
|
246
259
|
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
247
260
|
})
|
|
248
261
|
child = ctx.shell.start(spec)
|
|
249
|
-
|
|
262
|
+
if (child && typeof child.on === 'function') {
|
|
263
|
+
child.on('error', (err) => {
|
|
264
|
+
whisperError = err?.message || String(err)
|
|
265
|
+
})
|
|
266
|
+
child.on('exit', (code) => {
|
|
267
|
+
if (code !== 0 && code !== null) {
|
|
268
|
+
whisperError = `whisper server process exited with code ${code}`
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
// Wait for it to become healthy (up to 30s for cold model loading on Metal/CPU).
|
|
273
|
+
for (let i = 0; i < 60; i++) {
|
|
250
274
|
await new Promise((r) => setTimeout(r, 500))
|
|
251
275
|
if (await whisperAlive()) {
|
|
252
276
|
whisperError = null
|
|
253
277
|
return true
|
|
254
278
|
}
|
|
279
|
+
if (child && child.exitCode !== null && child.exitCode !== undefined) {
|
|
280
|
+
whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
255
283
|
}
|
|
256
284
|
whisperError = 'whisper server process started but failed health check on port 8001'
|
|
257
285
|
return false
|
|
@@ -262,8 +290,6 @@ export function apply(ctx, baseConfig) {
|
|
|
262
290
|
finally { startingWhisper = false }
|
|
263
291
|
}
|
|
264
292
|
|
|
265
|
-
startWhisper().catch(() => {})
|
|
266
|
-
|
|
267
293
|
// SenseVoice-ONNX / Sherpa-ONNX: autostart and health check.
|
|
268
294
|
let sensevoiceChild = null
|
|
269
295
|
let sensevoiceError = null
|
|
@@ -302,12 +328,27 @@ export function apply(ctx, baseConfig) {
|
|
|
302
328
|
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
303
329
|
})
|
|
304
330
|
sensevoiceChild = ctx.shell.start(spec)
|
|
305
|
-
|
|
331
|
+
if (sensevoiceChild && typeof sensevoiceChild.on === 'function') {
|
|
332
|
+
sensevoiceChild.on('error', (err) => {
|
|
333
|
+
sensevoiceError = err?.message || String(err)
|
|
334
|
+
})
|
|
335
|
+
sensevoiceChild.on('exit', (code) => {
|
|
336
|
+
if (code !== 0 && code !== null) {
|
|
337
|
+
sensevoiceError = `sensevoice process exited with code ${code}`
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
// Wait for it to become healthy (up to 30s).
|
|
342
|
+
for (let i = 0; i < 60; i++) {
|
|
306
343
|
await new Promise((r) => setTimeout(r, 500))
|
|
307
344
|
if (await sensevoiceAlive()) {
|
|
308
345
|
sensevoiceError = null
|
|
309
346
|
return true
|
|
310
347
|
}
|
|
348
|
+
if (sensevoiceChild && sensevoiceChild.exitCode !== null && sensevoiceChild.exitCode !== undefined) {
|
|
349
|
+
sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
311
352
|
}
|
|
312
353
|
sensevoiceError = 'sensevoice process started but failed health check'
|
|
313
354
|
return false
|
|
@@ -318,7 +359,38 @@ export function apply(ctx, baseConfig) {
|
|
|
318
359
|
finally { startingSensevoice = false }
|
|
319
360
|
}
|
|
320
361
|
|
|
321
|
-
|
|
362
|
+
function triggerAutostart() {
|
|
363
|
+
startWhisper().catch((err) => {
|
|
364
|
+
ctx.logger?.debug?.('whisper autostart failed: %s', err?.message || err)
|
|
365
|
+
})
|
|
366
|
+
startSensevoice().catch((err) => {
|
|
367
|
+
ctx.logger?.debug?.('sensevoice autostart failed: %s', err?.message || err)
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
ctx.inject(['settings'], (sctx) => {
|
|
372
|
+
try {
|
|
373
|
+
scope = sctx.settings.register(NS, Config, { base: baseConfig })
|
|
374
|
+
if (scope && typeof scope.get === 'function') {
|
|
375
|
+
getConfig = () => scope.get() ?? readEntryConfig()
|
|
376
|
+
}
|
|
377
|
+
if (scope && typeof scope.watch === 'function') {
|
|
378
|
+
scope.watch(() => {
|
|
379
|
+
triggerAutostart()
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
} catch { /* modern DSH handles settings via SettingsForms */ }
|
|
383
|
+
sctx.effect(() => () => { getConfig = readEntryConfig; scope = null })
|
|
384
|
+
triggerAutostart()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
if (typeof ctx.on === 'function') {
|
|
388
|
+
ctx.on('loader/volatile-update', () => triggerAutostart())
|
|
389
|
+
ctx.on('settings/document-updated', () => triggerAutostart())
|
|
390
|
+
ctx.on('config', () => triggerAutostart())
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
triggerAutostart()
|
|
322
394
|
|
|
323
395
|
// Provider health and latency stats (Latency & Health Dashboard).
|
|
324
396
|
const statsTracker = createStatsTracker()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.38",
|
|
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",
|