@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.
@@ -0,0 +1,390 @@
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
+ 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' },
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
+ { from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
137
+ { from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
138
+ { from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
139
+ { from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
140
+ { from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
141
+ { from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
142
+ { from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
143
+ { from: 'API', to: 'апи', whole: true, lang: 'ru' },
144
+ { from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
145
+ { from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
146
+ { from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
147
+ { from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
148
+ { from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
149
+ { from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
150
+ { from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
151
+ { from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
152
+ { from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
153
+ { from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
154
+ { from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
155
+ { from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
156
+ { from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
157
+ { from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
158
+ { from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
159
+ { from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
160
+ { from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
161
+ ]
162
+
163
+ // Pronunciation dictionary editor: rules apply top-down.
164
+ function PronEditor(props) {
165
+ const t = props.t || ((key) => key)
166
+ const rows = Array.isArray(props.value) ? props.value : []
167
+ const change = (i, patch) => {
168
+ props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
169
+ }
170
+ const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
171
+ const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
172
+ const loadIt = () => {
173
+ const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
174
+ const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
175
+ props.onChange(rows.concat(toAdd))
176
+ }
177
+
178
+ return React.createElement('div', { className: 'dts-wrap-in' },
179
+ rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
180
+ React.createElement('input', {
181
+ className: 'dts-input dts-grow', placeholder: t('pronFrom'),
182
+ value: rule.from || '', disabled: !props.writable,
183
+ onChange: (e) => change(i, { from: e.target.value }),
184
+ }),
185
+ React.createElement('input', {
186
+ className: 'dts-input dts-grow', placeholder: t('pronTo'),
187
+ value: rule.to || '', disabled: !props.writable,
188
+ onChange: (e) => change(i, { to: e.target.value }),
189
+ }),
190
+ React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
191
+ React.createElement('input', {
192
+ type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
193
+ onChange: (e) => change(i, { whole: e.target.checked }),
194
+ }),
195
+ ),
196
+ React.createElement('input', {
197
+ className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
198
+ value: rule.lang || '', disabled: !props.writable,
199
+ onChange: (e) => change(i, { lang: e.target.value }),
200
+ }),
201
+ React.createElement('button', {
202
+ type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
203
+ onClick: () => {
204
+ if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
205
+ },
206
+ }, '\u25b6'),
207
+ React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
208
+ )),
209
+ React.createElement('div', { className: 'dts-row' },
210
+ React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
211
+ React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
212
+ React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
213
+ ),
214
+ )
215
+ }
216
+
217
+ function RolesEditor(props) {
218
+ const t = props.t || ((key) => key)
219
+ const value = props.value || {}
220
+ const [newRole, setNewRole] = React.useState('')
221
+ const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
222
+ const removeRole = (r) => {
223
+ const next = Object.assign({}, value)
224
+ delete next[r]
225
+ props.onChange(next)
226
+ }
227
+
228
+ const standardKeys = ['reply', 'approval', 'error']
229
+ const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
230
+ const allKeys = [...standardKeys, ...customKeys]
231
+
232
+ const addCustom = () => {
233
+ const name = newRole.trim().toLowerCase()
234
+ if (!name || value[name]) return
235
+ set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
236
+ setNewRole('')
237
+ }
238
+
239
+ return React.createElement('div', { className: 'dts-block' },
240
+ React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
241
+ allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
242
+ React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
243
+ React.createElement('select', {
244
+ className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
245
+ onChange: (e) => set(r, { provider: e.target.value }),
246
+ },
247
+ React.createElement('option', { value: '' }, '—'),
248
+ PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
249
+ ),
250
+ React.createElement('input', {
251
+ className: 'dts-input dts-grow', placeholder: 'model',
252
+ value: (value[r] && value[r].model) || '', disabled: !props.writable,
253
+ onChange: (e) => set(r, { model: e.target.value }),
254
+ }),
255
+ React.createElement('input', {
256
+ className: 'dts-input dts-grow', placeholder: 'voice',
257
+ value: (value[r] && value[r].voice) || '', disabled: !props.writable,
258
+ onChange: (e) => set(r, { voice: e.target.value }),
259
+ }),
260
+ React.createElement('input', {
261
+ className: 'dts-input', style: { maxWidth: '90px' }, placeholder: t('chime'),
262
+ value: (value[r] && value[r].chime) || '', disabled: !props.writable,
263
+ onChange: (e) => set(r, { chime: e.target.value }),
264
+ }),
265
+ React.createElement('input', {
266
+ className: 'dts-input dts-grow', placeholder: 'ssml',
267
+ value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
268
+ onChange: (e) => set(r, { ssmlStyle: e.target.value }),
269
+ }),
270
+ 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'),
271
+ !standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
272
+ )),
273
+ React.createElement('div', { className: 'dts-row' },
274
+ React.createElement('input', {
275
+ className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
276
+ value: newRole, disabled: !props.writable,
277
+ onChange: (e) => setNewRole(e.target.value),
278
+ onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
279
+ }),
280
+ React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
281
+ ),
282
+ )
283
+ }
284
+
285
+ function VoiceDuplexEditor(props) {
286
+ const t = props.t || ((key) => key)
287
+ const installed = !!props.installed
288
+
289
+ return React.createElement('div', { className: 'dts-block' },
290
+ React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
291
+ React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
292
+ !installed ? React.createElement('div', {
293
+ style: {
294
+ padding: '10px 14px',
295
+ borderRadius: '8px',
296
+ border: '1px solid var(--dsw-alias-state-warning-primary)',
297
+ background: 'var(--dsw-alias-bg-layer-2)',
298
+ display: 'flex',
299
+ flexDirection: 'column',
300
+ gap: '4px',
301
+ fontSize: '13px',
302
+ },
303
+ },
304
+ React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
305
+ '⚠️ ' + t('voiceNotInstalledTitle')
306
+ ),
307
+ React.createElement('span', { className: 'dts-sub' },
308
+ t('voiceInstallHint') + ': '
309
+ ),
310
+ React.createElement('code', {
311
+ style: {
312
+ fontFamily: 'monospace',
313
+ fontSize: '12px',
314
+ padding: '2px 6px',
315
+ borderRadius: '4px',
316
+ background: 'var(--dsw-alias-bg-layer-3)',
317
+ color: 'var(--dsw-alias-label-primary)',
318
+ width: 'fit-content',
319
+ },
320
+ }, 'dsh plugin --profile web add @goodandready/dsh-voice')
321
+ ) : null,
322
+ props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
323
+ props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
324
+ )
325
+ }
326
+
327
+ function MessengerIntegrationEditor(props) {
328
+ const t = props.t || ((key) => key)
329
+ const installed = !!props.installed
330
+
331
+ return React.createElement('div', { className: 'dts-block' },
332
+ React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
333
+ React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
334
+ !installed ? React.createElement('div', {
335
+ style: {
336
+ padding: '10px 14px',
337
+ borderRadius: '8px',
338
+ border: '1px solid var(--dsw-alias-state-warning-primary)',
339
+ background: 'var(--dsw-alias-bg-layer-2)',
340
+ display: 'flex',
341
+ flexDirection: 'column',
342
+ gap: '4px',
343
+ fontSize: '13px',
344
+ },
345
+ },
346
+ React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
347
+ '⚠️ ' + t('messengerNotInstalledTitle')
348
+ ),
349
+ React.createElement('span', { className: 'dts-sub' },
350
+ t('messengerInstallHint') + ': '
351
+ ),
352
+ React.createElement('code', {
353
+ style: {
354
+ fontFamily: 'monospace',
355
+ fontSize: '12px',
356
+ padding: '2px 6px',
357
+ borderRadius: '4px',
358
+ background: 'var(--dsw-alias-bg-layer-3)',
359
+ color: 'var(--dsw-alias-label-primary)',
360
+ width: 'fit-content',
361
+ },
362
+ }, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
363
+ ) : null,
364
+ props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
365
+ )
366
+ }
367
+
368
+ // Synthesis stats panel: live host counters.
369
+ function StatsPanel(props) {
370
+ const t = props.t || ((key) => key)
371
+ const [data, setData] = React.useState(null)
372
+ const loadStats = () => {
373
+ fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
374
+ }
375
+ React.useEffect(() => { loadStats() }, [])
376
+ const reset = () => {
377
+ fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
378
+ }
379
+ return React.createElement('div', { className: 'dts-block' },
380
+ React.createElement('span', { className: 'dts-sub' }, data
381
+ ? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
382
+ : t('loading')),
383
+ data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
384
+ p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
385
+ React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
386
+ )
387
+ }
388
+
389
+ // Plugin-settings tab card follows the shared card pattern: li in the core list,
390
+ // theme-reset header, body with a divider. Chevron is ours; core does not provide it.