@goodandready/dsh-tts 0.3.16 → 0.3.21
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/cache.js +6 -1
- package/lib/client/worklet.js +68 -1
- package/lib/client.js +427 -16
- package/lib/engines/f5.js +43 -2
- package/lib/engines/kokoro.js +68 -2
- package/lib/engines/manager.js +251 -0
- package/lib/engines/types.js +47 -2
- package/lib/index.js +230 -19
- package/lib/keys.js +9 -0
- package/lib/providers.js +34 -1
- package/lib/text.js +32 -3
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -93,6 +93,8 @@ window.__ModuleLoader__.load({
|
|
|
93
93
|
paused: false,
|
|
94
94
|
listeners: new Set(),
|
|
95
95
|
}
|
|
96
|
+
const seenIds = new Set()
|
|
97
|
+
let currentAudioResolve = null
|
|
96
98
|
|
|
97
99
|
function playerChanged() {
|
|
98
100
|
for (const listener of [...player.listeners]) {
|
|
@@ -164,7 +166,17 @@ window.__ModuleLoader__.load({
|
|
|
164
166
|
audio.playbackRate = Math.max(0.5, Math.min(2, player.rate || 1))
|
|
165
167
|
player.audio = audio
|
|
166
168
|
playerChanged()
|
|
167
|
-
|
|
169
|
+
let doneCalled = false
|
|
170
|
+
const done = () => {
|
|
171
|
+
if (doneCalled) return
|
|
172
|
+
doneCalled = true
|
|
173
|
+
currentAudioResolve = null
|
|
174
|
+
URL.revokeObjectURL(url)
|
|
175
|
+
player.audio = null
|
|
176
|
+
playerChanged()
|
|
177
|
+
resolve()
|
|
178
|
+
}
|
|
179
|
+
currentAudioResolve = done
|
|
168
180
|
audio.onended = done
|
|
169
181
|
audio.onerror = done
|
|
170
182
|
audio.play().catch(() => {
|
|
@@ -172,7 +184,10 @@ window.__ModuleLoader__.load({
|
|
|
172
184
|
// поломкой, просто ничего не звучит.
|
|
173
185
|
done()
|
|
174
186
|
})
|
|
175
|
-
} catch (cannotDecode) {
|
|
187
|
+
} catch (cannotDecode) {
|
|
188
|
+
currentAudioResolve = null
|
|
189
|
+
resolve()
|
|
190
|
+
}
|
|
176
191
|
})
|
|
177
192
|
}
|
|
178
193
|
|
|
@@ -198,8 +213,12 @@ window.__ModuleLoader__.load({
|
|
|
198
213
|
function stopPlayback() {
|
|
199
214
|
player.queue.length = 0
|
|
200
215
|
player.paused = false
|
|
201
|
-
if (
|
|
202
|
-
|
|
216
|
+
if (currentAudioResolve) {
|
|
217
|
+
currentAudioResolve()
|
|
218
|
+
} else if (player.audio) {
|
|
219
|
+
try { player.audio.pause() } catch (already) { /* стоит */ }
|
|
220
|
+
player.audio = null
|
|
221
|
+
}
|
|
203
222
|
try { if (window.speechSynthesis) window.speechSynthesis.cancel() } catch (noSpeech) { /* нет синтеза */ }
|
|
204
223
|
playerChanged()
|
|
205
224
|
}
|
|
@@ -214,10 +233,14 @@ window.__ModuleLoader__.load({
|
|
|
214
233
|
}
|
|
215
234
|
|
|
216
235
|
function skipCurrent() {
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
236
|
+
if (currentAudioResolve) {
|
|
237
|
+
currentAudioResolve()
|
|
238
|
+
} else if (player.audio) {
|
|
239
|
+
try { player.audio.pause() } catch (already) { /* стоит */ }
|
|
240
|
+
player.audio = null
|
|
241
|
+
playerChanged()
|
|
242
|
+
if (player.queue.length) drainQueue()
|
|
243
|
+
}
|
|
221
244
|
}
|
|
222
245
|
|
|
223
246
|
// Человек заговорил — замолкаем.
|
|
@@ -258,6 +281,12 @@ window.__ModuleLoader__.load({
|
|
|
258
281
|
// опроса, иначе порядок фраз рассыплется.
|
|
259
282
|
if (item.kind === 'reserved') break
|
|
260
283
|
player.after = item.id
|
|
284
|
+
if (seenIds.has(item.id)) continue
|
|
285
|
+
seenIds.add(item.id)
|
|
286
|
+
if (seenIds.size > 500) {
|
|
287
|
+
const first = seenIds.values().next().value
|
|
288
|
+
seenIds.delete(first)
|
|
289
|
+
}
|
|
261
290
|
if (item.text) recent.add(item.text)
|
|
262
291
|
player.queue.push(item)
|
|
263
292
|
}
|
|
@@ -474,6 +503,49 @@ window.__ModuleLoader__.load({
|
|
|
474
503
|
'customKeyEnv': 'Custom key env',
|
|
475
504
|
'customKeyEnvHint': 'Credential name for the custom endpoint bearer key.',
|
|
476
505
|
|
|
506
|
+
'localEnginesTitle': 'Local Engines (Offline)',
|
|
507
|
+
'localEnginesHint': 'Run speech synthesis 100% offline without API keys.',
|
|
508
|
+
'enableLocalEngines': 'Enable local engines',
|
|
509
|
+
'enableLocalEnginesHint': 'Use Kokoro or F5-TTS when installed.',
|
|
510
|
+
'streamingEnabled': 'Instant streaming (< 300 ms)',
|
|
511
|
+
'streamingEnabledHint': 'Play audio via AudioWorklet and SSE in real-time.',
|
|
512
|
+
'useKokoro': 'Enable Kokoro-82M (CPU)',
|
|
513
|
+
'useKokoroHint': 'Fast neural synthesis on CPU.',
|
|
514
|
+
'useF5': 'Enable F5-TTS (GPU)',
|
|
515
|
+
'useF5Hint': 'High-fidelity zero-shot synthesis on NVIDIA GPU.',
|
|
516
|
+
'installed': 'Installed',
|
|
517
|
+
'notInstalled': 'Not installed',
|
|
518
|
+
'downloading': 'Downloading',
|
|
519
|
+
'installKokoro': 'Install (350 MB)',
|
|
520
|
+
'installF5': 'Install (2.1 GB)',
|
|
521
|
+
'deleteModel': 'Delete',
|
|
522
|
+
|
|
523
|
+
'voiceDuplexTitle': 'Voice Duplex (Voice-to-Voice)',
|
|
524
|
+
'voiceDuplexHint': 'Interactive voice conversation integrated with dsh-voice.',
|
|
525
|
+
'voiceNotInstalledTitle': 'Required plugin @goodandready/dsh-voice is not installed',
|
|
526
|
+
'voiceInstallHint': 'To enable voice duplex, install the plugin with',
|
|
527
|
+
'voiceDuplex': 'Voice-to-Voice mode',
|
|
528
|
+
'voiceDuplexHintToggle': 'Automatically synthesize agent reply when finished talking.',
|
|
529
|
+
'vadBargeIn': 'VAD Barge-in',
|
|
530
|
+
'vadBargeInHint': 'Mute speech synthesis immediately when user voice is detected.',
|
|
531
|
+
'newSubagentRole': 'Subagent name (e.g. coder, reviewer)',
|
|
532
|
+
'addSubagentRole': 'Add Subagent',
|
|
533
|
+
'role_coder': 'Coder',
|
|
534
|
+
'role_reviewer': 'Reviewer',
|
|
535
|
+
'role_planner': 'Planner',
|
|
536
|
+
|
|
537
|
+
'enableItDictionary': 'Built-in IT dictionary',
|
|
538
|
+
'enableItDictionaryHint': 'Auto-correct pronunciation for SQL, Nginx, K8s, Docker, API, JSON, YAML, etc.',
|
|
539
|
+
'loadItDictionary': 'Populate IT terms',
|
|
540
|
+
'previewRule': 'Listen',
|
|
541
|
+
|
|
542
|
+
'messengerIntegrationTitle': 'Messenger Integration (Telegram, Discord)',
|
|
543
|
+
'messengerIntegrationHint': 'Voice replies and notes sent through @goodandready/dsh-messenger-gateway.',
|
|
544
|
+
'messengerNotInstalledTitle': 'Required plugin @goodandready/dsh-messenger-gateway is not installed',
|
|
545
|
+
'messengerInstallHint': 'To enable voice notes in messengers, install the plugin with',
|
|
546
|
+
'messengerTtsEnabled': 'Voice replies in messengers',
|
|
547
|
+
'messengerTtsEnabledHint': 'Allow messenger bridge to synthesize voice notes for agent replies.',
|
|
548
|
+
|
|
477
549
|
'dockLabel': 'Speech',
|
|
478
550
|
'dockPause': 'Pause',
|
|
479
551
|
'dockResume': 'Resume speech',
|
|
@@ -575,6 +647,131 @@ window.__ModuleLoader__.load({
|
|
|
575
647
|
'customBaseUrlHint': 'OpenAI-совместимый адрес, например http://localhost:8880/v1; пусто = выключен.',
|
|
576
648
|
'customKeyEnv': 'Имя ключа для endpoint',
|
|
577
649
|
'customKeyEnvHint': 'Имя credential для Bearer-ключа своего endpointа.',
|
|
650
|
+
|
|
651
|
+
'localEnginesTitle': 'Локальные движки (Offline)',
|
|
652
|
+
'localEnginesHint': 'Автономный синтез без API-ключей и интернета.',
|
|
653
|
+
'enableLocalEngines': 'Включить локальные движки',
|
|
654
|
+
'enableLocalEnginesHint': 'Использовать Kokoro или F5-TTS при наличии моделей.',
|
|
655
|
+
'streamingEnabled': 'Мгновенный потоковый звук (< 300 мс)',
|
|
656
|
+
'streamingEnabledHint': 'Воспроизводить звук через AudioWorklet и SSE без задержек.',
|
|
657
|
+
'useKokoro': 'Использовать Kokoro-82M (CPU)',
|
|
658
|
+
'useKokoroHint': 'Быстрый нейросинтез на процессоре.',
|
|
659
|
+
'useF5': 'Использовать F5-TTS (GPU)',
|
|
660
|
+
'useF5Hint': 'Высокоточный синтез на видеокарте NVIDIA.',
|
|
661
|
+
'installed': 'Установлена',
|
|
662
|
+
'notInstalled': 'Не установлена',
|
|
663
|
+
'downloading': 'Загрузка',
|
|
664
|
+
'installKokoro': 'Установить (350 МБ)',
|
|
665
|
+
'installF5': 'Установить (2.1 ГБ)',
|
|
666
|
+
'deleteModel': 'Удалить',
|
|
667
|
+
|
|
668
|
+
'voiceDuplexTitle': 'Голосовой диалог (Voice-to-Voice)',
|
|
669
|
+
'voiceDuplexHint': 'Интерактивный голосовой разговор при наличии плагина dsh-voice.',
|
|
670
|
+
'voiceNotInstalledTitle': 'Плагин @goodandready/dsh-voice не установлен',
|
|
671
|
+
'voiceInstallHint': 'Для включения голосового диалога установите плагин командой',
|
|
672
|
+
'voiceDuplex': 'Voice-to-Voice режим',
|
|
673
|
+
'voiceDuplexHintToggle': 'Автоматически отвечать голосом после завершения голосовой реплики.',
|
|
674
|
+
'vadBargeIn': 'VAD Barge-in',
|
|
675
|
+
'vadBargeInHint': 'Мгновенно глушить речь ассистента при обнаружении голоса пользователя.',
|
|
676
|
+
'newSubagentRole': 'Имя субагента (например, coder, reviewer)',
|
|
677
|
+
'addSubagentRole': 'Добавить субагента',
|
|
678
|
+
'role_coder': 'Кодер',
|
|
679
|
+
'role_reviewer': 'Ревьюер',
|
|
680
|
+
'role_planner': 'Планировщик',
|
|
681
|
+
|
|
682
|
+
'enableItDictionary': 'Предустановленный словарь IT-терминов',
|
|
683
|
+
'enableItDictionaryHint': 'Автоматически заменять SQL, Nginx, K8s, Docker, API, JSON, YAML и др. на правильное произношение.',
|
|
684
|
+
'loadItDictionary': 'Вставить IT-термины в таблицу',
|
|
685
|
+
'previewRule': 'Прослушать',
|
|
686
|
+
|
|
687
|
+
'messengerIntegrationTitle': 'Интеграция с мессенджерами (Telegram, Discord)',
|
|
688
|
+
'messengerIntegrationHint': 'Озвучивание ответов и голосовые заметки через @goodandready/dsh-messenger-gateway.',
|
|
689
|
+
'messengerNotInstalledTitle': 'Плагин @goodandready/dsh-messenger-gateway не установлен',
|
|
690
|
+
'messengerInstallHint': 'Для отправки голосовых сообщений в мессенджеры установите плагин командой',
|
|
691
|
+
'messengerTtsEnabled': 'Голосовые ответы в мессенджеры',
|
|
692
|
+
'messengerTtsEnabledHint': 'Разрешить мосту мессенджеров озвучивать ответы голосовыми заметками.',
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function LocalEnginesEditor(props) {
|
|
696
|
+
const t = props.t || ((key) => key)
|
|
697
|
+
const [models, setModels] = React.useState({})
|
|
698
|
+
const [loading, setLoading] = React.useState(false)
|
|
699
|
+
|
|
700
|
+
const loadStatus = React.useCallback(() => {
|
|
701
|
+
if (typeof fetch === 'undefined') return
|
|
702
|
+
fetch('/dsh-tts/models/status')
|
|
703
|
+
.then((r) => r.json())
|
|
704
|
+
.then((d) => { if (d && d.ok && d.models) setModels(d.models) })
|
|
705
|
+
.catch(() => {})
|
|
706
|
+
}, [])
|
|
707
|
+
|
|
708
|
+
React.useEffect(() => {
|
|
709
|
+
loadStatus()
|
|
710
|
+
const timer = setInterval(loadStatus, 3000)
|
|
711
|
+
return () => clearInterval(timer)
|
|
712
|
+
}, [loadStatus])
|
|
713
|
+
|
|
714
|
+
const install = (engine) => {
|
|
715
|
+
setLoading(true)
|
|
716
|
+
fetch('/dsh-tts/models/install', {
|
|
717
|
+
method: 'POST',
|
|
718
|
+
headers: { 'content-type': 'application/json' },
|
|
719
|
+
body: JSON.stringify({ engine }),
|
|
720
|
+
}).then(() => loadStatus()).finally(() => setLoading(false))
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const remove = (engine) => {
|
|
724
|
+
setLoading(true)
|
|
725
|
+
fetch('/dsh-tts/models/delete', {
|
|
726
|
+
method: 'DELETE',
|
|
727
|
+
headers: { 'content-type': 'application/json' },
|
|
728
|
+
body: JSON.stringify({ engine }),
|
|
729
|
+
}).then(() => loadStatus()).finally(() => setLoading(false))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const kokoro = models.kokoro || { installed: false, downloading: false, progress: 0 }
|
|
733
|
+
const f5 = models.f5 || { installed: false, downloading: false, progress: 0 }
|
|
734
|
+
|
|
735
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
736
|
+
React.createElement('div', { className: 'dts-h' }, t('localEnginesTitle')),
|
|
737
|
+
React.createElement('div', { className: 'dts-sub' }, t('localEnginesHint')),
|
|
738
|
+
props.boolField('enableLocalEngines', t('enableLocalEngines'), t('enableLocalEnginesHint')),
|
|
739
|
+
props.boolField('streamingEnabled', t('streamingEnabled'), t('streamingEnabledHint')),
|
|
740
|
+
React.createElement('div', { className: 'dts-entry' },
|
|
741
|
+
React.createElement('div', { className: 'dts-row' },
|
|
742
|
+
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'Kokoro-82M (CPU)'),
|
|
743
|
+
React.createElement('span', {
|
|
744
|
+
className: 'dts-badge' + (kokoro.installed ? ' dts-badge-on' : ''),
|
|
745
|
+
}, kokoro.downloading ? `${t('downloading')} ${kokoro.progress}%` : (kokoro.installed ? t('installed') : t('notInstalled'))),
|
|
746
|
+
!kokoro.installed && !kokoro.downloading ? React.createElement('button', {
|
|
747
|
+
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
748
|
+
onClick: () => install('kokoro'),
|
|
749
|
+
}, t('installKokoro')) : null,
|
|
750
|
+
kokoro.installed ? React.createElement('button', {
|
|
751
|
+
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
752
|
+
onClick: () => remove('kokoro'),
|
|
753
|
+
}, t('deleteModel')) : null,
|
|
754
|
+
),
|
|
755
|
+
kokoro.installed ? props.boolField('kokoroEnabled', t('useKokoro'), t('useKokoroHint')) : null,
|
|
756
|
+
),
|
|
757
|
+
React.createElement('div', { className: 'dts-entry' },
|
|
758
|
+
React.createElement('div', { className: 'dts-row' },
|
|
759
|
+
React.createElement('span', { className: 'dts-h', style: { minWidth: '150px' } }, 'F5-TTS (GPU)'),
|
|
760
|
+
React.createElement('span', {
|
|
761
|
+
className: 'dts-badge' + (f5.installed ? ' dts-badge-on' : ''),
|
|
762
|
+
}, f5.downloading ? `${t('downloading')} ${f5.progress}%` : (f5.installed ? t('installed') : t('notInstalled'))),
|
|
763
|
+
!f5.installed && !f5.downloading ? React.createElement('button', {
|
|
764
|
+
type: 'button', className: 'dts-save', disabled: !props.writable || loading,
|
|
765
|
+
onClick: () => install('f5'),
|
|
766
|
+
}, t('installF5')) : null,
|
|
767
|
+
f5.installed ? React.createElement('button', {
|
|
768
|
+
type: 'button', className: 'dts-link', disabled: !props.writable || loading,
|
|
769
|
+
onClick: () => remove('f5'),
|
|
770
|
+
}, t('deleteModel')) : null,
|
|
771
|
+
),
|
|
772
|
+
f5.installed ? props.boolField('f5Enabled', t('useF5'), t('useF5Hint')) : null,
|
|
773
|
+
),
|
|
774
|
+
)
|
|
578
775
|
}
|
|
579
776
|
|
|
580
777
|
function TtsSection(props) {
|
|
@@ -585,6 +782,7 @@ window.__ModuleLoader__.load({
|
|
|
585
782
|
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
586
783
|
const [saved, setSaved] = React.useState(false)
|
|
587
784
|
const [err, setErr] = React.useState('')
|
|
785
|
+
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
588
786
|
const writable = true
|
|
589
787
|
|
|
590
788
|
const applyPayload = (data) => {
|
|
@@ -600,6 +798,12 @@ window.__ModuleLoader__.load({
|
|
|
600
798
|
if (!alive) return
|
|
601
799
|
applyPayload(data)
|
|
602
800
|
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
801
|
+
|
|
802
|
+
fetch('/dsh-tts/integrations', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
803
|
+
if (!alive) return
|
|
804
|
+
if (data && data.ok) setIntegrations(data)
|
|
805
|
+
}).catch(() => {})
|
|
806
|
+
|
|
603
807
|
return () => { alive = false }
|
|
604
808
|
}, [])
|
|
605
809
|
|
|
@@ -674,10 +878,10 @@ window.__ModuleLoader__.load({
|
|
|
674
878
|
clearCacheNow().catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
675
879
|
}
|
|
676
880
|
|
|
677
|
-
const onPreview = (provider, model, voice) => {
|
|
881
|
+
const onPreview = (provider, model, voice, text) => {
|
|
678
882
|
fetch('/dsh-tts/preview', {
|
|
679
883
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
680
|
-
body: JSON.stringify({ provider, model, voice }),
|
|
884
|
+
body: JSON.stringify({ provider, model, voice, text }),
|
|
681
885
|
})
|
|
682
886
|
.then((r) => r.json())
|
|
683
887
|
.then((d) => {
|
|
@@ -687,6 +891,12 @@ window.__ModuleLoader__.load({
|
|
|
687
891
|
.catch((e) => setErr(String(e && e.message ? e.message : e)))
|
|
688
892
|
}
|
|
689
893
|
|
|
894
|
+
const onPreviewPhrase = (phrase) => {
|
|
895
|
+
if (!phrase) return
|
|
896
|
+
const first = draft && draft.chain && draft.chain[0]
|
|
897
|
+
onPreview((first && first.provider) || 'kokoro', (first && first.model) || '', (first && first.voice) || '', phrase)
|
|
898
|
+
}
|
|
899
|
+
|
|
690
900
|
const boolField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
691
901
|
React.createElement('input', {
|
|
692
902
|
type: 'checkbox', checked: !!(draft && draft[key]), disabled: !writable,
|
|
@@ -726,12 +936,28 @@ window.__ModuleLoader__.load({
|
|
|
726
936
|
React.createElement('div', { className: 'dts-sub' },
|
|
727
937
|
t('speakRepliesHint')),
|
|
728
938
|
),
|
|
939
|
+
React.createElement(LocalEnginesEditor, {
|
|
940
|
+
t: t, writable: writable, draft: draft,
|
|
941
|
+
boolField: boolField, setTop: setTop,
|
|
942
|
+
}),
|
|
943
|
+
React.createElement(VoiceDuplexEditor, {
|
|
944
|
+
t: t, writable: writable, draft: draft,
|
|
945
|
+
boolField: boolField, setTop: setTop,
|
|
946
|
+
installed: !!(integrations && integrations.voice && integrations.voice.installed),
|
|
947
|
+
}),
|
|
948
|
+
React.createElement(MessengerIntegrationEditor, {
|
|
949
|
+
t: t, writable: writable, draft: draft,
|
|
950
|
+
boolField: boolField, setTop: setTop,
|
|
951
|
+
installed: !!(integrations && integrations.messenger && integrations.messenger.installed),
|
|
952
|
+
}),
|
|
729
953
|
React.createElement('div', { className: 'dts-block' },
|
|
730
954
|
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
|
955
|
+
boolField('enableItDictionary', t('enableItDictionary'), t('enableItDictionaryHint')),
|
|
731
956
|
React.createElement(PronEditor, {
|
|
732
957
|
t: t, writable: writable,
|
|
733
958
|
value: draft && draft.pronunciation ? draft.pronunciation : [],
|
|
734
959
|
onChange: (v) => setTop('pronunciation', v),
|
|
960
|
+
onPreviewPhrase: onPreviewPhrase,
|
|
735
961
|
}),
|
|
736
962
|
),
|
|
737
963
|
React.createElement('div', { className: 'dts-block' },
|
|
@@ -817,6 +1043,34 @@ window.__ModuleLoader__.load({
|
|
|
817
1043
|
}
|
|
818
1044
|
|
|
819
1045
|
|
|
1046
|
+
const CLIENT_IT_TERMS = [
|
|
1047
|
+
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
1048
|
+
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
1049
|
+
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1050
|
+
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1051
|
+
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
1052
|
+
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
1053
|
+
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
1054
|
+
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
1055
|
+
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
1056
|
+
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
1057
|
+
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
1058
|
+
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
1059
|
+
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
1060
|
+
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
1061
|
+
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
1062
|
+
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
1063
|
+
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
1064
|
+
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
1065
|
+
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
1066
|
+
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
1067
|
+
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
1068
|
+
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
1069
|
+
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
1070
|
+
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
1071
|
+
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1072
|
+
]
|
|
1073
|
+
|
|
820
1074
|
// Редактор словаря произношения: таблица правил сверху вниз.
|
|
821
1075
|
function PronEditor(props) {
|
|
822
1076
|
const t = props.t || ((key) => key)
|
|
@@ -826,6 +1080,12 @@ window.__ModuleLoader__.load({
|
|
|
826
1080
|
}
|
|
827
1081
|
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
828
1082
|
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
1083
|
+
const loadIt = () => {
|
|
1084
|
+
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
1085
|
+
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
1086
|
+
props.onChange(rows.concat(toAdd))
|
|
1087
|
+
}
|
|
1088
|
+
|
|
829
1089
|
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
830
1090
|
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
831
1091
|
React.createElement('input', {
|
|
@@ -849,26 +1109,48 @@ window.__ModuleLoader__.load({
|
|
|
849
1109
|
value: rule.lang || '', disabled: !props.writable,
|
|
850
1110
|
onChange: (e) => change(i, { lang: e.target.value }),
|
|
851
1111
|
}),
|
|
1112
|
+
React.createElement('button', {
|
|
1113
|
+
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
1114
|
+
onClick: () => {
|
|
1115
|
+
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
1116
|
+
},
|
|
1117
|
+
}, '\u25b6'),
|
|
852
1118
|
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
853
1119
|
)),
|
|
854
1120
|
React.createElement('div', { className: 'dts-row' },
|
|
855
1121
|
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
1122
|
+
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
856
1123
|
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
857
1124
|
),
|
|
858
1125
|
)
|
|
859
1126
|
}
|
|
860
1127
|
|
|
861
|
-
// Роли: переопределение провайдера/модели/голоса/сигнала, пусто — как у цепочки.
|
|
862
|
-
const ROLE_KEYS = ['reply', 'approval', 'error']
|
|
863
|
-
|
|
864
1128
|
function RolesEditor(props) {
|
|
865
1129
|
const t = props.t || ((key) => key)
|
|
866
1130
|
const value = props.value || {}
|
|
867
|
-
const
|
|
1131
|
+
const [newRole, setNewRole] = React.useState('')
|
|
1132
|
+
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
1133
|
+
const removeRole = (r) => {
|
|
1134
|
+
const next = Object.assign({}, value)
|
|
1135
|
+
delete next[r]
|
|
1136
|
+
props.onChange(next)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const standardKeys = ['reply', 'approval', 'error']
|
|
1140
|
+
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1141
|
+
const allKeys = [...standardKeys, ...customKeys]
|
|
1142
|
+
|
|
1143
|
+
const addCustom = () => {
|
|
1144
|
+
const name = newRole.trim().toLowerCase()
|
|
1145
|
+
if (!name || value[name]) return
|
|
1146
|
+
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1147
|
+
setNewRole('')
|
|
1148
|
+
}
|
|
1149
|
+
|
|
868
1150
|
return React.createElement('div', { className: 'dts-block' },
|
|
869
1151
|
React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
|
|
870
|
-
|
|
871
|
-
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r)),
|
|
1152
|
+
allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
|
|
1153
|
+
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
|
|
872
1154
|
React.createElement('select', {
|
|
873
1155
|
className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
|
|
874
1156
|
onChange: (e) => set(r, { provider: e.target.value }),
|
|
@@ -897,7 +1179,100 @@ window.__ModuleLoader__.load({
|
|
|
897
1179
|
onChange: (e) => set(r, { ssmlStyle: e.target.value }),
|
|
898
1180
|
}),
|
|
899
1181
|
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'),
|
|
1182
|
+
!standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
|
|
900
1183
|
)),
|
|
1184
|
+
React.createElement('div', { className: 'dts-row' },
|
|
1185
|
+
React.createElement('input', {
|
|
1186
|
+
className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
|
|
1187
|
+
value: newRole, disabled: !props.writable,
|
|
1188
|
+
onChange: (e) => setNewRole(e.target.value),
|
|
1189
|
+
onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
|
|
1190
|
+
}),
|
|
1191
|
+
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
|
|
1192
|
+
),
|
|
1193
|
+
)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function VoiceDuplexEditor(props) {
|
|
1197
|
+
const t = props.t || ((key) => key)
|
|
1198
|
+
const installed = !!props.installed
|
|
1199
|
+
|
|
1200
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1201
|
+
React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
|
|
1202
|
+
React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
|
|
1203
|
+
!installed ? React.createElement('div', {
|
|
1204
|
+
style: {
|
|
1205
|
+
padding: '10px 14px',
|
|
1206
|
+
borderRadius: '8px',
|
|
1207
|
+
border: '1px solid var(--dsw-alias-state-warning-primary, #eab308)',
|
|
1208
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1209
|
+
display: 'flex',
|
|
1210
|
+
flexDirection: 'column',
|
|
1211
|
+
gap: '4px',
|
|
1212
|
+
fontSize: '13px',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary, #eab308)' } },
|
|
1216
|
+
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1217
|
+
),
|
|
1218
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1219
|
+
t('voiceInstallHint') + ': '
|
|
1220
|
+
),
|
|
1221
|
+
React.createElement('code', {
|
|
1222
|
+
style: {
|
|
1223
|
+
fontFamily: 'monospace',
|
|
1224
|
+
fontSize: '12px',
|
|
1225
|
+
padding: '2px 6px',
|
|
1226
|
+
borderRadius: '4px',
|
|
1227
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1228
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1229
|
+
width: 'fit-content',
|
|
1230
|
+
},
|
|
1231
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1232
|
+
) : null,
|
|
1233
|
+
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1234
|
+
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1235
|
+
)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function MessengerIntegrationEditor(props) {
|
|
1239
|
+
const t = props.t || ((key) => key)
|
|
1240
|
+
const installed = !!props.installed
|
|
1241
|
+
|
|
1242
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1243
|
+
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1244
|
+
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1245
|
+
!installed ? React.createElement('div', {
|
|
1246
|
+
style: {
|
|
1247
|
+
padding: '10px 14px',
|
|
1248
|
+
borderRadius: '8px',
|
|
1249
|
+
border: '1px solid var(--dsw-alias-state-warning-primary, #eab308)',
|
|
1250
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1251
|
+
display: 'flex',
|
|
1252
|
+
flexDirection: 'column',
|
|
1253
|
+
gap: '4px',
|
|
1254
|
+
fontSize: '13px',
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary, #eab308)' } },
|
|
1258
|
+
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1259
|
+
),
|
|
1260
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1261
|
+
t('messengerInstallHint') + ': '
|
|
1262
|
+
),
|
|
1263
|
+
React.createElement('code', {
|
|
1264
|
+
style: {
|
|
1265
|
+
fontFamily: 'monospace',
|
|
1266
|
+
fontSize: '12px',
|
|
1267
|
+
padding: '2px 6px',
|
|
1268
|
+
borderRadius: '4px',
|
|
1269
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1270
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1271
|
+
width: 'fit-content',
|
|
1272
|
+
},
|
|
1273
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
|
|
1274
|
+
) : null,
|
|
1275
|
+
props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
|
|
901
1276
|
)
|
|
902
1277
|
}
|
|
903
1278
|
|
|
@@ -1106,6 +1481,42 @@ window.__ModuleLoader__.load({
|
|
|
1106
1481
|
window.addEventListener('keydown', onKey)
|
|
1107
1482
|
return () => window.removeEventListener('keydown', onKey)
|
|
1108
1483
|
}, 'dsh-tts: горячие клавиши')
|
|
1484
|
+
ctx.effect(() => {
|
|
1485
|
+
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
|
|
1486
|
+
let es = null
|
|
1487
|
+
try {
|
|
1488
|
+
es = new EventSource('/dsh-tts/stream')
|
|
1489
|
+
es.addEventListener('utterance', (e) => {
|
|
1490
|
+
try {
|
|
1491
|
+
const item = JSON.parse(e.data)
|
|
1492
|
+
if (!item || !item.id) return
|
|
1493
|
+
if (seenIds.has(item.id)) return
|
|
1494
|
+
seenIds.add(item.id)
|
|
1495
|
+
if (seenIds.size > 500) {
|
|
1496
|
+
const first = seenIds.values().next().value
|
|
1497
|
+
seenIds.delete(first)
|
|
1498
|
+
}
|
|
1499
|
+
player.queue.push(item)
|
|
1500
|
+
drainQueue()
|
|
1501
|
+
} catch {}
|
|
1502
|
+
})
|
|
1503
|
+
es.addEventListener('chime', (e) => {
|
|
1504
|
+
try {
|
|
1505
|
+
const item = JSON.parse(e.data)
|
|
1506
|
+
if (!item || !item.id) return
|
|
1507
|
+
if (seenIds.has(item.id)) return
|
|
1508
|
+
seenIds.add(item.id)
|
|
1509
|
+
if (seenIds.size > 500) {
|
|
1510
|
+
const first = seenIds.values().next().value
|
|
1511
|
+
seenIds.delete(first)
|
|
1512
|
+
}
|
|
1513
|
+
player.queue.push(item)
|
|
1514
|
+
drainQueue()
|
|
1515
|
+
} catch {}
|
|
1516
|
+
})
|
|
1517
|
+
} catch {}
|
|
1518
|
+
return () => { if (es) es.close() }
|
|
1519
|
+
}, 'dsh-tts: потоковое получение звука по SSE')
|
|
1109
1520
|
ctx.effect(() => {
|
|
1110
1521
|
const timer = setInterval(pollPending, 1000)
|
|
1111
1522
|
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
|
}
|