@goodandready/dsh-voice 0.6.1 → 0.7.0
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 +86 -9
- package/package.json +1 -1
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|