@goodandready/dsh-tts 0.4.0 → 0.4.2
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/README.md +10 -0
- package/README.ru.md +10 -0
- package/README.zh.md +8 -0
- package/lib/breaker.js +64 -0
- package/lib/cache.js +1 -1
- package/lib/client-src/00-open.js +85 -0
- package/lib/client-src/10-styles.js +54 -0
- package/lib/client-src/20-player.js +328 -0
- package/lib/client-src/30-locales.js +361 -0
- package/lib/client-src/40-editors.js +390 -0
- package/lib/client-src/50-settings.js +398 -0
- package/lib/client-src/60-card-dock.js +235 -0
- package/lib/client.js +896 -622
- package/lib/index.js +29 -4
- package/lib/local.js +5 -7
- package/lib/providers/constants.js +61 -0
- package/lib/providers.js +3 -61
- package/lib/routes.js +19 -9
- package/lib/stream-hub.js +61 -9
- package/lib/text.js +3 -3
- package/package.json +4 -2
- package/scripts/build-client.mjs +19 -0
package/lib/client.js
CHANGED
|
@@ -38,34 +38,96 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
f5: 'not bundled — offline neural runtime unavailable',
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
|
|
42
|
+
function createErrorBoundary() {
|
|
43
|
+
if (!React || typeof React.Component !== 'function') {
|
|
44
|
+
return function NoopBoundary(props) { return (props && props.children) || null }
|
|
45
|
+
}
|
|
46
|
+
return class ErrorBoundary extends React.Component {
|
|
47
|
+
constructor(props) {
|
|
48
|
+
super(props)
|
|
49
|
+
this.state = { hasError: false, error: null }
|
|
50
|
+
}
|
|
51
|
+
static getDerivedStateFromError(error) {
|
|
52
|
+
return { hasError: true, error }
|
|
53
|
+
}
|
|
54
|
+
componentDidCatch(error, errorInfo) {
|
|
55
|
+
console.error('[dsh-tts] React Error:', error, errorInfo)
|
|
56
|
+
}
|
|
57
|
+
render() {
|
|
58
|
+
if (this.state.hasError) {
|
|
59
|
+
return React.createElement(
|
|
60
|
+
'div',
|
|
61
|
+
{
|
|
62
|
+
className: 'dts-alert dts-alert-err',
|
|
63
|
+
style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
|
|
64
|
+
},
|
|
65
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ TTS UI Error:'),
|
|
66
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String((this.state.error && this.state.error.message) || this.state.error)),
|
|
67
|
+
React.createElement(
|
|
68
|
+
'button',
|
|
69
|
+
{
|
|
70
|
+
type: 'button',
|
|
71
|
+
className: 'dts-btn',
|
|
72
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
73
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
74
|
+
},
|
|
75
|
+
'Retry',
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return (this.props && this.props.children) || null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const ErrorBoundary = createErrorBoundary()
|
|
84
|
+
const SNAPSHOT_READY = Object.freeze({ status: 'ready', value: {} })
|
|
85
|
+
const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', value: {} })
|
|
86
|
+
|
|
41
87
|
const SET_CSS =
|
|
42
|
-
'.dts-wrap{display:flex;flex-direction:column;gap:
|
|
43
|
-
'.dts-
|
|
44
|
-
'.dts-
|
|
45
|
-
|
|
88
|
+
'.dts-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:900px}' +
|
|
89
|
+
'.dts-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
|
|
90
|
+
'.dts-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}' +
|
|
91
|
+
'.dts-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}' +
|
|
92
|
+
'.dts-block{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
93
|
+
'.dts-h{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
94
|
+
'.dts-sub{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
|
|
46
95
|
'.dts-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
47
96
|
'.dts-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
48
97
|
'.dts-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
49
98
|
'.dts-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
50
|
-
'.dts-sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
51
99
|
'.dts-chev{margin-left:auto;color:var(--dsw-alias-label-secondary);flex:none;transition:transform .16s}' +
|
|
52
100
|
'.dts-chevOpen{transform:rotate(180deg)}' +
|
|
53
|
-
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding
|
|
54
|
-
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:
|
|
55
|
-
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-
|
|
56
|
-
'.dts-
|
|
57
|
-
'.dts-
|
|
58
|
-
|
|
59
|
-
'.dts-
|
|
101
|
+
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0}' +
|
|
102
|
+
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
103
|
+
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
104
|
+
'.dts-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
105
|
+
'.dts-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:14px 0 4px}' +
|
|
106
|
+
'.dts-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:6px 16px;font-size:13px;font-weight:500;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);transition:all .15s ease}' +
|
|
107
|
+
'.dts-save:hover:not(:disabled){opacity:0.88}' +
|
|
108
|
+
'.dts-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
109
|
+
'.dts-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
110
|
+
'.dts-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
111
|
+
'.dts-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}' +
|
|
112
|
+
'.dts-entry{display:flex;flex-direction:column;gap:8px;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}' +
|
|
60
113
|
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
61
114
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
62
115
|
'.dts-row .dts-key{flex:1;min-width:180px}' +
|
|
63
|
-
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:
|
|
64
|
-
'.dts-
|
|
65
|
-
'.dts-
|
|
66
|
-
'.dts-
|
|
67
|
-
'.dts-badge-
|
|
68
|
-
'.dts-
|
|
116
|
+
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:13px;transition:all .15s ease}' +
|
|
117
|
+
'.dts-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2))}' +
|
|
118
|
+
'.dts-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);font-weight:500}' +
|
|
119
|
+
'.dts-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);font-weight:500}' +
|
|
120
|
+
'.dts-badge{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
121
|
+
'.dts-badge-on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}' +
|
|
122
|
+
'.dts-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}' +
|
|
123
|
+
'.dts-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}' +
|
|
124
|
+
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0;text-decoration:underline}' +
|
|
125
|
+
'.dts-link:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
|
|
126
|
+
'.dts-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
|
|
127
|
+
'.dts-alert-err{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 20%, transparent)}' +
|
|
128
|
+
'.dts-alert-warn{background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent);color:var(--dsw-alias-state-warning-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 20%, transparent)}' +
|
|
129
|
+
'.dts-alert-ok{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);border:1px solid rgba(16,185,129,0.2)}' +
|
|
130
|
+
'.dts-grow{flex:1;min-width:100px}'
|
|
69
131
|
|
|
70
132
|
const setCssId = 'dsh-tts/settings.module.css'
|
|
71
133
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-tts"]')) {
|
|
@@ -75,12 +137,43 @@ window.__ModuleLoader__.load({
|
|
|
75
137
|
tag.dataset.pluginCss = setCssId
|
|
76
138
|
document.head.appendChild(tag)
|
|
77
139
|
}
|
|
78
|
-
|
|
79
140
|
// Player.
|
|
80
141
|
//
|
|
81
142
|
// Each incoming chunk used to replace the previous one immediately; while reading
|
|
82
143
|
// For full replies this was invisible; with speak-as-it-goes only the last
|
|
83
144
|
// chunk would be heard. Now items queue: the next starts after the previous ends.
|
|
145
|
+
const autoplay = { blocked: false, listeners: new Set() }
|
|
146
|
+
|
|
147
|
+
function setAutoplayBlocked(next) {
|
|
148
|
+
if (autoplay.blocked === next) return
|
|
149
|
+
autoplay.blocked = next
|
|
150
|
+
for (const l of [...autoplay.listeners]) {
|
|
151
|
+
try { l() } catch { /* listener isolation */ }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unlockAudio() {
|
|
156
|
+
try {
|
|
157
|
+
const AC = window.AudioContext || window.webkitAudioContext
|
|
158
|
+
if (AC) {
|
|
159
|
+
const ctx = new AC()
|
|
160
|
+
if (ctx.state === 'suspended') ctx.resume().catch(() => {})
|
|
161
|
+
setTimeout(() => { try { ctx.close() } catch { /* closed */ } }, 300)
|
|
162
|
+
}
|
|
163
|
+
} catch { /* no AudioContext */ }
|
|
164
|
+
setAutoplayBlocked(false)
|
|
165
|
+
drainQueue()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function useAutoplayGate() {
|
|
169
|
+
const [, force] = React.useReducer((n) => n + 1, 0)
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
autoplay.listeners.add(force)
|
|
172
|
+
return () => { autoplay.listeners.delete(force) }
|
|
173
|
+
}, [])
|
|
174
|
+
return autoplay
|
|
175
|
+
}
|
|
176
|
+
|
|
84
177
|
const player = {
|
|
85
178
|
audio: null,
|
|
86
179
|
after: '',
|
|
@@ -92,13 +185,15 @@ window.__ModuleLoader__.load({
|
|
|
92
185
|
busy: false,
|
|
93
186
|
paused: false,
|
|
94
187
|
listeners: new Set(),
|
|
188
|
+
unlockAudio,
|
|
189
|
+
useAutoplayGate,
|
|
190
|
+
get autoplayBlocked() { return autoplay.blocked },
|
|
95
191
|
}
|
|
96
192
|
const seenIds = new Set()
|
|
97
193
|
let currentAudioResolve = null
|
|
98
|
-
|
|
99
194
|
function playerChanged() {
|
|
100
195
|
for (const listener of [...player.listeners]) {
|
|
101
|
-
try { listener() } catch (listenerFailure) { /*
|
|
196
|
+
try { listener() } catch (listenerFailure) { /* foreign listener failure is not ours */ }
|
|
102
197
|
}
|
|
103
198
|
}
|
|
104
199
|
|
|
@@ -133,8 +228,8 @@ window.__ModuleLoader__.load({
|
|
|
133
228
|
osc.stop(at + 0.2)
|
|
134
229
|
at += 0.22
|
|
135
230
|
}
|
|
136
|
-
setTimeout(() => { try { audioCtx.close() } catch (already) { /*
|
|
137
|
-
} catch
|
|
231
|
+
setTimeout(() => { try { audioCtx.close() } catch (already) { /* already closed */ } resolve() }, 600)
|
|
232
|
+
} catch { resolve() /* no AudioContext */ }
|
|
138
233
|
})
|
|
139
234
|
}
|
|
140
235
|
|
|
@@ -149,7 +244,7 @@ window.__ModuleLoader__.load({
|
|
|
149
244
|
utterance.onend = () => resolve()
|
|
150
245
|
utterance.onerror = () => resolve()
|
|
151
246
|
window.speechSynthesis.speak(utterance)
|
|
152
|
-
} catch
|
|
247
|
+
} catch { resolve() /* no speechSynthesis */ }
|
|
153
248
|
})
|
|
154
249
|
}
|
|
155
250
|
|
|
@@ -164,7 +259,7 @@ window.__ModuleLoader__.load({
|
|
|
164
259
|
const audio = new Audio(url)
|
|
165
260
|
audio.preload = 'auto'
|
|
166
261
|
item.prepared = { audio, url }
|
|
167
|
-
} catch
|
|
262
|
+
} catch { /* decode error — item stays unprepared */ }
|
|
168
263
|
return item
|
|
169
264
|
}
|
|
170
265
|
|
|
@@ -192,7 +287,7 @@ window.__ModuleLoader__.load({
|
|
|
192
287
|
if (doneCalled) return
|
|
193
288
|
doneCalled = true
|
|
194
289
|
currentAudioResolve = null
|
|
195
|
-
try { URL.revokeObjectURL(url) } catch
|
|
290
|
+
try { URL.revokeObjectURL(url) } catch { /* URL already revoked */ }
|
|
196
291
|
item.prepared = null
|
|
197
292
|
player.audio = null
|
|
198
293
|
playerChanged()
|
|
@@ -201,11 +296,14 @@ window.__ModuleLoader__.load({
|
|
|
201
296
|
currentAudioResolve = done
|
|
202
297
|
audio.onended = done
|
|
203
298
|
audio.onerror = done
|
|
204
|
-
audio.play().catch(() => {
|
|
205
|
-
|
|
299
|
+
audio.play().catch((playErr) => {
|
|
300
|
+
// Autoplay without a user gesture is blocked — surface unlock UX, then stop this chunk.
|
|
301
|
+
if (playErr && (playErr.name === 'NotAllowedError' || playErr.name === 'SecurityError')) {
|
|
302
|
+
setAutoplayBlocked(true)
|
|
303
|
+
}
|
|
206
304
|
done()
|
|
207
305
|
})
|
|
208
|
-
} catch
|
|
306
|
+
} catch { /* cannot decode — skip chunk */
|
|
209
307
|
currentAudioResolve = null
|
|
210
308
|
resolve()
|
|
211
309
|
}
|
|
@@ -234,26 +332,27 @@ window.__ModuleLoader__.load({
|
|
|
234
332
|
function stopPlayback() {
|
|
235
333
|
for (const item of player.queue) {
|
|
236
334
|
if (item && item.prepared && item.prepared.url) {
|
|
237
|
-
try { URL.revokeObjectURL(item.prepared.url) } catch
|
|
335
|
+
try { URL.revokeObjectURL(item.prepared.url) } catch { /* already revoked */ }
|
|
238
336
|
item.prepared = null
|
|
239
337
|
}
|
|
240
338
|
}
|
|
241
339
|
player.queue.length = 0
|
|
242
340
|
player.paused = false
|
|
341
|
+
if (player.audio) {
|
|
342
|
+
try { player.audio.pause() } catch { /* already paused */ }
|
|
343
|
+
}
|
|
243
344
|
if (currentAudioResolve) {
|
|
244
345
|
currentAudioResolve()
|
|
245
|
-
} else if (player.audio) {
|
|
246
|
-
try { player.audio.pause() } catch (already) { /* стоит */ }
|
|
247
|
-
player.audio = null
|
|
248
346
|
}
|
|
249
|
-
|
|
347
|
+
player.audio = null
|
|
348
|
+
try { if (window.speechSynthesis) window.speechSynthesis.cancel() } catch { /* no speechSynthesis */ }
|
|
250
349
|
playerChanged()
|
|
251
350
|
}
|
|
252
351
|
|
|
253
352
|
function togglePause() {
|
|
254
353
|
player.paused = !player.paused
|
|
255
354
|
if (player.audio) {
|
|
256
|
-
try { player.paused ? player.audio.pause() : player.audio.play() } catch (already) { /*
|
|
355
|
+
try { player.paused ? player.audio.pause() : player.audio.play() } catch (already) { /* ignore play/pause failure */ }
|
|
257
356
|
}
|
|
258
357
|
if (!player.paused) drainQueue()
|
|
259
358
|
playerChanged()
|
|
@@ -263,7 +362,7 @@ window.__ModuleLoader__.load({
|
|
|
263
362
|
if (currentAudioResolve) {
|
|
264
363
|
currentAudioResolve()
|
|
265
364
|
} else if (player.audio) {
|
|
266
|
-
try { player.audio.pause() } catch (already) { /*
|
|
365
|
+
try { player.audio.pause() } catch (already) { /* already stopped */ }
|
|
267
366
|
player.audio = null
|
|
268
367
|
playerChanged()
|
|
269
368
|
if (player.queue.length) drainQueue()
|
|
@@ -317,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
317
416
|
player.queue.push(item)
|
|
318
417
|
}
|
|
319
418
|
if (player.queue.length) drainQueue()
|
|
320
|
-
} catch (hostRestarting) { /*
|
|
419
|
+
} catch (hostRestarting) { /* host restarting */ }
|
|
321
420
|
}
|
|
322
421
|
|
|
323
422
|
// Recent utterances: last reply texts and favorites (localStorage).
|
|
@@ -328,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
328
427
|
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(KEY) : null
|
|
329
428
|
st = JSON.parse(raw) || { items: [], favs: [] }
|
|
330
429
|
} catch (broken) { st = { items: [], favs: [] } }
|
|
331
|
-
const persist = () => { try { if (typeof localStorage !== 'undefined') localStorage.setItem(KEY, JSON.stringify(st)) } catch (full) { /*
|
|
430
|
+
const persist = () => { try { if (typeof localStorage !== 'undefined') localStorage.setItem(KEY, JSON.stringify(st)) } catch (full) { /* private mode / quota */ } }
|
|
332
431
|
return {
|
|
333
432
|
add(text) {
|
|
334
433
|
if (!text || text.length < 2) return
|
|
@@ -366,77 +465,29 @@ window.__ModuleLoader__.load({
|
|
|
366
465
|
return (map && map[provider]) || { configured: false, writable: true, ref: '' }
|
|
367
466
|
}
|
|
368
467
|
|
|
369
|
-
function ChainEditor(props) {
|
|
370
|
-
// Translator comes from the card: the slot passes props.t only to it.
|
|
371
|
-
const t = props.t || ((key) => key)
|
|
372
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
373
|
-
const change = (i, patch) => {
|
|
374
|
-
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
375
|
-
props.onChange(next)
|
|
376
|
-
}
|
|
377
|
-
const move = (i, delta) => {
|
|
378
|
-
const j = i + delta
|
|
379
|
-
if (j < 0 || j >= rows.length) return
|
|
380
|
-
const next = rows.slice()
|
|
381
|
-
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
382
|
-
props.onChange(next)
|
|
383
|
-
}
|
|
384
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
385
|
-
const add = () => props.onChange(rows.concat([{ provider: 'espeak', model: '', voice: '' }]))
|
|
386
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
387
|
-
rows.map((row, i) => {
|
|
388
|
-
const cloud = !!CLOUD[row.provider]
|
|
389
|
-
const cred = credOf(props.credentials, row.provider)
|
|
390
|
-
const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
|
|
391
|
-
const badge = !cloud ? null
|
|
392
|
-
: (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
|
|
393
|
-
return React.createElement('div', { className: 'dts-entry', key: i },
|
|
394
|
-
React.createElement('div', { className: 'dts-row' },
|
|
395
|
-
React.createElement('select', {
|
|
396
|
-
className: 'dts-input',
|
|
397
|
-
value: row.provider, disabled: !props.writable,
|
|
398
|
-
onChange: (e) => change(i, { provider: e.target.value }),
|
|
399
|
-
}, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
400
|
-
React.createElement('input', {
|
|
401
|
-
className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
|
|
402
|
-
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
403
|
-
}),
|
|
404
|
-
React.createElement('input', {
|
|
405
|
-
className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
|
|
406
|
-
placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
|
|
407
|
-
}),
|
|
408
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
|
|
409
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
|
|
410
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
|
|
411
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
412
|
-
),
|
|
413
|
-
cloud ? React.createElement('div', { className: 'dts-row' },
|
|
414
|
-
React.createElement('input', {
|
|
415
|
-
className: 'dts-key dts-input', type: 'password', autoComplete: 'off',
|
|
416
|
-
value: draft, disabled: !props.writable || !cred.writable,
|
|
417
|
-
placeholder: cred.configured ? 'leave blank to keep' : 'paste API key',
|
|
418
|
-
onChange: (e) => props.onDraft(row.provider, e.target.value),
|
|
419
|
-
onBlur: (e) => props.onCommitKey(row.provider, e.target.value),
|
|
420
|
-
}),
|
|
421
|
-
React.createElement('span', { className: 'dts-badge' + (cred.configured ? ' dts-badge-on' : '') }, badge),
|
|
422
|
-
cred.configured && cred.writable ? React.createElement('button', {
|
|
423
|
-
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
424
|
-
onClick: () => props.onClearKey(row.provider),
|
|
425
|
-
}, t('clear')) : null,
|
|
426
|
-
) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
|
|
427
|
-
cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
|
|
428
|
-
)
|
|
429
|
-
}),
|
|
430
|
-
React.createElement('div', { className: 'dts-row' },
|
|
431
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
432
|
-
React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
|
|
433
|
-
),
|
|
434
|
-
)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Card strings live in the locale registry so a separate package can translate
|
|
438
|
-
// them without touching this plugin. English is the source language and fallback.
|
|
439
468
|
const en = {
|
|
469
|
+
'statusOnline': 'Host online',
|
|
470
|
+
'recent': 'Recent',
|
|
471
|
+
'recentEmpty': 'No recent speech yet',
|
|
472
|
+
'retry': 'Retry',
|
|
473
|
+
'advancedCollapsed': 'Advanced',
|
|
474
|
+
'confirmClearCache': 'Clear the synthesis cache? This cannot be undone.',
|
|
475
|
+
'confirmDeleteModel': 'Delete downloaded weights for this engine?',
|
|
476
|
+
'autoplayBlocked': 'Sound is blocked until you click or tap once',
|
|
477
|
+
'enableSound': 'Enable sound',
|
|
478
|
+
'runtimeNotBundled': 'Neural inference runtime is not bundled',
|
|
479
|
+
'runtimeNotBundledHint': 'Install weights does not enable speech. Use Edge, Piper, or eSpeak for offline TTS.',
|
|
480
|
+
'testProvider': 'Test provider',
|
|
481
|
+
'breakerOpen': 'Circuit open',
|
|
482
|
+
'lastError': 'Last error',
|
|
483
|
+
'activeProvider': 'Active provider',
|
|
484
|
+
'queueLen': 'Queue',
|
|
485
|
+
'sseState': 'SSE',
|
|
486
|
+
'currentVoice': 'Voice',
|
|
487
|
+
|
|
488
|
+
'statusOffline': 'Host unreachable',
|
|
489
|
+
'cacheStats': 'Cache',
|
|
490
|
+
|
|
440
491
|
'general': 'General',
|
|
441
492
|
'loading': 'Loading\u2026',
|
|
442
493
|
'chainHintCard': 'Pick a provider, paste its API key, and Save (or leave the key field). The key is written to the host credentials store immediately. The browser never reads it back.',
|
|
@@ -470,6 +521,24 @@ window.__ModuleLoader__.load({
|
|
|
470
521
|
'edgeHint': 'Python CLI from the edge-tts package.',
|
|
471
522
|
'azureRegion': 'Azure region',
|
|
472
523
|
'azureHint': 'Required for the azure provider, e.g. eastus.',
|
|
524
|
+
'openaiBaseUrl': 'OpenAI base URL',
|
|
525
|
+
'openaiBaseUrlHint': 'OpenAI-compatible origin for the openai provider.',
|
|
526
|
+
'mimoBaseUrl': 'MiMo base URL',
|
|
527
|
+
'mimoBaseUrlHint': 'MiMo API root; synthesis uses its chat endpoint.',
|
|
528
|
+
'mimoFormat': 'MiMo format',
|
|
529
|
+
'mimoFormatHint': 'mp3 or wav.',
|
|
530
|
+
'minimaxBin': 'MiniMax CLI',
|
|
531
|
+
'minimaxBinHint': 'Looked up in PATH unless absolute; install and log in separately.',
|
|
532
|
+
'advancedTitle': 'Advanced',
|
|
533
|
+
'advancedHint': 'Limits and endpoint overrides. Provider API keys stay in the chain editor; credential slot names (*KeyEnv) are config-only.',
|
|
534
|
+
'maxChars': 'Max characters',
|
|
535
|
+
'maxCharsHint': 'Hard cap before long-reply handling; 0 disables.',
|
|
536
|
+
'sentenceChars': 'Sentence chunk size',
|
|
537
|
+
'sentenceCharsHint': 'Max characters per streaming sentence piece.',
|
|
538
|
+
'timeoutMs': 'Provider timeout (ms)',
|
|
539
|
+
'timeoutMsHint': 'Abort a provider attempt after this many milliseconds.',
|
|
540
|
+
'maxQueue': 'Max queue length',
|
|
541
|
+
'maxQueueHint': 'Drop oldest pending pieces beyond this count.',
|
|
473
542
|
'chainTitle': 'Provider chain',
|
|
474
543
|
'addProvider': 'Add provider',
|
|
475
544
|
'localProvider': 'Local / CLI provider — no API key',
|
|
@@ -577,6 +646,28 @@ window.__ModuleLoader__.load({
|
|
|
577
646
|
'dockStop': 'Stop speech',
|
|
578
647
|
}
|
|
579
648
|
const ru = {
|
|
649
|
+
'statusOnline': 'Хост на связи',
|
|
650
|
+
'recent': 'Недавние',
|
|
651
|
+
'recentEmpty': 'Пока нет реплик',
|
|
652
|
+
'retry': 'Повторить',
|
|
653
|
+
'advancedCollapsed': 'Дополнительно',
|
|
654
|
+
'confirmClearCache': 'Очистить кэш синтеза? Действие необратимо.',
|
|
655
|
+
'confirmDeleteModel': 'Удалить скачанные веса движка?',
|
|
656
|
+
'autoplayBlocked': 'Звук заблокирован — один раз кликните или коснитесь экрана',
|
|
657
|
+
'enableSound': 'Включить звук',
|
|
658
|
+
'runtimeNotBundled': 'Рантайм нейросинтеза не входит в пакет',
|
|
659
|
+
'runtimeNotBundledHint': 'Установка весов не включает речь. Для offline используйте Edge, Piper или eSpeak.',
|
|
660
|
+
'testProvider': 'Проверить провайдера',
|
|
661
|
+
'breakerOpen': 'Переключатель открыт',
|
|
662
|
+
'lastError': 'Последняя ошибка',
|
|
663
|
+
'activeProvider': 'Активный провайдер',
|
|
664
|
+
'queueLen': 'Очередь',
|
|
665
|
+
'sseState': 'SSE',
|
|
666
|
+
'currentVoice': 'Голос',
|
|
667
|
+
|
|
668
|
+
'statusOffline': 'Хост недоступен',
|
|
669
|
+
'cacheStats': 'Кэш',
|
|
670
|
+
|
|
580
671
|
'dockLabel': 'Чтение',
|
|
581
672
|
'dockPause': 'Пауза',
|
|
582
673
|
'dockResume': 'Продолжить чтение',
|
|
@@ -615,6 +706,24 @@ window.__ModuleLoader__.load({
|
|
|
615
706
|
'edgeHint': 'Консольная утилита из пакета edge-tts.',
|
|
616
707
|
'azureRegion': 'Azure: регион',
|
|
617
708
|
'azureHint': 'Нужен провайдеру azure, например eastus.',
|
|
709
|
+
'openaiBaseUrl': 'OpenAI base URL',
|
|
710
|
+
'openaiBaseUrlHint': 'OpenAI-совместимый origin для провайдера openai.',
|
|
711
|
+
'mimoBaseUrl': 'MiMo base URL',
|
|
712
|
+
'mimoBaseUrlHint': 'Корень API MiMo; синтез идёт через chat endpoint.',
|
|
713
|
+
'mimoFormat': 'Формат MiMo',
|
|
714
|
+
'mimoFormatHint': 'mp3 или wav.',
|
|
715
|
+
'minimaxBin': 'MiniMax CLI',
|
|
716
|
+
'minimaxBinHint': 'Ищется в PATH, если путь не абсолютный; вход в CLI — отдельно.',
|
|
717
|
+
'advancedTitle': 'Дополнительно',
|
|
718
|
+
'advancedHint': 'Лимиты и override endpoint. API-ключи — в редакторе цепочки; имена credential-слотов (*KeyEnv) — только в конфиге.',
|
|
719
|
+
'maxChars': 'Максимум символов',
|
|
720
|
+
'maxCharsHint': 'Жёсткий лимит до обработки длинных ответов; 0 — без лимита.',
|
|
721
|
+
'sentenceChars': 'Размер куска (символы)',
|
|
722
|
+
'sentenceCharsHint': 'Максимум символов в streaming-куске предложения.',
|
|
723
|
+
'timeoutMs': 'Таймаут провайдера (мс)',
|
|
724
|
+
'timeoutMsHint': 'Прервать попытку провайдера через столько миллисекунд.',
|
|
725
|
+
'maxQueue': 'Длина очереди',
|
|
726
|
+
'maxQueueHint': 'Отбрасывать самые старые куски сверх этого числа.',
|
|
618
727
|
'chainTitle': 'Цепочка провайдеров',
|
|
619
728
|
'addProvider': 'Добавить провайдера',
|
|
620
729
|
'localProvider': 'Локальный провайдер — ключ не нужен',
|
|
@@ -717,136 +826,467 @@ window.__ModuleLoader__.load({
|
|
|
717
826
|
'messengerTtsEnabledHint': 'Разрешить мосту мессенджеров озвучивать ответы голосовыми заметками.',
|
|
718
827
|
}
|
|
719
828
|
|
|
829
|
+
function ChainEditor(props) {
|
|
830
|
+
// Translator comes from the card: the slot passes props.t only to it.
|
|
831
|
+
const t = props.t || ((key) => key)
|
|
832
|
+
const rows = Array.isArray(props.value) ? props.value : []
|
|
833
|
+
const change = (i, patch) => {
|
|
834
|
+
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
835
|
+
props.onChange(next)
|
|
836
|
+
}
|
|
837
|
+
const move = (i, delta) => {
|
|
838
|
+
const j = i + delta
|
|
839
|
+
if (j < 0 || j >= rows.length) return
|
|
840
|
+
const next = rows.slice()
|
|
841
|
+
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
842
|
+
props.onChange(next)
|
|
843
|
+
}
|
|
844
|
+
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
845
|
+
const add = () => props.onChange(rows.concat([{ provider: 'espeak', model: '', voice: '' }]))
|
|
846
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
847
|
+
rows.map((row, i) => {
|
|
848
|
+
const cloud = !!CLOUD[row.provider]
|
|
849
|
+
const cred = credOf(props.credentials, row.provider)
|
|
850
|
+
const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
|
|
851
|
+
const badge = !cloud ? null
|
|
852
|
+
: (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
|
|
853
|
+
return React.createElement('div', { className: 'dts-entry', key: i },
|
|
854
|
+
React.createElement('div', { className: 'dts-row' },
|
|
855
|
+
React.createElement('select', {
|
|
856
|
+
className: 'dts-input',
|
|
857
|
+
value: row.provider, disabled: !props.writable,
|
|
858
|
+
onChange: (e) => change(i, { provider: e.target.value }),
|
|
859
|
+
}, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
860
|
+
React.createElement('input', {
|
|
861
|
+
className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
|
|
862
|
+
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
863
|
+
}),
|
|
864
|
+
React.createElement('input', {
|
|
865
|
+
className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
|
|
866
|
+
placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
|
|
867
|
+
}),
|
|
868
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
|
|
869
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
|
|
870
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
|
|
871
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
872
|
+
),
|
|
873
|
+
cloud ? React.createElement('div', { className: 'dts-row' },
|
|
874
|
+
React.createElement('input', {
|
|
875
|
+
className: 'dts-key dts-input', type: 'password', autoComplete: 'off',
|
|
876
|
+
value: draft, disabled: !props.writable || !cred.writable,
|
|
877
|
+
placeholder: cred.configured ? 'leave blank to keep' : 'paste API key',
|
|
878
|
+
onChange: (e) => props.onDraft(row.provider, e.target.value),
|
|
879
|
+
onBlur: (e) => props.onCommitKey(row.provider, e.target.value),
|
|
880
|
+
}),
|
|
881
|
+
React.createElement('span', { className: 'dts-badge' + (cred.configured ? ' dts-badge-on' : '') }, badge),
|
|
882
|
+
cred.configured && cred.writable ? React.createElement('button', {
|
|
883
|
+
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
884
|
+
onClick: () => props.onClearKey(row.provider),
|
|
885
|
+
}, t('clear')) : null,
|
|
886
|
+
) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
|
|
887
|
+
cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
|
|
888
|
+
(() => {
|
|
889
|
+
const hit = Array.isArray(props.breaker) ? props.breaker.find((b) => b && b.id === row.provider) : null
|
|
890
|
+
if (!hit || (!hit.open && !hit.lastError)) return null
|
|
891
|
+
return React.createElement('div', { className: 'dts-sub' },
|
|
892
|
+
(hit.open ? t('breakerOpen') + ' · ' : '') + (hit.lastError || '')
|
|
893
|
+
)
|
|
894
|
+
})(),
|
|
895
|
+
)
|
|
896
|
+
}),
|
|
897
|
+
React.createElement('div', { className: 'dts-row' },
|
|
898
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
899
|
+
React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
|
|
900
|
+
),
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Card strings live in the locale registry so a separate package can translate
|
|
905
|
+
// them without touching this plugin. English is the source language and fallback.
|
|
906
|
+
|
|
720
907
|
function LocalEnginesEditor(props) {
|
|
721
908
|
const t = props.t || ((key) => key)
|
|
722
909
|
const [models, setModels] = React.useState({})
|
|
723
|
-
const [loading, setLoading] = React.useState(false)
|
|
724
910
|
|
|
725
911
|
const loadStatus = React.useCallback(() => {
|
|
726
912
|
if (typeof fetch === 'undefined') return
|
|
727
913
|
fetch('/dsh-tts/models/status')
|
|
728
914
|
.then((r) => r.json())
|
|
729
915
|
.then((d) => { if (d && d.ok && d.models) setModels(d.models) })
|
|
730
|
-
.catch(() => {})
|
|
916
|
+
.catch(() => {}) // status poll best-effort
|
|
731
917
|
}, [])
|
|
732
918
|
|
|
733
919
|
React.useEffect(() => {
|
|
734
920
|
loadStatus()
|
|
735
|
-
const timer = setInterval(loadStatus,
|
|
921
|
+
const timer = setInterval(loadStatus, 5000)
|
|
736
922
|
return () => clearInterval(timer)
|
|
737
923
|
}, [loadStatus])
|
|
738
924
|
|
|
739
|
-
const install = (engine) => {
|
|
740
|
-
setLoading(true)
|
|
741
|
-
fetch('/dsh-tts/models/install', {
|
|
742
|
-
method: 'POST',
|
|
743
|
-
headers: { 'content-type': 'application/json' },
|
|
744
|
-
body: JSON.stringify({ engine }),
|
|
745
|
-
}).then(() => loadStatus()).finally(() => setLoading(false))
|
|
746
|
-
}
|
|
747
|
-
|
|
748
925
|
const remove = (engine) => {
|
|
749
|
-
|
|
926
|
+
if (typeof window !== 'undefined' && window.confirm) {
|
|
927
|
+
if (!window.confirm(t('confirmDeleteModel'))) return
|
|
928
|
+
}
|
|
750
929
|
fetch('/dsh-tts/models/delete', {
|
|
751
930
|
method: 'DELETE',
|
|
752
931
|
headers: { 'content-type': 'application/json' },
|
|
753
932
|
body: JSON.stringify({ engine }),
|
|
754
|
-
}).then(() => loadStatus()).
|
|
933
|
+
}).then(() => loadStatus()).catch(() => {})
|
|
755
934
|
}
|
|
756
935
|
|
|
757
936
|
const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
|
|
758
937
|
const f5 = models.f5 || { installed: false, downloading: false, progress: 0 }
|
|
759
938
|
|
|
939
|
+
const engineRow = (label, state) => React.createElement('div', { className: 'dts-entry' },
|
|
940
|
+
React.createElement('div', { className: 'dts-row' },
|
|
941
|
+
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, label),
|
|
942
|
+
React.createElement('span', {
|
|
943
|
+
className: 'dts-badge dts-badge-warn',
|
|
944
|
+
}, t('runtimeNotBundled')),
|
|
945
|
+
state.installed ? React.createElement('button', {
|
|
946
|
+
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
947
|
+
onClick: () => remove(label.toLowerCase().includes('kokoro') ? 'kokoro' : 'f5'),
|
|
948
|
+
}, t('deleteModel')) : null,
|
|
949
|
+
),
|
|
950
|
+
React.createElement('div', { className: 'dts-sub' }, t('runtimeNotBundledHint')),
|
|
951
|
+
)
|
|
952
|
+
|
|
760
953
|
return React.createElement('div', { className: 'dts-block' },
|
|
761
954
|
React.createElement('div', { className: 'dts-h' }, t('localEnginesTitle')),
|
|
762
955
|
React.createElement('div', { className: 'dts-sub' }, t('localEnginesHint')),
|
|
763
|
-
|
|
956
|
+
React.createElement('div', { className: 'dts-alert-warn dts-sub' }, t('runtimeNotBundledHint')),
|
|
764
957
|
props.boolField('streamingEnabled', t('streamingEnabled'), t('streamingEnabledHint')),
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'Kokoro-82M (CPU)'),
|
|
768
|
-
React.createElement('span', {
|
|
769
|
-
className: 'dts-badge' + (kokoro.installed ? ' dts-badge-on' : ''),
|
|
770
|
-
}, kokoro.downloading ? `${t('downloading')} ${kokoro.progress}%` : (kokoro.installed ? t('installed') : t('notInstalled'))),
|
|
771
|
-
!kokoro.installed && !kokoro.downloading ? React.createElement('button', {
|
|
772
|
-
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
773
|
-
onClick: () => install('kokoro'),
|
|
774
|
-
}, t('installKokoro')) : null,
|
|
775
|
-
kokoro.installed ? React.createElement('button', {
|
|
776
|
-
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
777
|
-
onClick: () => remove('kokoro'),
|
|
778
|
-
}, t('deleteModel')) : null,
|
|
779
|
-
),
|
|
780
|
-
kokoro.installed ? props.boolField('kokoroEnabled', t('useKokoro'), t('useKokoroHint')) : null,
|
|
781
|
-
),
|
|
782
|
-
React.createElement('div', { className: 'dts-entry' },
|
|
783
|
-
React.createElement('div', { className: 'dts-row' },
|
|
784
|
-
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'F5-TTS (GPU)'),
|
|
785
|
-
React.createElement('span', {
|
|
786
|
-
className: 'dts-badge' + (f5.installed ? ' dts-badge-on' : ''),
|
|
787
|
-
}, f5.downloading ? `${t('downloading')} ${f5.progress}%` : (f5.installed ? t('installed') : t('notInstalled'))),
|
|
788
|
-
!f5.installed && !f5.downloading ? React.createElement('button', {
|
|
789
|
-
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
790
|
-
onClick: () => install('f5'),
|
|
791
|
-
}, t('installF5')) : null,
|
|
792
|
-
f5.installed ? React.createElement('button', {
|
|
793
|
-
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
794
|
-
onClick: () => remove('f5'),
|
|
795
|
-
}, t('deleteModel')) : null,
|
|
796
|
-
),
|
|
797
|
-
f5.installed ? props.boolField('f5Enabled', t('useF5'), t('useF5Hint')) : null,
|
|
798
|
-
),
|
|
958
|
+
engineRow('Kokoro-82M (CPU)', kokoro),
|
|
959
|
+
engineRow('F5-TTS (GPU)', f5),
|
|
799
960
|
)
|
|
800
961
|
}
|
|
801
962
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
},
|
|
828
|
-
|
|
829
|
-
// Snapshot status matters more than the value:
|
|
830
|
-
// loading — host has not answered yet;
|
|
831
|
-
// unavailable — host answered but the settings namespace is not ready;
|
|
832
|
-
// ready — values are present.
|
|
833
|
-
// On unavailable, writable defaults true; without a status check the card
|
|
834
|
-
// looks like a working empty form.
|
|
835
|
-
const ready = !scope || (!!snap && snap.status === 'ready')
|
|
836
|
-
const writable = !scope ? true : (ready && snap.writable !== false)
|
|
963
|
+
const CLIENT_IT_TERMS = [
|
|
964
|
+
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
965
|
+
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
966
|
+
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
967
|
+
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
968
|
+
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
969
|
+
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
970
|
+
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
971
|
+
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
972
|
+
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
973
|
+
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
974
|
+
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
975
|
+
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
976
|
+
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
977
|
+
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
978
|
+
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
979
|
+
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
980
|
+
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
981
|
+
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
982
|
+
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
983
|
+
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
984
|
+
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
985
|
+
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
986
|
+
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
987
|
+
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
988
|
+
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
989
|
+
]
|
|
837
990
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
991
|
+
// Pronunciation dictionary editor: rules apply top-down.
|
|
992
|
+
function PronEditor(props) {
|
|
993
|
+
const t = props.t || ((key) => key)
|
|
994
|
+
const rows = Array.isArray(props.value) ? props.value : []
|
|
995
|
+
const change = (i, patch) => {
|
|
996
|
+
props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
843
997
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
998
|
+
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
999
|
+
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
1000
|
+
const loadIt = () => {
|
|
1001
|
+
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
1002
|
+
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
1003
|
+
props.onChange(rows.concat(toAdd))
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
1007
|
+
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
1008
|
+
React.createElement('input', {
|
|
1009
|
+
className: 'dts-input dts-grow', placeholder: t('pronFrom'),
|
|
1010
|
+
value: rule.from || '', disabled: !props.writable,
|
|
1011
|
+
onChange: (e) => change(i, { from: e.target.value }),
|
|
1012
|
+
}),
|
|
1013
|
+
React.createElement('input', {
|
|
1014
|
+
className: 'dts-input dts-grow', placeholder: t('pronTo'),
|
|
1015
|
+
value: rule.to || '', disabled: !props.writable,
|
|
1016
|
+
onChange: (e) => change(i, { to: e.target.value }),
|
|
1017
|
+
}),
|
|
1018
|
+
React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
|
|
1019
|
+
React.createElement('input', {
|
|
1020
|
+
type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
|
|
1021
|
+
onChange: (e) => change(i, { whole: e.target.checked }),
|
|
1022
|
+
}),
|
|
1023
|
+
),
|
|
1024
|
+
React.createElement('input', {
|
|
1025
|
+
className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
|
|
1026
|
+
value: rule.lang || '', disabled: !props.writable,
|
|
1027
|
+
onChange: (e) => change(i, { lang: e.target.value }),
|
|
1028
|
+
}),
|
|
1029
|
+
React.createElement('button', {
|
|
1030
|
+
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
1031
|
+
onClick: () => {
|
|
1032
|
+
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
1033
|
+
},
|
|
1034
|
+
}, '\u25b6'),
|
|
1035
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
1036
|
+
)),
|
|
1037
|
+
React.createElement('div', { className: 'dts-row' },
|
|
1038
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
1039
|
+
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
1040
|
+
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
1041
|
+
),
|
|
1042
|
+
)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function RolesEditor(props) {
|
|
1046
|
+
const t = props.t || ((key) => key)
|
|
1047
|
+
const value = props.value || {}
|
|
1048
|
+
const [newRole, setNewRole] = React.useState('')
|
|
1049
|
+
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
1050
|
+
const removeRole = (r) => {
|
|
1051
|
+
const next = Object.assign({}, value)
|
|
1052
|
+
delete next[r]
|
|
1053
|
+
props.onChange(next)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const standardKeys = ['reply', 'approval', 'error']
|
|
1057
|
+
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1058
|
+
const allKeys = [...standardKeys, ...customKeys]
|
|
1059
|
+
|
|
1060
|
+
const addCustom = () => {
|
|
1061
|
+
const name = newRole.trim().toLowerCase()
|
|
1062
|
+
if (!name || value[name]) return
|
|
1063
|
+
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1064
|
+
setNewRole('')
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1068
|
+
React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
|
|
1069
|
+
allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
|
|
1070
|
+
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
|
|
1071
|
+
React.createElement('select', {
|
|
1072
|
+
className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
|
|
1073
|
+
onChange: (e) => set(r, { provider: e.target.value }),
|
|
1074
|
+
},
|
|
1075
|
+
React.createElement('option', { value: '' }, '—'),
|
|
1076
|
+
PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
|
|
1077
|
+
),
|
|
1078
|
+
React.createElement('input', {
|
|
1079
|
+
className: 'dts-input dts-grow', placeholder: 'model',
|
|
1080
|
+
value: (value[r] && value[r].model) || '', disabled: !props.writable,
|
|
1081
|
+
onChange: (e) => set(r, { model: e.target.value }),
|
|
1082
|
+
}),
|
|
1083
|
+
React.createElement('input', {
|
|
1084
|
+
className: 'dts-input dts-grow', placeholder: 'voice',
|
|
1085
|
+
value: (value[r] && value[r].voice) || '', disabled: !props.writable,
|
|
1086
|
+
onChange: (e) => set(r, { voice: e.target.value }),
|
|
1087
|
+
}),
|
|
1088
|
+
React.createElement('input', {
|
|
1089
|
+
className: 'dts-input', style: { maxWidth: '90px' }, placeholder: t('chime'),
|
|
1090
|
+
value: (value[r] && value[r].chime) || '', disabled: !props.writable,
|
|
1091
|
+
onChange: (e) => set(r, { chime: e.target.value }),
|
|
1092
|
+
}),
|
|
1093
|
+
React.createElement('input', {
|
|
1094
|
+
className: 'dts-input dts-grow', placeholder: 'ssml',
|
|
1095
|
+
value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
|
|
1096
|
+
onChange: (e) => set(r, { ssmlStyle: e.target.value }),
|
|
1097
|
+
}),
|
|
1098
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => props.onPreview((value[r] && value[r].provider) || '', (value[r] && value[r].model) || '', (value[r] && value[r].voice) || '') }, '\u25b6'),
|
|
1099
|
+
!standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
|
|
1100
|
+
)),
|
|
1101
|
+
React.createElement('div', { className: 'dts-row' },
|
|
1102
|
+
React.createElement('input', {
|
|
1103
|
+
className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
|
|
1104
|
+
value: newRole, disabled: !props.writable,
|
|
1105
|
+
onChange: (e) => setNewRole(e.target.value),
|
|
1106
|
+
onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
|
|
1107
|
+
}),
|
|
1108
|
+
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
|
|
1109
|
+
),
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function VoiceDuplexEditor(props) {
|
|
1114
|
+
const t = props.t || ((key) => key)
|
|
1115
|
+
const installed = !!props.installed
|
|
1116
|
+
|
|
1117
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1118
|
+
React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
|
|
1119
|
+
React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
|
|
1120
|
+
!installed ? React.createElement('div', {
|
|
1121
|
+
style: {
|
|
1122
|
+
padding: '10px 14px',
|
|
1123
|
+
borderRadius: '8px',
|
|
1124
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1125
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1126
|
+
display: 'flex',
|
|
1127
|
+
flexDirection: 'column',
|
|
1128
|
+
gap: '4px',
|
|
1129
|
+
fontSize: '13px',
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1133
|
+
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1134
|
+
),
|
|
1135
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1136
|
+
t('voiceInstallHint') + ': '
|
|
1137
|
+
),
|
|
1138
|
+
React.createElement('code', {
|
|
1139
|
+
style: {
|
|
1140
|
+
fontFamily: 'monospace',
|
|
1141
|
+
fontSize: '12px',
|
|
1142
|
+
padding: '2px 6px',
|
|
1143
|
+
borderRadius: '4px',
|
|
1144
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1145
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1146
|
+
width: 'fit-content',
|
|
1147
|
+
},
|
|
1148
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1149
|
+
) : null,
|
|
1150
|
+
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1151
|
+
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1152
|
+
)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function MessengerIntegrationEditor(props) {
|
|
1156
|
+
const t = props.t || ((key) => key)
|
|
1157
|
+
const installed = !!props.installed
|
|
1158
|
+
|
|
1159
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1160
|
+
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1161
|
+
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1162
|
+
!installed ? React.createElement('div', {
|
|
1163
|
+
style: {
|
|
1164
|
+
padding: '10px 14px',
|
|
1165
|
+
borderRadius: '8px',
|
|
1166
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1167
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1168
|
+
display: 'flex',
|
|
1169
|
+
flexDirection: 'column',
|
|
1170
|
+
gap: '4px',
|
|
1171
|
+
fontSize: '13px',
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1175
|
+
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1176
|
+
),
|
|
1177
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1178
|
+
t('messengerInstallHint') + ': '
|
|
1179
|
+
),
|
|
1180
|
+
React.createElement('code', {
|
|
1181
|
+
style: {
|
|
1182
|
+
fontFamily: 'monospace',
|
|
1183
|
+
fontSize: '12px',
|
|
1184
|
+
padding: '2px 6px',
|
|
1185
|
+
borderRadius: '4px',
|
|
1186
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1187
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1188
|
+
width: 'fit-content',
|
|
1189
|
+
},
|
|
1190
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
|
|
1191
|
+
) : null,
|
|
1192
|
+
props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
|
|
1193
|
+
)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Synthesis stats panel: live host counters.
|
|
1197
|
+
function StatsPanel(props) {
|
|
1198
|
+
const t = props.t || ((key) => key)
|
|
1199
|
+
const [data, setData] = React.useState(null)
|
|
1200
|
+
const loadStats = () => {
|
|
1201
|
+
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
|
|
1202
|
+
}
|
|
1203
|
+
React.useEffect(() => { loadStats() }, [])
|
|
1204
|
+
const reset = () => {
|
|
1205
|
+
fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
|
|
1206
|
+
}
|
|
1207
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1208
|
+
React.createElement('span', { className: 'dts-sub' }, data
|
|
1209
|
+
? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
|
|
1210
|
+
: t('loading')),
|
|
1211
|
+
data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
|
|
1212
|
+
p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
|
|
1213
|
+
React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
|
|
1214
|
+
)
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Plugin-settings tab card follows the shared card pattern: li in the core list,
|
|
1218
|
+
// theme-reset header, body with a divider. Chevron is ours; core does not provide it.
|
|
1219
|
+
function TtsSection(props) {
|
|
1220
|
+
// Translator comes from the slot because its registration sets locale.
|
|
1221
|
+
const t = (props && props.t) || ((key) => key)
|
|
1222
|
+
const [draft, setDraft] = React.useState(null)
|
|
1223
|
+
const [credentials, setCredentials] = React.useState({})
|
|
1224
|
+
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
1225
|
+
const [saved, setSaved] = React.useState(false)
|
|
1226
|
+
const [err, setErr] = React.useState('')
|
|
1227
|
+
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
1228
|
+
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0, sseClients: 0, breaker: [], provider: '', queue: 0 })
|
|
1229
|
+
const [advancedOpen, setAdvancedOpen] = React.useState(() => {
|
|
1230
|
+
try { return localStorage.getItem('dsh-tts/advancedOpen') === '1' } catch { return false }
|
|
1231
|
+
})
|
|
1232
|
+
const autoplay = useAutoplayGate()
|
|
1233
|
+
|
|
1234
|
+
const ctx = props && props.ctx
|
|
1235
|
+
const scope = React.useMemo(() => {
|
|
1236
|
+
const s = (ctx && ctx.get && ctx.get('lanSettings')) || (ctx && ctx.settingsScope)
|
|
1237
|
+
if (!s || !s.bind) return null
|
|
1238
|
+
try {
|
|
1239
|
+
return s.bind({ namespace: NS })
|
|
1240
|
+
} catch (_) {
|
|
1241
|
+
return null
|
|
1242
|
+
}
|
|
1243
|
+
}, [ctx])
|
|
1244
|
+
|
|
1245
|
+
const subscribe = React.useMemo(() => {
|
|
1246
|
+
return (cb) => {
|
|
1247
|
+
if (!scope || !scope.subscribe) return () => {}
|
|
1248
|
+
try {
|
|
1249
|
+
return scope.subscribe(cb) || (() => {})
|
|
1250
|
+
} catch (_) {
|
|
1251
|
+
return () => {}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}, [scope])
|
|
1255
|
+
|
|
1256
|
+
const getSnapshot = React.useCallback(() => {
|
|
1257
|
+
if (!scope || !scope.getSnapshot) return SNAPSHOT_LOADING
|
|
1258
|
+
try {
|
|
1259
|
+
return scope.getSnapshot() || SNAPSHOT_LOADING
|
|
1260
|
+
} catch (_) {
|
|
1261
|
+
return SNAPSHOT_LOADING
|
|
1262
|
+
}
|
|
1263
|
+
}, [scope])
|
|
1264
|
+
|
|
1265
|
+
const snap = (React.useSyncExternalStore
|
|
1266
|
+
? React.useSyncExternalStore(subscribe, getSnapshot, React.useCallback(() => SNAPSHOT_LOADING, []))
|
|
1267
|
+
: null) || (scope && scope.getSnapshot ? scope.getSnapshot() : SNAPSHOT_LOADING)
|
|
1268
|
+
|
|
1269
|
+
// Snapshot status matters more than the value:
|
|
1270
|
+
// loading — host has not answered yet;
|
|
1271
|
+
// unavailable — host answered but the settings namespace is not ready;
|
|
1272
|
+
// ready — values are present.
|
|
1273
|
+
// On unavailable, writable defaults true; without a status check the card
|
|
1274
|
+
// looks like a working empty form.
|
|
1275
|
+
const ready = !scope || (!!snap && snap.status === 'ready')
|
|
1276
|
+
const writable = !scope ? true : (ready && snap.writable !== false)
|
|
1277
|
+
|
|
1278
|
+
const applyPayload = (data) => {
|
|
1279
|
+
const cfg = data && data.config ? data.config : {}
|
|
1280
|
+
setDraft(JSON.parse(JSON.stringify(cfg)))
|
|
1281
|
+
setCredentials(data && data.credentials ? data.credentials : {})
|
|
1282
|
+
player.enabled = !!cfg.speakReplies
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
React.useEffect(() => {
|
|
1286
|
+
let alive = true
|
|
1287
|
+
fetch('/dsh-tts/config', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
1288
|
+
if (!alive) return
|
|
1289
|
+
applyPayload(data)
|
|
850
1290
|
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
851
1291
|
|
|
852
1292
|
fetch('/dsh-tts/integrations', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
@@ -854,7 +1294,33 @@ window.__ModuleLoader__.load({
|
|
|
854
1294
|
if (data && data.ok) setIntegrations(data)
|
|
855
1295
|
}).catch(() => {})
|
|
856
1296
|
|
|
857
|
-
|
|
1297
|
+
const loadTelemetry = () => {
|
|
1298
|
+
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
1299
|
+
if (!alive || !st) return
|
|
1300
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1301
|
+
cacheHits: st.cacheHits || 0,
|
|
1302
|
+
total: st.total || 0,
|
|
1303
|
+
online: true,
|
|
1304
|
+
breaker: st.breaker || prev.breaker || [],
|
|
1305
|
+
}))
|
|
1306
|
+
}).catch(() => {
|
|
1307
|
+
if (alive) setTelemetry((prev) => Object.assign({}, prev, { online: false }))
|
|
1308
|
+
})
|
|
1309
|
+
fetch('/dsh-tts/status', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
1310
|
+
if (!alive || !st) return
|
|
1311
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1312
|
+
sseClients: st.sseClients || 0,
|
|
1313
|
+
sseOk: (st.sseClients || 0) >= 0,
|
|
1314
|
+
provider: (player.audio && player.provider) || (player.queue[0] && player.queue[0].provider) || '',
|
|
1315
|
+
queue: player.queue.length,
|
|
1316
|
+
}))
|
|
1317
|
+
}).catch(() => {})
|
|
1318
|
+
}
|
|
1319
|
+
loadTelemetry()
|
|
1320
|
+
const telTimer = setInterval(loadTelemetry, 3000)
|
|
1321
|
+
// interval cleaned by effect return below
|
|
1322
|
+
|
|
1323
|
+
return () => { alive = false; clearInterval(telTimer) }
|
|
858
1324
|
}, [])
|
|
859
1325
|
|
|
860
1326
|
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
@@ -925,6 +1391,9 @@ window.__ModuleLoader__.load({
|
|
|
925
1391
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
926
1392
|
}
|
|
927
1393
|
const onClearCache = () => {
|
|
1394
|
+
if (typeof window !== 'undefined' && window.confirm) {
|
|
1395
|
+
if (!window.confirm(t('confirmClearCache'))) return
|
|
1396
|
+
}
|
|
928
1397
|
clearCacheNow().catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
929
1398
|
}
|
|
930
1399
|
|
|
@@ -965,390 +1434,186 @@ window.__ModuleLoader__.load({
|
|
|
965
1434
|
|
|
966
1435
|
const textField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
967
1436
|
React.createElement('input', {
|
|
968
|
-
className: 'dts-input',
|
|
969
|
-
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
970
|
-
onChange: (e) => setTop(key, e.target.value),
|
|
971
|
-
}),
|
|
972
|
-
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
),
|
|
989
|
-
React.createElement(
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}),
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
React.createElement(RolesEditor, {
|
|
1016
|
-
t: t, writable: writable, onPreview: onPreview,
|
|
1017
|
-
value: draft && draft.roles ? draft.roles : {},
|
|
1018
|
-
onChange: (v) => setTop('roles', v),
|
|
1019
|
-
}),
|
|
1020
|
-
),
|
|
1021
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1022
|
-
React.createElement('div', { className: 'dts-h' }, t('statsTitle')),
|
|
1023
|
-
React.createElement(StatsPanel, { t: t }),
|
|
1024
|
-
),
|
|
1025
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1026
|
-
React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
|
|
1027
|
-
React.createElement('div', { className: 'dts-sub' },
|
|
1028
|
-
t('chainHintCard')),
|
|
1029
|
-
React.createElement(ChainEditor, {
|
|
1030
|
-
t: t,
|
|
1031
|
-
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
1032
|
-
credentials: credentials, keyDrafts: keyDrafts,
|
|
1033
|
-
onChange: (v) => setTop('chain', v),
|
|
1034
|
-
onDraft: setDraftKey,
|
|
1035
|
-
onCommitKey: onCommitKey,
|
|
1036
|
-
onClearKey: clearKey,
|
|
1037
|
-
onPreview: onPreview,
|
|
1038
|
-
}),
|
|
1039
|
-
),
|
|
1040
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1041
|
-
React.createElement('div', { className: 'dts-h' }, t('general')),
|
|
1042
|
-
boolField('speakAsItGoes', t('speakAsItGoes'),
|
|
1043
|
-
t('speakAsItGoesHint')),
|
|
1044
|
-
numberField('rate', t('rate'), t('rateHint'), 0.1),
|
|
1045
|
-
boolField('bargeIn', t('bargeIn'),
|
|
1046
|
-
t('bargeInHint')),
|
|
1047
|
-
boolField('announceApproval', t('announce'),
|
|
1048
|
-
t('announceHint')),
|
|
1049
|
-
boolField('skipCode', t('skipCode'),
|
|
1050
|
-
t('skipCodeHint')),
|
|
1051
|
-
boolField('cache', t('cache'),
|
|
1052
|
-
t('cacheHint')),
|
|
1053
|
-
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
1054
|
-
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1055
|
-
React.createElement('select', {
|
|
1056
|
-
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1057
|
-
onChange: (e) => setTop('longReply', e.target.value),
|
|
1058
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1059
|
-
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1060
|
-
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1061
|
-
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1062
|
-
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1063
|
-
React.createElement('select', {
|
|
1064
|
-
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1065
|
-
onChange: (e) => setTop('longReply', e.target.value),
|
|
1066
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1067
|
-
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1068
|
-
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1069
|
-
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1070
|
-
boolField('autoDetect', t('autoDetect'), t('autoDetectHint')),
|
|
1071
|
-
boolField('narrateQuotesOnly', t('narrateQuotesOnly'), t('narrateQuotesHint')),
|
|
1072
|
-
boolField('skipActions', t('skipActions'), t('skipActionsHint')),
|
|
1073
|
-
textField('removeRegex', t('removeRegex'), t('removeRegexHint')),
|
|
1074
|
-
textField('customBaseUrl', t('customBaseUrl'), t('customBaseUrlHint')),
|
|
1075
|
-
textField('customKeyEnv', t('customKeyEnv'), t('customKeyEnvHint')),
|
|
1076
|
-
textField('approvalText', t('approvalText'), t('approvalHint')),
|
|
1077
|
-
textField('questionText', t('questionText'), t('questionHint')),
|
|
1078
|
-
textField('chime', t('chime'), t('chimeHint')),
|
|
1079
|
-
textField('language', t('language'), t('languageHint')),
|
|
1080
|
-
textField('piperModel', t('piperModel'), t('piperModelHint')),
|
|
1081
|
-
textField('piperBin', t('piperBin'), t('binHint')),
|
|
1082
|
-
textField('espeakBin', t('espeakBin'), t('espeakHint')),
|
|
1083
|
-
textField('edgeBin', t('edgeBin'), t('edgeHint')),
|
|
1084
|
-
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
1085
|
-
),
|
|
1086
|
-
React.createElement('div', { className: 'dts-foot' },
|
|
1087
|
-
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
1088
|
-
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
|
|
1089
|
-
saved ? React.createElement('span', { className: 'dts-ok' }, t('saved')) : null,
|
|
1090
|
-
err ? React.createElement('span', { className: 'dts-bad' }, err) : null,
|
|
1091
|
-
),
|
|
1092
|
-
)
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const CLIENT_IT_TERMS = [
|
|
1097
|
-
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
1098
|
-
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
1099
|
-
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1100
|
-
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1101
|
-
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
1102
|
-
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
1103
|
-
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
1104
|
-
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
1105
|
-
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
1106
|
-
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
1107
|
-
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
1108
|
-
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
1109
|
-
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
1110
|
-
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
1111
|
-
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
1112
|
-
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
1113
|
-
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
1114
|
-
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
1115
|
-
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
1116
|
-
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
1117
|
-
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
1118
|
-
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
1119
|
-
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
1120
|
-
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
1121
|
-
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1122
|
-
]
|
|
1123
|
-
|
|
1124
|
-
// Pronunciation dictionary editor: rules apply top-down.
|
|
1125
|
-
function PronEditor(props) {
|
|
1126
|
-
const t = props.t || ((key) => key)
|
|
1127
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
1128
|
-
const change = (i, patch) => {
|
|
1129
|
-
props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
1130
|
-
}
|
|
1131
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
1132
|
-
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
1133
|
-
const loadIt = () => {
|
|
1134
|
-
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
1135
|
-
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
1136
|
-
props.onChange(rows.concat(toAdd))
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
1140
|
-
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
1141
|
-
React.createElement('input', {
|
|
1142
|
-
className: 'dts-input dts-grow', placeholder: t('pronFrom'),
|
|
1143
|
-
value: rule.from || '', disabled: !props.writable,
|
|
1144
|
-
onChange: (e) => change(i, { from: e.target.value }),
|
|
1145
|
-
}),
|
|
1146
|
-
React.createElement('input', {
|
|
1147
|
-
className: 'dts-input dts-grow', placeholder: t('pronTo'),
|
|
1148
|
-
value: rule.to || '', disabled: !props.writable,
|
|
1149
|
-
onChange: (e) => change(i, { to: e.target.value }),
|
|
1150
|
-
}),
|
|
1151
|
-
React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
|
|
1152
|
-
React.createElement('input', {
|
|
1153
|
-
type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
|
|
1154
|
-
onChange: (e) => change(i, { whole: e.target.checked }),
|
|
1155
|
-
}),
|
|
1156
|
-
),
|
|
1157
|
-
React.createElement('input', {
|
|
1158
|
-
className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
|
|
1159
|
-
value: rule.lang || '', disabled: !props.writable,
|
|
1160
|
-
onChange: (e) => change(i, { lang: e.target.value }),
|
|
1161
|
-
}),
|
|
1162
|
-
React.createElement('button', {
|
|
1163
|
-
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
1164
|
-
onClick: () => {
|
|
1165
|
-
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
1166
|
-
},
|
|
1167
|
-
}, '\u25b6'),
|
|
1168
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
1169
|
-
)),
|
|
1170
|
-
React.createElement('div', { className: 'dts-row' },
|
|
1171
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
1172
|
-
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
1173
|
-
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
1174
|
-
),
|
|
1175
|
-
)
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function RolesEditor(props) {
|
|
1179
|
-
const t = props.t || ((key) => key)
|
|
1180
|
-
const value = props.value || {}
|
|
1181
|
-
const [newRole, setNewRole] = React.useState('')
|
|
1182
|
-
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
1183
|
-
const removeRole = (r) => {
|
|
1184
|
-
const next = Object.assign({}, value)
|
|
1185
|
-
delete next[r]
|
|
1186
|
-
props.onChange(next)
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
const standardKeys = ['reply', 'approval', 'error']
|
|
1190
|
-
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1191
|
-
const allKeys = [...standardKeys, ...customKeys]
|
|
1192
|
-
|
|
1193
|
-
const addCustom = () => {
|
|
1194
|
-
const name = newRole.trim().toLowerCase()
|
|
1195
|
-
if (!name || value[name]) return
|
|
1196
|
-
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1197
|
-
setNewRole('')
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1201
|
-
React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
|
|
1202
|
-
allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
|
|
1203
|
-
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
|
|
1204
|
-
React.createElement('select', {
|
|
1205
|
-
className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
|
|
1206
|
-
onChange: (e) => set(r, { provider: e.target.value }),
|
|
1207
|
-
},
|
|
1208
|
-
React.createElement('option', { value: '' }, '—'),
|
|
1209
|
-
PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
|
|
1437
|
+
className: 'dts-input',
|
|
1438
|
+
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
1439
|
+
onChange: (e) => setTop(key, e.target.value),
|
|
1440
|
+
}),
|
|
1441
|
+
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
1442
|
+
|
|
1443
|
+
const autoplayBanner = autoplay.blocked
|
|
1444
|
+
? React.createElement('div', { className: 'dts-unlock' },
|
|
1445
|
+
React.createElement('span', null, t('autoplayBlocked')),
|
|
1446
|
+
React.createElement('button', {
|
|
1447
|
+
type: 'button', className: 'dts-unlock-btn',
|
|
1448
|
+
onClick: () => player.unlockAudio && player.unlockAudio(),
|
|
1449
|
+
}, t('enableSound')),
|
|
1450
|
+
)
|
|
1451
|
+
: null
|
|
1452
|
+
const breakerBadges = (telemetry.breaker || []).filter((b) => b && b.open).slice(0, 2)
|
|
1453
|
+
.map((b) => React.createElement('span', { key: b.id, className: 'dts-badge dts-badge-bad' }, t('breakerOpen') + ': ' + b.id))
|
|
1454
|
+
const telemetryBadges = React.createElement('div', { className: 'dts-row', style: { marginBottom: '8px' }, 'data-dsh-tts-telemetry': '1' },
|
|
1455
|
+
React.createElement('span', {
|
|
1456
|
+
className: 'dts-badge ' + (telemetry.online ? 'dts-badge-on' : 'dts-badge-bad'),
|
|
1457
|
+
}, telemetry.online ? t('statusOnline') : t('statusOffline')),
|
|
1458
|
+
React.createElement('span', {
|
|
1459
|
+
className: 'dts-badge ' + (telemetry.sseClients > 0 ? 'dts-badge-on' : ''),
|
|
1460
|
+
}, t('sseState') + ': ' + (telemetry.sseClients || 0)),
|
|
1461
|
+
React.createElement('span', { className: 'dts-badge' },
|
|
1462
|
+
t('queueLen') + ': ' + (telemetry.queue || player.queue.length || 0)),
|
|
1463
|
+
telemetry.provider ? React.createElement('span', { className: 'dts-badge' },
|
|
1464
|
+
t('activeProvider') + ': ' + telemetry.provider) : null,
|
|
1465
|
+
React.createElement('span', { className: 'dts-badge' },
|
|
1466
|
+
t('cacheStats') + ': ' + telemetry.cacheHits + ' / ' + telemetry.total),
|
|
1467
|
+
...breakerBadges,
|
|
1468
|
+
)
|
|
1469
|
+
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-header' },
|
|
1470
|
+
React.createElement('div', { className: 'dts-page-title' }, '🔊 ' + t('title')),
|
|
1471
|
+
React.createElement('div', { className: 'dts-page-sub' }, t('subtitle')),
|
|
1472
|
+
telemetryBadges,
|
|
1473
|
+
)
|
|
1474
|
+
return React.createElement('div', { className: 'dts-wrap' },
|
|
1475
|
+
autoplayBanner,
|
|
1476
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1477
|
+
titleLine,
|
|
1478
|
+
React.createElement('label', { className: 'dts-row' },
|
|
1479
|
+
React.createElement('input', {
|
|
1480
|
+
type: 'checkbox', checked: !!(draft && draft.speakReplies), disabled: !writable,
|
|
1481
|
+
onChange: (e) => setTop('speakReplies', e.target.checked),
|
|
1482
|
+
}),
|
|
1483
|
+
React.createElement('span', null, t('speakReplies')),
|
|
1210
1484
|
),
|
|
1211
|
-
React.createElement('
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1485
|
+
React.createElement('div', { className: 'dts-sub' },
|
|
1486
|
+
t('speakRepliesHint')),
|
|
1487
|
+
),
|
|
1488
|
+
React.createElement(LocalEnginesEditor, {
|
|
1489
|
+
t: t, writable: writable, draft: draft,
|
|
1490
|
+
boolField: boolField, setTop: setTop,
|
|
1491
|
+
}),
|
|
1492
|
+
React.createElement(VoiceDuplexEditor, {
|
|
1493
|
+
t: t, writable: writable, draft: draft,
|
|
1494
|
+
boolField: boolField, setTop: setTop,
|
|
1495
|
+
installed: !!(integrations && integrations.voice && integrations.voice.installed),
|
|
1496
|
+
}),
|
|
1497
|
+
React.createElement(MessengerIntegrationEditor, {
|
|
1498
|
+
t: t, writable: writable, draft: draft,
|
|
1499
|
+
boolField: boolField, setTop: setTop,
|
|
1500
|
+
installed: !!(integrations && integrations.messenger && integrations.messenger.installed),
|
|
1501
|
+
}),
|
|
1502
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1503
|
+
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
|
1504
|
+
boolField('enableItDictionary', t('enableItDictionary'), t('enableItDictionaryHint')),
|
|
1505
|
+
React.createElement(PronEditor, {
|
|
1506
|
+
t: t, writable: writable,
|
|
1507
|
+
value: draft && draft.pronunciation ? draft.pronunciation : [],
|
|
1508
|
+
onChange: (v) => setTop('pronunciation', v),
|
|
1509
|
+
onPreviewPhrase: onPreviewPhrase,
|
|
1225
1510
|
}),
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1511
|
+
),
|
|
1512
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1513
|
+
React.createElement('div', { className: 'dts-h' }, t('rolesTitle')),
|
|
1514
|
+
React.createElement(RolesEditor, {
|
|
1515
|
+
t: t, writable: writable, onPreview: onPreview,
|
|
1516
|
+
value: draft && draft.roles ? draft.roles : {},
|
|
1517
|
+
onChange: (v) => setTop('roles', v),
|
|
1230
1518
|
}),
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1519
|
+
),
|
|
1520
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1521
|
+
React.createElement('div', { className: 'dts-h' }, t('statsTitle')),
|
|
1522
|
+
React.createElement(StatsPanel, { t: t }),
|
|
1523
|
+
),
|
|
1524
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1525
|
+
React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
|
|
1526
|
+
React.createElement('div', { className: 'dts-sub' },
|
|
1527
|
+
t('chainHintCard')),
|
|
1528
|
+
React.createElement(ChainEditor, {
|
|
1529
|
+
breaker: telemetry.breaker,
|
|
1530
|
+
t: t,
|
|
1531
|
+
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
1532
|
+
credentials: credentials, keyDrafts: keyDrafts,
|
|
1533
|
+
onChange: (v) => setTop('chain', v),
|
|
1534
|
+
onDraft: setDraftKey,
|
|
1535
|
+
onCommitKey: onCommitKey,
|
|
1536
|
+
onClearKey: clearKey,
|
|
1537
|
+
onPreview: onPreview,
|
|
1240
1538
|
}),
|
|
1241
|
-
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
|
|
1242
1539
|
),
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
),
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
),
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1540
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1541
|
+
React.createElement('div', { className: 'dts-h' }, t('general')),
|
|
1542
|
+
boolField('speakAsItGoes', t('speakAsItGoes'),
|
|
1543
|
+
t('speakAsItGoesHint')),
|
|
1544
|
+
numberField('rate', t('rate'), t('rateHint'), 0.1),
|
|
1545
|
+
boolField('bargeIn', t('bargeIn'),
|
|
1546
|
+
t('bargeInHint')),
|
|
1547
|
+
boolField('announceApproval', t('announce'),
|
|
1548
|
+
t('announceHint')),
|
|
1549
|
+
boolField('skipCode', t('skipCode'),
|
|
1550
|
+
t('skipCodeHint')),
|
|
1551
|
+
boolField('cache', t('cache'),
|
|
1552
|
+
t('cacheHint')),
|
|
1553
|
+
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
1554
|
+
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1555
|
+
React.createElement('select', {
|
|
1556
|
+
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1557
|
+
onChange: (e) => setTop('longReply', e.target.value),
|
|
1558
|
+
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1559
|
+
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1560
|
+
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1561
|
+
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1562
|
+
boolField('autoDetect', t('autoDetect'), t('autoDetectHint')),
|
|
1563
|
+
boolField('narrateQuotesOnly', t('narrateQuotesOnly'), t('narrateQuotesHint')),
|
|
1564
|
+
boolField('skipActions', t('skipActions'), t('skipActionsHint')),
|
|
1565
|
+
textField('removeRegex', t('removeRegex'), t('removeRegexHint')),
|
|
1566
|
+
textField('customBaseUrl', t('customBaseUrl'), t('customBaseUrlHint')),
|
|
1567
|
+
textField('customKeyEnv', t('customKeyEnv'), t('customKeyEnvHint')),
|
|
1568
|
+
textField('approvalText', t('approvalText'), t('approvalHint')),
|
|
1569
|
+
textField('questionText', t('questionText'), t('questionHint')),
|
|
1570
|
+
textField('chime', t('chime'), t('chimeHint')),
|
|
1571
|
+
textField('language', t('language'), t('languageHint')),
|
|
1572
|
+
textField('piperModel', t('piperModel'), t('piperModelHint')),
|
|
1573
|
+
textField('piperBin', t('piperBin'), t('binHint')),
|
|
1574
|
+
textField('espeakBin', t('espeakBin'), t('espeakHint')),
|
|
1575
|
+
textField('edgeBin', t('edgeBin'), t('edgeHint')),
|
|
1576
|
+
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
1577
|
+
),
|
|
1578
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1579
|
+
React.createElement('button', {
|
|
1580
|
+
type: 'button',
|
|
1581
|
+
className: 'dts-head',
|
|
1582
|
+
'aria-expanded': advancedOpen,
|
|
1583
|
+
onClick: () => {
|
|
1584
|
+
setAdvancedOpen((v) => {
|
|
1585
|
+
const next = !v
|
|
1586
|
+
try { localStorage.setItem('dsh-tts/advancedOpen', next ? '1' : '0') } catch { /* private mode */ }
|
|
1587
|
+
return next
|
|
1588
|
+
})
|
|
1280
1589
|
},
|
|
1281
|
-
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1282
|
-
) : null,
|
|
1283
|
-
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1284
|
-
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1285
|
-
)
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
function MessengerIntegrationEditor(props) {
|
|
1289
|
-
const t = props.t || ((key) => key)
|
|
1290
|
-
const installed = !!props.installed
|
|
1291
|
-
|
|
1292
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1293
|
-
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1294
|
-
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1295
|
-
!installed ? React.createElement('div', {
|
|
1296
|
-
style: {
|
|
1297
|
-
padding: '10px 14px',
|
|
1298
|
-
borderRadius: '8px',
|
|
1299
|
-
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1300
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1301
|
-
display: 'flex',
|
|
1302
|
-
flexDirection: 'column',
|
|
1303
|
-
gap: '4px',
|
|
1304
|
-
fontSize: '13px',
|
|
1305
1590
|
},
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
React.createElement('span', { className: 'dts-sub' },
|
|
1311
|
-
t('messengerInstallHint') + ': '
|
|
1591
|
+
React.createElement('div', { className: 'dts-headText' },
|
|
1592
|
+
React.createElement('div', { className: 'dts-title' }, t('advancedTitle')),
|
|
1593
|
+
React.createElement('div', { className: 'dts-sub' }, t('advancedHint')),
|
|
1594
|
+
),
|
|
1312
1595
|
),
|
|
1313
|
-
React.createElement('
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1596
|
+
advancedOpen ? React.createElement('div', { className: 'dts-body' },
|
|
1597
|
+
numberField('maxChars', t('maxChars'), t('maxCharsHint'), 100),
|
|
1598
|
+
numberField('sentenceChars', t('sentenceChars'), t('sentenceCharsHint'), 20),
|
|
1599
|
+
numberField('timeoutMs', t('timeoutMs'), t('timeoutMsHint'), 1000),
|
|
1600
|
+
numberField('maxQueue', t('maxQueue'), t('maxQueueHint'), 1),
|
|
1601
|
+
textField('openaiBaseUrl', t('openaiBaseUrl'), t('openaiBaseUrlHint')),
|
|
1602
|
+
textField('mimoBaseUrl', t('mimoBaseUrl'), t('mimoBaseUrlHint')),
|
|
1603
|
+
textField('mimoFormat', t('mimoFormat'), t('mimoFormatHint')),
|
|
1604
|
+
textField('minimaxBin', t('minimaxBin'), t('minimaxBinHint')),
|
|
1605
|
+
) : null,
|
|
1606
|
+
),
|
|
1607
|
+
React.createElement('div', { className: 'dts-foot' },
|
|
1608
|
+
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
1609
|
+
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
|
|
1610
|
+
saved ? React.createElement('span', { className: 'dts-ok' }, t('saved')) : null,
|
|
1611
|
+
err ? React.createElement('span', { className: 'dts-bad' }, err) : null,
|
|
1612
|
+
),
|
|
1326
1613
|
)
|
|
1327
1614
|
}
|
|
1328
1615
|
|
|
1329
|
-
// Synthesis stats panel: live host counters.
|
|
1330
|
-
function StatsPanel(props) {
|
|
1331
|
-
const t = props.t || ((key) => key)
|
|
1332
|
-
const [data, setData] = React.useState(null)
|
|
1333
|
-
const loadStats = () => {
|
|
1334
|
-
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
|
|
1335
|
-
}
|
|
1336
|
-
React.useEffect(() => { loadStats() }, [])
|
|
1337
|
-
const reset = () => {
|
|
1338
|
-
fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
|
|
1339
|
-
}
|
|
1340
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1341
|
-
React.createElement('span', { className: 'dts-sub' }, data
|
|
1342
|
-
? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
|
|
1343
|
-
: t('loading')),
|
|
1344
|
-
data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
|
|
1345
|
-
p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
|
|
1346
|
-
React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
|
|
1347
|
-
)
|
|
1348
|
-
}
|
|
1349
1616
|
|
|
1350
|
-
// Plugin-settings tab card follows the shared card pattern: li in the core list,
|
|
1351
|
-
// theme-reset header, body with a divider. Chevron is ours; core does not provide it.
|
|
1352
1617
|
function TtsCard(props) {
|
|
1353
1618
|
const t = (props && props.t) || ((key) => key)
|
|
1354
1619
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1376,7 +1641,9 @@ window.__ModuleLoader__.load({
|
|
|
1376
1641
|
),
|
|
1377
1642
|
),
|
|
1378
1643
|
open ? React.createElement('div', { className: 'dts-body' },
|
|
1379
|
-
React.createElement(
|
|
1644
|
+
React.createElement(ErrorBoundary, null,
|
|
1645
|
+
React.createElement(TtsSection, Object.assign({}, props, { compact: true, ctx: props && props.ctx })),
|
|
1646
|
+
),
|
|
1380
1647
|
) : null,
|
|
1381
1648
|
)
|
|
1382
1649
|
}
|
|
@@ -1396,7 +1663,7 @@ window.__ModuleLoader__.load({
|
|
|
1396
1663
|
React.createElement('button', {
|
|
1397
1664
|
type: 'button', className: 'dts-link', style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
1398
1665
|
title: text,
|
|
1399
|
-
onClick: () => playStandalone(text),
|
|
1666
|
+
onClick: () => playStandalone(text), title: text,
|
|
1400
1667
|
}, text.slice(0, 80)),
|
|
1401
1668
|
React.createElement('button', {
|
|
1402
1669
|
type: 'button', className: 'dts-link', title: '★',
|
|
@@ -1405,7 +1672,7 @@ window.__ModuleLoader__.load({
|
|
|
1405
1672
|
)
|
|
1406
1673
|
return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
|
|
1407
1674
|
React.createElement('button', {
|
|
1408
|
-
type: 'button', className: 'dts-link', title: '
|
|
1675
|
+
type: 'button', className: 'dts-link', title: t('recent') || 'Recent',
|
|
1409
1676
|
onClick: () => setShowList((v) => !v),
|
|
1410
1677
|
}, '☰'),
|
|
1411
1678
|
showList ? React.createElement('div', {
|
|
@@ -1416,7 +1683,7 @@ window.__ModuleLoader__.load({
|
|
|
1416
1683
|
lists.favs.map((text) => row(text, true)),
|
|
1417
1684
|
React.createElement('div', { className: 'dts-h' }, '⟳'),
|
|
1418
1685
|
lists.items.filter((x) => !lists.favs.includes(x)).map((text) => row(text, false)),
|
|
1419
|
-
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, '
|
|
1686
|
+
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, t('recentEmpty') || 'No recent speech yet') : null,
|
|
1420
1687
|
) : null,
|
|
1421
1688
|
React.createElement('button', {
|
|
1422
1689
|
type: 'button',
|
|
@@ -1434,11 +1701,10 @@ window.__ModuleLoader__.load({
|
|
|
1434
1701
|
}
|
|
1435
1702
|
|
|
1436
1703
|
/**
|
|
1437
|
-
*
|
|
1704
|
+
* Fallback translator for the dock.
|
|
1438
1705
|
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
* принципе, и привязка — единственный способ дать ей язык.
|
|
1706
|
+
* Bound at registration for the page lifetime. The core draws the slot label;
|
|
1707
|
+
* props.t never reaches it, so the binder is the only way to give it a language.
|
|
1442
1708
|
*/
|
|
1443
1709
|
let fallbackDockText = (key) => key
|
|
1444
1710
|
|
|
@@ -1473,7 +1739,7 @@ window.__ModuleLoader__.load({
|
|
|
1473
1739
|
ctx.effect(() => {
|
|
1474
1740
|
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1475
1741
|
return () => { for (const off of undo) off() }
|
|
1476
|
-
}, 'dsh-tts:
|
|
1742
|
+
}, 'dsh-tts: locales')
|
|
1477
1743
|
// The sidebar draws the section label, not our component: props.t never
|
|
1478
1744
|
// reaches it, so bind the translator to the namespace ourselves.
|
|
1479
1745
|
const t = ctx.locale.bind(NS)
|
|
@@ -1509,7 +1775,7 @@ window.__ModuleLoader__.load({
|
|
|
1509
1775
|
label: () => t('title'),
|
|
1510
1776
|
inject: () => ({ ctx: ctx }),
|
|
1511
1777
|
},
|
|
1512
|
-
TtsSection,
|
|
1778
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(TtsSection, props)),
|
|
1513
1779
|
))
|
|
1514
1780
|
}
|
|
1515
1781
|
|
|
@@ -1517,7 +1783,7 @@ window.__ModuleLoader__.load({
|
|
|
1517
1783
|
exports.apply = function apply(ctx) {
|
|
1518
1784
|
registerSettings(ctx)
|
|
1519
1785
|
registerSpeaker(ctx)
|
|
1520
|
-
ctx.effect(() => listenForVoice(), 'dsh-tts:
|
|
1786
|
+
ctx.effect(() => listenForVoice(), 'dsh-tts: barge-in on user speech')
|
|
1521
1787
|
ctx.effect(() => {
|
|
1522
1788
|
if (typeof window === 'undefined') return () => {}
|
|
1523
1789
|
const onKey = (e) => {
|
|
@@ -1527,47 +1793,54 @@ window.__ModuleLoader__.load({
|
|
|
1527
1793
|
}
|
|
1528
1794
|
window.addEventListener('keydown', onKey)
|
|
1529
1795
|
return () => window.removeEventListener('keydown', onKey)
|
|
1530
|
-
}, 'dsh-tts:
|
|
1796
|
+
}, 'dsh-tts: hotkeys')
|
|
1531
1797
|
ctx.effect(() => {
|
|
1532
1798
|
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
1533
1799
|
let es = null
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1800
|
+
let closed = false
|
|
1801
|
+
let attempt = 0
|
|
1802
|
+
let retryTimer = null
|
|
1803
|
+
const onItem = (e) => {
|
|
1804
|
+
try {
|
|
1805
|
+
const item = JSON.parse(e.data)
|
|
1806
|
+
if (!item || !item.id) return
|
|
1807
|
+
if (seenIds.has(item.id)) return
|
|
1808
|
+
seenIds.add(item.id)
|
|
1809
|
+
if (seenIds.size > 500) {
|
|
1810
|
+
const first = seenIds.values().next().value
|
|
1811
|
+
seenIds.delete(first)
|
|
1812
|
+
}
|
|
1813
|
+
player.queue.push(item)
|
|
1814
|
+
drainQueue()
|
|
1815
|
+
} catch { /* ignore malformed SSE frame */ }
|
|
1816
|
+
}
|
|
1817
|
+
const connect = () => {
|
|
1818
|
+
if (closed) return
|
|
1819
|
+
try {
|
|
1820
|
+
es = new EventSource('/dsh-tts/stream')
|
|
1821
|
+
es.addEventListener('open', () => { attempt = 0 })
|
|
1822
|
+
es.addEventListener('utterance', onItem)
|
|
1823
|
+
es.addEventListener('chime', onItem)
|
|
1824
|
+
es.onerror = () => {
|
|
1825
|
+
try { es.close() } catch { /* already closed */ }
|
|
1826
|
+
if (closed) return
|
|
1827
|
+
attempt += 1
|
|
1828
|
+
const delay = Math.min(15000, 500 * Math.pow(2, Math.min(attempt, 5)))
|
|
1829
|
+
retryTimer = setTimeout(connect, delay)
|
|
1830
|
+
}
|
|
1831
|
+
} catch { /* EventSource constructor failed — poll remains */ }
|
|
1832
|
+
}
|
|
1833
|
+
connect()
|
|
1834
|
+
return () => {
|
|
1835
|
+
closed = true
|
|
1836
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
1837
|
+
if (es) { try { es.close() } catch { /* already closed */ } }
|
|
1838
|
+
}
|
|
1839
|
+
}, 'dsh-tts: SSE audio stream')
|
|
1567
1840
|
ctx.effect(() => {
|
|
1568
1841
|
const timer = setInterval(pollPending, 1000)
|
|
1569
1842
|
return () => clearInterval(timer)
|
|
1570
|
-
}, 'dsh-tts:
|
|
1843
|
+
}, 'dsh-tts: poll pending audio')
|
|
1571
1844
|
}
|
|
1572
1845
|
return module.exports
|
|
1573
1846
|
},
|
|
@@ -1575,3 +1848,4 @@ window.__ModuleLoader__.load({
|
|
|
1575
1848
|
|
|
1576
1849
|
// ModelManager UI placeholder - manual install buttons for Kokoro/F5
|
|
1577
1850
|
// Actual implementation would show download progress and status
|
|
1851
|
+
|