@goodandready/dsh-tts 0.4.28 → 0.4.30
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/CHANGELOG.md +42 -0
- package/README.md +9 -8
- package/README.ru.md +11 -10
- package/README.zh.md +5 -5
- package/lib/client.js +151 -46
- package/lib/engines/manager.js +37 -9
- package/lib/index.js +94 -36
- package/lib/providers/cloud.js +9 -1
- package/lib/providers.js +1 -1
- package/lib/routes.js +12 -11
- package/lib/stream-hub.js +19 -16
- package/lib/text.js +7 -1
- package/lib/updater.js +4 -5
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -163,9 +163,9 @@ export const Config = z.object({
|
|
|
163
163
|
.description('Longer replies are truncated before synthesis.'),
|
|
164
164
|
chain: z.array(ChainEntry).volatile()
|
|
165
165
|
.default([
|
|
166
|
-
{ provider: 'edge', model: '', voice: '
|
|
166
|
+
{ provider: 'edge', model: '', voice: 'en-US-JennyNeural' },
|
|
167
167
|
{ provider: 'piper', model: '', voice: '' },
|
|
168
|
-
{ provider: 'espeak', model: '', voice: '
|
|
168
|
+
{ provider: 'espeak', model: '', voice: 'en' },
|
|
169
169
|
])
|
|
170
170
|
.description('Fallback chain. Order is the order of attempts. A provider without a key is skipped.'),
|
|
171
171
|
openaiKeyEnv: z.string().volatile().default('OPENAI_API_KEY'),
|
|
@@ -198,23 +198,56 @@ export const Config = z.object({
|
|
|
198
198
|
maxQueue: z.number().step(1).min(0).volatile().default(8),
|
|
199
199
|
})
|
|
200
200
|
|
|
201
|
+
export function extractRevision(desc, targetNs = NS) {
|
|
202
|
+
if (!desc) return undefined
|
|
203
|
+
if (typeof desc.revision === 'number' || typeof desc.revision === 'string') {
|
|
204
|
+
return desc.revision
|
|
205
|
+
}
|
|
206
|
+
let list = null
|
|
207
|
+
if (Array.isArray(desc)) {
|
|
208
|
+
list = desc
|
|
209
|
+
} else if (Array.isArray(desc.namespaces)) {
|
|
210
|
+
list = desc.namespaces
|
|
211
|
+
} else if (Array.isArray(desc.entries)) {
|
|
212
|
+
list = desc.entries
|
|
213
|
+
}
|
|
214
|
+
if (list) {
|
|
215
|
+
const match = list.find((item) => {
|
|
216
|
+
if (!item || typeof item !== 'object') return false
|
|
217
|
+
const name = item.name || item.id || item.ns || item.namespace || item.key
|
|
218
|
+
return name === targetNs || name === 'goodandready/dsh-tts' || name === 'dsh-tts' || name === '@goodandready/dsh-tts'
|
|
219
|
+
})
|
|
220
|
+
if (match && (typeof match.revision === 'number' || typeof match.revision === 'string')) {
|
|
221
|
+
return match.revision
|
|
222
|
+
}
|
|
223
|
+
if (list.length === 1 && (typeof list[0].revision === 'number' || typeof list[0].revision === 'string')) {
|
|
224
|
+
return list[0].revision
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (typeof desc === 'object') {
|
|
228
|
+
for (const k of [targetNs, 'goodandready/dsh-tts', 'dsh-tts', '@goodandready/dsh-tts']) {
|
|
229
|
+
if (desc[k] && (typeof desc[k].revision === 'number' || typeof desc[k].revision === 'string')) {
|
|
230
|
+
return desc[k].revision
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
201
237
|
let utteranceSeq = 0
|
|
202
238
|
|
|
203
239
|
export function apply(ctx, config) {
|
|
204
240
|
const providerBreaker = createProviderBreaker()
|
|
205
241
|
|
|
206
|
-
let savedConfig = null
|
|
207
242
|
const live = () => {
|
|
208
243
|
const fromBoxes = plainConfig(config)
|
|
209
|
-
|
|
210
|
-
return plainConfig(Config(merged))
|
|
244
|
+
return plainConfig(Config(fromBoxes))
|
|
211
245
|
}
|
|
212
246
|
|
|
213
247
|
const settingsApi = {
|
|
214
248
|
get: () => live(),
|
|
215
249
|
replace: async (next, expectedRevision) => {
|
|
216
250
|
const plain = plainConfig(next)
|
|
217
|
-
const prevSaved = savedConfig
|
|
218
251
|
const svc = ctx?.get?.('settings') ?? ctx?.settings ?? ctx?.get?.('settingsForms') ?? ctx?.settingsForms
|
|
219
252
|
if (!svc) {
|
|
220
253
|
throw new Error('Settings service is unavailable')
|
|
@@ -223,28 +256,33 @@ export function apply(ctx, config) {
|
|
|
223
256
|
if (rev === undefined && typeof svc.describe === 'function') {
|
|
224
257
|
try {
|
|
225
258
|
const desc = await svc.describe(NS)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
} else if (desc && Array.isArray(desc.namespaces)) {
|
|
229
|
-
const entry = desc.namespaces.find((n) => n.ns === NS)
|
|
230
|
-
if (entry && typeof entry.revision === 'number') rev = entry.revision
|
|
231
|
-
}
|
|
232
|
-
} catch {
|
|
259
|
+
rev = extractRevision(desc, NS)
|
|
260
|
+
} catch (_err) {
|
|
233
261
|
// ignore describe error
|
|
234
262
|
}
|
|
235
263
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
264
|
+
if (typeof svc.replace === 'function') {
|
|
265
|
+
await svc.replace(NS, plain, rev)
|
|
266
|
+
} else if (typeof svc.update === 'function') {
|
|
267
|
+
await svc.update(NS, plain, rev)
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error('Settings service does not support replace or update')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Mutate volatile boxes directly so runtime dynamically reflects new settings (#152)
|
|
273
|
+
if (config && typeof config === 'object') {
|
|
274
|
+
for (const [k, v] of Object.entries(plain)) {
|
|
275
|
+
if (config[k] && typeof config[k] === 'object') {
|
|
276
|
+
const sym = Object.getOwnPropertySymbols(config[k]).find((s) => String(s).includes('volatile.write'))
|
|
277
|
+
if (sym && typeof config[k][sym] === 'function') {
|
|
278
|
+
try { config[k][sym](v) } catch (_wErr) { /* best effort */ }
|
|
279
|
+
} else if ('value' in config[k]) {
|
|
280
|
+
config[k].value = v
|
|
281
|
+
}
|
|
282
|
+
} else if (config[k] !== undefined) {
|
|
283
|
+
config[k] = v
|
|
284
|
+
}
|
|
243
285
|
}
|
|
244
|
-
savedConfig = plain
|
|
245
|
-
} catch (err) {
|
|
246
|
-
savedConfig = prevSaved
|
|
247
|
-
throw err
|
|
248
286
|
}
|
|
249
287
|
return plain
|
|
250
288
|
},
|
|
@@ -265,7 +303,7 @@ export function apply(ctx, config) {
|
|
|
265
303
|
try {
|
|
266
304
|
const resolved = await ctx.credentials.resolve(credentialRef(ref))
|
|
267
305
|
if (resolved && resolved.value) return resolved.value
|
|
268
|
-
} catch { /* fall through to env */ }
|
|
306
|
+
} catch (_err) { /* fall through to env */ }
|
|
269
307
|
return process.env[ref] || ''
|
|
270
308
|
}
|
|
271
309
|
|
|
@@ -285,7 +323,7 @@ export function apply(ctx, config) {
|
|
|
285
323
|
}
|
|
286
324
|
const resolved = await ctx.credentials.resolve(credentialRef(ref))
|
|
287
325
|
return { provider, ref, configured: !!(resolved && resolved.value), writable: true }
|
|
288
|
-
} catch {
|
|
326
|
+
} catch (_err) {
|
|
289
327
|
return base
|
|
290
328
|
}
|
|
291
329
|
}
|
|
@@ -327,10 +365,21 @@ export function apply(ctx, config) {
|
|
|
327
365
|
}
|
|
328
366
|
|
|
329
367
|
async function configResponse() {
|
|
368
|
+
let revision = null
|
|
369
|
+
const svc = ctx?.get?.('settings') ?? ctx?.settings ?? ctx?.get?.('settingsForms') ?? ctx?.settingsForms
|
|
370
|
+
if (svc && typeof svc.describe === 'function') {
|
|
371
|
+
try {
|
|
372
|
+
const desc = await svc.describe(NS)
|
|
373
|
+
revision = extractRevision(desc, NS) ?? null
|
|
374
|
+
} catch (_err) {
|
|
375
|
+
// best effort describe
|
|
376
|
+
}
|
|
377
|
+
}
|
|
330
378
|
return {
|
|
331
379
|
ok: true,
|
|
332
380
|
config: publicConfig(live()),
|
|
333
381
|
credentials: await credentialsView(),
|
|
382
|
+
revision,
|
|
334
383
|
}
|
|
335
384
|
}
|
|
336
385
|
|
|
@@ -381,7 +430,7 @@ export function apply(ctx, config) {
|
|
|
381
430
|
if (retold) {
|
|
382
431
|
return speechPhrases(cfg.language).summaryIntro + ' ' + retold
|
|
383
432
|
}
|
|
384
|
-
} catch { /* quiet fallback to truncation */ }
|
|
433
|
+
} catch (_err) { /* quiet fallback to truncation */ }
|
|
385
434
|
}
|
|
386
435
|
return text.slice(0, maxChars)
|
|
387
436
|
}
|
|
@@ -389,7 +438,8 @@ export function apply(ctx, config) {
|
|
|
389
438
|
}
|
|
390
439
|
|
|
391
440
|
function synthesisFingerprint(text, provider, model, voice, role, ssmlStyle, cfg, lang) {
|
|
392
|
-
const
|
|
441
|
+
const credRef = needsApiKey(provider) ? (keyEnvName(cfg, provider) || '') : ''
|
|
442
|
+
const parts = [text, provider, model || '', voice || '', role || '', ssmlStyle || '', lang || '', credRef]
|
|
393
443
|
if (provider === 'custom') parts.push(cfg.customBaseUrl || '')
|
|
394
444
|
else if (provider === 'openai') parts.push(cfg.openaiBaseUrl || '')
|
|
395
445
|
else if (provider === 'mimo') parts.push(cfg.mimoBaseUrl || '', cfg.mimoFormat || '')
|
|
@@ -402,12 +452,13 @@ export function apply(ctx, config) {
|
|
|
402
452
|
|
|
403
453
|
function flightFingerprint(text, order, models, voices, role, ssmlStyle, cfg, lang) {
|
|
404
454
|
const details = order.map((p) => {
|
|
455
|
+
const credRef = needsApiKey(p) ? (keyEnvName(cfg, p) || '') : ''
|
|
405
456
|
const extra = p === 'custom' ? (cfg.customBaseUrl || '') :
|
|
406
457
|
p === 'openai' ? (cfg.openaiBaseUrl || '') :
|
|
407
458
|
p === 'mimo' ? `${cfg.mimoBaseUrl || ''}:${cfg.mimoFormat || ''}` :
|
|
408
459
|
p === 'azure' ? (cfg.azureRegion || '') :
|
|
409
460
|
p === 'piper' ? `${cfg.piperModel || ''}:${cfg.piperBin || ''}` : ''
|
|
410
|
-
return `${p}:${models[p] || ''}:${voices[p] || ''}:${extra}`
|
|
461
|
+
return `${p}:${models[p] || ''}:${voices[p] || ''}:${credRef}:${extra}`
|
|
411
462
|
}).join(';')
|
|
412
463
|
return cacheKey([text, details, role, ssmlStyle, lang])
|
|
413
464
|
}
|
|
@@ -656,7 +707,7 @@ export function apply(ctx, config) {
|
|
|
656
707
|
if (p && p.id && activeReservations.has(p.id)) {
|
|
657
708
|
const resv = activeReservations.get(p.id)
|
|
658
709
|
resv.dropped = true
|
|
659
|
-
try { resv.controller.abort() } catch {}
|
|
710
|
+
try { resv.controller.abort() } catch (_abortErr) { /* ignore aborted controller */ }
|
|
660
711
|
clearTimeout(resv.timer)
|
|
661
712
|
activeReservations.delete(p.id)
|
|
662
713
|
}
|
|
@@ -671,7 +722,7 @@ export function apply(ctx, config) {
|
|
|
671
722
|
const resv = activeReservations.get(dropped.id)
|
|
672
723
|
if (resv) {
|
|
673
724
|
resv.dropped = true
|
|
674
|
-
try { resv.controller.abort() } catch {}
|
|
725
|
+
try { resv.controller.abort() } catch (_abortErr) { /* ignore aborted controller */ }
|
|
675
726
|
clearTimeout(resv.timer)
|
|
676
727
|
activeReservations.delete(dropped.id)
|
|
677
728
|
}
|
|
@@ -720,8 +771,10 @@ export function apply(ctx, config) {
|
|
|
720
771
|
if (!cfg.speakReplies) return
|
|
721
772
|
const sid = session && session.id
|
|
722
773
|
if (!sid) return
|
|
723
|
-
const isMessenger = session && (session.kind === 'im' || session.kind === 'messenger' || session.channel === 'messenger' || session.type === 'im')
|
|
724
|
-
if (isMessenger && cfg.messengerTtsEnabled
|
|
774
|
+
const isMessenger = session && (session.kind === 'im' || session.kind === 'messenger' || session.channel === 'messenger' || session.type === 'im' || session.source === 'messenger')
|
|
775
|
+
if (isMessenger && !cfg.messengerTtsEnabled) return
|
|
776
|
+
const isVoice = session && (session.kind === 'voice' || session.channel === 'voice' || session.type === 'voice' || session.mode === 'voice')
|
|
777
|
+
if (isVoice && !cfg.voiceDuplexEnabled) return
|
|
725
778
|
// The agent is waiting for human approval — worth speaking aloud
|
|
726
779
|
// so the user notices without watching the screen.
|
|
727
780
|
if (event.type === 'approval/asked') {
|
|
@@ -737,7 +790,7 @@ export function apply(ctx, config) {
|
|
|
737
790
|
}
|
|
738
791
|
|
|
739
792
|
if (event.type === 'assistant/message') {
|
|
740
|
-
const text = assistantText(event.data && event.data.message)
|
|
793
|
+
const text = assistantText(event.data && (event.data.message ?? event.data.text ?? event.data))
|
|
741
794
|
const agentName = (event.data && (
|
|
742
795
|
event.data.subagent ||
|
|
743
796
|
event.data.agent ||
|
|
@@ -750,8 +803,12 @@ export function apply(ctx, config) {
|
|
|
750
803
|
// in sentence pieces; waiting for turn end means silence while the agent works.
|
|
751
804
|
|
|
752
805
|
if (cfg.speakAsItGoes) {
|
|
753
|
-
|
|
754
|
-
|
|
806
|
+
prepareSpeechText(text, cfg).then((ready) => {
|
|
807
|
+
if (!ready) return
|
|
808
|
+
for (const piece of splitSentences(ready, cfg.sentenceChars)) speakPiece(sid, piece, cfg, activeRole)
|
|
809
|
+
}).catch((_err) => {
|
|
810
|
+
// best-effort speech fallback
|
|
811
|
+
})
|
|
755
812
|
return
|
|
756
813
|
}
|
|
757
814
|
const cur = collectors.get(sid) || { parts: [], role: activeRole }
|
|
@@ -816,6 +873,7 @@ export function apply(ctx, config) {
|
|
|
816
873
|
additionalProperties: false,
|
|
817
874
|
properties: {
|
|
818
875
|
ok: { type: 'boolean' },
|
|
876
|
+
id: { type: 'string' },
|
|
819
877
|
provider: { type: 'string' },
|
|
820
878
|
mime: { type: 'string' },
|
|
821
879
|
bytes: { type: 'number' },
|
package/lib/providers/cloud.js
CHANGED
|
@@ -192,7 +192,15 @@ export function makeCloudProviders(deps, req, withCloudTimeout) {
|
|
|
192
192
|
const m = mimeType.match(/rate=(\d+)/i)
|
|
193
193
|
if (m) sampleRate = parseInt(m[1], 10)
|
|
194
194
|
const isWavAlready = raw.length >= 4 && raw.toString('ascii', 0, 4) === 'RIFF'
|
|
195
|
-
|
|
195
|
+
let audio = null
|
|
196
|
+
const lowerMime = mimeType.toLowerCase()
|
|
197
|
+
if (isWavAlready || lowerMime.includes('wav')) {
|
|
198
|
+
audio = raw
|
|
199
|
+
} else if (!lowerMime || lowerMime.includes('pcm') || lowerMime.includes('raw') || lowerMime.includes('l16')) {
|
|
200
|
+
audio = pcmToWav(raw, sampleRate, 1)
|
|
201
|
+
} else {
|
|
202
|
+
return { ok: false, provider: 'google', reason: `unsupported Google TTS MIME: ${mimeType}` }
|
|
203
|
+
}
|
|
196
204
|
return { ok: audio.length > 0, provider: 'google', audio, mime: 'audio/wav', reason: audio.length ? '' : 'empty audio' }
|
|
197
205
|
} finally {
|
|
198
206
|
cleanup()
|
package/lib/providers.js
CHANGED
|
@@ -29,7 +29,7 @@ export function makeProviders(deps, req) {
|
|
|
29
29
|
cleanup: () => {
|
|
30
30
|
clearTimeout(timer)
|
|
31
31
|
if (parentSignal && onParentAbort) {
|
|
32
|
-
try { parentSignal.removeEventListener('abort', onParentAbort) } catch {}
|
|
32
|
+
try { parentSignal.removeEventListener('abort', onParentAbort) } catch (_err) { /* parent abort listener already removed */ }
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
}
|
package/lib/routes.js
CHANGED
|
@@ -119,7 +119,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
119
119
|
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'missing engine parameter' } })
|
|
120
120
|
return
|
|
121
121
|
}
|
|
122
|
-
if (!MODEL_SPECS
|
|
122
|
+
if (!Object.hasOwn(MODEL_SPECS, engine)) {
|
|
123
123
|
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'unknown engine: ' + engine } })
|
|
124
124
|
return
|
|
125
125
|
}
|
|
@@ -160,7 +160,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
160
160
|
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'missing engine parameter' } })
|
|
161
161
|
return
|
|
162
162
|
}
|
|
163
|
-
if (!MODEL_SPECS
|
|
163
|
+
if (!Object.hasOwn(MODEL_SPECS, engine)) {
|
|
164
164
|
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'unknown engine: ' + engine } })
|
|
165
165
|
return
|
|
166
166
|
}
|
|
@@ -187,7 +187,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
187
187
|
try {
|
|
188
188
|
if (typeof ctx.get === 'function' && ctx.get(id)) return true
|
|
189
189
|
if (ctx[id]) return true
|
|
190
|
-
} catch {}
|
|
190
|
+
} catch (_err) { /* service check fallback */ }
|
|
191
191
|
try {
|
|
192
192
|
const reg = ctx.registry || ctx.root?.registry
|
|
193
193
|
if (reg && typeof reg.entries === 'function') {
|
|
@@ -197,7 +197,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
197
197
|
if (id === 'messenger' && (name.includes('messenger') || name.includes('dsh-im'))) return true
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
} catch {}
|
|
200
|
+
} catch (_err) { /* registry check fallback */ }
|
|
201
201
|
try {
|
|
202
202
|
if (ctx.webServer) {
|
|
203
203
|
if (typeof ctx.webServer.hasRoute === 'function') {
|
|
@@ -207,7 +207,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
207
207
|
if (ctx.webServer.routes.some((r) => r && r.path && r.path.includes(id))) return true
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
} catch {}
|
|
210
|
+
} catch (_err) { /* webServer route check fallback */ }
|
|
211
211
|
return false
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -467,6 +467,11 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
467
467
|
const rawText = String(payload.text || '')
|
|
468
468
|
if (!rawText.trim()) { writeJson(res, 400, { ok: false, error: { code: 'no-text', message: 'no text' } }); return }
|
|
469
469
|
const cfg = live()
|
|
470
|
+
const isMessenger = payload.channel === 'messenger' || payload.source === 'messenger' || req.headers['x-dsh-channel'] === 'messenger'
|
|
471
|
+
if (isMessenger && !cfg.messengerTtsEnabled) {
|
|
472
|
+
writeJson(res, 403, { ok: false, error: { code: 'disabled', message: 'messenger tts is disabled' } })
|
|
473
|
+
return
|
|
474
|
+
}
|
|
470
475
|
const controller = new AbortController()
|
|
471
476
|
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
472
477
|
try {
|
|
@@ -490,15 +495,11 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
490
495
|
},
|
|
491
496
|
}), 'dsh-tts: /speak route')
|
|
492
497
|
|
|
493
|
-
|
|
494
|
-
endpoint: '/dsh-tts/updater',
|
|
495
|
-
packageName: '@goodandready/dsh-tts',
|
|
496
|
-
manifestUrl: new URL('../package.json', import.meta.url),
|
|
497
|
-
}), 'dsh-tts: updater route /dsh-tts/updater')
|
|
498
|
-
|
|
498
|
+
const updaterMutex = { installing: false }
|
|
499
499
|
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
500
500
|
endpoint: '/api/dsh-tts/update',
|
|
501
501
|
packageName: '@goodandready/dsh-tts',
|
|
502
502
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
503
|
+
mutex: updaterMutex,
|
|
503
504
|
}), 'dsh-tts: updater route /api/dsh-tts/update')
|
|
504
505
|
}
|
package/lib/stream-hub.js
CHANGED
|
@@ -20,25 +20,22 @@ function detach(res) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function safeWrite(res, payload) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
try { res.end() } catch {}
|
|
34
|
-
return false
|
|
35
|
-
}
|
|
23
|
+
const payloadBytes = Buffer.byteLength(payload)
|
|
24
|
+
const currentBuffered = typeof res.writableLength === 'number'
|
|
25
|
+
? res.writableLength
|
|
26
|
+
: (res.__dtsBufferedBytes || 0)
|
|
27
|
+
|
|
28
|
+
if (res.__dtsBackpressured && currentBuffered + payloadBytes > MAX_SUBSCRIBER_BUFFER) {
|
|
29
|
+
subscribers.delete(res)
|
|
30
|
+
detach(res)
|
|
31
|
+
try { res.end() } catch (_err) { /* already ended */ }
|
|
32
|
+
return false
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
let writeOk = true
|
|
39
36
|
try {
|
|
40
37
|
writeOk = res.write(payload)
|
|
41
|
-
} catch {
|
|
38
|
+
} catch (_err) {
|
|
42
39
|
subscribers.delete(res)
|
|
43
40
|
detach(res)
|
|
44
41
|
return false
|
|
@@ -46,7 +43,13 @@ function safeWrite(res, payload) {
|
|
|
46
43
|
|
|
47
44
|
if (writeOk === false) {
|
|
48
45
|
res.__dtsBackpressured = true
|
|
49
|
-
res.__dtsBufferedBytes = (res.__dtsBufferedBytes || 0) +
|
|
46
|
+
res.__dtsBufferedBytes = (res.__dtsBufferedBytes || 0) + payloadBytes
|
|
47
|
+
if (res.__dtsBufferedBytes > MAX_SUBSCRIBER_BUFFER) {
|
|
48
|
+
subscribers.delete(res)
|
|
49
|
+
detach(res)
|
|
50
|
+
try { res.end() } catch (_err) { /* already ended */ }
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
50
53
|
if (!res.__dtsOnDrain) {
|
|
51
54
|
const onDrain = () => {
|
|
52
55
|
res.__dtsBackpressured = false
|
|
@@ -55,7 +58,7 @@ function safeWrite(res, payload) {
|
|
|
55
58
|
res.__dtsOnDrain = onDrain
|
|
56
59
|
try {
|
|
57
60
|
if (typeof res.on === 'function') res.on('drain', onDrain)
|
|
58
|
-
} catch { /* non-eventemitter mock */ }
|
|
61
|
+
} catch (_err) { /* non-eventemitter mock */ }
|
|
59
62
|
}
|
|
60
63
|
} else if (res.__dtsBackpressured && (!res.writableLength || res.writableLength === 0)) {
|
|
61
64
|
res.__dtsBackpressured = false
|
package/lib/text.js
CHANGED
|
@@ -138,7 +138,13 @@ export function stripForSpeech(raw, maxChars, opts) {
|
|
|
138
138
|
|
|
139
139
|
export function assistantText(message) {
|
|
140
140
|
if (!message) return ''
|
|
141
|
-
|
|
141
|
+
if (typeof message === 'string') return message
|
|
142
|
+
if (typeof message === 'object') {
|
|
143
|
+
if (message.content !== undefined) return blocksToText(message.content)
|
|
144
|
+
if (typeof message.text === 'string') return message.text
|
|
145
|
+
if (typeof message.message === 'string') return message.message
|
|
146
|
+
}
|
|
147
|
+
return ''
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
// Protect common English abbreviations from false sentence splits
|
package/lib/updater.js
CHANGED
|
@@ -225,7 +225,6 @@ async function installExact(target, packageSpec, options) {
|
|
|
225
225
|
'--profile',
|
|
226
226
|
target.profileName,
|
|
227
227
|
'add',
|
|
228
|
-
'--config.minimumReleaseAge=0',
|
|
229
228
|
packageSpec,
|
|
230
229
|
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
231
230
|
],
|
|
@@ -268,7 +267,7 @@ function json(response, statusCode, value) {
|
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
export function registerPluginUpdater(ctx, options) {
|
|
271
|
-
|
|
270
|
+
const mutex = options?.mutex || { installing: false }
|
|
272
271
|
if (typeof ctx?.webServer?.register !== 'function') {
|
|
273
272
|
ctx?.logger?.warn?.('[dsh-tts] webServer service unavailable, updater skipped')
|
|
274
273
|
return () => {}
|
|
@@ -298,11 +297,11 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
298
297
|
json(response, 403, { error: 'Rejected non-local or cross-origin update request.' })
|
|
299
298
|
return
|
|
300
299
|
}
|
|
301
|
-
if (installing) {
|
|
300
|
+
if (mutex.installing) {
|
|
302
301
|
json(response, 409, { error: 'This plugin is already updating.' })
|
|
303
302
|
return
|
|
304
303
|
}
|
|
305
|
-
installing = true
|
|
304
|
+
mutex.installing = true
|
|
306
305
|
try {
|
|
307
306
|
const before = await status(options, target)
|
|
308
307
|
if (before.latestVersion === undefined) {
|
|
@@ -322,7 +321,7 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
322
321
|
} catch (err) {
|
|
323
322
|
json(response, 503, { error: `Plugin update failed: ${String(err?.message || err)}` })
|
|
324
323
|
} finally {
|
|
325
|
-
installing = false
|
|
324
|
+
mutex.installing = false
|
|
326
325
|
}
|
|
327
326
|
} catch (error) {
|
|
328
327
|
ctx?.logger?.warn?.(`[dsh-tts] plugin updater failed: ${String(error)}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|