@goodandready/dsh-tts 0.4.26 → 0.4.28
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 +40 -1
- package/README.md +9 -5
- package/lib/cache.js +70 -40
- package/lib/client.js +257 -160
- package/lib/engines/manager.js +103 -69
- package/lib/index.js +277 -54
- package/lib/local.js +1 -1
- package/lib/providers/cloud.js +36 -5
- package/lib/providers/constants.js +4 -4
- package/lib/providers/local.js +1 -1
- package/lib/providers.js +11 -2
- package/lib/routes.js +102 -36
- package/lib/stream-hub.js +51 -7
- package/lib/text.js +18 -1
- package/package.json +1 -1
package/lib/routes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MODEL_SPECS } from './engines/manager.js'
|
|
1
2
|
import { resolve } from 'node:path'
|
|
2
3
|
import { registerPluginUpdater } from './updater.js'
|
|
3
4
|
import { writeJson, readBody, isTrustedSettingsRequest } from './http-util.js'
|
|
@@ -17,6 +18,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
17
18
|
stats,
|
|
18
19
|
speechCache,
|
|
19
20
|
cleanText,
|
|
21
|
+
prepareSpeechText,
|
|
20
22
|
synthesize,
|
|
21
23
|
credentialsView,
|
|
22
24
|
configResponse,
|
|
@@ -32,6 +34,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
32
34
|
path: '/dsh-tts/status',
|
|
33
35
|
handler: async (req, res) => {
|
|
34
36
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
37
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
38
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
39
|
+
return
|
|
40
|
+
}
|
|
35
41
|
const cfg = live()
|
|
36
42
|
writeJson(res, 200, {
|
|
37
43
|
ok: true,
|
|
@@ -42,7 +48,9 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
42
48
|
streamingEnabled: cfg.streamingEnabled,
|
|
43
49
|
rate: cfg.rate,
|
|
44
50
|
chime: cfg.chime,
|
|
45
|
-
bargeIn: cfg.bargeIn,
|
|
51
|
+
bargeIn: cfg.bargeIn && cfg.vadBargeIn !== false,
|
|
52
|
+
voiceDuplexEnabled: cfg.voiceDuplexEnabled !== false,
|
|
53
|
+
messengerTtsEnabled: cfg.messengerTtsEnabled !== false,
|
|
46
54
|
language: cfg.language,
|
|
47
55
|
chain: cfg.chain,
|
|
48
56
|
roles: cfg.roles,
|
|
@@ -74,7 +82,6 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
74
82
|
},
|
|
75
83
|
}), 'dsh-tts: /stream route')
|
|
76
84
|
|
|
77
|
-
|
|
78
85
|
ctx.effect(() => ctx.webServer.register({
|
|
79
86
|
kind: 'exact',
|
|
80
87
|
path: '/dsh-tts/models/status',
|
|
@@ -112,6 +119,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
112
119
|
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'missing engine parameter' } })
|
|
113
120
|
return
|
|
114
121
|
}
|
|
122
|
+
if (!MODEL_SPECS[engine]) {
|
|
123
|
+
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'unknown engine: ' + engine } })
|
|
124
|
+
return
|
|
125
|
+
}
|
|
115
126
|
try {
|
|
116
127
|
// Fire installation in background or await initial start
|
|
117
128
|
modelManager.installModel(engine).catch(() => { /* background install error tracked in manager state */ })
|
|
@@ -149,6 +160,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
149
160
|
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'missing engine parameter' } })
|
|
150
161
|
return
|
|
151
162
|
}
|
|
163
|
+
if (!MODEL_SPECS[engine]) {
|
|
164
|
+
writeJson(res, 400, { ok: false, error: { code: 'param', message: 'unknown engine: ' + engine } })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
152
167
|
try {
|
|
153
168
|
const result = await modelManager.deleteModel(engine)
|
|
154
169
|
writeJson(res, 200, { ok: true, model: result })
|
|
@@ -167,30 +182,56 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
167
182
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
168
183
|
return
|
|
169
184
|
}
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
function isPluginOrServicePresent(id) {
|
|
186
|
+
if (!ctx) return false
|
|
187
|
+
try {
|
|
188
|
+
if (typeof ctx.get === 'function' && ctx.get(id)) return true
|
|
189
|
+
if (ctx[id]) return true
|
|
190
|
+
} catch {}
|
|
191
|
+
try {
|
|
192
|
+
const reg = ctx.registry || ctx.root?.registry
|
|
193
|
+
if (reg && typeof reg.entries === 'function') {
|
|
194
|
+
for (const [k, v] of reg.entries()) {
|
|
195
|
+
const name = String(v?.name || k?.name || '')
|
|
196
|
+
if (name === id || name === `dsh-${id}` || name === `@goodandready/dsh-${id}`) return true
|
|
197
|
+
if (id === 'messenger' && (name.includes('messenger') || name.includes('dsh-im'))) return true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
try {
|
|
202
|
+
if (ctx.webServer) {
|
|
203
|
+
if (typeof ctx.webServer.hasRoute === 'function') {
|
|
204
|
+
if (ctx.webServer.hasRoute(`/dsh-${id}/status`)) return true
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(ctx.webServer.routes)) {
|
|
207
|
+
if (ctx.webServer.routes.some((r) => r && r.path && r.path.includes(id))) return true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
172
213
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
voiceInstalled = ctx.webServer.hasRoute('/dsh-voice/status')
|
|
176
|
-
messengerInstalled = ctx.webServer.hasRoute('/dsh-messenger-gateway/status')
|
|
177
|
-
}
|
|
178
|
-
} catch { /* ignore malformed client frame */ }
|
|
214
|
+
const voiceInstalled = isPluginOrServicePresent('voice')
|
|
215
|
+
const messengerInstalled = isPluginOrServicePresent('messenger')
|
|
179
216
|
|
|
180
217
|
// Runtime routes are the only reliable signal. Do not probe ../ siblings
|
|
181
218
|
// or node_modules layouts — they lie outside a normal npm install.
|
|
182
|
-
|
|
183
219
|
writeJson(res, 200, {
|
|
184
220
|
ok: true,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
221
|
+
voiceInstalled,
|
|
222
|
+
messengerInstalled,
|
|
223
|
+
note: 'State reflects active routes in the running harness.',
|
|
224
|
+
integrations: {
|
|
225
|
+
voice: {
|
|
226
|
+
installed: voiceInstalled,
|
|
227
|
+
package: '@goodandready/dsh-voice',
|
|
228
|
+
hint: 'dsh plugin --profile web add @goodandready/dsh-voice',
|
|
229
|
+
},
|
|
230
|
+
messenger: {
|
|
231
|
+
installed: messengerInstalled,
|
|
232
|
+
package: '@goodandready/dsh-messenger-gateway',
|
|
233
|
+
hint: 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway',
|
|
234
|
+
},
|
|
194
235
|
},
|
|
195
236
|
})
|
|
196
237
|
},
|
|
@@ -200,7 +241,7 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
200
241
|
kind: 'exact',
|
|
201
242
|
path: '/dsh-tts/cache',
|
|
202
243
|
handler: async (req, res) => {
|
|
203
|
-
if (req.method !== 'DELETE') {
|
|
244
|
+
if (req.method !== 'DELETE' && req.method !== 'POST') {
|
|
204
245
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'DELETE only' } })
|
|
205
246
|
return
|
|
206
247
|
}
|
|
@@ -209,10 +250,27 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
209
250
|
return
|
|
210
251
|
}
|
|
211
252
|
const removed = await speechCache.clear()
|
|
212
|
-
writeJson(res, 200, { ok: true, removed })
|
|
253
|
+
writeJson(res, 200, { ok: true, removed, cleared: removed })
|
|
213
254
|
},
|
|
214
255
|
}), 'dsh-tts: /cache route')
|
|
215
256
|
|
|
257
|
+
ctx.effect(() => ctx.webServer.register({
|
|
258
|
+
kind: 'exact',
|
|
259
|
+
path: '/dsh-tts/cache/clear',
|
|
260
|
+
handler: async (req, res) => {
|
|
261
|
+
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
262
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
266
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const removed = await speechCache.clear()
|
|
270
|
+
writeJson(res, 200, { ok: true, removed, cleared: removed })
|
|
271
|
+
},
|
|
272
|
+
}), 'dsh-tts: /cache/clear route')
|
|
273
|
+
|
|
216
274
|
ctx.effect(() => ctx.webServer.register({
|
|
217
275
|
kind: 'exact',
|
|
218
276
|
path: '/dsh-tts/preview',
|
|
@@ -281,6 +339,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
281
339
|
kind: 'exact',
|
|
282
340
|
path: '/dsh-tts/config',
|
|
283
341
|
handler: async (req, res) => {
|
|
342
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
343
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'dsh-tts: settings writes are same-origin only' } })
|
|
344
|
+
return
|
|
345
|
+
}
|
|
284
346
|
if (req.method === 'GET') {
|
|
285
347
|
writeJson(res, 200, await configResponse())
|
|
286
348
|
return
|
|
@@ -289,10 +351,6 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
289
351
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or PUT' } })
|
|
290
352
|
return
|
|
291
353
|
}
|
|
292
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
293
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'dsh-tts: settings writes are same-origin only' } })
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
354
|
const settingsApi = typeof getSettingsApi === 'function' ? getSettingsApi() : null
|
|
297
355
|
if (!settingsApi) {
|
|
298
356
|
writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
|
|
@@ -308,8 +366,9 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
308
366
|
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
309
367
|
return
|
|
310
368
|
}
|
|
369
|
+
const revision = payload?.revision ?? (typeof payload?.config === 'object' ? payload.config?.revision : undefined)
|
|
311
370
|
if (payload && typeof payload.config === 'object') {
|
|
312
|
-
payload = { ...payload.config, keys: payload.keys }
|
|
371
|
+
payload = { ...payload.config, keys: payload.keys, revision: payload.revision ?? payload.config.revision }
|
|
313
372
|
}
|
|
314
373
|
const keys = pendingKeyWrites(payload && payload.keys)
|
|
315
374
|
const stripped = stripSecretsFromConfig(payload)
|
|
@@ -322,10 +381,13 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
322
381
|
for (const item of keys) {
|
|
323
382
|
await storeProviderKey(item.provider, item.value)
|
|
324
383
|
}
|
|
325
|
-
await settingsApi.replace(parsed)
|
|
384
|
+
await settingsApi.replace(parsed, revision)
|
|
326
385
|
writeJson(res, 200, await configResponse())
|
|
327
386
|
} catch (e) {
|
|
328
|
-
|
|
387
|
+
const isConflict = e?.code === 'conflict' || e?.name === 'SettingsConflictError' || /conflict|revision/i.test(e?.message || '')
|
|
388
|
+
const statusCode = isConflict ? 409 : 500
|
|
389
|
+
const code = isConflict ? 'conflict' : 'save'
|
|
390
|
+
writeJson(res, statusCode, { ok: false, error: { code, message: String(e && e.message || e) } })
|
|
329
391
|
}
|
|
330
392
|
},
|
|
331
393
|
}), 'dsh-tts: /config route')
|
|
@@ -402,13 +464,17 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
402
464
|
}
|
|
403
465
|
let payload
|
|
404
466
|
try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { /* invalid JSON treated as empty */ /* invalid JSON treated as empty */ payload = {} }
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
467
|
+
const rawText = String(payload.text || '')
|
|
468
|
+
if (!rawText.trim()) { writeJson(res, 400, { ok: false, error: { code: 'no-text', message: 'no text' } }); return }
|
|
407
469
|
const cfg = live()
|
|
408
470
|
const controller = new AbortController()
|
|
409
471
|
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
410
472
|
try {
|
|
411
|
-
const
|
|
473
|
+
const text = typeof prepareSpeechText === 'function'
|
|
474
|
+
? await prepareSpeechText(rawText, cfg, controller.signal)
|
|
475
|
+
: cleanText(rawText, cfg, 0)
|
|
476
|
+
if (!text) { writeJson(res, 400, { ok: false, error: { code: 'no-text', message: 'no text' } }); return }
|
|
477
|
+
const out = await synthesize(text, cfg, controller.signal, 'reply', { skipClean: true })
|
|
412
478
|
writeJson(res, 200, {
|
|
413
479
|
ok: true,
|
|
414
480
|
provider: out.provider,
|
|
@@ -424,15 +490,15 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
424
490
|
},
|
|
425
491
|
}), 'dsh-tts: /speak route')
|
|
426
492
|
|
|
427
|
-
registerPluginUpdater(ctx, {
|
|
493
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
428
494
|
endpoint: '/dsh-tts/updater',
|
|
429
495
|
packageName: '@goodandready/dsh-tts',
|
|
430
496
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
431
|
-
})
|
|
497
|
+
}), 'dsh-tts: updater route /dsh-tts/updater')
|
|
432
498
|
|
|
433
|
-
registerPluginUpdater(ctx, {
|
|
499
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
434
500
|
endpoint: '/api/dsh-tts/update',
|
|
435
501
|
packageName: '@goodandready/dsh-tts',
|
|
436
502
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
437
|
-
})
|
|
503
|
+
}), 'dsh-tts: updater route /api/dsh-tts/update')
|
|
438
504
|
}
|
package/lib/stream-hub.js
CHANGED
|
@@ -3,24 +3,66 @@
|
|
|
3
3
|
const subscribers = new Set()
|
|
4
4
|
let heartbeatTimer = null
|
|
5
5
|
|
|
6
|
+
// Max buffer size for a slow subscriber before disconnecting (512 KB)
|
|
7
|
+
export const MAX_SUBSCRIBER_BUFFER = 512 * 1024
|
|
8
|
+
|
|
6
9
|
function detach(res) {
|
|
7
10
|
try {
|
|
8
|
-
if (res.__dtsOnClose) res.off('close', res.__dtsOnClose)
|
|
9
|
-
if (res.__dtsOnError) res.off('error', res.__dtsOnError)
|
|
11
|
+
if (res.__dtsOnClose && typeof res.off === 'function') res.off('close', res.__dtsOnClose)
|
|
12
|
+
if (res.__dtsOnError && typeof res.off === 'function') res.off('error', res.__dtsOnError)
|
|
13
|
+
if (res.__dtsOnDrain && typeof res.off === 'function') res.off('drain', res.__dtsOnDrain)
|
|
10
14
|
} catch { /* listeners may already be gone */ }
|
|
11
15
|
res.__dtsOnClose = null
|
|
12
16
|
res.__dtsOnError = null
|
|
17
|
+
res.__dtsOnDrain = null
|
|
18
|
+
res.__dtsBackpressured = false
|
|
19
|
+
res.__dtsBufferedBytes = 0
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
function safeWrite(res, payload) {
|
|
23
|
+
// If subscriber is already backpressured, check buffered limit
|
|
24
|
+
if (res.__dtsBackpressured) {
|
|
25
|
+
const currentBuffered = typeof res.writableLength === 'number'
|
|
26
|
+
? res.writableLength
|
|
27
|
+
: (res.__dtsBufferedBytes || 0)
|
|
28
|
+
const payloadBytes = Buffer.byteLength(payload)
|
|
29
|
+
if (currentBuffered + payloadBytes > MAX_SUBSCRIBER_BUFFER) {
|
|
30
|
+
// Disconnect slow subscriber exceeding memory buffer limit (#186)
|
|
31
|
+
subscribers.delete(res)
|
|
32
|
+
detach(res)
|
|
33
|
+
try { res.end() } catch {}
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let writeOk = true
|
|
16
39
|
try {
|
|
17
|
-
res.write(payload)
|
|
18
|
-
return true
|
|
40
|
+
writeOk = res.write(payload)
|
|
19
41
|
} catch {
|
|
20
42
|
subscribers.delete(res)
|
|
21
43
|
detach(res)
|
|
22
44
|
return false
|
|
23
45
|
}
|
|
46
|
+
|
|
47
|
+
if (writeOk === false) {
|
|
48
|
+
res.__dtsBackpressured = true
|
|
49
|
+
res.__dtsBufferedBytes = (res.__dtsBufferedBytes || 0) + Buffer.byteLength(payload)
|
|
50
|
+
if (!res.__dtsOnDrain) {
|
|
51
|
+
const onDrain = () => {
|
|
52
|
+
res.__dtsBackpressured = false
|
|
53
|
+
res.__dtsBufferedBytes = 0
|
|
54
|
+
}
|
|
55
|
+
res.__dtsOnDrain = onDrain
|
|
56
|
+
try {
|
|
57
|
+
if (typeof res.on === 'function') res.on('drain', onDrain)
|
|
58
|
+
} catch { /* non-eventemitter mock */ }
|
|
59
|
+
}
|
|
60
|
+
} else if (res.__dtsBackpressured && (!res.writableLength || res.writableLength === 0)) {
|
|
61
|
+
res.__dtsBackpressured = false
|
|
62
|
+
res.__dtsBufferedBytes = 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return writeOk
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
function ensureHeartbeat() {
|
|
@@ -47,8 +89,10 @@ export function addStreamSubscriber(res) {
|
|
|
47
89
|
res.__dtsOnClose = onClose
|
|
48
90
|
res.__dtsOnError = onClose
|
|
49
91
|
try {
|
|
50
|
-
res.on
|
|
51
|
-
|
|
92
|
+
if (typeof res.on === 'function') {
|
|
93
|
+
res.on('close', onClose)
|
|
94
|
+
res.on('error', onClose)
|
|
95
|
+
}
|
|
52
96
|
} catch { /* non-node response object in tests */ }
|
|
53
97
|
ensureHeartbeat()
|
|
54
98
|
}
|
|
@@ -80,4 +124,4 @@ export function clearStreamSubscribers() {
|
|
|
80
124
|
clearInterval(heartbeatTimer)
|
|
81
125
|
heartbeatTimer = null
|
|
82
126
|
}
|
|
83
|
-
}
|
|
127
|
+
}
|
package/lib/text.js
CHANGED
|
@@ -59,7 +59,24 @@ function escapeRe(s) {
|
|
|
59
59
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export const BUILTIN_IT_DICTIONARY = [
|
|
62
|
+
export const BUILTIN_IT_DICTIONARY = [
|
|
63
|
+
{ from: 'API', to: 'A P I', whole: true, lang: '' },
|
|
64
|
+
{ from: 'CLI', to: 'C L I', whole: true, lang: '' },
|
|
65
|
+
{ from: 'CPU', to: 'C P U', whole: true, lang: '' },
|
|
66
|
+
{ from: 'GPU', to: 'G P U', whole: true, lang: '' },
|
|
67
|
+
{ from: 'RAM', to: 'ram', whole: true, lang: '' },
|
|
68
|
+
{ from: 'SQL', to: 'sequel', whole: true, lang: '' },
|
|
69
|
+
{ from: 'UI', to: 'U I', whole: true, lang: '' },
|
|
70
|
+
{ from: 'URL', to: 'U R L', whole: true, lang: '' },
|
|
71
|
+
{ from: 'UUID', to: 'U U I D', whole: true, lang: '' },
|
|
72
|
+
{ from: 'JSON', to: 'j-son', whole: true, lang: '' },
|
|
73
|
+
{ from: 'YAML', to: 'yam-el', whole: true, lang: '' },
|
|
74
|
+
{ from: 'HTML', to: 'H T M L', whole: true, lang: '' },
|
|
75
|
+
{ from: 'CSS', to: 'C S S', whole: true, lang: '' },
|
|
76
|
+
{ from: 'SSH', to: 'S S H', whole: true, lang: '' },
|
|
77
|
+
{ from: 'DNS', to: 'D N S', whole: true, lang: '' },
|
|
78
|
+
{ from: 'IP', to: 'I P', whole: true, lang: '' },
|
|
79
|
+
]
|
|
63
80
|
|
|
64
81
|
export function applyPronunciation(text, rules, language, enableBuiltinIt = false) {
|
|
65
82
|
let out = String(text || '')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.28",
|
|
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",
|