@goodandready/dsh-tts 0.4.28 → 0.4.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +9 -8
- package/README.ru.md +11 -10
- package/README.zh.md +5 -5
- package/lib/client.js +151 -46
- package/lib/engines/manager.js +37 -9
- package/lib/index.js +94 -36
- package/lib/providers/cloud.js +9 -1
- package/lib/providers.js +1 -1
- package/lib/routes.js +12 -11
- package/lib/stream-hub.js +19 -16
- package/lib/text.js +7 -1
- package/lib/updater.js +4 -5
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -51,6 +51,31 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
function clientFetch(url, options) {
|
|
55
|
+
const opts = options || {}
|
|
56
|
+
const timeoutMs = Number(opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 15000
|
|
57
|
+
const controller = new AbortController()
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
59
|
+
|
|
60
|
+
if (opts.signal) {
|
|
61
|
+
if (opts.signal.aborted) controller.abort()
|
|
62
|
+
else opts.signal.addEventListener('abort', () => controller.abort(), { once: true })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fetchOpts = Object.assign({}, opts, { signal: controller.signal })
|
|
66
|
+
delete fetchOpts.timeoutMs
|
|
67
|
+
|
|
68
|
+
return fetch(url, fetchOpts).finally(() => clearTimeout(timer))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getModelHint(provider, t) {
|
|
72
|
+
if (typeof t === 'function') {
|
|
73
|
+
const loc = t('modelHint_' + provider)
|
|
74
|
+
if (loc && !loc.startsWith('modelHint_')) return loc
|
|
75
|
+
}
|
|
76
|
+
return MODEL_HINT[provider] || ''
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
function createErrorBoundary() {
|
|
55
80
|
if (!React || typeof React.Component !== 'function') {
|
|
56
81
|
return function NoopBoundary(props) { return (props && props.children) || null }
|
|
@@ -74,7 +99,7 @@ window.__ModuleLoader__.load({
|
|
|
74
99
|
className: 'dts-alert dts-alert-err',
|
|
75
100
|
style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
|
|
76
101
|
},
|
|
77
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ TTS UI Error:'),
|
|
102
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, (typeof fallbackDockText === 'function' ? fallbackDockText('ttsUiError') : '⚠️ TTS UI Error:')),
|
|
78
103
|
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String((this.state.error && this.state.error.message) || this.state.error)),
|
|
79
104
|
React.createElement(
|
|
80
105
|
'button',
|
|
@@ -114,12 +139,12 @@ window.__ModuleLoader__.load({
|
|
|
114
139
|
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0}' +
|
|
115
140
|
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
116
141
|
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
117
|
-
'.dts-input:focus{outline:none;border-color:var(--dsw-alias-
|
|
142
|
+
'.dts-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary, #6366f1)}' +
|
|
118
143
|
'.dts-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:14px 0 4px}' +
|
|
119
144
|
'.dts-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:6px 16px;font-size:13px;font-weight:500;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);transition:all .15s ease}' +
|
|
120
145
|
'.dts-save:hover:not(:disabled){opacity:0.88}' +
|
|
121
146
|
'.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}' +
|
|
122
|
-
'.dts-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-
|
|
147
|
+
'.dts-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
123
148
|
'.dts-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
124
149
|
'.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)}' +
|
|
125
150
|
'.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)}' +
|
|
@@ -127,18 +152,18 @@ window.__ModuleLoader__.load({
|
|
|
127
152
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
128
153
|
'.dts-row .dts-key{flex:1;min-width:180px}' +
|
|
129
154
|
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:13px;transition:all .15s ease}' +
|
|
130
|
-
'.dts-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-
|
|
155
|
+
'.dts-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2))}' +
|
|
131
156
|
'.dts-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);font-weight:500}' +
|
|
132
157
|
'.dts-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);font-weight:500}' +
|
|
133
158
|
'.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}' +
|
|
134
159
|
'.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)}' +
|
|
135
|
-
'.dts-badge-warn{color:var(--dsw-alias-state-
|
|
160
|
+
'.dts-badge-warn{color:var(--dsw-alias-state-warn-primary, #f59e0b);border-color:var(--dsw-alias-state-warn-primary, #f59e0b);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 8%, transparent)}' +
|
|
136
161
|
'.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)}' +
|
|
137
162
|
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0;text-decoration:underline}' +
|
|
138
163
|
'.dts-link:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
|
|
139
164
|
'.dts-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
|
|
140
165
|
'.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)}' +
|
|
141
|
-
'.dts-alert-warn{background:color-mix(in srgb, var(--dsw-alias-state-
|
|
166
|
+
'.dts-alert-warn{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 10%, transparent);color:var(--dsw-alias-state-warn-primary, #f59e0b);border:1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 20%, transparent)}' +
|
|
142
167
|
'.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)}' +
|
|
143
168
|
'.dts-grow{flex:1;min-width:100px}'
|
|
144
169
|
|
|
@@ -165,7 +190,16 @@ window.__ModuleLoader__.load({
|
|
|
165
190
|
}
|
|
166
191
|
}
|
|
167
192
|
|
|
193
|
+
let clearGestureListeners = null
|
|
194
|
+
function removeGestureListeners() {
|
|
195
|
+
if (typeof clearGestureListeners === 'function') {
|
|
196
|
+
clearGestureListeners()
|
|
197
|
+
clearGestureListeners = null
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
function unlockAudio() {
|
|
202
|
+
removeGestureListeners()
|
|
169
203
|
try {
|
|
170
204
|
const AC = window.AudioContext || window.webkitAudioContext
|
|
171
205
|
if (AC) {
|
|
@@ -330,16 +364,21 @@ window.__ModuleLoader__.load({
|
|
|
330
364
|
player.busy = false
|
|
331
365
|
playerChanged()
|
|
332
366
|
if (typeof window !== 'undefined') {
|
|
367
|
+
removeGestureListeners()
|
|
333
368
|
const onUserGesture = () => {
|
|
334
|
-
|
|
335
|
-
window.removeEventListener('keydown', onUserGesture)
|
|
336
|
-
window.removeEventListener('touchstart', onUserGesture)
|
|
369
|
+
removeGestureListeners()
|
|
337
370
|
unlockAudio()
|
|
338
371
|
}
|
|
339
372
|
window.addEventListener('click', onUserGesture, { once: true })
|
|
340
373
|
window.addEventListener('keydown', onUserGesture, { once: true })
|
|
341
374
|
window.addEventListener('touchstart', onUserGesture, { once: true })
|
|
375
|
+
clearGestureListeners = () => {
|
|
376
|
+
window.removeEventListener('click', onUserGesture)
|
|
377
|
+
window.removeEventListener('keydown', onUserGesture)
|
|
378
|
+
window.removeEventListener('touchstart', onUserGesture)
|
|
379
|
+
}
|
|
342
380
|
}
|
|
381
|
+
resolve()
|
|
343
382
|
return
|
|
344
383
|
}
|
|
345
384
|
done()
|
|
@@ -370,6 +409,9 @@ window.__ModuleLoader__.load({
|
|
|
370
409
|
}
|
|
371
410
|
|
|
372
411
|
function stopPlayback() {
|
|
412
|
+
removeGestureListeners()
|
|
413
|
+
player.blocked = false
|
|
414
|
+
setAutoplayBlocked(false)
|
|
373
415
|
for (const item of player.queue) {
|
|
374
416
|
if (item && item.prepared && item.prepared.url) {
|
|
375
417
|
try { URL.revokeObjectURL(item.prepared.url) } catch { /* already revoked */ }
|
|
@@ -402,16 +444,22 @@ window.__ModuleLoader__.load({
|
|
|
402
444
|
}
|
|
403
445
|
|
|
404
446
|
function skipCurrent() {
|
|
447
|
+
try {
|
|
448
|
+
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
|
449
|
+
window.speechSynthesis.cancel()
|
|
450
|
+
}
|
|
451
|
+
} catch (_err) { /* no speechSynthesis */ }
|
|
405
452
|
if (player.audio) {
|
|
406
453
|
try {
|
|
407
454
|
player.audio.pause()
|
|
408
455
|
player.audio.currentTime = 0
|
|
409
|
-
} catch { /* ignore pause error */ }
|
|
456
|
+
} catch (_err) { /* ignore pause error */ }
|
|
410
457
|
}
|
|
411
458
|
if (currentAudioResolve) {
|
|
412
459
|
currentAudioResolve()
|
|
413
460
|
} else {
|
|
414
461
|
player.audio = null
|
|
462
|
+
player.busy = false
|
|
415
463
|
playerChanged()
|
|
416
464
|
if (player.queue.length) drainQueue()
|
|
417
465
|
}
|
|
@@ -469,13 +517,13 @@ window.__ModuleLoader__.load({
|
|
|
469
517
|
if (player.enabled === false) return
|
|
470
518
|
player.queue.push(item)
|
|
471
519
|
drainQueue()
|
|
472
|
-
} catch {}
|
|
520
|
+
} catch (_err) { /* ignore malformed chime payload */ }
|
|
473
521
|
}
|
|
474
522
|
|
|
475
523
|
const connect = () => {
|
|
476
524
|
if (sseDisposed) return
|
|
477
525
|
if (activeSse) {
|
|
478
|
-
try { activeSse.close() } catch {}
|
|
526
|
+
try { activeSse.close() } catch (_err) { /* active sse already closed */ }
|
|
479
527
|
activeSse = null
|
|
480
528
|
}
|
|
481
529
|
try {
|
|
@@ -486,7 +534,7 @@ window.__ModuleLoader__.load({
|
|
|
486
534
|
es.addEventListener('chime', onChime)
|
|
487
535
|
es.onerror = () => {
|
|
488
536
|
if (activeSse === es) {
|
|
489
|
-
try { es.close() } catch {}
|
|
537
|
+
try { es.close() } catch (_err) { /* eventsource already closed */ }
|
|
490
538
|
activeSse = null
|
|
491
539
|
}
|
|
492
540
|
if (sseDisposed) return
|
|
@@ -504,7 +552,7 @@ window.__ModuleLoader__.load({
|
|
|
504
552
|
sseDisposed = true
|
|
505
553
|
if (activeSseTimer) clearTimeout(activeSseTimer)
|
|
506
554
|
if (activeSse) {
|
|
507
|
-
try { activeSse.close() } catch {}
|
|
555
|
+
try { activeSse.close() } catch (_err) { /* active sse already closed on dispose */ }
|
|
508
556
|
activeSse = null
|
|
509
557
|
}
|
|
510
558
|
}
|
|
@@ -512,7 +560,7 @@ window.__ModuleLoader__.load({
|
|
|
512
560
|
|
|
513
561
|
async function pollPending() {
|
|
514
562
|
try {
|
|
515
|
-
const st = await
|
|
563
|
+
const st = await clientFetch('/dsh-tts/status', { cache: 'no-store', timeoutMs: 8000 })
|
|
516
564
|
if (!st.ok) return
|
|
517
565
|
const meta = await st.json()
|
|
518
566
|
player.enabled = !!(meta && meta.speakReplies)
|
|
@@ -521,7 +569,7 @@ window.__ModuleLoader__.load({
|
|
|
521
569
|
if (meta && meta.roles) player.roles = meta.roles
|
|
522
570
|
if (meta && typeof meta.bargeIn === 'boolean') player.bargeIn = meta.bargeIn
|
|
523
571
|
if (!player.enabled) return
|
|
524
|
-
const res = await
|
|
572
|
+
const res = await clientFetch('/dsh-tts/pending?after=' + encodeURIComponent(player.after || ''), { cache: 'no-store', timeoutMs: 8000 })
|
|
525
573
|
if (!res.ok) return
|
|
526
574
|
const data = await res.json()
|
|
527
575
|
const items = data && Array.isArray(data.items) ? data.items : []
|
|
@@ -577,10 +625,11 @@ window.__ModuleLoader__.load({
|
|
|
577
625
|
function playStandalone(text) {
|
|
578
626
|
if (!text) return
|
|
579
627
|
stopPlayback()
|
|
580
|
-
|
|
628
|
+
clientFetch('/dsh-tts/speak', {
|
|
581
629
|
method: 'POST',
|
|
582
630
|
headers: { 'Content-Type': 'application/json' },
|
|
583
631
|
body: JSON.stringify({ text }),
|
|
632
|
+
timeoutMs: 15000,
|
|
584
633
|
})
|
|
585
634
|
.then((r) => r.json())
|
|
586
635
|
.then((d) => {
|
|
@@ -592,15 +641,18 @@ window.__ModuleLoader__.load({
|
|
|
592
641
|
audioBase64: d.audioBase64,
|
|
593
642
|
})
|
|
594
643
|
})
|
|
595
|
-
.catch(() => {
|
|
644
|
+
.catch((err) => {
|
|
645
|
+
console.warn('[dsh-tts] playStandalone fetch failed:', err)
|
|
646
|
+
})
|
|
596
647
|
}
|
|
597
648
|
|
|
598
649
|
function previewSpeech(provider, model, voice, text) {
|
|
599
650
|
stopPlayback()
|
|
600
|
-
return
|
|
651
|
+
return clientFetch('/dsh-tts/preview', {
|
|
601
652
|
method: 'POST',
|
|
602
653
|
headers: { 'Content-Type': 'application/json' },
|
|
603
654
|
body: JSON.stringify({ provider, model, voice, text }),
|
|
655
|
+
timeoutMs: 15000,
|
|
604
656
|
})
|
|
605
657
|
.then((r) => r.json())
|
|
606
658
|
.then((d) => {
|
|
@@ -626,10 +678,11 @@ window.__ModuleLoader__.load({
|
|
|
626
678
|
if (list && list.items && list.items.length) phrase = list.items[0]
|
|
627
679
|
}
|
|
628
680
|
if (!phrase) return
|
|
629
|
-
|
|
681
|
+
clientFetch('/dsh-tts/speak', {
|
|
630
682
|
method: 'POST',
|
|
631
683
|
headers: { 'Content-Type': 'application/json' },
|
|
632
684
|
body: JSON.stringify({ text: phrase }),
|
|
685
|
+
timeoutMs: 15000,
|
|
633
686
|
})
|
|
634
687
|
.then((r) => r.json())
|
|
635
688
|
.then((d) => {
|
|
@@ -655,6 +708,25 @@ window.__ModuleLoader__.load({
|
|
|
655
708
|
return (map && map[provider]) || { configured: false, writable: true, ref: '' }
|
|
656
709
|
}
|
|
657
710
|
const en = {
|
|
711
|
+
'ttsUiError': '⚠️ TTS UI Error:',
|
|
712
|
+
'modelHint_openai': 'provider default',
|
|
713
|
+
'modelHint_elevenlabs': 'provider default',
|
|
714
|
+
'modelHint_google': 'provider default',
|
|
715
|
+
'modelHint_azure': 'region voice name',
|
|
716
|
+
'modelHint_groq': 'provider default',
|
|
717
|
+
'modelHint_deepgram': 'provider default (English-only voices)',
|
|
718
|
+
'modelHint_openrouter': 'provider/model id',
|
|
719
|
+
'modelHint_siliconflow': 'model id (e.g. FunAudioLLM/CosyVoice2-0.5B)',
|
|
720
|
+
'modelHint_deepinfra': 'model id (e.g. hexgrad/Kokoro-82M)',
|
|
721
|
+
'modelHint_fireworks': 'model id (e.g. kokoro)',
|
|
722
|
+
'modelHint_mimo': 'model id (e.g. mimo-v2.5-tts)',
|
|
723
|
+
'modelHint_custom': 'custom model',
|
|
724
|
+
'modelHint_minimax': 'model id',
|
|
725
|
+
'modelHint_edge': 'BCP-47 voice, e.g. en-US-AriaNeural',
|
|
726
|
+
'modelHint_piper': 'path to .onnx',
|
|
727
|
+
'modelHint_espeak': 'language code, e.g. en',
|
|
728
|
+
'modelHint_kokoro': 'not bundled — offline neural runtime unavailable',
|
|
729
|
+
'modelHint_f5': 'not bundled — offline neural runtime unavailable',
|
|
658
730
|
'dockLabel': 'Speech',
|
|
659
731
|
'dockPause': 'Pause',
|
|
660
732
|
'dockResume': 'Resume speech',
|
|
@@ -862,6 +934,25 @@ window.__ModuleLoader__.load({
|
|
|
862
934
|
}
|
|
863
935
|
|
|
864
936
|
const zh = {
|
|
937
|
+
'ttsUiError': '⚠️ TTS 界面错误:',
|
|
938
|
+
'modelHint_openai': '服务商默认',
|
|
939
|
+
'modelHint_elevenlabs': '服务商默认',
|
|
940
|
+
'modelHint_google': '服务商默认',
|
|
941
|
+
'modelHint_azure': '区域语音名称',
|
|
942
|
+
'modelHint_groq': '服务商默认',
|
|
943
|
+
'modelHint_deepgram': '服务商默认 (仅英语语音)',
|
|
944
|
+
'modelHint_openrouter': '服务商/模型 ID',
|
|
945
|
+
'modelHint_siliconflow': '模型 ID (如 FunAudioLLM/CosyVoice2-0.5B)',
|
|
946
|
+
'modelHint_deepinfra': '模型 ID (如 hexgrad/Kokoro-82M)',
|
|
947
|
+
'modelHint_fireworks': '模型 ID (如 kokoro)',
|
|
948
|
+
'modelHint_mimo': '模型 ID (如 mimo-v2.5-tts)',
|
|
949
|
+
'modelHint_custom': '自定义模型',
|
|
950
|
+
'modelHint_minimax': '模型 ID',
|
|
951
|
+
'modelHint_edge': 'BCP-47 语音名称,如 en-US-AriaNeural',
|
|
952
|
+
'modelHint_piper': '.onnx 模型文件路径',
|
|
953
|
+
'modelHint_espeak': '语言代码,如 en',
|
|
954
|
+
'modelHint_kokoro': '未内置 — 离线神经语音运行时不可用',
|
|
955
|
+
'modelHint_f5': '未内置 — 离线神经语音运行时不可用',
|
|
865
956
|
'dockLabel': '语音朗读',
|
|
866
957
|
'dockPause': '暂停朗读',
|
|
867
958
|
'dockResume': '继续朗读',
|
|
@@ -1100,7 +1191,7 @@ window.__ModuleLoader__.load({
|
|
|
1100
1191
|
}, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
1101
1192
|
React.createElement('input', {
|
|
1102
1193
|
className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
|
|
1103
|
-
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
1194
|
+
placeholder: (typeof getModelHint === 'function' ? getModelHint(row.provider, t) : (MODEL_HINT[row.provider] || '')), onChange: (e) => change(i, { model: e.target.value }),
|
|
1104
1195
|
}),
|
|
1105
1196
|
React.createElement('input', {
|
|
1106
1197
|
className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
|
|
@@ -1150,8 +1241,8 @@ window.__ModuleLoader__.load({
|
|
|
1150
1241
|
const [models, setModels] = React.useState({})
|
|
1151
1242
|
|
|
1152
1243
|
const loadStatus = React.useCallback(() => {
|
|
1153
|
-
if (typeof fetch === 'undefined') return
|
|
1154
|
-
|
|
1244
|
+
if (typeof clientFetch === 'undefined' && typeof fetch === 'undefined') return
|
|
1245
|
+
clientFetch('/dsh-tts/models/status', { timeoutMs: 8000 })
|
|
1155
1246
|
.then((r) => r.json())
|
|
1156
1247
|
.then((d) => { if (d && d.ok && d.models) setModels(d.models) })
|
|
1157
1248
|
.catch(() => {}) // status poll best-effort
|
|
@@ -1167,11 +1258,14 @@ window.__ModuleLoader__.load({
|
|
|
1167
1258
|
if (typeof window !== 'undefined' && window.confirm) {
|
|
1168
1259
|
if (!window.confirm(t('confirmDeleteModel'))) return
|
|
1169
1260
|
}
|
|
1170
|
-
|
|
1261
|
+
clientFetch('/dsh-tts/models/delete', {
|
|
1171
1262
|
method: 'DELETE',
|
|
1172
1263
|
headers: { 'content-type': 'application/json' },
|
|
1173
1264
|
body: JSON.stringify({ engine }),
|
|
1174
|
-
|
|
1265
|
+
timeoutMs: 10000,
|
|
1266
|
+
}).then(() => loadStatus()).catch((err) => {
|
|
1267
|
+
console.warn('[dsh-tts] model delete failed:', err)
|
|
1268
|
+
})
|
|
1175
1269
|
}
|
|
1176
1270
|
|
|
1177
1271
|
const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
|
|
@@ -1411,7 +1505,7 @@ window.__ModuleLoader__.load({
|
|
|
1411
1505
|
style: {
|
|
1412
1506
|
padding: '10px 14px',
|
|
1413
1507
|
borderRadius: '8px',
|
|
1414
|
-
border: '1px solid var(--dsw-alias-state-
|
|
1508
|
+
border: '1px solid var(--dsw-alias-state-warn-primary, #f59e0b)',
|
|
1415
1509
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1416
1510
|
display: 'flex',
|
|
1417
1511
|
flexDirection: 'column',
|
|
@@ -1419,7 +1513,7 @@ window.__ModuleLoader__.load({
|
|
|
1419
1513
|
fontSize: '13px',
|
|
1420
1514
|
},
|
|
1421
1515
|
},
|
|
1422
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-
|
|
1516
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary, #f59e0b)' } },
|
|
1423
1517
|
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1424
1518
|
),
|
|
1425
1519
|
React.createElement('span', { className: 'dts-sub' },
|
|
@@ -1453,7 +1547,7 @@ window.__ModuleLoader__.load({
|
|
|
1453
1547
|
style: {
|
|
1454
1548
|
padding: '10px 14px',
|
|
1455
1549
|
borderRadius: '8px',
|
|
1456
|
-
border: '1px solid var(--dsw-alias-state-
|
|
1550
|
+
border: '1px solid var(--dsw-alias-state-warn-primary, #f59e0b)',
|
|
1457
1551
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1458
1552
|
display: 'flex',
|
|
1459
1553
|
flexDirection: 'column',
|
|
@@ -1461,7 +1555,7 @@ window.__ModuleLoader__.load({
|
|
|
1461
1555
|
fontSize: '13px',
|
|
1462
1556
|
},
|
|
1463
1557
|
},
|
|
1464
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-
|
|
1558
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary, #f59e0b)' } },
|
|
1465
1559
|
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1466
1560
|
),
|
|
1467
1561
|
React.createElement('span', { className: 'dts-sub' },
|
|
@@ -1496,8 +1590,9 @@ window.__ModuleLoader__.load({
|
|
|
1496
1590
|
setLoading(true)
|
|
1497
1591
|
setMsg(null)
|
|
1498
1592
|
try {
|
|
1499
|
-
const res = await
|
|
1593
|
+
const res = await clientFetch('/api/dsh-tts/update', {
|
|
1500
1594
|
headers: { 'x-dsh-plugin-update': '1' },
|
|
1595
|
+
timeoutMs: 10000,
|
|
1501
1596
|
})
|
|
1502
1597
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
1503
1598
|
const data = await res.json()
|
|
@@ -1513,12 +1608,13 @@ window.__ModuleLoader__.load({
|
|
|
1513
1608
|
setUpdating(true)
|
|
1514
1609
|
setMsg(null)
|
|
1515
1610
|
try {
|
|
1516
|
-
const res = await
|
|
1611
|
+
const res = await clientFetch('/api/dsh-tts/update', {
|
|
1517
1612
|
method: 'POST',
|
|
1518
1613
|
headers: {
|
|
1519
1614
|
'x-dsh-plugin-update': '1',
|
|
1520
1615
|
'content-type': 'application/json',
|
|
1521
1616
|
},
|
|
1617
|
+
timeoutMs: 60000,
|
|
1522
1618
|
})
|
|
1523
1619
|
const data = await res.json()
|
|
1524
1620
|
if (!res.ok) throw new Error((data && (data.error || data.message)) || 'HTTP ' + res.status)
|
|
@@ -1562,7 +1658,7 @@ window.__ModuleLoader__.load({
|
|
|
1562
1658
|
{
|
|
1563
1659
|
type: 'button',
|
|
1564
1660
|
className: 'dts-btn',
|
|
1565
|
-
style: { borderColor: 'var(--dsw-alias-state-
|
|
1661
|
+
style: { borderColor: 'var(--dsw-alias-state-warn-primary, #f59e0b)', color: 'var(--dsw-alias-state-warn-primary, #f59e0b)' },
|
|
1566
1662
|
onClick: onUpdateNow,
|
|
1567
1663
|
disabled: updating || loading,
|
|
1568
1664
|
},
|
|
@@ -1592,11 +1688,11 @@ window.__ModuleLoader__.load({
|
|
|
1592
1688
|
const t = props.t || ((key) => key)
|
|
1593
1689
|
const [data, setData] = React.useState(null)
|
|
1594
1690
|
const loadStats = () => {
|
|
1595
|
-
|
|
1691
|
+
clientFetch('/dsh-tts/stats', { cache: 'no-store', timeoutMs: 8000 }).then((r) => r.json()).then(setData).catch((err) => console.warn('[dsh-tts] loadStats failed:', err))
|
|
1596
1692
|
}
|
|
1597
1693
|
React.useEffect(() => { loadStats() }, [])
|
|
1598
1694
|
const reset = () => {
|
|
1599
|
-
|
|
1695
|
+
clientFetch('/dsh-tts/stats', { method: 'DELETE', timeoutMs: 8000 }).then(loadStats).catch((err) => console.warn('[dsh-tts] resetStats failed:', err))
|
|
1600
1696
|
}
|
|
1601
1697
|
return React.createElement('div', { className: 'dts-block' },
|
|
1602
1698
|
React.createElement('span', { className: 'dts-sub' }, data
|
|
@@ -1668,6 +1764,7 @@ window.__ModuleLoader__.load({
|
|
|
1668
1764
|
// looks like a working empty form.
|
|
1669
1765
|
// Snapshot status contract (#163):
|
|
1670
1766
|
// Without valid scope, or when loading/unavailable, editing is disabled.
|
|
1767
|
+
const [revision, setRevision] = React.useState(null)
|
|
1671
1768
|
const ready = !!scope && !!snap && snap.status === 'ready'
|
|
1672
1769
|
const writable = ready && snap.writable !== false
|
|
1673
1770
|
|
|
@@ -1675,23 +1772,24 @@ window.__ModuleLoader__.load({
|
|
|
1675
1772
|
const cfg = data && data.config ? data.config : {}
|
|
1676
1773
|
setDraft(JSON.parse(JSON.stringify(cfg)))
|
|
1677
1774
|
setCredentials(data && data.credentials ? data.credentials : {})
|
|
1775
|
+
if (data && data.revision !== undefined) setRevision(data.revision)
|
|
1678
1776
|
player.enabled = !!cfg.speakReplies
|
|
1679
1777
|
}
|
|
1680
1778
|
|
|
1681
1779
|
React.useEffect(() => {
|
|
1682
1780
|
let alive = true
|
|
1683
|
-
|
|
1781
|
+
clientFetch('/dsh-tts/config', { cache: 'no-store', timeoutMs: 8000 }).then((res) => res.json()).then((data) => {
|
|
1684
1782
|
if (!alive) return
|
|
1685
1783
|
applyPayload(data)
|
|
1686
1784
|
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
1687
1785
|
|
|
1688
|
-
|
|
1786
|
+
clientFetch('/dsh-tts/integrations', { cache: 'no-store', timeoutMs: 8000 }).then((res) => res.json()).then((data) => {
|
|
1689
1787
|
if (!alive) return
|
|
1690
|
-
if (data && data.ok) setIntegrations(data)
|
|
1788
|
+
if (data && data.ok) setIntegrations(data.integrations || data)
|
|
1691
1789
|
}).catch(() => {})
|
|
1692
1790
|
|
|
1693
1791
|
const loadTelemetry = () => {
|
|
1694
|
-
|
|
1792
|
+
clientFetch('/dsh-tts/stats', { cache: 'no-store', timeoutMs: 8000 }).then((res) => res.json()).then((st) => {
|
|
1695
1793
|
if (!alive || !st) return
|
|
1696
1794
|
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1697
1795
|
cacheHits: st.cacheHits || 0,
|
|
@@ -1702,7 +1800,7 @@ window.__ModuleLoader__.load({
|
|
|
1702
1800
|
}).catch(() => {
|
|
1703
1801
|
if (alive) setTelemetry((prev) => Object.assign({}, prev, { online: false }))
|
|
1704
1802
|
})
|
|
1705
|
-
|
|
1803
|
+
clientFetch('/dsh-tts/status', { cache: 'no-store', timeoutMs: 8000 }).then((res) => res.json()).then((st) => {
|
|
1706
1804
|
if (!alive || !st) return
|
|
1707
1805
|
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1708
1806
|
sseClients: st.sseClients || 0,
|
|
@@ -1733,10 +1831,11 @@ window.__ModuleLoader__.load({
|
|
|
1733
1831
|
const value = String(raw != null ? raw : ((keyDrafts && keyDrafts[provider]) || '')).trim()
|
|
1734
1832
|
if (!value) return
|
|
1735
1833
|
setErr('')
|
|
1736
|
-
const res = await
|
|
1834
|
+
const res = await clientFetch('/dsh-tts/credential', {
|
|
1737
1835
|
method: 'PUT',
|
|
1738
1836
|
headers: { 'Content-Type': 'application/json' },
|
|
1739
1837
|
body: JSON.stringify({ provider: provider, value: value }),
|
|
1838
|
+
timeoutMs: 10000,
|
|
1740
1839
|
})
|
|
1741
1840
|
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
1742
1841
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
@@ -1747,10 +1846,11 @@ window.__ModuleLoader__.load({
|
|
|
1747
1846
|
const clearKey = async (provider) => {
|
|
1748
1847
|
setErr('')
|
|
1749
1848
|
try {
|
|
1750
|
-
const res = await
|
|
1849
|
+
const res = await clientFetch('/dsh-tts/credential', {
|
|
1751
1850
|
method: 'DELETE',
|
|
1752
1851
|
headers: { 'Content-Type': 'application/json' },
|
|
1753
1852
|
body: JSON.stringify({ provider: provider }),
|
|
1853
|
+
timeoutMs: 10000,
|
|
1754
1854
|
})
|
|
1755
1855
|
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
1756
1856
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
@@ -1768,10 +1868,15 @@ window.__ModuleLoader__.load({
|
|
|
1768
1868
|
const v = String(keyDrafts[p] || '').trim()
|
|
1769
1869
|
if (v) keys[p] = v
|
|
1770
1870
|
})
|
|
1771
|
-
const
|
|
1871
|
+
const bodyPayload = Object.assign({}, draft, { keys: keys })
|
|
1872
|
+
if (revision !== null && revision !== undefined) {
|
|
1873
|
+
bodyPayload.revision = revision
|
|
1874
|
+
}
|
|
1875
|
+
const res = await clientFetch('/dsh-tts/config', {
|
|
1772
1876
|
method: 'PUT',
|
|
1773
1877
|
headers: { 'Content-Type': 'application/json' },
|
|
1774
|
-
body: JSON.stringify(
|
|
1878
|
+
body: JSON.stringify(bodyPayload),
|
|
1879
|
+
timeoutMs: 10000,
|
|
1775
1880
|
})
|
|
1776
1881
|
const data = await res.json().catch(() => ({ /* parse error */ }))
|
|
1777
1882
|
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
@@ -1787,7 +1892,7 @@ window.__ModuleLoader__.load({
|
|
|
1787
1892
|
|
|
1788
1893
|
const clearCacheNow = async () => {
|
|
1789
1894
|
setErr('')
|
|
1790
|
-
const res = await
|
|
1895
|
+
const res = await clientFetch('/dsh-tts/cache', { method: 'DELETE', timeoutMs: 10000 })
|
|
1791
1896
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
1792
1897
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
1793
1898
|
}
|
|
@@ -1885,12 +1990,12 @@ window.__ModuleLoader__.load({
|
|
|
1885
1990
|
React.createElement(VoiceDuplexEditor, {
|
|
1886
1991
|
t: t, writable: writable, draft: draft,
|
|
1887
1992
|
boolField: boolField, setTop: setTop,
|
|
1888
|
-
installed: !!(integrations && integrations.voice
|
|
1993
|
+
installed: !!(integrations && (integrations.voice?.installed ?? integrations.integrations?.voice?.installed ?? integrations.voiceInstalled)),
|
|
1889
1994
|
}),
|
|
1890
1995
|
React.createElement(MessengerIntegrationEditor, {
|
|
1891
1996
|
t: t, writable: writable, draft: draft,
|
|
1892
1997
|
boolField: boolField, setTop: setTop,
|
|
1893
|
-
installed: !!(integrations && integrations.messenger
|
|
1998
|
+
installed: !!(integrations && (integrations.messenger?.installed ?? integrations.integrations?.messenger?.installed ?? integrations.messengerInstalled)),
|
|
1894
1999
|
}),
|
|
1895
2000
|
React.createElement('div', { className: 'dts-block' },
|
|
1896
2001
|
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
package/lib/engines/manager.js
CHANGED
|
@@ -11,12 +11,12 @@ export const MODEL_SPECS = {
|
|
|
11
11
|
files: [
|
|
12
12
|
{
|
|
13
13
|
name: 'kokoro-v0_19.onnx',
|
|
14
|
-
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/
|
|
14
|
+
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/f905c3175b9f7a77d4c1b9be894ff99ff7e9a8f2/kokoro-v0_19.onnx',
|
|
15
15
|
minBytes: 1024,
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: 'voices.json',
|
|
19
|
-
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/
|
|
19
|
+
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/f905c3175b9f7a77d4c1b9be894ff99ff7e9a8f2/voices.json',
|
|
20
20
|
minBytes: 1024,
|
|
21
21
|
},
|
|
22
22
|
],
|
|
@@ -45,13 +45,26 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
45
45
|
return path.join(baseDir, engineId)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
async function validateFileIntegrity(filePath, minBytes = 1024) {
|
|
49
|
+
try {
|
|
50
|
+
const st = await fs.promises.stat(filePath)
|
|
51
|
+
if (!st.isFile() || st.size < minBytes) return false
|
|
52
|
+
const fd = await fs.promises.open(filePath, 'r')
|
|
53
|
+
const buf = Buffer.alloc(Math.min(st.size, 1024))
|
|
54
|
+
await fd.read(buf, 0, buf.length, 0)
|
|
55
|
+
await fd.close()
|
|
56
|
+
const str = buf.toString('utf8').trim().toLowerCase()
|
|
57
|
+
if (str.startsWith('<!doctype') || str.startsWith('<html') || str.startsWith('<head') || str.includes('<html')) {
|
|
53
58
|
return false
|
|
54
59
|
}
|
|
60
|
+
return true
|
|
61
|
+
} catch (_err) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function checkFileExists(filePath, minBytes = 1024) {
|
|
67
|
+
return await validateFileIntegrity(filePath, minBytes)
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
async function getDirSize(dirPath) {
|
|
@@ -236,6 +249,10 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
236
249
|
if (st.size < minExpected) {
|
|
237
250
|
throw new Error(`File ${f.name} truncated: got ${st.size} bytes, expected at least ${minExpected}`)
|
|
238
251
|
}
|
|
252
|
+
const valid = await validateFileIntegrity(tmpFile, minExpected)
|
|
253
|
+
if (!valid) {
|
|
254
|
+
throw new Error(`File ${f.name} failed integrity check: invalid format or HTML error`)
|
|
255
|
+
}
|
|
239
256
|
|
|
240
257
|
await fs.promises.rename(tmpFile, targetFile)
|
|
241
258
|
const idx = createdTmpFiles.indexOf(tmpFile)
|
|
@@ -255,7 +272,9 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
255
272
|
lastErrors.set(engineId, { message: sanitized, failedAt: Date.now() })
|
|
256
273
|
throw err
|
|
257
274
|
} finally {
|
|
258
|
-
activeDownloads.
|
|
275
|
+
if (activeDownloads.get(engineId) === downloadState) {
|
|
276
|
+
activeDownloads.delete(engineId)
|
|
277
|
+
}
|
|
259
278
|
}
|
|
260
279
|
}
|
|
261
280
|
|
|
@@ -269,7 +288,16 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
269
288
|
const active = activeDownloads.get(engineId)
|
|
270
289
|
if (active && active.controller) {
|
|
271
290
|
active.controller.abort()
|
|
272
|
-
|
|
291
|
+
if (active.promise) {
|
|
292
|
+
try {
|
|
293
|
+
await active.promise
|
|
294
|
+
} catch (_err) {
|
|
295
|
+
// aborted download rejected as expected
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (activeDownloads.get(engineId) === active) {
|
|
299
|
+
activeDownloads.delete(engineId)
|
|
300
|
+
}
|
|
273
301
|
lastErrors.delete(engineId)
|
|
274
302
|
return true
|
|
275
303
|
}
|