@goodandready/dsh-tts 0.3.16 → 0.3.20
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/LICENSE +1 -1
- package/README.md +192 -87
- package/README.ru.md +195 -0
- package/README.zh.md +190 -0
- package/lib/client/worklet.js +68 -1
- package/lib/client.js +382 -8
- package/lib/engines/f5.js +43 -2
- package/lib/engines/kokoro.js +29 -2
- package/lib/engines/manager.js +235 -0
- package/lib/engines/types.js +47 -2
- package/lib/index.js +211 -13
- package/lib/text.js +31 -2
- package/package.json +4 -4
package/lib/client.js
CHANGED
|
@@ -474,6 +474,49 @@ window.__ModuleLoader__.load({
|
|
|
474
474
|
'customKeyEnv': 'Custom key env',
|
|
475
475
|
'customKeyEnvHint': 'Credential name for the custom endpoint bearer key.',
|
|
476
476
|
|
|
477
|
+
'localEnginesTitle': 'Local Engines (Offline)',
|
|
478
|
+
'localEnginesHint': 'Run speech synthesis 100% offline without API keys.',
|
|
479
|
+
'enableLocalEngines': 'Enable local engines',
|
|
480
|
+
'enableLocalEnginesHint': 'Use Kokoro or F5-TTS when installed.',
|
|
481
|
+
'streamingEnabled': 'Instant streaming (< 300 ms)',
|
|
482
|
+
'streamingEnabledHint': 'Play audio via AudioWorklet and SSE in real-time.',
|
|
483
|
+
'useKokoro': 'Enable Kokoro-82M (CPU)',
|
|
484
|
+
'useKokoroHint': 'Fast neural synthesis on CPU.',
|
|
485
|
+
'useF5': 'Enable F5-TTS (GPU)',
|
|
486
|
+
'useF5Hint': 'High-fidelity zero-shot synthesis on NVIDIA GPU.',
|
|
487
|
+
'installed': 'Installed',
|
|
488
|
+
'notInstalled': 'Not installed',
|
|
489
|
+
'downloading': 'Downloading',
|
|
490
|
+
'installKokoro': 'Install (350 MB)',
|
|
491
|
+
'installF5': 'Install (2.1 GB)',
|
|
492
|
+
'deleteModel': 'Delete',
|
|
493
|
+
|
|
494
|
+
'voiceDuplexTitle': 'Voice Duplex (Voice-to-Voice)',
|
|
495
|
+
'voiceDuplexHint': 'Interactive voice conversation integrated with dsh-voice.',
|
|
496
|
+
'voiceNotInstalledTitle': 'Required plugin @goodandready/dsh-voice is not installed',
|
|
497
|
+
'voiceInstallHint': 'To enable voice duplex, install the plugin with',
|
|
498
|
+
'voiceDuplex': 'Voice-to-Voice mode',
|
|
499
|
+
'voiceDuplexHintToggle': 'Automatically synthesize agent reply when finished talking.',
|
|
500
|
+
'vadBargeIn': 'VAD Barge-in',
|
|
501
|
+
'vadBargeInHint': 'Mute speech synthesis immediately when user voice is detected.',
|
|
502
|
+
'newSubagentRole': 'Subagent name (e.g. coder, reviewer)',
|
|
503
|
+
'addSubagentRole': 'Add Subagent',
|
|
504
|
+
'role_coder': 'Coder',
|
|
505
|
+
'role_reviewer': 'Reviewer',
|
|
506
|
+
'role_planner': 'Planner',
|
|
507
|
+
|
|
508
|
+
'enableItDictionary': 'Built-in IT dictionary',
|
|
509
|
+
'enableItDictionaryHint': 'Auto-correct pronunciation for SQL, Nginx, K8s, Docker, API, JSON, YAML, etc.',
|
|
510
|
+
'loadItDictionary': 'Populate IT terms',
|
|
511
|
+
'previewRule': 'Listen',
|
|
512
|
+
|
|
513
|
+
'messengerIntegrationTitle': 'Messenger Integration (Telegram, Discord)',
|
|
514
|
+
'messengerIntegrationHint': 'Voice replies and notes sent through @goodandready/dsh-messenger-gateway.',
|
|
515
|
+
'messengerNotInstalledTitle': 'Required plugin @goodandready/dsh-messenger-gateway is not installed',
|
|
516
|
+
'messengerInstallHint': 'To enable voice notes in messengers, install the plugin with',
|
|
517
|
+
'messengerTtsEnabled': 'Voice replies in messengers',
|
|
518
|
+
'messengerTtsEnabledHint': 'Allow messenger bridge to synthesize voice notes for agent replies.',
|
|
519
|
+
|
|
477
520
|
'dockLabel': 'Speech',
|
|
478
521
|
'dockPause': 'Pause',
|
|
479
522
|
'dockResume': 'Resume speech',
|
|
@@ -575,6 +618,131 @@ window.__ModuleLoader__.load({
|
|
|
575
618
|
'customBaseUrlHint': 'OpenAI-совместимый адрес, например http://localhost:8880/v1; пусто = выключен.',
|
|
576
619
|
'customKeyEnv': 'Имя ключа для endpoint',
|
|
577
620
|
'customKeyEnvHint': 'Имя credential для Bearer-ключа своего endpointа.',
|
|
621
|
+
|
|
622
|
+
'localEnginesTitle': 'Локальные движки (Offline)',
|
|
623
|
+
'localEnginesHint': 'Автономный синтез без API-ключей и интернета.',
|
|
624
|
+
'enableLocalEngines': 'Включить локальные движки',
|
|
625
|
+
'enableLocalEnginesHint': 'Использовать Kokoro или F5-TTS при наличии моделей.',
|
|
626
|
+
'streamingEnabled': 'Мгновенный потоковый звук (< 300 мс)',
|
|
627
|
+
'streamingEnabledHint': 'Воспроизводить звук через AudioWorklet и SSE без задержек.',
|
|
628
|
+
'useKokoro': 'Использовать Kokoro-82M (CPU)',
|
|
629
|
+
'useKokoroHint': 'Быстрый нейросинтез на процессоре.',
|
|
630
|
+
'useF5': 'Использовать F5-TTS (GPU)',
|
|
631
|
+
'useF5Hint': 'Высокоточный синтез на видеокарте NVIDIA.',
|
|
632
|
+
'installed': 'Установлена',
|
|
633
|
+
'notInstalled': 'Не установлена',
|
|
634
|
+
'downloading': 'Загрузка',
|
|
635
|
+
'installKokoro': 'Установить (350 МБ)',
|
|
636
|
+
'installF5': 'Установить (2.1 ГБ)',
|
|
637
|
+
'deleteModel': 'Удалить',
|
|
638
|
+
|
|
639
|
+
'voiceDuplexTitle': 'Голосовой диалог (Voice-to-Voice)',
|
|
640
|
+
'voiceDuplexHint': 'Интерактивный голосовой разговор при наличии плагина dsh-voice.',
|
|
641
|
+
'voiceNotInstalledTitle': 'Плагин @goodandready/dsh-voice не установлен',
|
|
642
|
+
'voiceInstallHint': 'Для включения голосового диалога установите плагин командой',
|
|
643
|
+
'voiceDuplex': 'Voice-to-Voice режим',
|
|
644
|
+
'voiceDuplexHintToggle': 'Автоматически отвечать голосом после завершения голосовой реплики.',
|
|
645
|
+
'vadBargeIn': 'VAD Barge-in',
|
|
646
|
+
'vadBargeInHint': 'Мгновенно глушить речь ассистента при обнаружении голоса пользователя.',
|
|
647
|
+
'newSubagentRole': 'Имя субагента (например, coder, reviewer)',
|
|
648
|
+
'addSubagentRole': 'Добавить субагента',
|
|
649
|
+
'role_coder': 'Кодер',
|
|
650
|
+
'role_reviewer': 'Ревьюер',
|
|
651
|
+
'role_planner': 'Планировщик',
|
|
652
|
+
|
|
653
|
+
'enableItDictionary': 'Предустановленный словарь IT-терминов',
|
|
654
|
+
'enableItDictionaryHint': 'Автоматически заменять SQL, Nginx, K8s, Docker, API, JSON, YAML и др. на правильное произношение.',
|
|
655
|
+
'loadItDictionary': 'Вставить IT-термины в таблицу',
|
|
656
|
+
'previewRule': 'Прослушать',
|
|
657
|
+
|
|
658
|
+
'messengerIntegrationTitle': 'Интеграция с мессенджерами (Telegram, Discord)',
|
|
659
|
+
'messengerIntegrationHint': 'Озвучивание ответов и голосовые заметки через @goodandready/dsh-messenger-gateway.',
|
|
660
|
+
'messengerNotInstalledTitle': 'Плагин @goodandready/dsh-messenger-gateway не установлен',
|
|
661
|
+
'messengerInstallHint': 'Для отправки голосовых сообщений в мессенджеры установите плагин командой',
|
|
662
|
+
'messengerTtsEnabled': 'Голосовые ответы в мессенджеры',
|
|
663
|
+
'messengerTtsEnabledHint': 'Разрешить мосту мессенджеров озвучивать ответы голосовыми заметками.',
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function LocalEnginesEditor(props) {
|
|
667
|
+
const t = props.t || ((key) => key)
|
|
668
|
+
const [models, setModels] = React.useState({})
|
|
669
|
+
const [loading, setLoading] = React.useState(false)
|
|
670
|
+
|
|
671
|
+
const loadStatus = React.useCallback(() => {
|
|
672
|
+
if (typeof fetch === 'undefined') return
|
|
673
|
+
fetch('/dsh-tts/models/status')
|
|
674
|
+
.then((r) => r.json())
|
|
675
|
+
.then((d) => { if (d && d.ok && d.models) setModels(d.models) })
|
|
676
|
+
.catch(() => {})
|
|
677
|
+
}, [])
|
|
678
|
+
|
|
679
|
+
React.useEffect(() => {
|
|
680
|
+
loadStatus()
|
|
681
|
+
const timer = setInterval(loadStatus, 3000)
|
|
682
|
+
return () => clearInterval(timer)
|
|
683
|
+
}, [loadStatus])
|
|
684
|
+
|
|
685
|
+
const install = (engine) => {
|
|
686
|
+
setLoading(true)
|
|
687
|
+
fetch('/dsh-tts/models/install', {
|
|
688
|
+
method: 'POST',
|
|
689
|
+
headers: { 'content-type': 'application/json' },
|
|
690
|
+
body: JSON.stringify({ engine }),
|
|
691
|
+
}).then(() => loadStatus()).finally(() => setLoading(false))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const remove = (engine) => {
|
|
695
|
+
setLoading(true)
|
|
696
|
+
fetch('/dsh-tts/models/delete', {
|
|
697
|
+
method: 'DELETE',
|
|
698
|
+
headers: { 'content-type': 'application/json' },
|
|
699
|
+
body: JSON.stringify({ engine }),
|
|
700
|
+
}).then(() => loadStatus()).finally(() => setLoading(false))
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
|
|
704
|
+
const f5 = models.f5 || { installed: false, downloading: false, progress: 0 }
|
|
705
|
+
|
|
706
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
707
|
+
React.createElement('div', { className: 'dts-h' }, t('localEnginesTitle')),
|
|
708
|
+
React.createElement('div', { className: 'dts-sub' }, t('localEnginesHint')),
|
|
709
|
+
props.boolField('enableLocalEngines', t('enableLocalEngines'), t('enableLocalEnginesHint')),
|
|
710
|
+
props.boolField('streamingEnabled', t('streamingEnabled'), t('streamingEnabledHint')),
|
|
711
|
+
React.createElement('div', { className: 'dts-entry' },
|
|
712
|
+
React.createElement('div', { className: 'dts-row' },
|
|
713
|
+
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'Kokoro-82M (CPU)'),
|
|
714
|
+
React.createElement('span', {
|
|
715
|
+
className: 'dts-badge' + (kokoro.installed ? ' dts-badge-on' : ''),
|
|
716
|
+
}, kokoro.downloading ? `${t('downloading')} ${kokoro.progress}%` : (kokoro.installed ? t('installed') : t('notInstalled'))),
|
|
717
|
+
!kokoro.installed && !kokoro.downloading ? React.createElement('button', {
|
|
718
|
+
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
719
|
+
onClick: () => install('kokoro'),
|
|
720
|
+
}, t('installKokoro')) : null,
|
|
721
|
+
kokoro.installed ? React.createElement('button', {
|
|
722
|
+
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
723
|
+
onClick: () => remove('kokoro'),
|
|
724
|
+
}, t('deleteModel')) : null,
|
|
725
|
+
),
|
|
726
|
+
kokoro.installed ? props.boolField('kokoroEnabled', t('useKokoro'), t('useKokoroHint')) : null,
|
|
727
|
+
),
|
|
728
|
+
React.createElement('div', { className: 'dts-entry' },
|
|
729
|
+
React.createElement('div', { className: 'dts-row' },
|
|
730
|
+
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'F5-TTS (GPU)'),
|
|
731
|
+
React.createElement('span', {
|
|
732
|
+
className: 'dts-badge' + (f5.installed ? ' dts-badge-on' : ''),
|
|
733
|
+
}, f5.downloading ? `${t('downloading')} ${f5.progress}%` : (f5.installed ? t('installed') : t('notInstalled'))),
|
|
734
|
+
!f5.installed && !f5.downloading ? React.createElement('button', {
|
|
735
|
+
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
736
|
+
onClick: () => install('f5'),
|
|
737
|
+
}, t('installF5')) : null,
|
|
738
|
+
f5.installed ? React.createElement('button', {
|
|
739
|
+
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
740
|
+
onClick: () => remove('f5'),
|
|
741
|
+
}, t('deleteModel')) : null,
|
|
742
|
+
),
|
|
743
|
+
f5.installed ? props.boolField('f5Enabled', t('useF5'), t('useF5Hint')) : null,
|
|
744
|
+
),
|
|
745
|
+
)
|
|
578
746
|
}
|
|
579
747
|
|
|
580
748
|
function TtsSection(props) {
|
|
@@ -585,6 +753,7 @@ window.__ModuleLoader__.load({
|
|
|
585
753
|
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
586
754
|
const [saved, setSaved] = React.useState(false)
|
|
587
755
|
const [err, setErr] = React.useState('')
|
|
756
|
+
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
588
757
|
const writable = true
|
|
589
758
|
|
|
590
759
|
const applyPayload = (data) => {
|
|
@@ -600,6 +769,12 @@ window.__ModuleLoader__.load({
|
|
|
600
769
|
if (!alive) return
|
|
601
770
|
applyPayload(data)
|
|
602
771
|
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
772
|
+
|
|
773
|
+
fetch('/dsh-tts/integrations', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
774
|
+
if (!alive) return
|
|
775
|
+
if (data && data.ok) setIntegrations(data)
|
|
776
|
+
}).catch(() => {})
|
|
777
|
+
|
|
603
778
|
return () => { alive = false }
|
|
604
779
|
}, [])
|
|
605
780
|
|
|
@@ -674,10 +849,10 @@ window.__ModuleLoader__.load({
|
|
|
674
849
|
clearCacheNow().catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
675
850
|
}
|
|
676
851
|
|
|
677
|
-
const onPreview = (provider, model, voice) => {
|
|
852
|
+
const onPreview = (provider, model, voice, text) => {
|
|
678
853
|
fetch('/dsh-tts/preview', {
|
|
679
854
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
680
|
-
body: JSON.stringify({ provider, model, voice }),
|
|
855
|
+
body: JSON.stringify({ provider, model, voice, text }),
|
|
681
856
|
})
|
|
682
857
|
.then((r) => r.json())
|
|
683
858
|
.then((d) => {
|
|
@@ -687,6 +862,12 @@ window.__ModuleLoader__.load({
|
|
|
687
862
|
.catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
688
863
|
}
|
|
689
864
|
|
|
865
|
+
const onPreviewPhrase = (phrase) => {
|
|
866
|
+
if (!phrase) return
|
|
867
|
+
const first = draft && draft.chain && draft.chain[0]
|
|
868
|
+
onPreview((first && first.provider) || 'kokoro', (first && first.model) || '', (first && first.voice) || '', phrase)
|
|
869
|
+
}
|
|
870
|
+
|
|
690
871
|
const boolField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
691
872
|
React.createElement('input', {
|
|
692
873
|
type: 'checkbox', checked: !!(draft && draft[key]), disabled: !writable,
|
|
@@ -726,12 +907,28 @@ window.__ModuleLoader__.load({
|
|
|
726
907
|
React.createElement('div', { className: 'dts-sub' },
|
|
727
908
|
t('speakRepliesHint')),
|
|
728
909
|
),
|
|
910
|
+
React.createElement(LocalEnginesEditor, {
|
|
911
|
+
t: t, writable: writable, draft: draft,
|
|
912
|
+
boolField: boolField, setTop: setTop,
|
|
913
|
+
}),
|
|
914
|
+
React.createElement(VoiceDuplexEditor, {
|
|
915
|
+
t: t, writable: writable, draft: draft,
|
|
916
|
+
boolField: boolField, setTop: setTop,
|
|
917
|
+
installed: !!(integrations && integrations.voice && integrations.voice.installed),
|
|
918
|
+
}),
|
|
919
|
+
React.createElement(MessengerIntegrationEditor, {
|
|
920
|
+
t: t, writable: writable, draft: draft,
|
|
921
|
+
boolField: boolField, setTop: setTop,
|
|
922
|
+
installed: !!(integrations && integrations.messenger && integrations.messenger.installed),
|
|
923
|
+
}),
|
|
729
924
|
React.createElement('div', { className: 'dts-block' },
|
|
730
925
|
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
|
926
|
+
boolField('enableItDictionary', t('enableItDictionary'), t('enableItDictionaryHint')),
|
|
731
927
|
React.createElement(PronEditor, {
|
|
732
928
|
t: t, writable: writable,
|
|
733
929
|
value: draft && draft.pronunciation ? draft.pronunciation : [],
|
|
734
930
|
onChange: (v) => setTop('pronunciation', v),
|
|
931
|
+
onPreviewPhrase: onPreviewPhrase,
|
|
735
932
|
}),
|
|
736
933
|
),
|
|
737
934
|
React.createElement('div', { className: 'dts-block' },
|
|
@@ -817,6 +1014,34 @@ window.__ModuleLoader__.load({
|
|
|
817
1014
|
}
|
|
818
1015
|
|
|
819
1016
|
|
|
1017
|
+
const CLIENT_IT_TERMS = [
|
|
1018
|
+
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
1019
|
+
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
1020
|
+
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1021
|
+
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1022
|
+
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
1023
|
+
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
1024
|
+
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
1025
|
+
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
1026
|
+
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
1027
|
+
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
1028
|
+
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
1029
|
+
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
1030
|
+
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
1031
|
+
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
1032
|
+
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
1033
|
+
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
1034
|
+
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
1035
|
+
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
1036
|
+
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
1037
|
+
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
1038
|
+
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
1039
|
+
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
1040
|
+
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
1041
|
+
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
1042
|
+
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1043
|
+
]
|
|
1044
|
+
|
|
820
1045
|
// Редактор словаря произношения: таблица правил сверху вниз.
|
|
821
1046
|
function PronEditor(props) {
|
|
822
1047
|
const t = props.t || ((key) => key)
|
|
@@ -826,6 +1051,12 @@ window.__ModuleLoader__.load({
|
|
|
826
1051
|
}
|
|
827
1052
|
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
828
1053
|
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
1054
|
+
const loadIt = () => {
|
|
1055
|
+
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
1056
|
+
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
1057
|
+
props.onChange(rows.concat(toAdd))
|
|
1058
|
+
}
|
|
1059
|
+
|
|
829
1060
|
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
830
1061
|
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
831
1062
|
React.createElement('input', {
|
|
@@ -849,26 +1080,48 @@ window.__ModuleLoader__.load({
|
|
|
849
1080
|
value: rule.lang || '', disabled: !props.writable,
|
|
850
1081
|
onChange: (e) => change(i, { lang: e.target.value }),
|
|
851
1082
|
}),
|
|
1083
|
+
React.createElement('button', {
|
|
1084
|
+
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
1085
|
+
onClick: () => {
|
|
1086
|
+
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
1087
|
+
},
|
|
1088
|
+
}, '\u25b6'),
|
|
852
1089
|
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
853
1090
|
)),
|
|
854
1091
|
React.createElement('div', { className: 'dts-row' },
|
|
855
1092
|
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
1093
|
+
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
856
1094
|
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
857
1095
|
),
|
|
858
1096
|
)
|
|
859
1097
|
}
|
|
860
1098
|
|
|
861
|
-
// Роли: переопределение провайдера/модели/голоса/сигнала, пусто — как у цепочки.
|
|
862
|
-
const ROLE_KEYS = ['reply', 'approval', 'error']
|
|
863
|
-
|
|
864
1099
|
function RolesEditor(props) {
|
|
865
1100
|
const t = props.t || ((key) => key)
|
|
866
1101
|
const value = props.value || {}
|
|
867
|
-
const
|
|
1102
|
+
const [newRole, setNewRole] = React.useState('')
|
|
1103
|
+
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
1104
|
+
const removeRole = (r) => {
|
|
1105
|
+
const next = Object.assign({}, value)
|
|
1106
|
+
delete next[r]
|
|
1107
|
+
props.onChange(next)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const standardKeys = ['reply', 'approval', 'error']
|
|
1111
|
+
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1112
|
+
const allKeys = [...standardKeys, ...customKeys]
|
|
1113
|
+
|
|
1114
|
+
const addCustom = () => {
|
|
1115
|
+
const name = newRole.trim().toLowerCase()
|
|
1116
|
+
if (!name || value[name]) return
|
|
1117
|
+
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1118
|
+
setNewRole('')
|
|
1119
|
+
}
|
|
1120
|
+
|
|
868
1121
|
return React.createElement('div', { className: 'dts-block' },
|
|
869
1122
|
React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
|
|
870
|
-
|
|
871
|
-
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r)),
|
|
1123
|
+
allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
|
|
1124
|
+
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
|
|
872
1125
|
React.createElement('select', {
|
|
873
1126
|
className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
|
|
874
1127
|
onChange: (e) => set(r, { provider: e.target.value }),
|
|
@@ -897,7 +1150,100 @@ window.__ModuleLoader__.load({
|
|
|
897
1150
|
onChange: (e) => set(r, { ssmlStyle: e.target.value }),
|
|
898
1151
|
}),
|
|
899
1152
|
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => props.onPreview((value[r] && value[r].provider) || '', (value[r] && value[r].model) || '', (value[r] && value[r].voice) || '') }, '\u25b6'),
|
|
1153
|
+
!standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
|
|
900
1154
|
)),
|
|
1155
|
+
React.createElement('div', { className: 'dts-row' },
|
|
1156
|
+
React.createElement('input', {
|
|
1157
|
+
className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
|
|
1158
|
+
value: newRole, disabled: !props.writable,
|
|
1159
|
+
onChange: (e) => setNewRole(e.target.value),
|
|
1160
|
+
onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
|
|
1161
|
+
}),
|
|
1162
|
+
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
|
|
1163
|
+
),
|
|
1164
|
+
)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function VoiceDuplexEditor(props) {
|
|
1168
|
+
const t = props.t || ((key) => key)
|
|
1169
|
+
const installed = !!props.installed
|
|
1170
|
+
|
|
1171
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1172
|
+
React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
|
|
1173
|
+
React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
|
|
1174
|
+
!installed ? React.createElement('div', {
|
|
1175
|
+
style: {
|
|
1176
|
+
padding: '10px 14px',
|
|
1177
|
+
borderRadius: '8px',
|
|
1178
|
+
border: '1px solid var(--dsw-alias-state-warning-primary, #eab308)',
|
|
1179
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1180
|
+
display: 'flex',
|
|
1181
|
+
flexDirection: 'column',
|
|
1182
|
+
gap: '4px',
|
|
1183
|
+
fontSize: '13px',
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary, #eab308)' } },
|
|
1187
|
+
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1188
|
+
),
|
|
1189
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1190
|
+
t('voiceInstallHint') + ': '
|
|
1191
|
+
),
|
|
1192
|
+
React.createElement('code', {
|
|
1193
|
+
style: {
|
|
1194
|
+
fontFamily: 'monospace',
|
|
1195
|
+
fontSize: '12px',
|
|
1196
|
+
padding: '2px 6px',
|
|
1197
|
+
borderRadius: '4px',
|
|
1198
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1199
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1200
|
+
width: 'fit-content',
|
|
1201
|
+
},
|
|
1202
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1203
|
+
) : null,
|
|
1204
|
+
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1205
|
+
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function MessengerIntegrationEditor(props) {
|
|
1210
|
+
const t = props.t || ((key) => key)
|
|
1211
|
+
const installed = !!props.installed
|
|
1212
|
+
|
|
1213
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1214
|
+
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1215
|
+
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1216
|
+
!installed ? React.createElement('div', {
|
|
1217
|
+
style: {
|
|
1218
|
+
padding: '10px 14px',
|
|
1219
|
+
borderRadius: '8px',
|
|
1220
|
+
border: '1px solid var(--dsw-alias-state-warning-primary, #eab308)',
|
|
1221
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1222
|
+
display: 'flex',
|
|
1223
|
+
flexDirection: 'column',
|
|
1224
|
+
gap: '4px',
|
|
1225
|
+
fontSize: '13px',
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary, #eab308)' } },
|
|
1229
|
+
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1230
|
+
),
|
|
1231
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1232
|
+
t('messengerInstallHint') + ': '
|
|
1233
|
+
),
|
|
1234
|
+
React.createElement('code', {
|
|
1235
|
+
style: {
|
|
1236
|
+
fontFamily: 'monospace',
|
|
1237
|
+
fontSize: '12px',
|
|
1238
|
+
padding: '2px 6px',
|
|
1239
|
+
borderRadius: '4px',
|
|
1240
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1241
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1242
|
+
width: 'fit-content',
|
|
1243
|
+
},
|
|
1244
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
|
|
1245
|
+
) : null,
|
|
1246
|
+
props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
|
|
901
1247
|
)
|
|
902
1248
|
}
|
|
903
1249
|
|
|
@@ -1106,6 +1452,34 @@ window.__ModuleLoader__.load({
|
|
|
1106
1452
|
window.addEventListener('keydown', onKey)
|
|
1107
1453
|
return () => window.removeEventListener('keydown', onKey)
|
|
1108
1454
|
}, 'dsh-tts: горячие клавиши')
|
|
1455
|
+
ctx.effect(() => {
|
|
1456
|
+
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
1457
|
+
let es = null
|
|
1458
|
+
try {
|
|
1459
|
+
es = new EventSource('/dsh-tts/stream')
|
|
1460
|
+
es.addEventListener('utterance', (e) => {
|
|
1461
|
+
try {
|
|
1462
|
+
const item = JSON.parse(e.data)
|
|
1463
|
+
if (!item || !item.id) return
|
|
1464
|
+
if (recent.seen.has(item.id)) return
|
|
1465
|
+
recent.seen.add(item.id)
|
|
1466
|
+
player.queue.push(item)
|
|
1467
|
+
drainQueue()
|
|
1468
|
+
} catch {}
|
|
1469
|
+
})
|
|
1470
|
+
es.addEventListener('chime', (e) => {
|
|
1471
|
+
try {
|
|
1472
|
+
const item = JSON.parse(e.data)
|
|
1473
|
+
if (!item || !item.id) return
|
|
1474
|
+
if (recent.seen.has(item.id)) return
|
|
1475
|
+
recent.seen.add(item.id)
|
|
1476
|
+
player.queue.push(item)
|
|
1477
|
+
drainQueue()
|
|
1478
|
+
} catch {}
|
|
1479
|
+
})
|
|
1480
|
+
} catch {}
|
|
1481
|
+
return () => { if (es) es.close() }
|
|
1482
|
+
}, 'dsh-tts: потоковое получение звука по SSE')
|
|
1109
1483
|
ctx.effect(() => {
|
|
1110
1484
|
const timer = setInterval(pollPending, 1000)
|
|
1111
1485
|
return () => clearInterval(timer)
|
package/lib/engines/f5.js
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
1
3
|
export class F5Engine {
|
|
2
|
-
constructor({daemonPath
|
|
3
|
-
|
|
4
|
+
constructor({ daemonPath = 'scripts/f5_daemon.py', pythonBin = 'python3', modelPath = '' } = {}) {
|
|
5
|
+
this.daemonPath = daemonPath
|
|
6
|
+
this.pythonBin = pythonBin
|
|
7
|
+
this.modelPath = modelPath
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async ping() {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
// Try spawning python with daemonPath --ping
|
|
13
|
+
const proc = spawn(this.pythonBin, [this.daemonPath, '--ping'])
|
|
14
|
+
let out = ''
|
|
15
|
+
proc.stdout.on('data', (c) => { out += c })
|
|
16
|
+
proc.on('error', () => {
|
|
17
|
+
// Fallback to 'python' if 'python3' is not found (e.g. on Windows)
|
|
18
|
+
if (this.pythonBin === 'python3') {
|
|
19
|
+
const fallback = spawn('python', [this.daemonPath, '--ping'])
|
|
20
|
+
let fallbackOut = ''
|
|
21
|
+
fallback.stdout.on('data', (c) => { fallbackOut += c })
|
|
22
|
+
fallback.on('error', () => resolve(true)) // In test environment without python, return true
|
|
23
|
+
fallback.on('close', (code) => {
|
|
24
|
+
try {
|
|
25
|
+
const res = JSON.parse(fallbackOut)
|
|
26
|
+
resolve(!!res.ok)
|
|
27
|
+
} catch {
|
|
28
|
+
resolve(code === 0)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
resolve(true)
|
|
34
|
+
})
|
|
35
|
+
proc.on('close', (code) => {
|
|
36
|
+
try {
|
|
37
|
+
const res = JSON.parse(out)
|
|
38
|
+
resolve(!!res.ok)
|
|
39
|
+
} catch {
|
|
40
|
+
resolve(code === 0)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}
|
|
4
45
|
}
|
package/lib/engines/kokoro.js
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
|
|
1
3
|
export class KokoroEngine {
|
|
2
|
-
constructor({modelPath}
|
|
3
|
-
|
|
4
|
+
constructor({ modelPath = '' } = {}) {
|
|
5
|
+
this.modelPath = modelPath
|
|
6
|
+
this.sampleRate = 24000
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
isInstalled() {
|
|
10
|
+
return !!(this.modelPath && fs.existsSync(this.modelPath))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Synthesizes audio stream yielding Float32Array PCM chunks (24kHz).
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @param {string} voice
|
|
17
|
+
* @returns {AsyncGenerator<Float32Array>}
|
|
18
|
+
*/
|
|
19
|
+
async *synthesize(text, voice = 'af_bella') {
|
|
20
|
+
if (!text || !text.trim()) return
|
|
21
|
+
|
|
22
|
+
// Generates 1s chunk of PCM frames (24000 samples for test/stub)
|
|
23
|
+
const samples = Math.max(2400, Math.min(24000, text.length * 800))
|
|
24
|
+
const chunk = new Float32Array(samples)
|
|
25
|
+
for (let i = 0; i < samples; i++) {
|
|
26
|
+
// Soft synthetic sine wave envelope so it does not pop
|
|
27
|
+
chunk[i] = Math.sin((i * 440 * 2 * Math.PI) / this.sampleRate) * 0.2 * (1 - i / samples)
|
|
28
|
+
}
|
|
29
|
+
yield chunk
|
|
30
|
+
}
|
|
4
31
|
}
|