@goodandready/dsh-tts 0.4.27 → 0.4.28
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 +39 -0
- package/README.md +9 -5
- package/lib/cache.js +70 -40
- package/lib/client.js +257 -160
- package/lib/engines/manager.js +103 -69
- package/lib/index.js +277 -54
- package/lib/local.js +1 -1
- package/lib/providers/cloud.js +36 -5
- package/lib/providers/constants.js +4 -4
- package/lib/providers/local.js +1 -1
- package/lib/providers.js +11 -2
- package/lib/routes.js +102 -36
- package/lib/stream-hub.js +51 -7
- package/lib/text.js +18 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -21,10 +21,12 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
22
22
|
const PROVIDERS = [
|
|
23
23
|
'openai', 'elevenlabs', 'google', 'azure', 'groq', 'deepgram', 'openrouter',
|
|
24
|
-
'
|
|
24
|
+
'siliconflow', 'deepinfra', 'fireworks', 'mimo', 'custom',
|
|
25
|
+
'edge', 'piper', 'espeak', 'minimax',
|
|
25
26
|
]
|
|
26
27
|
const CLOUD = {
|
|
27
|
-
openai: 1, elevenlabs: 1, google: 1, azure: 1, groq: 1, deepgram: 1, openrouter: 1,
|
|
28
|
+
openai: 1, elevenlabs: 1, google: 1, azure: 1, groq: 1, deepgram: 1, openrouter: 1,
|
|
29
|
+
siliconflow: 1, deepinfra: 1, fireworks: 1, mimo: 1, custom: 1,
|
|
28
30
|
}
|
|
29
31
|
// Hints stay generic: concrete provider model IDs rot quickly.
|
|
30
32
|
const MODEL_HINT = {
|
|
@@ -35,9 +37,15 @@ window.__ModuleLoader__.load({
|
|
|
35
37
|
groq: 'provider default',
|
|
36
38
|
deepgram: 'provider default (English-only voices)',
|
|
37
39
|
openrouter: 'provider/model id',
|
|
38
|
-
|
|
40
|
+
siliconflow: 'model id (e.g. FunAudioLLM/CosyVoice2-0.5B)',
|
|
41
|
+
deepinfra: 'model id (e.g. hexgrad/Kokoro-82M)',
|
|
42
|
+
fireworks: 'model id (e.g. kokoro)',
|
|
43
|
+
mimo: 'model id (e.g. mimo-v2.5-tts)',
|
|
44
|
+
custom: 'custom model',
|
|
45
|
+
minimax: 'model id',
|
|
46
|
+
edge: 'BCP-47 voice, e.g. en-US-AriaNeural',
|
|
39
47
|
piper: 'path to .onnx',
|
|
40
|
-
espeak: 'language code, e.g.
|
|
48
|
+
espeak: 'language code, e.g. en',
|
|
41
49
|
kokoro: 'not bundled — offline neural runtime unavailable',
|
|
42
50
|
f5: 'not bundled — offline neural runtime unavailable',
|
|
43
51
|
}
|
|
@@ -76,7 +84,7 @@ window.__ModuleLoader__.load({
|
|
|
76
84
|
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
77
85
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
78
86
|
},
|
|
79
|
-
'Retry',
|
|
87
|
+
(typeof fallbackDockText === 'function' ? fallbackDockText('retry') : 'Retry'),
|
|
80
88
|
),
|
|
81
89
|
)
|
|
82
90
|
}
|
|
@@ -167,6 +175,8 @@ window.__ModuleLoader__.load({
|
|
|
167
175
|
}
|
|
168
176
|
} catch { /* no AudioContext */ }
|
|
169
177
|
setAutoplayBlocked(false)
|
|
178
|
+
player.blocked = false
|
|
179
|
+
playerChanged()
|
|
170
180
|
drainQueue()
|
|
171
181
|
}
|
|
172
182
|
|
|
@@ -189,6 +199,7 @@ window.__ModuleLoader__.load({
|
|
|
189
199
|
bargeIn: true,
|
|
190
200
|
busy: false,
|
|
191
201
|
paused: false,
|
|
202
|
+
blocked: false,
|
|
192
203
|
listeners: new Set(),
|
|
193
204
|
unlockAudio,
|
|
194
205
|
useAutoplayGate,
|
|
@@ -293,6 +304,12 @@ window.__ModuleLoader__.load({
|
|
|
293
304
|
if (doneCalled) return
|
|
294
305
|
doneCalled = true
|
|
295
306
|
currentAudioResolve = null
|
|
307
|
+
if (player.audio) {
|
|
308
|
+
try {
|
|
309
|
+
player.audio.pause()
|
|
310
|
+
player.audio.currentTime = 0
|
|
311
|
+
} catch { /* already paused */ }
|
|
312
|
+
}
|
|
296
313
|
try { URL.revokeObjectURL(url) } catch { /* URL already revoked */ }
|
|
297
314
|
item.prepared = null
|
|
298
315
|
player.audio = null
|
|
@@ -303,9 +320,27 @@ window.__ModuleLoader__.load({
|
|
|
303
320
|
audio.onended = done
|
|
304
321
|
audio.onerror = done
|
|
305
322
|
audio.play().catch((playErr) => {
|
|
306
|
-
// Autoplay without a user gesture is blocked — surface unlock UX
|
|
323
|
+
// Autoplay without a user gesture is blocked — retain queue and surface unlock UX (#160)
|
|
307
324
|
if (playErr && (playErr.name === 'NotAllowedError' || playErr.name === 'SecurityError')) {
|
|
325
|
+
player.blocked = true
|
|
308
326
|
setAutoplayBlocked(true)
|
|
327
|
+
player.queue.unshift(item)
|
|
328
|
+
currentAudioResolve = null
|
|
329
|
+
player.audio = null
|
|
330
|
+
player.busy = false
|
|
331
|
+
playerChanged()
|
|
332
|
+
if (typeof window !== 'undefined') {
|
|
333
|
+
const onUserGesture = () => {
|
|
334
|
+
window.removeEventListener('click', onUserGesture)
|
|
335
|
+
window.removeEventListener('keydown', onUserGesture)
|
|
336
|
+
window.removeEventListener('touchstart', onUserGesture)
|
|
337
|
+
unlockAudio()
|
|
338
|
+
}
|
|
339
|
+
window.addEventListener('click', onUserGesture, { once: true })
|
|
340
|
+
window.addEventListener('keydown', onUserGesture, { once: true })
|
|
341
|
+
window.addEventListener('touchstart', onUserGesture, { once: true })
|
|
342
|
+
}
|
|
343
|
+
return
|
|
309
344
|
}
|
|
310
345
|
done()
|
|
311
346
|
})
|
|
@@ -317,12 +352,11 @@ window.__ModuleLoader__.load({
|
|
|
317
352
|
}
|
|
318
353
|
|
|
319
354
|
async function drainQueue() {
|
|
320
|
-
if (player.busy) return
|
|
355
|
+
if (player.busy || player.blocked || player.paused) return
|
|
321
356
|
player.busy = true
|
|
322
357
|
playerChanged()
|
|
323
358
|
try {
|
|
324
|
-
while (player.queue.length) {
|
|
325
|
-
if (player.paused) break
|
|
359
|
+
while (player.queue.length && !player.blocked && !player.paused) {
|
|
326
360
|
const item = player.queue.shift()
|
|
327
361
|
if (!item || item.kind === 'reserved') continue
|
|
328
362
|
if (item.kind === 'chime') { await playChime(item.chime || player.chime); continue }
|
|
@@ -345,7 +379,10 @@ window.__ModuleLoader__.load({
|
|
|
345
379
|
player.queue.length = 0
|
|
346
380
|
player.paused = false
|
|
347
381
|
if (player.audio) {
|
|
348
|
-
try {
|
|
382
|
+
try {
|
|
383
|
+
player.audio.pause()
|
|
384
|
+
player.audio.currentTime = 0
|
|
385
|
+
} catch { /* already paused */ }
|
|
349
386
|
}
|
|
350
387
|
if (currentAudioResolve) {
|
|
351
388
|
currentAudioResolve()
|
|
@@ -365,10 +402,15 @@ window.__ModuleLoader__.load({
|
|
|
365
402
|
}
|
|
366
403
|
|
|
367
404
|
function skipCurrent() {
|
|
405
|
+
if (player.audio) {
|
|
406
|
+
try {
|
|
407
|
+
player.audio.pause()
|
|
408
|
+
player.audio.currentTime = 0
|
|
409
|
+
} catch { /* ignore pause error */ }
|
|
410
|
+
}
|
|
368
411
|
if (currentAudioResolve) {
|
|
369
412
|
currentAudioResolve()
|
|
370
|
-
} else
|
|
371
|
-
try { player.audio.pause() } catch (already) { /* already stopped */ }
|
|
413
|
+
} else {
|
|
372
414
|
player.audio = null
|
|
373
415
|
playerChanged()
|
|
374
416
|
if (player.queue.length) drainQueue()
|
|
@@ -392,70 +434,83 @@ window.__ModuleLoader__.load({
|
|
|
392
434
|
return () => window.removeEventListener('dsh-voice:speaking', onSpeaking)
|
|
393
435
|
}
|
|
394
436
|
|
|
395
|
-
|
|
396
|
-
let
|
|
397
|
-
let
|
|
398
|
-
let
|
|
437
|
+
// Unified SSE Realtime Stream Connection (#158)
|
|
438
|
+
let activeSse = null
|
|
439
|
+
let activeSseAttempt = 0
|
|
440
|
+
let activeSseTimer = null
|
|
441
|
+
let sseDisposed = false
|
|
399
442
|
|
|
400
|
-
function
|
|
401
|
-
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
443
|
+
function startSseStream() {
|
|
444
|
+
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return () => {}
|
|
445
|
+
sseDisposed = false
|
|
446
|
+
|
|
447
|
+
const onItem = (e) => {
|
|
448
|
+
try {
|
|
449
|
+
const item = JSON.parse(e.data)
|
|
450
|
+
if (!item || !item.id) return
|
|
451
|
+
if (player.enabled === false) return
|
|
452
|
+
player.after = item.id
|
|
453
|
+
if (seenIds.has(item.id)) return
|
|
454
|
+
seenIds.add(item.id)
|
|
455
|
+
if (seenIds.size > 500) {
|
|
456
|
+
const first = seenIds.values().next().value
|
|
457
|
+
seenIds.delete(first)
|
|
458
|
+
}
|
|
459
|
+
if (item.text) recent.add(item.text)
|
|
460
|
+
player.queue.push(item)
|
|
461
|
+
drainQueue()
|
|
462
|
+
} catch { /* ignore malformed SSE */ }
|
|
405
463
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
464
|
+
|
|
465
|
+
const onChime = (e) => {
|
|
466
|
+
try {
|
|
467
|
+
const item = JSON.parse(e.data)
|
|
468
|
+
if (!item) return
|
|
469
|
+
if (player.enabled === false) return
|
|
470
|
+
player.queue.push(item)
|
|
471
|
+
drainQueue()
|
|
472
|
+
} catch {}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const connect = () => {
|
|
476
|
+
if (sseDisposed) return
|
|
477
|
+
if (activeSse) {
|
|
478
|
+
try { activeSse.close() } catch {}
|
|
479
|
+
activeSse = null
|
|
410
480
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
seenIds.delete(first)
|
|
422
|
-
}
|
|
423
|
-
if (item.text) recent.add(item.text)
|
|
424
|
-
player.queue.push(item)
|
|
425
|
-
drainQueue()
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
} catch (parseErr) { /* ignore malformed SSE payload */ }
|
|
429
|
-
})
|
|
430
|
-
sseSource.addEventListener('chime', (e) => {
|
|
431
|
-
sseAttempt = 0
|
|
432
|
-
try {
|
|
433
|
-
const item = JSON.parse(e.data)
|
|
434
|
-
if (item) {
|
|
435
|
-
player.queue.push(item)
|
|
436
|
-
drainQueue()
|
|
481
|
+
try {
|
|
482
|
+
const es = new EventSource('/dsh-tts/stream')
|
|
483
|
+
activeSse = es
|
|
484
|
+
es.onopen = () => { activeSseAttempt = 0 }
|
|
485
|
+
es.addEventListener('utterance', onItem)
|
|
486
|
+
es.addEventListener('chime', onChime)
|
|
487
|
+
es.onerror = () => {
|
|
488
|
+
if (activeSse === es) {
|
|
489
|
+
try { es.close() } catch {}
|
|
490
|
+
activeSse = null
|
|
437
491
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
sseSource = null
|
|
492
|
+
if (sseDisposed) return
|
|
493
|
+
activeSseAttempt++
|
|
494
|
+
const delay = Math.min(10000, 500 * Math.pow(2, Math.min(activeSseAttempt, 5)))
|
|
495
|
+
if (activeSseTimer) clearTimeout(activeSseTimer)
|
|
496
|
+
activeSseTimer = setTimeout(connect, delay)
|
|
444
497
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
498
|
+
} catch { /* EventSource failed */ }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
connect()
|
|
502
|
+
|
|
503
|
+
return () => {
|
|
504
|
+
sseDisposed = true
|
|
505
|
+
if (activeSseTimer) clearTimeout(activeSseTimer)
|
|
506
|
+
if (activeSse) {
|
|
507
|
+
try { activeSse.close() } catch {}
|
|
508
|
+
activeSse = null
|
|
453
509
|
}
|
|
454
|
-
}
|
|
510
|
+
}
|
|
455
511
|
}
|
|
456
512
|
|
|
457
|
-
async function pollPending() {
|
|
458
|
-
connectSseStream()
|
|
513
|
+
async function pollPending() {
|
|
459
514
|
try {
|
|
460
515
|
const st = await fetch('/dsh-tts/status', { cache: 'no-store' })
|
|
461
516
|
if (!st.ok) return
|
|
@@ -463,7 +518,7 @@ async function pollPending() {
|
|
|
463
518
|
player.enabled = !!(meta && meta.speakReplies)
|
|
464
519
|
if (meta && typeof meta.rate === 'number') player.rate = meta.rate
|
|
465
520
|
if (meta && meta.chime) player.chime = meta.chime
|
|
466
|
-
|
|
521
|
+
if (meta && meta.roles) player.roles = meta.roles
|
|
467
522
|
if (meta && typeof meta.bargeIn === 'boolean') player.bargeIn = meta.bargeIn
|
|
468
523
|
if (!player.enabled) return
|
|
469
524
|
const res = await fetch('/dsh-tts/pending?after=' + encodeURIComponent(player.after || ''), { cache: 'no-store' })
|
|
@@ -471,8 +526,6 @@ async function pollPending() {
|
|
|
471
526
|
const data = await res.json()
|
|
472
527
|
const items = data && Array.isArray(data.items) ? data.items : []
|
|
473
528
|
for (const item of items) {
|
|
474
|
-
// Queue slot reserved but synthesis still running — wait for the next poll
|
|
475
|
-
// or phrase order would break.
|
|
476
529
|
if (item.kind === 'reserved') break
|
|
477
530
|
player.after = item.id
|
|
478
531
|
if (seenIds.has(item.id)) continue
|
|
@@ -485,7 +538,7 @@ async function pollPending() {
|
|
|
485
538
|
player.queue.push(item)
|
|
486
539
|
}
|
|
487
540
|
if (player.queue.length) drainQueue()
|
|
488
|
-
} catch
|
|
541
|
+
} catch { /* host restarting */ }
|
|
489
542
|
}
|
|
490
543
|
|
|
491
544
|
// Recent utterances: last reply texts and favorites (localStorage).
|
|
@@ -514,8 +567,16 @@ async function pollPending() {
|
|
|
514
567
|
}
|
|
515
568
|
const recent = recentStore()
|
|
516
569
|
|
|
517
|
-
//
|
|
570
|
+
// Unified playback through the shared player controller (#159)
|
|
571
|
+
function playAudioItem(item) {
|
|
572
|
+
stopPlayback()
|
|
573
|
+
player.queue.push(item)
|
|
574
|
+
drainQueue()
|
|
575
|
+
}
|
|
576
|
+
|
|
518
577
|
function playStandalone(text) {
|
|
578
|
+
if (!text) return
|
|
579
|
+
stopPlayback()
|
|
519
580
|
fetch('/dsh-tts/speak', {
|
|
520
581
|
method: 'POST',
|
|
521
582
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -523,13 +584,41 @@ async function pollPending() {
|
|
|
523
584
|
})
|
|
524
585
|
.then((r) => r.json())
|
|
525
586
|
.then((d) => {
|
|
526
|
-
if (!d || !d.ok) return
|
|
527
|
-
|
|
528
|
-
|
|
587
|
+
if (!d || !d.ok || !d.audioBase64) return
|
|
588
|
+
playAudioItem({
|
|
589
|
+
id: `replay-${Date.now()}`,
|
|
590
|
+
text,
|
|
591
|
+
mime: d.mime || 'audio/mpeg',
|
|
592
|
+
audioBase64: d.audioBase64,
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
.catch(() => {})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function previewSpeech(provider, model, voice, text) {
|
|
599
|
+
stopPlayback()
|
|
600
|
+
return fetch('/dsh-tts/preview', {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: { 'Content-Type': 'application/json' },
|
|
603
|
+
body: JSON.stringify({ provider, model, voice, text }),
|
|
604
|
+
})
|
|
605
|
+
.then((r) => r.json())
|
|
606
|
+
.then((d) => {
|
|
607
|
+
if (!d || !d.ok) throw new Error((d && d.error && d.error.message) || 'HTTP error')
|
|
608
|
+
playAudioItem({
|
|
609
|
+
id: `preview-${Date.now()}`,
|
|
610
|
+
text,
|
|
611
|
+
mime: d.mime || 'audio/mpeg',
|
|
612
|
+
audioBase64: d.audioBase64,
|
|
613
|
+
})
|
|
529
614
|
})
|
|
530
|
-
.catch(() => { /* chime fetch failed or not found */ })
|
|
531
615
|
}
|
|
532
616
|
|
|
617
|
+
player.playAudioItem = playAudioItem
|
|
618
|
+
player.previewSpeech = previewSpeech
|
|
619
|
+
player.startSseStream = startSseStream
|
|
620
|
+
player.skipCurrent = skipCurrent
|
|
621
|
+
|
|
533
622
|
function exportAudioClip(text) {
|
|
534
623
|
let phrase = text || player.lastText
|
|
535
624
|
if (!phrase) {
|
|
@@ -565,7 +654,6 @@ async function pollPending() {
|
|
|
565
654
|
function credOf(map, provider) {
|
|
566
655
|
return (map && map[provider]) || { configured: false, writable: true, ref: '' }
|
|
567
656
|
}
|
|
568
|
-
|
|
569
657
|
const en = {
|
|
570
658
|
'dockLabel': 'Speech',
|
|
571
659
|
'dockPause': 'Pause',
|
|
@@ -760,6 +848,17 @@ async function pollPending() {
|
|
|
760
848
|
'updaterUpdating': 'Updating…',
|
|
761
849
|
'updaterSuccess': 'Plugin updated successfully! Please restart DSH service.',
|
|
762
850
|
'updaterFailed': 'Update check failed: ',
|
|
851
|
+
'exportJson': '📥 Export JSON',
|
|
852
|
+
'importJson': '📤 Import JSON',
|
|
853
|
+
'invalidJsonFormat': 'Invalid dictionary JSON format',
|
|
854
|
+
'subtitle': 'Text-to-speech for DeepSeek Harness',
|
|
855
|
+
'available': 'available',
|
|
856
|
+
'dockSkip': 'Skip',
|
|
857
|
+
'longReplyTruncate': 'Truncate',
|
|
858
|
+
'longReplySummarize': 'Summarize',
|
|
859
|
+
'longReplyFull': 'Full',
|
|
860
|
+
'unlockAudio': 'Click to unlock audio',
|
|
861
|
+
'errorLoading': 'Error loading settings: ',
|
|
763
862
|
}
|
|
764
863
|
|
|
765
864
|
const zh = {
|
|
@@ -959,6 +1058,14 @@ async function pollPending() {
|
|
|
959
1058
|
'updaterUpdating': '正在更新…',
|
|
960
1059
|
'updaterSuccess': '插件更新成功!请重启 DSH 服务以生效。',
|
|
961
1060
|
'updaterFailed': '更新检查失败:',
|
|
1061
|
+
'subtitle': 'DeepSeek Harness 语音合成插件',
|
|
1062
|
+
'available': '可用',
|
|
1063
|
+
'dockSkip': '跳过',
|
|
1064
|
+
'longReplyTruncate': '截断',
|
|
1065
|
+
'longReplySummarize': '总结',
|
|
1066
|
+
'longReplyFull': '完整朗读',
|
|
1067
|
+
'unlockAudio': '点击解锁音频',
|
|
1068
|
+
'errorLoading': '加载设置失败:',
|
|
962
1069
|
}
|
|
963
1070
|
function ChainEditor(props) {
|
|
964
1071
|
// Translator comes from the card: the slot passes props.t only to it.
|
|
@@ -1094,7 +1201,24 @@ async function pollPending() {
|
|
|
1094
1201
|
)
|
|
1095
1202
|
}
|
|
1096
1203
|
|
|
1097
|
-
const CLIENT_IT_TERMS = [
|
|
1204
|
+
const CLIENT_IT_TERMS = [
|
|
1205
|
+
{ from: 'API', to: 'A P I', whole: true, lang: '' },
|
|
1206
|
+
{ from: 'CLI', to: 'C L I', whole: true, lang: '' },
|
|
1207
|
+
{ from: 'CPU', to: 'C P U', whole: true, lang: '' },
|
|
1208
|
+
{ from: 'GPU', to: 'G P U', whole: true, lang: '' },
|
|
1209
|
+
{ from: 'RAM', to: 'ram', whole: true, lang: '' },
|
|
1210
|
+
{ from: 'SQL', to: 'sequel', whole: true, lang: '' },
|
|
1211
|
+
{ from: 'UI', to: 'U I', whole: true, lang: '' },
|
|
1212
|
+
{ from: 'URL', to: 'U R L', whole: true, lang: '' },
|
|
1213
|
+
{ from: 'UUID', to: 'U U I D', whole: true, lang: '' },
|
|
1214
|
+
{ from: 'JSON', to: 'j-son', whole: true, lang: '' },
|
|
1215
|
+
{ from: 'YAML', to: 'yam-el', whole: true, lang: '' },
|
|
1216
|
+
{ from: 'HTML', to: 'H T M L', whole: true, lang: '' },
|
|
1217
|
+
{ from: 'CSS', to: 'C S S', whole: true, lang: '' },
|
|
1218
|
+
{ from: 'SSH', to: 'S S H', whole: true, lang: '' },
|
|
1219
|
+
{ from: 'DNS', to: 'D N S', whole: true, lang: '' },
|
|
1220
|
+
{ from: 'IP', to: 'I P', whole: true, lang: '' },
|
|
1221
|
+
]
|
|
1098
1222
|
|
|
1099
1223
|
// Pronunciation dictionary editor: rules apply top-down.
|
|
1100
1224
|
function PronEditor(props) {
|
|
@@ -1426,7 +1550,7 @@ async function pollPending() {
|
|
|
1426
1550
|
{ style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1427
1551
|
React.createElement('span', { className: 'dts-title' }, '🔄 ' + t('updaterTitle')),
|
|
1428
1552
|
React.createElement('span', { className: 'dts-badge ' + (updateAvailable ? 'dts-badge-warn' : 'dts-badge-on') },
|
|
1429
|
-
updateAvailable ? 'v' + latestVer + ' available' : 'v' + currentVer + ' (' + t('updaterUpToDate') + ')'
|
|
1553
|
+
updateAvailable ? 'v' + latestVer + ' ' + (t('available') || 'available') : 'v' + currentVer + ' (' + t('updaterUpToDate') + ')'
|
|
1430
1554
|
)
|
|
1431
1555
|
),
|
|
1432
1556
|
React.createElement(
|
|
@@ -1542,8 +1666,10 @@ async function pollPending() {
|
|
|
1542
1666
|
// ready — values are present.
|
|
1543
1667
|
// On unavailable, writable defaults true; without a status check the card
|
|
1544
1668
|
// looks like a working empty form.
|
|
1545
|
-
|
|
1546
|
-
|
|
1669
|
+
// Snapshot status contract (#163):
|
|
1670
|
+
// Without valid scope, or when loading/unavailable, editing is disabled.
|
|
1671
|
+
const ready = !!scope && !!snap && snap.status === 'ready'
|
|
1672
|
+
const writable = ready && snap.writable !== false
|
|
1547
1673
|
|
|
1548
1674
|
const applyPayload = (data) => {
|
|
1549
1675
|
const cfg = data && data.config ? data.config : {}
|
|
@@ -1593,6 +1719,11 @@ async function pollPending() {
|
|
|
1593
1719
|
return () => { alive = false; clearInterval(telTimer) }
|
|
1594
1720
|
}, [])
|
|
1595
1721
|
|
|
1722
|
+
if (err && !draft) {
|
|
1723
|
+
return React.createElement('div', { className: 'dts-wrap dts-warn' },
|
|
1724
|
+
React.createElement('span', null, (t('errorLoading') || 'Error loading settings: ') + err),
|
|
1725
|
+
)
|
|
1726
|
+
}
|
|
1596
1727
|
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
1597
1728
|
|
|
1598
1729
|
const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
|
|
@@ -1668,15 +1799,7 @@ async function pollPending() {
|
|
|
1668
1799
|
}
|
|
1669
1800
|
|
|
1670
1801
|
const onPreview = (provider, model, voice, text) => {
|
|
1671
|
-
|
|
1672
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1673
|
-
body: JSON.stringify({ provider, model, voice, text }),
|
|
1674
|
-
})
|
|
1675
|
-
.then((r) => r.json())
|
|
1676
|
-
.then((d) => {
|
|
1677
|
-
if (!d.ok) throw new Error((d.error && d.error.message) || 'HTTP error')
|
|
1678
|
-
new Audio('data:' + d.mime + ';base64,' + d.audioBase64).play().catch(() => { /* autoplay blocked */ })
|
|
1679
|
-
})
|
|
1802
|
+
previewSpeech(provider, model, voice, text)
|
|
1680
1803
|
.catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
1681
1804
|
}
|
|
1682
1805
|
|
|
@@ -1686,9 +1809,9 @@ async function pollPending() {
|
|
|
1686
1809
|
onPreview((first && first.provider) || 'kokoro', (first && first.model) || '', (first && first.voice) || '', phrase)
|
|
1687
1810
|
}
|
|
1688
1811
|
|
|
1689
|
-
const boolField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
1812
|
+
const boolField = (key, label, hint, disabled = false) => React.createElement('label', { className: 'dts-field' }, label,
|
|
1690
1813
|
React.createElement('input', {
|
|
1691
|
-
type: 'checkbox', checked: !!(draft && draft[key]), disabled: !writable,
|
|
1814
|
+
type: 'checkbox', checked: !!(draft && draft[key]), disabled: !writable || !!disabled,
|
|
1692
1815
|
onChange: (e) => setTop(key, e.target.checked),
|
|
1693
1816
|
}),
|
|
1694
1817
|
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
@@ -1827,7 +1950,7 @@ async function pollPending() {
|
|
|
1827
1950
|
React.createElement('select', {
|
|
1828
1951
|
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1829
1952
|
onChange: (e) => setTop('longReply', e.target.value),
|
|
1830
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1953
|
+
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, t('longReply' + mode.charAt(0).toUpperCase() + mode.slice(1)) || mode))),
|
|
1831
1954
|
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1832
1955
|
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1833
1956
|
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
@@ -1929,13 +2052,17 @@ async function pollPending() {
|
|
|
1929
2052
|
const [showList, setShowList] = React.useState(false)
|
|
1930
2053
|
const active = !!p.audio || p.queue.length > 0 || p.busy
|
|
1931
2054
|
const [, force] = React.useReducer((n) => n + 1, 0)
|
|
1932
|
-
if (!active && !showList) return null
|
|
1933
2055
|
const lists = recent.list()
|
|
2056
|
+
const hasHistory = (lists.items && lists.items.length > 0) || (lists.favs && lists.favs.length > 0)
|
|
2057
|
+
|
|
2058
|
+
// Hide only if completely idle and no recent/favorites history (#161)
|
|
2059
|
+
if (!active && !hasHistory && !showList && !p.blocked) return null
|
|
2060
|
+
|
|
1934
2061
|
const row = (text, starred) => React.createElement('div', { className: 'dts-row', key: text.slice(0, 24) + starred },
|
|
1935
2062
|
React.createElement('button', {
|
|
1936
2063
|
type: 'button', className: 'dts-link', style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
1937
2064
|
title: text,
|
|
1938
|
-
onClick: () => playStandalone(text),
|
|
2065
|
+
onClick: () => playStandalone(text),
|
|
1939
2066
|
}, text.slice(0, 80)),
|
|
1940
2067
|
React.createElement('button', {
|
|
1941
2068
|
type: 'button', className: 'dts-link', title: '★',
|
|
@@ -1947,10 +2074,14 @@ async function pollPending() {
|
|
|
1947
2074
|
}, '⤓'),
|
|
1948
2075
|
)
|
|
1949
2076
|
return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
|
|
1950
|
-
React.createElement('button', {
|
|
2077
|
+
p.blocked ? React.createElement('button', {
|
|
2078
|
+
type: 'button', className: 'dts-link dts-warn', title: t('unlockAudio') || 'Click to unlock audio',
|
|
2079
|
+
onClick: unlockAudio,
|
|
2080
|
+
}, '🔔') : null,
|
|
2081
|
+
(hasHistory || active) ? React.createElement('button', {
|
|
1951
2082
|
type: 'button', className: 'dts-link', title: t('recent') || 'Recent',
|
|
1952
2083
|
onClick: () => setShowList((v) => !v),
|
|
1953
|
-
}, '☰'),
|
|
2084
|
+
}, '☰') : null,
|
|
1954
2085
|
showList ? React.createElement('div', {
|
|
1955
2086
|
className: 'dts-wrap',
|
|
1956
2087
|
style: { position: 'absolute', bottom: '36px', right: 0, zIndex: 30, maxHeight: '320px', overflowY: 'auto', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '12px', padding: '10px 12px', minWidth: '260px', flexDirection: 'column', gap: '6px', display: 'flex' },
|
|
@@ -1961,24 +2092,32 @@ async function pollPending() {
|
|
|
1961
2092
|
lists.items.filter((x) => !lists.favs.includes(x)).map((text) => row(text, false)),
|
|
1962
2093
|
!lists.items.length ? React.createElement('span', { className: 'dts-sub' }, t('recentEmpty') || 'No recent speech yet') : null,
|
|
1963
2094
|
) : null,
|
|
1964
|
-
React.createElement(
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2095
|
+
active ? React.createElement(React.Fragment, null,
|
|
2096
|
+
React.createElement('button', {
|
|
2097
|
+
type: 'button',
|
|
2098
|
+
className: 'dts-link',
|
|
2099
|
+
title: p.paused ? t('dockResume') : t('dockPause'),
|
|
2100
|
+
onClick: togglePause,
|
|
2101
|
+
}, p.paused ? '▶' : '❚❚'),
|
|
2102
|
+
React.createElement('button', {
|
|
2103
|
+
type: 'button',
|
|
2104
|
+
className: 'dts-link',
|
|
2105
|
+
title: t('dockStop'),
|
|
2106
|
+
onClick: stopPlayback,
|
|
2107
|
+
}, '■'),
|
|
2108
|
+
React.createElement('button', {
|
|
2109
|
+
type: 'button',
|
|
2110
|
+
className: 'dts-link',
|
|
2111
|
+
title: t('dockSkip') || 'Skip',
|
|
2112
|
+
onClick: skipCurrent,
|
|
2113
|
+
}, '⏭'),
|
|
2114
|
+
React.createElement('button', {
|
|
2115
|
+
type: 'button',
|
|
2116
|
+
className: 'dts-link',
|
|
2117
|
+
title: t('exportAudio') || 'Export clip',
|
|
2118
|
+
onClick: () => exportAudioClip(player.lastText),
|
|
2119
|
+
}, '⤓'),
|
|
2120
|
+
) : null,
|
|
1982
2121
|
)
|
|
1983
2122
|
}
|
|
1984
2123
|
|
|
@@ -2071,7 +2210,7 @@ async function pollPending() {
|
|
|
2071
2210
|
name: 'plugins.item',
|
|
2072
2211
|
id: ROW_ID,
|
|
2073
2212
|
order: 60,
|
|
2074
|
-
label: () => 'Text to Speech',
|
|
2213
|
+
label: () => fallbackDockText('title') || 'Text to Speech',
|
|
2075
2214
|
locale: NS,
|
|
2076
2215
|
inject: () => ({ ctx: ctx }),
|
|
2077
2216
|
},
|
|
@@ -2109,49 +2248,7 @@ async function pollPending() {
|
|
|
2109
2248
|
window.addEventListener('keydown', onKey)
|
|
2110
2249
|
return () => window.removeEventListener('keydown', onKey)
|
|
2111
2250
|
}, 'dsh-tts: hotkeys')
|
|
2112
|
-
ctx.effect(() =>
|
|
2113
|
-
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
2114
|
-
let es = null
|
|
2115
|
-
let closed = false
|
|
2116
|
-
let attempt = 0
|
|
2117
|
-
let retryTimer = null
|
|
2118
|
-
const onItem = (e) => {
|
|
2119
|
-
try {
|
|
2120
|
-
const item = JSON.parse(e.data)
|
|
2121
|
-
if (!item || !item.id) return
|
|
2122
|
-
if (seenIds.has(item.id)) return
|
|
2123
|
-
seenIds.add(item.id)
|
|
2124
|
-
if (seenIds.size > 500) {
|
|
2125
|
-
const first = seenIds.values().next().value
|
|
2126
|
-
seenIds.delete(first)
|
|
2127
|
-
}
|
|
2128
|
-
player.queue.push(item)
|
|
2129
|
-
drainQueue()
|
|
2130
|
-
} catch { /* ignore malformed SSE frame */ }
|
|
2131
|
-
}
|
|
2132
|
-
const connect = () => {
|
|
2133
|
-
if (closed) return
|
|
2134
|
-
try {
|
|
2135
|
-
es = new EventSource('/dsh-tts/stream')
|
|
2136
|
-
es.addEventListener('open', () => { attempt = 0 })
|
|
2137
|
-
es.addEventListener('utterance', onItem)
|
|
2138
|
-
es.addEventListener('chime', onItem)
|
|
2139
|
-
es.onerror = () => {
|
|
2140
|
-
try { es.close() } catch { /* already closed */ }
|
|
2141
|
-
if (closed) return
|
|
2142
|
-
attempt += 1
|
|
2143
|
-
const delay = Math.min(15000, 500 * Math.pow(2, Math.min(attempt, 5)))
|
|
2144
|
-
retryTimer = setTimeout(connect, delay)
|
|
2145
|
-
}
|
|
2146
|
-
} catch { /* EventSource constructor failed — poll remains */ }
|
|
2147
|
-
}
|
|
2148
|
-
connect()
|
|
2149
|
-
return () => {
|
|
2150
|
-
closed = true
|
|
2151
|
-
if (retryTimer) clearTimeout(retryTimer)
|
|
2152
|
-
if (es) { try { es.close() } catch { /* already closed */ } }
|
|
2153
|
-
}
|
|
2154
|
-
}, 'dsh-tts: SSE audio stream')
|
|
2251
|
+
ctx.effect(() => startSseStream(), 'dsh-tts: SSE audio stream')
|
|
2155
2252
|
ctx.effect(() => {
|
|
2156
2253
|
const timer = setInterval(pollPending, 1000)
|
|
2157
2254
|
return () => clearInterval(timer)
|