@goodandready/dsh-voice 0.6.1 → 0.7.1

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/cordis.patch.yml CHANGED
@@ -10,8 +10,12 @@
10
10
  #
11
11
  # - id: dsh-voice
12
12
  # config:
13
- # whisperBin: /opt/whisper.cpp/build/bin/whisper-server
14
- # whisperModel: /opt/whisper.cpp/models/ggml-medium-q8_0.bin
13
+ # whisperBin: /path/to/whisper-server
14
+ # whisperModel: /path/to/ggml-model.bin
15
+ #
16
+ # Both are absolute paths on the machine that runs the harness, and both are
17
+ # yours to fill in: the package cannot know where a whisper.cpp build and its
18
+ # model happen to live.
15
19
  #
16
20
  - insert:
17
21
  - id: dsh-voice
package/lib/client.js CHANGED
@@ -125,6 +125,27 @@ window.__ModuleLoader__.load({
125
125
  actions.setDraft(draft ? draft + ' ' + text : text)
126
126
  }
127
127
 
128
+ // Как клавиша называется на человеческом языке.
129
+ const KEY_LABELS = {
130
+ Control: 'Ctrl', Alt: 'Alt', Shift: 'Shift', Meta: 'Win',
131
+ Space: 'Пробел', Escape: 'Esc',
132
+ }
133
+
134
+ function keyLabel(name) {
135
+ if (!name) return 'не задана'
136
+ if (KEY_LABELS[name]) return KEY_LABELS[name]
137
+ // Коды вида KeyR и Digit5 показываем без служебной приставки.
138
+ return String(name).replace(/^Key/, '').replace(/^Digit/, '')
139
+ }
140
+
141
+ // Что записать в настройку по нажатию. Чистый модификатор запоминаем по
142
+ // имени: код у левого и правого разный, а человек нажимает «какой-нибудь».
143
+ function keyFromEvent(event) {
144
+ if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return event.key
145
+ if (event.code) return event.code
146
+ return event.key || ''
147
+ }
148
+
128
149
  // Объявление о том, что человек заговорил.
129
150
  //
130
151
  // Нужно, чтобы озвучка немедленно замолчала: слушать ответ и говорить
@@ -629,17 +650,32 @@ window.__ModuleLoader__.load({
629
650
 
630
651
  // --------------------------------------------------------------- slots
631
652
  function registerComposer(ctx) {
632
- // Горячая клавиша живёт всё время, пока плагин применён.
653
+ // Горячая клавиша живёт всё время, пока плагин применён, и меняется без
654
+ // перезапуска: карточка настроек кричит в окно, композер перечитывает.
633
655
  ctx.effect(() => {
634
656
  let dispose = () => {}
635
- fetch('/dsh-voice/status', { cache: 'no-store' })
636
- .then((res) => res.json())
637
- .then((data) => {
638
- const key = data && data.hotkey
639
- if (key) dispose = installHotkey(ctx, key, 'message')
640
- })
641
- .catch(() => { /* без подсказки хоста горячей клавиши просто не будет */ })
642
- return () => dispose()
657
+ let alive = true
658
+
659
+ const reload = () => {
660
+ fetch('/dsh-voice/status', { cache: 'no-store' })
661
+ .then((res) => res.json())
662
+ .then((data) => {
663
+ if (!alive) return
664
+ dispose()
665
+ dispose = () => {}
666
+ const key = data && data.hotkey
667
+ if (key) dispose = installHotkey(ctx, key, 'message')
668
+ })
669
+ .catch(() => { /* без подсказки хоста клавиши просто не будет */ })
670
+ }
671
+
672
+ reload()
673
+ window.addEventListener('dsh-voice:settings-saved', reload)
674
+ return () => {
675
+ alive = false
676
+ window.removeEventListener('dsh-voice:settings-saved', reload)
677
+ dispose()
678
+ }
643
679
  }, 'dsh-voice: горячая клавиша удержания')
644
680
 
645
681
  ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
@@ -801,6 +837,27 @@ window.__ModuleLoader__.load({
801
837
  const [saved, setSaved] = React.useState(false)
802
838
  const [err, setErr] = React.useState('')
803
839
 
840
+ // Выбор клавиши: не поле для ввода имени, а «нажмите ту, которую хотите».
841
+ // Имена вроде KeyR человек знать не обязан.
842
+ //
843
+ // Хуки объявлены здесь, выше всех возвратов: если объявить их ниже, при
844
+ // неготовых настройках карточка вернётся раньше, хуков окажется меньше,
845
+ // и React снимет весь раздел с ошибкой.
846
+ const [catching, setCatching] = React.useState(false)
847
+ React.useEffect(() => {
848
+ if (!catching) return undefined
849
+ const onKey = (event) => {
850
+ event.preventDefault()
851
+ event.stopPropagation()
852
+ if (event.key === 'Escape') { setCatching(false); return }
853
+ const chosen = keyFromEvent(event)
854
+ setDraft((d) => Object.assign({}, d || {}, { hotkey: chosen }))
855
+ setCatching(false)
856
+ }
857
+ document.addEventListener('keydown', onKey, true)
858
+ return () => document.removeEventListener('keydown', onKey, true)
859
+ }, [catching])
860
+
804
861
  React.useEffect(() => {
805
862
  let alive = true
806
863
  const render = () => { if (alive) setSnap(scope.getSnapshot()) }
@@ -877,6 +934,9 @@ window.__ModuleLoader__.load({
877
934
  vadSilenceMs: Number(draft.dictation && draft.dictation.vadSilenceMs) || 700,
878
935
  autoSendMs: Number(draft.message && draft.message.autoSendMs) || 4000,
879
936
  }
937
+ // Композер держит обработчик клавиши: пусть перечитает настройку,
938
+ // иначе новая клавиша заработает только после перезагрузки страницы.
939
+ try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* некому */ }
880
940
  setSaved(true); setTimeout(() => setSaved(false), 2000)
881
941
  } catch (e) { setErr(String(e && e.message ? e.message : e)) }
882
942
  }
@@ -894,6 +954,22 @@ window.__ModuleLoader__.load({
894
954
  .filter((k) => k && BUILTIN.indexOf(k) < 0),
895
955
  )
896
956
 
957
+ const hotkeyField = () => React.createElement('label', { className: 'dvs-field' },
958
+ 'Клавиша для голосового сообщения',
959
+ React.createElement('div', { className: 'dvs-row' },
960
+ React.createElement('button', {
961
+ type: 'button', className: 'dvs-save', disabled: !writable,
962
+ onClick: () => setCatching(true),
963
+ }, catching ? 'Нажмите клавишу…' : keyLabel(draft && draft.hotkey)),
964
+ React.createElement('button', {
965
+ type: 'button', className: 'dvs-mini', title: 'Убрать клавишу',
966
+ disabled: !writable, onClick: () => setDraft((d) => Object.assign({}, d || {}, { hotkey: '' })),
967
+ }, '×'),
968
+ ),
969
+ React.createElement('span', { className: 'dvs-sub' },
970
+ 'Держите её — идёт запись, отпустите — уйдёт агенту, Esc — отмена. '
971
+ + 'Можно любую: буква, F-клавиша или модификатор.'))
972
+
897
973
  const langField = (mode) => React.createElement('label', { className: 'dvs-field' }, 'Язык',
898
974
  React.createElement('select', {
899
975
  value: modeVal(mode, 'language', 'ru'), disabled: !writable,
@@ -948,6 +1024,7 @@ window.__ModuleLoader__.load({
948
1024
  ),
949
1025
  React.createElement('div', { className: 'dvs-block' },
950
1026
  React.createElement('div', { className: 'dvs-h' }, 'Общее'),
1027
+ hotkeyField(),
951
1028
  textField('whisperUrl', 'Локальный whisper: endpoint', 'POST /inference сервера whisper.cpp'),
952
1029
  textField('whisperBin', 'Локальный whisper: бинарь', 'используется при автозапуске'),
953
1030
  textField('whisperModel', 'Локальный whisper: модель', 'используется при автозапуске'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
5
5
  "license": "MIT",
6
6
  "type": "module",