@goodandready/dsh-voice 0.7.1 → 0.7.2
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 +230 -68
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -19,6 +19,157 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
|
|
20
20
|
const NS = 'dsh-voice'
|
|
21
21
|
|
|
22
|
+
// Строки интерфейса живут в реестре локалей: так их переводит отдельный
|
|
23
|
+
// пакет, не трогая код этого плагина. Английский — язык по умолчанию,
|
|
24
|
+
// на него же приходится откат, если перевода нет.
|
|
25
|
+
const en = {
|
|
26
|
+
'composerUnavailable': 'Composer unavailable',
|
|
27
|
+
'keySpace': 'Space',
|
|
28
|
+
'keyUnset': 'not set',
|
|
29
|
+
'recognitionError': 'recognition failed',
|
|
30
|
+
'micUnavailable': 'Microphone unavailable: needs HTTPS or localhost',
|
|
31
|
+
'noRecorder': 'MediaRecorder is not supported by this browser',
|
|
32
|
+
'browserFailed': 'Browser did not recognize: ',
|
|
33
|
+
'nothingHeard': 'Nothing was recognized',
|
|
34
|
+
'dictationBtn': 'Voice typing',
|
|
35
|
+
'messageBtn': 'Voice message — click or hold',
|
|
36
|
+
'dictationPill': 'Dictation — text is appended to the input',
|
|
37
|
+
'messagePill': 'Recording a voice message',
|
|
38
|
+
'cancel': 'Cancel',
|
|
39
|
+
'holdHint': 'Hold — release to send',
|
|
40
|
+
'listening': 'Listening in the browser…',
|
|
41
|
+
'stop': 'Stop',
|
|
42
|
+
'transcribing': 'Transcribing…',
|
|
43
|
+
'sendingIn': 'Sending to the agent in',
|
|
44
|
+
'secondsShort': ' s',
|
|
45
|
+
'keepPending': 'Do not send',
|
|
46
|
+
'hide': 'Hide',
|
|
47
|
+
'title': 'Voice',
|
|
48
|
+
'recordSlot': 'Voice recording',
|
|
49
|
+
'browserHint': 'the browser does the recognition, no key needed',
|
|
50
|
+
'openaiHint': 'whisper-1 · key OPENAI_API_KEY',
|
|
51
|
+
'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
|
|
52
|
+
'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY',
|
|
53
|
+
'fireworksHint': 'whisper-v3-turbo · key FIREWORKS_API_KEY',
|
|
54
|
+
'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY',
|
|
55
|
+
'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
|
|
56
|
+
'localHint': 'set when the server is started',
|
|
57
|
+
'up': 'Up',
|
|
58
|
+
'down': 'Down',
|
|
59
|
+
'remove': 'Remove',
|
|
60
|
+
'addProvider': 'Add provider',
|
|
61
|
+
'chainHint': 'Top to bottom is the order they are tried in',
|
|
62
|
+
'customName': 'name used in the chain',
|
|
63
|
+
'customModel': 'model',
|
|
64
|
+
'customKeyName': 'key name',
|
|
65
|
+
'customModelHint': 'model to request (empty means the built-in one)',
|
|
66
|
+
'addCustom': 'Add your own provider',
|
|
67
|
+
'loadingSettings': 'Loading settings…',
|
|
68
|
+
'notReady1': 'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
|
|
69
|
+
'notReady2': 'the section will appear on its own in a few seconds.',
|
|
70
|
+
'hotkey': 'Voice message key',
|
|
71
|
+
'pressKey': 'Press a key…',
|
|
72
|
+
'clearKey': 'Clear the key',
|
|
73
|
+
'hotkeyHint1': 'Hold it to record, release to send to the agent, Esc cancels. ',
|
|
74
|
+
'hotkeyHint2': 'Any key will do: a letter, an F-key or a modifier.',
|
|
75
|
+
'language': 'Language',
|
|
76
|
+
'dictationHint': 'Speech is cut at pauses and the text is appended to the input.',
|
|
77
|
+
'pauseMs': 'Pause that ends a phrase, ms',
|
|
78
|
+
'pauseHint': 'Lower means more frequent chunks and faster text, but a higher risk of cutting a word',
|
|
79
|
+
'messageTitle': 'Voice message',
|
|
80
|
+
'messageHint': 'One whole recording, sent to the agent once it is transcribed.',
|
|
81
|
+
'undoMs': 'Undo window, ms',
|
|
82
|
+
'undoHint': 'How long the automatic send can still be called off',
|
|
83
|
+
'customTitle': 'Your own providers',
|
|
84
|
+
'customHint': 'Any OpenAI-compatible API. The name becomes available in the chains above.',
|
|
85
|
+
'general': 'General',
|
|
86
|
+
'whisperEndpoint': 'Local whisper: endpoint',
|
|
87
|
+
'whisperEndpointHint': 'POST /inference of a whisper.cpp server',
|
|
88
|
+
'whisperBin': 'Local whisper: binary',
|
|
89
|
+
'whisperBinHint': 'used when autostart is on',
|
|
90
|
+
'whisperModel': 'Local whisper: model',
|
|
91
|
+
'whisperAutostart': 'Autostart the local whisper',
|
|
92
|
+
'save': 'Save',
|
|
93
|
+
'saved': 'Saved ✓',
|
|
94
|
+
'openrouterWarning': 'OpenRouter has no /audio/transcriptions \u2014 use the openai-chat-audio template there',
|
|
95
|
+
}
|
|
96
|
+
const ru = {
|
|
97
|
+
'composerUnavailable': 'Композер недоступен',
|
|
98
|
+
'keySpace': 'Пробел',
|
|
99
|
+
'keyUnset': 'не задана',
|
|
100
|
+
'recognitionError': 'ошибка распознавания',
|
|
101
|
+
'micUnavailable': 'Микрофон недоступен: нужен HTTPS или localhost',
|
|
102
|
+
'noRecorder': 'MediaRecorder не поддерживается браузером',
|
|
103
|
+
'browserFailed': 'Браузер не распознал: ',
|
|
104
|
+
'nothingHeard': 'Речь не распознана',
|
|
105
|
+
'dictationBtn': 'Голосовой набор',
|
|
106
|
+
'messageBtn': 'Голосовое сообщение — нажать или удерживать',
|
|
107
|
+
'dictationPill': 'Диктовка — текст дописывается в строку',
|
|
108
|
+
'messagePill': 'Запись голосового',
|
|
109
|
+
'cancel': 'Отмена',
|
|
110
|
+
'holdHint': 'Держите — отпустите, чтобы отправить',
|
|
111
|
+
'listening': 'Слушаю в браузере…',
|
|
112
|
+
'stop': 'Стоп',
|
|
113
|
+
'transcribing': 'Распознаю…',
|
|
114
|
+
'sendingIn': 'Отправляю агенту через',
|
|
115
|
+
'secondsShort': ' с',
|
|
116
|
+
'keepPending': 'Отменить отправку',
|
|
117
|
+
'hide': 'Скрыть',
|
|
118
|
+
'title': 'Голос',
|
|
119
|
+
'recordSlot': 'Запись голоса',
|
|
120
|
+
'browserHint': 'распознаёт сам браузер, ключ не нужен',
|
|
121
|
+
'openaiHint': 'whisper-1 · ключ OPENAI_API_KEY',
|
|
122
|
+
'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · ключ SILICONFLOW_API_KEY',
|
|
123
|
+
'deepinfraHint': 'openai/whisper-large-v3-turbo · ключ DEEPINFRA_API_KEY',
|
|
124
|
+
'fireworksHint': 'whisper-v3-turbo · ключ FIREWORKS_API_KEY',
|
|
125
|
+
'mistralHint': 'voxtral-mini-latest · ключ MISTRAL_API_KEY',
|
|
126
|
+
'openrouterHint': 'google/gemini-2.5-flash · ключ OPENROUTER_API_KEY',
|
|
127
|
+
'localHint': 'задаётся при запуске сервера',
|
|
128
|
+
'up': 'Выше',
|
|
129
|
+
'down': 'Ниже',
|
|
130
|
+
'remove': 'Убрать',
|
|
131
|
+
'addProvider': 'Добавить провайдера',
|
|
132
|
+
'chainHint': 'Порядок сверху вниз — порядок попыток',
|
|
133
|
+
'customName': 'имя для цепочки',
|
|
134
|
+
'customModel': 'модель',
|
|
135
|
+
'customKeyName': 'имя ключа',
|
|
136
|
+
'customModelHint': 'указание модели (пусто — встроенное)',
|
|
137
|
+
'addCustom': 'Добавить своего провайдера',
|
|
138
|
+
'loadingSettings': 'Загрузка настроек…',
|
|
139
|
+
'notReady1': 'Харнесс ещё не объявил настройки плагина. Если он только что перезапустился, ',
|
|
140
|
+
'notReady2': 'раздел появится сам через несколько секунд.',
|
|
141
|
+
'hotkey': 'Клавиша для голосового сообщения',
|
|
142
|
+
'pressKey': 'Нажмите клавишу…',
|
|
143
|
+
'clearKey': 'Убрать клавишу',
|
|
144
|
+
'hotkeyHint1': 'Держите её — идёт запись, отпустите — уйдёт агенту, Esc — отмена. ',
|
|
145
|
+
'hotkeyHint2': 'Можно любую: буква, F-клавиша или модификатор.',
|
|
146
|
+
'language': 'Язык',
|
|
147
|
+
'dictationHint': 'Речь режется по паузам, текст дописывается в строку ввода.',
|
|
148
|
+
'pauseMs': 'Пауза до конца фразы, мс',
|
|
149
|
+
'pauseHint': 'Меньше — чаще куски и быстрее текст, но выше риск обрезать слово',
|
|
150
|
+
'messageTitle': 'Голосовое сообщение',
|
|
151
|
+
'messageHint': 'Одна запись целиком, после распознавания уходит агенту.',
|
|
152
|
+
'undoMs': 'Окно отмены, мс',
|
|
153
|
+
'undoHint': 'Сколько времени можно отменить автоматическую отправку',
|
|
154
|
+
'customTitle': 'Свои провайдеры',
|
|
155
|
+
'customHint': 'Любой OpenAI-совместимый API. Имя становится доступным в цепочках выше.',
|
|
156
|
+
'general': 'Общее',
|
|
157
|
+
'whisperEndpoint': 'Локальный whisper: endpoint',
|
|
158
|
+
'whisperEndpointHint': 'POST /inference сервера whisper.cpp',
|
|
159
|
+
'whisperBin': 'Локальный whisper: бинарь',
|
|
160
|
+
'whisperBinHint': 'используется при автозапуске',
|
|
161
|
+
'whisperModel': 'Локальный whisper: модель',
|
|
162
|
+
'whisperAutostart': 'Автозапуск локального whisper',
|
|
163
|
+
'save': 'Сохранить',
|
|
164
|
+
'saved': 'Сохранено ✓',
|
|
165
|
+
'openrouterWarning': 'У OpenRouter нет /audio/transcriptions \u2014 там нужен шаблон openai-chat-audio',
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Строки нужны и вне компонентов — в обработчиках записи, в подписях
|
|
169
|
+
// слотов. Поэтому переводчик модульный, а не только через props.
|
|
170
|
+
let moduleT = (key) => key
|
|
171
|
+
const t = (key) => moduleT(key)
|
|
172
|
+
|
|
22
173
|
// ------------------------------------------------------------------ css
|
|
23
174
|
const CSS =
|
|
24
175
|
'.dvo-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;box-sizing:border-box}' +
|
|
@@ -118,7 +269,7 @@ window.__ModuleLoader__.load({
|
|
|
118
269
|
function appendDraft(text) {
|
|
119
270
|
const actions = voice.inputActions
|
|
120
271
|
if (!actions || typeof actions.setDraft !== 'function') {
|
|
121
|
-
voice.set({ phase: 'error', error: '
|
|
272
|
+
voice.set({ phase: 'error', error: t('composerUnavailable') })
|
|
122
273
|
return
|
|
123
274
|
}
|
|
124
275
|
const draft = voice.input && typeof voice.input.draft === 'string' ? voice.input.draft : ''
|
|
@@ -128,11 +279,11 @@ window.__ModuleLoader__.load({
|
|
|
128
279
|
// Как клавиша называется на человеческом языке.
|
|
129
280
|
const KEY_LABELS = {
|
|
130
281
|
Control: 'Ctrl', Alt: 'Alt', Shift: 'Shift', Meta: 'Win',
|
|
131
|
-
Space: '
|
|
282
|
+
Space: t('keySpace'), Escape: 'Esc',
|
|
132
283
|
}
|
|
133
284
|
|
|
134
285
|
function keyLabel(name) {
|
|
135
|
-
if (!name) return '
|
|
286
|
+
if (!name) return t('keyUnset')
|
|
136
287
|
if (KEY_LABELS[name]) return KEY_LABELS[name]
|
|
137
288
|
// Коды вида KeyR и Digit5 показываем без служебной приставки.
|
|
138
289
|
return String(name).replace(/^Key/, '').replace(/^Digit/, '')
|
|
@@ -202,7 +353,7 @@ window.__ModuleLoader__.load({
|
|
|
202
353
|
// no-speech и aborted — обычная жизнь, а не поломка.
|
|
203
354
|
const code = event && event.error
|
|
204
355
|
if (code === 'no-speech' || code === 'aborted') return
|
|
205
|
-
options.onError(code || '
|
|
356
|
+
options.onError(code || t('recognitionError'))
|
|
206
357
|
}
|
|
207
358
|
// Браузер обрывает распознавание сам: на паузах, по таймауту. Пока нас не
|
|
208
359
|
// остановили — поднимаем заново, иначе диктовка молча умрёт на первой паузе.
|
|
@@ -252,9 +403,9 @@ window.__ModuleLoader__.load({
|
|
|
252
403
|
|
|
253
404
|
async function openMic(mode) {
|
|
254
405
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
255
|
-
throw new Error('
|
|
406
|
+
throw new Error(t('micUnavailable'))
|
|
256
407
|
}
|
|
257
|
-
if (typeof MediaRecorder === 'undefined') throw new Error('
|
|
408
|
+
if (typeof MediaRecorder === 'undefined') throw new Error(t('noRecorder'))
|
|
258
409
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
259
410
|
audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
|
|
260
411
|
})
|
|
@@ -341,7 +492,7 @@ window.__ModuleLoader__.load({
|
|
|
341
492
|
// Браузер отказал уже после старта — честно говорим об этом, а не
|
|
342
493
|
// делаем вид, что слушаем.
|
|
343
494
|
voice.browser = null
|
|
344
|
-
voice.set({ phase: 'error', error: '
|
|
495
|
+
voice.set({ phase: 'error', error: t('browserFailed') + reason })
|
|
345
496
|
},
|
|
346
497
|
})
|
|
347
498
|
voice.browserFinals = finals
|
|
@@ -487,7 +638,7 @@ window.__ModuleLoader__.load({
|
|
|
487
638
|
voice.set({ phase: 'processing' })
|
|
488
639
|
try {
|
|
489
640
|
const text = await sendAudio(blob, rec.mime, mode)
|
|
490
|
-
if (!text) { voice.set({ phase: 'error', error: '
|
|
641
|
+
if (!text) { voice.set({ phase: 'error', error: t('nothingHeard') }); return }
|
|
491
642
|
appendDraft(text)
|
|
492
643
|
if (mode === 'message') {
|
|
493
644
|
voice.set({ phase: 'pending', pending: { text: text, leftMs: voice.settings.autoSendMs } })
|
|
@@ -524,11 +675,11 @@ window.__ModuleLoader__.load({
|
|
|
524
675
|
return React.createElement(React.Fragment, null,
|
|
525
676
|
React.createElement('button', {
|
|
526
677
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
527
|
-
title: err ? v.error : '
|
|
678
|
+
title: err ? v.error : t('dictationBtn'), onClick: startDictation,
|
|
528
679
|
}, micIcon()),
|
|
529
680
|
React.createElement('button', {
|
|
530
681
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
531
|
-
title: err ? v.error : '
|
|
682
|
+
title: err ? v.error : t('messageBtn'),
|
|
532
683
|
// Удержание: пишет, пока держишь; увёл курсор далеко вверх — отмена.
|
|
533
684
|
onPointerDown: (e) => { e.preventDefault(); beginHold('message') },
|
|
534
685
|
onPointerUp: () => endHold(false),
|
|
@@ -609,12 +760,12 @@ window.__ModuleLoader__.load({
|
|
|
609
760
|
|
|
610
761
|
if (v.phase === 'recording') {
|
|
611
762
|
const inBrowser = !!voice.browser
|
|
612
|
-
const hint = v.mode === 'dictation' ? '
|
|
763
|
+
const hint = v.mode === 'dictation' ? t('dictationPill') : t('messagePill')
|
|
613
764
|
// Живая подпись: что слышно прямо сейчас. Пока браузер не выдал
|
|
614
765
|
// окончательный кусок, текст черновой и меняется на глазах.
|
|
615
766
|
const caption = voice.caption || (inBrowser ? '' : null)
|
|
616
767
|
return React.createElement('div', { className: 'dvo-pill' },
|
|
617
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: '
|
|
768
|
+
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('cancel'), onClick: cancelCurrent }, xIcon()),
|
|
618
769
|
inBrowser
|
|
619
770
|
? null
|
|
620
771
|
: React.createElement('canvas', { className: 'dvo-wave', ref: canvasRef, width: 720, height: 40 }),
|
|
@@ -622,29 +773,29 @@ window.__ModuleLoader__.load({
|
|
|
622
773
|
caption
|
|
623
774
|
? caption
|
|
624
775
|
: (voice.holding
|
|
625
|
-
? '
|
|
626
|
-
: (inBrowser ? '
|
|
627
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: '
|
|
776
|
+
? t('holdHint')
|
|
777
|
+
: (inBrowser ? t('listening') : hint))),
|
|
778
|
+
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('stop'), onClick: stopCurrent }, stopIcon()),
|
|
628
779
|
)
|
|
629
780
|
}
|
|
630
781
|
|
|
631
782
|
if (v.phase === 'processing') {
|
|
632
783
|
return React.createElement('div', { className: 'dvo-pill' },
|
|
633
|
-
React.createElement('span', { className: 'dvo-status' }, spinIcon(), '
|
|
784
|
+
React.createElement('span', { className: 'dvo-status' }, spinIcon(), t('transcribing')))
|
|
634
785
|
}
|
|
635
786
|
|
|
636
787
|
if (v.phase === 'pending') {
|
|
637
788
|
const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
|
|
638
789
|
return React.createElement('div', { className: 'dvo-pill' },
|
|
639
|
-
React.createElement('span', { className: 'dvo-status' }, '
|
|
640
|
-
React.createElement('span', { className: 'dvo-count' }, left + '
|
|
641
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: '
|
|
790
|
+
React.createElement('span', { className: 'dvo-status' }, t('sendingIn')),
|
|
791
|
+
React.createElement('span', { className: 'dvo-count' }, left + t('secondsShort')),
|
|
792
|
+
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('keepPending'), onClick: keepPending }, xIcon()),
|
|
642
793
|
)
|
|
643
794
|
}
|
|
644
795
|
|
|
645
796
|
return React.createElement('div', { className: 'dvo-pill' },
|
|
646
797
|
React.createElement('span', { className: 'dvo-status dvo-err' }, warnIcon(), v.error),
|
|
647
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: '
|
|
798
|
+
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('hide'), onClick: () => voice.set({ phase: 'idle', error: '' }) }, xIcon()),
|
|
648
799
|
)
|
|
649
800
|
}
|
|
650
801
|
|
|
@@ -679,11 +830,11 @@ window.__ModuleLoader__.load({
|
|
|
679
830
|
}, 'dsh-voice: горячая клавиша удержания')
|
|
680
831
|
|
|
681
832
|
ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
|
|
682
|
-
{ name: 'conversation.input.right', id: '@goodandready/dsh-voice', order: 6, label: () => '
|
|
833
|
+
{ name: 'conversation.input.right', id: '@goodandready/dsh-voice', order: 6, label: () => t('title') },
|
|
683
834
|
(props) => React.createElement(VoiceButtons, { input: props.input, inputActions: props.inputActions }),
|
|
684
835
|
))
|
|
685
836
|
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
686
|
-
{ name: 'conversation.input.dock', id: 'dsh-voice-rec', order: 0, label: () => '
|
|
837
|
+
{ name: 'conversation.input.dock', id: 'dsh-voice-rec', order: 0, label: () => t('recordSlot') },
|
|
687
838
|
(props) => React.createElement(RecordPill, { input: props.input, inputActions: props.inputActions, ctx: ctx }),
|
|
688
839
|
))
|
|
689
840
|
}
|
|
@@ -696,15 +847,15 @@ window.__ModuleLoader__.load({
|
|
|
696
847
|
]
|
|
697
848
|
const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
698
849
|
const MODEL_HINT = {
|
|
699
|
-
browser: '
|
|
700
|
-
openai: '
|
|
701
|
-
siliconflow: '
|
|
702
|
-
deepinfra: '
|
|
703
|
-
fireworks: '
|
|
704
|
-
mistral: '
|
|
705
|
-
openrouter: '
|
|
850
|
+
browser: t('browserHint'),
|
|
851
|
+
openai: t('openaiHint'),
|
|
852
|
+
siliconflow: t('siliconflowHint'),
|
|
853
|
+
deepinfra: t('deepinfraHint'),
|
|
854
|
+
fireworks: t('fireworksHint'),
|
|
855
|
+
mistral: t('mistralHint'),
|
|
856
|
+
openrouter: t('openrouterHint'),
|
|
706
857
|
deepgram: 'nova-2', groq: 'whisper-large-v3-turbo',
|
|
707
|
-
hf: 'openai/whisper-large-v3', 'local-whisper': '
|
|
858
|
+
hf: 'openai/whisper-large-v3', 'local-whisper': t('localHint'),
|
|
708
859
|
}
|
|
709
860
|
const LANGS = ['auto', 'ru', 'en', 'uk', 'de']
|
|
710
861
|
|
|
@@ -763,13 +914,13 @@ window.__ModuleLoader__.load({
|
|
|
763
914
|
className: 'dvs-model', value: row.model || '', disabled: !props.writable,
|
|
764
915
|
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
765
916
|
}),
|
|
766
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: '
|
|
767
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: '
|
|
768
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: '
|
|
917
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '↑'),
|
|
918
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '↓'),
|
|
919
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '×'),
|
|
769
920
|
)),
|
|
770
921
|
React.createElement('div', { className: 'dvs-row' },
|
|
771
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: '
|
|
772
|
-
React.createElement('span', { className: 'dvs-sub' }, '
|
|
922
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
923
|
+
React.createElement('span', { className: 'dvs-sub' }, t('chainHint')),
|
|
773
924
|
),
|
|
774
925
|
)
|
|
775
926
|
}
|
|
@@ -790,13 +941,13 @@ window.__ModuleLoader__.load({
|
|
|
790
941
|
return React.createElement('div', { className: 'dvs-block' },
|
|
791
942
|
rows.map((row, i) => React.createElement('div', { className: 'dvs-card', key: i },
|
|
792
943
|
React.createElement('div', { className: 'dvs-row' },
|
|
793
|
-
field(i, row, 'key', '
|
|
944
|
+
field(i, row, 'key', t('customName')),
|
|
794
945
|
React.createElement('select', {
|
|
795
946
|
value: row.template || 'openai-transcriptions', disabled: !props.writable,
|
|
796
947
|
onChange: (e) => change(i, { template: e.target.value }),
|
|
797
948
|
}, TEMPLATES.map((t) => React.createElement('option', { key: t, value: t }, t))),
|
|
798
949
|
React.createElement('button', {
|
|
799
|
-
type: 'button', className: 'dvs-mini', title: '
|
|
950
|
+
type: 'button', className: 'dvs-mini', title: t('remove'),
|
|
800
951
|
disabled: !props.writable, onClick: () => remove(i),
|
|
801
952
|
}, '\u00d7'),
|
|
802
953
|
),
|
|
@@ -804,26 +955,27 @@ window.__ModuleLoader__.load({
|
|
|
804
955
|
field(i, row, 'baseURL', 'https://openrouter.ai/api/v1', true),
|
|
805
956
|
),
|
|
806
957
|
React.createElement('div', { className: 'dvs-row' },
|
|
807
|
-
field(i, row, 'model', '
|
|
808
|
-
field(i, row, 'keyEnv', '
|
|
958
|
+
field(i, row, 'model', t('customModel'), true),
|
|
959
|
+
field(i, row, 'keyEnv', t('customKeyName')),
|
|
809
960
|
),
|
|
810
961
|
row.template === 'openai-chat-audio'
|
|
811
962
|
? React.createElement('div', { className: 'dvs-row' },
|
|
812
|
-
field(i, row, 'prompt', '
|
|
963
|
+
field(i, row, 'prompt', t('customModelHint'), true))
|
|
813
964
|
: null,
|
|
814
965
|
)),
|
|
815
966
|
React.createElement('div', { className: 'dvs-row' },
|
|
816
967
|
React.createElement('button', {
|
|
817
|
-
type: 'button', className: 'dvs-mini', title: '
|
|
968
|
+
type: 'button', className: 'dvs-mini', title: t('addCustom'),
|
|
818
969
|
disabled: !props.writable, onClick: add,
|
|
819
970
|
}, '+'),
|
|
820
971
|
React.createElement('span', { className: 'dvs-sub' },
|
|
821
|
-
'
|
|
972
|
+
t('openrouterWarning')),
|
|
822
973
|
),
|
|
823
974
|
)
|
|
824
975
|
}
|
|
825
976
|
|
|
826
977
|
function VoiceSection(props) {
|
|
978
|
+
const t = (props && props.t) || moduleT
|
|
827
979
|
const ctx = props.ctx
|
|
828
980
|
// На странице, открытой не с localhost, ядро отключает настройки целиком:
|
|
829
981
|
// общий вид документа не читается, каждый раздел получает "unavailable",
|
|
@@ -911,9 +1063,9 @@ window.__ModuleLoader__.load({
|
|
|
911
1063
|
const waiting = !snap || snap.status === 'loading'
|
|
912
1064
|
return React.createElement('div', { className: 'dvs-wrap' },
|
|
913
1065
|
React.createElement('div', { className: 'dvs-wait' }, waiting
|
|
914
|
-
? '
|
|
915
|
-
: '
|
|
916
|
-
+ '
|
|
1066
|
+
? t('loadingSettings')
|
|
1067
|
+
: t('notReady1')
|
|
1068
|
+
+ t('notReady2')),
|
|
917
1069
|
)
|
|
918
1070
|
}
|
|
919
1071
|
|
|
@@ -955,22 +1107,22 @@ window.__ModuleLoader__.load({
|
|
|
955
1107
|
)
|
|
956
1108
|
|
|
957
1109
|
const hotkeyField = () => React.createElement('label', { className: 'dvs-field' },
|
|
958
|
-
'
|
|
1110
|
+
t('hotkey'),
|
|
959
1111
|
React.createElement('div', { className: 'dvs-row' },
|
|
960
1112
|
React.createElement('button', {
|
|
961
1113
|
type: 'button', className: 'dvs-save', disabled: !writable,
|
|
962
1114
|
onClick: () => setCatching(true),
|
|
963
|
-
}, catching ? '
|
|
1115
|
+
}, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
|
|
964
1116
|
React.createElement('button', {
|
|
965
|
-
type: 'button', className: 'dvs-mini', title: '
|
|
1117
|
+
type: 'button', className: 'dvs-mini', title: t('clearKey'),
|
|
966
1118
|
disabled: !writable, onClick: () => setDraft((d) => Object.assign({}, d || {}, { hotkey: '' })),
|
|
967
1119
|
}, '×'),
|
|
968
1120
|
),
|
|
969
1121
|
React.createElement('span', { className: 'dvs-sub' },
|
|
970
|
-
'
|
|
971
|
-
+ '
|
|
1122
|
+
t('hotkeyHint1')
|
|
1123
|
+
+ t('hotkeyHint2')))
|
|
972
1124
|
|
|
973
|
-
const langField = (mode) => React.createElement('label', { className: 'dvs-field' }, '
|
|
1125
|
+
const langField = (mode) => React.createElement('label', { className: 'dvs-field' }, t('language'),
|
|
974
1126
|
React.createElement('select', {
|
|
975
1127
|
value: modeVal(mode, 'language', 'ru'), disabled: !writable,
|
|
976
1128
|
onChange: (e) => setIn(mode, 'language', e.target.value),
|
|
@@ -992,51 +1144,51 @@ window.__ModuleLoader__.load({
|
|
|
992
1144
|
|
|
993
1145
|
return React.createElement('div', { className: 'dvs-wrap' },
|
|
994
1146
|
React.createElement('div', { className: 'dvs-block' },
|
|
995
|
-
React.createElement('div', { className: 'dvs-h' }, '
|
|
996
|
-
React.createElement('div', { className: 'dvs-sub' }, '
|
|
1147
|
+
React.createElement('div', { className: 'dvs-h' }, t('dictationBtn')),
|
|
1148
|
+
React.createElement('div', { className: 'dvs-sub' }, t('dictationHint')),
|
|
997
1149
|
React.createElement(ChainEditor, {
|
|
998
1150
|
value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
|
|
999
1151
|
options: chainOptions,
|
|
1000
1152
|
onChange: (v) => setIn('dictation', 'chain', v),
|
|
1001
1153
|
}),
|
|
1002
1154
|
langField('dictation'),
|
|
1003
|
-
numField('dictation', 'vadSilenceMs', '
|
|
1155
|
+
numField('dictation', 'vadSilenceMs', t('pauseMs'), t('pauseHint')),
|
|
1004
1156
|
),
|
|
1005
1157
|
React.createElement('div', { className: 'dvs-block' },
|
|
1006
|
-
React.createElement('div', { className: 'dvs-h' }, '
|
|
1007
|
-
React.createElement('div', { className: 'dvs-sub' }, '
|
|
1158
|
+
React.createElement('div', { className: 'dvs-h' }, t('messageTitle')),
|
|
1159
|
+
React.createElement('div', { className: 'dvs-sub' }, t('messageHint')),
|
|
1008
1160
|
React.createElement(ChainEditor, {
|
|
1009
1161
|
value: draft && draft.message ? draft.message.chain : [], writable: writable,
|
|
1010
1162
|
options: chainOptions,
|
|
1011
1163
|
onChange: (v) => setIn('message', 'chain', v),
|
|
1012
1164
|
}),
|
|
1013
1165
|
langField('message'),
|
|
1014
|
-
numField('message', 'autoSendMs', '
|
|
1166
|
+
numField('message', 'autoSendMs', t('undoMs'), t('undoHint')),
|
|
1015
1167
|
),
|
|
1016
1168
|
React.createElement('div', { className: 'dvs-block' },
|
|
1017
|
-
React.createElement('div', { className: 'dvs-h' }, '
|
|
1169
|
+
React.createElement('div', { className: 'dvs-h' }, t('customTitle')),
|
|
1018
1170
|
React.createElement('div', { className: 'dvs-sub' },
|
|
1019
|
-
'
|
|
1171
|
+
t('customHint')),
|
|
1020
1172
|
React.createElement(CustomEditor, {
|
|
1021
1173
|
value: draft && draft.customProviders ? draft.customProviders : [], writable: writable,
|
|
1022
1174
|
onChange: (v) => setTop('customProviders', v),
|
|
1023
1175
|
}),
|
|
1024
1176
|
),
|
|
1025
1177
|
React.createElement('div', { className: 'dvs-block' },
|
|
1026
|
-
React.createElement('div', { className: 'dvs-h' }, '
|
|
1178
|
+
React.createElement('div', { className: 'dvs-h' }, t('general')),
|
|
1027
1179
|
hotkeyField(),
|
|
1028
|
-
textField('whisperUrl', '
|
|
1029
|
-
textField('whisperBin', '
|
|
1030
|
-
textField('whisperModel', '
|
|
1031
|
-
React.createElement('label', { className: 'dvs-field' }, '
|
|
1180
|
+
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
1181
|
+
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
1182
|
+
textField('whisperModel', t('whisperModel'), t('whisperBinHint')),
|
|
1183
|
+
React.createElement('label', { className: 'dvs-field' }, t('whisperAutostart'),
|
|
1032
1184
|
React.createElement('input', {
|
|
1033
1185
|
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
1034
1186
|
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
1035
1187
|
})),
|
|
1036
1188
|
),
|
|
1037
1189
|
React.createElement('div', { className: 'dvs-row' },
|
|
1038
|
-
React.createElement('button', { type: 'button', className: 'dvs-save', disabled: !writable, onClick: save }, '
|
|
1039
|
-
saved ? React.createElement('span', { className: 'dvs-ok' }, '
|
|
1190
|
+
React.createElement('button', { type: 'button', className: 'dvs-save', disabled: !writable, onClick: save }, t('save')),
|
|
1191
|
+
saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
|
|
1040
1192
|
err ? React.createElement('span', { className: 'dvs-bad' }, err) : null,
|
|
1041
1193
|
),
|
|
1042
1194
|
)
|
|
@@ -1044,13 +1196,23 @@ window.__ModuleLoader__.load({
|
|
|
1044
1196
|
|
|
1045
1197
|
function registerSettings(ctx) {
|
|
1046
1198
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1047
|
-
{
|
|
1199
|
+
{
|
|
1200
|
+
name: 'settings.section',
|
|
1201
|
+
id: '@goodandready/dsh-voice',
|
|
1202
|
+
order: 30,
|
|
1203
|
+
// locale в записи слота — то, из-за чего компонент получает props.t.
|
|
1204
|
+
locale: NS,
|
|
1205
|
+
label: () => t('title'),
|
|
1206
|
+
inject: () => ({ ctx: ctx }),
|
|
1207
|
+
},
|
|
1048
1208
|
VoiceSection,
|
|
1049
1209
|
))
|
|
1050
1210
|
}
|
|
1051
1211
|
|
|
1052
|
-
exports.inject = ['timer', 'slots', 'settingsScope']
|
|
1212
|
+
exports.inject = ['timer', 'slots', 'settingsScope', 'locale']
|
|
1053
1213
|
exports.apply = function apply(ctx) {
|
|
1214
|
+
ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-voice: словари')
|
|
1215
|
+
moduleT = ctx.locale.bind(NS)
|
|
1054
1216
|
registerComposer(ctx)
|
|
1055
1217
|
registerSettings(ctx)
|
|
1056
1218
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|