@goodandready/dsh-tts 0.4.1 → 0.4.5
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/breaker.js +64 -0
- package/lib/cache.js +1 -1
- package/lib/client-src/10-styles.js +8 -8
- package/lib/client-src/20-player.js +59 -21
- package/lib/client-src/30-locales.js +36 -0
- package/lib/client-src/40-editors.js +31 -51
- package/lib/client-src/50-settings.js +93 -36
- package/lib/client-src/60-card-dock.js +50 -44
- package/lib/client.js +277 -160
- package/lib/index.js +27 -4
- package/lib/local.js +5 -7
- package/lib/providers/cloud.js +278 -0
- package/lib/providers/constants.js +61 -0
- package/lib/providers/local.js +99 -0
- package/lib/providers.js +11 -435
- package/lib/routes.js +17 -10
- package/lib/stream-hub.js +61 -9
- package/lib/text.js +8 -8
- package/package.json +1 -1
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
const [saved, setSaved] = React.useState(false)
|
|
8
8
|
const [err, setErr] = React.useState('')
|
|
9
9
|
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
10
|
-
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0 })
|
|
10
|
+
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0, sseClients: 0, breaker: [], provider: '', queue: 0 })
|
|
11
|
+
const [advancedOpen, setAdvancedOpen] = React.useState(() => {
|
|
12
|
+
try { return localStorage.getItem('dsh-tts/advancedOpen') === '1' } catch { /* localStorage unavailable */ return false }
|
|
13
|
+
})
|
|
14
|
+
const autoplay = useAutoplayGate()
|
|
15
|
+
|
|
11
16
|
const ctx = props && props.ctx
|
|
12
17
|
const scope = React.useMemo(() => {
|
|
13
18
|
const s = (ctx && ctx.get && ctx.get('lanSettings')) || (ctx && ctx.settingsScope)
|
|
14
19
|
if (!s || !s.bind) return null
|
|
15
20
|
try {
|
|
16
21
|
return s.bind({ namespace: NS })
|
|
17
|
-
} catch (_) {
|
|
22
|
+
} catch (_) { /* settings scope binding failed */
|
|
18
23
|
return null
|
|
19
24
|
}
|
|
20
25
|
}, [ctx])
|
|
@@ -24,7 +29,7 @@
|
|
|
24
29
|
if (!scope || !scope.subscribe) return () => {}
|
|
25
30
|
try {
|
|
26
31
|
return scope.subscribe(cb) || (() => {})
|
|
27
|
-
} catch (_) {
|
|
32
|
+
} catch (_) { /* settings scope binding failed */
|
|
28
33
|
return () => {}
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -34,7 +39,7 @@
|
|
|
34
39
|
if (!scope || !scope.getSnapshot) return SNAPSHOT_LOADING
|
|
35
40
|
try {
|
|
36
41
|
return scope.getSnapshot() || SNAPSHOT_LOADING
|
|
37
|
-
} catch (_) {
|
|
42
|
+
} catch (_) { /* settings scope binding failed */
|
|
38
43
|
return SNAPSHOT_LOADING
|
|
39
44
|
}
|
|
40
45
|
}, [scope])
|
|
@@ -71,18 +76,33 @@
|
|
|
71
76
|
if (data && data.ok) setIntegrations(data)
|
|
72
77
|
}).catch(() => {})
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const loadTelemetry = () => {
|
|
80
|
+
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
81
|
+
if (!alive || !st) return
|
|
82
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
83
|
+
cacheHits: st.cacheHits || 0,
|
|
84
|
+
total: st.total || 0,
|
|
85
|
+
online: true,
|
|
86
|
+
breaker: st.breaker || prev.breaker || [],
|
|
87
|
+
}))
|
|
88
|
+
}).catch(() => {
|
|
89
|
+
if (alive) setTelemetry((prev) => Object.assign({}, prev, { online: false }))
|
|
90
|
+
})
|
|
91
|
+
fetch('/dsh-tts/status', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
92
|
+
if (!alive || !st) return
|
|
93
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
94
|
+
sseClients: st.sseClients || 0,
|
|
95
|
+
sseOk: (st.sseClients || 0) >= 0,
|
|
96
|
+
provider: (player.audio && player.provider) || (player.queue[0] && player.queue[0].provider) || '',
|
|
97
|
+
queue: player.queue.length,
|
|
98
|
+
}))
|
|
99
|
+
}).catch(() => {})
|
|
100
|
+
}
|
|
101
|
+
loadTelemetry()
|
|
102
|
+
const telTimer = setInterval(loadTelemetry, 3000)
|
|
103
|
+
// interval cleaned by effect return below
|
|
84
104
|
|
|
85
|
-
return () => { alive = false }
|
|
105
|
+
return () => { alive = false; clearInterval(telTimer) }
|
|
86
106
|
}, [])
|
|
87
107
|
|
|
88
108
|
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
@@ -99,7 +119,7 @@
|
|
|
99
119
|
headers: { 'Content-Type': 'application/json' },
|
|
100
120
|
body: JSON.stringify({ provider: provider, value: value }),
|
|
101
121
|
})
|
|
102
|
-
const data = await res.json().catch(() => ({}))
|
|
122
|
+
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
103
123
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
104
124
|
if (data && data.credentials) setCredentials(data.credentials)
|
|
105
125
|
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
@@ -113,7 +133,7 @@
|
|
|
113
133
|
headers: { 'Content-Type': 'application/json' },
|
|
114
134
|
body: JSON.stringify({ provider: provider }),
|
|
115
135
|
})
|
|
116
|
-
const data = await res.json().catch(() => ({}))
|
|
136
|
+
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
117
137
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
118
138
|
if (data && data.credentials) setCredentials(data.credentials)
|
|
119
139
|
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
@@ -134,7 +154,7 @@
|
|
|
134
154
|
headers: { 'Content-Type': 'application/json' },
|
|
135
155
|
body: JSON.stringify(Object.assign({}, draft, { keys: keys })),
|
|
136
156
|
})
|
|
137
|
-
const data = await res.json().catch(() => ({}))
|
|
157
|
+
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
138
158
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
139
159
|
applyPayload(data)
|
|
140
160
|
setKeyDrafts({})
|
|
@@ -153,6 +173,9 @@
|
|
|
153
173
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
154
174
|
}
|
|
155
175
|
const onClearCache = () => {
|
|
176
|
+
if (typeof window !== 'undefined' && window.confirm) {
|
|
177
|
+
if (!window.confirm(t('confirmClearCache'))) return
|
|
178
|
+
}
|
|
156
179
|
clearCacheNow().catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
157
180
|
}
|
|
158
181
|
|
|
@@ -164,7 +187,7 @@
|
|
|
164
187
|
.then((r) => r.json())
|
|
165
188
|
.then((d) => {
|
|
166
189
|
if (!d.ok) throw new Error((d.error && d.error.message) || 'HTTP error')
|
|
167
|
-
new Audio('data:' + d.mime + ';base64,' + d.audioBase64).play().catch(() => {})
|
|
190
|
+
new Audio('data:' + d.mime + ';base64,' + d.audioBase64).play().catch(() => { /* autoplay blocked */ })
|
|
168
191
|
})
|
|
169
192
|
.catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
170
193
|
}
|
|
@@ -199,16 +222,31 @@
|
|
|
199
222
|
}),
|
|
200
223
|
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
201
224
|
|
|
202
|
-
const
|
|
225
|
+
const autoplayBanner = autoplay.blocked
|
|
226
|
+
? React.createElement('div', { className: 'dts-unlock' },
|
|
227
|
+
React.createElement('span', null, t('autoplayBlocked')),
|
|
228
|
+
React.createElement('button', {
|
|
229
|
+
type: 'button', className: 'dts-unlock-btn',
|
|
230
|
+
onClick: () => player.unlockAudio && player.unlockAudio(),
|
|
231
|
+
}, t('enableSound')),
|
|
232
|
+
)
|
|
233
|
+
: null
|
|
234
|
+
const breakerBadges = (telemetry.breaker || []).filter((b) => b && b.open).slice(0, 2)
|
|
235
|
+
.map((b) => React.createElement('span', { key: b.id, className: 'dts-badge dts-badge-bad' }, t('breakerOpen') + ': ' + b.id))
|
|
236
|
+
const telemetryBadges = React.createElement('div', { className: 'dts-row', style: { marginBottom: '8px' }, 'data-dsh-tts-telemetry': '1' },
|
|
203
237
|
React.createElement('span', {
|
|
204
238
|
className: 'dts-badge ' + (telemetry.online ? 'dts-badge-on' : 'dts-badge-bad'),
|
|
205
|
-
}, telemetry.online ?
|
|
206
|
-
React.createElement('span', {
|
|
207
|
-
className: 'dts-badge dts-badge-on',
|
|
208
|
-
}, '⚡ SSE Active'),
|
|
239
|
+
}, telemetry.online ? t('statusOnline') : t('statusOffline')),
|
|
209
240
|
React.createElement('span', {
|
|
210
|
-
className: 'dts-badge',
|
|
211
|
-
},
|
|
241
|
+
className: 'dts-badge ' + (telemetry.sseClients > 0 ? 'dts-badge-on' : ''),
|
|
242
|
+
}, t('sseState') + ': ' + (telemetry.sseClients || 0)),
|
|
243
|
+
React.createElement('span', { className: 'dts-badge' },
|
|
244
|
+
t('queueLen') + ': ' + (telemetry.queue || player.queue.length || 0)),
|
|
245
|
+
telemetry.provider ? React.createElement('span', { className: 'dts-badge' },
|
|
246
|
+
t('activeProvider') + ': ' + telemetry.provider) : null,
|
|
247
|
+
React.createElement('span', { className: 'dts-badge' },
|
|
248
|
+
t('cacheStats') + ': ' + telemetry.cacheHits + ' / ' + telemetry.total),
|
|
249
|
+
...breakerBadges,
|
|
212
250
|
)
|
|
213
251
|
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-header' },
|
|
214
252
|
React.createElement('div', { className: 'dts-page-title' }, '🔊 ' + t('title')),
|
|
@@ -216,6 +254,7 @@
|
|
|
216
254
|
telemetryBadges,
|
|
217
255
|
)
|
|
218
256
|
return React.createElement('div', { className: 'dts-wrap' },
|
|
257
|
+
autoplayBanner,
|
|
219
258
|
React.createElement('div', { className: 'dts-block' },
|
|
220
259
|
titleLine,
|
|
221
260
|
React.createElement('label', { className: 'dts-row' },
|
|
@@ -269,6 +308,7 @@
|
|
|
269
308
|
React.createElement('div', { className: 'dts-sub' },
|
|
270
309
|
t('chainHintCard')),
|
|
271
310
|
React.createElement(ChainEditor, {
|
|
311
|
+
breaker: telemetry.breaker,
|
|
272
312
|
t: t,
|
|
273
313
|
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
274
314
|
credentials: credentials, keyDrafts: keyDrafts,
|
|
@@ -318,16 +358,33 @@
|
|
|
318
358
|
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
319
359
|
),
|
|
320
360
|
React.createElement('div', { className: 'dts-block' },
|
|
321
|
-
React.createElement('
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
361
|
+
React.createElement('button', {
|
|
362
|
+
type: 'button',
|
|
363
|
+
className: 'dts-head',
|
|
364
|
+
'aria-expanded': advancedOpen,
|
|
365
|
+
onClick: () => {
|
|
366
|
+
setAdvancedOpen((v) => {
|
|
367
|
+
const next = !v
|
|
368
|
+
try { localStorage.setItem('dsh-tts/advancedOpen', next ? '1' : '0') } catch { /* private mode */ }
|
|
369
|
+
return next
|
|
370
|
+
})
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
React.createElement('div', { className: 'dts-headText' },
|
|
374
|
+
React.createElement('div', { className: 'dts-title' }, t('advancedTitle')),
|
|
375
|
+
React.createElement('div', { className: 'dts-sub' }, t('advancedHint')),
|
|
376
|
+
),
|
|
377
|
+
),
|
|
378
|
+
advancedOpen ? React.createElement('div', { className: 'dts-body' },
|
|
379
|
+
numberField('maxChars', t('maxChars'), t('maxCharsHint'), 100),
|
|
380
|
+
numberField('sentenceChars', t('sentenceChars'), t('sentenceCharsHint'), 20),
|
|
381
|
+
numberField('timeoutMs', t('timeoutMs'), t('timeoutMsHint'), 1000),
|
|
382
|
+
numberField('maxQueue', t('maxQueue'), t('maxQueueHint'), 1),
|
|
383
|
+
textField('openaiBaseUrl', t('openaiBaseUrl'), t('openaiBaseUrlHint')),
|
|
384
|
+
textField('mimoBaseUrl', t('mimoBaseUrl'), t('mimoBaseUrlHint')),
|
|
385
|
+
textField('mimoFormat', t('mimoFormat'), t('mimoFormatHint')),
|
|
386
|
+
textField('minimaxBin', t('minimaxBin'), t('minimaxBinHint')),
|
|
387
|
+
) : null,
|
|
331
388
|
),
|
|
332
389
|
React.createElement('div', { className: 'dts-foot' },
|
|
333
390
|
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
React.createElement('button', {
|
|
48
48
|
type: 'button', className: 'dts-link', style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
49
49
|
title: text,
|
|
50
|
-
onClick: () => playStandalone(text),
|
|
50
|
+
onClick: () => playStandalone(text), title: text,
|
|
51
51
|
}, text.slice(0, 80)),
|
|
52
52
|
React.createElement('button', {
|
|
53
53
|
type: 'button', className: 'dts-link', title: '★',
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
)
|
|
57
57
|
return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
|
|
58
58
|
React.createElement('button', {
|
|
59
|
-
type: 'button', className: 'dts-link', title: '
|
|
59
|
+
type: 'button', className: 'dts-link', title: t('recent') || 'Recent',
|
|
60
60
|
onClick: () => setShowList((v) => !v),
|
|
61
61
|
}, '☰'),
|
|
62
62
|
showList ? React.createElement('div', {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
lists.favs.map((text) => row(text, true)),
|
|
68
68
|
React.createElement('div', { className: 'dts-h' }, '⟳'),
|
|
69
69
|
lists.items.filter((x) => !lists.favs.includes(x)).map((text) => row(text, false)),
|
|
70
|
-
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, '
|
|
70
|
+
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, t('recentEmpty') || 'No recent speech yet') : null,
|
|
71
71
|
) : null,
|
|
72
72
|
React.createElement('button', {
|
|
73
73
|
type: 'button',
|
|
@@ -85,11 +85,10 @@
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
88
|
+
* Fallback translator for the dock.
|
|
89
89
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* принципе, и привязка — единственный способ дать ей язык.
|
|
90
|
+
* Bound at registration for the page lifetime. The core draws the slot label;
|
|
91
|
+
* props.t never reaches it, so the binder is the only way to give it a language.
|
|
93
92
|
*/
|
|
94
93
|
let fallbackDockText = (key) => key
|
|
95
94
|
|
|
@@ -124,7 +123,7 @@
|
|
|
124
123
|
ctx.effect(() => {
|
|
125
124
|
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
126
125
|
return () => { for (const off of undo) off() }
|
|
127
|
-
}, 'dsh-tts:
|
|
126
|
+
}, 'dsh-tts: locales')
|
|
128
127
|
// The sidebar draws the section label, not our component: props.t never
|
|
129
128
|
// reaches it, so bind the translator to the namespace ourselves.
|
|
130
129
|
const t = ctx.locale.bind(NS)
|
|
@@ -168,7 +167,7 @@
|
|
|
168
167
|
exports.apply = function apply(ctx) {
|
|
169
168
|
registerSettings(ctx)
|
|
170
169
|
registerSpeaker(ctx)
|
|
171
|
-
ctx.effect(() => listenForVoice(), 'dsh-tts:
|
|
170
|
+
ctx.effect(() => listenForVoice(), 'dsh-tts: barge-in on user speech')
|
|
172
171
|
ctx.effect(() => {
|
|
173
172
|
if (typeof window === 'undefined') return () => {}
|
|
174
173
|
const onKey = (e) => {
|
|
@@ -178,47 +177,54 @@
|
|
|
178
177
|
}
|
|
179
178
|
window.addEventListener('keydown', onKey)
|
|
180
179
|
return () => window.removeEventListener('keydown', onKey)
|
|
181
|
-
}, 'dsh-tts:
|
|
180
|
+
}, 'dsh-tts: hotkeys')
|
|
182
181
|
ctx.effect(() => {
|
|
183
182
|
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
184
183
|
let es = null
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
184
|
+
let closed = false
|
|
185
|
+
let attempt = 0
|
|
186
|
+
let retryTimer = null
|
|
187
|
+
const onItem = (e) => {
|
|
188
|
+
try {
|
|
189
|
+
const item = JSON.parse(e.data)
|
|
190
|
+
if (!item || !item.id) return
|
|
191
|
+
if (seenIds.has(item.id)) return
|
|
192
|
+
seenIds.add(item.id)
|
|
193
|
+
if (seenIds.size > 500) {
|
|
194
|
+
const first = seenIds.values().next().value
|
|
195
|
+
seenIds.delete(first)
|
|
196
|
+
}
|
|
197
|
+
player.queue.push(item)
|
|
198
|
+
drainQueue()
|
|
199
|
+
} catch { /* ignore malformed SSE frame */ }
|
|
200
|
+
}
|
|
201
|
+
const connect = () => {
|
|
202
|
+
if (closed) return
|
|
203
|
+
try {
|
|
204
|
+
es = new EventSource('/dsh-tts/stream')
|
|
205
|
+
es.addEventListener('open', () => { attempt = 0 })
|
|
206
|
+
es.addEventListener('utterance', onItem)
|
|
207
|
+
es.addEventListener('chime', onItem)
|
|
208
|
+
es.onerror = () => {
|
|
209
|
+
try { es.close() } catch { /* already closed */ }
|
|
210
|
+
if (closed) return
|
|
211
|
+
attempt += 1
|
|
212
|
+
const delay = Math.min(15000, 500 * Math.pow(2, Math.min(attempt, 5)))
|
|
213
|
+
retryTimer = setTimeout(connect, delay)
|
|
214
|
+
}
|
|
215
|
+
} catch { /* EventSource constructor failed — poll remains */ }
|
|
216
|
+
}
|
|
217
|
+
connect()
|
|
218
|
+
return () => {
|
|
219
|
+
closed = true
|
|
220
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
221
|
+
if (es) { try { es.close() } catch { /* already closed */ } }
|
|
222
|
+
}
|
|
223
|
+
}, 'dsh-tts: SSE audio stream')
|
|
218
224
|
ctx.effect(() => {
|
|
219
225
|
const timer = setInterval(pollPending, 1000)
|
|
220
226
|
return () => clearInterval(timer)
|
|
221
|
-
}, 'dsh-tts:
|
|
227
|
+
}, 'dsh-tts: poll pending audio')
|
|
222
228
|
}
|
|
223
229
|
return module.exports
|
|
224
230
|
},
|