@goodandready/dsh-tts 0.4.22 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/lib/client.js +125 -0
- package/lib/engines/manager.js +14 -2
- package/lib/http-util.js +70 -15
- package/lib/routes.js +7 -0
- package/package.json +3 -2
- package/lib/client-src/00-open.js +0 -89
- package/lib/client-src/10-styles.js +0 -55
- package/lib/client-src/20-player.js +0 -424
- package/lib/client-src/30-locales.js +0 -374
- package/lib/client-src/40-editors.js +0 -422
- package/lib/client-src/50-settings.js +0 -399
- package/lib/client-src/60-card-dock.js +0 -278
- package/scripts/build-client.mjs +0 -19
- package/scripts/f5_daemon.py +0 -40
- package/scripts/publish-github.sh +0 -184
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
function TtsSection(props) {
|
|
2
|
-
// Translator comes from the slot because its registration sets locale.
|
|
3
|
-
const t = (props && props.t) || ((key) => key)
|
|
4
|
-
const [draft, setDraft] = React.useState(null)
|
|
5
|
-
const [credentials, setCredentials] = React.useState({})
|
|
6
|
-
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
7
|
-
const [saved, setSaved] = React.useState(false)
|
|
8
|
-
const [err, setErr] = React.useState('')
|
|
9
|
-
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
10
|
-
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0, sseClients: 0, breaker: [], provider: '', queue: 0 })
|
|
11
|
-
const [advancedOpen, setAdvancedOpen] = React.useState(() => {
|
|
12
|
-
try { return localStorage.getItem('dsh-tts/advancedOpen') === '1' } catch { /* localStorage unavailable */ return false }
|
|
13
|
-
})
|
|
14
|
-
const autoplay = useAutoplayGate()
|
|
15
|
-
|
|
16
|
-
const ctx = props && props.ctx
|
|
17
|
-
const scope = React.useMemo(() => {
|
|
18
|
-
const s = (ctx && ctx.get && ctx.get('lanSettings')) || (ctx && ctx.configForms)
|
|
19
|
-
if (!s || !s.get) return null
|
|
20
|
-
try {
|
|
21
|
-
return s.get(NS)
|
|
22
|
-
} catch (_) { /* settings scope binding failed */
|
|
23
|
-
return null
|
|
24
|
-
}
|
|
25
|
-
}, [ctx])
|
|
26
|
-
|
|
27
|
-
const subscribe = React.useMemo(() => {
|
|
28
|
-
return (cb) => {
|
|
29
|
-
if (!scope || !scope.subscribe) return () => {}
|
|
30
|
-
try {
|
|
31
|
-
return scope.subscribe(cb) || (() => {})
|
|
32
|
-
} catch (_) { /* settings scope binding failed */
|
|
33
|
-
return () => {}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}, [scope])
|
|
37
|
-
|
|
38
|
-
const getSnapshot = React.useCallback(() => {
|
|
39
|
-
if (!scope || !scope.getSnapshot) return SNAPSHOT_LOADING
|
|
40
|
-
try {
|
|
41
|
-
return scope.getSnapshot() || SNAPSHOT_LOADING
|
|
42
|
-
} catch (_) { /* settings scope binding failed */
|
|
43
|
-
return SNAPSHOT_LOADING
|
|
44
|
-
}
|
|
45
|
-
}, [scope])
|
|
46
|
-
|
|
47
|
-
const snap = (React.useSyncExternalStore
|
|
48
|
-
? React.useSyncExternalStore(subscribe, getSnapshot, React.useCallback(() => SNAPSHOT_LOADING, []))
|
|
49
|
-
: null) || (scope && scope.getSnapshot ? scope.getSnapshot() : SNAPSHOT_LOADING)
|
|
50
|
-
|
|
51
|
-
// Snapshot status matters more than the value:
|
|
52
|
-
// loading — host has not answered yet;
|
|
53
|
-
// unavailable — host answered but the settings namespace is not ready;
|
|
54
|
-
// ready — values are present.
|
|
55
|
-
// On unavailable, writable defaults true; without a status check the card
|
|
56
|
-
// looks like a working empty form.
|
|
57
|
-
const ready = !scope || (!!snap && snap.status === 'ready')
|
|
58
|
-
const writable = !scope ? true : (ready && snap.writable !== false)
|
|
59
|
-
|
|
60
|
-
const applyPayload = (data) => {
|
|
61
|
-
const cfg = data && data.config ? data.config : {}
|
|
62
|
-
setDraft(JSON.parse(JSON.stringify(cfg)))
|
|
63
|
-
setCredentials(data && data.credentials ? data.credentials : {})
|
|
64
|
-
player.enabled = !!cfg.speakReplies
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
React.useEffect(() => {
|
|
68
|
-
let alive = true
|
|
69
|
-
fetch('/dsh-tts/config', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
70
|
-
if (!alive) return
|
|
71
|
-
applyPayload(data)
|
|
72
|
-
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
73
|
-
|
|
74
|
-
fetch('/dsh-tts/integrations', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
75
|
-
if (!alive) return
|
|
76
|
-
if (data && data.ok) setIntegrations(data)
|
|
77
|
-
}).catch(() => {})
|
|
78
|
-
|
|
79
|
-
const loadTelemetry = () => {
|
|
80
|
-
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
81
|
-
if (!alive || !st) return
|
|
82
|
-
setTelemetry((prev) => Object.assign({}, prev, {
|
|
83
|
-
cacheHits: st.cacheHits || 0,
|
|
84
|
-
total: st.total || 0,
|
|
85
|
-
online: true,
|
|
86
|
-
breaker: st.breaker || prev.breaker || [],
|
|
87
|
-
}))
|
|
88
|
-
}).catch(() => {
|
|
89
|
-
if (alive) setTelemetry((prev) => Object.assign({}, prev, { online: false }))
|
|
90
|
-
})
|
|
91
|
-
fetch('/dsh-tts/status', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
92
|
-
if (!alive || !st) return
|
|
93
|
-
setTelemetry((prev) => Object.assign({}, prev, {
|
|
94
|
-
sseClients: st.sseClients || 0,
|
|
95
|
-
sseOk: (st.sseClients || 0) >= 0,
|
|
96
|
-
provider: (player.audio && player.provider) || (player.queue[0] && player.queue[0].provider) || '',
|
|
97
|
-
queue: player.queue.length,
|
|
98
|
-
}))
|
|
99
|
-
}).catch(() => {})
|
|
100
|
-
}
|
|
101
|
-
loadTelemetry()
|
|
102
|
-
const telTimer = setInterval(loadTelemetry, 3000)
|
|
103
|
-
// interval cleaned by effect return below
|
|
104
|
-
|
|
105
|
-
return () => { alive = false; clearInterval(telTimer) }
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
109
|
-
|
|
110
|
-
const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
|
|
111
|
-
const setDraftKey = (provider, value) => setKeyDrafts((d) => Object.assign({}, d, { [provider]: value }))
|
|
112
|
-
|
|
113
|
-
const commitKey = async (provider, raw) => {
|
|
114
|
-
const value = String(raw != null ? raw : ((keyDrafts && keyDrafts[provider]) || '')).trim()
|
|
115
|
-
if (!value) return
|
|
116
|
-
setErr('')
|
|
117
|
-
const res = await fetch('/dsh-tts/credential', {
|
|
118
|
-
method: 'PUT',
|
|
119
|
-
headers: { 'Content-Type': 'application/json' },
|
|
120
|
-
body: JSON.stringify({ provider: provider, value: value }),
|
|
121
|
-
})
|
|
122
|
-
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
123
|
-
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
124
|
-
if (data && data.credentials) setCredentials(data.credentials)
|
|
125
|
-
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const clearKey = async (provider) => {
|
|
129
|
-
setErr('')
|
|
130
|
-
try {
|
|
131
|
-
const res = await fetch('/dsh-tts/credential', {
|
|
132
|
-
method: 'DELETE',
|
|
133
|
-
headers: { 'Content-Type': 'application/json' },
|
|
134
|
-
body: JSON.stringify({ provider: provider }),
|
|
135
|
-
})
|
|
136
|
-
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
137
|
-
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
138
|
-
if (data && data.credentials) setCredentials(data.credentials)
|
|
139
|
-
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
140
|
-
} catch (e) { setErr(String(e && e.message ? e.message : e)) }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const save = async () => {
|
|
144
|
-
setErr(''); setSaved(false)
|
|
145
|
-
if (!draft) return
|
|
146
|
-
try {
|
|
147
|
-
const keys = {}
|
|
148
|
-
Object.keys(keyDrafts || {}).forEach((p) => {
|
|
149
|
-
const v = String(keyDrafts[p] || '').trim()
|
|
150
|
-
if (v) keys[p] = v
|
|
151
|
-
})
|
|
152
|
-
const res = await fetch('/dsh-tts/config', {
|
|
153
|
-
method: 'PUT',
|
|
154
|
-
headers: { 'Content-Type': 'application/json' },
|
|
155
|
-
body: JSON.stringify(Object.assign({}, draft, { keys: keys })),
|
|
156
|
-
})
|
|
157
|
-
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
158
|
-
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
159
|
-
applyPayload(data)
|
|
160
|
-
setKeyDrafts({})
|
|
161
|
-
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
162
|
-
} catch (e) { setErr(String(e && e.message ? e.message : e)) }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const onCommitKey = (provider, raw) => {
|
|
166
|
-
commitKey(provider, raw).catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const clearCacheNow = async () => {
|
|
170
|
-
setErr('')
|
|
171
|
-
const res = await fetch('/dsh-tts/cache', { method: 'DELETE' })
|
|
172
|
-
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
173
|
-
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
174
|
-
}
|
|
175
|
-
const onClearCache = () => {
|
|
176
|
-
if (typeof window !== 'undefined' && window.confirm) {
|
|
177
|
-
if (!window.confirm(t('confirmClearCache'))) return
|
|
178
|
-
}
|
|
179
|
-
clearCacheNow().catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const onPreview = (provider, model, voice, text) => {
|
|
183
|
-
fetch('/dsh-tts/preview', {
|
|
184
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
185
|
-
body: JSON.stringify({ provider, model, voice, text }),
|
|
186
|
-
})
|
|
187
|
-
.then((r) => r.json())
|
|
188
|
-
.then((d) => {
|
|
189
|
-
if (!d.ok) throw new Error((d.error && d.error.message) || 'HTTP error')
|
|
190
|
-
new Audio('data:' + d.mime + ';base64,' + d.audioBase64).play().catch(() => { /* autoplay blocked */ })
|
|
191
|
-
})
|
|
192
|
-
.catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const onPreviewPhrase = (phrase) => {
|
|
196
|
-
if (!phrase) return
|
|
197
|
-
const first = draft && draft.chain && draft.chain[0]
|
|
198
|
-
onPreview((first && first.provider) || 'kokoro', (first && first.model) || '', (first && first.voice) || '', phrase)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const boolField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
202
|
-
React.createElement('input', {
|
|
203
|
-
type: 'checkbox', checked: !!(draft && draft[key]), disabled: !writable,
|
|
204
|
-
onChange: (e) => setTop(key, e.target.checked),
|
|
205
|
-
}),
|
|
206
|
-
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
207
|
-
|
|
208
|
-
const numberField = (key, label, hint, step) => React.createElement('label', { className: 'dts-field' }, label,
|
|
209
|
-
React.createElement('input', {
|
|
210
|
-
type: 'number', step: step || 1,
|
|
211
|
-
className: 'dts-input',
|
|
212
|
-
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
213
|
-
onChange: (e) => setTop(key, Number(e.target.value)),
|
|
214
|
-
}),
|
|
215
|
-
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
216
|
-
|
|
217
|
-
const textField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
218
|
-
React.createElement('input', {
|
|
219
|
-
className: 'dts-input',
|
|
220
|
-
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
221
|
-
onChange: (e) => setTop(key, e.target.value),
|
|
222
|
-
}),
|
|
223
|
-
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
224
|
-
|
|
225
|
-
const autoplayBanner = autoplay.blocked
|
|
226
|
-
? React.createElement('div', { className: 'dts-unlock' },
|
|
227
|
-
React.createElement('span', null, t('autoplayBlocked')),
|
|
228
|
-
React.createElement('button', {
|
|
229
|
-
type: 'button', className: 'dts-unlock-btn',
|
|
230
|
-
onClick: () => player.unlockAudio && player.unlockAudio(),
|
|
231
|
-
}, t('enableSound')),
|
|
232
|
-
)
|
|
233
|
-
: null
|
|
234
|
-
const breakerBadges = (telemetry.breaker || []).filter((b) => b && b.open).slice(0, 2)
|
|
235
|
-
.map((b) => React.createElement('span', { key: b.id, className: 'dts-badge dts-badge-bad' }, t('breakerOpen') + ': ' + b.id))
|
|
236
|
-
const telemetryBadges = React.createElement('div', { className: 'dts-row', style: { marginBottom: '8px' }, 'data-dsh-tts-telemetry': '1' },
|
|
237
|
-
React.createElement('span', {
|
|
238
|
-
className: 'dts-badge ' + (telemetry.online ? 'dts-badge-on' : 'dts-badge-bad'),
|
|
239
|
-
}, telemetry.online ? t('statusOnline') : t('statusOffline')),
|
|
240
|
-
React.createElement('span', {
|
|
241
|
-
className: 'dts-badge ' + (telemetry.sseClients > 0 ? 'dts-badge-on' : ''),
|
|
242
|
-
}, t('sseState') + ': ' + (telemetry.sseClients || 0)),
|
|
243
|
-
React.createElement('span', { className: 'dts-badge' },
|
|
244
|
-
t('queueLen') + ': ' + (telemetry.queue || player.queue.length || 0)),
|
|
245
|
-
telemetry.provider ? React.createElement('span', { className: 'dts-badge' },
|
|
246
|
-
t('activeProvider') + ': ' + telemetry.provider) : null,
|
|
247
|
-
React.createElement('span', { className: 'dts-badge' },
|
|
248
|
-
t('cacheStats') + ': ' + telemetry.cacheHits + ' / ' + telemetry.total),
|
|
249
|
-
...breakerBadges,
|
|
250
|
-
)
|
|
251
|
-
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-header' },
|
|
252
|
-
React.createElement('div', { className: 'dts-page-title' }, '🔊 ' + t('title')),
|
|
253
|
-
React.createElement('div', { className: 'dts-page-sub' }, t('subtitle')),
|
|
254
|
-
telemetryBadges,
|
|
255
|
-
)
|
|
256
|
-
return React.createElement('div', { className: 'dts-wrap' },
|
|
257
|
-
autoplayBanner,
|
|
258
|
-
React.createElement('div', { className: 'dts-block' },
|
|
259
|
-
titleLine,
|
|
260
|
-
React.createElement('label', { className: 'dts-row' },
|
|
261
|
-
React.createElement('input', {
|
|
262
|
-
type: 'checkbox', checked: !!(draft && draft.speakReplies), disabled: !writable,
|
|
263
|
-
onChange: (e) => setTop('speakReplies', e.target.checked),
|
|
264
|
-
}),
|
|
265
|
-
React.createElement('span', null, t('speakReplies')),
|
|
266
|
-
),
|
|
267
|
-
React.createElement('div', { className: 'dts-sub' },
|
|
268
|
-
t('speakRepliesHint')),
|
|
269
|
-
),
|
|
270
|
-
React.createElement(LocalEnginesEditor, {
|
|
271
|
-
t: t, writable: writable, draft: draft,
|
|
272
|
-
boolField: boolField, setTop: setTop,
|
|
273
|
-
}),
|
|
274
|
-
React.createElement(VoiceDuplexEditor, {
|
|
275
|
-
t: t, writable: writable, draft: draft,
|
|
276
|
-
boolField: boolField, setTop: setTop,
|
|
277
|
-
installed: !!(integrations && integrations.voice && integrations.voice.installed),
|
|
278
|
-
}),
|
|
279
|
-
React.createElement(MessengerIntegrationEditor, {
|
|
280
|
-
t: t, writable: writable, draft: draft,
|
|
281
|
-
boolField: boolField, setTop: setTop,
|
|
282
|
-
installed: !!(integrations && integrations.messenger && integrations.messenger.installed),
|
|
283
|
-
}),
|
|
284
|
-
React.createElement('div', { className: 'dts-block' },
|
|
285
|
-
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
|
286
|
-
boolField('enableItDictionary', t('enableItDictionary'), t('enableItDictionaryHint')),
|
|
287
|
-
React.createElement(PronEditor, {
|
|
288
|
-
t: t, writable: writable,
|
|
289
|
-
value: draft && draft.pronunciation ? draft.pronunciation : [],
|
|
290
|
-
onChange: (v) => setTop('pronunciation', v),
|
|
291
|
-
onPreviewPhrase: onPreviewPhrase,
|
|
292
|
-
}),
|
|
293
|
-
),
|
|
294
|
-
React.createElement('div', { className: 'dts-block' },
|
|
295
|
-
React.createElement('div', { className: 'dts-h' }, t('rolesTitle')),
|
|
296
|
-
boolField('autoDetectSubagent', t('autoDetectSubagent'), t('autoDetectSubagent')),
|
|
297
|
-
React.createElement(RolesEditor, {
|
|
298
|
-
t: t, writable: writable, onPreview: onPreview,
|
|
299
|
-
value: draft && draft.roles ? draft.roles : {},
|
|
300
|
-
onChange: (v) => setTop('roles', v),
|
|
301
|
-
}),
|
|
302
|
-
),
|
|
303
|
-
React.createElement('div', { className: 'dts-block' },
|
|
304
|
-
React.createElement('div', { className: 'dts-h' }, t('statsTitle')),
|
|
305
|
-
React.createElement(StatsPanel, { t: t }),
|
|
306
|
-
),
|
|
307
|
-
React.createElement('div', { className: 'dts-block' },
|
|
308
|
-
React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
|
|
309
|
-
React.createElement('div', { className: 'dts-sub' },
|
|
310
|
-
t('chainHintCard')),
|
|
311
|
-
React.createElement(ChainEditor, {
|
|
312
|
-
breaker: telemetry.breaker,
|
|
313
|
-
t: t,
|
|
314
|
-
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
315
|
-
credentials: credentials, keyDrafts: keyDrafts,
|
|
316
|
-
onChange: (v) => setTop('chain', v),
|
|
317
|
-
onDraft: setDraftKey,
|
|
318
|
-
onCommitKey: onCommitKey,
|
|
319
|
-
onClearKey: clearKey,
|
|
320
|
-
onPreview: onPreview,
|
|
321
|
-
}),
|
|
322
|
-
),
|
|
323
|
-
React.createElement('div', { className: 'dts-block' },
|
|
324
|
-
React.createElement('div', { className: 'dts-h' }, t('general')),
|
|
325
|
-
boolField('speakAsItGoes', t('speakAsItGoes'),
|
|
326
|
-
t('speakAsItGoesHint')),
|
|
327
|
-
numberField('rate', t('rate'), t('rateHint'), 0.1),
|
|
328
|
-
boolField('bargeIn', t('bargeIn'),
|
|
329
|
-
t('bargeInHint')),
|
|
330
|
-
boolField('announceApproval', t('announce'),
|
|
331
|
-
t('announceHint')),
|
|
332
|
-
boolField('skipCode', t('skipCode'),
|
|
333
|
-
t('skipCodeHint')),
|
|
334
|
-
boolField('cache', t('cache'),
|
|
335
|
-
t('cacheHint')),
|
|
336
|
-
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
337
|
-
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
338
|
-
React.createElement('select', {
|
|
339
|
-
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
340
|
-
onChange: (e) => setTop('longReply', e.target.value),
|
|
341
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
342
|
-
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
343
|
-
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
344
|
-
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
345
|
-
boolField('autoDetect', t('autoDetect'), t('autoDetectHint')),
|
|
346
|
-
boolField('narrateQuotesOnly', t('narrateQuotesOnly'), t('narrateQuotesHint')),
|
|
347
|
-
boolField('skipActions', t('skipActions'), t('skipActionsHint')),
|
|
348
|
-
textField('removeRegex', t('removeRegex'), t('removeRegexHint')),
|
|
349
|
-
textField('customBaseUrl', t('customBaseUrl'), t('customBaseUrlHint')),
|
|
350
|
-
textField('customKeyEnv', t('customKeyEnv'), t('customKeyEnvHint')),
|
|
351
|
-
textField('approvalText', t('approvalText'), t('approvalHint')),
|
|
352
|
-
textField('questionText', t('questionText'), t('questionHint')),
|
|
353
|
-
textField('chime', t('chime'), t('chimeHint')),
|
|
354
|
-
textField('language', t('language'), t('languageHint')),
|
|
355
|
-
textField('piperModel', t('piperModel'), t('piperModelHint')),
|
|
356
|
-
textField('piperBin', t('piperBin'), t('binHint')),
|
|
357
|
-
textField('espeakBin', t('espeakBin'), t('espeakHint')),
|
|
358
|
-
textField('edgeBin', t('edgeBin'), t('edgeHint')),
|
|
359
|
-
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
360
|
-
),
|
|
361
|
-
React.createElement('div', { className: 'dts-block' },
|
|
362
|
-
React.createElement('button', {
|
|
363
|
-
type: 'button',
|
|
364
|
-
className: 'dts-head',
|
|
365
|
-
'aria-expanded': advancedOpen,
|
|
366
|
-
onClick: () => {
|
|
367
|
-
setAdvancedOpen((v) => {
|
|
368
|
-
const next = !v
|
|
369
|
-
try { localStorage.setItem('dsh-tts/advancedOpen', next ? '1' : '0') } catch { /* private mode */ }
|
|
370
|
-
return next
|
|
371
|
-
})
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
React.createElement('div', { className: 'dts-headText' },
|
|
375
|
-
React.createElement('div', { className: 'dts-title' }, t('advancedTitle')),
|
|
376
|
-
React.createElement('div', { className: 'dts-sub' }, t('advancedHint')),
|
|
377
|
-
),
|
|
378
|
-
),
|
|
379
|
-
advancedOpen ? React.createElement('div', { className: 'dts-body' },
|
|
380
|
-
numberField('maxChars', t('maxChars'), t('maxCharsHint'), 100),
|
|
381
|
-
numberField('sentenceChars', t('sentenceChars'), t('sentenceCharsHint'), 20),
|
|
382
|
-
numberField('timeoutMs', t('timeoutMs'), t('timeoutMsHint'), 1000),
|
|
383
|
-
numberField('maxQueue', t('maxQueue'), t('maxQueueHint'), 1),
|
|
384
|
-
textField('openaiBaseUrl', t('openaiBaseUrl'), t('openaiBaseUrlHint')),
|
|
385
|
-
textField('mimoBaseUrl', t('mimoBaseUrl'), t('mimoBaseUrlHint')),
|
|
386
|
-
textField('mimoFormat', t('mimoFormat'), t('mimoFormatHint')),
|
|
387
|
-
textField('minimaxBin', t('minimaxBin'), t('minimaxBinHint')),
|
|
388
|
-
) : null,
|
|
389
|
-
),
|
|
390
|
-
React.createElement('div', { className: 'dts-foot' },
|
|
391
|
-
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
392
|
-
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
|
|
393
|
-
saved ? React.createElement('span', { className: 'dts-ok' }, t('saved')) : null,
|
|
394
|
-
err ? React.createElement('span', { className: 'dts-bad' }, err) : null,
|
|
395
|
-
),
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
function TtsCard(props) {
|
|
2
|
-
const t = (props && props.t) || ((key) => key)
|
|
3
|
-
const [open, setOpen] = React.useState(false)
|
|
4
|
-
return React.createElement('li', { className: 'dts-card' },
|
|
5
|
-
React.createElement('button', {
|
|
6
|
-
type: 'button',
|
|
7
|
-
className: 'dts-head',
|
|
8
|
-
onClick: () => setOpen(!open),
|
|
9
|
-
'aria-expanded': open,
|
|
10
|
-
},
|
|
11
|
-
React.createElement('div', { className: 'dts-headText' },
|
|
12
|
-
React.createElement('div', { className: 'dts-title' }, t('title')),
|
|
13
|
-
React.createElement('div', { className: 'dts-sub' }, t('cardHint')),
|
|
14
|
-
),
|
|
15
|
-
ChevronIcon
|
|
16
|
-
? React.createElement(ChevronIcon, { className: 'dts-chev' + (open ? ' dts-chevOpen' : '') })
|
|
17
|
-
: React.createElement('svg', {
|
|
18
|
-
className: 'dts-chev' + (open ? ' dts-chevOpen' : ''),
|
|
19
|
-
width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none',
|
|
20
|
-
},
|
|
21
|
-
React.createElement('path', {
|
|
22
|
-
d: 'M4 6l4 4 4-4', stroke: 'currentColor', 'stroke-width': 1.5,
|
|
23
|
-
'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
24
|
-
}),
|
|
25
|
-
),
|
|
26
|
-
),
|
|
27
|
-
open ? React.createElement('div', { className: 'dts-body' },
|
|
28
|
-
React.createElement(ErrorBoundary, null,
|
|
29
|
-
React.createElement(TtsSection, Object.assign({}, props, { compact: true, ctx: props && props.ctx })),
|
|
30
|
-
),
|
|
31
|
-
) : null,
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Input-dock button: shows what is playing, stop control, and recents for replay.
|
|
36
|
-
function SpeakerControl(props) {
|
|
37
|
-
// A slot with `locale` injects translations into props; the fallback is the
|
|
38
|
-
// binder created at registration, in case the slot drops translations.
|
|
39
|
-
const t = (props && props.t) || fallbackDockText
|
|
40
|
-
const p = usePlayer()
|
|
41
|
-
const [showList, setShowList] = React.useState(false)
|
|
42
|
-
const active = !!p.audio || p.queue.length > 0 || p.busy
|
|
43
|
-
const [, force] = React.useReducer((n) => n + 1, 0)
|
|
44
|
-
if (!active && !showList) return null
|
|
45
|
-
const lists = recent.list()
|
|
46
|
-
const row = (text, starred) => React.createElement('div', { className: 'dts-row', key: text.slice(0, 24) + starred },
|
|
47
|
-
React.createElement('button', {
|
|
48
|
-
type: 'button', className: 'dts-link', style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
49
|
-
title: text,
|
|
50
|
-
onClick: () => playStandalone(text), title: text,
|
|
51
|
-
}, text.slice(0, 80)),
|
|
52
|
-
React.createElement('button', {
|
|
53
|
-
type: 'button', className: 'dts-link', title: '★',
|
|
54
|
-
onClick: () => { recent.toggleFav(text); force() },
|
|
55
|
-
}, starred ? '★' : '☆'),
|
|
56
|
-
React.createElement('button', {
|
|
57
|
-
type: 'button', className: 'dts-link', title: t('exportAudio') || 'Export',
|
|
58
|
-
onClick: () => exportAudioClip(text),
|
|
59
|
-
}, '⤓'),
|
|
60
|
-
)
|
|
61
|
-
return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
|
|
62
|
-
React.createElement('button', {
|
|
63
|
-
type: 'button', className: 'dts-link', title: t('recent') || 'Recent',
|
|
64
|
-
onClick: () => setShowList((v) => !v),
|
|
65
|
-
}, '☰'),
|
|
66
|
-
showList ? React.createElement('div', {
|
|
67
|
-
className: 'dts-wrap',
|
|
68
|
-
style: { position: 'absolute', bottom: '36px', right: 0, zIndex: 30, maxHeight: '320px', overflowY: 'auto', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '12px', padding: '10px 12px', minWidth: '260px', flexDirection: 'column', gap: '6px', display: 'flex' },
|
|
69
|
-
},
|
|
70
|
-
lists.favs.length ? React.createElement('div', { className: 'dts-h' }, '★') : null,
|
|
71
|
-
lists.favs.map((text) => row(text, true)),
|
|
72
|
-
React.createElement('div', { className: 'dts-h' }, '⟳'),
|
|
73
|
-
lists.items.filter((x) => !lists.favs.includes(x)).map((text) => row(text, false)),
|
|
74
|
-
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, t('recentEmpty') || 'No recent speech yet') : null,
|
|
75
|
-
) : null,
|
|
76
|
-
React.createElement('button', {
|
|
77
|
-
type: 'button',
|
|
78
|
-
className: 'dts-link',
|
|
79
|
-
title: p.paused ? t('dockResume') : t('dockPause'),
|
|
80
|
-
onClick: togglePause,
|
|
81
|
-
}, p.paused ? '▶' : '❚❚'),
|
|
82
|
-
React.createElement('button', {
|
|
83
|
-
type: 'button',
|
|
84
|
-
className: 'dts-link',
|
|
85
|
-
title: t('dockStop'),
|
|
86
|
-
onClick: stopPlayback,
|
|
87
|
-
}, '■'),
|
|
88
|
-
React.createElement('button', {
|
|
89
|
-
type: 'button',
|
|
90
|
-
className: 'dts-link',
|
|
91
|
-
title: t('exportAudio') || 'Export clip',
|
|
92
|
-
onClick: () => exportAudioClip(player.lastText),
|
|
93
|
-
}, '⤓'),
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Fallback translator for the dock.
|
|
99
|
-
*
|
|
100
|
-
* Bound at registration for the page lifetime. The core draws the slot label;
|
|
101
|
-
* props.t never reaches it, so the binder is the only way to give it a language.
|
|
102
|
-
*/
|
|
103
|
-
let fallbackDockText = (key) => key
|
|
104
|
-
|
|
105
|
-
function registerSpeaker(ctx) {
|
|
106
|
-
// `locale` in the slot descriptor is how core injects translations into props.
|
|
107
|
-
// Without it the dock would show raw keys only.
|
|
108
|
-
fallbackDockText = ctx.locale.bind(NS)
|
|
109
|
-
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
110
|
-
{
|
|
111
|
-
name: 'conversation.input.dock',
|
|
112
|
-
id: '@goodandready/dsh-tts',
|
|
113
|
-
locale: NS,
|
|
114
|
-
order: 5,
|
|
115
|
-
label: () => fallbackDockText('dockLabel'),
|
|
116
|
-
},
|
|
117
|
-
SpeakerControl,
|
|
118
|
-
))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Plugins page row seat (`plugins.row.config`, current core).
|
|
123
|
-
*
|
|
124
|
-
* The host page draws the title, icon and crumb and provides the content
|
|
125
|
-
* padding, so the page view renders the settings form bare: our own card
|
|
126
|
-
* chrome would double the border and shift the form out of the content
|
|
127
|
-
* area. The summary view feeds the one-liner under the row title.
|
|
128
|
-
*/
|
|
129
|
-
function TtsRowConfig(props) {
|
|
130
|
-
const t = (props && props.t) || ((key) => key)
|
|
131
|
-
if (props && props.view === 'summary') {
|
|
132
|
-
return React.createElement('div', { className: 'dts-sub' }, t('subtitle'))
|
|
133
|
-
}
|
|
134
|
-
return React.createElement('div', { className: 'dts-page' },
|
|
135
|
-
React.createElement(ErrorBoundary, null,
|
|
136
|
-
React.createElement(TtsSection, Object.assign({}, props, { ctx: props && props.ctx })),
|
|
137
|
-
),
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function registerSettings(ctx) {
|
|
142
|
-
// Dictionary packages may also register languages for other namespaces.
|
|
143
|
-
// Core throws on duplicate namespace+language; an unguarded call used to
|
|
144
|
-
// take down the whole plugin ("Failed to load plugins"). Register each
|
|
145
|
-
// language separately: skip if taken; English still lands.
|
|
146
|
-
const addLocale = (locale, dictionary) => {
|
|
147
|
-
try {
|
|
148
|
-
return ctx.locale.register(NS, locale, dictionary)
|
|
149
|
-
} catch (alreadyTaken) {
|
|
150
|
-
return () => {}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
ctx.effect(() => {
|
|
154
|
-
const undo = [addLocale('en', en), addLocale('zh', zh)]
|
|
155
|
-
return () => { for (const off of undo) off() }
|
|
156
|
-
}, 'dsh-tts: locales')
|
|
157
|
-
// The sidebar draws the section label, not our component: props.t never
|
|
158
|
-
// reaches it, so bind the translator to the namespace ourselves.
|
|
159
|
-
const t = ctx.locale.bind(NS)
|
|
160
|
-
// Current core: settings live on the plugin's own row on the Plugins page
|
|
161
|
-
// (`plugins.row.config`). Key = '<package name>#<row id>'; the row gains a
|
|
162
|
-
// configure control that asks for view:'summary' and view:'page'.
|
|
163
|
-
try {
|
|
164
|
-
ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
|
|
165
|
-
{
|
|
166
|
-
name: 'plugins.row.config',
|
|
167
|
-
key: ROW_CONFIG_KEY,
|
|
168
|
-
locale: NS,
|
|
169
|
-
inject: () => ({ ctx: ctx }),
|
|
170
|
-
},
|
|
171
|
-
TtsRowConfig,
|
|
172
|
-
))
|
|
173
|
-
} catch (rowSeatUnavailable) {
|
|
174
|
-
console.warn('[dsh-tts] row seat plugins.row.config not available:', rowSeatUnavailable && rowSeatUnavailable.message)
|
|
175
|
-
}
|
|
176
|
-
// Plugin-list seat (`plugins.item`): the seat the current core (0.1.6-alpha.2)
|
|
177
|
-
// renders as the plugin's own page with its configuration. The label is a static
|
|
178
|
-
// string on purpose — it is resolved while the page renders, and a locale lookup
|
|
179
|
-
// there would take the whole client batch down with it.
|
|
180
|
-
try {
|
|
181
|
-
ctx.slots.inject('plugins.item', () => ctx.slots.register(
|
|
182
|
-
{
|
|
183
|
-
name: 'plugins.item',
|
|
184
|
-
id: ROW_ID,
|
|
185
|
-
order: 60,
|
|
186
|
-
label: () => 'Text to Speech',
|
|
187
|
-
locale: NS,
|
|
188
|
-
inject: () => ({ ctx: ctx }),
|
|
189
|
-
},
|
|
190
|
-
TtsRowConfig,
|
|
191
|
-
))
|
|
192
|
-
} catch (itemSeatUnavailable) {
|
|
193
|
-
console.warn('[dsh-tts] list seat plugins.item not available:', itemSeatUnavailable && itemSeatUnavailable.message)
|
|
194
|
-
}
|
|
195
|
-
// Fallback for hosts that render the older plugin-settings tab card. The tab
|
|
196
|
-
// looks up a slot by entryKey equal to the namespace name: key must equal NS
|
|
197
|
-
// or the card never appears, with no log error.
|
|
198
|
-
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
199
|
-
{
|
|
200
|
-
name: 'settings.plugin.item',
|
|
201
|
-
key: NS,
|
|
202
|
-
locale: NS,
|
|
203
|
-
inject: () => ({ ctx: ctx }),
|
|
204
|
-
},
|
|
205
|
-
TtsCard,
|
|
206
|
-
))
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
exports.inject = ['slots', 'configForms', 'locale']
|
|
210
|
-
exports.apply = function apply(ctx) {
|
|
211
|
-
registerSettings(ctx)
|
|
212
|
-
registerSpeaker(ctx)
|
|
213
|
-
ctx.effect(() => listenForVoice(), 'dsh-tts: barge-in on user speech')
|
|
214
|
-
ctx.effect(() => {
|
|
215
|
-
if (typeof window === 'undefined') return () => {}
|
|
216
|
-
const onKey = (e) => {
|
|
217
|
-
if (e.ctrlKey && e.key === 'Escape') { togglePause(); e.preventDefault() }
|
|
218
|
-
else if (e.altKey && (e.code === 'KeyS' || e.key === 's' || e.key === 'S')) { stopPlayback(); e.preventDefault() }
|
|
219
|
-
else if (e.altKey && e.key === 'ArrowRight') { skipCurrent(); e.preventDefault() }
|
|
220
|
-
}
|
|
221
|
-
window.addEventListener('keydown', onKey)
|
|
222
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
223
|
-
}, 'dsh-tts: hotkeys')
|
|
224
|
-
ctx.effect(() => {
|
|
225
|
-
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
226
|
-
let es = null
|
|
227
|
-
let closed = false
|
|
228
|
-
let attempt = 0
|
|
229
|
-
let retryTimer = null
|
|
230
|
-
const onItem = (e) => {
|
|
231
|
-
try {
|
|
232
|
-
const item = JSON.parse(e.data)
|
|
233
|
-
if (!item || !item.id) return
|
|
234
|
-
if (seenIds.has(item.id)) return
|
|
235
|
-
seenIds.add(item.id)
|
|
236
|
-
if (seenIds.size > 500) {
|
|
237
|
-
const first = seenIds.values().next().value
|
|
238
|
-
seenIds.delete(first)
|
|
239
|
-
}
|
|
240
|
-
player.queue.push(item)
|
|
241
|
-
drainQueue()
|
|
242
|
-
} catch { /* ignore malformed SSE frame */ }
|
|
243
|
-
}
|
|
244
|
-
const connect = () => {
|
|
245
|
-
if (closed) return
|
|
246
|
-
try {
|
|
247
|
-
es = new EventSource('/dsh-tts/stream')
|
|
248
|
-
es.addEventListener('open', () => { attempt = 0 })
|
|
249
|
-
es.addEventListener('utterance', onItem)
|
|
250
|
-
es.addEventListener('chime', onItem)
|
|
251
|
-
es.onerror = () => {
|
|
252
|
-
try { es.close() } catch { /* already closed */ }
|
|
253
|
-
if (closed) return
|
|
254
|
-
attempt += 1
|
|
255
|
-
const delay = Math.min(15000, 500 * Math.pow(2, Math.min(attempt, 5)))
|
|
256
|
-
retryTimer = setTimeout(connect, delay)
|
|
257
|
-
}
|
|
258
|
-
} catch { /* EventSource constructor failed — poll remains */ }
|
|
259
|
-
}
|
|
260
|
-
connect()
|
|
261
|
-
return () => {
|
|
262
|
-
closed = true
|
|
263
|
-
if (retryTimer) clearTimeout(retryTimer)
|
|
264
|
-
if (es) { try { es.close() } catch { /* already closed */ } }
|
|
265
|
-
}
|
|
266
|
-
}, 'dsh-tts: SSE audio stream')
|
|
267
|
-
ctx.effect(() => {
|
|
268
|
-
const timer = setInterval(pollPending, 1000)
|
|
269
|
-
return () => clearInterval(timer)
|
|
270
|
-
}, 'dsh-tts: poll pending audio')
|
|
271
|
-
}
|
|
272
|
-
return module.exports
|
|
273
|
-
},
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
// ModelManager UI placeholder - manual install buttons for Kokoro/F5
|
|
277
|
-
// Actual implementation would show download progress and status
|
|
278
|
-
|