@goodandready/dsh-voice 0.8.25 → 0.8.26

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/README.md CHANGED
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
28
+ <br><br>
29
+ 🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
package/README.ru.md CHANGED
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
28
+ <br><br>
29
+ 🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
package/README.zh.md CHANGED
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
28
+ <br><br>
29
+ 🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
package/lib/client.js CHANGED
@@ -132,7 +132,7 @@ window.__ModuleLoader__.load({
132
132
  'whisperAutostart': 'Autostart the local whisper',
133
133
  'save': 'Save',
134
134
  'saved': 'Saved ✓',
135
- 'openrouterWarning': 'OpenRouter has no /audio/transcriptions \u2014 use the openai-chat-audio template there',
135
+ 'openrouterWarning': 'OpenRouter has no /audio/transcriptions use the openai-chat-audio template there',
136
136
  'noiseSuppression': 'Hardware noise suppression',
137
137
  'noiseSuppressionHint': 'Enable browser noise suppression, echo cancellation, and auto gain control',
138
138
  'contextGlossary': 'Context glossary injection',
@@ -180,13 +180,178 @@ window.__ModuleLoader__.load({
180
180
  'testMic': 'Test Mic',
181
181
  'stopTest': 'Stop Test',
182
182
  'micLevel': 'Mic level',
183
+ 'noiseGate': 'Noise Gate (dB)',
184
+ 'noiseGateHint': 'Mute background fan hum and keyboard clicks below threshold. Off = disabled',
185
+ 'noiseGateOff': 'Off',
186
+ }
187
+
188
+ const zh = {
189
+ 'saveFailed': '部分字段未保存 —',
190
+ 'cardHint': '听写与语音消息:服务商、备用链、本地 whisper',
191
+ 'expand': '展开',
192
+ 'collapse': '收起',
193
+ 'composerUnavailable': '输入框不可用',
194
+ 'keySpace': '空格',
195
+ 'keyUnset': '未设置',
196
+ 'recognitionError': '识别失败',
197
+ 'micUnavailable': '麦克风不可用:需要 HTTPS 或 localhost',
198
+ 'noRecorder': '当前浏览器不支持 MediaRecorder',
199
+ 'browserFailed': '浏览器未能识别:',
200
+ 'nothingHeard': '未识别到任何内容',
201
+ 'dictationBtn': '语音输入',
202
+ 'messageBtn': '语音消息 — 点击或按住',
203
+ 'dictationPill': '听写 — 文本将追加至输入框',
204
+ 'messagePill': '正在录制语音消息',
205
+ 'cancel': '取消',
206
+ 'holdHint': '按住录音 — 松开即发送',
207
+ 'listening': '浏览器正在倾听…',
208
+ 'stop': '停止',
209
+ 'transcribing': '正在转写…',
210
+ 'sendingIn': '发送至智能体倒计时',
211
+ 'secondsShort': ' 秒',
212
+ 'keepPending': '不发送',
213
+ 'hide': '隐藏',
214
+ 'title': '语音',
215
+ 'recordSlot': '语音录制',
216
+ 'browserHint': '浏览器直接识别,无需密钥',
217
+ 'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY',
218
+ 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
219
+ 'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY',
220
+ 'fireworksHint': 'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
221
+ 'mistralHint': 'voxtral-mini-latest · 密钥 MISTRAL_API_KEY',
222
+ 'openrouterHint': 'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
223
+ 'localHint': '服务器启动时配置',
224
+ 'up': '上移',
225
+ 'down': '下移',
226
+ 'remove': '移除',
227
+ 'addProvider': '添加服务商',
228
+ 'chainHint': '自上而下为故障转移尝试顺序',
229
+ 'customName': '备用链中使用的名称',
230
+ 'customModel': '模型',
231
+ 'customKeyName': '密钥名称',
232
+ 'customModelHint': '请求的模型(留空使用内置默认值)',
233
+ 'addCustom': '添加自定义服务商',
234
+ 'loadingSettings': '正在加载设置…',
235
+ 'notReady1': 'Harness 尚未同步此插件的设置。如果刚重启,',
236
+ 'notReady2': '设置项将在数秒内自动呈现。',
237
+ 'hotkey': '语音消息快捷键',
238
+ 'pressKey': '请按下按键…',
239
+ 'clearKey': '清除按键',
240
+ 'hotkeyHint1': '按住录音,松开发送给智能体,按 Esc 取消。',
241
+ 'hotkeyHint2': '支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
242
+ 'language': '识别语言',
243
+ 'dictationHint': '语音按停顿自动切句,文字实时追加至输入框。',
244
+ 'pauseMs': '断句静音停顿时长(毫秒)',
245
+ 'pauseHint': '数值越小切分越频繁、出字越快,但可能切断未说完的词',
246
+ 'speaking': '正在说话…',
247
+ 'silence': '停顿…',
248
+ 'normalizeTranscript': '规范化转写文本',
249
+ 'undo': '撤销上次插入',
250
+ 'undone': '插入已撤销',
251
+ 'nothingToUndo': '没有可撤销的内容',
252
+ 'beep': '启停提示音',
253
+ 'localOnly': '仅使用本地 whisper',
254
+ 'localOnlyHint': '将双链限制为仅使用本地 whisper.cpp 服务:纯离线运行。',
255
+ 'sendDelay': '听写延迟追加 (ms)',
256
+ 'sendDelayHint': '追加听写文本前的缓冲等待时间,允许撤回。0 为关闭',
257
+ 'mic': '麦克风设备',
258
+ 'micDefault': '系统默认',
259
+ 'vocabulary': '自定义词汇表(每行一个词)',
260
+ 'polish': '模型润色转写文本',
261
+ 'polishHint': '插入前通过模型修正标点符号并消除语气口头禅',
262
+ 'stream': '连续流式听写',
263
+ 'streamHint': '说话期间按定时器切分短句,无需等待长时间停顿',
264
+ 'streamChunkMs': '流式切片时长 (ms)',
265
+ 'vadAdapt': '自适应静音阈值',
266
+ 'vadAdaptHint': '根据语速动态调整停顿静音阈值。0 为固定阈值',
267
+ 'wakeWord': '语音唤醒词',
268
+ 'wakeWordHint': '说话以此短语开头时自动触发录音。留空为关闭',
269
+ 'bargeIn': '语音插话打断 (Barge-in)',
270
+ 'polishSend': '发送前润色整篇草稿',
271
+ 'polishSendHint': '发送给智能体前对整个输入框草稿进行一次整体润色',
272
+ 'sessionCommands': '会话语音控制指令',
273
+ 'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
274
+ 'polishBaseUrl': '离线润色服务端点',
275
+ 'polishBaseUrlHint': '兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型',
276
+ 'polishModel': '离线润色模型',
277
+ 'polishKeyEnv': '离线润色密钥凭据名',
278
+ 'voiceCommandsLabel': '语音编辑指令("换行"、"段落")',
279
+ 'normalizeTranscriptHint': 'transcribe_audio:口语数字转阿拉伯数字,整理标点符号',
280
+ 'messageTitle': '语音消息',
281
+ 'messageHint': '录制单条完整语音,转写完成后自动发送给智能体。',
282
+ 'undoMs': '撤回窗口时长(毫秒)',
283
+ 'undoHint': '转写完成后多久以内可以随时取消发送',
284
+ 'customTitle': '自定义语音服务商',
285
+ 'customHint': '兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。',
286
+ 'general': '通用设置',
287
+ 'whisperEndpoint': '本地 whisper:端点地址',
288
+ 'whisperEndpointHint': 'whisper.cpp 服务器的 POST /inference 接口',
289
+ 'deepgramEndpoint': 'Deepgram:基础地址',
290
+ 'deepgramEndpointHint': 'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)',
291
+ 'whisperBin': '本地 whisper:可执行文件',
292
+ 'whisperBinHint': '开启自动启动时使用',
293
+ 'whisperModel': '本地 whisper:模型文件',
294
+ 'whisperModelHint': 'ggml 模型文件的绝对路径',
295
+ 'whisperAutostart': '自动启动本地 whisper',
296
+ 'save': '保存',
297
+ 'saved': '已保存 ✓',
298
+ 'openrouterWarning': 'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板',
299
+ 'noiseSuppression': '硬件级降噪',
300
+ 'noiseSuppressionHint': '启用浏览器硬件降噪、回声消除与自动增益控制',
301
+ 'contextGlossary': '上下文专业词汇注入',
302
+ 'contextGlossaryHint': '自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
303
+ 'providerDashboard': '服务商延迟与健康监控',
304
+ 'avgLatency': '平均延迟',
305
+ 'successRate': '成功率',
306
+ 'fast': '极速',
307
+ 'normal': '正常',
308
+ 'slow': '较慢',
309
+ 'error': '异常',
310
+ 'idle': '无调用记录',
311
+ 'play': '播放',
312
+ 'pause': '暂停',
313
+ 'listenBack': '试听回放',
314
+ 'lastRecording': '最近一条语音记录',
315
+ 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)',
316
+ 'sensevoiceEndpoint': 'SenseVoice:端点地址',
317
+ 'sensevoiceEndpointHint': 'sherpa-onnx 或兼容服务器的 POST 端点',
318
+ 'sensevoiceBin': 'SenseVoice:可执行文件',
319
+ 'sensevoiceBinHint': '开启自动启动时使用',
320
+ 'sensevoiceModel': 'SenseVoice:模型路径',
321
+ 'sensevoiceModelHint': 'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
322
+ 'sensevoiceAutostart': '自动启动 SenseVoice 服务',
323
+ 'visualizerStyle': '音频动态波形样式',
324
+ 'visualizerStyleHint': '录音胶囊面板内的动态音频波形动画效果',
325
+ 'visLiquidWave': '流体水波 (Liquid Wave)',
326
+ 'visDynamicOrb': '动态脉冲球体 (Dynamic Orb)',
327
+ 'visBars': '经典频条 (Classic Bars)',
328
+ 'visOff': '关闭',
329
+ 'statusTitle': '连接与引擎状态看板',
330
+ 'statusDesc': '当前语音后端与识别服务的实时网络与进程就绪状态。',
331
+ 'badgeHostOnline': '服务端在线 ({ms} ms)',
332
+ 'badgeHostOffline': '服务端不可达',
333
+ 'badgeWhisperActive': 'Whisper 就绪',
334
+ 'badgeWhisperInactive': 'Whisper 未就绪',
335
+ 'badgeSenseVoiceActive': 'SenseVoice 就绪',
336
+ 'badgeSenseVoiceInactive': 'SenseVoice 未就绪',
337
+ 'badgeProviders': '{count} 个服务商已就绪',
338
+ 'refreshStats': '刷新状态',
339
+ 'hardwareTitle': '硬件与本地引擎',
340
+ 'hardwareDesc': '麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。',
341
+ 'uiError': '语音界面错误:',
342
+ 'retry': '重试',
343
+ 'testMic': '测试麦克风',
344
+ 'stopTest': '停止测试',
345
+ 'micLevel': '麦克风电平',
346
+ 'noiseGate': '噪声门 (dB)',
347
+ 'noiseGateHint': '静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭',
348
+ 'noiseGateOff': '关闭',
183
349
  }
184
350
 
185
351
  // Strings are also needed outside components — in recording handlers and
186
352
  // slot labels — so the translator is module-level, not only via props.
187
353
  let moduleT = (key) => key
188
354
  const t = (key) => moduleT(key)
189
-
190
355
  // ------------------------------------------------------------------ css
191
356
  const CSS =
192
357
  '.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;user-select:none;-webkit-user-select:none;touch-action:manipulation}' +
@@ -219,7 +384,8 @@ window.__ModuleLoader__.load({
219
384
  '.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}' +
220
385
  '.dvo-mic-test{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}' +
221
386
  '.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}' +
222
- '.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}'
387
+ '.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}' +
388
+ '.dvo-meter-gate{position:absolute;top:0;bottom:0;width:2px;background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 4px var(--dsw-alias-state-warning-primary);z-index:2;pointer-events:none}'
223
389
  const cssId = 'dsh-voice/client.module.css'
224
390
  if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
225
391
  const tag = document.createElement('style')
@@ -228,7 +394,6 @@ window.__ModuleLoader__.load({
228
394
  tag.dataset.pluginCss = cssId
229
395
  document.head.appendChild(tag)
230
396
  }
231
-
232
397
  // ---------------------------------------------------------------- store
233
398
  const voice = {
234
399
  phase: 'idle', // idle | recording | processing | pending | error
@@ -581,9 +746,21 @@ window.__ModuleLoader__.load({
581
746
  if (rec.audioCtx.state === 'suspended') { rec.audioCtx.resume().catch(() => {}) }
582
747
  const src = rec.audioCtx.createMediaStreamSource(stream)
583
748
  rec.srcNode = src
749
+
750
+ let lastNode = src
751
+ try {
752
+ const filter = rec.audioCtx.createBiquadFilter()
753
+ filter.type = 'highpass'
754
+ filter.frequency.value = 80
755
+ filter.Q.value = 0.707
756
+ src.connect(filter)
757
+ lastNode = filter
758
+ rec.filterNode = filter
759
+ } catch (e) {}
760
+
584
761
  rec.analyser = rec.audioCtx.createAnalyser()
585
762
  rec.analyser.fftSize = 128
586
- src.connect(rec.analyser)
763
+ lastNode.connect(rec.analyser)
587
764
  }
588
765
  // No timeslice: only then each stop() yields a standalone webm file.
589
766
  recorder.start()
@@ -596,7 +773,14 @@ window.__ModuleLoader__.load({
596
773
  rec.analyser.getByteFrequencyData(data)
597
774
  let sum = 0
598
775
  for (let i = 0; i < data.length; i++) sum += data[i]
599
- return Math.min(1, (sum / data.length / 255) * 2.2)
776
+ const raw = Math.min(1, (sum / data.length / 255) * 2.2)
777
+
778
+ const gateDb = Number(voice.settings && voice.settings.noiseGateDb !== undefined ? voice.settings.noiseGateDb : -45)
779
+ if (gateDb > -90) {
780
+ const gateAmp = Math.pow(10, gateDb / 20) * 2.2
781
+ if (raw < gateAmp) return 0
782
+ }
783
+ return raw
600
784
  }
601
785
 
602
786
  // Cut the current phrase: stop the recorder, send the finished file and
@@ -1674,12 +1858,13 @@ window.__ModuleLoader__.load({
1674
1858
  sessionCommands: !!(value && value.message && value.message.sessionCommands),
1675
1859
  polishBaseUrl: String((value && value.polishBaseUrl) || ''),
1676
1860
  noiseSuppression: value.noiseSuppression !== false,
1861
+ noiseGateDb: Number((value && value.noiseGateDb) !== undefined ? value.noiseGateDb : -45),
1677
1862
  contextGlossary: value.contextGlossary !== false,
1678
1863
  visualizerStyle: (value && value.visualizerStyle) || 'liquid-wave',
1679
1864
  })
1680
1865
  }, [ready, value])
1681
1866
 
1682
- // Interactive microphone tester
1867
+ // Interactive microphone tester
1683
1868
  const [testingMic, setTestingMic] = React.useState(false)
1684
1869
  const [testLevel, setTestLevel] = React.useState(0)
1685
1870
  const testRef = React.useRef(null)
@@ -1807,6 +1992,7 @@ window.__ModuleLoader__.load({
1807
1992
  streamChunkMs: Number(draft.dictation && draft.dictation.streamChunkMs) || 1200,
1808
1993
  vadAdapt: Number(draft.dictation && draft.dictation.vadAdapt) || 0,
1809
1994
  noiseSuppression: draft.noiseSuppression !== false,
1995
+ noiseGateDb: Number(draft.noiseGateDb !== undefined ? draft.noiseGateDb : -45),
1810
1996
  contextGlossary: draft.contextGlossary !== false,
1811
1997
  visualizerStyle: draft.visualizerStyle || 'liquid-wave',
1812
1998
  })
@@ -1843,6 +2029,9 @@ window.__ModuleLoader__.load({
1843
2029
  React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
1844
2030
  )
1845
2031
 
2032
+ const gateDbVal = Number((draft && draft.noiseGateDb) !== undefined ? draft.noiseGateDb : (value && value.noiseGateDb) !== undefined ? value.noiseGateDb : -45)
2033
+ const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
2034
+
1846
2035
  const micField = () => React.createElement('div', { className: 'cb-field' },
1847
2036
  React.createElement('label', null, t('mic')),
1848
2037
  React.createElement('div', { className: 'cb-row' },
@@ -1866,9 +2055,25 @@ window.__ModuleLoader__.load({
1866
2055
  React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('micLevel') + ':'),
1867
2056
  React.createElement('div', { className: 'dvo-meter-bar' },
1868
2057
  React.createElement('div', { className: 'dvo-meter-fill', style: { width: `${testLevel}%` } }),
2058
+ gateThresholdPercent > 0 ? React.createElement('div', { className: 'dvo-meter-gate', style: { left: `${gateThresholdPercent}%` }, title: `${gateDbVal} dB` }) : null,
1869
2059
  ),
1870
2060
  React.createElement('span', { style: { fontSize: '11px', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right' } }, `${testLevel}%`),
1871
2061
  ) : null,
2062
+ React.createElement('div', { className: 'cb-row', style: { marginTop: '6px' } },
2063
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', minWidth: '120px' } }, t('noiseGate') + ':'),
2064
+ React.createElement('select', {
2065
+ value: String(gateDbVal), disabled: !writable,
2066
+ onChange: (e) => setTop('noiseGateDb', Number(e.target.value)),
2067
+ style: { flex: 1 },
2068
+ },
2069
+ React.createElement('option', { value: '-999' }, t('noiseGateOff')),
2070
+ React.createElement('option', { value: '-50' }, '-50 dB (Low / Gentle)'),
2071
+ React.createElement('option', { value: '-45' }, '-45 dB (Standard Default)'),
2072
+ React.createElement('option', { value: '-35' }, '-35 dB (Medium Room)'),
2073
+ React.createElement('option', { value: '-25' }, '-25 dB (High / Mechanical Clicks)'),
2074
+ ),
2075
+ ),
2076
+ React.createElement('span', { className: 'dvs-sub' }, t('noiseGateHint')),
1872
2077
  )
1873
2078
 
1874
2079
  const langField = (mode) => React.createElement('div', { className: 'cb-field' },
@@ -2180,7 +2385,6 @@ window.__ModuleLoader__.load({
2180
2385
  ),
2181
2386
  )
2182
2387
  }
2183
-
2184
2388
  // Card in Settings → Plugins → Plugin settings (#18)
2185
2389
  function PluginCard(props) {
2186
2390
  const [open, setOpen] = React.useState(false)
@@ -2221,9 +2425,9 @@ window.__ModuleLoader__.load({
2221
2425
 
2222
2426
  exports.inject = ['timer', 'slots', 'settingsScope', 'locale']
2223
2427
  exports.apply = function apply(ctx) {
2224
- // English is the source language. Other languages come from the
2225
- // separate translation plugin at runtime. Re-registering the same
2226
- // namespace+language throws, so tolerate a pre-registered dictionary.
2428
+ // English is the source and fallback language; Chinese is the built-in
2429
+ // localized user dictionary. Other languages (like Russian) come from
2430
+ // the separate translation plugin at runtime.
2227
2431
  const addLocale = (locale, dictionary) => {
2228
2432
  try {
2229
2433
  return ctx.locale.register(NS, locale, dictionary)
@@ -2232,7 +2436,7 @@ window.__ModuleLoader__.load({
2232
2436
  }
2233
2437
  }
2234
2438
  ctx.effect(() => {
2235
- const undo = [addLocale('en', en)]
2439
+ const undo = [addLocale('en', en), addLocale('zh', zh)]
2236
2440
  return () => { for (const off of undo) off() }
2237
2441
  }, 'dsh-voice: locale dictionaries')
2238
2442
  moduleT = ctx.locale.bind(NS)
package/lib/index.js CHANGED
@@ -126,6 +126,8 @@ export const Config = z.object({
126
126
  .description('Restrict both chains to local-whisper only: fully offline, no cloud providers.'),
127
127
  micDeviceId: z.string().default('')
128
128
  .description('Microphone device id for recording. Empty means the system default.'),
129
+ noiseGateDb: z.number().default(-45)
130
+ .description('Audio noise gate threshold in dB for client-side recording (-50 to -25 dB, or <= -90 to disable). Silence below this threshold cuts ambient background noise and prevents false VAD triggers.'),
129
131
  historyLimit: z.number().default(20)
130
132
  .description('How many recent dictation inserts to keep for undo in the browser. 0 disables history.'),
131
133
  vocabulary: z.array(z.string()).default([])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.8.25",
3
+ "version": "0.8.26",
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",
@@ -12,9 +12,11 @@
12
12
  "./cordis.patch.yml": "./cordis.patch.yml"
13
13
  },
14
14
  "files": [
15
- "lib/",
15
+ "lib/*.js",
16
16
  "cordis.patch.yml",
17
17
  "README.md",
18
+ "README.zh.md",
19
+ "README.ru.md",
18
20
  "LICENSE"
19
21
  ],
20
22
  "keywords": [
@@ -1,24 +0,0 @@
1
- // dsh-voice — client half (browser).
2
- //
3
- // Two buttons in conversation.input.right:
4
- // mic — dictation: speech is cut on pauses, each chunk is recognized and
5
- // appended to the composer; send stays with the user;
6
- // wave — voice message: one whole recording; after recognition the text is
7
- // sent to the agent when the cancel window expires.
8
- //
9
- // The recording pill lives in conversation.input.dock; settings are a card
10
- // in settings.plugin.item.
11
-
12
- window.__ModuleLoader__.load({
13
- id: '@goodandready/dsh-voice',
14
- factory: (require) => {
15
- var module = { exports: {} }
16
- var exports = module.exports
17
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
18
- let React = require('react')
19
-
20
- const NS = 'dsh-voice'
21
-
22
- // UI strings live in the locale registry so a separate package can
23
- // translate them without touching this plugin. English is the source
24
- // language, the default, and the fallback.
@@ -1,165 +0,0 @@
1
- const en = {
2
- 'saveFailed': 'Some fields were not saved —',
3
- 'cardHint': 'Dictation and voice messages: providers, chains, local whisper',
4
- 'expand': 'Expand',
5
- 'collapse': 'Collapse',
6
- 'composerUnavailable': 'Composer unavailable',
7
- 'keySpace': 'Space',
8
- 'keyUnset': 'not set',
9
- 'recognitionError': 'recognition failed',
10
- 'micUnavailable': 'Microphone unavailable: needs HTTPS or localhost',
11
- 'noRecorder': 'MediaRecorder is not supported by this browser',
12
- 'browserFailed': 'Browser did not recognize: ',
13
- 'nothingHeard': 'Nothing was recognized',
14
- 'dictationBtn': 'Voice typing',
15
- 'messageBtn': 'Voice message — click or hold',
16
- 'dictationPill': 'Dictation — text is appended to the input',
17
- 'messagePill': 'Recording a voice message',
18
- 'cancel': 'Cancel',
19
- 'holdHint': 'Hold — release to send',
20
- 'listening': 'Listening in the browser…',
21
- 'stop': 'Stop',
22
- 'transcribing': 'Transcribing…',
23
- 'sendingIn': 'Sending to the agent in',
24
- 'secondsShort': ' s',
25
- 'keepPending': 'Do not send',
26
- 'hide': 'Hide',
27
- 'title': 'Voice',
28
- 'recordSlot': 'Voice recording',
29
- 'browserHint': 'the browser does the recognition, no key needed',
30
- 'openaiHint': 'whisper-1 · key OPENAI_API_KEY',
31
- 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
32
- 'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY',
33
- 'fireworksHint': 'whisper-v3-turbo · key FIREWORKS_API_KEY',
34
- 'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY',
35
- 'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
36
- 'localHint': 'set when the server is started',
37
- 'up': 'Up',
38
- 'down': 'Down',
39
- 'remove': 'Remove',
40
- 'addProvider': 'Add provider',
41
- 'chainHint': 'Top to bottom is the order they are tried in',
42
- 'customName': 'name used in the chain',
43
- 'customModel': 'model',
44
- 'customKeyName': 'key name',
45
- 'customModelHint': 'model to request (empty means the built-in one)',
46
- 'addCustom': 'Add your own provider',
47
- 'loadingSettings': 'Loading settings…',
48
- 'notReady1': 'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
49
- 'notReady2': 'the section will appear on its own in a few seconds.',
50
- 'hotkey': 'Voice message key',
51
- 'pressKey': 'Press a key…',
52
- 'clearKey': 'Clear the key',
53
- 'hotkeyHint1': 'Hold it to record, release to send to the agent, Esc cancels. ',
54
- 'hotkeyHint2': 'Any key will do: a letter, an F-key or a modifier.',
55
- 'language': 'Language',
56
- 'dictationHint': 'Speech is cut at pauses and the text is appended to the input.',
57
- 'pauseMs': 'Pause that ends a phrase, ms',
58
- 'pauseHint': 'Lower means more frequent chunks and faster text, but a higher risk of cutting a word',
59
- 'speaking': 'You are speaking…',
60
- 'silence': 'Pause…',
61
- 'normalizeTranscript': 'Normalize file transcripts',
62
- 'undo': 'Undo last insert',
63
- 'undone': 'Insert undone',
64
- 'nothingToUndo': 'Nothing to undo',
65
- 'beep': 'Beep on start/stop',
66
- 'localOnly': 'Local whisper only',
67
- 'localOnlyHint': 'Restrict both chains to the local whisper.cpp server: fully offline.',
68
- 'sendDelay': 'Dictation send delay (ms)',
69
- 'sendDelayHint': 'Wait before appending a dictated phrase, with a cancel window. 0 = off',
70
- 'mic': 'Microphone',
71
- 'micDefault': 'System default',
72
- 'vocabulary': 'Custom vocabulary (one word per line)',
73
- 'polish': 'Polish transcript with model',
74
- 'polishHint': 'Fix punctuation and fillers via the harness model before inserting',
75
- 'stream': 'Continuous dictation',
76
- 'streamHint': 'Cut phrases by a timer while you speak instead of waiting for a long pause',
77
- 'streamChunkMs': 'Stream chunk (ms)',
78
- 'vadAdapt': 'Adaptive silence',
79
- 'vadAdaptHint': 'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
80
- 'wakeWord': 'Wake word',
81
- 'wakeWordHint': 'Browser recognition starts recording when speech begins with this phrase. Empty = off',
82
- 'bargeIn': 'Barge-in',
83
- 'polishSend': 'Polish whole draft before sending',
84
- 'polishSendHint': 'Run the composed draft through the model right before sending',
85
- 'sessionCommands': 'Voice session commands',
86
- 'sessionCommandsHint': '"send", "cancel", "stop", "continue" act on the session instead of becoming text',
87
- 'polishBaseUrl': 'Offline polish endpoint',
88
- 'polishBaseUrlHint': 'OpenAI-compatible /chat/completions base URL, e.g. a local Ollama. Empty = harness model',
89
- 'polishModel': 'Offline polish model',
90
- 'polishKeyEnv': 'Offline polish key credential',
91
- 'voiceCommandsLabel': 'Voice edit commands ("new line", "paragraph")',
92
- 'normalizeTranscriptHint': 'transcribe_audio: spoken numbers to digits, tidy punctuation',
93
- 'messageTitle': 'Voice message',
94
- 'messageHint': 'One whole recording, sent to the agent once it is transcribed.',
95
- 'undoMs': 'Undo window, ms',
96
- 'undoHint': 'How long the automatic send can still be called off',
97
- 'customTitle': 'Your own providers',
98
- 'customHint': 'Any OpenAI-compatible API. The name becomes available in the chains above.',
99
- 'general': 'General',
100
- 'whisperEndpoint': 'Local whisper: endpoint',
101
- 'whisperEndpointHint': 'POST /inference of a whisper.cpp server',
102
- 'deepgramEndpoint': 'Deepgram: base URL',
103
- 'deepgramEndpointHint': 'Base URL for Deepgram or self-hosted deployment (default https://api.deepgram.com)',
104
- 'whisperBin': 'Local whisper: binary',
105
- 'whisperBinHint': 'used when autostart is on',
106
- 'whisperModel': 'Local whisper: model',
107
- 'whisperModelHint': 'Absolute path to the ggml model file',
108
- 'whisperAutostart': 'Autostart the local whisper',
109
- 'save': 'Save',
110
- 'saved': 'Saved ✓',
111
- 'openrouterWarning': 'OpenRouter has no /audio/transcriptions \u2014 use the openai-chat-audio template there',
112
- 'noiseSuppression': 'Hardware noise suppression',
113
- 'noiseSuppressionHint': 'Enable browser noise suppression, echo cancellation, and auto gain control',
114
- 'contextGlossary': 'Context glossary injection',
115
- 'contextGlossaryHint': 'Auto-extract code identifiers and terms from composer to improve STT accuracy',
116
- 'providerDashboard': 'Provider Latency & Health',
117
- 'avgLatency': 'Avg latency',
118
- 'successRate': 'Success',
119
- 'fast': 'Fast',
120
- 'normal': 'Normal',
121
- 'slow': 'Slow',
122
- 'error': 'Error',
123
- 'idle': 'No calls',
124
- 'play': 'Play',
125
- 'pause': 'Pause',
126
- 'listenBack': 'Listen back',
127
- 'lastRecording': 'Last voice note',
128
- 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · ultra-fast local STT (~50ms)',
129
- 'sensevoiceEndpoint': 'SenseVoice: endpoint',
130
- 'sensevoiceEndpointHint': 'POST endpoint of sherpa-onnx or compatible server',
131
- 'sensevoiceBin': 'SenseVoice: binary',
132
- 'sensevoiceBinHint': 'used when autostart is on',
133
- 'sensevoiceModel': 'SenseVoice: model path',
134
- 'sensevoiceModelHint': 'Absolute path or identifier of the SenseVoice / sherpa-onnx model',
135
- 'sensevoiceAutostart': 'Autostart the SenseVoice server',
136
- 'visualizerStyle': 'Audio visualizer style',
137
- 'visualizerStyleHint': 'Waveform animation inside the recording pill',
138
- 'visLiquidWave': 'Liquid Wave',
139
- 'visDynamicOrb': 'Dynamic Orb',
140
- 'visBars': 'Classic Bars',
141
- 'visOff': 'Off',
142
- 'statusTitle': 'Connection & Engine Status',
143
- 'statusDesc': 'Current runtime connectivity to voice backends and recognition services.',
144
- 'badgeHostOnline': 'Host online ({ms} ms)',
145
- 'badgeHostOffline': 'Host unreachable',
146
- 'badgeWhisperActive': 'Whisper active',
147
- 'badgeWhisperInactive': 'Whisper offline',
148
- 'badgeSenseVoiceActive': 'SenseVoice active',
149
- 'badgeSenseVoiceInactive': 'SenseVoice offline',
150
- 'badgeProviders': '{count} providers ready',
151
- 'refreshStats': 'Refresh Stats',
152
- 'hardwareTitle': 'Hardware & Local Engines',
153
- 'hardwareDesc': 'Microphone device, local whisper.cpp and SenseVoice runtime settings.',
154
- 'uiError': 'Voice UI Error:',
155
- 'retry': 'Retry',
156
- 'testMic': 'Test Mic',
157
- 'stopTest': 'Stop Test',
158
- 'micLevel': 'Mic level',
159
- }
160
-
161
- // Strings are also needed outside components — in recording handlers and
162
- // slot labels — so the translator is module-level, not only via props.
163
- let moduleT = (key) => key
164
- const t = (key) => moduleT(key)
165
-