@goodandready/dsh-tts 0.4.1 → 0.4.5
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/lib/breaker.js +64 -0
- package/lib/cache.js +1 -1
- package/lib/client-src/10-styles.js +8 -8
- package/lib/client-src/20-player.js +59 -21
- package/lib/client-src/30-locales.js +36 -0
- package/lib/client-src/40-editors.js +31 -51
- package/lib/client-src/50-settings.js +93 -36
- package/lib/client-src/60-card-dock.js +50 -44
- package/lib/client.js +277 -160
- package/lib/index.js +27 -4
- package/lib/local.js +5 -7
- package/lib/providers/cloud.js +278 -0
- package/lib/providers/constants.js +61 -0
- package/lib/providers/local.js +99 -0
- package/lib/providers.js +11 -435
- package/lib/routes.js +17 -10
- package/lib/stream-hub.js +61 -9
- package/lib/text.js +8 -8
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -107,8 +107,8 @@ window.__ModuleLoader__.load({
|
|
|
107
107
|
'.dts-save:hover:not(:disabled){opacity:0.88}' +
|
|
108
108
|
'.dts-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
109
109
|
'.dts-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
110
|
-
'.dts-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:
|
|
111
|
-
'.dts-btn-danger:hover:not(:disabled){background:
|
|
110
|
+
'.dts-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
111
|
+
'.dts-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}' +
|
|
112
112
|
'.dts-entry{display:flex;flex-direction:column;gap:8px;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}' +
|
|
113
113
|
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
114
114
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
@@ -118,15 +118,15 @@ window.__ModuleLoader__.load({
|
|
|
118
118
|
'.dts-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);font-weight:500}' +
|
|
119
119
|
'.dts-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);font-weight:500}' +
|
|
120
120
|
'.dts-badge{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
121
|
-
'.dts-badge-on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);background:
|
|
122
|
-
'.dts-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:var(--dsw-alias-state-warning-primary);background:
|
|
123
|
-
'.dts-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:
|
|
121
|
+
'.dts-badge-on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}' +
|
|
122
|
+
'.dts-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}' +
|
|
123
|
+
'.dts-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}' +
|
|
124
124
|
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0;text-decoration:underline}' +
|
|
125
125
|
'.dts-link:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
|
|
126
126
|
'.dts-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
|
|
127
|
-
'.dts-alert-err{background:
|
|
128
|
-
'.dts-alert-warn{background:
|
|
129
|
-
'.dts-alert-ok{background:
|
|
127
|
+
'.dts-alert-err{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 20%, transparent)}' +
|
|
128
|
+
'.dts-alert-warn{background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent);color:var(--dsw-alias-state-warning-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 20%, transparent)}' +
|
|
129
|
+
'.dts-alert-ok{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 20%, transparent)}' +
|
|
130
130
|
'.dts-grow{flex:1;min-width:100px}'
|
|
131
131
|
|
|
132
132
|
const setCssId = 'dsh-tts/settings.module.css'
|
|
@@ -142,6 +142,38 @@ window.__ModuleLoader__.load({
|
|
|
142
142
|
// Each incoming chunk used to replace the previous one immediately; while reading
|
|
143
143
|
// For full replies this was invisible; with speak-as-it-goes only the last
|
|
144
144
|
// chunk would be heard. Now items queue: the next starts after the previous ends.
|
|
145
|
+
const autoplay = { blocked: false, listeners: new Set() }
|
|
146
|
+
|
|
147
|
+
function setAutoplayBlocked(next) {
|
|
148
|
+
if (autoplay.blocked === next) return
|
|
149
|
+
autoplay.blocked = next
|
|
150
|
+
for (const l of [...autoplay.listeners]) {
|
|
151
|
+
try { l() } catch { /* listener isolation */ }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unlockAudio() {
|
|
156
|
+
try {
|
|
157
|
+
const AC = window.AudioContext || window.webkitAudioContext
|
|
158
|
+
if (AC) {
|
|
159
|
+
const ctx = new AC()
|
|
160
|
+
if (ctx.state === 'suspended') ctx.resume().catch(() => { /* audio context unlock failure ignored */ })
|
|
161
|
+
setTimeout(() => { try { ctx.close() } catch { /* closed */ } }, 300)
|
|
162
|
+
}
|
|
163
|
+
} catch { /* no AudioContext */ }
|
|
164
|
+
setAutoplayBlocked(false)
|
|
165
|
+
drainQueue()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function useAutoplayGate() {
|
|
169
|
+
const [, force] = React.useReducer((n) => n + 1, 0)
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
autoplay.listeners.add(force)
|
|
172
|
+
return () => { autoplay.listeners.delete(force) }
|
|
173
|
+
}, [])
|
|
174
|
+
return autoplay
|
|
175
|
+
}
|
|
176
|
+
|
|
145
177
|
const player = {
|
|
146
178
|
audio: null,
|
|
147
179
|
after: '',
|
|
@@ -153,13 +185,15 @@ window.__ModuleLoader__.load({
|
|
|
153
185
|
busy: false,
|
|
154
186
|
paused: false,
|
|
155
187
|
listeners: new Set(),
|
|
188
|
+
unlockAudio,
|
|
189
|
+
useAutoplayGate,
|
|
190
|
+
get autoplayBlocked() { return autoplay.blocked },
|
|
156
191
|
}
|
|
157
192
|
const seenIds = new Set()
|
|
158
193
|
let currentAudioResolve = null
|
|
159
|
-
|
|
160
194
|
function playerChanged() {
|
|
161
195
|
for (const listener of [...player.listeners]) {
|
|
162
|
-
try { listener() } catch (listenerFailure) { /*
|
|
196
|
+
try { listener() } catch (listenerFailure) { /* foreign listener failure is not ours */ }
|
|
163
197
|
}
|
|
164
198
|
}
|
|
165
199
|
|
|
@@ -194,8 +228,8 @@ window.__ModuleLoader__.load({
|
|
|
194
228
|
osc.stop(at + 0.2)
|
|
195
229
|
at += 0.22
|
|
196
230
|
}
|
|
197
|
-
setTimeout(() => { try { audioCtx.close() } catch (already) { /*
|
|
198
|
-
} catch
|
|
231
|
+
setTimeout(() => { try { audioCtx.close() } catch (already) { /* already closed */ } resolve() }, 600)
|
|
232
|
+
} catch { resolve() /* no AudioContext */ }
|
|
199
233
|
})
|
|
200
234
|
}
|
|
201
235
|
|
|
@@ -210,7 +244,7 @@ window.__ModuleLoader__.load({
|
|
|
210
244
|
utterance.onend = () => resolve()
|
|
211
245
|
utterance.onerror = () => resolve()
|
|
212
246
|
window.speechSynthesis.speak(utterance)
|
|
213
|
-
} catch
|
|
247
|
+
} catch { resolve() /* no speechSynthesis */ }
|
|
214
248
|
})
|
|
215
249
|
}
|
|
216
250
|
|
|
@@ -225,7 +259,7 @@ window.__ModuleLoader__.load({
|
|
|
225
259
|
const audio = new Audio(url)
|
|
226
260
|
audio.preload = 'auto'
|
|
227
261
|
item.prepared = { audio, url }
|
|
228
|
-
} catch
|
|
262
|
+
} catch { /* decode error — item stays unprepared */ }
|
|
229
263
|
return item
|
|
230
264
|
}
|
|
231
265
|
|
|
@@ -253,7 +287,7 @@ window.__ModuleLoader__.load({
|
|
|
253
287
|
if (doneCalled) return
|
|
254
288
|
doneCalled = true
|
|
255
289
|
currentAudioResolve = null
|
|
256
|
-
try { URL.revokeObjectURL(url) } catch
|
|
290
|
+
try { URL.revokeObjectURL(url) } catch { /* URL already revoked */ }
|
|
257
291
|
item.prepared = null
|
|
258
292
|
player.audio = null
|
|
259
293
|
playerChanged()
|
|
@@ -262,11 +296,14 @@ window.__ModuleLoader__.load({
|
|
|
262
296
|
currentAudioResolve = done
|
|
263
297
|
audio.onended = done
|
|
264
298
|
audio.onerror = done
|
|
265
|
-
audio.play().catch(() => {
|
|
266
|
-
|
|
299
|
+
audio.play().catch((playErr) => {
|
|
300
|
+
// Autoplay without a user gesture is blocked — surface unlock UX, then stop this chunk.
|
|
301
|
+
if (playErr && (playErr.name === 'NotAllowedError' || playErr.name === 'SecurityError')) {
|
|
302
|
+
setAutoplayBlocked(true)
|
|
303
|
+
}
|
|
267
304
|
done()
|
|
268
305
|
})
|
|
269
|
-
} catch
|
|
306
|
+
} catch { /* cannot decode — skip chunk */
|
|
270
307
|
currentAudioResolve = null
|
|
271
308
|
resolve()
|
|
272
309
|
}
|
|
@@ -295,26 +332,27 @@ window.__ModuleLoader__.load({
|
|
|
295
332
|
function stopPlayback() {
|
|
296
333
|
for (const item of player.queue) {
|
|
297
334
|
if (item && item.prepared && item.prepared.url) {
|
|
298
|
-
try { URL.revokeObjectURL(item.prepared.url) } catch
|
|
335
|
+
try { URL.revokeObjectURL(item.prepared.url) } catch { /* already revoked */ }
|
|
299
336
|
item.prepared = null
|
|
300
337
|
}
|
|
301
338
|
}
|
|
302
339
|
player.queue.length = 0
|
|
303
340
|
player.paused = false
|
|
341
|
+
if (player.audio) {
|
|
342
|
+
try { player.audio.pause() } catch { /* already paused */ }
|
|
343
|
+
}
|
|
304
344
|
if (currentAudioResolve) {
|
|
305
345
|
currentAudioResolve()
|
|
306
|
-
} else if (player.audio) {
|
|
307
|
-
try { player.audio.pause() } catch (already) { /* стоит */ }
|
|
308
|
-
player.audio = null
|
|
309
346
|
}
|
|
310
|
-
|
|
347
|
+
player.audio = null
|
|
348
|
+
try { if (window.speechSynthesis) window.speechSynthesis.cancel() } catch { /* no speechSynthesis */ }
|
|
311
349
|
playerChanged()
|
|
312
350
|
}
|
|
313
351
|
|
|
314
352
|
function togglePause() {
|
|
315
353
|
player.paused = !player.paused
|
|
316
354
|
if (player.audio) {
|
|
317
|
-
try { player.paused ? player.audio.pause() : player.audio.play() } catch (already) { /*
|
|
355
|
+
try { player.paused ? player.audio.pause() : player.audio.play() } catch (already) { /* ignore play/pause failure */ }
|
|
318
356
|
}
|
|
319
357
|
if (!player.paused) drainQueue()
|
|
320
358
|
playerChanged()
|
|
@@ -324,7 +362,7 @@ window.__ModuleLoader__.load({
|
|
|
324
362
|
if (currentAudioResolve) {
|
|
325
363
|
currentAudioResolve()
|
|
326
364
|
} else if (player.audio) {
|
|
327
|
-
try { player.audio.pause() } catch (already) { /*
|
|
365
|
+
try { player.audio.pause() } catch (already) { /* already stopped */ }
|
|
328
366
|
player.audio = null
|
|
329
367
|
playerChanged()
|
|
330
368
|
if (player.queue.length) drainQueue()
|
|
@@ -378,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
378
416
|
player.queue.push(item)
|
|
379
417
|
}
|
|
380
418
|
if (player.queue.length) drainQueue()
|
|
381
|
-
} catch (hostRestarting) { /*
|
|
419
|
+
} catch (hostRestarting) { /* host restarting */ }
|
|
382
420
|
}
|
|
383
421
|
|
|
384
422
|
// Recent utterances: last reply texts and favorites (localStorage).
|
|
@@ -389,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
389
427
|
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(KEY) : null
|
|
390
428
|
st = JSON.parse(raw) || { items: [], favs: [] }
|
|
391
429
|
} catch (broken) { st = { items: [], favs: [] } }
|
|
392
|
-
const persist = () => { try { if (typeof localStorage !== 'undefined') localStorage.setItem(KEY, JSON.stringify(st)) } catch (full) { /*
|
|
430
|
+
const persist = () => { try { if (typeof localStorage !== 'undefined') localStorage.setItem(KEY, JSON.stringify(st)) } catch (full) { /* private mode / quota */ } }
|
|
393
431
|
return {
|
|
394
432
|
add(text) {
|
|
395
433
|
if (!text || text.length < 2) return
|
|
@@ -418,9 +456,9 @@ window.__ModuleLoader__.load({
|
|
|
418
456
|
.then((d) => {
|
|
419
457
|
if (!d || !d.ok) return
|
|
420
458
|
const audio = new Audio('data:' + (d.mime || 'audio/mpeg') + ';base64,' + d.audioBase64)
|
|
421
|
-
audio.play().catch(() => {})
|
|
459
|
+
audio.play().catch(() => { /* chime autoplay blocked or interrupted */ })
|
|
422
460
|
})
|
|
423
|
-
.catch(() => {})
|
|
461
|
+
.catch(() => { /* chime fetch failed or not found */ })
|
|
424
462
|
}
|
|
425
463
|
|
|
426
464
|
function credOf(map, provider) {
|
|
@@ -429,6 +467,24 @@ window.__ModuleLoader__.load({
|
|
|
429
467
|
|
|
430
468
|
const en = {
|
|
431
469
|
'statusOnline': 'Host online',
|
|
470
|
+
'recent': 'Recent',
|
|
471
|
+
'recentEmpty': 'No recent speech yet',
|
|
472
|
+
'retry': 'Retry',
|
|
473
|
+
'advancedCollapsed': 'Advanced',
|
|
474
|
+
'confirmClearCache': 'Clear the synthesis cache? This cannot be undone.',
|
|
475
|
+
'confirmDeleteModel': 'Delete downloaded weights for this engine?',
|
|
476
|
+
'autoplayBlocked': 'Sound is blocked until you click or tap once',
|
|
477
|
+
'enableSound': 'Enable sound',
|
|
478
|
+
'runtimeNotBundled': 'Neural inference runtime is not bundled',
|
|
479
|
+
'runtimeNotBundledHint': 'Install weights does not enable speech. Use Edge, Piper, or eSpeak for offline TTS.',
|
|
480
|
+
'testProvider': 'Test provider',
|
|
481
|
+
'breakerOpen': 'Circuit open',
|
|
482
|
+
'lastError': 'Last error',
|
|
483
|
+
'activeProvider': 'Active provider',
|
|
484
|
+
'queueLen': 'Queue',
|
|
485
|
+
'sseState': 'SSE',
|
|
486
|
+
'currentVoice': 'Voice',
|
|
487
|
+
|
|
432
488
|
'statusOffline': 'Host unreachable',
|
|
433
489
|
'cacheStats': 'Cache',
|
|
434
490
|
|
|
@@ -591,6 +647,24 @@ window.__ModuleLoader__.load({
|
|
|
591
647
|
}
|
|
592
648
|
const ru = {
|
|
593
649
|
'statusOnline': 'Хост на связи',
|
|
650
|
+
'recent': 'Недавние',
|
|
651
|
+
'recentEmpty': 'Пока нет реплик',
|
|
652
|
+
'retry': 'Повторить',
|
|
653
|
+
'advancedCollapsed': 'Дополнительно',
|
|
654
|
+
'confirmClearCache': 'Очистить кэш синтеза? Действие необратимо.',
|
|
655
|
+
'confirmDeleteModel': 'Удалить скачанные веса движка?',
|
|
656
|
+
'autoplayBlocked': 'Звук заблокирован — один раз кликните или коснитесь экрана',
|
|
657
|
+
'enableSound': 'Включить звук',
|
|
658
|
+
'runtimeNotBundled': 'Рантайм нейросинтеза не входит в пакет',
|
|
659
|
+
'runtimeNotBundledHint': 'Установка весов не включает речь. Для offline используйте Edge, Piper или eSpeak.',
|
|
660
|
+
'testProvider': 'Проверить провайдера',
|
|
661
|
+
'breakerOpen': 'Переключатель открыт',
|
|
662
|
+
'lastError': 'Последняя ошибка',
|
|
663
|
+
'activeProvider': 'Активный провайдер',
|
|
664
|
+
'queueLen': 'Очередь',
|
|
665
|
+
'sseState': 'SSE',
|
|
666
|
+
'currentVoice': 'Голос',
|
|
667
|
+
|
|
594
668
|
'statusOffline': 'Хост недоступен',
|
|
595
669
|
'cacheStats': 'Кэш',
|
|
596
670
|
|
|
@@ -793,7 +867,7 @@ window.__ModuleLoader__.load({
|
|
|
793
867
|
}),
|
|
794
868
|
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
|
|
795
869
|
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
|
|
796
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
|
|
870
|
+
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'),
|
|
797
871
|
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
798
872
|
),
|
|
799
873
|
cloud ? React.createElement('div', { className: 'dts-row' },
|
|
@@ -811,6 +885,13 @@ window.__ModuleLoader__.load({
|
|
|
811
885
|
}, t('clear')) : null,
|
|
812
886
|
) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
|
|
813
887
|
cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
|
|
888
|
+
(() => {
|
|
889
|
+
const hit = Array.isArray(props.breaker) ? props.breaker.find((b) => b && b.id === row.provider) : null
|
|
890
|
+
if (!hit || (!hit.open && !hit.lastError)) return null
|
|
891
|
+
return React.createElement('div', { className: 'dts-sub dts-bad' },
|
|
892
|
+
(hit.open ? t('breakerOpen') + ' · ' : '') + (hit.lastError || '')
|
|
893
|
+
)
|
|
894
|
+
})(),
|
|
814
895
|
)
|
|
815
896
|
}),
|
|
816
897
|
React.createElement('div', { className: 'dts-row' },
|
|
@@ -826,86 +907,59 @@ window.__ModuleLoader__.load({
|
|
|
826
907
|
function LocalEnginesEditor(props) {
|
|
827
908
|
const t = props.t || ((key) => key)
|
|
828
909
|
const [models, setModels] = React.useState({})
|
|
829
|
-
const [loading, setLoading] = React.useState(false)
|
|
830
910
|
|
|
831
911
|
const loadStatus = React.useCallback(() => {
|
|
832
912
|
if (typeof fetch === 'undefined') return
|
|
833
913
|
fetch('/dsh-tts/models/status')
|
|
834
914
|
.then((r) => r.json())
|
|
835
915
|
.then((d) => { if (d && d.ok && d.models) setModels(d.models) })
|
|
836
|
-
.catch(() => {})
|
|
916
|
+
.catch(() => {}) // status poll best-effort
|
|
837
917
|
}, [])
|
|
838
918
|
|
|
839
919
|
React.useEffect(() => {
|
|
840
920
|
loadStatus()
|
|
841
|
-
const timer = setInterval(loadStatus,
|
|
921
|
+
const timer = setInterval(loadStatus, 5000)
|
|
842
922
|
return () => clearInterval(timer)
|
|
843
923
|
}, [loadStatus])
|
|
844
924
|
|
|
845
|
-
const install = (engine) => {
|
|
846
|
-
setLoading(true)
|
|
847
|
-
fetch('/dsh-tts/models/install', {
|
|
848
|
-
method: 'POST',
|
|
849
|
-
headers: { 'content-type': 'application/json' },
|
|
850
|
-
body: JSON.stringify({ engine }),
|
|
851
|
-
}).then(() => loadStatus()).finally(() => setLoading(false))
|
|
852
|
-
}
|
|
853
|
-
|
|
854
925
|
const remove = (engine) => {
|
|
855
|
-
|
|
926
|
+
if (typeof window !== 'undefined' && window.confirm) {
|
|
927
|
+
if (!window.confirm(t('confirmDeleteModel'))) return
|
|
928
|
+
}
|
|
856
929
|
fetch('/dsh-tts/models/delete', {
|
|
857
930
|
method: 'DELETE',
|
|
858
931
|
headers: { 'content-type': 'application/json' },
|
|
859
932
|
body: JSON.stringify({ engine }),
|
|
860
|
-
}).then(() => loadStatus()).
|
|
933
|
+
}).then(() => loadStatus()).catch(() => {})
|
|
861
934
|
}
|
|
862
935
|
|
|
863
936
|
const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
|
|
864
937
|
const f5 = models.f5 || { installed: false, downloading: false, progress: 0 }
|
|
865
938
|
|
|
939
|
+
const engineRow = (label, state) => React.createElement('div', { className: 'dts-entry' },
|
|
940
|
+
React.createElement('div', { className: 'dts-row' },
|
|
941
|
+
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, label),
|
|
942
|
+
React.createElement('span', {
|
|
943
|
+
className: 'dts-badge dts-badge-warn',
|
|
944
|
+
}, t('runtimeNotBundled')),
|
|
945
|
+
state.installed ? React.createElement('button', {
|
|
946
|
+
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
947
|
+
onClick: () => remove(label.toLowerCase().includes('kokoro') ? 'kokoro' : 'f5'),
|
|
948
|
+
}, t('deleteModel')) : null,
|
|
949
|
+
),
|
|
950
|
+
React.createElement('div', { className: 'dts-sub' }, t('runtimeNotBundledHint')),
|
|
951
|
+
)
|
|
952
|
+
|
|
866
953
|
return React.createElement('div', { className: 'dts-block' },
|
|
867
954
|
React.createElement('div', { className: 'dts-h' }, t('localEnginesTitle')),
|
|
868
955
|
React.createElement('div', { className: 'dts-sub' }, t('localEnginesHint')),
|
|
869
|
-
|
|
956
|
+
React.createElement('div', { className: 'dts-alert-warn dts-sub' }, t('runtimeNotBundledHint')),
|
|
870
957
|
props.boolField('streamingEnabled', t('streamingEnabled'), t('streamingEnabledHint')),
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'Kokoro-82M (CPU)'),
|
|
874
|
-
React.createElement('span', {
|
|
875
|
-
className: 'dts-badge' + (kokoro.installed ? ' dts-badge-on' : ''),
|
|
876
|
-
}, kokoro.downloading ? `${t('downloading')} ${kokoro.progress}%` : (kokoro.installed ? t('installed') : t('notInstalled'))),
|
|
877
|
-
!kokoro.installed && !kokoro.downloading ? React.createElement('button', {
|
|
878
|
-
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
879
|
-
onClick: () => install('kokoro'),
|
|
880
|
-
}, t('installKokoro')) : null,
|
|
881
|
-
kokoro.installed ? React.createElement('button', {
|
|
882
|
-
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
883
|
-
onClick: () => remove('kokoro'),
|
|
884
|
-
}, t('deleteModel')) : null,
|
|
885
|
-
),
|
|
886
|
-
kokoro.installed ? props.boolField('kokoroEnabled', t('useKokoro'), t('useKokoroHint')) : null,
|
|
887
|
-
),
|
|
888
|
-
React.createElement('div', { className: 'dts-entry' },
|
|
889
|
-
React.createElement('div', { className: 'dts-row' },
|
|
890
|
-
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'F5-TTS (GPU)'),
|
|
891
|
-
React.createElement('span', {
|
|
892
|
-
className: 'dts-badge' + (f5.installed ? ' dts-badge-on' : ''),
|
|
893
|
-
}, f5.downloading ? `${t('downloading')} ${f5.progress}%` : (f5.installed ? t('installed') : t('notInstalled'))),
|
|
894
|
-
!f5.installed && !f5.downloading ? React.createElement('button', {
|
|
895
|
-
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
896
|
-
onClick: () => install('f5'),
|
|
897
|
-
}, t('installF5')) : null,
|
|
898
|
-
f5.installed ? React.createElement('button', {
|
|
899
|
-
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
900
|
-
onClick: () => remove('f5'),
|
|
901
|
-
}, t('deleteModel')) : null,
|
|
902
|
-
),
|
|
903
|
-
f5.installed ? props.boolField('f5Enabled', t('useF5'), t('useF5Hint')) : null,
|
|
904
|
-
),
|
|
958
|
+
engineRow('Kokoro-82M (CPU)', kokoro),
|
|
959
|
+
engineRow('F5-TTS (GPU)', f5),
|
|
905
960
|
)
|
|
906
961
|
}
|
|
907
962
|
|
|
908
|
-
|
|
909
963
|
const CLIENT_IT_TERMS = [
|
|
910
964
|
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
911
965
|
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
@@ -1171,14 +1225,19 @@ window.__ModuleLoader__.load({
|
|
|
1171
1225
|
const [saved, setSaved] = React.useState(false)
|
|
1172
1226
|
const [err, setErr] = React.useState('')
|
|
1173
1227
|
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
1174
|
-
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0 })
|
|
1228
|
+
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0, sseClients: 0, breaker: [], provider: '', queue: 0 })
|
|
1229
|
+
const [advancedOpen, setAdvancedOpen] = React.useState(() => {
|
|
1230
|
+
try { return localStorage.getItem('dsh-tts/advancedOpen') === '1' } catch { /* localStorage unavailable */ return false }
|
|
1231
|
+
})
|
|
1232
|
+
const autoplay = useAutoplayGate()
|
|
1233
|
+
|
|
1175
1234
|
const ctx = props && props.ctx
|
|
1176
1235
|
const scope = React.useMemo(() => {
|
|
1177
1236
|
const s = (ctx && ctx.get && ctx.get('lanSettings')) || (ctx && ctx.settingsScope)
|
|
1178
1237
|
if (!s || !s.bind) return null
|
|
1179
1238
|
try {
|
|
1180
1239
|
return s.bind({ namespace: NS })
|
|
1181
|
-
} catch (_) {
|
|
1240
|
+
} catch (_) { /* settings scope binding failed */
|
|
1182
1241
|
return null
|
|
1183
1242
|
}
|
|
1184
1243
|
}, [ctx])
|
|
@@ -1188,7 +1247,7 @@ window.__ModuleLoader__.load({
|
|
|
1188
1247
|
if (!scope || !scope.subscribe) return () => {}
|
|
1189
1248
|
try {
|
|
1190
1249
|
return scope.subscribe(cb) || (() => {})
|
|
1191
|
-
} catch (_) {
|
|
1250
|
+
} catch (_) { /* settings scope binding failed */
|
|
1192
1251
|
return () => {}
|
|
1193
1252
|
}
|
|
1194
1253
|
}
|
|
@@ -1198,7 +1257,7 @@ window.__ModuleLoader__.load({
|
|
|
1198
1257
|
if (!scope || !scope.getSnapshot) return SNAPSHOT_LOADING
|
|
1199
1258
|
try {
|
|
1200
1259
|
return scope.getSnapshot() || SNAPSHOT_LOADING
|
|
1201
|
-
} catch (_) {
|
|
1260
|
+
} catch (_) { /* settings scope binding failed */
|
|
1202
1261
|
return SNAPSHOT_LOADING
|
|
1203
1262
|
}
|
|
1204
1263
|
}, [scope])
|
|
@@ -1235,18 +1294,33 @@ window.__ModuleLoader__.load({
|
|
|
1235
1294
|
if (data && data.ok) setIntegrations(data)
|
|
1236
1295
|
}).catch(() => {})
|
|
1237
1296
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1297
|
+
const loadTelemetry = () => {
|
|
1298
|
+
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
1299
|
+
if (!alive || !st) return
|
|
1300
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1301
|
+
cacheHits: st.cacheHits || 0,
|
|
1302
|
+
total: st.total || 0,
|
|
1303
|
+
online: true,
|
|
1304
|
+
breaker: st.breaker || prev.breaker || [],
|
|
1305
|
+
}))
|
|
1306
|
+
}).catch(() => {
|
|
1307
|
+
if (alive) setTelemetry((prev) => Object.assign({}, prev, { online: false }))
|
|
1308
|
+
})
|
|
1309
|
+
fetch('/dsh-tts/status', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
1310
|
+
if (!alive || !st) return
|
|
1311
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1312
|
+
sseClients: st.sseClients || 0,
|
|
1313
|
+
sseOk: (st.sseClients || 0) >= 0,
|
|
1314
|
+
provider: (player.audio && player.provider) || (player.queue[0] && player.queue[0].provider) || '',
|
|
1315
|
+
queue: player.queue.length,
|
|
1316
|
+
}))
|
|
1317
|
+
}).catch(() => {})
|
|
1318
|
+
}
|
|
1319
|
+
loadTelemetry()
|
|
1320
|
+
const telTimer = setInterval(loadTelemetry, 3000)
|
|
1321
|
+
// interval cleaned by effect return below
|
|
1248
1322
|
|
|
1249
|
-
return () => { alive = false }
|
|
1323
|
+
return () => { alive = false; clearInterval(telTimer) }
|
|
1250
1324
|
}, [])
|
|
1251
1325
|
|
|
1252
1326
|
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
@@ -1263,7 +1337,7 @@ window.__ModuleLoader__.load({
|
|
|
1263
1337
|
headers: { 'Content-Type': 'application/json' },
|
|
1264
1338
|
body: JSON.stringify({ provider: provider, value: value }),
|
|
1265
1339
|
})
|
|
1266
|
-
const data = await res.json().catch(() => ({}))
|
|
1340
|
+
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
1267
1341
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
1268
1342
|
if (data && data.credentials) setCredentials(data.credentials)
|
|
1269
1343
|
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
@@ -1277,7 +1351,7 @@ window.__ModuleLoader__.load({
|
|
|
1277
1351
|
headers: { 'Content-Type': 'application/json' },
|
|
1278
1352
|
body: JSON.stringify({ provider: provider }),
|
|
1279
1353
|
})
|
|
1280
|
-
const data = await res.json().catch(() => ({}))
|
|
1354
|
+
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
1281
1355
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
1282
1356
|
if (data && data.credentials) setCredentials(data.credentials)
|
|
1283
1357
|
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
@@ -1298,7 +1372,7 @@ window.__ModuleLoader__.load({
|
|
|
1298
1372
|
headers: { 'Content-Type': 'application/json' },
|
|
1299
1373
|
body: JSON.stringify(Object.assign({}, draft, { keys: keys })),
|
|
1300
1374
|
})
|
|
1301
|
-
const data = await res.json().catch(() => ({}))
|
|
1375
|
+
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
1302
1376
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
1303
1377
|
applyPayload(data)
|
|
1304
1378
|
setKeyDrafts({})
|
|
@@ -1317,6 +1391,9 @@ window.__ModuleLoader__.load({
|
|
|
1317
1391
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
1318
1392
|
}
|
|
1319
1393
|
const onClearCache = () => {
|
|
1394
|
+
if (typeof window !== 'undefined' && window.confirm) {
|
|
1395
|
+
if (!window.confirm(t('confirmClearCache'))) return
|
|
1396
|
+
}
|
|
1320
1397
|
clearCacheNow().catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
1321
1398
|
}
|
|
1322
1399
|
|
|
@@ -1328,7 +1405,7 @@ window.__ModuleLoader__.load({
|
|
|
1328
1405
|
.then((r) => r.json())
|
|
1329
1406
|
.then((d) => {
|
|
1330
1407
|
if (!d.ok) throw new Error((d.error && d.error.message) || 'HTTP error')
|
|
1331
|
-
new Audio('data:' + d.mime + ';base64,' + d.audioBase64).play().catch(() => {})
|
|
1408
|
+
new Audio('data:' + d.mime + ';base64,' + d.audioBase64).play().catch(() => { /* autoplay blocked */ })
|
|
1332
1409
|
})
|
|
1333
1410
|
.catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
1334
1411
|
}
|
|
@@ -1363,16 +1440,31 @@ window.__ModuleLoader__.load({
|
|
|
1363
1440
|
}),
|
|
1364
1441
|
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
1365
1442
|
|
|
1366
|
-
const
|
|
1443
|
+
const autoplayBanner = autoplay.blocked
|
|
1444
|
+
? React.createElement('div', { className: 'dts-unlock' },
|
|
1445
|
+
React.createElement('span', null, t('autoplayBlocked')),
|
|
1446
|
+
React.createElement('button', {
|
|
1447
|
+
type: 'button', className: 'dts-unlock-btn',
|
|
1448
|
+
onClick: () => player.unlockAudio && player.unlockAudio(),
|
|
1449
|
+
}, t('enableSound')),
|
|
1450
|
+
)
|
|
1451
|
+
: null
|
|
1452
|
+
const breakerBadges = (telemetry.breaker || []).filter((b) => b && b.open).slice(0, 2)
|
|
1453
|
+
.map((b) => React.createElement('span', { key: b.id, className: 'dts-badge dts-badge-bad' }, t('breakerOpen') + ': ' + b.id))
|
|
1454
|
+
const telemetryBadges = React.createElement('div', { className: 'dts-row', style: { marginBottom: '8px' }, 'data-dsh-tts-telemetry': '1' },
|
|
1367
1455
|
React.createElement('span', {
|
|
1368
1456
|
className: 'dts-badge ' + (telemetry.online ? 'dts-badge-on' : 'dts-badge-bad'),
|
|
1369
|
-
}, telemetry.online ?
|
|
1370
|
-
React.createElement('span', {
|
|
1371
|
-
className: 'dts-badge dts-badge-on',
|
|
1372
|
-
}, '⚡ SSE Active'),
|
|
1457
|
+
}, telemetry.online ? t('statusOnline') : t('statusOffline')),
|
|
1373
1458
|
React.createElement('span', {
|
|
1374
|
-
className: 'dts-badge',
|
|
1375
|
-
},
|
|
1459
|
+
className: 'dts-badge ' + (telemetry.sseClients > 0 ? 'dts-badge-on' : ''),
|
|
1460
|
+
}, t('sseState') + ': ' + (telemetry.sseClients || 0)),
|
|
1461
|
+
React.createElement('span', { className: 'dts-badge' },
|
|
1462
|
+
t('queueLen') + ': ' + (telemetry.queue || player.queue.length || 0)),
|
|
1463
|
+
telemetry.provider ? React.createElement('span', { className: 'dts-badge' },
|
|
1464
|
+
t('activeProvider') + ': ' + telemetry.provider) : null,
|
|
1465
|
+
React.createElement('span', { className: 'dts-badge' },
|
|
1466
|
+
t('cacheStats') + ': ' + telemetry.cacheHits + ' / ' + telemetry.total),
|
|
1467
|
+
...breakerBadges,
|
|
1376
1468
|
)
|
|
1377
1469
|
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-header' },
|
|
1378
1470
|
React.createElement('div', { className: 'dts-page-title' }, '🔊 ' + t('title')),
|
|
@@ -1380,6 +1472,7 @@ window.__ModuleLoader__.load({
|
|
|
1380
1472
|
telemetryBadges,
|
|
1381
1473
|
)
|
|
1382
1474
|
return React.createElement('div', { className: 'dts-wrap' },
|
|
1475
|
+
autoplayBanner,
|
|
1383
1476
|
React.createElement('div', { className: 'dts-block' },
|
|
1384
1477
|
titleLine,
|
|
1385
1478
|
React.createElement('label', { className: 'dts-row' },
|
|
@@ -1433,6 +1526,7 @@ window.__ModuleLoader__.load({
|
|
|
1433
1526
|
React.createElement('div', { className: 'dts-sub' },
|
|
1434
1527
|
t('chainHintCard')),
|
|
1435
1528
|
React.createElement(ChainEditor, {
|
|
1529
|
+
breaker: telemetry.breaker,
|
|
1436
1530
|
t: t,
|
|
1437
1531
|
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
1438
1532
|
credentials: credentials, keyDrafts: keyDrafts,
|
|
@@ -1482,16 +1576,33 @@ window.__ModuleLoader__.load({
|
|
|
1482
1576
|
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
1483
1577
|
),
|
|
1484
1578
|
React.createElement('div', { className: 'dts-block' },
|
|
1485
|
-
React.createElement('
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1579
|
+
React.createElement('button', {
|
|
1580
|
+
type: 'button',
|
|
1581
|
+
className: 'dts-head',
|
|
1582
|
+
'aria-expanded': advancedOpen,
|
|
1583
|
+
onClick: () => {
|
|
1584
|
+
setAdvancedOpen((v) => {
|
|
1585
|
+
const next = !v
|
|
1586
|
+
try { localStorage.setItem('dsh-tts/advancedOpen', next ? '1' : '0') } catch { /* private mode */ }
|
|
1587
|
+
return next
|
|
1588
|
+
})
|
|
1589
|
+
},
|
|
1590
|
+
},
|
|
1591
|
+
React.createElement('div', { className: 'dts-headText' },
|
|
1592
|
+
React.createElement('div', { className: 'dts-title' }, t('advancedTitle')),
|
|
1593
|
+
React.createElement('div', { className: 'dts-sub' }, t('advancedHint')),
|
|
1594
|
+
),
|
|
1595
|
+
),
|
|
1596
|
+
advancedOpen ? React.createElement('div', { className: 'dts-body' },
|
|
1597
|
+
numberField('maxChars', t('maxChars'), t('maxCharsHint'), 100),
|
|
1598
|
+
numberField('sentenceChars', t('sentenceChars'), t('sentenceCharsHint'), 20),
|
|
1599
|
+
numberField('timeoutMs', t('timeoutMs'), t('timeoutMsHint'), 1000),
|
|
1600
|
+
numberField('maxQueue', t('maxQueue'), t('maxQueueHint'), 1),
|
|
1601
|
+
textField('openaiBaseUrl', t('openaiBaseUrl'), t('openaiBaseUrlHint')),
|
|
1602
|
+
textField('mimoBaseUrl', t('mimoBaseUrl'), t('mimoBaseUrlHint')),
|
|
1603
|
+
textField('mimoFormat', t('mimoFormat'), t('mimoFormatHint')),
|
|
1604
|
+
textField('minimaxBin', t('minimaxBin'), t('minimaxBinHint')),
|
|
1605
|
+
) : null,
|
|
1495
1606
|
),
|
|
1496
1607
|
React.createElement('div', { className: 'dts-foot' },
|
|
1497
1608
|
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
@@ -1552,7 +1663,7 @@ window.__ModuleLoader__.load({
|
|
|
1552
1663
|
React.createElement('button', {
|
|
1553
1664
|
type: 'button', className: 'dts-link', style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
1554
1665
|
title: text,
|
|
1555
|
-
onClick: () => playStandalone(text),
|
|
1666
|
+
onClick: () => playStandalone(text), title: text,
|
|
1556
1667
|
}, text.slice(0, 80)),
|
|
1557
1668
|
React.createElement('button', {
|
|
1558
1669
|
type: 'button', className: 'dts-link', title: '★',
|
|
@@ -1561,7 +1672,7 @@ window.__ModuleLoader__.load({
|
|
|
1561
1672
|
)
|
|
1562
1673
|
return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
|
|
1563
1674
|
React.createElement('button', {
|
|
1564
|
-
type: 'button', className: 'dts-link', title: '
|
|
1675
|
+
type: 'button', className: 'dts-link', title: t('recent') || 'Recent',
|
|
1565
1676
|
onClick: () => setShowList((v) => !v),
|
|
1566
1677
|
}, '☰'),
|
|
1567
1678
|
showList ? React.createElement('div', {
|
|
@@ -1572,7 +1683,7 @@ window.__ModuleLoader__.load({
|
|
|
1572
1683
|
lists.favs.map((text) => row(text, true)),
|
|
1573
1684
|
React.createElement('div', { className: 'dts-h' }, '⟳'),
|
|
1574
1685
|
lists.items.filter((x) => !lists.favs.includes(x)).map((text) => row(text, false)),
|
|
1575
|
-
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, '
|
|
1686
|
+
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, t('recentEmpty') || 'No recent speech yet') : null,
|
|
1576
1687
|
) : null,
|
|
1577
1688
|
React.createElement('button', {
|
|
1578
1689
|
type: 'button',
|
|
@@ -1590,11 +1701,10 @@ window.__ModuleLoader__.load({
|
|
|
1590
1701
|
}
|
|
1591
1702
|
|
|
1592
1703
|
/**
|
|
1593
|
-
*
|
|
1704
|
+
* Fallback translator for the dock.
|
|
1594
1705
|
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
* принципе, и привязка — единственный способ дать ей язык.
|
|
1706
|
+
* Bound at registration for the page lifetime. The core draws the slot label;
|
|
1707
|
+
* props.t never reaches it, so the binder is the only way to give it a language.
|
|
1598
1708
|
*/
|
|
1599
1709
|
let fallbackDockText = (key) => key
|
|
1600
1710
|
|
|
@@ -1629,7 +1739,7 @@ window.__ModuleLoader__.load({
|
|
|
1629
1739
|
ctx.effect(() => {
|
|
1630
1740
|
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1631
1741
|
return () => { for (const off of undo) off() }
|
|
1632
|
-
}, 'dsh-tts:
|
|
1742
|
+
}, 'dsh-tts: locales')
|
|
1633
1743
|
// The sidebar draws the section label, not our component: props.t never
|
|
1634
1744
|
// reaches it, so bind the translator to the namespace ourselves.
|
|
1635
1745
|
const t = ctx.locale.bind(NS)
|
|
@@ -1673,7 +1783,7 @@ window.__ModuleLoader__.load({
|
|
|
1673
1783
|
exports.apply = function apply(ctx) {
|
|
1674
1784
|
registerSettings(ctx)
|
|
1675
1785
|
registerSpeaker(ctx)
|
|
1676
|
-
ctx.effect(() => listenForVoice(), 'dsh-tts:
|
|
1786
|
+
ctx.effect(() => listenForVoice(), 'dsh-tts: barge-in on user speech')
|
|
1677
1787
|
ctx.effect(() => {
|
|
1678
1788
|
if (typeof window === 'undefined') return () => {}
|
|
1679
1789
|
const onKey = (e) => {
|
|
@@ -1683,47 +1793,54 @@ window.__ModuleLoader__.load({
|
|
|
1683
1793
|
}
|
|
1684
1794
|
window.addEventListener('keydown', onKey)
|
|
1685
1795
|
return () => window.removeEventListener('keydown', onKey)
|
|
1686
|
-
}, 'dsh-tts:
|
|
1796
|
+
}, 'dsh-tts: hotkeys')
|
|
1687
1797
|
ctx.effect(() => {
|
|
1688
1798
|
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
1689
1799
|
let es = null
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1800
|
+
let closed = false
|
|
1801
|
+
let attempt = 0
|
|
1802
|
+
let retryTimer = null
|
|
1803
|
+
const onItem = (e) => {
|
|
1804
|
+
try {
|
|
1805
|
+
const item = JSON.parse(e.data)
|
|
1806
|
+
if (!item || !item.id) return
|
|
1807
|
+
if (seenIds.has(item.id)) return
|
|
1808
|
+
seenIds.add(item.id)
|
|
1809
|
+
if (seenIds.size > 500) {
|
|
1810
|
+
const first = seenIds.values().next().value
|
|
1811
|
+
seenIds.delete(first)
|
|
1812
|
+
}
|
|
1813
|
+
player.queue.push(item)
|
|
1814
|
+
drainQueue()
|
|
1815
|
+
} catch { /* ignore malformed SSE frame */ }
|
|
1816
|
+
}
|
|
1817
|
+
const connect = () => {
|
|
1818
|
+
if (closed) return
|
|
1819
|
+
try {
|
|
1820
|
+
es = new EventSource('/dsh-tts/stream')
|
|
1821
|
+
es.addEventListener('open', () => { attempt = 0 })
|
|
1822
|
+
es.addEventListener('utterance', onItem)
|
|
1823
|
+
es.addEventListener('chime', onItem)
|
|
1824
|
+
es.onerror = () => {
|
|
1825
|
+
try { es.close() } catch { /* already closed */ }
|
|
1826
|
+
if (closed) return
|
|
1827
|
+
attempt += 1
|
|
1828
|
+
const delay = Math.min(15000, 500 * Math.pow(2, Math.min(attempt, 5)))
|
|
1829
|
+
retryTimer = setTimeout(connect, delay)
|
|
1830
|
+
}
|
|
1831
|
+
} catch { /* EventSource constructor failed — poll remains */ }
|
|
1832
|
+
}
|
|
1833
|
+
connect()
|
|
1834
|
+
return () => {
|
|
1835
|
+
closed = true
|
|
1836
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
1837
|
+
if (es) { try { es.close() } catch { /* already closed */ } }
|
|
1838
|
+
}
|
|
1839
|
+
}, 'dsh-tts: SSE audio stream')
|
|
1723
1840
|
ctx.effect(() => {
|
|
1724
1841
|
const timer = setInterval(pollPending, 1000)
|
|
1725
1842
|
return () => clearInterval(timer)
|
|
1726
|
-
}, 'dsh-tts:
|
|
1843
|
+
}, 'dsh-tts: poll pending audio')
|
|
1727
1844
|
}
|
|
1728
1845
|
return module.exports
|
|
1729
1846
|
},
|