@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/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 set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r], patch) }))
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
- ROLE_KEYS.map((r) => React.createElement('div', { className: 'dts-row', key: r },
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}){ this.daemonPath=daemonPath }
3
- async ping(){ return true }
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
  }
@@ -1,4 +1,31 @@
1
+ import fs from 'node:fs'
2
+
1
3
  export class KokoroEngine {
2
- constructor({modelPath}){ this.modelPath=modelPath }
3
- async *synthesize(text, voice){ yield new Float32Array(24000) }
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
  }