@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.
Files changed (2) hide show
  1. package/lib/client.js +230 -68
  2. 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: 'Пробел', Escape: 'Esc',
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('Микрофон недоступен: нужен HTTPS или localhost')
406
+ throw new Error(t('micUnavailable'))
256
407
  }
257
- if (typeof MediaRecorder === 'undefined') throw new Error('MediaRecorder не поддерживается браузером')
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: 'Браузер не распознал: ' + reason })
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: 'Речь не распознана' }); return }
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 : 'Голосовой набор', onClick: startDictation,
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: 'Отмена', onClick: cancelCurrent }, xIcon()),
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 ? 'Слушаю в браузере…' : hint))),
627
- React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: 'Стоп', onClick: stopCurrent }, stopIcon()),
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: 'Отменить отправку', onClick: keepPending }, xIcon()),
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: 'Скрыть', onClick: () => voice.set({ phase: 'idle', error: '' }) }, xIcon()),
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: 'whisper-1 · ключ OPENAI_API_KEY',
701
- siliconflow: 'FunAudioLLM/SenseVoiceSmall · ключ SILICONFLOW_API_KEY',
702
- deepinfra: 'openai/whisper-large-v3-turbo · ключ DEEPINFRA_API_KEY',
703
- fireworks: 'whisper-v3-turbo · ключ FIREWORKS_API_KEY',
704
- mistral: 'voxtral-mini-latest · ключ MISTRAL_API_KEY',
705
- openrouter: 'google/gemini-2.5-flash · ключ OPENROUTER_API_KEY',
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: 'Выше', disabled: !props.writable, onClick: () => move(i, -1) }, '↑'),
767
- React.createElement('button', { type: 'button', className: 'dvs-mini', title: 'Ниже', disabled: !props.writable, onClick: () => move(i, 1) }, '↓'),
768
- React.createElement('button', { type: 'button', className: 'dvs-mini', title: 'Убрать', disabled: !props.writable, onClick: () => remove(i) }, '×'),
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: 'Добавить провайдера', disabled: !props.writable, onClick: add }, '+'),
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', 'модель', true),
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', 'указание модели (пусто — встроенное)', true))
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
- 'У OpenRouter нет /audio/transcriptions \u2014 там нужен шаблон openai-chat-audio'),
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 ? 'Нажмите клавишу…' : keyLabel(draft && draft.hotkey)),
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
- 'Держите её — идёт запись, отпустите — уйдёт агенту, Esc — отмена. '
971
- + 'Можно любую: буква, F-клавиша или модификатор.'))
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
- 'Любой OpenAI-совместимый API. Имя становится доступным в цепочках выше.'),
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', 'Локальный whisper: endpoint', 'POST /inference сервера whisper.cpp'),
1029
- textField('whisperBin', 'Локальный whisper: бинарь', 'используется при автозапуске'),
1030
- textField('whisperModel', 'Локальный whisper: модель', 'используется при автозапуске'),
1031
- React.createElement('label', { className: 'dvs-field' }, 'Автозапуск локального whisper',
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' }, 'Сохранено ✓') : null,
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
- { name: 'settings.section', id: '@goodandready/dsh-voice', order: 30, label: () => 'Голос', inject: () => ({ ctx: ctx }) },
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.1",
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",