@goodandready/dsh-voice 0.6.0 → 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 +103 -9
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -125,6 +125,39 @@ 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
|
+
|
|
149
|
+
// Объявление о том, что человек заговорил.
|
|
150
|
+
//
|
|
151
|
+
// Нужно, чтобы озвучка немедленно замолчала: слушать ответ и говорить
|
|
152
|
+
// одновременно невозможно, а перекрикивать собственный плагин — глупо.
|
|
153
|
+
// Связи между плагинами нет: голос кричит в окно, кто хочет — слышит.
|
|
154
|
+
// Поэтому оба плагина работают и поодиночке.
|
|
155
|
+
function announceVoice(phase) {
|
|
156
|
+
try {
|
|
157
|
+
window.dispatchEvent(new CustomEvent('dsh-voice:speaking', { detail: { phase } }))
|
|
158
|
+
} catch (noEvents) { /* окна нет — значит и слушать некому */ }
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
// ------------------------------------------------- распознавание в браузере
|
|
129
162
|
//
|
|
130
163
|
// Отдельная нога, не похожая на все остальные: речь распознаёт сам браузер,
|
|
@@ -318,6 +351,9 @@ window.__ModuleLoader__.load({
|
|
|
318
351
|
|
|
319
352
|
function startRecording(mode) {
|
|
320
353
|
if (voice.phase !== 'idle' && voice.phase !== 'error') return
|
|
354
|
+
// Кричим до открытия микрофона, а не после: чем раньше замолчит
|
|
355
|
+
// озвучка, тем меньше её попадёт в запись.
|
|
356
|
+
announceVoice('start')
|
|
321
357
|
voice.set({ phase: 'recording', mode, error: '', levels: [], caption: '' })
|
|
322
358
|
modeChain(mode).then((info) => {
|
|
323
359
|
// Браузерная нога — только если её прямо поставили первой в цепочке.
|
|
@@ -399,6 +435,7 @@ window.__ModuleLoader__.load({
|
|
|
399
435
|
}
|
|
400
436
|
|
|
401
437
|
function cancelCurrent() {
|
|
438
|
+
announceVoice('end')
|
|
402
439
|
if (voice.browser) {
|
|
403
440
|
voice.browser.abort()
|
|
404
441
|
voice.browser = null
|
|
@@ -418,6 +455,7 @@ window.__ModuleLoader__.load({
|
|
|
418
455
|
// Останов по второму нажатию: диктовка досылает хвост, голосовое —
|
|
419
456
|
// отправляет всю запись и открывает окно отмены.
|
|
420
457
|
function stopCurrent() {
|
|
458
|
+
announceVoice('end')
|
|
421
459
|
if (voice.browser) {
|
|
422
460
|
const mode = voice.mode
|
|
423
461
|
const said = (voice.browserFinals || []).join(' ').trim()
|
|
@@ -612,17 +650,32 @@ window.__ModuleLoader__.load({
|
|
|
612
650
|
|
|
613
651
|
// --------------------------------------------------------------- slots
|
|
614
652
|
function registerComposer(ctx) {
|
|
615
|
-
// Горячая клавиша живёт всё время, пока плагин
|
|
653
|
+
// Горячая клавиша живёт всё время, пока плагин применён, и меняется без
|
|
654
|
+
// перезапуска: карточка настроек кричит в окно, композер перечитывает.
|
|
616
655
|
ctx.effect(() => {
|
|
617
656
|
let dispose = () => {}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
+
}
|
|
626
679
|
}, 'dsh-voice: горячая клавиша удержания')
|
|
627
680
|
|
|
628
681
|
ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
|
|
@@ -784,6 +837,27 @@ window.__ModuleLoader__.load({
|
|
|
784
837
|
const [saved, setSaved] = React.useState(false)
|
|
785
838
|
const [err, setErr] = React.useState('')
|
|
786
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
|
+
|
|
787
861
|
React.useEffect(() => {
|
|
788
862
|
let alive = true
|
|
789
863
|
const render = () => { if (alive) setSnap(scope.getSnapshot()) }
|
|
@@ -860,6 +934,9 @@ window.__ModuleLoader__.load({
|
|
|
860
934
|
vadSilenceMs: Number(draft.dictation && draft.dictation.vadSilenceMs) || 700,
|
|
861
935
|
autoSendMs: Number(draft.message && draft.message.autoSendMs) || 4000,
|
|
862
936
|
}
|
|
937
|
+
// Композер держит обработчик клавиши: пусть перечитает настройку,
|
|
938
|
+
// иначе новая клавиша заработает только после перезагрузки страницы.
|
|
939
|
+
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* некому */ }
|
|
863
940
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
864
941
|
} catch (e) { setErr(String(e && e.message ? e.message : e)) }
|
|
865
942
|
}
|
|
@@ -877,6 +954,22 @@ window.__ModuleLoader__.load({
|
|
|
877
954
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
878
955
|
)
|
|
879
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
|
+
|
|
880
973
|
const langField = (mode) => React.createElement('label', { className: 'dvs-field' }, 'Язык',
|
|
881
974
|
React.createElement('select', {
|
|
882
975
|
value: modeVal(mode, 'language', 'ru'), disabled: !writable,
|
|
@@ -931,6 +1024,7 @@ window.__ModuleLoader__.load({
|
|
|
931
1024
|
),
|
|
932
1025
|
React.createElement('div', { className: 'dvs-block' },
|
|
933
1026
|
React.createElement('div', { className: 'dvs-h' }, 'Общее'),
|
|
1027
|
+
hotkeyField(),
|
|
934
1028
|
textField('whisperUrl', 'Локальный whisper: endpoint', 'POST /inference сервера whisper.cpp'),
|
|
935
1029
|
textField('whisperBin', 'Локальный whisper: бинарь', 'используется при автозапуске'),
|
|
936
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",
|