@goodandready/dsh-tts 0.4.22 → 0.4.24

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.
@@ -1,422 +0,0 @@
1
- function ChainEditor(props) {
2
- // Translator comes from the card: the slot passes props.t only to it.
3
- const t = props.t || ((key) => key)
4
- const rows = Array.isArray(props.value) ? props.value : []
5
- const change = (i, patch) => {
6
- const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
7
- props.onChange(next)
8
- }
9
- const move = (i, delta) => {
10
- const j = i + delta
11
- if (j < 0 || j >= rows.length) return
12
- const next = rows.slice()
13
- const tmp = next[i]; next[i] = next[j]; next[j] = tmp
14
- props.onChange(next)
15
- }
16
- const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
17
- const add = () => props.onChange(rows.concat([{ provider: 'espeak', model: '', voice: '' }]))
18
- return React.createElement('div', { className: 'dts-block' },
19
- rows.map((row, i) => {
20
- const cloud = !!CLOUD[row.provider]
21
- const cred = credOf(props.credentials, row.provider)
22
- const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
23
- const badge = !cloud ? null
24
- : (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
25
- return React.createElement('div', { className: 'dts-entry', key: i },
26
- React.createElement('div', { className: 'dts-row' },
27
- React.createElement('select', {
28
- className: 'dts-input',
29
- value: row.provider, disabled: !props.writable,
30
- onChange: (e) => change(i, { provider: e.target.value }),
31
- }, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
32
- React.createElement('input', {
33
- className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
34
- placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
35
- }),
36
- React.createElement('input', {
37
- className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
38
- placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
39
- }),
40
- React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
41
- React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
42
- React.createElement('button', { type: 'button', className: 'dts-mini', title: t('testProvider') || t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
43
- React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
44
- ),
45
- cloud ? React.createElement('div', { className: 'dts-row' },
46
- React.createElement('input', {
47
- className: 'dts-key dts-input', type: 'password', autoComplete: 'off',
48
- value: draft, disabled: !props.writable || !cred.writable,
49
- placeholder: cred.configured ? 'leave blank to keep' : 'paste API key',
50
- onChange: (e) => props.onDraft(row.provider, e.target.value),
51
- onBlur: (e) => props.onCommitKey(row.provider, e.target.value),
52
- }),
53
- React.createElement('span', { className: 'dts-badge' + (cred.configured ? ' dts-badge-on' : '') }, badge),
54
- cred.configured && cred.writable ? React.createElement('button', {
55
- type: 'button', className: 'dts-link', disabled: !props.writable,
56
- onClick: () => props.onClearKey(row.provider),
57
- }, t('clear')) : null,
58
- ) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
59
- cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
60
- (() => {
61
- const hit = Array.isArray(props.breaker) ? props.breaker.find((b) => b && b.id === row.provider) : null
62
- if (!hit || (!hit.open && !hit.lastError)) return null
63
- return React.createElement('div', { className: 'dts-sub dts-bad' },
64
- (hit.open ? t('breakerOpen') + ' · ' : '') + (hit.lastError || '')
65
- )
66
- })(),
67
- )
68
- }),
69
- React.createElement('div', { className: 'dts-row' },
70
- React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
71
- React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
72
- ),
73
- )
74
- }
75
-
76
- // Card strings live in the locale registry so a separate package can translate
77
- // them without touching this plugin. English is the source language and fallback.
78
-
79
- function LocalEnginesEditor(props) {
80
- const t = props.t || ((key) => key)
81
- const [models, setModels] = React.useState({})
82
-
83
- const loadStatus = React.useCallback(() => {
84
- if (typeof fetch === 'undefined') return
85
- fetch('/dsh-tts/models/status')
86
- .then((r) => r.json())
87
- .then((d) => { if (d && d.ok && d.models) setModels(d.models) })
88
- .catch(() => {}) // status poll best-effort
89
- }, [])
90
-
91
- React.useEffect(() => {
92
- loadStatus()
93
- const timer = setInterval(loadStatus, 5000)
94
- return () => clearInterval(timer)
95
- }, [loadStatus])
96
-
97
- const remove = (engine) => {
98
- if (typeof window !== 'undefined' && window.confirm) {
99
- if (!window.confirm(t('confirmDeleteModel'))) return
100
- }
101
- fetch('/dsh-tts/models/delete', {
102
- method: 'DELETE',
103
- headers: { 'content-type': 'application/json' },
104
- body: JSON.stringify({ engine }),
105
- }).then(() => loadStatus()).catch(() => {})
106
- }
107
-
108
- const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
109
- const f5 = models.f5 || { installed: false, downloading: false, progress: 0 }
110
-
111
- const engineRow = (label, state) => React.createElement('div', { className: 'dts-entry' },
112
- React.createElement('div', { className: 'dts-row' },
113
- React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, label),
114
- React.createElement('span', {
115
- className: 'dts-badge dts-badge-warn',
116
- }, t('runtimeNotBundled')),
117
- state.installed ? React.createElement('button', {
118
- type: 'button', className: 'dts-link', disabled: !props.writable,
119
- onClick: () => remove(label.toLowerCase().includes('kokoro') ? 'kokoro' : 'f5'),
120
- }, t('deleteModel')) : null,
121
- ),
122
- React.createElement('div', { className: 'dts-sub' }, t('runtimeNotBundledHint')),
123
- )
124
-
125
- return React.createElement('div', { className: 'dts-block' },
126
- React.createElement('div', { className: 'dts-h' }, t('localEnginesTitle')),
127
- React.createElement('div', { className: 'dts-sub' }, t('localEnginesHint')),
128
- React.createElement('div', { className: 'dts-alert-warn dts-sub' }, t('runtimeNotBundledHint')),
129
- props.boolField('streamingEnabled', t('streamingEnabled'), t('streamingEnabledHint')),
130
- engineRow('Kokoro-82M (CPU)', kokoro),
131
- engineRow('F5-TTS (GPU)', f5),
132
- )
133
- }
134
-
135
- const CLIENT_IT_TERMS = []
136
-
137
- // Pronunciation dictionary editor: rules apply top-down.
138
- function PronEditor(props) {
139
- const t = props.t || ((key) => key)
140
- const rows = Array.isArray(props.value) ? props.value : []
141
- const fileInputRef = React.useRef(null)
142
-
143
- const change = (i, patch) => {
144
- props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
145
- }
146
- const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
147
- const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
148
- const loadIt = () => {
149
- const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
150
- const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
151
- props.onChange(rows.concat(toAdd))
152
- }
153
-
154
- const exportJson = () => {
155
- const data = JSON.stringify(rows, null, 2)
156
- const blob = new Blob([data], { type: 'application/json' })
157
- const url = URL.createObjectURL(blob)
158
- const a = document.createElement('a')
159
- a.href = url
160
- a.download = 'pronunciation-dictionary.json'
161
- document.body.appendChild(a)
162
- a.click()
163
- document.body.removeChild(a)
164
- setTimeout(() => { try { URL.revokeObjectURL(url) } catch (alreadyRevoked) { /* already revoked */ } }, 1000)
165
- }
166
-
167
- const triggerImport = () => {
168
- if (fileInputRef.current) fileInputRef.current.click()
169
- }
170
-
171
- const handleImportFile = (e) => {
172
- const file = e.target.files && e.target.files[0]
173
- if (!file) return
174
- const reader = new FileReader()
175
- reader.onload = (event) => {
176
- try {
177
- const parsed = JSON.parse(event.target.result)
178
- if (!Array.isArray(parsed)) {
179
- if (typeof window !== 'undefined' && window.alert) window.alert(t('invalidJsonFormat'))
180
- return
181
- }
182
- const importedRules = parsed.map((item) => ({
183
- from: String(item.from || ''),
184
- to: String(item.to || ''),
185
- whole: !!item.whole,
186
- lang: String(item.lang || ''),
187
- })).filter((r) => r.from || r.to)
188
-
189
- const existingKeys = new Set(rows.map((r) => String(r.from).toLowerCase()))
190
- const newItems = importedRules.filter((r) => !existingKeys.has(String(r.from).toLowerCase()))
191
- props.onChange(rows.concat(newItems))
192
- } catch (err) {
193
- if (typeof window !== 'undefined' && window.alert) window.alert(t('invalidJsonFormat'))
194
- }
195
- e.target.value = ''
196
- }
197
- reader.readAsText(file)
198
- }
199
-
200
- return React.createElement('div', { className: 'dts-wrap-in' },
201
- React.createElement('input', {
202
- type: 'file',
203
- ref: fileInputRef,
204
- accept: '.json',
205
- style: { display: 'none' },
206
- onChange: handleImportFile,
207
- }),
208
- rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
209
- React.createElement('input', {
210
- className: 'dts-input dts-grow', placeholder: t('pronFrom'),
211
- value: rule.from || '', disabled: !props.writable,
212
- onChange: (e) => change(i, { from: e.target.value }),
213
- }),
214
- React.createElement('input', {
215
- className: 'dts-input dts-grow', placeholder: t('pronTo'),
216
- value: rule.to || '', disabled: !props.writable,
217
- onChange: (e) => change(i, { to: e.target.value }),
218
- }),
219
- React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
220
- React.createElement('input', {
221
- type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
222
- onChange: (e) => change(i, { whole: e.target.checked }),
223
- }),
224
- ),
225
- React.createElement('input', {
226
- className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
227
- value: rule.lang || '', disabled: !props.writable,
228
- onChange: (e) => change(i, { lang: e.target.value }),
229
- }),
230
- React.createElement('button', {
231
- type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
232
- onClick: () => {
233
- if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
234
- },
235
- }, '\u25b6'),
236
- React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
237
- )),
238
- React.createElement('div', { className: 'dts-row' },
239
- React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
240
- React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
241
- React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: exportJson }, t('exportJson')),
242
- React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: triggerImport }, t('importJson')),
243
- React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
244
- ),
245
- )
246
- }
247
-
248
- function RolesEditor(props) {
249
- const t = props.t || ((key) => key)
250
- const value = props.value || {}
251
- const [newRole, setNewRole] = React.useState('')
252
- const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
253
- const removeRole = (r) => {
254
- const next = Object.assign({}, value)
255
- delete next[r]
256
- props.onChange(next)
257
- }
258
-
259
- const standardKeys = ['reply', 'approval', 'error']
260
- const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
261
- const allKeys = [...standardKeys, ...customKeys]
262
-
263
- const addCustom = () => {
264
- const name = newRole.trim().toLowerCase()
265
- if (!name || value[name]) return
266
- set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
267
- setNewRole('')
268
- }
269
-
270
- return React.createElement('div', { className: 'dts-block' },
271
- React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
272
- allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
273
- React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
274
- React.createElement('select', {
275
- className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
276
- onChange: (e) => set(r, { provider: e.target.value }),
277
- },
278
- React.createElement('option', { value: '' }, '—'),
279
- PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
280
- ),
281
- React.createElement('input', {
282
- className: 'dts-input dts-grow', placeholder: 'model',
283
- value: (value[r] && value[r].model) || '', disabled: !props.writable,
284
- onChange: (e) => set(r, { model: e.target.value }),
285
- }),
286
- React.createElement('input', {
287
- className: 'dts-input dts-grow', placeholder: 'voice',
288
- value: (value[r] && value[r].voice) || '', disabled: !props.writable,
289
- onChange: (e) => set(r, { voice: e.target.value }),
290
- }),
291
- React.createElement('input', {
292
- className: 'dts-input', style: { maxWidth: '90px' }, placeholder: t('chime'),
293
- value: (value[r] && value[r].chime) || '', disabled: !props.writable,
294
- onChange: (e) => set(r, { chime: e.target.value }),
295
- }),
296
- React.createElement('input', {
297
- className: 'dts-input dts-grow', placeholder: t('ssmlStyle') || 'ssml',
298
- title: t('ssmlStyleHint') || 'SSML tone style',
299
- value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
300
- onChange: (e) => set(r, { ssmlStyle: e.target.value }),
301
- }),
302
- 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'),
303
- !standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
304
- )),
305
- React.createElement('div', { className: 'dts-row' },
306
- React.createElement('input', {
307
- className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
308
- value: newRole, disabled: !props.writable,
309
- onChange: (e) => setNewRole(e.target.value),
310
- onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
311
- }),
312
- React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
313
- ),
314
- )
315
- }
316
-
317
- function VoiceDuplexEditor(props) {
318
- const t = props.t || ((key) => key)
319
- const installed = !!props.installed
320
-
321
- return React.createElement('div', { className: 'dts-block' },
322
- React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
323
- React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
324
- !installed ? React.createElement('div', {
325
- style: {
326
- padding: '10px 14px',
327
- borderRadius: '8px',
328
- border: '1px solid var(--dsw-alias-state-warning-primary)',
329
- background: 'var(--dsw-alias-bg-layer-2)',
330
- display: 'flex',
331
- flexDirection: 'column',
332
- gap: '4px',
333
- fontSize: '13px',
334
- },
335
- },
336
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
337
- '⚠️ ' + t('voiceNotInstalledTitle')
338
- ),
339
- React.createElement('span', { className: 'dts-sub' },
340
- t('voiceInstallHint') + ': '
341
- ),
342
- React.createElement('code', {
343
- style: {
344
- fontFamily: 'monospace',
345
- fontSize: '12px',
346
- padding: '2px 6px',
347
- borderRadius: '4px',
348
- background: 'var(--dsw-alias-bg-layer-3)',
349
- color: 'var(--dsw-alias-label-primary)',
350
- width: 'fit-content',
351
- },
352
- }, 'dsh plugin --profile web add @goodandready/dsh-voice')
353
- ) : null,
354
- props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
355
- props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
356
- )
357
- }
358
-
359
- function MessengerIntegrationEditor(props) {
360
- const t = props.t || ((key) => key)
361
- const installed = !!props.installed
362
-
363
- return React.createElement('div', { className: 'dts-block' },
364
- React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
365
- React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
366
- !installed ? React.createElement('div', {
367
- style: {
368
- padding: '10px 14px',
369
- borderRadius: '8px',
370
- border: '1px solid var(--dsw-alias-state-warning-primary)',
371
- background: 'var(--dsw-alias-bg-layer-2)',
372
- display: 'flex',
373
- flexDirection: 'column',
374
- gap: '4px',
375
- fontSize: '13px',
376
- },
377
- },
378
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
379
- '⚠️ ' + t('messengerNotInstalledTitle')
380
- ),
381
- React.createElement('span', { className: 'dts-sub' },
382
- t('messengerInstallHint') + ': '
383
- ),
384
- React.createElement('code', {
385
- style: {
386
- fontFamily: 'monospace',
387
- fontSize: '12px',
388
- padding: '2px 6px',
389
- borderRadius: '4px',
390
- background: 'var(--dsw-alias-bg-layer-3)',
391
- color: 'var(--dsw-alias-label-primary)',
392
- width: 'fit-content',
393
- },
394
- }, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
395
- ) : null,
396
- props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
397
- )
398
- }
399
-
400
- // Synthesis stats panel: live host counters.
401
- function StatsPanel(props) {
402
- const t = props.t || ((key) => key)
403
- const [data, setData] = React.useState(null)
404
- const loadStats = () => {
405
- fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
406
- }
407
- React.useEffect(() => { loadStats() }, [])
408
- const reset = () => {
409
- fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
410
- }
411
- return React.createElement('div', { className: 'dts-block' },
412
- React.createElement('span', { className: 'dts-sub' }, data
413
- ? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
414
- : t('loading')),
415
- data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
416
- p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
417
- React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
418
- )
419
- }
420
-
421
- // Plugin-settings tab card follows the shared card pattern: li in the core list,
422
- // theme-reset header, body with a divider. Chevron is ours; core does not provide it.