@goodandready/dsh-tts 0.3.23 → 0.4.1

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.
@@ -0,0 +1,410 @@
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('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
+ }),
62
+ React.createElement('div', { className: 'dts-row' },
63
+ React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
64
+ React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
65
+ ),
66
+ )
67
+ }
68
+
69
+ // Card strings live in the locale registry so a separate package can translate
70
+ // them without touching this plugin. English is the source language and fallback.
71
+
72
+ function LocalEnginesEditor(props) {
73
+ const t = props.t || ((key) => key)
74
+ const [models, setModels] = React.useState({})
75
+ const [loading, setLoading] = React.useState(false)
76
+
77
+ const loadStatus = React.useCallback(() => {
78
+ if (typeof fetch === 'undefined') return
79
+ fetch('/dsh-tts/models/status')
80
+ .then((r) => r.json())
81
+ .then((d) => { if (d && d.ok && d.models) setModels(d.models) })
82
+ .catch(() => {})
83
+ }, [])
84
+
85
+ React.useEffect(() => {
86
+ loadStatus()
87
+ const timer = setInterval(loadStatus, 3000)
88
+ return () => clearInterval(timer)
89
+ }, [loadStatus])
90
+
91
+ const install = (engine) => {
92
+ setLoading(true)
93
+ fetch('/dsh-tts/models/install', {
94
+ method: 'POST',
95
+ headers: { 'content-type': 'application/json' },
96
+ body: JSON.stringify({ engine }),
97
+ }).then(() => loadStatus()).finally(() => setLoading(false))
98
+ }
99
+
100
+ const remove = (engine) => {
101
+ setLoading(true)
102
+ fetch('/dsh-tts/models/delete', {
103
+ method: 'DELETE',
104
+ headers: { 'content-type': 'application/json' },
105
+ body: JSON.stringify({ engine }),
106
+ }).then(() => loadStatus()).finally(() => setLoading(false))
107
+ }
108
+
109
+ const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
110
+ const f5 = models.f5 || { installed: false, downloading: false, progress: 0 }
111
+
112
+ return React.createElement('div', { className: 'dts-block' },
113
+ React.createElement('div', { className: 'dts-h' }, t('localEnginesTitle')),
114
+ React.createElement('div', { className: 'dts-sub' }, t('localEnginesHint')),
115
+ props.boolField('enableLocalEngines', t('enableLocalEngines'), t('enableLocalEnginesHint')),
116
+ props.boolField('streamingEnabled', t('streamingEnabled'), t('streamingEnabledHint')),
117
+ React.createElement('div', { className: 'dts-entry' },
118
+ React.createElement('div', { className: 'dts-row' },
119
+ React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'Kokoro-82M (CPU)'),
120
+ React.createElement('span', {
121
+ className: 'dts-badge' + (kokoro.installed ? ' dts-badge-on' : ''),
122
+ }, kokoro.downloading ? `${t('downloading')} ${kokoro.progress}%` : (kokoro.installed ? t('installed') : t('notInstalled'))),
123
+ !kokoro.installed && !kokoro.downloading ? React.createElement('button', {
124
+ type: 'button', className: 'dts-save', disabled: !props.writable || loading,
125
+ onClick: () => install('kokoro'),
126
+ }, t('installKokoro')) : null,
127
+ kokoro.installed ? React.createElement('button', {
128
+ type: 'button', className: 'dts-link', disabled: !props.writable || loading,
129
+ onClick: () => remove('kokoro'),
130
+ }, t('deleteModel')) : null,
131
+ ),
132
+ kokoro.installed ? props.boolField('kokoroEnabled', t('useKokoro'), t('useKokoroHint')) : null,
133
+ ),
134
+ React.createElement('div', { className: 'dts-entry' },
135
+ React.createElement('div', { className: 'dts-row' },
136
+ React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'F5-TTS (GPU)'),
137
+ React.createElement('span', {
138
+ className: 'dts-badge' + (f5.installed ? ' dts-badge-on' : ''),
139
+ }, f5.downloading ? `${t('downloading')} ${f5.progress}%` : (f5.installed ? t('installed') : t('notInstalled'))),
140
+ !f5.installed && !f5.downloading ? React.createElement('button', {
141
+ type: 'button', className: 'dts-save', disabled: !props.writable || loading,
142
+ onClick: () => install('f5'),
143
+ }, t('installF5')) : null,
144
+ f5.installed ? React.createElement('button', {
145
+ type: 'button', className: 'dts-link', disabled: !props.writable || loading,
146
+ onClick: () => remove('f5'),
147
+ }, t('deleteModel')) : null,
148
+ ),
149
+ f5.installed ? props.boolField('f5Enabled', t('useF5'), t('useF5Hint')) : null,
150
+ ),
151
+ )
152
+ }
153
+
154
+
155
+ const CLIENT_IT_TERMS = [
156
+ { from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
157
+ { from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
158
+ { from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
159
+ { from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
160
+ { from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
161
+ { from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
162
+ { from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
163
+ { from: 'API', to: 'апи', whole: true, lang: 'ru' },
164
+ { from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
165
+ { from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
166
+ { from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
167
+ { from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
168
+ { from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
169
+ { from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
170
+ { from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
171
+ { from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
172
+ { from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
173
+ { from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
174
+ { from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
175
+ { from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
176
+ { from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
177
+ { from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
178
+ { from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
179
+ { from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
180
+ { from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
181
+ ]
182
+
183
+ // Pronunciation dictionary editor: rules apply top-down.
184
+ function PronEditor(props) {
185
+ const t = props.t || ((key) => key)
186
+ const rows = Array.isArray(props.value) ? props.value : []
187
+ const change = (i, patch) => {
188
+ props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
189
+ }
190
+ const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
191
+ const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
192
+ const loadIt = () => {
193
+ const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
194
+ const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
195
+ props.onChange(rows.concat(toAdd))
196
+ }
197
+
198
+ return React.createElement('div', { className: 'dts-wrap-in' },
199
+ rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
200
+ React.createElement('input', {
201
+ className: 'dts-input dts-grow', placeholder: t('pronFrom'),
202
+ value: rule.from || '', disabled: !props.writable,
203
+ onChange: (e) => change(i, { from: e.target.value }),
204
+ }),
205
+ React.createElement('input', {
206
+ className: 'dts-input dts-grow', placeholder: t('pronTo'),
207
+ value: rule.to || '', disabled: !props.writable,
208
+ onChange: (e) => change(i, { to: e.target.value }),
209
+ }),
210
+ React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
211
+ React.createElement('input', {
212
+ type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
213
+ onChange: (e) => change(i, { whole: e.target.checked }),
214
+ }),
215
+ ),
216
+ React.createElement('input', {
217
+ className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
218
+ value: rule.lang || '', disabled: !props.writable,
219
+ onChange: (e) => change(i, { lang: e.target.value }),
220
+ }),
221
+ React.createElement('button', {
222
+ type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
223
+ onClick: () => {
224
+ if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
225
+ },
226
+ }, '\u25b6'),
227
+ React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
228
+ )),
229
+ React.createElement('div', { className: 'dts-row' },
230
+ React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
231
+ React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
232
+ React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
233
+ ),
234
+ )
235
+ }
236
+
237
+ function RolesEditor(props) {
238
+ const t = props.t || ((key) => key)
239
+ const value = props.value || {}
240
+ const [newRole, setNewRole] = React.useState('')
241
+ const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
242
+ const removeRole = (r) => {
243
+ const next = Object.assign({}, value)
244
+ delete next[r]
245
+ props.onChange(next)
246
+ }
247
+
248
+ const standardKeys = ['reply', 'approval', 'error']
249
+ const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
250
+ const allKeys = [...standardKeys, ...customKeys]
251
+
252
+ const addCustom = () => {
253
+ const name = newRole.trim().toLowerCase()
254
+ if (!name || value[name]) return
255
+ set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
256
+ setNewRole('')
257
+ }
258
+
259
+ return React.createElement('div', { className: 'dts-block' },
260
+ React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
261
+ allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
262
+ React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
263
+ React.createElement('select', {
264
+ className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
265
+ onChange: (e) => set(r, { provider: e.target.value }),
266
+ },
267
+ React.createElement('option', { value: '' }, '—'),
268
+ PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
269
+ ),
270
+ React.createElement('input', {
271
+ className: 'dts-input dts-grow', placeholder: 'model',
272
+ value: (value[r] && value[r].model) || '', disabled: !props.writable,
273
+ onChange: (e) => set(r, { model: e.target.value }),
274
+ }),
275
+ React.createElement('input', {
276
+ className: 'dts-input dts-grow', placeholder: 'voice',
277
+ value: (value[r] && value[r].voice) || '', disabled: !props.writable,
278
+ onChange: (e) => set(r, { voice: e.target.value }),
279
+ }),
280
+ React.createElement('input', {
281
+ className: 'dts-input', style: { maxWidth: '90px' }, placeholder: t('chime'),
282
+ value: (value[r] && value[r].chime) || '', disabled: !props.writable,
283
+ onChange: (e) => set(r, { chime: e.target.value }),
284
+ }),
285
+ React.createElement('input', {
286
+ className: 'dts-input dts-grow', placeholder: 'ssml',
287
+ value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
288
+ onChange: (e) => set(r, { ssmlStyle: e.target.value }),
289
+ }),
290
+ 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'),
291
+ !standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
292
+ )),
293
+ React.createElement('div', { className: 'dts-row' },
294
+ React.createElement('input', {
295
+ className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
296
+ value: newRole, disabled: !props.writable,
297
+ onChange: (e) => setNewRole(e.target.value),
298
+ onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
299
+ }),
300
+ React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
301
+ ),
302
+ )
303
+ }
304
+
305
+ function VoiceDuplexEditor(props) {
306
+ const t = props.t || ((key) => key)
307
+ const installed = !!props.installed
308
+
309
+ return React.createElement('div', { className: 'dts-block' },
310
+ React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
311
+ React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
312
+ !installed ? React.createElement('div', {
313
+ style: {
314
+ padding: '10px 14px',
315
+ borderRadius: '8px',
316
+ border: '1px solid var(--dsw-alias-state-warning-primary)',
317
+ background: 'var(--dsw-alias-bg-layer-2)',
318
+ display: 'flex',
319
+ flexDirection: 'column',
320
+ gap: '4px',
321
+ fontSize: '13px',
322
+ },
323
+ },
324
+ React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
325
+ '⚠️ ' + t('voiceNotInstalledTitle')
326
+ ),
327
+ React.createElement('span', { className: 'dts-sub' },
328
+ t('voiceInstallHint') + ': '
329
+ ),
330
+ React.createElement('code', {
331
+ style: {
332
+ fontFamily: 'monospace',
333
+ fontSize: '12px',
334
+ padding: '2px 6px',
335
+ borderRadius: '4px',
336
+ background: 'var(--dsw-alias-bg-layer-3)',
337
+ color: 'var(--dsw-alias-label-primary)',
338
+ width: 'fit-content',
339
+ },
340
+ }, 'dsh plugin --profile web add @goodandready/dsh-voice')
341
+ ) : null,
342
+ props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
343
+ props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
344
+ )
345
+ }
346
+
347
+ function MessengerIntegrationEditor(props) {
348
+ const t = props.t || ((key) => key)
349
+ const installed = !!props.installed
350
+
351
+ return React.createElement('div', { className: 'dts-block' },
352
+ React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
353
+ React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
354
+ !installed ? React.createElement('div', {
355
+ style: {
356
+ padding: '10px 14px',
357
+ borderRadius: '8px',
358
+ border: '1px solid var(--dsw-alias-state-warning-primary)',
359
+ background: 'var(--dsw-alias-bg-layer-2)',
360
+ display: 'flex',
361
+ flexDirection: 'column',
362
+ gap: '4px',
363
+ fontSize: '13px',
364
+ },
365
+ },
366
+ React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
367
+ '⚠️ ' + t('messengerNotInstalledTitle')
368
+ ),
369
+ React.createElement('span', { className: 'dts-sub' },
370
+ t('messengerInstallHint') + ': '
371
+ ),
372
+ React.createElement('code', {
373
+ style: {
374
+ fontFamily: 'monospace',
375
+ fontSize: '12px',
376
+ padding: '2px 6px',
377
+ borderRadius: '4px',
378
+ background: 'var(--dsw-alias-bg-layer-3)',
379
+ color: 'var(--dsw-alias-label-primary)',
380
+ width: 'fit-content',
381
+ },
382
+ }, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
383
+ ) : null,
384
+ props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
385
+ )
386
+ }
387
+
388
+ // Synthesis stats panel: live host counters.
389
+ function StatsPanel(props) {
390
+ const t = props.t || ((key) => key)
391
+ const [data, setData] = React.useState(null)
392
+ const loadStats = () => {
393
+ fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
394
+ }
395
+ React.useEffect(() => { loadStats() }, [])
396
+ const reset = () => {
397
+ fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
398
+ }
399
+ return React.createElement('div', { className: 'dts-block' },
400
+ React.createElement('span', { className: 'dts-sub' }, data
401
+ ? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
402
+ : t('loading')),
403
+ data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
404
+ p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
405
+ React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
406
+ )
407
+ }
408
+
409
+ // Plugin-settings tab card follows the shared card pattern: li in the core list,
410
+ // theme-reset header, body with a divider. Chevron is ours; core does not provide it.